commit 4043163fc491170c64ea2e50569b02d0f68c88e2 Author: Gaetan Delannay Date: Mon Jun 29 14:06:01 2009 +0200 Initial import diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..8b13789 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ + diff --git a/bin/__init__.py b/bin/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/bin/clean.py b/bin/clean.py new file mode 100755 index 0000000..e4d5255 --- /dev/null +++ b/bin/clean.py @@ -0,0 +1,33 @@ +# Imports ---------------------------------------------------------------------- +import os, os.path +from appy.shared import appyPath +from appy.shared.utils import FolderDeleter + +# ------------------------------------------------------------------------------ +class Cleaner: + exts = ('.pyc', '.class') + def run(self, verbose=True): + print 'Cleaning folder', appyPath, '...' + # Remove files with an extension listed in self.exts + for root, dirs, files in os.walk(appyPath): + for fileName in files: + ext = os.path.splitext(fileName)[1] + if (ext in Cleaner.exts) or ext.endswith('~'): + fileToRemove = os.path.join(root, fileName) + if verbose: + print 'Removing %s...' % fileToRemove + os.remove(fileToRemove) + # 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/publish.py b/bin/publish.py new file mode 100755 index 0000000..ea27e67 --- /dev/null +++ b/bin/publish.py @@ -0,0 +1,519 @@ +# Imports ---------------------------------------------------------------------- +import os, os.path, shutil, re, zipfile, sys, ftplib +from appy.shared import appyPath +from appy.shared.utils import FolderDeleter +from appy.bin.clean import Cleaner +from appy.gen.utils import produceNiceMessage + +# ------------------------------------------------------------------------------ +versionRex = re.compile('(\d+\.\d+\.\d+)') +eggInfo = '''import os, setuptools +setuptools.setup( + name = "appy", version = "%s", description = "The Appy framework", + long_description = "See http://appyframework.org", + author = "Gaetan Delannay", author_email = "gaetan.delannay AT gmail.com", + license = "GPL", keywords = "plone, pod, pdf, odt, document", + url = 'http://appyframework.org', + classifiers = ['Development Status :: 4 - Beta', "License :: OSI Approved"], + packages = setuptools.find_packages('src'), include_package_data = True, + package_dir = {'':'src'}, data_files = [('.', [])], + namespace_packages = ['appy'], zip_safe = False)''' + +def askLogin(): + print 'Login: ', + login = sys.stdin.readline().strip() + print 'Password: ', + passwd = sys.stdin.readline().strip() + return (login, passwd) + +def askQuestion(question, default='yes'): + '''Asks a question to the user (yes/no) and returns True if the user + answered "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 + +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 + 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) + +# ------------------------------------------------------------------------------ +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 (not elemName.startswith('.')) and \ + (not elemName.startswith('_')): + 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 = False + self.htmlFile.write( + '

%s

\n' % self.getCleanLine(line, True)) + self.htmlFile.write('\n\n\n') + self.txtFile.close() + self.htmlFile.close() + +class TodoConverter(Text2Html): + title = 'To do' + firstChar = 1 # Position of the first relevant char in each line + def retainLine(self, line): + return line.startswith('v') and len(line) > 2 + +class VersionsConverter(Text2Html): + title = 'Versions' + firstChar = 0 + svnUrl = 'http://svn.communesplone.org/svn/communesplone/appy' + setupToolsUrl = 'http://peak.telecommunity.com/DevCenter/setuptools' + def retainLine(self, line): + return len(line) > 1 + def getCleanLine(self, line, isTitle=False): + line = Text2Html.getCleanLine(self, line, isTitle) + if isTitle: + # This title represents a version of the appy framework. + version = versionRex.search(line).group(1) + if os.path.exists('%s/versions/appy.%s.zip' % ( + appyPath, version)): + line = '%s (download zip)' %( + line, version) + return line + def getProlog(self): + return '

Appy releases are available for download as zip files ' \ + 'below. Under Windows, unzip the file with a tool like ' \ + '7zip and copy the ' \ + '"appy" folder to <where_you_installed_python>\Lib\s' \ + 'ite-packages\. Under Linux, unzip the file by typing "unzip ' \ + 'appy-x.x.x.zip", copy the appy folder wherever you want (in ' \ + '/opt/appy for example) and make a symbolic link in your ' \ + 'Python lib folder (for example: "ln -s /opt/appy /usr/lib/' \ + 'python2.5/site-packages/appy").

' \ + ' ' \ + '

In order to check that everything works, launch a Python ' \ + 'shell and type "import appy". If you get the >>> '\ + 'prompt again without error it\'s ok. You may also want to ' \ + 'launch the automated pod test suite: go to the pod test ' \ + 'folder (in <pod folder>/test, where <pod ' \ + 'folder> may be something like /usr/lib/python2.5/' \ + 'site-packages/appy/pod or /usr/lib/python2.5/site-packages/' \ + 'appy-0.3.0-py2.5.egg/appy/pod) and type "sudo python ' \ + 'Tester.py".

' \ + '' % ( + self.setupToolsUrl, self.setupToolsUrl, + self.svnUrl, self.svnUrl) + +# ------------------------------------------------------------------------------ +class Publisher: + '''Publishes Appy on the web.''' + pageBody = re.compile('(.*)', re.S) + eggVersion = re.compile('version\s*=\s*".*?"') + pythonTargets = ('2.4', '2.5') + svnServer = 'http://svn.communesplone.org/svn/communesplone/appy' + + 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.versionLong = f.readline().strip() + # Long version includes version number & date + self.versionShort = versionRex.search(self.versionLong).group(1).strip() + # Short version includes version number only + f.close() + + def executeCommand(self, cmd): + '''Executes the system command p_cmd.''' + print 'Executing %s...' % cmd + os.system(cmd) + + def createCodeAndEggReleases(self): + '''Updates the subversion repository as needed (tags, branches) + and publishes the needed eggs on pypi.python.org.''' + # Update subversion repository + curdir = os.getcwd() + # Create a branch for this new version if the user wants it. + lastDotIndex = self.versionShort.rfind('.') + branchName = self.versionShort[:lastDotIndex] + cmd = 'svn cp -m "Branch for releases %s.x" %s/trunk %s/branches/%s' % ( + branchName, self.svnServer, self.svnServer, branchName) + if askQuestion('Create new branch? (%s)' % cmd, default='no'): + os.system(cmd) + # Create a tag for this version if the user wants it. + tagUrl = '%s/tags/%s' % (self.svnServer, self.versionShort) + cmd = 'svn cp -m "Tag for release %s" %s/trunk %s' % ( + self.versionShort, self.svnServer, tagUrl) + if askQuestion('Create new tag? (%s)' % cmd, default='no'): + os.system(cmd) + if askQuestion('Upload eggs on PyPI?', default='no'): + # Create egg structure + eggFolder = '%s/egg' % self.genFolder + os.mkdir(eggFolder) + f = file('%s/setup.py' % eggFolder, 'w') + f.write(eggInfo % self.versionShort) + f.close() + os.mkdir('%s/docs' % eggFolder) + os.mkdir('%s/src' % eggFolder) + os.mkdir('%s/src/appy' % eggFolder) + shutil.copy('%s/doc/version.txt' % appyPath, + '%s/docs/HISTORY.txt' % eggFolder) + shutil.copy('%s/doc/license.txt' % appyPath, + '%s/docs/LICENSE.txt' % eggFolder) + # Move appy sources within the egg + os.rename('%s/appy' % self.genFolder, '%s/src/appy' % eggFolder) + # Create eggs and publish them on pypi + os.chdir(eggFolder) + print 'Uploading appy%s source egg on PyPI...' % self.versionShort + #self.executeCommand('python setup.py sdist upload') + self.executeCommand('python setup.py sdist') + for pythonTarget in self.pythonTargets: + print 'Uploading appy%s binary egg for python%s...' % \ + (self.versionShort, pythonTarget) + #self.executeCommand('python%s setup.py bdist_egg upload' % \ + # pythonTarget) + self.executeCommand('python%s setup.py bdist_egg' % \ + pythonTarget) + os.chdir(curdir) + + 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 askQuestion('"%s" already exists. Replace it?' % \ + newZipRelease, default='yes'): + print 'Publication cancelled.' + 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() + # Copy the new zip release to the gen folder + shutil.copy(newZipRelease, '%s/versions' % self.genFolder) + 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 + i, j = pageContent.find(''), pageContent.find('') + pageTitle = 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('') + 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)[1:] + sectionNb = 0 + for url, title in links: + if title in ('gen', '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() + + def prepareGenFolder(self): + '''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) + shutil.copytree('%s/versions' % appyPath, '%s/versions' %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 ('gen', 'pod', 'shared'): + shutil.copytree('%s/%s' % (appyPath, aFolder), + '%s/%s' % (genSrcFolder, aFolder)) + # 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/todo.txt' % self.genFolder) + 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)) + # Generates the "to do" and "versions" pages, based on todo.txt and + # version.txt + TodoConverter('%s/doc/todo.txt' % appyPath, + '%s/todo.html' % self.genFolder).run() + VersionsConverter('%s/doc/version.txt' % appyPath, + '%s/version.html' % self.genFolder).run() + + def run(self): + Cleaner().run(verbose=False) + print 'Generating site in %s...' % self.genFolder + self.prepareGenFolder() + self.createDocToc() + self.applyTemplate() + self.createZipRelease() + self.createCodeAndEggReleases() + if askQuestion('Do you want to publish the site on ' \ + 'appyframework.org?', default='no'): + AppySite().publish() + if askQuestion('Delete locally generated site ?', default='no'): + FolderDeleter.delete(self.genFolder) + +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + Publisher().run() +# ------------------------------------------------------------------------------ diff --git a/bin/runOpenOffice.sh b/bin/runOpenOffice.sh new file mode 100755 index 0000000..29db906 --- /dev/null +++ b/bin/runOpenOffice.sh @@ -0,0 +1,4 @@ +#!/bin/sh +/opt/openoffice.org3/program/soffice "-accept=socket,host=localhost,port=2002;urp;" +echo "Press ..." +read R diff --git a/bin/zip.py b/bin/zip.py new file mode 100755 index 0000000..f55cdf5 --- /dev/null +++ b/bin/zip.py @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------------------ +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/doc/appy.css b/doc/appy.css new file mode 100755 index 0000000..be462c3 --- /dev/null +++ b/doc/appy.css @@ -0,0 +1,150 @@ +/* HTML element types */ + +a:link { COLOR: #888888; text-decoration: none; } +a:visited{ COLOR: #888888; text-decoration: none; } +a:active { COLOR: #888888; text-decoration: none; } +a:hover { COLOR: #888888; text-decoration: underline; } + +input { + font-family: Verdana; + font-size: 9pt; + font-weight:normal; +} + +textarea { + font-family: Verdana; + font-size: 9pt; + width: auto; +} + +select { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; + font-weight:normal; + width: auto; +} + +table { + border-collapse: separate; + border-spacing: 0 0; +} + +tr { + vertical-align: top; +} + +td { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; +} + +th { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; + font-style: italic; + font-weight: bold; + padding: 0 1em 0.1em 0; + border-bottom: 1px solid black; + text-align: left; +} + +p { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; +} + +img { + border-width: 0 +} + +div { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; +} + +body { + margin:10; + padding-left:10px; + padding-right:10px; +} + +h1 { + font-family: Verdana, helvetica, sans-serif; + font-size: 10pt; + font-weight: bold; +} + +h2 { + font-family: Verdana, helvetica, sans-serif; + font-size: 10pt; + font-style: italic; + font-weight: normal; +} + +ul { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; + list-style-type:circle; +} + +li { + font-family: Verdana, helvetica, sans-serif; + font-size: 9pt; +} + +/* Classes */ + +.borders { + border-bottom-style: inset; + border-bottom-width: 1px; + padding-top: 8px; + padding-bottom: 8px; +} + +.code { + font-family: Courier new, Nimbus Mono L, sans-serif; + font-size: 10pt; +} + +.appyTable { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.siteTitle { + font-size: 12pt; +} + +.headerStrip { + text-align: right; + font-size: 10pt; + font-style: italic; + background-color: #f1f1ed; + padding-right: 1em; +} + +.bottomStrip { + font-size: 9pt; + font-weight:bold; + background-color: #F8F8F8; + padding-bottom: 0.1em; + padding-top: 0.1em; +} + +.doc { + padding-left: 2em; + padding-top: 0.5em; + font-size: 9pt; + font-style: italic; +} + +.tabs { + padding-left: 1em; + font-size: 9pt; +} + +.footer { + text-align: right; + font-size: 9pt; + font-style: italic; +} diff --git a/doc/artwork.odg b/doc/artwork.odg new file mode 100755 index 0000000..4bd1899 Binary files /dev/null and b/doc/artwork.odg differ diff --git a/doc/docToc.html b/doc/docToc.html new file mode 100755 index 0000000..653e037 --- /dev/null +++ b/doc/docToc.html @@ -0,0 +1,9 @@ + +
+ <b>Appy documentation</b> - Table of contents + +
+ + {{ doc }} + + diff --git a/doc/gen.html b/doc/gen.html new file mode 100755 index 0000000..85eceed --- /dev/null +++ b/doc/gen.html @@ -0,0 +1,166 @@ + + + An introduction to <b>gen</b> + + + +

What is gen ?

+ +

gen is a code generator that allows you write web applications without having to face and understand the plumbery of a given web framework. gen protects you. Concentrate on functionalities that need to be implemented: gen will fight for you, by itself, against the low-level twisted machineries and will let you evolve in your pure, elegant and minimalistic Python world.

+ +

OK, but concretely, on what technologies is gen built upon?

+ +

gen generates code that will run on Plone 2.5. Soon, the code will also be compatible with the latest Plone version. In my point of view, Plone 2.5 has reached the maximum level of complexity a Python developer may tolerate. Plone 2.5 heavily relies on Zope 2. While Plone 3 still runs on Zope 2, it has become heavily based on Zope 3 through the use of the "Five" product (=Zope 2 + Zope 3), that allows to use Zope 3 functionalities within Zope 2. Some people began to be angry about how complex certain tasks (like creating a portlet) became with Plone 3 (consult this, for instance.) In order to fight against this trend, we decided to create a new code generator (a kind of concurrent to ArchGenXML so) that already makes sense for Plone 2.5 and will be more and more relevant for the current and future Plone versions, as the Plone community took the debatable decision to move to Zope 3.

+ +

Before starting, let's get bored by some (counter-)principles that underlie gen

+ +

If you have strict deadlines, skip this.

+
    +
  • The code-centric approach. Other approaches for generating code like the idea of starting from some abstract vision like a (graphical) model (boxes, arrows, things like that); from it, tools generate code skeletons that need to be completed in a subsequent "development" phase. Such "transformational" approaches (I vaguely know some buzzwords for it: MDA, MDD I think) may even count more than two phases and manipulate different models at different abstraction levels before producing code (I am not joking: I've learned that at the university). Such approaches spread the information in several places, because every model or representation has limited expressivity. It produces redundancy and eventually leads to maintenance problems. It violates the DRY principle, which is closely related to our null-IT principle. On the contrary, gen knows only about code. The "model" you write is a simple Python file or package. Being written in a high-level programming language, it does not constrain your expressivity in any way. More important: this code *is* the running code, and thus the only place where you describe your software. Simply, "wrappers" are added to it in order to plug him into the low-level Plone and Zope machinery. A gen-powered Python program acts like a comedian manipulating a tuned marionette: simple moves from the comedian produce complex, cascading and funny behaviours at the puppet level. Moreover, a code-based approach has the following benefits:
  • +
      +
    • when using a graphical model, you are emprisoned into a poorly expressive notation. Let's take an example. If you express a state machine with a UML state diagram, how will you be able to define another state machine based on the previous one? If you express it with code, it is as simple as using class inheritance. Typically, with appy.gen, you may achieve nice results like workflow inheritance; it is completely impossible with ArchGenXML. Of course, using graphical models for communicating or focusing on some parts of your program may be very valuable; this is why we foresee to implement model generation (class diagrams, statecharts, etc) from a appy.gen application. This is our way to see how to use graphical models: as views generated from the code. We don't believe at all in approaches like generating code from models or round-trip engineering.
    • +
    • when using some centralized code repository like subversion, a UML model, for example, is typically stored as a binary file. So it is impossible to work concurrently on various parts of it; nor is it possible to view who has changed what in it, etc;
    • +
    • factoring similar groups of attributes or doing other intelligent treatment on a graphical model is not possible;
    • +
    • there is no need to write and maintain a model parser (like XMI);
    • +
    • yes, you can use cut-and-paste with any text editor! If you want to do similar things with a model, you will probably need to buy some expensive UML editor;
    • +
    +
  • Violating the model-view-controller pattern (and a lot of other patterns, too). Design patterns are elegant low-level constructs used to overcome the limitations of programming languages (ie statically-typed languages like Java) or frameworks. Using them implies adding classes just for making the plumbery work; it augments code complexity and, again, spreads information at several places. Separating code describing data from code presenting it produces the same problem. appy.gen takes the approach of grouping everything at the same place. For example, information that dictates how a given field will be displayed is part of the field definition. +
  • +
  • All-in-one objects. As a consequence of the two previous bullets, gen objects (which are Plain Old Python Objects: you do not even have to inherit from a base gen class!) are self-sufficient. If you want to understand some (group of) functionality within a well-designed gen Python program, you will not loose yourself walking through an intricate web of Python classes and XML definition files. +
  • +
  • Building complex and shared web applications. While Plone and related tools are mainly targeted at building CMS websites (Content Management Systems = websites whose content may be edited by normal human beings), they provide poor support for those who want to build complex and shared web applications. +
  • +
      +
    • By "complex", I mean real "business applications" (or "information systems", like accounting systems, HR systems or online booking systems) whose interfaces are web interfaces; a "CMS" website being a particular case whose main purpose is to provide information (the website of a city or a company, etc) with some limited degree of interaction with the user. Web business applications are characterized by (1) a rich conceptual model featuring a complex web of inter-related classes. Moreover, such applications also need (2) complex querying and reporting facilities, be it through-the-web or within generated documents. Standard Plone provides little support for both (1) and (2). For instance, the basic reference fields and widgets are embryonic (ie no built-in notion of order among references): creating complex inter-related objects is tedious. Standard Plone does not provide any standard document-generation solution and provides limited through-the-web querying facilities. appy.gen comes with a brand new ordered Reference field/widget that automates a lot of repetitive programming tasks and yields unexpected recurrent visualization possibilities. Through its integration with appy.pod, any gen-powered app is by default ready for document generation. gen also generates paramerized through-the-web views and queries. +
    • +
    • Currently, free software is widespread within the "IT infrastructure" (operating systems, networking components, web servers, application servers...) and contaminates more and more general-purpose software applications, like word processors, spreadsheets or multimedia players. For the most of it, the free movement currently reaches domains where requirements are either perfectly known by the developers themselves, deduced from observing proprietary software or part of some general cultural background. In order to raise freedom at the higher levels of business and innovation, we need new mechanisms allowing to tackle (business-)specific requirements, while maintaining the possibility to spread and share software from one organization to the other. appy.gen was built to cope with this new scale of challenges and proposes a set of built-in constructs for creating generic business kernels implementing common requirements and allowing to tailor it to the specific needs of a given organization. This work results from experience gained from a deep involvement in the PloneGov community, an international group of public administrations developing and sharing free software for their own needs. It also benefits from a close collaboration with several research initiatives (the PRECISE research center and the MoVES project) exploring Software Product Lines Engineering and inventing new ways to manage variability among software systems. +
    • +
    +
+ +

Getting started with gen

+ +

Read only this if you want to run a "Hello world" gen application on your machine. This section is dedicated to Joel, a tremedous application and framework tester that ignores the essence of his gift.

+ +

Note for Windows users

+ +

I've never installed or tested gen on Windows. Feedback is welcome!

+ +

First step: download and install Plone

+ +

You need to get Plone 2.5.5 installed on your system. The simplest way to do it is run the unified installer available here. If a message warns you that the release is not supported anymore, please send a mail to plone.org asking them why the official Plone site still uses a dangerous unsupported Plone version (at this time of writing, December 8th 2008).

+ +

Let's suppose you have Plone, Zope and Python now installed in /opt/Plone-2.5.5. The unifier installed created a ZEO cluster in /opt/Plone-2.5.5/zeocluster. A ZEO cluster is a kind of load balancer that runs in front of several Zope servers, also called Zope "instances" (and also called "ZEO clients" in this context). For developement, a ZEO cluster is not needed; it is preferable to start from a fresh Zope server (from now on I will use the term "Zope instance") that you will install somewhere in your home folder.

+ +

Create a new Zope instance by typing /opt/Plone-2.5.5/Python-2.4.4/bin/python /opt/Plone-2.5.5/bin/mkzopeinstance.py. This is important to run this Python script with the Python interpreter that will run your Zope instance, ie the one installed in /opt/Plone-2.5.5/Python2.4.4. I will suppose you have created it in [myZopeInstance]. You will use the username and password asked by the script for connecting to the Zope instance as administrator. A Zope instance has the following structure: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
bincontains the script for starting/stopping the Zope instance. Go to this folder and type ./zopectl fg. It will start Zope in debug mode, in 'foreground' in your shell, by default on port 8080. If you want to start and stop the instance normally, (without being tied to your shell) use ./zopectl start and ./zopectl stop.
etccontains zope.conf, the configuration file of your Zope instance. Every time you modify this file you will need to restart the server. It is well documented; edit this if, for example, you need to change the port on which the instance listens.
ExtensionsI don't care about this.
importI don't care about this.
lib, and more specifically lib/python, is the folder where additional Python packages used by your Zope instance will lie. As gen-applications are standard Python packages, this is the typical place where you will store them.
logcontains the log files of your instance: Z2.log is the web server log (every HTTP request dumps a line into it); event.log contains more relevant, "application-level" information (debug info, warnings, infos, etc). When running the instance in foreground, events dumped in event.log will also show up in your shell.
Productsis the folder where Zope "add-ons" will reside. Although a Zope "product" is also a Python package, it contains additional ugly code that transforms it into a real Zope add-on. For every gen-application that you will create or sim-link in lib/python, gen will create and maintain for you the corresponding Zope Product in Products. There is a 1-1 relationship between a gen-application and the generated Zope product.
varis where Zope stores its database (DB) and related files. Unlike other DB technologies, there is no separate process that controls the DB and that needs to be called by the web/application server for reading or writing into the DB. Here, the Zope instance itself is directly connected to the DB, which is a single file named Data.fs. The DB technology used by Zope is named ZODB (which stands for Zope Object DataBase).
+

But what about Plone, huh? Plone is simply a bunch of Zope Products that add plenty of nice predefined functionalities and pages to Zope which is a bit arid as-is. So let's take a look at the Products folder of your Zope instance. It is empty! For transforming it into a Plone-ready Zope instance, simply copy the Plone products from /opt/Plone-2.5.5/zeocluster/Products. Go to [myZopeInstance]/Products and type cp -R /opt/Plone-2.5.5/zeocluster/Products/* .

+ +

Your Zope instance is now ready-to-use. Start it and go to http://localhost:8080/manage with a web browser. Type the username and password you have entered while creating the instance and you will arrive in the ZMI (the Zope Management Interface). You may see the ZMI as a database viewer that shows you the content of Data.fs. You may also see it as an admin interface allowing you to trigger administrative functions. Because the ZODB is an object-oriented database, both visions are merged: functions and data are bundled into objects that are organized into a hierarchy (and more). The figure below shows the ZMI.

+ +

+ +

You will notice that an empty Zope database still contains some predefined objects and folders: a "control panel", a folder (acl_users) where users are stored (it contains currently only one user), a web page "index_html" that is shown if you go to http://localhost:8080, etc. Everything is an object there, even the main error_log which is a nice way to browse through-the-web the log entries also dumped on the file system in [myZopeInstance]/log/event.log.

+ +

A Plone site is simply one more object to create within the Zope hierarchy of objects. In the ZMI, select, in the listbox at the right-top corner, "Plone site" and click on the "Add" button. Choose "Appy" as Id and "Hello world" as Title and click on "Add Plone Site". You have now created a Plone site! If you want to access a given object through-the-web, you simply have to type the "path" of the object within the Zope hierarchy. So now, go to http://localhost:8080/Appy and you will see the main page of your Plone site, like shown below.

+ +

+ +

Second step: download and install Appy

+ +

The underlying technologies required by gen are now up-and-running. Let's install gen. gen is a simple Python package available as a zip file (and more) here. For example, you may unzip it in [myZopeInstance]/lib/python or in /opt/Plone-2.5.5/Python-2.4.4/lib/python2.4/site-packages. If you put it in the latter place, it will be enabled for all future Zope instances you may use or create (including the ZEO cluster in /opt/Plone-2.5.5/zeocluster).

+ +

Third step: develop the "Hello world" application

+ +

We are ready to create our first gen-application. Imagine we are a software company that creates components using Zope 3. The company is a start-up, but after a few months, it has already developed hundreds of Zope 3 components (indeed, every single web page is considered a component). The company decides to create a simple tool for managing those small pieces of code. Let's create a simple gen-application for this, in a file named ZopeComponent.py:

+ +

+ 01 from appy.gen import *
+ 02 
+ 03 class ZopeComponent:
+ 04   root = True
+ 05   description = String()
+ 06   technicalDescription = String(format=String.XHTML)
+ 07   status = String(validator=['underDevelopement', 'stillSomeWorkToPerform',
+ 08     'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient'])
+ 09   funeralDate = Date()
+

+ +

9 lines of code (with a blank one; please note that I'm obsessed by lines that do not span more than 80 characters): yes, you are done. Every Zope component will be described by this bunch of attributes. Specifying the class as root makes it a class of special importance to gen; it will be treated with some honors. We will see how in a moment.

+ +

Fourth step: generate the Zope/Plone product

+ +

Please put ZopeComponent.py in [myZopeInstance]/lib/python, cd into this folder and type the following line.

+ +

python [whereYouInstalledAppy]/appy/gen/generator.py ZopeComponent.py plone25 ../../Products

+ +

You will get an output similar to this:

+ +

+ Generating product in /home/gde/ZopeInstance2/Products...
+ Generating class ZopeComponent.ZopeComponent...
+ Done.
+

+ +

If you are familiar with Zope/Plone products, you may take a look to the one gen has generated in [myZopeInstance]/Products/ZopeComponent.

+ +

Fifth step: admire the result

+ +

Restart your Zope instance, go to http://localhost:8080/Appy, log in with your Zope admin password and click on the link "site setup" in the top-right corner: you arrive in the Plone configuration panel. Click on "Add/Remove products". Among the "products available for install" you will find ZopeComponent:select it and install it. Right. Click now on the Plone logo to come back to the main page, and see what's new. On the left, you see a new "portlet" entitled "zope component": this is the main entry point for your application. gen generates a portlet for every gen-application. The link "Zope component" allows you to access a kind of "dashboard" displaying all Zope components. The page is empty for the moment: no component was created yet:

+ +

+ +

Click on the "plus" sign for creating your first Zope component. A form allows you to enter information according to your Python class definition in ZopeComponent.py.

+ +

+ +

Note that the order of the widgets correspond to the order of the fields in the Python class definition. Clicking on "Save" creates your first Zope 3 component, and brings you to the "view" page for your component. Clicking again on the portlet link will show you this:

+ +

+ +

On this screen, every Python class you declare as "root" in your gen-application will get a tab allowing you to view instances of this class or add new ones. If you've defined more than one root class, a global tab will precede all others and display all instances of all root classes. Table columns are all sortable; they also contain special fields used for filtering rows according to type entered. Actions the user may trigger on Zope components are available in the last column. Clicking on the component title will bring you back to the "view" page for it.

+ +

Besides the view/edit pages and this dashboard, gen also generates a configuration panel for your application. It is directly accessible through the portlet by clicking on the hammer.

+ +

+ +

This configuration panel is called a "tool". By default, the ZopeComponent tool defines one "flavour" named "ZopeComponent". A flavour is a particular set of configuration options that may apply only to a subset of your application objects. The concept of flavour allows you to get, in a single web application, several variants of it running concurrently, all with distinct configuration options. If, for example, you create a software for managing the agenda and decisions of a company's meetings, you may need to create one flavour for each meeting type (IT department meetings, HR department meetings, board of directors, etc): every meeting type will get its own configuration options. In the application portlet, one link is created for every flavour defined in the tool. If you have only one flavour, it makes sense to rename it with something like "All components". You may do it by clicking on the pen besides the flavour name. This way, the application portlet will look like this:

+ +

+ +

A the tool level, some general configuration options are defined (they apply to all defined flavours), like the way Plone will contact OpenOffice in server mode for producing documents (document-generation through appy.pod is built-in for every gen-app) or the number of elements to display per page on the dashboard. If you click on the flavour title, you will discover some configuration options that you may customize at the flavour level (the whole set of options in the flavour depends on options specified in your Python classes). For example, clicking on the pen within the "user interface" tab allows you to customize the columns shown in the dashboard for objects corresponding to this flavour. Select one or more columns here (keep the "control" key pressed if you want to select several columns), save the result and see how the dashboard evolves accordingly.

+ + diff --git a/doc/genCreatingAdvancedClasses.html b/doc/genCreatingAdvancedClasses.html new file mode 100755 index 0000000..14cc3ac --- /dev/null +++ b/doc/genCreatingAdvancedClasses.html @@ -0,0 +1,1098 @@ + + + <b>gen</b> - Creating advanced classes + + + +

Class inheritance

+ +

A gen-class may inherit from another gen-class. Suppose you work as director of human resources in a company and you need to measure employee productivity. Incredible but true: you are also a Python programmer. So you start developing a small application for managing employees. Although you are a director, you have reached a high level of wisdom (maybe due to the fact that you program in Python). So you know that there are two kinds of employees: those that work and those that don't. You decide to create 2 different classes to reflect this:

+ +

+ class Person:
+   root = True
+   title = String(show=False)
+   firstName = String()
+   name = String()
+   def onEdit(self, created):
+     self.title = self.firstName + ' ' + self.name
+
+ class Worker(Person):
+   root = True
+   productivity = Integer()
+

+ +

Indeed, evaluating productivity on persons that do not work has no sense. Because both Persons and Workers are specified with root=True, they become key concepts and you can create instances of both classes through the dashboard:

+ +

+ +

The "edit" view for creating a person looks like this:

+ +

+ +

The "edit" view for creating a worker looks like this:

+ +

+ +

After a while, you become anxious about having 95% of the data in your database (=Person instances) serving absolutely no purpose. Logically, you decide to register the hair color for every non-worker. You realize that you need to change your model to be able to do this:

+ +

+ class Person:
+   abstract = True
+   title = String(show=False)
+   firstName = String()
+   name = String()
+   def onEdit(self, created):
+     self.title = self.firstName + ' ' + self.name
+
+ class Worker(Person):
+   root = True
+   productivity = Integer()
+
+ class Parasite(Person):
+   root = True
+   hairColor= String()
+

+ +

With this new model, class Person serves the single purpose of defining fields which are common to Workers and Parasites. It has no sense to create Person instances anymore, so it becomes abstract. Specifying classes as abstract is as simple as adding abstract=True in the class definition. There is no specific Python construct for declaring classes as abstract. With this new model, the dashboard evolves:

+ +

+ +

While the "edit" view is left untouched for workers, it evolves for parasites:

+ +

+ +

After a while, you become so excited about encoding hair colors that you would like to encode it even before encoding a parasite's name and first name. You have noticed that with gen, order of fields within the "edit" and "consult" views follows order of field declarations in the corresponding gen-classes; furthermore, fields defined in child classes appear after the fields defined in parent classes. Fortunately, the "move" parameter allows to change this default setting. Changing the Parasite class this way produces the desired result:

+ +

+ class Parasite(Person):
+   root = True
+   hairColor= String(move=-2)
+

+ +

+ +

Special methods

+ +

When defining a gen-class, some method names are reserved. Until now, we have already encountered the method onEdit, like in this example:

+ +

+ class Person:
+   abstract = True
+   title = String(show=False)
+   firstName = String()
+   name = String()
+   def onEdit(self, created):
+     self.title = self.firstName + ' ' + self.name
+

+ +

This method is called by gen every time an instance of Person is created (be it through-the-web or through code --yes, at present we have only created instances through-the-web; it is also possible to do it through code like explained below) or modified. Besides the self parameter (which is the newly created or modified instance), the method has one boolean parameter, created. When an object is newly created, created=True. When the object is modified, created=False. Note that the method is not called when an object is modified through code (else it could lead to infinite recursion). In the example, the title of a person is updated from its name and first name every time it is created or modified.

+ +

Another special method is named validate. While the field-specific validators are used for validating field values individually, the validate method allows to perform "inter-field" validation when all individual field validations have succeeded. Consider this extension of class Parasite:

+ +

+ class Parasite(Person):
+   root = True
+   hairColor= String(move=-2)
+   def validate(self, new, errors):
+     if (new.hairColor == 'flashy') and (new.firstName == 'Gerard'):
+       errors.hairColor = True
+       errors.firstName = "Flashy Gerards are disgusting."
+

+ +

Besides the self parameter, the validate method has 2 parameters: new is an object containing new values entered by the user for every visible field of the currently displayed page; errors is an empty object waiting for your action. Every time you consider that a field has not the right value, feel free to add, to the errors object, a new attribute whose name is the name of the erroneous field and whose value is either a text message or simply a boolean value. In the latter case, gen will render the standard error message for that field (more about error messages below). In the above example, you, as a director, felt that people whose first name is Gerard and whose hair color is too flashy are simply not allowed to work in your company. Trying to encode such a disgusting person would lead to this screen:

+ +

+ +

By the way, I have changed class Person such that the field name is now mandatory (name = String(multiplicity=(1,1))). So I can show you now that inter-field validation is only triggered when all individual field validations succeed. Check what happens if you don't give Gerard a name:

+ +

+ +

The validate method didn't come into play (yet...).

+ +

Integration with pod

+ +

pod (Python Open Document) is another component that is part of the Appy framework. It allows to produce documents from data available to Python code. Guess what? gen is tightly integrated with pod! Until now, gen allows us to produce "web" (edit and consult) views from gen-classes. Through pod, we will now create "document" views from gen-classes, like ODT, PDF, Doc or RTF documents.

+ +

Let's begin with the "hello world" pod-gen integration. Suppose you want to produce a document from a given gen-class, let's say the class Person. In this class, simply add the declaration pod=True. Re-generate your product, re-install it through Plone (Site setup) and go the configuration panel for your application. Go to the default flavour: a new tab "document generation" has been added:

+ +

+ +

Now, create this beautiful document with your favorite word processor and save it as "helloWorld.odt":

+ +

+ +

self.title must be typed while the word processor is in mode "record changes". Now, go back to your browser and click on the "plus" icon for associating the POD template you just created to the class Person:

+ +

+ +

Save this and go to the consult view of a Person. In the example below, I am on the consult view of a worker:

+ +

+ +

The list of available documents one may generate from this person are visible in the top-right corner of the consult view. Here, only one document may be generated: "Secret file". Click on it and you will get this file:

+ +

+ +

You have noticed that a class inherits from POD templates defined in its parents: the "Secret file" template was defined for class Person and is available for Worker and Parasite instances. Let's add another POD template that we will build specifically for parasites. First, add pod=True in the class Parasite. Please add also another field to parasites (a String in XHTML format): it will allow to illustrate how to render such a field in a document. So add this line to class Parasite:

+ +

sordidGossips = String(format = String.XHTML)

+ +

Now, please create this parasite:

+ +

+ +

Create this POD template and associate it to class Parasite in the default flavour:

+ +

+ +

With OpenOffice, create a note by selecting Insert->Note in the menu. The "document generation" tab in the flavour should now look like this:

+ +

+ +

From any parasite, it is now possible to generate 2 documents:

+ +

+ +

Clicking on "gossips" wil produce this file:

+ +

+ +

Exhaustive documentation about writing POD templates may be found on this site (start here). You have noticed that the set of POD templates associated to a given class is specific to a given flavour. Although a POD template is associated to a class, the POD template may render a lot of information coming from a complex web of interrelated objects. Indeed, the object that is referred to as self in the POD template is only a starting point. Our example doesn't allow to illustrate this because we have a single class which has no Ref fields. That said, in the future we will also provide the possibility to define POD templates for rendering dashboard views. The starting point here will not be a single instance but the list of objects that is currently displayed in the dashboard.

+ +

What you need to know when using pod with gen is the exact pod context (ie the set of Python objects, variables, modules, etc) that is given by gen to your pod template. The table below presents all entries included in it.

+ + + + + + + + + + + + + + + + + + + + + + +
EntryDescription
selfThe object that is the starting point for this template.
userThe user that is currently logged in. Its id is given by user.id.
podTemplateThe POD template (object) that is currently rendered. Attributes of a POD template are: title, description, podTemplate (= the file descriptor to the corresponding ODT POD template), podFormat (the output format, a string that may be odt, pdf, doc or rtf). +
projectFolder  A string containing the absolute path of the folder where your gen-application resides on disk. If your gen-application is a folder hierarchy, projectFolder is the root folder of it.
+ +

In the previous examples, we have always rendered documents in Odt format. When generating ODT, gen and pod do not need any other piece of software. If you configure a template for generating documents in Adobe PDF (Pdf), Rich Text Format (Rtf) or Microsoft Word (Doc), you will need to run OpenOffice in server mode. Indeed, for producing any of these formats, pod will generate, from your POD template, an ODT file and will ask OpenOffice to convert it into PDF, DOC or RTF. Suppose you modify the ODT template named "Gossips" for producing documents in PDF instead of ODT (in the config, default flavour, tab "document generation", edit the template named "gossips" and choose "Pdf" as "Pod format"). If now you go back to the consult view for a parasite, the PDF icon will show up for the template "Gossips":

+ +

+ +

If you click now on "Gossips", gen will produce an error page because he can't connect to OpenOffice. Please run it now as explained here (section "Launching OpenOffice in server mode"). If now you retry to generate gossips, you will probably have an error again. Why? 2 main causes: (1) The Python interpreter that runs your Zope and Plone does not include the OpenOffice connectors (="UNO"); (2) You did not run OpenOffice on port 2002 which is the port configured by default in any gen-application. For solving both problems, any configuration panel of any gen-application allows you to configure 2 parameters. In the portlet of your gen-application, click on the hammer and then on the pen that lies besides the title of the application:

+ +

+ +

In this example, the Python interpreter that runs my Zope is not UNO-compliant. So I have specified, in parameter "Uno enabled python", the path of such an interpreter. My machine runs Ubuntu: the interpreter installed at /usr/bin/python is UNO-enabled. If you don't have Ubuntu, the simplest way is to specify the path to the UNO-enabled Python interpreter that ships with OpenOffice. When clicking on "Save", if the specified path does not correspond to an UNO-enabled Python interpreter, you will get a validation error. Change the OpenOffice port if necessary; now, when trying to get PDF gossips it should work.

+ +

Until now, we have uploaded POD templates in the configuration panel (for a given flavour). It is also possible to specify "file-system" POD templates through code. In fact, it is even the default behaviour. When declaring a class with pod=True, when (re-)installing the application, gen will check on the file system if a POD template exists for this class. If yes, it will already load it. If no, it simply proposes a non-filled widget in the flavour that will allow you to upload POD templates through-the-web (this is what happened in our examples so far). Suppose that the class Parasite lies in /home/gde/ZopeInstance1/lib/python/ZopeComponent.py. Move your file gossips.odt to /home/gde/ZopeInstance1/lib/python/Parasite.odt. In the flavour, remove the existing ODT template named "Gossips" for class Parasite. Now, reinstall you gen-application. In the same folder as where class Parasite is defined on the file system, gen finds a file named <class_name>.odt (<class_name> being Parasite in this case.) So it will load the corresponding template (for every flavour defined in your gen-application). Now, if you go back to your flavour (tab "document generation"), you will find an ODT template named "Parasite":

+ +

+ +

Instead of writing pod=True, you may define a list of names. Remove again the POD template named "Parasite" from the flavour. Then, on the file system, move Parasite.odt to SordidGossips.odt and copy it also to MoreGossips.odt in the same folder. Then, in class Parasite, replace pod=True with pod=['SordidGossips', 'MoreGossips']. Re-generate your Plone product, restart Zope and re-install your gen-application. Now go back to your flavour (tab "document generation"), you should get this:

+ +

+ +

When POD templates are loaded from code, only minimalistic information is available for getting the corresponding POD template objects in the flavour: fields title and podTemplate (=the ODT file) are filled, but field description is empty and field podFormat is always Odt. Although you may now modify all those data through-the-web, in the future gen will allow you to write things like pod=[PodTemplate('SordidGossips', format='odt', description='blabla'...),...]. Moreover, new fields will be added to the POD template object in the flavour: a condition and a permission for restricting document generation from an ODT template to some users or under some circumstances; the possibility to define a "freeze event" (when this event occurs --typically a workflow transition--, the generated document is written in the Plone database and subsequent clicks do not compute a new document but simply download the frozen one), etc.

+ +

View layouts into pages and groups

+ +

We already know that for each gen-class, gen creates the web "consult" and "edit" views. We also know from the page "Creating basic classes", that both views may be splitted into several pages if the number of widgets becomes too large; on every page, widgets may lie together into groups. It can be accomplished through attributes page and group that one may define on class attributes.

+ +

By default, all widgets are rendered on both edit and consult views on a single page which is named main (main is the default value for parameter page). The default value for parameter group is None: by default, a widget is not rendered into a group but simply added at the end of the current page.

+ +

Let's consider the following example. It is an improved version of the Human Resources software developed by yourself several minutes ago (see above). I have added more fields in order to illustrate how to layout fields into pages and groups.

+ +

+ class Person:
+   abstract = True
+   pod = True
+   title = String(show=False)
+   n = 'name_3'
+   firstName = String(group=n, width=15)
+   middleInitial = String(group=n, width=3)
+   name = String(multiplicity=(1,1), group=n, width=30)
+   contractDetails = String(format=String.XHTML)
+   cia = {'page': 'contactInformation', 'group': 'address_2'}
+   street = String(**cia)
+   number = Integer(**cia)
+   country = String(**cia)
+   zipCode = Integer(**cia)
+   cio = {'page': 'contactInformation', 'group': 'numbers_3', 'width': 20}
+   phoneNumber = String(**cio)
+   faxNumber = String(**cio)
+   mobilePhone = String(**cio)
+   workPhoneNumber = String(**cio)
+   workFaxNumber = String(**cio)
+   workMobilePhone = String(**cio)
+   def onEdit(self, created):
+     self.title = self.firstName + ' ' + self.name
+
+ class Worker(Person):
+   root = True
+   productivity = Integer()
+
+ class Parasite(Person):
+   root = True
+   pod = ['SordidGossips', 'MoreGossips']
+   hairColor = String(group='hairyCharacteristics')
+   sordidGossips = String(format = String.XHTML, page='Gossips')
+   parasiteIndex = String(validator=['low', 'medium', 'high'],
+     page='contactInformation', group='numbers')
+   avoidAnyPhysicalContact = Boolean(page='contactInformation')
+   def validate(self, new, errors):
+     if (new.hairColor == 'flashy') and (new.firstName == 'Gerard'):
+       errors.hairColor = True
+       errors.firstName = "Flashy Gerards are disgusting."
+

+ +

Oufti! Let's give some explanations about this bunch of lines. Attributes firstName, middleInitial and name of class Person are in group name which will be rendered as a table having 3 columns because of the trailing _3 of n = 'name_3'. For those fields, no page was specified; they will be rendered on the first (=main) page. When defining several widgets in a group, the shortest way to write it is to define a dictionary (like cia or cio) containing common parameters (page and group) and use it with the ** prefix that "converts" it into parameters (like for attributes street, number, etc).

+ +

Based on this new definition, the "consult" view for a worker looks like this (main tab):

+ +

+ +

The page "contact information" is here:

+ +

+ +

Changing the layout is as simple as changing some details into your class, re-generating and restarting Zope. For example, try to render group "numbers" in 2 columns instead of 3:

+ +

+ +

Better ! The "edit" view renders the widgets in the same way, but uses their "editable" version instead. Here is the main page:

+ +

+ +

Moreover, buttons "next" and "back" are displayed when relevant. Here is the page "contact information" from the "edit" view:

+ +

+ +

Now, for displaying parasites, we can of course reuse some pages and groups from the parent class Person and potentially add new pages and/or groups. In our example, attribute hairColor was added to the main page, in a new group named hairyCharacteristics:

+ +

+ +

Attribute sordidGossips was put in a page that does not exist for workers:

+ +

+ +

Attribute parasiteIndex was put in an existing group of an existing page, while avoidAnyPhysicalContact was added to the end of an existing page outside any group:

+ +

+ +

Tip: try to avoid performing inter-field validation on fields that are not on the same page.

+ +

In the future, gen will provide specific Page and Group class descriptors that will allow to go further into the layout customization. For example, you will be able to write things like hairColor = String(group=Group('hairyCharacteristics', columns=['50%', '25%', '25%'])).

+ +

The configuration panel (=Tool) and its flavours

+ +

In the introductory page about gen, we have already introduced the tool and its flavours that are available in any gen-application; at several other places we have also described features involving them. In this section, we will go one step further and describe the tool and its flavours in a more systematic way (or at least give pointers to explanations given in other places).

+ +

We will use a gen-application named ZopeComponent.py, which contains the ZopeComponent class as defined in this page, augmented with classes Person, Worker and Parasite as defined above. Here is the portlet for this gen-application:

+ +

+ +

When clicking on the hammer, you are redirected to the main page of the configuration panel (we will say "tool" in the remainder of this page):

+ +

+ +

Before talking about flavours, let's explain the parameters that are directly visible in this page. Those parameters apply throughout your whole gen-application (for all flavours). Parameters in group "Connection to Open Office" are used for connecting Plone to OpenOffice in server mode for producing documents from gen-applications in PDF, DOC or RTF. This is already described here. The "number of results per page" is the maximum number of objects that are displayed on any dashboard page. Set this number to "4" (click on the pen besides "ZopeComponent"). Then, go back to the Plone main page (by clicking on the blue tab named "home", for example) and click on the link in the application portlet. If you created more than 4 objects, you will get a screen like this one:

+ +

+ +

An additional banner in the bottom of the page allows to browse the next/previous objects. Finally, the boolean parameter "Show workflow comment field" allows to display or not a field allowing you to introduce a comment every time you trigger a workflow transition on an object. Workflows and security are covered in more detail in the next page. When enabled (which is the default), on every main page (consult view) of every object you will get a field besides the buttons for triggering transitions, like this:

+ +

+ +

Clicking on any of these buttons will trigger a transition on the object; if you have entered a comment in the field it will be stored as comment for the transition.

+ +

Let's come back to the flavours. As already explained, the tool defines a series of flavours. Every flavour is a given set of configuration options that will apply to a subset of all objects in your application. In order to illustrate this, let's create a second flavour by clicking on the "plus" icon. Name it "Free components":

+ +

+ +

Rename the first flavour "Proprietary components" to get this:

+ +

+ +

The portlet was updated accordingly:

+ +

+ +

Clicking on "Free components" will retrieve no object at all:

+ +

+ +

Indeed, all objects created so far were created for the flavour renamed "Proprietary components". Before entering into the details of flavours, you need to get a more precise explanation about the "dashboard". As you can see from the previous screenshot, the dashboard proposes one "tab" for every "root" class (defined with root=True, more info here) and one tab (named "consult all" by default) for consulting all instances of all root classes. Clicking on the tab of a given root class displays all instances of this class; clicking on the "plus icon" related to this tab brings you to a form that will allow you to create a new instance of this class. Any dashboard page displays a table that contains one row per object. All those tables have at least 2 columns: title (object title is also called "name") and "actions" (this column presents actions that one may perform on objects (edit, delete, etc). The example below shows the dashbord page for zope components:

+ +

+ +

The dashboard page "consult all" contains one more column containing the type or class the object belongs to:

+ +

+ +

Now let's talk about flavours. Indeed, within a given flavour (in tab "user interface"), you may add new columns to dashboard pages. In this tab, for every root class, a listbox allows you to choose zero, one or more class attributes (this list contains an additional special attribute which represents workflow state) for displaying them into the dashboard. In flavour "Proprietary software", go to the "edit" view of this tab, select the values as shown below (keep the "control" key pressed for selecting multiple values) and click on "save":

+ +

+ +

Now, check how dashboard pages have evolved. For example, the dashboard for Zope components looks like this now:

+ +

+ +

This change only impacts a given flavour. If you create a Zope component among "Free components", you will get the default dashboard page:

+ +

+ +

When using additional columns this way, the "consult all" tab may also evolve. In fact, if an attribute with a given name is selected as dashboard column for every root class, it will also appear in the tab "consult all". This can be useful when root classes all inherit from a parent class for example.

+ +

Every flavour also contains a tab "document generation". A detailed explanation about this tab can be found here, so I will not explain it further in this section.

+ +

parameter optional

+ +

When introducing parameters common to all fields (check here), we have introduced field optional. When a field is defined as optional, it may or not be used from flavour to flavour. Let's take an example. Please modify class ZopeComponent and add parameter optional=True to attributes status and funeralDate. Re-generate your application, restart Zope and re-install the corresponding Plone product. Now, in every flavour, you have the possibility to use or not those fields. Go for example to flavour "Free components". A new tab "data" appears (it groups all parameters that customize the conceptual model behind your application). Edit this tab and select both fields:

+ +

+ +

In flavour "Proprietary components", do the same but select only field "status".

+ +

In flavour "Free components", for every Zope component, both fields appear (on the "edit" and "consult" views). Here is the consult view, for example:

+ +

+ +

In flavour "Proprietary components", for every Zope component, the field funeralDate does not appear anymore. Here is the edit view, for example:

+ +

+ +

Making fields optional or not has no impact in the Zope database. All fields are always present, but simply hidden in all views if optional=False. It means that you may easily change your mind and decide at any time to start using a optional field for a given flavour.

+ +

parameter editDefault

+ +

When introducing parameters common to all fields (check here), we have introduced field editDefault. In a lot of situations, we need default values for fields. But in some cases, instead of "hard-coding" the default value (in the Python code) it is preferable to have the possibility to modify this default value throug-the-web. This will happen with gen, on a per-flavour basis, for every field declared with editDefault=True: a new widget will appear in every flavour for editing the default value.

+ +

Let's illustrate this with an example. For class ZopeComponent, add parameter editDefault=True to attributes description and status. Re-generate your application, restart Zope and re-install the corresponding Plone product. Now, in every flavour (tab "data"), widgets were added. Go for example to flavour "Free components" and edit this tab this way:

+ +

+ +

If you try to create a new Zope component in flavour "Free components" you will get this "pre-filled" form:

+ +

+ +

Customizing the Tool and its flavours

+ +

Until now, we have seen all Tool and Flavour attributes managed by gen itself. If you want to add your own attributes, you can also do it. This way, the Tool and its flavours may become the unique configuration panel for your whole application. Beyond some simple configuration options that one may edit through-the-web, one interesting use case justifying tool and flavour customization is the definition, through Ref attributes added to your custom Tool or Flavour, of some "global" objects, typically controlled by a limited number of power-users, that are referred to by user-defined, "root-class-like" objects.

+ +

Let's illustrate it first by defining a custom Tool. We will use our ZopeComponent example. Suppose that the company that creates those components is organized into bunches of geeks. Every component is developed under the responsibility of a bunch. We first need a class for defining bunches:

+ +

+ class BunchOfGeek:
+   description = String(format=String.TEXT)
+

+ +

Creating a custom tool is as simple as inheriting from class appy.gen.Tool:

+ +

+ class ZopeComponentTool(Tool):
+   someUsefulConfigurationOption = String()
+   bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
+     link=False, back=Ref(attribute='backToTool'),
+     shownInfo=('description',))
+

+ +

In this tool, I have created a dummy attribute for storing some configuration option as a String, and a Ref attribute that will, at the Tool level, maintain all bunches defined in our company.

+ +

Now please modify class ZopeComponent by adding a Ref attribute that will allow to assign the component to a given bunch:

+ +

+   responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,
+     link=True, back=Ref(attribute='components'))
+

+ +

Pshhhhhhhhhh! 9 new lines of code, my synapses are melting. Again: re-generate, re-start Zope and re-install the Plone product. Then, go to the Tool: our 2 new attributes are there!

+ +

+ +

Create a bunch of bunches of geeks:

+ +

+ +

Now, go to the dashboard for flavour "Proprietary components" and edit a given Zope component. In the edit view, a new field allows you to select the responsible bunch. You can choose among your 3 bunches.

+ +

+ +

Now, go to flavour "Free components" and try to edit a given Zope component. Aaargh! For field "responsible bunch" the selection box is empty! Why? Remember that flavours are a way to partition your application objects into independent sets, each one having its own configuration. But what about instances of BunchOfGeek? To what set do they belong? You have found the answer: all objects you define at the Tool level (through Ref attributes) belong to the "set" defined by the first flavour that gen creates when your gen-application comes to life. It means that bunches of geeks should be defined at the flavour level and not at the tool level. In a lot of situations, though, you will have a single flavour; in this case, all your global objects may reside at the Tool level.

+ +

We will now transform our example in order to define bunches of geeks at the flavour level. First, delete from your tool the 3 bunches you have created. Then, move attribute bunchesOfGeeks from your custom tool to a new custom Flavour:

+ +

+ class ZopeComponentTool(Tool):
+   someUsefulConfigurationOption = String()
+
+ class ZopeComponentFlavour(Flavour):
+   anIntegerOption = Integer()
+   bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
+     link=False, back=Ref(attribute='backToTool'),
+     shownInfo=('description',), page='data')
+

+ +

Notice an additional small change: within the flavour, the attribute will be present in page data, which is one of the default flavour pages. Re-generate, blablah... Then, this page for every Flavour will look like this:

+ +

+ +

Now, let's create 2 bunches for every flavour. Create or edit a Zope component within flavour "Free components". Field "Responsible bunch" will only be filled with the ones you have created within this flavour.

+ +

As you may have noticed, the default Tool class defines only one page (the main page). Feel free to add pages to your custom tool. The default Flavour class defines the following pages; within your custom flavour, you may either add fields to those existing pages (like in the previous example) or add your own pages.

+ + + + + + + + + + + + + + + + + + + + + + +
Page nameDescription
mainBy default, the main Flavour page (corresponding to the left-most tab) only shows the Flavour name and a link to go back to the Tool.
documentGeneration  All stuff tied to generation of documents from POD templates.
dataConfiguration options related to the conceptual model behind your application. Usage of optional fields or default editable values are configured through this page for example.
userInterfaceConfiguration options related to the way your gen-application looks. Columns displayed in the dashboards are configured here for example.
+ +

When defining fields on custom tools and flavours, some parameters have no sense. This is the case for all parameters enabling "configurability", like editDefault, optional, etc: you can't meta-configure (=configure the configuration). If you try to use those parameters on fields defined on custom tools and flavour they will be ignored by gen.

+ +

Now that we know everything about tools and flavours, we may give more precisions about object storage, first introduced in the previous page. The tool for your application, be it customized or not, corresponds to a Zope object that lies directly within the object corresponding to your Plone site. You may see it from the ZMI:

+ +

+ +

The name of the Tool object is based on your application name. Here, within the Plone Appy site, the tool corresponding to the application named ZopeComponent is called (=has id) portal_zopecomponent. Furthermore, you see that flavours are folders contained within the tool. Every object created through Ref fields associated to a tool or flavour will be stored within the folder that corresponds to this tool or flavour. For example, you may verify that bunches of geeks are stored within their corresponding flavour.

+ +

Manipulating objects from code

+ +

Until now, we have already encountered several places where we manipulate objects from code (ie within an onEdit method) or from POD templates, for, i.e., getting or setting field values. You deserve more explanations about those objects. The truth is: they are not real instances of the classes you define in your application. Why?

+ +

"Real" objects are created and managed by Zope (remember: we use Zope as underlying framework). But those objects are complex and squeezed, their interface is ugly and contains an incredible number of methods inherited from dozens of Zope classes. Yes, I know, I am responsible for the choice of Zope for Appy. Understand me: Zope already implements a lot of interesting things like security. In order to preserve Appy developers from Zope complexity I decided to create some nice, simple, minimalistic wrappers around Zope objects. When you manipulate objects, the truth is that you manipulate instances of those wrappers.

+ +

The nice news about wrapper classes is that they inherit from your gen-classes. So you are always able to use on objects/wrappers the methods you have defined in your classes. But they also inherit from a wrapper class that inherits itself from other wrappers (like wrappers corresponding to parent classes of your gen-class if relevant) and ultimately from an abstract root AbstractWrapper class. If you are curious, you may consult all wrapper definitions which are generated in [yourZopeInstance]/Products/[yourApplicatonName]/Extensions/appyWrappers.py.

+ +

This "wrapper mechanism" abstracts the Appy developer from the underlying technology. It means that gen could potentially run with other Python-based frameworks than Zope/Plone. The gen architecture is ready for integrating other code generators; but for the moment, only one generator has been written (the Plone generator).

+ +

Pragmatically, what you need to know is the following. For every Appy field defined in a gen-class, the corresponding wrapper class contains a Python property (more info here) that has the same name, for which a getter function is defined. Every time you write a thing like:

+ +

self.bunchesOfGeeks

+ +

The getter function is triggered behind the scenes and queries the real Zope object for getting the expected result. After it, the function may potentialy adapt the result before giving it to you. In this example, every "Zope" bunch of geeks is wrapped and you get a list of BunchOfGeek wrappers. This way, you always manipulate wrappers and you may forget everything about the cruel Zope world.

+ +

Doing the same thing on a Computed field will trigger the machinery you have defined for computing the field; you will simply get the result!

+ +

For setting field values on Zope objects, wrappers override the standard Python method __setattr__ (more info here; it did not work by defining a setter through on the Python property). Again, when writing things like

+ +

self.title = self.firstName + ' ' + self.name

+ +

field title of the corresponding Zope object is updated through a real, behind-the-scenes call to the corresponding Zope method.

+ +

If you are an experienced Zope developer and you feel nostalgic about manipulating real Zope objects, or if you believe that in some situations the wrapping mechanism may constitute a potential performance problem, Appy still allows you to manipulate real Zope objects directly. Suppose self represents a wrapper; writing

+ +

self.o + +

gives you the real Zope object. In these pages I will not document Zope objects because I am in the process of trying to forget everything about them.

+ +

Beyond getters and setters, Appy wrappers give you plenty of nice attributes and methods for manipulating your objects. The following table shows you available attributes (or Python properties).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
field namewritable?description
toolNoFrom any wrapper, you may access the application tool through this property.
sessionYesGives you access to the Zope "session" object. By "writable" I mean you may put things into it (this is a dict-like object), but don't try to replace this object with anything else.
typeNameNoGives you a string containing the name of the gen-class tied to this object.
idNoThe id of the underlying Zope object.
stateNoThe current workflow state of the object (as a string value).
stateLabel  NoThe translated label of the current workflow state.
klassNoThe Python class (=gen-class) related to this class. Indeed, because the instances you manipulate in the code inherit from special wrappers, writing self.__class__ will give you a wrapper class. So write self.klass instead.
+ +

The following table shows you available methods.

+ + + + + + + + + + + + + + + + + +
method nameparametersdescription
createfieldName, **kwargsCreates a new object and links it to this one through Ref field having name fieldName. Remaining parameters **kwargs are used for initialising fields of the newly created object. Here is an example, inspired from the one described above. Suppose that, in every flavour, a bunch of geeks called "Escadron de la mort" must absolutely be present: it includes the best geeks that you use for debugging the most critical Zope components. So every time you create or edit a flavour, you need to ensure that this bunch is there. If not, you will create it automatically. Code for class ZopeComponentFlavour must evolve this way: +

+ class ZopeComponentFlavour(Flavour):
+   anIntegerOption = Integer()
+   bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
+     link=False, back=Ref(attribute='backToTool'),
+     shownInfo=('description',), page='data')
+   def onEdit(self, created):
+     if 'Escadron de la mort' not in [b.title for b in self.bunchesOfGeeks]:
+       self.create('bunchesOfGeeks', title='Escadron de la mort',
+         description='I want those guys everywhere!')
+

+ The create method creates the bunch, and appends it to the existing bunches (if any): it does not remove the existing bunches. The create method returns the newly created object. In this example I don't need it so I don't put the method result into some variable. +
linkfieldName, objLinks the existing object obj to the current one through Ref field fieldName. Already linked objects are not unlinked. This method is used by method create but may also be used independently.
+ +

For setting Ref fields, please use the create and link methods instead of the predefined setters which may not work as expected.

+ +

Manipulating tools and flavours from code

+ +

Tool and Flavour objects, be they customized or not, adopt the same "wrapper" mechanism as described above. In this section, we will simply explain how to get/set programmatically the predefined fields that gen generates automatically on tools and flavours. The following table presents the names of the predefined attributes defined on any Tool:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
field namedescription
flavoursThe list of flavours defined in this tool.
unoEnabledPythonThe path to a potential UNO-enabled Python interpreter (for connecting to Open Office in server mode).
openOfficePortThe port on which OpenOffice runs in server mode.
numberOfResultsPerPageThe maximum number of results shown on any dashboard page.
listBoxesMaximumWidthThe maximum width of listboxes managed by gen (the ones used for displaying the list of objets which are available through Ref fields defined with link=True, for example).
showWorkflowCommentField  The boolean indicating if the field for entering comments when triggering a worflow transition must be shown or not.
+ +

For flavours, things are a little more complex. Imagine we have, throughout our gen-application, 25 fields parameterized with editDefault=True in 6 classes. The corresponding attributes that hold the default values in the flavours have an ugly name that includes the full package name of the class. Instead of forcing you to remember this obscure naming convention, a nice method defined on the Flavour class allows to retrieve this attribute name:

+ + + + + + + + + + + + +
method nameparametersdescription
getAttributeName  attributeType, klass, attrName=NoneThis method generates the attribute name based on attributeType, + a klass from your gen-application, and an attrName (given only if needed, for example if attributeType is defaultValue). attributeType may be:

+ + + + + + + + + + + + + + + + + +
defaultValueAttribute that stores the editable default value for a given attrName of a given klass
podTemplatesAttribute that stores the POD templates that are defined for a given klass
resultColumnsAttribute that stores the list of columns that must be shown on the dashboard when displaying instances of a given root klass
optionalFieldsAttribute that stores the list of optional attributes that are in use in the current flavour for the given klass

+ Here is an example. Suppose you want to modify programmatically, on a given flavour, the list of columns that are shown on the dashboard that present ZopeComponents. You can do it this way: + +

+ attrName = flavour.getAttributeName('resultColumns', ZopeComponent)
+ columns = ['status', 'funeralDate', 'responsibleBunch']
+ setattr(flavour, attrName, columns)
+

+
+ +

Besides flavour attributes having oscure names, some attributes have a normal name:

+ + + + + + + + + + + + +
field namewritable?description
numberNoThe flavour number.
+ +

When choosing field and method names for your gen-classes, try to avoid using names corresponding to fields or methods from base Appy classes (wrappers, tool, flavours).

+ +

Defining a custom installation procedure

+ +

When (re-)installing your gen-application, you may want to initialize it with some data or configuration options. You do this by specifying a special action named install on your customized tool, like in the example below (the ZopeComponent application).

+ +

+ class ZopeComponentTool(Tool):
+   someUsefulConfigurationOption = String()
+   def onInstall(self):
+     self.someUsefulConfigurationOption = 'My app is configured now!'
+   install = Action(action=onInstall)
+

+ +

Re-generate your gen-application, re-start Zope, log in as administrator and go to site setup->Add/Remove products. Your product wil look like this (it needs re-installation):

+ +

+ +

Re-install your product and go to the consult view of your configuration panel. The install action has been executed:

+ +

+ +

Moreover, because your installation procedure is an action, you may trigger your custom installation procedure via the available "install" button.

+ +

Note that actions defined on custom tools or flavours are well suited for importing data or executing migration scripts (so actions may, in a certain sense, represent a way to replace the notion of "profiles" available in Plone. Actions are easier to trigger because they can be displayed anywhere on your tool, flavour or on any gen-class).

+ +

i18n (internationalization)

+ +

gen-applications benefit from an automated support for i18n. How does it work? First of all, you need to declare what language(s) need to be supported in your gen-application. This is done through the creation, anywhere in your gen-application, of an instance of class appy.gen.Config:

+ +

+ from appy.gen import Config
+ c = Config()
+ c.languages = ('en', 'fr')
+

+ +

By configuring your Config instance this way, you tell gen to support English and French. If you don't do this, only English is supported by default.

+ +

Every time you (re-)generate your gen-application, i18n files are created or updated in the corresponding generated Plone product. With the above settings, gen will generate the following files, in [yourZopeInstance]/Products/[yourApplication]/i18n (the product here is named ZopeComponent):

+ +

+ ZopeComponent.pot
+ ZopeComponent-en.po
+ ZopeComponent-fr.po
+

+ +

ZopeComponent.pot contains all i18n labels generated for your application, together with their default values (in English). English translations are in ZopeComponent-en.po, while French translations are in ZopeComponent-fr.po.

+ +

The format of these files is quite standard in the i18n world. Le'ts take a look to the beginning of ZopeComponent.pot:

+ +

+ msgid ""
+ msgstr ""
+ "Project-Id-Version: ZopeComponent\n"
+ "POT-Creation-Date: 2008-12-12 14:18-46\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: ZopeComponent\n"
+

+ +

An interesting general information here is the domain name (last line) which is ZopeComponent. Indeed, Plone structures translations into domains. A domain is a group of translations that relate to a bunch of functionality or to a given Plone component. gen creates a specific domain for every gen-application. In the example, domain ZopeComponent has been created. Why do I explain this to you? Really, I don't know. With gen, you don't care about i18n domains, gen manages all this boring stuff for you. Sorry. Mmh. Let's continue to analyse the file. In the (very similar) headers of the English and French translation files (ZopeComponent-en.po and ZopeComponent-fr.po), the important thing that is filled is the code and name of the supported language (parameters Language-code and Language-name).

+ +

After this header, you will find a list of labels. In the pot file, every label is defined like this one:

+ +

+ #. Default: "Funeral date"
+ msgid "ZopeComponent_ZopeComponent_funeralDate"
+ msgstr ""
+

+ +

In every related po file, you will find the same entry. Translating a gen-application is as simple as editing, for every po file, every msgstr line for every i18n label. If you don't fill a given msgstr, the default value will be used. You may insert HTML code within msgstr entries.

+ +

The i18n machinery of Plone works this way: every time Zope/Plone encounters an i18n label in a web page (or part of it), it tries to find, in the language specified by the web browser, a translation in the corresponding po file (or a cached version of it; if you change the translations in the po files you need to restart Zope or refresh, through the ZMI, the corresponding "catalog object" Zope has created in Root Folder/Control_Panel/TranslationService). Plone and Appy-generated web pages do not "hard-code" any translation; they always consult Zope i18n catalog objects. So after having translated all labels in all po files, changing your browser language and refreshing a given page will produce the same page, fully translated in the new specified language.

+ +

gen creates and maintains pot and po files itself. So gen implements the same functionality as tools like i18dude. You don't need such tools to manage i18n files of gen-applications. Although i18n files are stored in the generated Plone product, they will never be deleted by triggering a code (re-)generation. gen will potentially complete the files but will never delete them.

+ +

Now, you need to know the meaning of every label gen creates and maintains in the po(t) file(s), and at what place(s) they are used within the generated edit and consult views (or in the dashboards, etc). The following table explains this. Dynamic parts used in labels (like [className]) are explained in yet another table below. For every label, the default value is specified. For the majority of labels, gen proposes a "nice" default value. For example, field responsibleBunch will have default value Responsible bunch; class BunchOfGeeks will have default value Bunch of geeks. The "nice" algorithm tries simply to recognize camel-cased words and separates them.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
label "pattern"usagedefault value
[className]_[fieldName]The label of the Appy field of a given gen-class. It appears on both edit and consult views, as widget label. For example, field funeralDate of class ZopeComponent in ZopeComponent.py will produce label ZopeComponent_ZopeComponent_funeralDate.nice
[className]_[fieldName]_descrA description associated to a given Appy field. It appears on the edit view, between the widget label and the widget. Here is an example of a field shown on an edit view, with a label and description.
+ +
empty
[className]_[fieldName]_list_[fieldValue]When defining a String field with a list of values as validator, such a label is created for every value of this list. Consider the following field declaration (in class ZopeComponent from ZopeComponent.py):
status = String(validator=['underDevelopement', 'stillSomeWorkToPerform', 'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient']).
gen will generate 5 labels (one for each possible value). The first one will be ZopeComponent_ZopeComponent_status_list_underDevelopement.
nice
[className]_[fieldName]_validError message produced when validation fails on a given field (edit view) and the validation mechanism does not return an error message."Please fill or correct this."
[className]_[fieldName]_backLabel of a back reference. Consider the following field definition in class ZopeComponent:
responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False, link=True, back=Ref(attribute='components'))
+ On the consult view related to BunchOfGeek, the label of the back reference for consulting components for which this bunch is responsible for will be ZopeComponent_BunchOfGeek_components_back.
nice
[className]_[fieldName]_action_okOnly generated for Action fields. Represents the message to display when the action succeeds."The action has been successfully executed."
[className]_[fieldName]_action_koOnly generated for Action fields. Represents the message to display when the action fails."A problem occurred while executing the action."
[applicationName]This unique label translates the name of the application. It is used as title for the application portlet. + nice
[className]The name of a gen-class. It appears at several places (dashboard tabs, edit views, ...). This label is then "declined" into several others, suffixed with [_flavourNumber] for flavours 2 to n. It means that you may name your concepts differently from one flavour to the other. For example, a class named Meeting may be named "Government meeting" in one flavour and "Parliament meeting" in the other.nice
[className]_edit_descrThe description of a gen-class. It appears on edit views, when creating instances of this class. Like the previous label, this one is then "declined" into several others, suffixed with [_flavourNumber] for flavours 2 to n.empty
[className]_page_[pageName]This label is used for translating the name of a page which is not the main page (the main page takes the --translated-- name of the corresponding gen-class). Page names are visible on page tabs. Because this label is prefixed with the className, for every child class of a given class that defines pages, gen will produce an additional label prefixed with the child class name.nice
[className]_group_[groupName]This label is used for translating the name of a group, used as label for group fieldsets on both edit and consult views. Because this label is prefixed with the className, for every child class of a given class that defines groups, gen will produce an additional label prefixed with the child class name.nice
[workflowName]_[stateName]This label is used for translating the name of a workflow state. Because this label is prefixed with the workflowName, for every child workflow of the one that defines the corresponding state, gen will produce an additional label prefixed with the child workflow name.nice
[workflowName]_[transitionName]This label is used for translating the name of a workflow transition. Because this label is prefixed with the workflowName, for every child workflow of the one that defines the corresponding transition, gen will produce an additional label prefixed with the child workflow name.nice
workflow_stateTranslation of term "workflow state" (used a.o. if the corresponding column is shown in a dashboard)."state"
root_typeTranslation of term "type" used for the corresponding column on the dashboard tab "consult all"."type"
workflow_commentLabel of the field allowing to enter comments when triggering a workflow transition."Optional comment"
choose_a_valueTranslation of the "empty" value that appears in listboxes when the user has not chosen any value."- none -"
min_ref_violatedError message shown when, according to multiplicities, too few elements are selected for a Ref field."You must choose more elements here."
max_ref_violatedError message shown when, according to multiplicities, too many elements are selected for a Ref field."Too much elements are selected here."
no_refText shown when a Ref field contains no object."No object."
add_refText shown as tooltip for icons that allow to add an object through a Ref field."Add a new one"
ref_nameWhen displaying referenced objects for a Ref field with showHeaders=True, this label translates the title of the first column (=name or title of the referenced object)."name"
ref_actionsWhen displaying referenced objects for a Ref field with showHeaders=True, this label translates the title of the last column (=actions)."actions"
move_upTooltip for the icon allowing to move an element up in a Ref field."Move up"
move_downTooltip for the icon allowing to move an element down in a Ref field."Move down"
query_createText shown as tooltip for icons that allow to create a new root object on a dashboard."create"
query_no_resultText shown when a dashboard or query contains no object."Nothing to see for the moment."
query_consult_allLabel of the leftmost dashboard tab (consult all instances of all root classes for a given flavour)."consult all"
bad_intGeneral error message displayed when a non-integer value is entered in an Integer field."An integer value is expected; do not enter any space."
bad_floatGeneral error message displayed when a non-float value is entered in a Float field."A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
bad_emailGeneral error message displayed when an invalid email is entered in a String field with validator=String.EMAIL."Please enter a valid email."
bad_urlGeneral error message displayed when an invalid URL is entered in a String field with validator=String.URL."Please enter a valid URL."
bad_alphanumericGeneral error message displayed when an invalid alphanumeric value is entered in a String field with validator=String.ALPHANUMERIC."Please enter a valid alphanumeric value."
+ +

As already mentioned, in the table below, some label "parts" are explained.

+ + + + + + + + + + + + + + + + + + +
label partdescription
applicationName   + The name of your gen-application. If your application is a Python package (=a file), it corresponds to the name of the file without its extension (application name for ZopeComponent.py is ZopeComponent). If you application is a Python module (=a folder), it corresponds to the name of this folder.
className + Refers to the full package name of a gen-class, where dots have been replaced with underscores. For example, class ZopeComponent in ZopeComponent.py will have className "ZopeComponent_ZopeComponent". There are 2 exceptions to this rule. className for a flavour, be it the default one or a custom class in your gen-application, will always be [applicationName]Flavour. In the same way, className for a tool will always be [applicationName]Flavour.
workflowName + Refers to the full package name of a gen-workflow, where dots have been replaced with underscores and all characters have been lowerized. For example, workflow ZopeComponentWorkflow in ZopeComponent.py will have workflowName "zopecomponent_zopecomponentworkflow".
fieldName + Refers to the name of an Appy field, declared as a static attribute in a gen-class. For example, attribute responsibleBunch of class ZopeComponent will have fieldName "responsibleBunch".
+ +

Creating and using new i18n labels

+ +

Although gen tries to manage automatically the whole i18n thing, in some cases you may need to create and use specific labels. You create new labels by adding them "by hand" in the pot file. For example, edit ZopeComponent.pot and add the following label:

+ +

+ #. Default: "Please be honest and do not pretend there is any kind of simplicity in your Zope 3 component."
+ msgid "zope_3_is_not_simple"
+ msgstr "" +

+ +

As part of the generation process, gen synchronizes the pot and po files. So re-generate your product and consult ZopeComponent-fr.po and ZopeComponent-en.po: the label has been added in both files. You may now edit those translations and save the edited po files.

+ +

In the following example, we use the new label for producing a translated validation message on a given field.

+ +

+ class ZopeComponent: +   ...
+   def validateDescription(self, value):
+     res = True
+     if value.find('simple') != -1:
+       res = self.translate('zope_3_is_not_simple')
+     return res
+   technicalDescription = String(format=String.XHTML, validator=validateDescription)
+   ...
+

+ +

On any (wrapped) instance of a gen-class, a method named translate allows you to translate any label. This method accepts 2 parameters: label and domain. In the example, no value was given for parameter domain; it defaults to the application-specific domain (in this case, ZopeComponent). Maybe one day you will need to use and translate labels packaged with Plone or another Plone product; in this case you will need to specify another domain. The main domain for Plone is named plone (case sensitive). For example, "delete" icons in Appy pages have a tooltip corresponding to label "label_remove" defined in domain "plone". If you want to use it, write self.translate('label_remove', domain='plone'). Labels in standard Plone domains are already translated in a great number of languages.

+ +

Now imagine you need to change the default value for this label in ZopeComponent.pot. It means that your translations in ZopeComponent-fr.po and ZopeComponent-en.po need a potential revision. Edit the default value and re-generate your product. The labels in the po files will be flagged as "fuzzy":

+ +

+ #. Default: "Please be honest and do not pretend there is any kind of simplicity in your Zope 3 component. I changed the default value."
+ #, fuzzy
+ msgid "zope_3_is_not_simple"
+ msgstr "ZOPE 3 n'est pas simple!"
+

+ +

Edit the translation, remove the line with "fuzzy" and you are done!

+ +

Field interactions: masters and slaves

+ +

It is possible to make your gen pages more dynamic by defining relationships between fields belonging to the same page. For example, by selecting a given item in a list (on a master field), another (slave) field may become visible. This is achieved by using parameters master and masterValue. Let's try it on an example. Remember that, in our HR system, we have added the ability to associate a "parasite index" to a parasite. Suppose that in some cases, the parasite is so weird that you are unable to give him a parasite index. Among the values for field parasiteIndex, we add the value unquantifiable; but in this case, you must give, in another field, details about why you can't quantify its parasiteness:

+ +

+ class Parasite(Person):
+   ...
+   parasiteIndex = String(validator=['low', 'medium', 'high', 'unquantifiable'],
+     page='contactInformation', group='numbers')
+   details = String(page='contactInformation', group='numbers',
+     master=parasiteIndex, masterValue='unquantifiable')
+   ...
+

+ +

Now, if you edit page "contactInformation" for any parasite, selecting, for field parasiteIndex, any value excepted "unquantifiable" will give you something like this:

+ +

+ +

Selecting "unquantifiable" will display field details:

+ +

+ +

Of course, you may use the validation machinery (method validate) and check that, when parasiteIndex is "unquantifiable", field details is not empty. This will be your exercise for tonight.

+ +

Note that master/slaves relationships also modify the "consult" pages. When "unquantifiable" is chosen, both fields are rendered:

+ +

+ +

When another value is selected, field details is invisible:

+ +

+ +

A master widget may have any number of slaves. Of course, masters and slaves must be on the same page. For the moment, the only behaviour on slaves is to display them or not. In future gen releases, other ways to persecute slaves will be implemented, like changing default values, adapting the list of possible values, etc). For the moment, only the following fields may become masters:

+ +
    +
  • Single-valued lists (String fields with max multiplicity = 1 and validator being a list of string values): this was the case in the previous example). In this case, masterValue must be a string belonging to the master's validator list);
  • +
  • Multiple-valued lists (idem, but with max multiplicity > 1);
  • +
  • Checkboxes (Boolean fields). In this case, masterValue must be a boolean value.
  • +
+ +

In future gen releases, masterValue will accept a list of values or a single value.

+ +

The Config instance

+ +

As already mentioned, a series of configuration options for your gen-application may be defined in an instance of class appy.gen.Config. There must be only one such instance by gen-application. The following table describes the available options defined as Config attributes.

+ + + + + + + + + + + + + + + + + + + + + + +
AttributeDefault valueDescription
languages['en']See here.
defaultCreators['Manager', 'Owner']See here.
minimalistPlone  FalseIf True, this flag will produce a minimalist Plone, where some user interface elements, like actions, portlets or other stuff less relevant for building web applications, are removed or hidden. Using this produces effects on your whole Plone site! This can be interesting on development machines: less visual gadgets make geeks more productive.
+ +

Here is an example of a Config instance:

+ +

+ from appy.gen import Config
+ c = Config()
+ c.languages = ('en', 'fr')
+ c.defaultCreators += ['ZLeader']
+ c.minimalistPlone = True +

+ +

This code may be placed anywhere in your gen-application (in the main package, in a sub-package...)

+ + diff --git a/doc/genCreatingBasicClasses.html b/doc/genCreatingBasicClasses.html new file mode 100755 index 0000000..06b3e75 --- /dev/null +++ b/doc/genCreatingBasicClasses.html @@ -0,0 +1,644 @@ + + + <b>gen</b> - Creating basic classes + + + +

Gen-applications

+ +

A gen-application is a simple Python module (a single Python file) or package (a hierarchy of folders where every (sub-)folder contains a file named __init__.py). In the main gen presentation page, we've created a simple application in the form of a Python module, we've run it by installing Zope and Plone and by generating a Plone product. Working with a Python package instead of a Python module is quite easy: instead of creating MyModule.py in [myZopeInstance]/lib/python you simply create a folder "MyPackage" at the same place.

+ +

Within your Python module or package, you create standard Python classes. Those classes do not need to inherit from any base class provided by gen. If you want to turn a given class into a "gen-class" (= a class whose instances will be visualized, created and edited throug-the-web with Plone), you need to provide static attributes that will represent the associated data you need to edit and/or view through-the-web. Those attributes must be instances of any sub-class of appy.gen.Type. We will see that some attribute and method names are "reserved" for specific uses; all other methods and attributes you will define on "gen-classes" will be kept untouched by gen. "gen-classes" do not need any constructor at all, because instances will be created throug-the-web or via some gen-specific mechanisms that we will explain later.

+ +

What gen tries to do is to be as invisible as possible, by leaving your Python classes as "pure" as possible.

+ +

Gen-classes and attributes

+ +

The code below shows an example of a gen-class.

+ +

+ from appy.gen import *
+ class A:
+   at1 = String()
+   at2 = Integer()
+   at3 = Float()
+   at4 = Date()
+   at5 = 'Hello'
+   def sayHello(self):
+     print self.at5 + str(self.at2)
+

+ +

String, Integer, Float and Date all inherit from appy.gen.Type. You may still define standard Python attributes like at5 and methods like sayHello. The list of basic types provided by gen is shown below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Integer Holds an integer value.
Float Holds a floating-point number.
String Holds a string value (entered by the user, selected from a drop-down list, etc).
Boolean Holds a boolean value (typically rendered as a checkbox).
Date Holds a date (with or without hours and minutes).
File Holds a binary file.
Ref Holds references to one or more other objects.
Computed Holds nothing; the field value is computed from a specified Python method.
Action Holds nothing; the field represents a button or icon that triggers a function specified as a Python method.
+ +

When defining instances of those types, you will typically give some parameters to it. Below is the list of parameters that are common to all types. In the next sections, we will see into more detail every type with its specificities.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parameterdefault valueexplanation
validatorNoneA validator is something that restricts the valid range of values that the field may hold. It can be: +
    +
  • A Python (instance) method belonging to the class defining the field (or one of its parents). Every time an object is created or updated, this method will be called, with a single argument containing the new value the user has entered for the field. If this method returns a string (more precisely, an instance of basestring), validation will fail and this string will be shown to the user as error message. Else, validation will succeed if the return value is True (or equivalent value) and will fail else. In this latter case, the error message that the user will get will correspond to i18n label [full_class_name]_[field_name]_valid in the application-specific i18n domain, where [full_class_name] is the class name including the package prefix where dots have been replaced with underscores. More information about i18n may be found here. Examples are presented hereafter.
  • +
  • A list of string values. It only applies for String fields. This list contains the possible values the field may hold.
  • +
  • A regular expression (under the form of a compiled regular expression generated with re.compile).
  • +
+
multiplicity(0,1)Multiplicity is a 2-tuple that represents the minimum and maximum number of values the field may hold. (0,1) means that the field may be empty or contain one value. (1,1) means that a value is required. For all types excepted Ref and some Strings, the maximum value must be 1. The Python value None is used for representing an unbounded value: (1,None) means "at least 1" (no upper bound).
defaultNoneThe default value for the field. The default value must be a Python value that corresponds to the Appy type. Correspondence with Python and Appy types is given here: + + + + + + + + + + + + + + + + + + + + + + + + + +
Appy typePython type
Integerint, long
Floatfloat
Stringstr, unicode
Booleanbool
DateThe Date type shipped with Zope (class DateTime.DateTime). The corresponding type from standard datetime package is less sexy.
+
optionalFalseWhen you define a field as optional, you may turn this field on or off in every flavour (a specific widget will be included in the flavour for this). If you turn the field on, it will appear in view/edit pages; else, it will completely disappear. This is one of the features that allow to tackle variability: in a given organisation the field may be used, in another one it may not. Or even within the same application, the field can be enabled in a given flavour and disabled in another. More information about this parameter may be found here.
editDefaultFalseWhen editDefault = True, a special widget will be present in every flavour; it will allow you to enter or edit the default value for the field. Instead of "hardcoding" the default value through parameter default, using editDefault allows to do it through-the-web, flavour by flavour. More information about this parameter may be found here.
showTrueYou may specify here a special condition under which the field will be visible or not. This condition may be: +
    +
  • a simple boolean value;
  • +
  • a Python (instance) method that returns True, False (one any other equivalent value). This method does not take any argument.
  • +
+ Note that visibility of a given field does not only depend on this parameter. It also depends on its optionality (see parameter optional) and on the user having or not the permission to view and/or edit the field (see parameters specificReadPermission and specificWritePermission below). +
pagemainBy default, for every gen-class you define, gen will produce 2 views: one for consulting information about an instance, one for editing this information. If you have a limited number of fields, a single page is sufficient for displaying all fields on both views. You may also decide to create several pages. The default page is named "main". If, for this parameter, you specify another page, gen will automatically create it (on both views) and allow the user to navigate from one page to the other while consulting or editing an instance (every page will be represented as a tab). If you define several pages for a given class, the main page will take the internationalized name of the class (it corresponds to i18n label [full_class_name] in i18n domain plone, where [full_class_name] is the class name including the package prefix where dots have been replaced with underscores), and the other pages will take their names from i18n label [full_class_name]_page_[page_name] again in i18n domain plone. More information about pages may be found here; more information about i18n may be found here.
groupNoneWithin every page, you may put widgets into "groups" that will be graphically rendered as fieldsets. Normally, widgets are rendered in the same order as the order of their declaration in the Python class; putting them into groups may change this behaviour. If you specify a string here, a group will be created and this field will be rendered into it on both views (consult/edit). The name of the group will correspond to i18n label [full_class_name]_group_[group_name] in i18n domain plone, where [full_class_name] is the class name including the package prefix where dots have been replaced with underscores. More information about i18n may be found here. If you add something like _[number] to the group name, widgets from the group will be rendered into columns; number being the number of columns in the group. For example, if you define several fields with parameter group="groupA_3", field values will be put in group groupA that will span 3 columns. If you specify different number of columns every time you use the group parameter for different fields, all numbers will be ignored excepted the one of the first field declaration. For subsequent field declarations, you don't need to specify the columns number again. More information about pages and groups may be found here.
move0Normally, fields are rendered in consult/edit pages in the order of their declaration in the Python class. You may change this by specifying an integer value for the parameter move. For example, specifing move=-2 for a given field will move the field up to 2 positions in the list of field declarations. This feature may be useful when you have a class hierarchy and you want to place fields from a child class at a given position among the fields from a parent class.
searchableFalseWhen defining a field as searchable, the field declaration will be registered in the low-level indexing mechanisms provided by Zope and Plone, allowing fast queries based on this field; the searches performed via the global "search" in Plone will take the field into account, too.
specificReadPermissionNoneBy default, permission to read every field declared in a class is granted if the user has the right to read class instances as a whole. If you want this field to get its own "read" permission, set this parameter to True. More information about security may be found here; specific details about usage of this field may be found here. +
specificWritePermissionNoneBy default, permission to write (=edit) every field declared in a class is granted if the user has the right to create an edit instances as a whole. If you want this field to get its own "write" permission, set this parameter to True. More information about security may be found here; specific details about usage of this field may be found here. +
widthNoneAn integer value that represents the width of a widget. For the moment, it is only used for Strings whose format is String.LINE. For those Strings, default value is 50.
masterNoneAnother field that will in some manner influence the current field (display it or not, for example). More information about master/slave relationships between fields may be found here.
masterValueNoneIf a master is specified (see previous parameter), this parameter specifies the value of the master field that will influence the current field (display it or not, for example). More information about master/slave relationships between fields may be found here.
+ +

Integers and Floats

+ +

Integers and floats have no additional parameters. In this section, we will simply illustrate, on Integers and Floats, some parameters defined in the previous section. Let's consider the following class:

+ +

from appy.gen import *
+ class Zzz:
+   root = True
+   def show_f1(self): return True
+   def validate_i2(self, value):
+     if (value != None) and (value < 10):
+       return 'Value must be higher or equal to 10.'
+     return True
+   i1 = Integer(show=False)
+   i2 = Integer(validator = validate_i2)
+   f1 = Float(show=show_f1, page='other')
+   f2 = Float(multiplicity=(1,1))
+

+ +

Recall from the introduction that a class declared as root is of special importance: it represents a "main" concept in your application. For every root class, a tab is available in the dashboard for viewing, editing and deleting objects of this class.

+ +

Because i1 is defined with show=False, it will never appear on Appy views. i2 illustrates a validator method that prevents entered values to be lower than 10 (be careful: because the value is not mandatory (default multiplicity=(0,1)), validate_i2 will still be called even if value is None. f1 illustrates how to define a Python method for the show parameter; because this is a silly method that always return True, f1 will always be displayed. f1 will be displayed on another page named other. f2 will be mandatory. So when creating an instance of Zzz through-the-web, you get the following screen:

+ +

+ +

Plone needs a field named title; because you did not had any field named title in your class, Plone has added one automatically. This is because at several places Plone and gen use the title of objects. If you don't care about this, simply create an attribute title=String(multiplicity=(0,1), show=False). The field will disappear (don't forget to specify this multiplicity; else, the field will not show up but will still be mandatory: it will produce an error and you will be blocked); an internal title will be generated instead that will produce ugly results at some places: so it is not recommanded to do it. Let's see how the validation system behaves if we type a wrong value in i2 and nothing in f2:

+ +

+ +

If we enter a value lower than 10 in i2 we get our specific error message:

+ +

+ +

After corrections have been made, clicking on "next" will bring us to the second page where f1 lies.

+ +

+ +

Now, through the green tabs, you may browse the 2 pages for any Zzz instance. Clicking into the tab will display the consult view, while clicking on the pen will bring the edit view. Going from one edit page to the other can also be done through "next" and "previous" buttons.

+ +

Beyond validation of specific fields, gen also allows to perform global, "inter-field" validation. More information here.

+ +

Strings

+ +

Strings have an additional attribute named format which may take the following values:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
valuedefault?exampleresult (edit view)result (consult view)
String.LINEyesoneLineString = String()
String.TEXTnotextString = String(format=String.TEXT)
String.XHTMLnotextXhtml = String(format=String.XHTML)
+ +

With Strings, adequate use of arguments validator and multiplicity may produce more widgets and/or behaviours. Consider the following class:

+ +

+ class SeveralStrings:
+   root=True
+   anEmail = String(validator=String.EMAIL)
+   anUrl = String(validator=String.URL)
+   anAlphanumericValue = String(validator=String.ALPHANUMERIC)
+   aSingleSelectedValue = String(validator=['valueA', 'valueB', 'valueC'])
+   aSingleMandatorySelectedValue = String(
+     validator=['valueX', 'valueY', 'valueZ'], multiplicity=(1,1))
+   aMultipleSelectedValue = String(
+     validator=['valueS', 'valueT', 'valueU', 'valueV'],
+     multiplicity=(1,None), searchable=True)
+

+ +

The edit view generated from this class looks like this:

+ +

+ +

For field anEmail, a valid email was entered so the validation machinery does not complain. For anUrl and anAlphanumericValue (the 2 other predefined regular expressions for validating String fields shipped with gen) an error message is generated. The field aSingleSelectedValue uses a list of strings as validator and maximum multiplicity is 1: the generated widget is a listbox where a single value may be selected. The field aSingleMandatorySelectedValue is mandatory because of the multiplicity parameter being (1,1); a validation error is generated because no value was selected. The field aMultipleSelectedValue does not limit the maximum number of chosen values (multiplicity=(1,None)): the widget is a listbox where several values may be selected.

+ +

Field aMultipleSelectedValue has also been specified as searchable. Suppose we have created this instance of SeveralStrings:

+ +

+ +

When using the Plone global search in the right top corner, you will notice that entering "Value u" will produce a match for our instance:

+ +

+ +

Entering "Value x" for example will produce no match at all because aSingleMandatorySelectedValue was not specified as searchable. Note that title fields are automatically set as searchable.

+ +

Booleans

+ +

Booleans have no additional parameters. Specifying aBooleanValue = Boolean(default=True) will produce this on the edit view:

+ +

+ +

Dates

+ +

Dates have an additional attribute named format which may take the following values:

+ + + + + + + + + + + + + + + + + + + + + + + +
valuedefault?exampleresult (edit view)result (consult view)
Date.WITH_HOURyesdateWithHour = Date()
Date.WITHOUT_HOURnodateWithoutHour = Date(format=Date.WITHOUT_HOUR)
+ +

When editing a Date in any format, instead of using the listboxes for selecting values for year, month and day, you may click on the icon with a "12" on it: a nice Date chooser written in Javascript will pop up as shown above.

+ +

Files

+ +

When specifying this: anAttachedFile = File() you get this result on the edit view when an object is being created:

+ +

+ +

("Parcourir" means "Browse" in french). You get this result on the consult view:

+ +

+ +

If you want to edit the corresponding object, you will get this:

+ +

+ +

Any kind of file may be uploaded in File fields, but for png, jpg and gif files, you may specify an additional parameter isImage=True and your gen-ified Plone will render the image. Let's define this field:

+ +

anAttachedImage = File(isImage=True)

+ +

The consult view will render the image like on this example:

+ +

+ +

On the edit view, the widget that allows to modify the image will look like this:

+ +

+ +

References

+ +

References allow to specify associations between classes in order to build webs of interrelated objects. Suppose you want to implement this association:

+ +

+ +

The corresponding gen model looks like this:

+ +

+ class Order:
+   description = String(format=String.TEXT)
+
+ class Client:
+   root = True
+   title = String(show=False)
+   firstName = String()
+   name = String()
+   orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
+     back=Ref(attribute='client'))
+   def onEdit(self, created):
+     self.title = self.firstName + ' ' + self.name
+

+ +

Such an association is expressed in gen by 2 "crossed" Ref instances (see definition of attribute orders): +

    +
  • the attribute named orders specifies the "main" or "forward" reference;
  • +
  • the attribute named client specifies the "secondary" or "backward" reference.
  • +
+

+ +

As implemented in gen, an association is always dyssymmetric: one association end is more "important" than the other and provides some functionalities like adding or linking objects. In the above example, the association end named orders allows to create and add new Order instances (add=True). In the generated product, once you have created a Client instance, for field orders you will get the following consult view:

+ +

+ +

Remember that you can't get rid of the title field. So one elegant solution is to specify it as invisible (show=False) and compute it from other fields every time an object is created or updated (special method onEdit: when an object was just created, parameter created is True; when an object was just modified, created is False). Here, in both cases, we update the value of field title with firstName + ' ' + name.

+ +

On this view, because you specified add=True for field orders, the corresponding widget displays a "plus" icon for creating new Order instances and link them to you this client. Clicking on it will bring you to the edit view for Order:

+ +

+ +

Saving the order brings you to the consult view for this order:

+ +

+ +

On this view, a specific widget was added (it corresponds to backward reference client) that allows you to walk to the linked object. Clicking on "Gaetan Delannay" will bring you back to him. After repeating this process several times, you will get a result that will look like this:

+ +

+ +

If you had specified multiplicity=(0,4) for field orders, the "plus" icon would have disappeared, preventing you from creating an invalid fifth order. Unlike standard Plone references, gen Ref fields are ordered; the arrows allow you to move them up or down. The other icons allow you to edit and delete them.

+ +

Besides the "add" functionality, association ends may also provide the "link" functionality. This produces another widget that allows you to link an object to existing ones instead of creating + linking them. Suppose you extend you model with the concept of Product: an order may now specify one or several products. The model would include the new Product class and the Order class would get an additional Ref field:

+ +

+ class Product:
+   root = True
+   description = String(format=String.TEXT)
+
+ class Order:
+   description = String(format=String.TEXT)
+   products = Ref(Product, add=False, link=True, multiplicity=(1,None),
+     back=Ref(attribute='orders'))
+

+ +

Tip: when making changes to you model, re-generate it, relaunch Zope, go to "site setup"-> Add/Remove Products" and reinstall the generated product. Another tip: any change you make to your Python code needs a Zope restart; else it will not be taken into account.

+ +

So now you are able to create products. Because you specified class Product with root=True, on the main dashboard for you application you get a new tab that allows you to consult and create products. After some product creations you will get a setting like this:

+ +

+ +

Now, if you go back to the first order made by Gaetan Delannay, and go to the edit view by clicking on the pen, a new widget will allow to select which products are concerned by this order:

+ +

+ +

Clicking on "save" will bring you back to the consult view for this order:

+ +

+ +

What is a bit annoying for the moment is that the Ref widget configured with add=True is rendered only on the consult view, while the Ref widget configured with link=True behaves "normally" and renders on both edit and consult views. This is a technical limitation; we will try to improve this in the near future. Another improvement will be to be able to select both add=True and link=True (this is not possible right now).

+ +

You will also notice that when defining an association, both Ref instances are defined in one place (=at the forward reference, like in products = Ref(Product, add=False, link=True, multiplicity=(1,None),back=Ref(attribute='orders'))). The main reason for this choice is to be able in the future to link gen-classes with external, standard Plone content types. The name of the backward reference is given in the attribute parameter of the backward Ref instance. For example, from a Product instance p you may get all orders related to it by typing p.orders. The consult view uses this feature and displays it:

+ +

+ +

Rendering of backward references is currently less polished than forward references. If, for any reason, you don't want a backward reference to be visible, you can simply configure it like any other widget: back=Ref(attribute='orders', show=False)

+ +

Until now, all examples are done using the "admin" user. So for example all actions that one may trigger on objects (edit, delete, change order of references, etc) are enabled. We will present the security model that underlies Plone and gen later on; then we will be able to configure security.

+ +

For references, 2 more parameters allow to customize the way they are rendered: the boolean showHeaders and shownInfo. Let's consider again the consult view for a client (5 pictures above: gold client "Gaetan Delannay"). Beyond order's titles, I would like to display their description, too, in another column. But If I have several columns, it would be nice to get some columns headers. You may achieve the desired result by changing the definition of the field orders this way:

+ +

+   orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
+     back=Ref(attribute='client'), showHeaders=True,
+     shownInfo=('description',))
+

+ +

showHeaders simply tells gen to display or not headers for the table; shownInfo specifies (in a list or tuple) names of fields to display for the referenced objects. By default, field title is always displayed; you don't have to specify it in shownInfo. Here's the result:

+ +

+ +

The shownInfo parameter may also be used with Refs specifying link=True. For Ref field products of class Order, specifying shownInfo=('description',) will produce this, when creating a new Order:

+ +

+ +

The title/name of the referred object always appears; here, the description also appears. If you want the title to appear at a different place, simply specify it in the shownInfo parameter. For example, specifying shownInfo=('description','title') will produce:

+ +

+ +

By adding parameter wide=True, Ref tables take all available space. Returning to the previous example, specifying this parameter will produce this:

+ +

+ +

When using shownInfo, you may specify any field name, including Ref fields. If you specify shownInfo=('description', 'products') for the field orders of class Client and modify rendering of field products from class Order this way:

+ +

+ class Order:
+   description = String(format=String.TEXT)
+   products = Ref(Product, add=False, link=True, multiplicity=(1,None),
+     back=Ref(attribute='orders'), showHeaders=True,
+     shownInfo=('description',)) +

+ +

You will get this result:

+ +

+ +

If, for field products, add=True was specified, on this screen you would have been able to add directly new products to orders through specific "plus" icons:

+ +

+ +

Let's consider again attribute products of class Order (with add=False and link=True). When specifying this, gen allows every Order to be associated with any Product defined in the whole Plone site (in this case, Product A, Product B and Product C):

+ +

+ +

You may want to filter only some products instead of gathering all defined products. The select parameter may be used for this. Here is an example:

+ +

+ class Order:
+   description = String(format=String.TEXT)
+   def filterProducts(self, allProducts):
+     return [f for f in allProducts if f.description.find('Descr') != -1]
+   products = Ref(Product, add=False, link=True, multiplicity=(1,None),
+     back=Ref(attribute='orders'), showHeaders=True,
+     shownInfo=('description',), select=filterProducts)
+

+ +

This silly example only selects products whose description contains the word "Descr", which is only the case for Products Product B and Product C. So the "Products" widget will not contain Product A anymore:

+ +

+ +

The use of the select attribute may cause performance problems for large numbers of objects; an alternative attribute may appear in the future.

+ +

Computed fields

+ +

If you want to define a field whose value is not hardcoded in the database, but depends on some computation, then you must use a Computed field. Computed fields have two main purposes:

+ +
    +
  • displaying fields whose values are computed from other fields or other data (like the "reference" of an item, that includes some elements like a category's acronym, a year, etc);
  • +
  • producing nice representations of a field (for example, the computation may produce a graphical representation of the field value).
  • +
+ +

Because computed fields, like any other field, may be displayed on dashboards, it allows you to make the latters even more appealing! (please note how good I am at marketing gen)

+ +

Let's try it on our example. Suppose we want to produce nice references for orders, based on some random number (yes, it would have been better to use some incremental number: it it really easy to do this with gen, but you need to know how to customize the configuration panel, which is explained later). We need to define a field number that will hold the order number and will be invisible. Then, we will define a Computed field named reference that will produce the reference based on some prefix and the order number. Class Order need to be updated like this:

+ +

+ class Order:
+   ...
+   number = Float(show=False)
+   # Reference field
+   def getReference(self): return 'OR-%f' % self.number
+   reference = Computed(method=getReference)
+   ...
+   def onEdit(self, created):
+     if created:
+       import random
+       self.number = random.random()
+

+ +

Method onEdit is used to generate the order number when the order is created. The reference field is a Computed field: parameter method specifies the Python method that will compute the field value. In this case, this value is simply the order number with some prefix. Now, let's create this order and see what happens:

+ +

+ +

Computed fields do not appear on edit views, only on consult views. Clicking on "Save" will bring you to the following consult view:

+ +

+ +

Like any other field, Computed fields may appear on Ref fields or on dashboards. For example, if we change the definition of Ref field orders on class Client this way:

+ +

+ class Client:
+   ...
+   orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
+     back=Ref(attribute='client'), showHeaders=True,
+     shownInfo=('reference', 'description', 'products'), wide=True)
+

+ +

order references will appear on the corresponding consult view:

+ +

+ +

Python methods specified in attribute with may return HTML code.

+ +

Actions

+ +

Actions are special fields that allow to trigger functions. For the moment, they are represented as buttons and are shown only on consult views (not on edit views). Let's take an example. Suppose we modify class Product this way:

+ +

+ class Product:
+   root = True
+   description = String(format=String.TEXT)
+   stock = Integer()
+   def needOrder(self): return self.stock < 3
+   def orderProduct(self): self.stock = 3
+   order = Action(action=orderProduct, show=needOrder)
+

+ +

Firstly, we have added attribute stock, that allows us to know how many Product items we have in stock. Then, we have added a dummy action named order that allows us to re-order a product when the stock is too low. Suppose we have defined this product:

+ +

+ +

Because the stock is lower than 3, the order action (every action is defined as an instance of appy.gen.Action) is visible (because of parameter show of action order). The triggered behaviour is specified by a Python method given in parameter action. In this silly example, the action as the direct effect of setting stock to value 3. Clicking on button "order" will have this effect:

+ +

+ +

stock is equal to 3; the order action is not visible anymore (because the method specified in parameter show returns False.

+ +

Considering actions as "fields" is quite different from other frameworks or standard Plone. This has several advantages:

+ +
    +
  • You may reuse the security machinery linked to fields;
  • +
  • You may render them where you want, on any page/group, on Ref fields, or on dashboards.
  • +
+ +

Note that the action parameter may hold a list/tuple of Python methods instead of a single method (like in the previous example).

+ +

In the example, you've seen that a standard message was rendered: "The action has been successfully executed.". If you want to change this message, please read the section on i18n first. In fact, for every action field, gen generates 2 i18n labels: [full_class_name]_[field_name]_action_ok and [full_class_name]_[field_name]_action_ko. The first is rendered when the action succeeds; the second one is rendered when the action fails. The action succeeds when the Python method given in the action parameter:

+ +
    +
  • returns nothing (or None) (it was the case in the example);
  • +
  • returns the boolean value True or any Python equivalent.
  • +
+ +

The action fails when the Python method returns False or if it raises an exception. In this latter case, the exception message is part of the rendered message.

+ +

If the action parameter specifies several Python methods, the action succeeds if all Python methods succeed.

+ +

If you need to render different messages under different circumstances, the 2 labels generated by gen may not be sufficient. This is why a Python method specified in an action parameter may return a 2-tuple instead of None, True or False. The first element of this tuple determines if the method succeeded or not (True or False); the second element is a string containing the specific message to render. If this latter must be i18n'ed, you can create your own i18n label and use the method translate as described here (near the end of the section). In the case of multiple Python methods, messages returned are concatenated.

+ +

The installation procedure for your gen-application is defined as an action. More information about this here.

+ +

In future gen releases, you will be able to define an icon as an alternative way to render the action. We will also add the concept of action parameters.

+ +

Some thoughts about how gen-controlled objects are stored

+ +

Remember that the ZODB is a kind of folder hierarchy; the Plone site itself is a "folderish" object within that hierarchy. For every gen-application, a folder is created within the Plone site object. All objects created through this application (with the exception of objects tied to the application "configuration", more info here) will be created within this folder.

+ +

+ +

This screenshot shows the ZMI (Zope Management Interface) available at http://localhost:8080/manage. You see that within the Appy object (which is a Plone site) you have, among some predefined Plone objects like MailHost or Members, 2 folders named ZopeComponent and Zzz. Each of these 2 folders correspond to a gen-application that has the same name. Those folders are an Appy adaptation of the Plone standard content type named "Large Plone Folder", which is used for storing a large number of objects. If you click on this folder you will see its content (=all objects created through the corresponding gen-application). For several reasons, you may want to put more structure among this folder. Firstly, if you reach a large number of objects, it could lead to performance problems. Secondly, if you use the standard Plone "navigation" portlet, you will see in it all your objects in a single long and unstructured list under a single folder entry named according to your application. The solution is to tell gen that some classes are "folderish". You simply tell this by specifying folder=True on your class. Suppose you do this on class Client:

+ +

+ class Client:
+   root = True
+   folder = True
+   title = String(show=False)
+   firstName = String()
+   ... +

+ +

If now I start from a database with 3 products and 1 client and I add a new order I will get this:

+ +

+ +

You see in the "navigation" portlet on the left that "Gaetan Delannay" is now a folder that "contains" the order "First order".

+ +

Note that instances of classes tagged with root=True will always be created in the root application folder.

+ + + diff --git a/doc/genSecurityAndWorkflows.html b/doc/genSecurityAndWorkflows.html new file mode 100755 index 0000000..1c4dc24 --- /dev/null +++ b/doc/genSecurityAndWorkflows.html @@ -0,0 +1,371 @@ + + + <b>gen</b> - Security and workflows + + + +

The principles

+ +

The security model behinds gen-applications is similar to what Zope and Plone offer; simply, gen tries to simplify the way to declare and manage security within your applications. According to this model, users are put into groups; groups have some roles; roles are granted basic permissions on objects (create, read, write, delete, etc). Permissions-to-roles mappings may vary according to the state of objects.

+ +

Yes! We are done with the principles

+ +

In this chapter, we will use the ZopeComponent example, first introduced here and refined here. Our company developing Zope 3 components hires some lucky managers: they understand almost nothing to Zope but they are well paid. Then, there are some project leaders, still lucky and incompetent. Finally, one or two guys are Zope/Python developers.

+ +

According to the principles introduced above, we will begin by creating some users. Although the standard Plone interface for managing users, groups and roles is not perfect, gen has not (yet?) re-worked it; we will then use it now. In a standard Plone site, users register themselves. We need to change this setting in order to create users ourselves. Log in to Plone as administrator, go to "Site setup" and click on "Portal settings" and configure the "password policy" this way:

+ +

+ +

Now, go to "Users and Groups Administration" (still in "Site setup") and add the following users using the button "add new user" (do not check "Send a mail with the password" and enter a dummy email):

+ + + + + + + + + + +
User NamePassword
sydney + sydney +
ludovic + ludovic +
+ +

Now, we need groups. Guess what? We will not create groups. Why? Because gen will generate groups automatically for you!

+ +

Now that we have users and groups, it is time to create roles. Guess what? We will not do it. Why? Because it is simply not needed. gen will scan your code and find every role you mention and will create them automatically at the Zope/Plone level if they do not exist yet. We will use the following roles:

+ + + + + + + + + + + + + + + + + + +
role namedescription
ZManagerManager in our company that creates Zope components
ZLeaderProject leader in our company
ZDeveloperZope/Python developer
+ +

gen will create one group for every role defined in your application; the group will be granted only the corresponding role. Note that we will probably not use the role ZDeveloper. Indeed, developers work. They will probably not use a management tool. Now, let's tackle permissions. Again, it is not needed to create permissions (at least now): gen provides the following default permissions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
namecorresponding code objectdescription
create-Permission to create an object
readappy.gen.rPermission to access/view the content (=field values) of an object
writeappy.gen.wPermission to edit/modify the content (=field values) of an object
deleteappy.gen.dPermission to delete an object
+ +

All the security ingredients are now ready (users, groups, roles and permissions): we will now see how to use them to define security on a gen-application.

+ +

Managing the create permission

+ +

Permission to create objects is done at 2 levels. First, you may define a global list of roles that will, by default, be allowed to create any object of any class in your gen-application. In our company, ZLeaders are responsible for creating Zope components. You declare this global list in attribute defaultCreators of your appy.gen.Config instance introduced while presenting i18n:

+ +

+ c = Config()
+ c.languages = ('en', 'fr')
+ c.defaultCreators += ['ZLeader']
+

+ +

Why do I write += and not = ? Because the defaultCreators attribute is already initialised with this list of default Plone roles: ['Manager', 'Owner']. Manager is the role granted to any Plone/Zope administrator (like the admin user we have used in our examples so far); Owner is a special role that is granted to the user that created a given object.

+ +

Defining default creator roles for every class of your application may not be subtle enough. This is why gen allows you do it per class, with static attribute creators. For example, you may use this attribute on class ZopeComponent:

+ +

+ class ZopeComponent:
+   ...
+   creators = c.defaultCreators + ['ZLeader']
+

+ +

With this piece of code, Managers and ZLeaders will be able to create ZopeComponents; only Managers will be able to create instances of other classes in your application (provided no specific creators attribute is defined on them). Note that the creators attribute infringes the classical rules of class inheritance: If you have non abstract classes A and B(A), defining attribute creators on A will have absolutely no effect on B. + +

Managing all other permissions: defining workflows

+ +

For granting all other permissions (like read, write and delete, in short r, w, d), we will not use the same approach as for the create permission. Indeed, the permissions-to-roles mapping for a given object may depend on its state. For example, at some point in the object's life, we would like some users to be able to edit it; after a while (once the object has been validated by a manager, for example), we would like to prevent further modifications (at least for persons having certain roles). This is why we will use the concept of workflow as provided by Zope and Plone. This concept is simple: for a given gen-class, you may define several states (like "created", "validated", "ongoing" or whatever you want); for every state, you define a permissions-to-role mapping (while an object is in this state, what roles are granted what permissions on that object?). Finally, you need to decide what will be the initial state of the object and what are the valid state changes (= transitions).

+ +

Workflows are defined on a per-class basis. At present, if you don't define any workflow for one of your gen-classes, a default workflow provided by Plone will be used. As Plone is not really made for building web applications, this workflow will probably not be relevant for your class (it is a workflow for publishing web pages on a collaborative web site, with states like "under creation", "under validation" or "published"). In future gen releases, I will probably add an attribute defaultWorkflow in the Config instance and even provide some kind of web-application-minded default workflow (with some states like "active" and "inactive"). Hey I realize that it is useful to write documentation! It forces you to explore in a systematic way every aspect of the thing you have developed! Is it the birth of a new quality paradigm? Beuaaahhrk: I have written the word "quality".

+ +

So let's define a simple workflow for our class ZopeComponent. Until now our class looks like this:

+ +

+ class ZopeComponent:
+   root = True
+   def showDate(self):
+     return True
+   def validateDescription(self, value):
+     res = True
+     if value.find('simple') != -1:
+       res = self.translate('zope_3_is_not_simple')
+     return res
+   description = String(editDefault=True)
+   technicalDescription = String(format=String.XHTML,
+     validator=validateDescription)
+   status = String(validator=['underDevelopement', 'stillSomeWorkToPerform',
+     'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient'],
+     optional=True, editDefault=True)
+   funeralDate = Date(optional=True)
+   responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,
+     link=True, back=Ref(attribute='components'))
+

+ +

Field status seems to be a kind of workflow embryo. So we will remove it and create a workflow whose states will look like values of this field:

+ +

+ class ZopeComponentWorkflow:
+   # Roles
+   zManager = 'ZManager'
+   zLeader = 'ZLeader'
+   managerM = (zManager, 'Manager')
+   leaderM = (zLeader, 'Manager')
+   everybody = (zManager, zLeader, 'Manager')
+   # States
+   created = State({r:leaderM, w:leaderM, d:leaderM}, initial=True)
+   validated = State({r:everybody, w:everybody, d:None})
+   underDevelopment = State({r:everybody, w:leaderM, d:None})
+   whereIsTheClient = State({r:everybody, w:managerM, d:None})
+   # Transitions
+   validate = Transition( (created, validated), condition=managerM )
+   startDevelopment = Transition( (validated, underDevelopment),
+     condition=leaderM)
+   cancelDevelopment = Transition( (underDevelopment, whereIsTheClient),
+     condition=managerM)
+   cancel = Transition( ( (whereIsTheClient, underDevelopment),
+     (underDevelopment, validated),
+     (validated, created)), condition='Manager')
+
+ class ZopeComponent:
+   ...
+   workflow = ZopeComponentWorkflow
+   ...
+

+ +

21 lines of code for the workflow ! (including 3 lines of comments and several lines splitted because of this silly 80-characters-length constraint). Sorry, the states do not correspond exactly to the values of the removed status field; this is because I felt myself guilty about being so ironic.

+ +

Like gen-classes, gen-workflows do not inherit from any base class provided by gen. Simply, static fields are instances of classes provided by gen like appy.gen.State and appy.gen.Transition. gen will decide if your class is a gen-class or a gen-workflow by analysing its static attributes. So please avoid creating hybrid classes mixing field definitions (String, Ref, etc) and workflow definitions (State, Transition, etc).

+ +

As shown in the last lines of the example, associating a gen-workflow to a gen-class is done through the workflow attribute of a gen-class. The same workflow may be associated to different gen-classes. A gen-class defining no workflow inherits from a potential workflow association defined on a parent.

+ +

Let's analyse the workflow in itself. We begin by putting some roles in variables. It is not really necessary (this is not a role "registration" at all); I do it in order to avoid writing syntax errors within role names because it would lead to the creation of silly roles.

+ +

Then, we have the definitions of states. The first paramater is the permissions-to-roles mapping, that indicates, for every permission defined on the associated class, what role(s) have the permission. This parameter is a dictionary whose keys are permissions (remember that r, w, and d correspond to read, write and delete permissions; I can use them as is because of the clause from appy.gen import *) and whose values are, as suggested by the example, either a tuple/list of roles, a single role, or None. For example, when the component is underDevelopment, only project leaders (and administrators) may modify them; when it is in state whereIsTheClient, only managers (and administrators) may edit them. As soon as a component is validated, nobody may delete it: permission d is granted to None (=nobody). The parameter initial=True indicates that the first state is the one the object gets as soon as it is created. Avoid specifying this for more than one state.

+ +

Definitions of transitions are based on state definitions. Indeed, when defining a transition, the first parameter is a 2-tuple (startState, endState). So a transition is simply a specified way to go to from one state to the other. Additional parameter condition specifies under what circumstances the transition may be "triggered". In the example, only persons having roles Manager or ZManager are allowed to trigger transition validate, that will change object state from created to validated. It is also possible to define multi-transitions, which are transitions having multiple 2-tuples (startState, endState) (grouped in one big tuple) like transition cancel. Multi-transitions may be seen as a shortcut that allows you to write several similar transitions in only one. In the example, cancel transitions are used to "go backward", if a user triggered a transition by error.

+ +

Such a workflow is called a state machine. The following diagram represents the workflow defined above.

+ +

+ +

Other frameworks allow you to define your workflows this way, with tools like ArgoUML. This is the case for ArchGenXML for example. I have been a ArchGenXML user for 2 years, and this is why I decided to create a code-based approach for defining workflows in gen. Why? As already mentioned, working with a UML model gives you an additional dependency (towards a tool and a format), prevents collaborative work, cut & paste or more powerful subtleties like multi-transitions or workflow inheritance (see below). Moreover, a model is (when you compare it with code) a much poorer way to describe things. It abstracts a lot of "details", that you are forced to add in an unnatural way (like defining permissions-to-roles mappings in UML tagged values that you can't represent on the diagram), or, worse, that you can't simply put in the model (like the actions triggered by the workflow or specific conditions that you define with Python methods, like explained hereafter). The conclusion is: when using a model approach, you are always forced to complete it with a code approach (this is what happens typically with ArchGenXML: specific actions and conditions are written in additional Python scripts. It implies spreading and duplicating information about the workflow, augmenting complexity and the maintainability effort. That said, diagrams may represent a good way to communicate your ideas. This is why we plan to integrate in future gen releases the possibility to generate diagrams from gen-workflows and gen-classes.

+ +

Granting roles

+ +

In order to see our workflow at work, we need to perform a last action: granting roles to our users. Because gen managed automatically groups, roles, and their links, the only action we need to perform is to put sidney and ludovic in the right groups.

+ +

Re-generate your product, restart Zope, go to "Site setup", re-install your Plone product, go to "Site setup" -> "Users and Groups Administration" and click on tab "groups". You will get this screen:

+ +

+ +

Groups "Administrators" and "Reviewers" are default Plone groups. Your gen-application has added groups "ZManager_group" and "ZLeader_group": each one has the corresponding role. Click on "ZManager_group": there is nobody in it. Click on "show all": Ludovic and Sidney appear. Check the checkbox besides Sydney and add her to the group. In a similar way, add Ludovic to group "ZLeader_group".

+ +

We will first walk through the application as user admin, as usual. According to the workflow, admin, as Manager, is God: he can do everything. Besides this pleasant feeling, it will allow us to trigger all workflow transitions.

+ +

Because role Manager may add ZopeComponent instances (thanks to Config.defaultCreators), on the dashboard, the "plus" icon is available in tab "Zope component". Create a new Zope component: the consult view will look like this:

+ +

+ +

Besides the component title, its state appears (here: "Created"). According to the workflow, the only possible transition to trigger from this state is validate; as Manager I have the right to trigger it, so the corresponding button appears on the bottom of the page. Please enter a nice comment in the field and click on button "validate": the component will go in state validated as dictated by the workflow. The consult view has now evolved accordingly:

+ +

+ +

Component state is now "Validated". I have expanded the plus icon "History": all workflow actions triggered on the component appear in a table, with the (optional) comments entered by the triggering user(s). Again, according to the workflow, 2 actions may now be triggered, and I have te rights to trigger both: 2 new buttons appear... I guess you understand now how the workflow works: try now by yourself, walk through the state machine by triggering available actions and see how the history evolves.

+ +

The following screenshot shows how the dashboard may evolve according to permissions:

+ +

+ +

Because the workflow says that nobody may delete Zope components once they are validated, the delete icon is not available for component named "New component". By the way, you can display the workflow state in the dashboard: go to the corresponding flavour, click on tab "user interface" and, for class ZopeComponent, select "workflow state" in field "Columns to display while showing query results".

+ +

Now, please log out (a link is available in the top-right corner, within the blue strip) and log in as ludovic. Because ZLeaders are among default creators, as Ludovic we may create a new Zope component. If you do so, you will then get a consult view like this one:

+ +

+ +

No workflow action is shown because Ludovic has not the right to validate the component. Reconnect now as Sidney. First of all, let's view the dashboard as it is shown to her:

+ +

+ +

Sidney is not among ZopeComponent creators, so the "plus" icon is not shown in the corresponding tab. Moreover, according to the workflow, she does not have the right to modify components in state "Created": the "pen" icon is not available for component "Aaaa". But if you go to the consult view for this component, Sidney will be able to validate it:

+ +

+ +

We have now finished our first security tour. An important remark is that we have granted roles "globally" to groups: any user of the group has always the globally granted role, under all circumstances, on any related object in your gen-application. In our example, Ludovic and all potential other project leaders have the right to edit all created components. This may not be the desired behaviour. Maybe would you prefer any project leader to be able to edit his own components but not components created by other project leaders. This is where "local roles" come into play. A local role is a role that a user or group has, but only on a given object. The default Plone role "Owner" is an example of local role: this is not a role that you grant "globally" to a user or group (like the ones shown in tab "groups" or "users" of "Site setup -> Users and Groups Administration"); this is a role that is granted on an object to the user that created it. You may of course reference local roles within gen-workflows. For example, if you want to restrict component modifications to Owners and Managers when the component is created, you may modify the workflow state created like this:

+ +

created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM}, initial=True) +

+ +

Re-generate your product and re-install it. The Plone procedure for re-installing a product updates the workflow definition but does not update the permissions-to-roles mappings defined on existing objects. In order to synchronize them with the new workflow definition, you need to go, through the ZMI, in object "portal_workflow" within your Plone site. At the bottom of the page, you will find a button "Update security settings". Click on it. This may take a long time if you have a large number of objects in your database. In future gen releases, you will be able to re-install your product directly from your tool. This specific procedure will ask you if you want to "Update workflow settings on existing objects" or not.

+ +

Now, log in as Ludovic. Consider the following dashboard as seen by him:

+ +

+ +

Components "ZC1" and "Aaaa" were created by admin: Ludovic may not edit them. He can only edit the one he has created itself (= the last one in the table).

+ +

In future gen releases, you will be able to define and manage "custom" local roles.

+ +

Conditions and actions linked to transitions

+ +

Until now, we have seen that, as transition condition, you can specify role(s) (one, as a string, or a tuple of roles). You can also specify Python method(s) the same way, and even mix roles and Python methods. Specified Python method(s) must belong to your gen-workflow (or one of its parents, yes, we will soon talk about workflow inheritance!). With such methods, more complex conditions may be defined. Let's show it by refining our previous example. Suppose that components can be validated only if a funeral date (which is not a mandatory field) has been specified. Transition validate need to evolve:

+ +

+ class ZopeComponentWorkflow:
+   ...
+   def funeralOk(self, obj): return obj.funeralDate
+   validate = Transition( (created, validated), condition=managerM + (funeralOk,))
+   ...
+

+ +

It means that beyond having one of the roles defined in managerM, method funeralOk must also return True (or an equivalent value) as prerequisite for triggering transition validate. This kind of method takes a single parameter: the related object. In short: a transition may be triggered if the user has at least one of the roles specified and all specified methods return True. So gen computes an or-operator on roles and an and-operator on methods.

+ +

One may also define action(s) (as Python method(s)) that are executed after any transition has been triggered. Let's suppose we want to reinitialise the component description when we start its development. This is completely silly of course. But I like to force you doing silly things, it is a pleasant feeling. So let's update transition startDevelopment:

+ +

+ class ZopeComponentWorkflow:
+   ...
+   def updateDescription(self, obj):
+     obj.description = 'Description edited by my manager was silly.'
+   startDevelopment = Transition( (validated, underDevelopment),
+     condition=leaderM, action=updateDescription)
+   ...
+

+ +

We have specified a Python method in a new parameter named action. Now, try to click on button "startDevelopment" and you will see the description changing. As for conditions, actions need to be Python methods defined on the gen-workflow or one of its parents. Those methods take only one parameter: the related object. As already announced, the action parameter may also take a list or tuple of methods instead of a single method.

+ +

Specific field permissions

+ +

Until now, we have considered security as an homogeneous layer encompassing a whole gen-class: when someone may read or write objects of a gen-class, she may read or write any field on this object. In some cases, though, we may need to be more subtle, and define specific read or write permissions on individual fields. As already mentioned, this can be done at the time of field definition, with boolean parameters specificReadPermission and specificWritePermission. For every field for which you do not declare using a specific read or write permission, the gen-class-wide read or write permission will come into play for protecting it.

+ +

Let's try it on our class ZopeComponent. Suppose we need a specific write permission on field funeralDate and a specific read permission on field responsibleBunch:

+ +

+ class ZopeComponent:
+   ...
+   funeralDate = Date(optional=True, specificWritePermission=True)
+   responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,
+     link=True, back=Ref(attribute='components'),
+     specificReadPermission=True)
+   ... +

+ +

Now, in our workflow, for every state, we need to update the permissions-to-roles mapping by specifying the roles that will be granted those 2 new permissions. But first, we need a way to designate those permissions. This is done by using classes appy.gen.ReadPermission and appy.gen.WritePermission like this:

+ +

+ class ZopeComponentWorkflow:
+   # Specific permissions
+   wf = WritePermission('ZopeComponent.funeralDate')
+   rb = ReadPermission('ZopeComponent.responsibleBunch')
+   # Roles
+   ...
+

+ +

When constructing a WritePermission or ReadPermission instance, you give as parameter the "path name" of the field on which the corresponding specific permission was defined. Within this "path name", you find the name of the class where the field is defined (ZopeComponent in the example). If the workflow class and the field class are in the same package (like, in our case, ZopeComponentWorkflow and ZopeComponent), you can specify the "relative" class name of the field class (without prefixing it with the package name, ie ZopeComponent). Else, you need to specify the full package name of the class (ie ZopeComponent.ZopeComponent.funeralDate).

+ +

Now let's update every state definition by integrating those 2 permissions in the permissions-to-roles mappings:

+ +

+ class ZopeComponentWorkflow:
+   ...
+   # States
+   created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM, wf:'Owner', rb:everybody}, initial=True)
+   validated = State({r:everybody, w:everybody, d:None, wf:everybody, rb:everybody})
+   underDevelopment = State({r:everybody, w:leaderM, d:None, wf:leaderM, rb:everybody})
+   whereIsTheClient = State({r:everybody, w:managerM, d:None, wf:managerM, rb:everybody})
+   ...
+

+ +

Now, re-generate your product, restart Zope and re-install the product, update the security settings on portal_workflow and try, as admin, to edit a component that is in state created and was created by Ludovic. Because Managers have the right to modify components in this state, you will be able to get the edit view. But on this view, because you do not have the specific "edit" permission on field funeralDate (you are not the component Owner), the field will not show up:

+ +

+ +

Aaaaargh! The field is visible! Impossible! How can user admin bypass our security like this? This is the occasion to learn something about local roles: they propagate from a given object to its contained objects. Remember that Zope components, as root objects, are stored in a folder within the Plone site. This folder was created by the generated Plone product with the admin user: so admin has local role Owner on it (and, by the way, has local role Owner on the Plone site as well). It means that admin will have role Owner on all sub-objects of your Plone site. When you think about this, it is normal: admin is God (and you are admin).

+ +

In order to produce a working example, let's create a new user (let's call it gerard) and grant him role Manager. This way, we will get a Manager that is not Owner of all objects. Log in as gerard, and go the previous edit view:

+ +

+ +

Yes! You do not see (so you can't edit) field funeralDate. Consult views (or dashboards) will behave similarly with read permissions: fields for which the currently logged user have no read permission will be invisible. Note that if you don't have the whole-gen-class read (write) permission, and you have a read (write) permission on one of its fields, you will not be allowed to read (write) the specific field.

+ +

For the moment, for every state definition, you are forced to specify a permissions-to-roles mapping that includes all related permissions (class-wide and field-specific). In future gen releases, this will change. We will implement things like: if you don't specify roles for a specific read (write) field-permission, it will take the value from the corresponding read (write) class-wide permission; unspecified values may also be copied from the previous state definition, etc. This way, as usual, you will continue to be as lazy and concise as possible while writing gen-applications.

+ +

Workflow inheritance

+ +

With gen, workflows are Python classes. This allows us to benefit from class inheritance and apply it to workflows. Our company that creates Zope components is now used to heavy technologies. They got a business revelation: some managers discovered that COBOL and Zope 3 had a lot in common on both philosophical and technical points of view. So they decided to begin creating components in COBOL. They were so excited about it that they needed to update their management software as quickly as possible. So a new class was added for registering information about COBOL components. The associated workflow was almost similar to the existing ZopeComponentWorkflow; a new workflow inheriting from it was created:

+ +

+ class CobolComponentWorkflow(ZopeComponentWorkflow):
+   p = ZopeComponentWorkflow # Shortcut to workflow parent
+   # An additional state
+   finished = State(p.whereIsTheClient.permissions)
+   # Override validate: condition on funeralDate has no sense here
+   validate = Transition(p.validate.states, condition=p.managerM)
+   # Override cancelDevelopment: go to finished instead
+   cancelDevelopment = Transition( (p.underDevelopment, finished),
+     condition=p.managerM)
+   # Update cancel accordingly
+   cancel = Transition( ((finished, p.underDevelopment),) +p.cancel.states[1:],
+     condition=p.cancel.condition)
+
+ class CobolComponent:
+   root = True
+   workflow = CobolComponentWorkflow
+   description = String()
+

+ +

Basically, this new workflow "removes" state whereIsTheClient, creates a more optimistic end state finished and performs some surgical operations on transitions for reflecting navigation to and from the new state. For defining it, we reuse the permissions-to-roles mapping that was defined on state whereIsTheClient. Then, we have overridden transition validate because the condition that related to field funeralDate is not relevant anymore (COBOL components have no funeral date). Transition cancelDevelopment was also overridden: the end state is not whereIsTheClient anymore, but finished instead. We also need to override transition cancel for updating the tuple of (startState, endState).

+ +

And we are done! You may now test the result. As for classical inheritance, it is not really possible to remove elements in a child class. So state whereIsTheClient is still there, but unreachable because of our operations on transitions (so it is more or less the same as a deletion). Workflow inheritance ensures reuse and conciseness: any element that does not change from ZopeComponentWorkflow is kept in the child workflow; any change made in the reused part of the parent workflow will automatically impact the child workflow(s).

+ +

Workflows and i18n

+ +

As usual, for every workflow state and transition, i18n labels have been automatically generated (in the plone domain), together with a "nice" default value. The format of those labels is defined here. There is still a small problem with the CobolComponentWorkflow: the transition for finishing the work is called cancelDevelopment. I am too lazy for creating another transition, so I will simply modify here the translation of this transition in the corresponding i18n file (=ZopeComponent-plone-en.po in this case):

+ +

+ #. Default: "Cancel development"
+ msgid "zopecomponent_cobolcomponentworkflow_cancelDevelopment"
+ msgstr "Finish"
+

+ +

Note that i18n labels are "duplicated" for every child workflow. Here, I modify label zopecomponent_cobolcomponentworkflow_cancelDevelopment without perturbing parent label for the same transition which is zopecomponent_zopecomponentworkflow_cancelDevelopment.

+ + + diff --git a/doc/gossips.odt b/doc/gossips.odt new file mode 100755 index 0000000..d0d9a81 Binary files /dev/null and b/doc/gossips.odt differ diff --git a/doc/helloWorld.odt b/doc/helloWorld.odt new file mode 100755 index 0000000..2961146 Binary files /dev/null and b/doc/helloWorld.odt differ diff --git a/doc/img/ElseAmbiguous.png b/doc/img/ElseAmbiguous.png new file mode 100755 index 0000000..99b2fe9 Binary files /dev/null and b/doc/img/ElseAmbiguous.png differ diff --git a/doc/img/ElseAmbiguous.res.png b/doc/img/ElseAmbiguous.res.png new file mode 100755 index 0000000..8a90bcf Binary files /dev/null and b/doc/img/ElseAmbiguous.res.png differ diff --git a/doc/img/ElseNotAmbiguous.png b/doc/img/ElseNotAmbiguous.png new file mode 100755 index 0000000..497f7b8 Binary files /dev/null and b/doc/img/ElseNotAmbiguous.png differ diff --git a/doc/img/ElseNotAmbiguous.res.png b/doc/img/ElseNotAmbiguous.res.png new file mode 100755 index 0000000..3fdb162 Binary files /dev/null and b/doc/img/ElseNotAmbiguous.res.png differ diff --git a/doc/img/ElseStatements.png b/doc/img/ElseStatements.png new file mode 100755 index 0000000..621f148 Binary files /dev/null and b/doc/img/ElseStatements.png differ diff --git a/doc/img/ElseStatements.res.png b/doc/img/ElseStatements.res.png new file mode 100755 index 0000000..ae66ec1 Binary files /dev/null and b/doc/img/ElseStatements.res.png differ diff --git a/doc/img/ErrorExpression.png b/doc/img/ErrorExpression.png new file mode 100755 index 0000000..0456c72 Binary files /dev/null and b/doc/img/ErrorExpression.png differ diff --git a/doc/img/ErrorExpression.res.png b/doc/img/ErrorExpression.res.png new file mode 100755 index 0000000..7a51b10 Binary files /dev/null and b/doc/img/ErrorExpression.res.png differ diff --git a/doc/img/ErrorForParsetime.png b/doc/img/ErrorForParsetime.png new file mode 100755 index 0000000..77664b2 Binary files /dev/null and b/doc/img/ErrorForParsetime.png differ diff --git a/doc/img/ErrorForParsetime.res.png b/doc/img/ErrorForParsetime.res.png new file mode 100755 index 0000000..ac25424 Binary files /dev/null and b/doc/img/ErrorForParsetime.res.png differ diff --git a/doc/img/ErrorForRuntime.png b/doc/img/ErrorForRuntime.png new file mode 100755 index 0000000..c6ced14 Binary files /dev/null and b/doc/img/ErrorForRuntime.png differ diff --git a/doc/img/ErrorForRuntime.res.png b/doc/img/ErrorForRuntime.res.png new file mode 100755 index 0000000..c9e5519 Binary files /dev/null and b/doc/img/ErrorForRuntime.res.png differ diff --git a/doc/img/ErrorIf.png b/doc/img/ErrorIf.png new file mode 100755 index 0000000..ada79f4 Binary files /dev/null and b/doc/img/ErrorIf.png differ diff --git a/doc/img/ErrorIf.res.png b/doc/img/ErrorIf.res.png new file mode 100755 index 0000000..13acb82 Binary files /dev/null and b/doc/img/ErrorIf.res.png differ diff --git a/doc/img/ForCellNotEnough.png b/doc/img/ForCellNotEnough.png new file mode 100755 index 0000000..6b4128f Binary files /dev/null and b/doc/img/ForCellNotEnough.png differ diff --git a/doc/img/ForCellNotEnough.res.png b/doc/img/ForCellNotEnough.res.png new file mode 100755 index 0000000..0f81a76 Binary files /dev/null and b/doc/img/ForCellNotEnough.res.png differ diff --git a/doc/img/ForCellTooMuch2.png b/doc/img/ForCellTooMuch2.png new file mode 100755 index 0000000..9144722 Binary files /dev/null and b/doc/img/ForCellTooMuch2.png differ diff --git a/doc/img/ForCellTooMuch2.res.png b/doc/img/ForCellTooMuch2.res.png new file mode 100755 index 0000000..c81d7ce Binary files /dev/null and b/doc/img/ForCellTooMuch2.res.png differ diff --git a/doc/img/ForTableMinus.png b/doc/img/ForTableMinus.png new file mode 100755 index 0000000..dde05ed Binary files /dev/null and b/doc/img/ForTableMinus.png differ diff --git a/doc/img/ForTableMinus.res.png b/doc/img/ForTableMinus.res.png new file mode 100755 index 0000000..3ce83d6 Binary files /dev/null and b/doc/img/ForTableMinus.res.png differ diff --git a/doc/img/ForTableMinusError.png b/doc/img/ForTableMinusError.png new file mode 100755 index 0000000..2fd1d9d Binary files /dev/null and b/doc/img/ForTableMinusError.png differ diff --git a/doc/img/ForTableMinusError.res.png b/doc/img/ForTableMinusError.res.png new file mode 100755 index 0000000..2fa8339 Binary files /dev/null and b/doc/img/ForTableMinusError.res.png differ diff --git a/doc/img/FromWithFor.png b/doc/img/FromWithFor.png new file mode 100755 index 0000000..e20fa93 Binary files /dev/null and b/doc/img/FromWithFor.png differ diff --git a/doc/img/FromWithFor.res.png b/doc/img/FromWithFor.res.png new file mode 100755 index 0000000..7984b6b Binary files /dev/null and b/doc/img/FromWithFor.res.png differ diff --git a/doc/img/IfAndFors1.png b/doc/img/IfAndFors1.png new file mode 100755 index 0000000..0127422 Binary files /dev/null and b/doc/img/IfAndFors1.png differ diff --git a/doc/img/IfAndFors1.res.png b/doc/img/IfAndFors1.res.png new file mode 100755 index 0000000..7d40016 Binary files /dev/null and b/doc/img/IfAndFors1.res.png differ diff --git a/doc/img/IfExpression.png b/doc/img/IfExpression.png new file mode 100755 index 0000000..eb27ce4 Binary files /dev/null and b/doc/img/IfExpression.png differ diff --git a/doc/img/IfExpression.res.png b/doc/img/IfExpression.res.png new file mode 100755 index 0000000..3476132 Binary files /dev/null and b/doc/img/IfExpression.res.png differ diff --git a/doc/img/OnlyExpressions.png b/doc/img/OnlyExpressions.png new file mode 100755 index 0000000..9c3a82f Binary files /dev/null and b/doc/img/OnlyExpressions.png differ diff --git a/doc/img/OnlyExpressions.res.png b/doc/img/OnlyExpressions.res.png new file mode 100755 index 0000000..dafc4d2 Binary files /dev/null and b/doc/img/OnlyExpressions.res.png differ diff --git a/doc/img/SimpleFrom.png b/doc/img/SimpleFrom.png new file mode 100755 index 0000000..ce52bc7 Binary files /dev/null and b/doc/img/SimpleFrom.png differ diff --git a/doc/img/SimpleFrom.res.png b/doc/img/SimpleFrom.res.png new file mode 100755 index 0000000..6d24f17 Binary files /dev/null and b/doc/img/SimpleFrom.res.png differ diff --git a/doc/img/SimpleTest.png b/doc/img/SimpleTest.png new file mode 100755 index 0000000..d41c9a6 Binary files /dev/null and b/doc/img/SimpleTest.png differ diff --git a/doc/img/SimpleTest.res.png b/doc/img/SimpleTest.res.png new file mode 100755 index 0000000..d675807 Binary files /dev/null and b/doc/img/SimpleTest.res.png differ diff --git a/doc/img/actions1.png b/doc/img/actions1.png new file mode 100755 index 0000000..206fc96 Binary files /dev/null and b/doc/img/actions1.png differ diff --git a/doc/img/actions2.png b/doc/img/actions2.png new file mode 100755 index 0000000..e3d5da3 Binary files /dev/null and b/doc/img/actions2.png differ diff --git a/doc/img/advisory.png b/doc/img/advisory.png new file mode 100755 index 0000000..f11cb8d Binary files /dev/null and b/doc/img/advisory.png differ diff --git a/doc/img/booleans1.png b/doc/img/booleans1.png new file mode 100755 index 0000000..46ee8c4 Binary files /dev/null and b/doc/img/booleans1.png differ diff --git a/doc/img/builtinFunctionInPodExpression.png b/doc/img/builtinFunctionInPodExpression.png new file mode 100755 index 0000000..90f71e1 Binary files /dev/null and b/doc/img/builtinFunctionInPodExpression.png differ diff --git a/doc/img/builtinFunctionInPodExpression.res.png b/doc/img/builtinFunctionInPodExpression.res.png new file mode 100755 index 0000000..2e4b805 Binary files /dev/null and b/doc/img/builtinFunctionInPodExpression.res.png differ diff --git a/doc/img/computed1.png b/doc/img/computed1.png new file mode 100755 index 0000000..ef1a159 Binary files /dev/null and b/doc/img/computed1.png differ diff --git a/doc/img/computed2.png b/doc/img/computed2.png new file mode 100755 index 0000000..1bfc552 Binary files /dev/null and b/doc/img/computed2.png differ diff --git a/doc/img/computed3.png b/doc/img/computed3.png new file mode 100755 index 0000000..b2f1855 Binary files /dev/null and b/doc/img/computed3.png differ diff --git a/doc/img/contact.gif b/doc/img/contact.gif new file mode 100755 index 0000000..34cd0e8 Binary files /dev/null and b/doc/img/contact.gif differ diff --git a/doc/img/dates1.png b/doc/img/dates1.png new file mode 100755 index 0000000..cb9007f Binary files /dev/null and b/doc/img/dates1.png differ diff --git a/doc/img/dates2.png b/doc/img/dates2.png new file mode 100755 index 0000000..96d186d Binary files /dev/null and b/doc/img/dates2.png differ diff --git a/doc/img/dates3.png b/doc/img/dates3.png new file mode 100755 index 0000000..7a49506 Binary files /dev/null and b/doc/img/dates3.png differ diff --git a/doc/img/dates4.png b/doc/img/dates4.png new file mode 100755 index 0000000..49802ac Binary files /dev/null and b/doc/img/dates4.png differ diff --git a/doc/img/documentFunction1.png b/doc/img/documentFunction1.png new file mode 100755 index 0000000..50c1e98 Binary files /dev/null and b/doc/img/documentFunction1.png differ diff --git a/doc/img/documentFunction2.png b/doc/img/documentFunction2.png new file mode 100755 index 0000000..570588c Binary files /dev/null and b/doc/img/documentFunction2.png differ diff --git a/doc/img/documentFunction3.png b/doc/img/documentFunction3.png new file mode 100755 index 0000000..1a63522 Binary files /dev/null and b/doc/img/documentFunction3.png differ diff --git a/doc/img/download.gif b/doc/img/download.gif new file mode 100755 index 0000000..908efa9 Binary files /dev/null and b/doc/img/download.gif differ diff --git a/doc/img/emptyQuery.png b/doc/img/emptyQuery.png new file mode 100755 index 0000000..d70f5ab Binary files /dev/null and b/doc/img/emptyQuery.png differ diff --git a/doc/img/files1.png b/doc/img/files1.png new file mode 100755 index 0000000..c8f7160 Binary files /dev/null and b/doc/img/files1.png differ diff --git a/doc/img/files2.png b/doc/img/files2.png new file mode 100755 index 0000000..fd2d09e Binary files /dev/null and b/doc/img/files2.png differ diff --git a/doc/img/files3.png b/doc/img/files3.png new file mode 100755 index 0000000..c0b26b0 Binary files /dev/null and b/doc/img/files3.png differ diff --git a/doc/img/files4.png b/doc/img/files4.png new file mode 100755 index 0000000..cdd5ecf Binary files /dev/null and b/doc/img/files4.png differ diff --git a/doc/img/files5.png b/doc/img/files5.png new file mode 100755 index 0000000..eb7efc2 Binary files /dev/null and b/doc/img/files5.png differ diff --git a/doc/img/filledQuery.png b/doc/img/filledQuery.png new file mode 100755 index 0000000..a700bc6 Binary files /dev/null and b/doc/img/filledQuery.png differ diff --git a/doc/img/flavourOptions.png b/doc/img/flavourOptions.png new file mode 100755 index 0000000..037a6fb Binary files /dev/null and b/doc/img/flavourOptions.png differ diff --git a/doc/img/genpod1.png b/doc/img/genpod1.png new file mode 100755 index 0000000..b422d79 Binary files /dev/null and b/doc/img/genpod1.png differ diff --git a/doc/img/genpod10.png b/doc/img/genpod10.png new file mode 100755 index 0000000..5e31f20 Binary files /dev/null and b/doc/img/genpod10.png differ diff --git a/doc/img/genpod11.png b/doc/img/genpod11.png new file mode 100755 index 0000000..dd893bf Binary files /dev/null and b/doc/img/genpod11.png differ diff --git a/doc/img/genpod12.png b/doc/img/genpod12.png new file mode 100755 index 0000000..ff392db Binary files /dev/null and b/doc/img/genpod12.png differ diff --git a/doc/img/genpod13.png b/doc/img/genpod13.png new file mode 100755 index 0000000..12379b2 Binary files /dev/null and b/doc/img/genpod13.png differ diff --git a/doc/img/genpod14.png b/doc/img/genpod14.png new file mode 100755 index 0000000..e3943e6 Binary files /dev/null and b/doc/img/genpod14.png differ diff --git a/doc/img/genpod2.png b/doc/img/genpod2.png new file mode 100755 index 0000000..dc7cc15 Binary files /dev/null and b/doc/img/genpod2.png differ diff --git a/doc/img/genpod3.png b/doc/img/genpod3.png new file mode 100755 index 0000000..613cf79 Binary files /dev/null and b/doc/img/genpod3.png differ diff --git a/doc/img/genpod4.png b/doc/img/genpod4.png new file mode 100755 index 0000000..76e5252 Binary files /dev/null and b/doc/img/genpod4.png differ diff --git a/doc/img/genpod5.png b/doc/img/genpod5.png new file mode 100755 index 0000000..366690e Binary files /dev/null and b/doc/img/genpod5.png differ diff --git a/doc/img/genpod6.png b/doc/img/genpod6.png new file mode 100755 index 0000000..a7cf684 Binary files /dev/null and b/doc/img/genpod6.png differ diff --git a/doc/img/genpod7.png b/doc/img/genpod7.png new file mode 100755 index 0000000..f98e8b2 Binary files /dev/null and b/doc/img/genpod7.png differ diff --git a/doc/img/genpod8.png b/doc/img/genpod8.png new file mode 100755 index 0000000..f3a5d64 Binary files /dev/null and b/doc/img/genpod8.png differ diff --git a/doc/img/genpod9.png b/doc/img/genpod9.png new file mode 100755 index 0000000..daedd60 Binary files /dev/null and b/doc/img/genpod9.png differ diff --git a/doc/img/gnu.png b/doc/img/gnu.png new file mode 100755 index 0000000..a6c61ca Binary files /dev/null and b/doc/img/gnu.png differ diff --git a/doc/img/home.png b/doc/img/home.png new file mode 100755 index 0000000..5179421 Binary files /dev/null and b/doc/img/home.png differ diff --git a/doc/img/i18n1.png b/doc/img/i18n1.png new file mode 100755 index 0000000..32bb684 Binary files /dev/null and b/doc/img/i18n1.png differ diff --git a/doc/img/inherit1.png b/doc/img/inherit1.png new file mode 100755 index 0000000..460289a Binary files /dev/null and b/doc/img/inherit1.png differ diff --git a/doc/img/inherit2.png b/doc/img/inherit2.png new file mode 100755 index 0000000..86ae2cf Binary files /dev/null and b/doc/img/inherit2.png differ diff --git a/doc/img/inherit3.png b/doc/img/inherit3.png new file mode 100755 index 0000000..0e5159a Binary files /dev/null and b/doc/img/inherit3.png differ diff --git a/doc/img/inherit4.png b/doc/img/inherit4.png new file mode 100755 index 0000000..2ebe38b Binary files /dev/null and b/doc/img/inherit4.png differ diff --git a/doc/img/inherit5.png b/doc/img/inherit5.png new file mode 100755 index 0000000..f48abae Binary files /dev/null and b/doc/img/inherit5.png differ diff --git a/doc/img/inherit6.png b/doc/img/inherit6.png new file mode 100755 index 0000000..b394146 Binary files /dev/null and b/doc/img/inherit6.png differ diff --git a/doc/img/integersFloats1.png b/doc/img/integersFloats1.png new file mode 100755 index 0000000..8f78590 Binary files /dev/null and b/doc/img/integersFloats1.png differ diff --git a/doc/img/integersFloats2.png b/doc/img/integersFloats2.png new file mode 100755 index 0000000..2b766b9 Binary files /dev/null and b/doc/img/integersFloats2.png differ diff --git a/doc/img/integersFloats3.png b/doc/img/integersFloats3.png new file mode 100755 index 0000000..fe2271f Binary files /dev/null and b/doc/img/integersFloats3.png differ diff --git a/doc/img/integersFloats4.png b/doc/img/integersFloats4.png new file mode 100755 index 0000000..1d4d183 Binary files /dev/null and b/doc/img/integersFloats4.png differ diff --git a/doc/img/mastersSlaves1.png b/doc/img/mastersSlaves1.png new file mode 100755 index 0000000..a69eb12 Binary files /dev/null and b/doc/img/mastersSlaves1.png differ diff --git a/doc/img/mastersSlaves2.png b/doc/img/mastersSlaves2.png new file mode 100755 index 0000000..6d47d94 Binary files /dev/null and b/doc/img/mastersSlaves2.png differ diff --git a/doc/img/mastersSlaves3.png b/doc/img/mastersSlaves3.png new file mode 100755 index 0000000..7963996 Binary files /dev/null and b/doc/img/mastersSlaves3.png differ diff --git a/doc/img/mastersSlaves4.png b/doc/img/mastersSlaves4.png new file mode 100755 index 0000000..66685f2 Binary files /dev/null and b/doc/img/mastersSlaves4.png differ diff --git a/doc/img/objectStorage1.png b/doc/img/objectStorage1.png new file mode 100755 index 0000000..b12f1e6 Binary files /dev/null and b/doc/img/objectStorage1.png differ diff --git a/doc/img/objectStorage2.png b/doc/img/objectStorage2.png new file mode 100755 index 0000000..068fae3 Binary files /dev/null and b/doc/img/objectStorage2.png differ diff --git a/doc/img/pagesAndGroups1.png b/doc/img/pagesAndGroups1.png new file mode 100755 index 0000000..b12c7bd Binary files /dev/null and b/doc/img/pagesAndGroups1.png differ diff --git a/doc/img/pagesAndGroups2.png b/doc/img/pagesAndGroups2.png new file mode 100755 index 0000000..b7a47c0 Binary files /dev/null and b/doc/img/pagesAndGroups2.png differ diff --git a/doc/img/pagesAndGroups3.png b/doc/img/pagesAndGroups3.png new file mode 100755 index 0000000..9ec0e99 Binary files /dev/null and b/doc/img/pagesAndGroups3.png differ diff --git a/doc/img/pagesAndGroups4.png b/doc/img/pagesAndGroups4.png new file mode 100755 index 0000000..dfac3c4 Binary files /dev/null and b/doc/img/pagesAndGroups4.png differ diff --git a/doc/img/pagesAndGroups5.png b/doc/img/pagesAndGroups5.png new file mode 100755 index 0000000..2279153 Binary files /dev/null and b/doc/img/pagesAndGroups5.png differ diff --git a/doc/img/pagesAndGroups6.png b/doc/img/pagesAndGroups6.png new file mode 100755 index 0000000..46df16c Binary files /dev/null and b/doc/img/pagesAndGroups6.png differ diff --git a/doc/img/pagesAndGroups7.png b/doc/img/pagesAndGroups7.png new file mode 100755 index 0000000..ed70824 Binary files /dev/null and b/doc/img/pagesAndGroups7.png differ diff --git a/doc/img/pagesAndGroups8.png b/doc/img/pagesAndGroups8.png new file mode 100755 index 0000000..a3d9d60 Binary files /dev/null and b/doc/img/pagesAndGroups8.png differ diff --git a/doc/img/plone.png b/doc/img/plone.png new file mode 100755 index 0000000..ff6fa21 Binary files /dev/null and b/doc/img/plone.png differ diff --git a/doc/img/portlet.png b/doc/img/portlet.png new file mode 100755 index 0000000..26acaea Binary files /dev/null and b/doc/img/portlet.png differ diff --git a/doc/img/python.gif b/doc/img/python.gif new file mode 100755 index 0000000..7d6a722 Binary files /dev/null and b/doc/img/python.gif differ diff --git a/doc/img/refs1.png b/doc/img/refs1.png new file mode 100755 index 0000000..2bcdf8a Binary files /dev/null and b/doc/img/refs1.png differ diff --git a/doc/img/refs10.png b/doc/img/refs10.png new file mode 100755 index 0000000..a73e14d Binary files /dev/null and b/doc/img/refs10.png differ diff --git a/doc/img/refs10b.png b/doc/img/refs10b.png new file mode 100755 index 0000000..7bca2e5 Binary files /dev/null and b/doc/img/refs10b.png differ diff --git a/doc/img/refs10c.png b/doc/img/refs10c.png new file mode 100755 index 0000000..b39f955 Binary files /dev/null and b/doc/img/refs10c.png differ diff --git a/doc/img/refs11.png b/doc/img/refs11.png new file mode 100755 index 0000000..dfe4801 Binary files /dev/null and b/doc/img/refs11.png differ diff --git a/doc/img/refs12.png b/doc/img/refs12.png new file mode 100755 index 0000000..0885aaa Binary files /dev/null and b/doc/img/refs12.png differ diff --git a/doc/img/refs13.png b/doc/img/refs13.png new file mode 100755 index 0000000..b6aa56a Binary files /dev/null and b/doc/img/refs13.png differ diff --git a/doc/img/refs14.png b/doc/img/refs14.png new file mode 100755 index 0000000..e3d41d8 Binary files /dev/null and b/doc/img/refs14.png differ diff --git a/doc/img/refs15.png b/doc/img/refs15.png new file mode 100755 index 0000000..63365c2 Binary files /dev/null and b/doc/img/refs15.png differ diff --git a/doc/img/refs2.png b/doc/img/refs2.png new file mode 100755 index 0000000..8cefcc0 Binary files /dev/null and b/doc/img/refs2.png differ diff --git a/doc/img/refs3.png b/doc/img/refs3.png new file mode 100755 index 0000000..eb5aae6 Binary files /dev/null and b/doc/img/refs3.png differ diff --git a/doc/img/refs4.png b/doc/img/refs4.png new file mode 100755 index 0000000..ce27712 Binary files /dev/null and b/doc/img/refs4.png differ diff --git a/doc/img/refs5.png b/doc/img/refs5.png new file mode 100755 index 0000000..c12595d Binary files /dev/null and b/doc/img/refs5.png differ diff --git a/doc/img/refs6.png b/doc/img/refs6.png new file mode 100755 index 0000000..fb765c0 Binary files /dev/null and b/doc/img/refs6.png differ diff --git a/doc/img/refs7.png b/doc/img/refs7.png new file mode 100755 index 0000000..05488e0 Binary files /dev/null and b/doc/img/refs7.png differ diff --git a/doc/img/refs8.png b/doc/img/refs8.png new file mode 100755 index 0000000..6f442a1 Binary files /dev/null and b/doc/img/refs8.png differ diff --git a/doc/img/refs9.png b/doc/img/refs9.png new file mode 100755 index 0000000..b0c53c4 Binary files /dev/null and b/doc/img/refs9.png differ diff --git a/doc/img/specialMethods1.png b/doc/img/specialMethods1.png new file mode 100755 index 0000000..96be9db Binary files /dev/null and b/doc/img/specialMethods1.png differ diff --git a/doc/img/specialMethods2.png b/doc/img/specialMethods2.png new file mode 100755 index 0000000..e361687 Binary files /dev/null and b/doc/img/specialMethods2.png differ diff --git a/doc/img/strings1.png b/doc/img/strings1.png new file mode 100755 index 0000000..ca43156 Binary files /dev/null and b/doc/img/strings1.png differ diff --git a/doc/img/strings2.png b/doc/img/strings2.png new file mode 100755 index 0000000..82b2cdb Binary files /dev/null and b/doc/img/strings2.png differ diff --git a/doc/img/strings3.png b/doc/img/strings3.png new file mode 100755 index 0000000..a872532 Binary files /dev/null and b/doc/img/strings3.png differ diff --git a/doc/img/strings4.png b/doc/img/strings4.png new file mode 100755 index 0000000..68291ce Binary files /dev/null and b/doc/img/strings4.png differ diff --git a/doc/img/strings5.png b/doc/img/strings5.png new file mode 100755 index 0000000..8c821d0 Binary files /dev/null and b/doc/img/strings5.png differ diff --git a/doc/img/strings6.png b/doc/img/strings6.png new file mode 100755 index 0000000..738ff82 Binary files /dev/null and b/doc/img/strings6.png differ diff --git a/doc/img/strings7.png b/doc/img/strings7.png new file mode 100755 index 0000000..a76de93 Binary files /dev/null and b/doc/img/strings7.png differ diff --git a/doc/img/strings8.png b/doc/img/strings8.png new file mode 100755 index 0000000..b3c85c0 Binary files /dev/null and b/doc/img/strings8.png differ diff --git a/doc/img/strings9.png b/doc/img/strings9.png new file mode 100755 index 0000000..9ce99d4 Binary files /dev/null and b/doc/img/strings9.png differ diff --git a/doc/img/todo.gif b/doc/img/todo.gif new file mode 100755 index 0000000..bb58a82 Binary files /dev/null and b/doc/img/todo.gif differ diff --git a/doc/img/tool.png b/doc/img/tool.png new file mode 100755 index 0000000..0ee847b Binary files /dev/null and b/doc/img/tool.png differ diff --git a/doc/img/toolAndFlavours1.png b/doc/img/toolAndFlavours1.png new file mode 100755 index 0000000..db2fd00 Binary files /dev/null and b/doc/img/toolAndFlavours1.png differ diff --git a/doc/img/toolAndFlavours10.png b/doc/img/toolAndFlavours10.png new file mode 100755 index 0000000..f68c385 Binary files /dev/null and b/doc/img/toolAndFlavours10.png differ diff --git a/doc/img/toolAndFlavours11.png b/doc/img/toolAndFlavours11.png new file mode 100755 index 0000000..a13be9a Binary files /dev/null and b/doc/img/toolAndFlavours11.png differ diff --git a/doc/img/toolAndFlavours12.png b/doc/img/toolAndFlavours12.png new file mode 100755 index 0000000..9bab3a8 Binary files /dev/null and b/doc/img/toolAndFlavours12.png differ diff --git a/doc/img/toolAndFlavours13.png b/doc/img/toolAndFlavours13.png new file mode 100755 index 0000000..7f36729 Binary files /dev/null and b/doc/img/toolAndFlavours13.png differ diff --git a/doc/img/toolAndFlavours14.png b/doc/img/toolAndFlavours14.png new file mode 100755 index 0000000..66d57d4 Binary files /dev/null and b/doc/img/toolAndFlavours14.png differ diff --git a/doc/img/toolAndFlavours15.png b/doc/img/toolAndFlavours15.png new file mode 100755 index 0000000..b9ebcfe Binary files /dev/null and b/doc/img/toolAndFlavours15.png differ diff --git a/doc/img/toolAndFlavours16.png b/doc/img/toolAndFlavours16.png new file mode 100755 index 0000000..08a2083 Binary files /dev/null and b/doc/img/toolAndFlavours16.png differ diff --git a/doc/img/toolAndFlavours17.png b/doc/img/toolAndFlavours17.png new file mode 100755 index 0000000..a0a73c0 Binary files /dev/null and b/doc/img/toolAndFlavours17.png differ diff --git a/doc/img/toolAndFlavours18.png b/doc/img/toolAndFlavours18.png new file mode 100755 index 0000000..52ddce8 Binary files /dev/null and b/doc/img/toolAndFlavours18.png differ diff --git a/doc/img/toolAndFlavours19.png b/doc/img/toolAndFlavours19.png new file mode 100755 index 0000000..5b63eba Binary files /dev/null and b/doc/img/toolAndFlavours19.png differ diff --git a/doc/img/toolAndFlavours2.png b/doc/img/toolAndFlavours2.png new file mode 100755 index 0000000..2c444eb Binary files /dev/null and b/doc/img/toolAndFlavours2.png differ diff --git a/doc/img/toolAndFlavours20.png b/doc/img/toolAndFlavours20.png new file mode 100755 index 0000000..fc96d93 Binary files /dev/null and b/doc/img/toolAndFlavours20.png differ diff --git a/doc/img/toolAndFlavours21.png b/doc/img/toolAndFlavours21.png new file mode 100755 index 0000000..74a378f Binary files /dev/null and b/doc/img/toolAndFlavours21.png differ diff --git a/doc/img/toolAndFlavours22.png b/doc/img/toolAndFlavours22.png new file mode 100755 index 0000000..238cd9a Binary files /dev/null and b/doc/img/toolAndFlavours22.png differ diff --git a/doc/img/toolAndFlavours23.png b/doc/img/toolAndFlavours23.png new file mode 100755 index 0000000..2a2ec25 Binary files /dev/null and b/doc/img/toolAndFlavours23.png differ diff --git a/doc/img/toolAndFlavours2b.png b/doc/img/toolAndFlavours2b.png new file mode 100755 index 0000000..fa85999 Binary files /dev/null and b/doc/img/toolAndFlavours2b.png differ diff --git a/doc/img/toolAndFlavours2c.png b/doc/img/toolAndFlavours2c.png new file mode 100755 index 0000000..08a4f93 Binary files /dev/null and b/doc/img/toolAndFlavours2c.png differ diff --git a/doc/img/toolAndFlavours3.png b/doc/img/toolAndFlavours3.png new file mode 100755 index 0000000..69f0f46 Binary files /dev/null and b/doc/img/toolAndFlavours3.png differ diff --git a/doc/img/toolAndFlavours4.png b/doc/img/toolAndFlavours4.png new file mode 100755 index 0000000..32f68db Binary files /dev/null and b/doc/img/toolAndFlavours4.png differ diff --git a/doc/img/toolAndFlavours5.png b/doc/img/toolAndFlavours5.png new file mode 100755 index 0000000..2b0d71a Binary files /dev/null and b/doc/img/toolAndFlavours5.png differ diff --git a/doc/img/toolAndFlavours6.png b/doc/img/toolAndFlavours6.png new file mode 100755 index 0000000..fae49c9 Binary files /dev/null and b/doc/img/toolAndFlavours6.png differ diff --git a/doc/img/toolAndFlavours7.png b/doc/img/toolAndFlavours7.png new file mode 100755 index 0000000..c401e1b Binary files /dev/null and b/doc/img/toolAndFlavours7.png differ diff --git a/doc/img/toolAndFlavours8.png b/doc/img/toolAndFlavours8.png new file mode 100755 index 0000000..2784e22 Binary files /dev/null and b/doc/img/toolAndFlavours8.png differ diff --git a/doc/img/toolAndFlavours9.png b/doc/img/toolAndFlavours9.png new file mode 100755 index 0000000..af2256d Binary files /dev/null and b/doc/img/toolAndFlavours9.png differ diff --git a/doc/img/workflow1.png b/doc/img/workflow1.png new file mode 100755 index 0000000..6a13a1b Binary files /dev/null and b/doc/img/workflow1.png differ diff --git a/doc/img/workflow10.png b/doc/img/workflow10.png new file mode 100755 index 0000000..d66e6cd Binary files /dev/null and b/doc/img/workflow10.png differ diff --git a/doc/img/workflow11.png b/doc/img/workflow11.png new file mode 100755 index 0000000..bc87f59 Binary files /dev/null and b/doc/img/workflow11.png differ diff --git a/doc/img/workflow12.png b/doc/img/workflow12.png new file mode 100755 index 0000000..d6d6336 Binary files /dev/null and b/doc/img/workflow12.png differ diff --git a/doc/img/workflow2.png b/doc/img/workflow2.png new file mode 100755 index 0000000..a5f880a Binary files /dev/null and b/doc/img/workflow2.png differ diff --git a/doc/img/workflow3.png b/doc/img/workflow3.png new file mode 100755 index 0000000..18d1462 Binary files /dev/null and b/doc/img/workflow3.png differ diff --git a/doc/img/workflow4.png b/doc/img/workflow4.png new file mode 100755 index 0000000..9bee738 Binary files /dev/null and b/doc/img/workflow4.png differ diff --git a/doc/img/workflow5.png b/doc/img/workflow5.png new file mode 100755 index 0000000..ff101b0 Binary files /dev/null and b/doc/img/workflow5.png differ diff --git a/doc/img/workflow6.png b/doc/img/workflow6.png new file mode 100755 index 0000000..c110e75 Binary files /dev/null and b/doc/img/workflow6.png differ diff --git a/doc/img/workflow7.png b/doc/img/workflow7.png new file mode 100755 index 0000000..ba65362 Binary files /dev/null and b/doc/img/workflow7.png differ diff --git a/doc/img/workflow8.png b/doc/img/workflow8.png new file mode 100755 index 0000000..1bd8b38 Binary files /dev/null and b/doc/img/workflow8.png differ diff --git a/doc/img/workflow9.png b/doc/img/workflow9.png new file mode 100755 index 0000000..668d079 Binary files /dev/null and b/doc/img/workflow9.png differ diff --git a/doc/img/xhtmlChunk.png b/doc/img/xhtmlChunk.png new file mode 100755 index 0000000..bb4e14a Binary files /dev/null and b/doc/img/xhtmlChunk.png differ diff --git a/doc/img/xhtmlResult.png b/doc/img/xhtmlResult.png new file mode 100755 index 0000000..1733429 Binary files /dev/null and b/doc/img/xhtmlResult.png differ diff --git a/doc/img/xhtmlStylesMapping.png b/doc/img/xhtmlStylesMapping.png new file mode 100755 index 0000000..0171273 Binary files /dev/null and b/doc/img/xhtmlStylesMapping.png differ diff --git a/doc/img/xhtmlTemplate.png b/doc/img/xhtmlTemplate.png new file mode 100755 index 0000000..4f7d621 Binary files /dev/null and b/doc/img/xhtmlTemplate.png differ diff --git a/doc/img/zmi.png b/doc/img/zmi.png new file mode 100755 index 0000000..b65fe63 Binary files /dev/null and b/doc/img/zmi.png differ diff --git a/doc/img/zopeComponentEdit.png b/doc/img/zopeComponentEdit.png new file mode 100755 index 0000000..665cfbe Binary files /dev/null and b/doc/img/zopeComponentEdit.png differ diff --git a/doc/index.html b/doc/index.html new file mode 100755 index 0000000..8d37cec --- /dev/null +++ b/doc/index.html @@ -0,0 +1,27 @@ + + + <b>Ap</b>plications in <b>Py</b>thon + + + +

Appy (Applications in python) is a bunch of tools distributed under the GPL license for developing applications in the Python programming language. The framework features two main modules: gen and pod.

+ +

Developers, too, have the right to (h)appyness !

+ +

Developers are often guys that live on another planet. Some think this is because software development is so exciting that everything else is poorly considered. At Null-IT software, we are deeply convinced that most developers do not enjoy themselves. They spend their lives together with complex problems that never go away from their minds. Nobody understands them. Their family? Their managers? Their clients? Their friends? No. Nobody. Nobody is able to imagine how huge and complex their tasks are. Consequently, what they do is underestimated. Worst: they can't communicate. Don't believe this is due to some intrinseque geek attitude. Their geekness results from the global incapacity to apprehend the very nature of their abilities. So they are forced to work harder while experiencing the true impossibility to share their permanent software experience. Of course, it may lead to social disasters.

+ +

By publishing this high-level, easy-to-use software construction kit, itself based on the tremendously productivity-boosting Python programming language, our crazy hope is to empower developers in such a way that they can leave more often their software prison and spend more time to discover real life.

+ +

But (h)appyness has a price. Appy developers themselves accepted to pay. They have dealed their social life for one of the highest forms of social denial (sorry we can't reveal their working conditions), hoping their sacrifice will free the users of their work. So if one day you meet one of them, please be gentle and patient. But they will probably not discuss with you.

+ +

The null-IT principle

+ +

Our action is guided by the following principle:

+ +

Information Technology (IT) should be as transparent and invisible as possible.

+ +

While this may seem obvious, this principle is largely ridiculed by a great number of widespread technologies. I will mention here JEE and XSL-FO which were taken as counter-examples while developing pod. In the Python world, Zope 3, by trying to mimick the ridiculous JEE component-model where code is viciously chopped into undersized chunks interconnected by obscure XML declarations, falls unfortunately into this category as well. This is a pity seeing Zope guys trying to abandon the elegant, simple, powerful (in a word: Pythonic) design of Zope 2. But don't be scared! Many people think that Zope 2 will last for many years... Null-IT, with others, is working hard to fight against the generalized Balkanization attitude that undermine IT innovation. If you believe there is still place for hope, you came at the right site. It is time to discover gen!

+ +

The Appy framework needs Python 2.4 or higher.

+ + diff --git a/doc/license.txt b/doc/license.txt new file mode 100755 index 0000000..ead25ca --- /dev/null +++ b/doc/license.txt @@ -0,0 +1,240 @@ +The Appy framework is copyright Gaetan Delannay and contributors + +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; version 2 of the License. + +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., 59 Temple Place, Suite 330, Boston, +MA 02111-1307 USA. + + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/doc/pod.html b/doc/pod.html new file mode 100755 index 0000000..6006932 --- /dev/null +++ b/doc/pod.html @@ -0,0 +1,39 @@ + + + An introduction to <b>pod</b> (Python Open Document) + + + +

What is pod ?

+ +

pod (python open document) is a library that allows to easily generate documents whose content is dynamic. The principle is simple: you create an ODF (Open Document Format) text document (with OpenOffice Writer 2.0 or higher for example), you insert some Python code at some places inside it, and from any program written in Python, you can call pod with, as input, the OpenDocument file and a bunch of Python objects. pod generates another ODF text document (ODT) that contains the desired result. If you prefer to get the result in another format, pod can call OpenOffice in server mode to generate the result in PDF, DOC, RTF or TXT format.

+ +

Getting started with pod

+ +

First, create a pod template, like the one besides this text. A pod template is an ODT document where:

+ +
    +
  • text inserted when editing the document in "track changes" mode is used for writing Python expressions;
  • +
  • notes are used for writing special Python-based statements that allow to conditionally include or repeat a portion of the document.
  • +
+ +

In this template, I wrote the Python expression IWillTellYouWhatInAMoment while being in "track changes" mode (with OpenOffice, in the Edit menu, choose Modifications->Record). I've also added 2 notes (with OpenOffice, in the Insert menu, choose Note). The first (before "It just claims...") contains the statement do text for i in range(3). The second contains do text if (not beingPaidForIt). Click here if you want to learn more about creating pod templates.

+ +

Here is the code for calling pod for generating a result in ODT format.

+ +

+ 01  from appy.pod.renderer import Renderer
+ 02  
+ 03  IWillTellYouWhatInAMoment = 'return'
+ 04  beingPaidForIt = True
+ 05  renderer = Renderer('SimpleTest.odt', globals(), 'result.odt')
+ 06  renderer.run()

+

+ +

First we need to import the Renderer class. Then we define some Python variables. We must then create an instance of the Renderer (line 5), with, as parameters, the name of the pod template (we assume here that the pod template shown above is called SimpleTest.odt and lies in the current folder), a dictionary of named Python objects (here we simply take the global environment) and the name of the result file. The script will generate it, with, as content, what is shown in the image below.

+ +

The second line of the template is repeated 3 times. It is the effect of the for loop in the first note. All text insertions in "track changes" mode were replaced by the results of evaluating them as Python expressions, thanks to the context given to the Renderer as second parameter of its constructor. Note that within a loop, a new name (the iterator variable, i in this case) is added in the context and can be used within the document part that is impacted by the for loop. The last line of the template was not rendered because the condition of the second note evaluated to False.

+ +

Click here if you want to learn more about rendering pod templates.

+ + diff --git a/doc/podRenderingTemplates.html b/doc/podRenderingTemplates.html new file mode 100755 index 0000000..e3bf8c1 --- /dev/null +++ b/doc/podRenderingTemplates.html @@ -0,0 +1,184 @@ + + + <b>pod</b> - Rendering templates + + + +

Rendering a pod template

+ +

In order to render a pod template, the first thing to do is to create a renderer (create a appy.pod.Renderer instance). The constructor for this class looks like this:

+ +

+ def __init__(self, template, context, result, pythonWithUnoPath=None, ooPort=2002, stylesMapping={}, forceOoCall=False):
+   '''This Python Open Document Renderer (PodRenderer) loads a document
+      template (p_template) which is a OpenDocument file with some elements
+      written in Python. Based on this template and some Python objects
+      defined in p_context, the renderer generates an OpenDocument file
+      (p_result) that instantiates the p_template and fills it with objects
+      from the p_context. If p_result does not end with .odt, the Renderer
+      will call OpenOffice to perform a conversion. If p_forceOoCall is True,
+      even if p_result ends with .odt, OpenOffice will be called, not for
+      performing a conversion, but for updating some elements like indexes
+      (table of contents, etc) and sections containing links to external
+      files (which is the case, for example, if you use the default function
+      "document"). If the Python interpreter
+      which runs the current script is not UNO-enabled, this script will
+      run, in another process, a UNO-enabled Python interpreter (whose path
+      is p_pythonWithUnoPath) which will call OpenOffice. In both cases, we
+      will try to connect to OpenOffice in server mode on port p_ooPort.
+      If you plan to make "XHTML to OpenDocument" conversions, you may specify
+      a styles mapping in p_stylesMapping.''' +

+ +

For the template and the result, you can specify absolute or relative paths. I guess it is better to always specify absolute paths.

+ +

The context may be either a dict, UserDict, or an instance. If it is an instance, its __dict__ attribute is used. For example, context may be the result of calling globals() or locals(). Every (key, value) pair defined in the context corresponds to a name (the key) that you can use within your template within pod statements or expressions. Those names may refer to any Python object: a function, a variable, an object, a module, etc.

+ +

Once you have the Renderer instance, simply call its run method. This method may raise a appy.pod.PodError exception.

+ +

Since pod 0.0.2, you may put a XHTML document somewhere in the context and ask pod to convert it as a chunk of OpenDocument into the resulting OpenDocument. You may want to customize the mapping between XHTML and OpenDocument styles. This can be done through the stylesMapping parameter. A detailed explanation about the "XHTML to OpenDocument" abilities of pod may be found here.

+ +

Result formats

+ +

If result ends with .odt, OpenOffice will NOT be called (unless forceOoCall is True). pod does not need OpenOffice to generate a result in ODT format, excepted in the following cases:

+ +
    +
  • you need to update fields in the result (ie a table of contents);
  • +
  • you need to include external documents into the result (ODT, PDF, Word, ...) by using special function document.
  • +
+ +

If result ends with:

+ +
    +
  • .pdf,
  • +
  • .doc (Microsoft Word 97),
  • +
  • .rtf or
  • +
  • .txt,
  • +
+ +

OpenOffice will be called in order to convert a temporary ODT file rendered by pod into the desired format. This will work only if your Python interpreter knows about the Python UNO bindings. UNO is the OpenOffice API. If typing import uno at the interpreter prompt does not produce an error, your interpreter is UNO-enabled. If not, there is probably a UNO-enabled Python interpreter within your OpenOffice copy (in <OpenOfficePath>/program). In this case you can specify this path in the pythonWithUnoPath parameter of the Renderer constructor. Note that when using a UNO-disabled interpreter, there will be one additional process fork for launching a Python-enabled interpreter.

+ +

During rendering, pod uses a temp folder located at <result>.temp.

+ +

Launching OpenOffice in server mode

+ +

You launch OpenOffice in server mode by running the command:

+ +

(under Windows: ) call "[path_to_oo]\program\soffice" "-accept=socket,host=localhost,port=2002;urp;"& (put this command in .bat file, for example)

+ +

Under Windows you may also need to define this environment variable (with OpenOffice 3.x) (here it is done in Python):

+ +

os.environ['URE_BOOTSTRAP']='file:///C:/Program%20Files/OpenOffice.org%203/program/fundamental.ini'

+ +

(under Linux: ) soffice "-accept=socket,host=localhost,port=2002;urp;"

+ +

Of course, use any port number you prefer.

+ +

Unfortunately, OpenOffice, even when launched in server mode, needs a Windowing system. So if your server runs Linux you will need to run a X server on it. If you want to avoid windows being shown on your server, you may launch a VNC server and define the variable DISPLAY=:1. If you run Ubuntu server and if you install package "openoffice.org-headless" you will not need a X server. On other Linux flavours you may also investigate the use of xvfb.

+ +

Rendering a pod template with Django

+ +

pod can be called from any Python program. Here is an example of integration of pod with Django for producing a PDF through the web. In the first code excerpt below, in a Django view I launch a pod Renderer and I give him a mix of Python objects and functions coming from various places (the Django session, other Python modules, etc).

+ +

+ 01 gotTheLock = Lock.acquire(10)
+ 02 if gotTheLock:
+ 03    template = '%s/pages/resultEpmDetails.odt' % os.path.dirname(faitesletest.__file__)
+ 04    params = self.getParameters()
+ 05    # Add 'time' package to renderer context
+ 06    import time
+ 07    params['time'] = time
+ 08    params['i18n'] = i21c(self.session['language'])
+ 09    params['floatToString'] = Converter.floatToString
+ 10    tmpFolder = os.path.join(os.path.dirname(faitesletest.__file__), 'temp')
+ 11    resultFile = os.path.join(tmpFolder, '%s_%f.%s' % (self.session.session_key, time.time(), self.docFormat))
+ 12    try:
+ 13        renderer = appy.pod.renderer.Renderer(template, params, resultFile, coOpenOfficePath)
+ 14        renderer.run()
+ 15        Lock.release()
+ 16    except PodError, pe:
+ 17        Lock.release()
+ 18        raise pe
+ 19    docFile = open(resultFile, 'rb')
+ 20    self.session['doc'] = docFile.read()
+ 21    self.session['docFormat'] = self.docFormat
+ 22    docFile.close()
+ 23    os.remove(resultFile)
+ 24 else:
+ 25    raise ViaActionError('docError')
+

+ +

When I wrote this, I was told of some unstabilities of OpenOffice in server mode (! this info is probably outdated, it was written in 2007). So I decided to send to OpenOffice one request at a time through a Locking system (lines 1 and 2). My site did not need to produce a lot of PDFs, but it may not be what you need for your site.

+ +

Line 11, I use the Django session id and current time to produce in a temp folder a unique name for dumping the PDF on disk. Let's assume that self.docFormat is pdf. Then, line 20, I read the content of this file into the Django session. I can then remove the temporary file (line 23). Finally, the chunk of code below is executed and a HttpResponse instance is returned.

+ +

+ 01 from django.http import HttpResponse
+ 02 ...
+ 03    # I am within a Django view
+ 04    res = HttpResponse(mimetype=self.getDocMimeType()) # Returns "application/pdf" for a PDF and "application/vnd.oasis.opendocument.text" for an ODT
+ 05    res['Content-Disposition'] = 'attachment; filename=%s.%s' % (
+ 06        self.getDocName(), self.extensions[self.getDocMimeType()])
+ 07    res.write(self.getDocContent())
+ 08    # Clean session
+ 09    self.session['doc'] = None
+ 10    self.session['docFormat'] = None
+ 11    return res
+

+ +

Line 7, self.getDocContent() returns the content of the PDF file, that we stored in self.session['doc'] (line 20 of previous code chunk). Line 5, by specifying attachment we force the browser to show a popup that asks the user if he wants to store the file on disk or open it with a given program. You can also specify inline. In this case the browser will try to open the file within its own window (potentially with the help of a plugin).

+ +

Rendering a pod template with Plone

+ +

The following code excerpt shows how to render the "ODT view" of a Plone object. Here, we start from an Archetypes class that was generated with ArchGenXML. It is a class named Avis that is part of a Zope/Plone product named Products.Avis. While "Avis" content type instances can be created, deleted, edited, etc, as any Plone object, it can also be rendered as ODT (and in any other format supported by pod/OpenOffice). As shown below we have added a method generateOdt to the Avis class, that calls a private method that does the job.

+ +

+ 01 import appy.pod.renderer
+ 02 import Products.Avis
+ 03 ...
+ 04    # We are in the Avis class
+ 05    security.declarePublic('generateOdt')
+ 06    def generateOdt(self, RESPONSE):
+ 07        '''Generates the ODT version of this advice.'''
+ 08        return self._generate(RESPONSE, 'odt')
+ 09
+ 10    # Manually created methods
+ 11
+ 12    def _generate(self, response, fileType):
+ 13        '''Generates a document that represents this advice.
+ 14           The document format is specified by p_fileType.'''
+ 15        # First, generate the ODT in a temp file
+ 16        tempFileName = '/tmp/%s.%f.%s' % (self._at_uid, time.time(), fileType)
+ 17        renderer = appy.pod.renderer.Renderer('%s/Avis.odt' % os.path.dirname(Products.Avis.__file__), {'avis': self}, tempFileName)
+ 18        renderer.run()
+ 19        # Tell the browser that the resulting page contains ODT
+ 20        response.setHeader('Content-type', 'application/%s' % fileType)
+ 21        response.setHeader('Content-disposition', 'inline;filename="%s.%s"' % (self.id, fileType))
+ 22        # Returns the doc and removes the temp file
+ 23        f = open(tempFileName, 'rb')
+ 24        doc = f.read()
+ 25        f.close()
+ 26        os.remove(tempFileName)
+ 27        return doc
+

+ +

First, I plan to create the ODT file on disk, in a temp folder. Line 16, I use a combination of the attribute self._at_uid of the Zope/Plone object and the current time to produce a unique name for the temp file. Then (lines 17 and 18) I create and call a pod renderer. I give him a pod template which is located in the Products folder (Avis subfolder) of my Zope instance. Lines 20 and 21, I manipulate the Zope object that represents the HTTP response to tell Zope that the resulting page will be an ODT document. Finally, the method returns the content of the temp file (line 27) that is deleted (line 26). In order to trigger the ODT generation from the "view" page of the Avis content type, I have overridden the header macro in Products/Avis/skins/Avis/avis_view.pt:

+ +

+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US" i18n:domain="plone">
+ <body>
+     <div metal:define-macro="header">
+     <div class="documentActions"><a href="" tal:attributes="href python:here['id'] + '/generateOdt'">Generate ODT version</a></div>
+     <h1 tal:content="here/title">Advice title</h1>
+     <div class="discreet">
+         If you want to edit this advice, click on the "edit" tab above.
+     </div>
+     <p></p>
+     </div>
+ </body>
+ </html>
+

+ +

Hum... I know there are cleanest ways to achieve this result :-D

+ + diff --git a/doc/podWritingAdvancedTemplates.html b/doc/podWritingAdvancedTemplates.html new file mode 100755 index 0000000..280dd5c --- /dev/null +++ b/doc/podWritingAdvancedTemplates.html @@ -0,0 +1,265 @@ + + + <b>pod</b> - Writing advanced templates + + + +

Inserting arbitrary content: the from clause

+ +

In the section "Writing templates", you've learned how to write pod statements (if, for). Every pod statement is linked to a given part of the pod template (a text paragraph, a title, a table, a table row, etc) and conditions how this part is rendered in the result (the if statement, for example, renders the part only if the related condition is True). This way to work has its limits. Indeed, you can't insert what you want into the result: you are forced to use the part of the document that is the target of the statement. Of course, in this part, you can still write funny things like Python expressions and statements, but it may not be sufficient.

+ +

This is why a special from clause may be added to every pod statement. A statement containing such a clause will replace the content of the targeted document part by the result of the from clause. This clause must specify a Python expression that must produce a valid chunk of ODT content.

+ +

In the example below, the statement has a from clause that produces a simple paragraph containing 'Hello'.

+ +

+ +

In the result, the targeted paragraph has been replaced by the chunk of odt content specified in the from expression. Note that the from clause MUST start on a new line in the note. Else, it will be considered as part of statement and will probably produce an error.

+ +

+ +

Surprise! This statement is neither a 'if' not a 'for' statement... It is a "null" statement whose sole objective is to replace the target by the content of the from expression. But you can also add from clauses to 'if' and 'for' statements. Here is an example with a 'for' statement.

+ +

+ +

Here's the result. Note that within the from clause you may use the iterator variable (i, in this case) defined by the for statement.

+ +

+ +

Actually, when you don't specify a from clause in a statement, pod generates an implicit from clause whose result comes from the odt chunk that is the target of the statement.

+ +

I agree with you: these examples are not very useful. Moreover, it requires you to have some knowledge of the ODF syntax. You also have to take care about the namespaces you specify (text:, style:, fo:, etc): they must match the ones used in your pod template. But these examples illustrate how from clauses work and how you may go further by yourself if pod does not implement (yet ;-)) what you need.

+ +

The remaining of this page presents much more useful use cases, in the form of built-in pod functions that you may use within from clauses. Indeed, a bunch of functions is inserted by default to every context given to the pod renderer.

+ +

Managing XHTML input: the xhtml function

+ +

One of these functions is the xhtml function, that allows to convert chunks of XHTML documents (given as strings in the context) into chunks of OpenDocument within the resulting OpenDocument. This functionality is useful, for example, when using pod with systems like Plone, that maintain a part of their data in XHTML format (Kupu fields, for example).

+ +

Suppose you want to render this chunk of XHTML code at some place in your pod result:

+ + + + + + + + + +
XHTML codeXHTML rendering (Plone)
+ <p>Te<b>s</b>t1 : <b>bold</b>, i<i>tal</i>ics, exponent<sup>34</sup>, sub<sub>45</sub>.</p>
+ <p>An <a href="http://www.google.com">hyperlink</a> to Google.</p>
+ <ol><li>Number list, item 1</li>
+ <ol><li>Sub-item 1</li><li>Sub-Item 2</li>
+ <ol><li>Sub-sub-item A</li><li>Sub-sub-item B <i>italic</i>.</li></ol>
+ </ol>
+ </ol>
+ <ul><li>A bullet</li>
+ <ul><li>A sub-bullet</li>
+ <ul><li>A sub-sub-bullet</li></ul>
+ <ol><li>A sub-sub number</li><li>Another.<br /></li></ol>
+ </ul>
+ </ul>
+ <h2>Heading<br /></h2>
+ Heading Blabla.<br />
+ <h3>SubHeading</h3>
+ Subheading blabla.<br /> +
+ +
+ +

pod comes with a function named xhtml that you may use within your pod templates, like this:

+ +

+ +

In this example, the name dummy is available in the context, and dummy.getAt1() produces a Python string that contains the XHTML chunk shown above. This string is given as paremeter of the built-in pod xhtml function.

+ +

Note that if you specify a key "xhtml" in the context given to the pod renderer, the default "xhtml" function will be overridden by the value specified in the context.

+ +

The rendering produces this document:

+ +

+ +

The OpenDocument rendering is a bit different than the XHTML rendering shown above. This is because pod uses the styles found in the pod template and tries to make a correspondence between style information in the XHTML chunk and styles present in the pod template. By default, when pod encounters a XHTML element:

+
    +
  • it checks if a "class" attribute is defined on this element. If yes, and if a style with the same "display name" is found in the OpenDocument template, this style will be used. The "display name" of an OpenDocument style is the name of the style as it appears in OpenOffice, for example;
  • +
  • if no "class" attribute is present, and if the XHTML element is a heading (h1 to h6), pod tries to find an OpenDocument style which has the same "outline level". For example, "h1" may be mapped to "Heading 1". This is what happened in the example above; +
  • +
  • else, no style at all is applied. +
  • +
+ +

You have the possibility to customize this behaviour by defining styles mappings (see below).

+ +

Defining styles mappings

+ +

You can define styles mappings at two different levels. First, when you create a renderer instance, you may give a styles mapping to the parameter stylesMapping, which is the global style mapping (The renderer's constructor is defined here). A styles mapping is a Python dictionary whose keys are either CSS class names or XHTML element names, and whose values are "display names" of OpenDocument styles that must be present in the pod template. Every time you invoke the xhtml function in a pod template, the global styles mapping comes into play.

+ +

Note that in an OpenDocument document, OpenOffice stores only the styles that are used in the document (I don't know how others OpenDocument-compliant word processors behave). The styles names ("Heading 1", "Standard"...) that appear when opening your template with OpenOffice, for example, are thus a super-set of the styles that are really recorded into your document. You may consult the list of available styles in your pod template programmatically by calling your pod renderer's getStyles method.

+ +

In a styles mapping you can also define a special key, h*, and define a positive or negative integer as value. When pod tries to establish a style correspondance based on outline level, it will use this number. For example, if you specify a styles mapping = {'h*' : -1}, when encountering element h2 (that does not define a "class" attribute), if an OpenDocument with an outlevel of 2-1 is found (ie "Heading 1"), it will be used.

+ +

Second, each time you invoke the xhtml function in a pod template, you may specify a local styles mapping in the parameter named stylesMapping, like shown below.

+ +

+ +

Local styles mappings override what you have (potentially) defined in the global styles mapping.

+ +

At present, the XHTML elements listed below may not be "styled-mapped" (they may not be present in styles mappings) because pod uses it own automatically-generated OpenDocument styles:

+ +
    +
  • ol
  • +
  • ul
  • +
  • li
  • +
  • a
  • +
+ +

This can be problematic if, for instance, you want to use special style-related attributes, specially for li elements that correspond to paragraphs. This is why any pod template includes some predefined styles that may apply to these elements. The following table shows them, grouped by element type.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementAvailable style(s)Description
olThe default pod style only.No pod-specific style is proposed at present. The unique default pod style of this element will always be used.
ulThe default pod style only.No pod-specific style is proposed at present. The unique default pod style of this element will always be used.
lipodItemKeepWithNextThis specific style adds the characteristic "Keep with next" to the target li paragraph. This way, the paragraph will always be present. This works for lis inside uls or ols.
aThe default pod style only.No pod-specific style is proposed at present. The unique default pod style of this element will be used.
+ +

In order to use one of those styles, you can specify its name in the "class" attribute of the target element, or you can go through a global or local styles mapping. For example, if you need a li element that will always stay on the same page as the paragraph below him, you can write <li class="podItemKeepWithNext"></li>.

+ +

Managing XHTML entities

+ +

By default, the xhtml function uses a standard XML parser (the Python "expatreader" parser) for parsing the XHTML code. This parser knows only about the 5 legal XML entities: &amp; (&), &quote; ("), &apos; ('), &lt; (<) and &gt; (>). If an XHTML entity is encountered (like &eacute;), the XML parser produces an error (numeric entities like &#234; seem to be supported). For solving this problem, pod may use another parser that comes with PyXML and resides in xml.sax.drivers2. If this parser is available in your Python interpreter, pod will use it and configure it: XHTML entities will then be supported and correctly converted. Type import xml.sax.drivers2 in your Python shell; if no exception is raised, the parser is installed and will be used by pod.

+ +

Integrating external files or images into the result: the document function

+ +

The document function allows you to integrate, into the ODT result, images or files that come from external sources. Here is the function signature; the table below explains each parameter. Examples follow.

+ +

document(content=None, at=None, format=None, anchor='as-char')

+ + + + + + + + + + + + + + + + + + + + + + +
ParameterDescription
contentIf you have the image or file content available in memory or via a file handler, use this parameter. content may hold the whole (binary) image or file content, or be an (opened) Python file instance (a Python file instance is obtained by calling the built-in Python method file or open (open being an alias for file).
atIf your image or file is available on disk, do not use the previous parameter and specify the file path in this parameter.
formatWhen using parameter at, pod guesses the file format based on file extension. But if you use parameter content, you must specify the file format here. The format may be a file extension (without the leading dot) or a MIME type. The currently supported formats are:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Parameter valueDescription
odt or application/vnd.oasis.opendocument.text  An OpenDocument Text document.
pdf or application/pdfAdobe PDF document. Note that pod needs Ghostscript installed for integrating PDFs into a pod result. It means that the program gs must be installed and available in the path. pod integrates a PDF file into the ODT result like this: (1) pod calls gs to split the PDF into images (one image per page); (2) pod uses internally function document to integrate every image into the pod result.
png or image/pngAn image in PNG format.
jpeg, jpg or image/jpegAn image in JPEG format.
gif or image/gifAn image in GIF format.
+
anchorThis parameter is used for images only. It determines the way to anchor the image in the result. Possible values are:

+ + + + + + + + + + + + + + + + + + + + + +
Parameter valueDescription
pageTo the page.
paragraphTo the paragraph.
charTo the character.
as-charAs character.
+
+ +

The following example shows a POD template part that integrates a PNG image from disk.

+ +

+ +

(Note that the from clause must be on a single line.). This could be rendered this way for example:

+ +

+ +

The following ODT template part reads PDF and ODT files from a database (The ZODB; it is a Plone site) and integrates them in the pod result.

+ +

+ +

For those who know Plone, annex is an instance of ATFile: annex.data returns its binary content, while annex.getContentType() returns is MIME type.

+ +

In future Appy releases:

+ +
    +
  • more formats will be supported (mainly, the Microsoft formats: doc, xls, etc);
  • +
  • more "protocols" will be supported for accessing the external file or image (HTTP, FTP, etc);
  • +
  • images or documents referenced in XHTML code that is imported through function xhtml will be integrated into the POD result.
  • +
+ +

Do not use built-in pod functions in pod expressions !

+ +

Pod built-in functions are designed to be used within pod statements (from clauses). If you try to use them in pod expressions, you will get strange results. The example below uses the xhtml function in a pod expression.

+ +

+ +

If dummy.getAt1() produces the XHTML chunk <p>Test1<br/></p>, the result will look like this:

+ +

+ + + diff --git a/doc/podWritingTemplates.html b/doc/podWritingTemplates.html new file mode 100755 index 0000000..6eaadc9 --- /dev/null +++ b/doc/podWritingTemplates.html @@ -0,0 +1,209 @@ + + + <b>pod</b> - Writing simple templates + + + +

Expressions

+ +

Within a pod template, any text written in "track changes" mode is interpreted as a Python expression. The interpretation context is given to pod like explained here. The example below contains some simple Python expressions (Python variables).

+ +

+ +

Running appy.pod with a context that associates a value to expr1, i1 and f1 produces a result like the one shown below.

+ +

+ +

Note that any style (bold, italic, etc) that is used within track-changed text is kept, but style variations within the text is ignored. For example, the first "expr1" text has an italicized "p", but this style information is ignored. The second "expr1" is bold, so the result is bold, too, and again the having an italicized "p" has no effect.

+ +

"if", "else" and "for" statements

+ +

An "if" statement is written in an ODT note and has the form do <document part> if <python_expression>. The <document part> which is the target of the statement will only be included in the result if the python expression resolves to True (the boolean value, or any equivalent Python value, like a non empty list, string, etc). "Document parts" that can be referenced in "if" statements are presented in the table below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
textA paragraph
titleA title
sectionA section
tableA whole table
rowA single row within a table
cellA single cell within a table row
+ +

A "for" statement is also written in a ODT note and has the form do <document part> for <variable_name> in <python_expression>. The <document part> which is the target of the statement will be included as many times as there are items in the sequence that is defined by the <python_expression>. Within the document part in question, one can use <variable_name>, or any Python expression that uses it, within track-changed text (it adds this name in the context). Allowed <document part>s are those described in the previous table. If <variable_name> was already defined in the context, it is hidden by this new variable definition only within the scope of the targeted <document part>.

+ +

The example below contains several "if" and "for" statements. Small yellow rectangles are ODT notes that you insert, for example, through the "Insert->Note" menu in OpenOffice. Yellow blocks containing Python code represent the content of the note.

+ +

+ +

Applying this template with a given context may produce a result like the following.

+ +

+ +

The template contained 1 section. The result contains 3 sections, because 3 groups were defined in the groups variable (which was a Python list or tuple containing instances of some Group class defined somewhere in a python file) used in the 1st "for" statement. The 2 first groups contained persons (instances of some Person class), so the table was rendered in the 2 first sections (this is the effect of the second note). Those tables contain one row by person, thanks to the "for" statement in the third note. The last paragraph of the template was only rendered once, for the last group, because this group contained no person (this is the effect of the last note).

+ +

In this example, for each group, I wanted to include a table if persons were defined, or a paragraph else. In order to express this, I've defined 2 "if" statements with 2 conditions, one being the negation of the other. pod (starting from version 0.2) allows to use "else" statements: instead of writing do text if not group.persons in the last note, I could have written do text else instead. But do you believe I have time to rework those old screenshots? Moreover, you have to be careful with "else" statements. pod statements are independent of each other: an "if" statement and an "else" statement are really 2 different statements, unlike what happens in programming languages for example. It means that pod must be able to link "if" and "else" statements, and in some cases it is not possible. The following example contains several "else" statements that pod could unambiguously link to their corresponding "if" statements.

+ +

+ +

When pod encounters an "else" statement, it tries to link it to the last defined "if" statement in the part of the pod template that precedes the "else" statement. In the last example, when encountering the "else" statement numbered "4", pod links it to the "if" statement numbered "3". Once the link is done, the linked "if" statement is "consumed": it can't be linked to any other "else" statement. When pod encounters the "else" statement numbered "5", it links it to the last "unconsumed if" statement: the "if" statement numbered "2". In the same way, the "else" numbered "6" is linked to the "if" numbered "1". Here is the result: +

+ +

+ +

In the example below, however, pod does not produce the desired result.

+ +

+ +

Here I wanted to link the "else" to the first "if", but pod linked it to the second one instead, mistakenly rendering the last line:

+ +

+ +

With the currently presented concepts, here you would be forced to replace the "else" by an "if" that duplicates and negates the condition written in the first "if". This duplication could lead to maintenance problems (ie you update the condition in the first "if" and forget to update its negation in the third one). So if we want to conform to the null-IT principle and make pod a tool as invisible as possible, we need here the concept of "named statement". From pod 0.2, any statement may be named. The name must conform to the regular expression "\w+" (it can contain alphanumeric characters only + the underscore as explained here) and must be unique among the (not consumed) named statements in the pod template. The next example is the right way to produce the result we wanted to achieve in the last example: the first "if" is named and can thus be referred to explicitly by the "else".

+ +

+ +

The result is superb! pod is invisible!

+ +

+ +

Note that it is legal to name any pod statement; however, pod uses statement names only for connecting "if" and "else" statements (at least for now).

+ +

But... where exactly can you place "if", "else" and "for" statements ?

+ +

You can place statements anywhere within the element you want to repeat or conditionally include. In a table, for example, you can place a "do table..." note in any cell of the table. The only thing you have to take care of is the order of notes that apply on nested elements. For example, a note that applies on a table must be placed in the table before a second note that applies on a row of this table. This note must itself be placed before a note that applies on a cell within that row. In the first cell of a table, for example, you can define several notes sequentially: one for the table, one for the row and one for the cell.

+ +

Back to expressions (just for a few minutes): the "if" expression

+ +

Besides the "if" and "else" statements, pod also proposes an "if" expression. Indeed, if the things you want to conditionnally include are small enough (a few words for example) you may find more convenient to use the "if" expression, that is implemented as a function named "test" that is directly available in the pod context as a default function. Here is an example:

+ +

+ +

The result:

+ +

+ +

Errors management

+ +

While interpreting expressions or statements, errors may be raised. In such cases, the result is still generated, but a note containing an explanation about the error, together with the Python traceback, is produced in a note instead of the expression result or the document part that is the target of a statement.

+ +

The example below defines an expression that uses a variable "A" that is not defined.

+ +

+ +

Instead of containing the result of evaluating A+B, the result contains a note that gives an explanation about the error and the Python traceback, as shown below.

+ +

+ +

In the next example, we try to render a table if a condition is True, but evaluating the condition produces an error.

+ +

+ +

The table is rendered but only contains a note explaining what happened.

+ +

+ +

The following example illustrate errors produced while evaluating "for" statements.

+ +

+ +

Notice that in the result shown below, the second note does not contain a traceback. Indeed, Python did not produce an error while evaluating expression 45. pod simply expected a sequence.

+ +

+ +

The previous examples were all examples of "runtime" errors, ie errors that were produced while rendering the template. Errors may also occur at "parsing" time, when pod reads the content of notes and track-changed text to analyse the Python-like expressions. In the example below, statements do not respect the pod syntax.

+ +

+ +

Oh! So much effort has been made to produce clear error messages that I do not need to add a comment here :-)

+ +

+ +

Repetition or conditional inclusion of table cells is a bit more tricky

+ +

Playing with table cells may render tables that do not contain a number of cells that produce complete rows. In the example below, the "for" statement will produce 5 cells in a 3-columns table (there are 3 persons defined in persons). So there is one missing cell. If you are bored with my explanations, you can make a pause now and listen to some musical illustration of this "missing cell" problem (ok I agree the artist has its own point of view on this question).

+ +

+ +

Hurrah! pod added the missing cell for you.

+ +

+ +

Sometimes the number of cells it too high. In the example below, again 3 persons will be inserted, producing a row of 6 cells instead of 4.

+ +

+ +

Hurrah again. pod broke this row into 2 complete rows.

+ +

+ +

Who said it is exactly the same problem as the previous one? Hmm. Hmm. Indeed. Ok. I must recognize that "too much" or "not enough" cells is just a question of point of view. You're right. But I will not change the names of my test files or refactor them to eliminate redundant ones. You are aware of this strange situation, now let's continue.

+ +

Conditional inclusion of cells through statements like do cell if trucmuche raise the same issues and are solved by pod in the same way.

+ +

In order to avoid having some cells to lack a right border like on my screenshots, check that all your table cells have 4 borders.

+ +

Removing tables and sections that were created just for inserting statements

+ +

Very often, when creating complex pod templates, you will create sections containing tables containing tables of tables of tables etc. Those hierarchical levels exist just because you need to insert a statement, but you don't care about keeping them in the final result. You can tell pod to remove tables and sections from the final result by using the "minus" operator, like shown below.

+ +

+ +

The result, produced while persons is a list of 8 persons, is shown below.

+ +

+ +

Of course, do that only if you don't care about any formatting associated with the table or section to remove. In the previous example, using the "minus" operator was not the right choice if we wanted to print those addresses on envelopes. In other cases, using this operator may prevent occurrence of a bug in OpenOffice (2.1): a table that spans multiple pages may be truncated after 1 or 2 pages. Some manual edition (like inserting a "carriage return" somewhere) is needed to correct this problem; so if you planned to convert your result automatically to PDF for example you are KO.

+ +

The "minus" operator is only allowed for sections and one-cell tables. When applying it to a table that has more than one cell...

+ +

+ +

... pod complains.

+ +

+ +

Tips for writing pod templates with OpenOffice.org

+ +

When you write text in "track changes" mode (for writing pod expressions), sometimes OpenOffice (2.2) splits this text in several chunks. For example, while typing "azerty" OpenOffice may create 2 contiguous text inserts, "aze" and "rty". So the best way to write expressions in a pod template with OpenOffice is to perform the following steps:

+ +
    +
  • write and edit your expression in normal mode;
  • +
  • when you're happy with it, cut it;
  • +
  • switch in "track changes" mode (Edit -> Changes -> Record)
  • +
  • paste your expression
  • +
  • switch back in normal mode (clicking again on Edit -> Changes -> Record will uncheck this menu option.)
  • +
+ +

Of course this can be boring. So you won't do that systematically. In order to check that your expressions are not split, go to Edit -> Changes -> Accept or reject. Do not accept or reject anything, but use the arrows to navigate within your expressions. You will immediately detect a truncated expression (or potentally merged expressions)

+ +

Try to minimize Python code within pod templates

+ +

pod templates should be as readable as possible. One day (who knows?), a non-developer may need to consult one of them. So try minimize the amount of Python code you put in it. For example, instead of writing a complex expression or a complex condition within a statement, think about writing this code in a Python method with a nice name and simply call it within your template (of course the object on which this method is defined must be in the pod context).

+ +

If you need to use the result of a resources-consuming method in several places within a pod template, instead of calling this method in several expressions (pod will execute the method in each expression), call it once before rendering the template and give the result under a new name in the context passed to pod.

+ +

For more information about calling pod to render a template with a given context, come here !

+ + diff --git a/doc/template.html b/doc/template.html new file mode 100755 index 0000000..a857397 --- /dev/null +++ b/doc/template.html @@ -0,0 +1,64 @@ + + + Appy framework + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + +
The Appy framework + +  Home +  Download +  Contact +  To do + + +
Documentation | gen - creating basic and advanced classes - security and workflows | pod - creating basic and advanced templates - rendering templates
+
{{ title }}
{{ content }}
 Download and install
+ + + + + +
+
+
+ + diff --git a/doc/todo.txt b/doc/todo.txt new file mode 100755 index 0000000..7efbf8f --- /dev/null +++ b/doc/todo.txt @@ -0,0 +1,69 @@ +vpod +v- Error in buffers.py line 422 (in method evaluate): evalEntry.action may be None but I don't know why (maybe due to buffers cut?) +v- Document the ability to read the template from memory, not only from the file system (it seems to work) +v- Size of resulting ODTs is too big. +v- Try to convert xhtml chunks that are not utf-8-encoded +v- XHTML conversion is not implemented for all XHTML tags. +v- XHTML conversion produces empty results for some XHTML tag combinations +v- Implement a function "pod" that allows to insert the content of another pod template into the current file +v- Implement a "retry n times" function when calling OO in server mode ("n" should be parameterized) +v- Add a "do bullet" (li, ul) ? +v- Add a function "text" that replaces "carriage returns" with </p><p> (ou <br/>?) and that replaces leading dashes with bullets. +v- Deprecate "xhtml" function -> New function text(format=...) with format="xhtml" or "text" or... +v- ImageImporter: adding 'g' to file names may lead to too long file names. +v- Finalize function "document" (manage Microsoft formats: doc, xls...) +v- converter.py: allow to call converter.py without updating sections and/or indexes (may cause performance problems?) +v- When an error occurs in a for loop, the iteratr variable may not be incremented, which duplicates the errors in subsequent iterations. Try:finally? + +vgen +v- View on AppyFolder must be query.pt +v- Build automatically a research page (included fields are selectable through the flavour) only with "searchable" fields. +v- Correct some navigation problems (after actions like delete, edit, etc) +v- Validation system: check if it is possible to forget about duplicating page and group labels in child classes. Possible to remove xxx_list labels? +v- Add a button for checking if current Python running Zope is UNO-compliant. +v- __setattr__ for Refs on Wrappers do not work, but the problem is solved in method "create" (inspiration source). +v- Generate a static site from a gen-application +v- Xml export (on file system or via webdav/REST) +v- Add an option to generator, where un-managed i18n labels are removed (useful during development, when fields are creates, deleted, renamed...) +v- Make the list of reserved attribute and method names (tool, trigger, state, ...) and raise errors on generation if those names are used by a gen-class +v- produceNiceDefault: manage numbers (when a number is encountered, add a space, etc). +v- When editing (not creating) a flavour (or when reinstalling the product), create ALL the portal_types if they do not exist (some may have been added after flavour creation). +v- By flavour or date object, add 2 params: date format with and without hour. Create a appy-specific macro for displaying dates, that will use those params. +v- Move some generated code (appyMixins, CommonMethods...) in appy.gen (minimize generated code). +v- Create the "pod" field type +v- Add on tool special action "uninstall" (similar to special action "install") + question "update workflow settings or not" ? +v- At startup, patch actions "tool.install" -> show = si selon Plone le Product doit etre reinstallé, i18n messages, etc. +v- Allow to create objects in the app root folder (and not only in the tool) from the tool (or anywhere else). +v- On dashboards and Ref tables, fields rendering does not always take into account permissions, show, etc. +v- Add "self.flavour" on any wrapper. +v- Config instance: add supposed number of flavours (for generating corresponding i18n labels _1, _2, etc, for flavour-specific content-types) +v- Config instance: create a defaultWorkflow attribute, similar to defaultCreators +v- Duplicate all special fields (optionalFieldsFor, defaultValueFor, etc) generated in a flavour for all child classes of a given class. +v- Permissions-to-roles mappings: if you don't specify a value for a given field-specific read/write permission, by default it will take the same value as for the whole-gen-class read/write permission. +v- Implement abstract workflows +v- displayTabs: do not display tabs for which no widget shows up (ie edit tabs on which no Ref widget is shown) +v- Add i18n labels for messages when transitions are triggered (! transition may potentially return a custom message!) +v- On all widgets: implement "warning method" -> kind of condition. If True, a warning message appears somewhere near the field. Useful for actions, transitions and pod templates. +v- Action fields: add param "confirm" (also for workflow transitions): if True a popup appears (with possibility to define a i18n message) +v- Ref fields: add parameter "sort" for sorting Ref fields (this param may be the name of a field or a custom method). On Refs with link=True, it is used for sorting the list of all references that may be linked with the current object; on Refs with add=True, it is used for inserting the newly created object. +v- tool.getReferenceLabel: use brain if fields are indexed (wake up object only when needed) +v- Delete tool.maxListBoxWidth and use param "width" of Ref field instead. +v- Comments in ZPTs: use everywhere tal tag "comment" +v- Common method "fieldValueSelected" -> does it produce the right value when the page is shown again due to a validation error? Must we check this case explicitly via emptyness of dict "errors" in request? +v- Document how to write your own code generator +v- pod integratio: when returning a PDF/ODT file, file name is not set (or file extension). +v- Add a param to pod fields will allow to freeze generated result, and to unfreeze or re-generate them (with admin access only or...?) +v- Take into account field-specific permissions when displaying backward fields +v- Improve graphical rendering of pages with additional classes (Group, Page,...) that may be specified instead of strings for parameters "group", "page" on fields +v- Add an event management system (generalize the mail notification mechanism from PloneMeeting) that one may customize per flavour, with some predefined action types (emails, log, freeze pdf,...) +v- Interoperability: class and worklow inheritance from one product to another and Refs to objects coming from other standard non Appy Plone products. +v- Add a mechanism that will allow developers to add things like ZPTs, etc, into the Plone product. +v- Write a test system. +v- Bug (seems to come from standard Plone): when defining a field multiselection mandatory, when unselecting all values, it does not do anything (if the field is not mandatory it works). +v- When we copy fields in class Flavour (because of editDefault), we must also recopy methods that were defined on the original Appy class on the FLavour class generated in appyWrappers.py. +v- Check accessors get_flavour defined multiple times in PodTemplate_Wrapper +v- Bug: when we specify encoding in a Python file, the generated Python AST is empty. +v- plone25: remove warnings by setting deprecated=True for all generated classes in configure.zcml +v- Workflow dependencies: permissions of a given object can depend on the ones defined on another (containing?). +v- Add an on-line help system (popup for every field) + diff --git a/doc/version.txt b/doc/version.txt new file mode 100755 index 0000000..bbbb948 --- /dev/null +++ b/doc/version.txt @@ -0,0 +1,41 @@ +0.3.1 (2009-04-10) +- gen: added the concept of "phase" for structuring a root content type. + +0.3.0 (2009-03-25) +- Includes the first version of appy.gen, a new code generator for Plone for building complex and shared web applications. +- pod: new function "document" that allows to integrate, in a pod template, the content of an external document or image. +- pod: converter.py: instead of asking to OpenOffice to convert an odt file into pdf, doc, etc, converter.py can now ask to update an odt document (refresh indexes and table of contents, resolve external links). Resolving external links is needed if you use the new "document" function (excepted for images and pdfs). +- pod: new boolean parameter to the renderer: "forceOoCall". If True, OpenOffice will be contacted in server mode even if the result is asked in odt. This way, OpenOffice will update indexes and resolve external links. Set this parameter to True when using the new "document" function. +- pod: you may now create pod templates with OpenOffice 3.x (parsing problem with notes is solved) +- pod: OO conversion: connection to OO in server mode under Windows solved ("import socket" before "import uno") +- pod: instruction "do ... for ...' accepts now all Python objects implementing the iterator protocol, like iterators from Django query sets (querySet.iterator()) + +0.2.1 (2008-06-04) +- XHTML to ODT conversion: possibility to use 2 different predefined pod-styles to apply to "li" elements: the default one, and a second one named "podItemKeepWithNext" that ensures that the targeted "li" element will always stay on the same page as the paragraph below him. +- Minor bugfixes. + +0.2.0 (2008-03-23) +- Implementation of an "else" statement. +- "if" statements may be named in order to solve ambiguities when linking "if" and "else" statements. +- Besides "if" and "else" statements, an "if" expression is also proposed under the form of a function named "test" that is defined in the default pod context. + +0.1.0 (2008-03-10) +- Because of huge international pressure, version 0.0.4 was published as 0.1.0. From now on, the pod numbering scheme will follow the rule MAJOR.MINOR.BUGFIX: development of major functions will increment MAJOR (the first figure); development of minor functions will increment MINOR (the second figure) while bug fixes will increment BUGFIX (the third figure). +- From now on, pod eggs will be published on http://pypi.python.org. + +0.0.4 (2008-03-10) +- Management of XHTML tables. +- XHTML parser is now case-insensitive. + +0.0.3 (2008-01-24) +- Refactoring in the SAX parsers used in pod has improved overall design and performance. +- Pod replacements now work in headers and footers. +- A "from" clause can now complete any pod statement, allowing to include arbitrary ODT content. +- The "xhtml" function MUST now be used in "from" clauses ("do text from xhtml(xhtmlChunk)") and not in pod expressions anymore (users migrating from pod 0.0.2 to 0.0.3 and using the "xhtml" function will need to modify their pod templates). + +0.0.2 (2007-10-01) +- Possibility to integrate XHTML chunks into ODT documents. You can customize the way pod maps html elements or CSS styles to ODT styles. +- New automated test system. + +0.0.1 (2007-05-22) +- First version of appy.pod. diff --git a/gen/__init__.py b/gen/__init__.py new file mode 100755 index 0000000..33a2aab --- /dev/null +++ b/gen/__init__.py @@ -0,0 +1,459 @@ +# ------------------------------------------------------------------------------ +import re +from appy.gen.utils import sequenceTypes, PageDescr + +# Default Appy permissions ----------------------------------------------------- +r, w, d = ('read', 'write', 'delete') + +# Descriptor classes used for refining descriptions of elements in types +# (pages, groups,...) ---------------------------------------------------------- +class Page: + def __init__(self, name, phase='main', show=True): + self.name = name + self.phase = phase + self.show = show + +# ------------------------------------------------------------------------------ +class Type: + '''Basic abstract class for defining any appy type.''' + def __init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, searchable, + specificReadPermission, specificWritePermission, width, + height, master, masterValue): + # 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 tuple indicating the minimum and maximum + # occurrences of values. + self.multiplicity = multiplicity + # Type of the index on the values. If you want to represent a simple + # (ordered) list of values, specify None. If you want to + # index your values with unordered integers or with other types like + # strings (thus creating a dictionary of values instead of a list), + # specify a type specification for the index, like Integer() or + # String(). Note that this concept of "index" has nothing to do with + # the concept of "database index". + self.index = index + # Default value + self.default = default + # Is the field optional or not ? + self.optional = optional + # May the user configure a default value ? + self.editDefault = editDefault + # 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? Default is ('main', 'main'). pageShow + # indicates if the page must be shown or not. + self.page, self.phase, self.pageShow = PageDescr.getPageInfo(page, Page) + # Within self.page, in what group of fields must this field value + # appear? + self.group = group + # The following attribute allows to move a field back to a previous + # position (useful for content types that inherit from others). + self.move = move + # If specified "searchable", the field will be referenced in low-level + # indexing mechanisms for fast access and search functionalities. + 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 + # create/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". + self.specificReadPermission = specificReadPermission + self.specificWritePermission = specificWritePermission + # Widget width and height + self.width = width + self.height = height + # The behaviour of this field may depend on another, "master" field + self.master = master + if master: + self.master.slaves.append(self) + # When master has some value(s), there is impact on this field. + self.masterValue = masterValue + self.id = id(self) + self.type = self.__class__.__name__ + self.pythonType = None # The True corresponding Python type + self.slaves = [] # The list of slaves of this field + self.selfClass = None # The Python class to which this Type definition + # is linked. This will be computed at runtime. + + def isMultiValued(self): + '''Does this type definition allow to define multiple values?''' + res = False + maxOccurs = self.multiplicity[1] + if (maxOccurs == None) or (maxOccurs > 1): + res = True + return res + +class Integer(Type): + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.pythonType = long + +class Float(Type): + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.pythonType = float + +class String(Type): + # 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 + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, format=LINE, + show=True, page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, searchable, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.format = format + 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, basestring): + res = False + break + else: + if not isinstance(self.validator, Selection): + res = False + return res + +class Boolean(Type): + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, searchable, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.pythonType = bool + +class Date(Type): + # Possible values for "format" + WITH_HOUR = 0 + WITHOUT_HOUR = 1 + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, + format=WITH_HOUR, show=True, page='main', group=None, move=0, + searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, searchable, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.format = format + +class File(Type): + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None, + isImage=False): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.isImage = isImage + +class Ref(Type): + def __init__(self, klass=None, attribute=None, validator=None, + multiplicity=(0,1), index=None, default=None, optional=False, + editDefault=False, add=False, link=True, unlink=False, + back=None, isBack=False, show=True, page='main', group=None, + showHeaders=False, shownInfo=(), wide=False, select=None, + move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, validator, multiplicity, index, default, optional, + editDefault, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.klass = klass + self.attribute = attribute + self.add = add # May the user add new objects through this ref ? + self.link = link # May the user link existing objects through this ref? + self.unlink = unlink # May the user unlink existing objects? + self.back = back + self.isBack = isBack # Should always be False + self.showHeaders = showHeaders # When displaying a tabular list of + # referenced objects, must we show the table headers? + self.shownInfo = shownInfo # When displaying referenced object(s), + # we will display its title + all other fields whose names are listed + # in this attribute. + self.wide = wide # If True, the table of references will be as wide + # as possible + self.select = select # If a method is defined here, it will be used to + # filter the list of available tied objects. + +class Computed(Type): + def __init__(self, validator=None, multiplicity=(0,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, method=None, plainText=True, + master=None, masterValue=None): + Type.__init__(self, None, multiplicity, index, default, optional, + False, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.method = method # The method used for computing the field value + self.plainText = plainText # Does field computation produce pain text + # or XHTML? + +class Action(Type): + '''An action is a workflow-independent Python method that can be triggered + by the user on a given gen-class. For example, the custom installation + procedure of a gen-application is implemented by an action on the custom + tool class. An action is rendered as a button.''' + def __init__(self, validator=None, multiplicity=(1,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, action=None, master=None, + masterValue=None): + Type.__init__(self, None, (0,1), index, default, optional, + False, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + self.action = action # Can be a single method or a list/tuple of methods + + def __call__(self, obj): + '''Calls the action on p_obj.''' + try: + if type(self.action) in sequenceTypes: + # There are multiple Python methods + res = [True, ''] + for act in self.action: + actRes = act(obj) + if type(actRes) in sequenceTypes: + res[0] = res[0] and actRes[0] + res[1] = res[1] + '\n' + actRes[1] + else: + res[0] = res[0] and actRes + else: + # There is only one Python method + actRes = self.action(obj) + if type(actRes) in 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 + except Exception, e: + res = (False, str(e)) + return res + +class Info(Type): + '''An info is a field whose purpose is to present information + (text, html...) to the user.''' + def __init__(self, validator=None, multiplicity=(1,1), index=None, + default=None, optional=False, editDefault=False, show=True, + page='main', group=None, move=0, searchable=False, + specificReadPermission=False, specificWritePermission=False, + width=None, height=None, master=None, masterValue=None): + Type.__init__(self, None, (0,1), index, default, optional, + False, show, page, group, move, False, + specificReadPermission, specificWritePermission, width, + height, master, masterValue) + +# Workflow-specific types ------------------------------------------------------ +class State: + def __init__(self, permissions, initial=False, phase='main', show=True): + self.permissions = permissions #~{s_permissionName:[s_roleName]}~ This + # dict 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.initial = initial + self.phase = phase + self.show = show + def getUsedRoles(self): + res = set() + for roleValue in self.permissions.itervalues(): + if isinstance(roleValue, basestring): + res.add(roleValue) + elif roleValue: + for role in roleValue: + res.add(role) + return list(res) + def getTransitions(self, transitions, selfIsFromState=True): + '''Among p_transitions, returns those whose fromState is p_self (if + p_selfIsFromState is True) or those whose toState is p_self (if + p_selfIsFromState is False).''' + res = [] + for t in transitions: + if self in t.getStates(selfIsFromState): + res.append(t) + return res + def getPermissions(self): + '''If you get the permissions mapping through self.permissions, dict + values may be of different types (a list of roles, a single role or + None). Iy you call this method, you will always get a list which + may be empty.''' + res = {} + for permission, roleValue in self.permissions.iteritems(): + if roleValue == None: + res[permission] = [] + elif isinstance(roleValue, basestring): + res[permission] = [roleValue] + else: + res[permission] = roleValue + return res + +class Transition: + def __init__(self, states, condition=True, action=None, notify=None): + self.states = states # In its simpler form, it is a tuple with 2 + # states: (fromState, toState). But it can also be a tuple of several + # (fromState, toState) sub-tuples. 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.condition = condition + self.action = action + self.notify = notify # If not None, it is a method telling who must be + # notified by email after the transition has been executed. + + def getUsedRoles(self): + '''If self.condition is specifies a role.''' + res = [] + if isinstance(self.condition, basestring): + res = [self.condition] + return res + + def isSingle(self): + '''If this transitions is only define between 2 states, returns True. + Else, returns False.''' + return isinstance(self.states[0], State) + + def getStates(self, fromStates=True): + '''Returns the fromState(s) if p_fromStates is True, the toState(s) + else. If you want to get the states grouped in tuples + (fromState, toState), simply use self.states.''' + res = [] + stateIndex = 1 + if fromStates: + stateIndex = 0 + if self.isSingle(): + res.append(self.states[stateIndex]) + else: + for states in self.states: + theState = states[stateIndex] + if theState not in res: + res.append(theState) + return res + + 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 + +class Permission: + '''If you need to define a specific read or write permission of a given + attribute of an Appy type, you use the specific boolean parameters + "specificReadPermission" or "specificWritePermission" for this attribute. + 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.''' + def __init__(self, fieldDescriptor): + self.fieldDescriptor = fieldDescriptor + +class ReadPermission(Permission): pass +class WritePermission(Permission): pass + +# ------------------------------------------------------------------------------ +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.''' + pass + +# ------------------------------------------------------------------------------ +class Tool: + '''If you want so define a custom tool class, she must inherit from this + class.''' +class Flavour: + '''A flavour represents a given group of configuration options. If you want + to define a custom flavour class, she must inherit from this class.''' + def __init__(self, name): self.name = name + +# ------------------------------------------------------------------------------ +class Config: + '''If you want to specify some configuration parameters for appy.gen and + your application, please create an instance of this class and modify its + attributes. You may put your instance anywhere in your application + (main package, sub-package, etc).''' + + # The default Config instance, used if the application does not give one. + defaultConfig = None + def getDefault(): + if not Config.defaultConfig: + Config.defaultConfig = Config() + return Config.defaultConfig + getDefault = staticmethod(getDefault) + + def __init__(self): + # For every language code that you specify in this list, appy.gen will + # produce and maintain translation files. + self.languages = ['en'] + # People having one of these roles will be able to create instances + # of classes defined in your application. + self.defaultCreators = ['Manager', 'Owner'] + # If True, the following flag will produce a minimalist Plone, where + # some actions, portlets or other stuff less relevant for building + # web applications, are removed or hidden. Using this produces + # effects on your whole Plone site! + self.minimalistPlone = False + # If you want to replace the Plone front page with a page coming from + # your application, use the following parameter. Setting + # frontPage = True will replace the Plone front page with a page + # whose content will come fron i18n label "front_page_text". + self.frontPage = False +# ------------------------------------------------------------------------------ diff --git a/gen/descriptors.py b/gen/descriptors.py new file mode 100644 index 0000000..11a53ca --- /dev/null +++ b/gen/descriptors.py @@ -0,0 +1,193 @@ +# ------------------------------------------------------------------------------ +from appy.gen import State, Transition, Type + +# ------------------------------------------------------------------------------ +class Descriptor: # Abstract + def __init__(self, klass, orderedAttributes, generator): + self.klass = klass # The corresponding Python class + self.orderedAttributes = orderedAttributes # Names of the static appy- + # compliant attributes declared in self.klass + self.generator = generator # A reference to the code generator. + + def __repr__(self): return '' % self.klass.__name__ + +class ClassDescriptor(Descriptor): + '''This class gives information about an Appy class.''' + def getOrderedAppyAttributes(self): + '''Returns the appy types for all attributes of this class and parent + class(es).''' + res = [] + # First, get the attributes for the current class + for attrName in self.orderedAttributes: + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, Type): + res.append( (attrName, attrValue) ) + # 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: + res = baseClassDescr.getOrderedAppyAttributes() + res + 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): + '''Gets the phases defined on fields of this class.''' + res = [] + for fieldName, appyType in self.getOrderedAppyAttributes(): + if appyType.phase not in res: + res.append(appyType.phase) + return res + +class WorkflowDescriptor(Descriptor): + '''This class gives information about an Appy workflow.''' + + def _getWorkflowElements(self, elemType): + res = [] + for attrName in dir(self.klass): + attrValue = getattr(self.klass, attrName) + condition = False + if elemType == 'states': + condition = isinstance(attrValue, State) + elif elemType == 'transitions': + condition = isinstance(attrValue, Transition) + elif elemType == 'all': + condition = isinstance(attrValue, State) or \ + isinstance(attrValue, Transition) + if condition: + res.append(attrValue) + return res + + def getStates(self): + return self._getWorkflowElements('states') + + def getTransitions(self): + return self._getWorkflowElements('transitions') + + def getStateNames(self, ordered=False): + res = [] + attrs = dir(self.klass) + allAttrs = attrs + if ordered: + attrs = self.orderedAttributes + allAttrs = dir(self.klass) + for attrName in attrs: + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, State): + res.append(attrName) + # Complete the list with inherited states. For the moment, we are unable + # to sort inherited states. + for attrName in allAttrs: + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, State) and (attrName not in attrs): + res.insert(0, attrName) + return res + + def getInitialStateName(self): + res = None + for attrName in dir(self.klass): + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, State) and attrValue.initial: + res = attrName + break + return res + + def getTransitionNamesOf(self, transitionName, transition, + limitToFromState=None): + '''Appy p_transition may correspond to several transitions of the + concrete workflow engine used. This method returns in a list the + name(s) of the "concrete" transition(s) corresponding to + p_transition.''' + res = [] + if transition.isSingle(): + res.append(transitionName) + else: + for fromState, toState in transition.states: + if not limitToFromState or \ + (limitToFromState and (fromState == limitToFromState)): + fromStateName = self.getNameOf(fromState) + toStateName = self.getNameOf(toState) + res.append('%s%s%sTo%s%s' % (transitionName, + fromStateName[0].upper(), fromStateName[1:], + toStateName[0].upper(), toStateName[1:])) + return res + + def getTransitionNames(self, limitToTransitions=None, limitToFromState=None, + withLabels=False): + '''Returns the name of all "concrete" transitions corresponding to the + Appy transitions of this worlflow. If p_limitToTransitions is not + None, it represents a list of Appy transitions and the result is a + list of the names of the "concrete" transitions that correspond to + those transitions only. If p_limitToFromState is not None, it + represents an Appy state; only transitions having this state as start + state will be taken into account. If p_withLabels is True, the method + returns a list of tuples (s_transitionName, s_transitionLabel); the + label being the name of the Appy transition.''' + res = [] + for attrName in dir(self.klass): + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, Transition): + # We encountered a transition. + t = attrValue + tName = attrName + if not limitToTransitions or \ + (limitToTransitions and t in limitToTransitions): + # We must take this transition into account according to + # param "limitToTransitions". + if (not limitToFromState) or \ + (limitToFromState and \ + t.hasState(limitToFromState, isFrom=True)): + # We must take this transition into account according + # to param "limitToFromState" + tNames = self.getTransitionNamesOf( + tName, t, limitToFromState) + if not withLabels: + res += tNames + else: + for tn in tNames: + res.append((tn, tName)) + return res + + def getEndStateName(self, transitionName): + '''Returns the name of the state where the "concrete" transition named + p_transitionName ends.''' + res = None + for attrName in dir(self.klass): + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, Transition): + # We got a transition. + t = attrValue + tName = attrName + if t.isSingle(): + if transitionName == tName: + endState = t.states[1] + res = self.getNameOf(endState) + else: + transNames = self.getTransitionNamesOf(tName, t) + if transitionName in transNames: + endState = t.states[transNames.index(transitionName)][1] + res = self.getNameOf(endState) + return res + + def getNameOf(self, stateOrTransition): + '''Gets the Appy name of a p_stateOrTransition.''' + res = None + for attrName in dir(self.klass): + attrValue = getattr(self.klass, attrName) + if attrValue == stateOrTransition: + res = attrName + break + return res +# ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py new file mode 100755 index 0000000..3482b99 --- /dev/null +++ b/gen/generator.py @@ -0,0 +1,420 @@ +# ------------------------------------------------------------------------------ +import os, os.path, sys, parser, symbol, token +from optparse import OptionParser +from appy.gen import Type, State, Config, Tool, Flavour +from appy.gen.descriptors import * +from appy.gen.utils import produceNiceMessage +import appy.pod, appy.pod.renderer +from appy.shared.utils import FolderDeleter + +# ------------------------------------------------------------------------------ +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) + def __init__(self, pyFile): + f = file(pyFile) + fContent = f.read() + f.close() + 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 + +# ------------------------------------------------------------------------------ +WARN_NO_TEMPLATE = 'Warning: the code generator should have a folder "%s" ' \ + 'containing all code templates.' +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, outputFolder, options): + self.application = application + # Determine application name + self.applicationName = os.path.basename(application) + if application.endswith('.py'): + self.applicationName = self.applicationName[:-3] + # Determine output folder (where to store the generated product) + self.outputFolder = '%s/%s' % (outputFolder, self.applicationName) + self.options = options + # Determine templates folder + exec 'import %s as genModule' % self.__class__.__module__ + self.templatesFolder = os.path.join(os.path.dirname(genModule.__file__), + 'templates') + if not os.path.exists(self.templatesFolder): + print WARN_NO_TEMPLATE % self.templatesFolder + # Default descriptor classes + self.classDescriptor = ClassDescriptor + self.workflowDescriptor = WorkflowDescriptor + self.customToolClassDescriptor = ClassDescriptor + self.customFlavourClassDescriptor = ClassDescriptor + # Custom tool and flavour classes, if they are defined in the + # application + self.customToolDescr = None + self.customFlavourDescr = None + # 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.workflows = [] + self.initialize() + self.config = Config.getDefault() + + def determineAppyType(self, klass): + '''Is p_klass an Appy class ? An Appy workflow? None of this ? + If it (or a parent) declares at least one appy type definition, + it will be considered an Appy class. If it (or a parent) declares at + least one state definition, it will be considered an Appy + workflow.''' + res = 'none' + for attrValue in klass.__dict__.itervalues(): + if isinstance(attrValue, Type): + res = 'class' + elif isinstance(attrValue, State): + res = 'workflow' + if not res: + for baseClass in klass.__bases__: + baseClassType = self.determineAppyType(baseClass) + if baseClassType != 'none': + res = baseClassType + break + return res + + IMPORT_ERROR = 'Warning: error while importing module %s (%s)' + SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)' + def walkModule(self, moduleName): + '''Visits a given (sub-*)module into the application.''' + try: + exec 'import %s' % moduleName + exec 'moduleObj = %s' % moduleName + moduleFile = moduleObj.__file__ + if moduleFile.endswith('.pyc'): + moduleFile = moduleFile[:-1] + astClasses = Ast(moduleFile).classes + except ImportError, ie: + # True import error or, simply, this is a simple folder within + # the application, not a sub-module. + print self.IMPORT_ERROR % (moduleName, str(ie)) + return + except SyntaxError, se: + print self.SYNTAX_ERROR % (moduleName, str(se)) + return + classType = type(Generator) + # Find all classes in this module + for moduleElemName in moduleObj.__dict__.keys(): + exec 'moduleElem = moduleObj.%s' % moduleElemName + if (type(moduleElem) == classType) and \ + (moduleElem.__module__ == moduleObj.__name__): + # We have found a Python class definition in this module. + appyType = self.determineAppyType(moduleElem) + if appyType != 'none': + # Produce a list of static class attributes (in the order + # of their definition). + attrs = astClasses[moduleElem.__name__].attributes + if appyType == 'class': + if issubclass(moduleElem, Tool): + descrClass = self.customToolClassDescriptor + self.customToolDescr = descrClass( + moduleElem, attrs, self) + elif issubclass(moduleElem, Flavour): + descrClass = self.customFlavourClassDescriptor + self.customFlavourDescr = descrClass( + moduleElem, attrs, self) + else: + descrClass = self.classDescriptor + self.classes.append( + descrClass(moduleElem, attrs, self)) + elif appyType == 'workflow': + descrClass = self.workflowDescriptor + self.workflows.append( + descrClass(moduleElem, attrs, self)) + elif isinstance(moduleElem, Config): + self.config = moduleElem + + # Walk potential sub-modules + if moduleFile.find('__init__.py') != -1: + # Potentially, sub-modules exist + moduleFolder = os.path.dirname(moduleFile) + for elem in os.listdir(moduleFolder): + 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) + self.walkModule(subModuleName) + + 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) + if os.path.isfile(self.application): + appName = os.path.splitext(appName)[0] + self.walkModule(appName) + 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.iteritems(): + 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 classDescr in self.classes: self.generateClass(classDescr) + for wfDescr in self.workflows: self.generateWorkflow(wfDescr) + self.finalize() + print 'Done.' + +# ------------------------------------------------------------------------------ +ERROR_CODE = 1 +VALID_PRODUCT_TYPES = ('plone25', 'odt') +APP_NOT_FOUND = 'Application not found at %s.' +WRONG_NG_OF_ARGS = 'Wrong number of arguments.' +WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.' +PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \ + 'following: %s' % str(VALID_PRODUCT_TYPES) +C_OPTION = 'Removes from i18n files all labels that are not automatically ' \ + 'generated from your gen-application. It can be useful during ' \ + 'development, when you do lots of name changes (classes, ' \ + 'attributes, states, transitions, etc): in this case, the Appy ' \ + 'i18n label generation machinery produces lots of labels that ' \ + 'then become obsolete.' +S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \ + 'generated i18n files, you will find first all labels ' \ + 'that are automatically generated by appy.gen, in some logical ' \ + 'order (ie: field-related labels appear together, in the order ' \ + 'they are declared in the gen-class). Then, if you have added ' \ + 'labels manually, they will appear afterwards. Sorting labels ' \ + 'may not be desired under development. Indeed, when no sorting ' \ + 'occurs, every time you add or modify a field, class, state, etc, ' \ + 'newly generated labels will all appear together at the end of ' \ + 'the file; so it will be easy to translate them all. When sorting ' \ + 'occurs, those elements may be spread at different places in the ' \ + 'i18n file. When the development is finished, it may be a good ' \ + 'idea to sort the labels to get a clean and logically ordered ' \ + 'set of translation files.' + +class GeneratorScript: + '''usage: %prog [options] app productType outputFolder + + "app" is the path to your Appy application, which may be a + Python module (= a file than ends with .py) or a Python + package (= a folder containing a file named __init__.py). + Your app may reside anywhere (but it needs to be + accessible by the underlying application server, ie Zope), + excepted within the generated product. Typically, if you + generate a Plone product, it may reside within + /lib/python, but not within the + generated product (typically stored in + /Products). + "productType" is the kind of product you want to generate + (currently, only "plone25" and 'odt' are supported; + in the near future, the "plone25" target will also produce + Plone 3-compliant code that will still work with + Plone 2.5). + "outputFolder" is the folder where the product will be generated. + For example, if you specify /my/output/folder for your + application /home/gde/MyApp.py, this script will create + a folder /my/output/folder/MyApp and put in it the + generated product. + + Example: generating a Plone product + ----------------------------------- + In your Zope instance named myZopeInstance, create a folder + "myZopeInstance/lib/python/MyApp". Create into it your Appy application + (we suppose here that it is a Python package, containing a __init__.py + file and other files). Then, chdir into this folder and type + "python /gen/generator.py . plone25 ../../../Products" and the + product will be generated in myZopeInstance/Products/MyApp. + "python" must refer to a Python interpreter that knows package appy.''' + + def generateProduct(self, options, application, productType, outputFolder): + exec 'from appy.gen.%s.generator import Generator' % productType + Generator(application, outputFolder, options).run() + + def manageArgs(self, parser, options, args): + # Check number of args + if len(args) != 3: + print WRONG_NG_OF_ARGS + parser.print_help() + sys.exit(ERROR_CODE) + # Check productType + if args[1] not in VALID_PRODUCT_TYPES: + print PRODUCT_TYPE_ERROR + sys.exit(ERROR_CODE) + # Check existence of application + if not os.path.exists(args[0]): + print APP_NOT_FOUND % args[0] + sys.exit(ERROR_CODE) + # Check existence of outputFolder basic type + if not os.path.exists(args[2]): + print WRONG_OUTPUT_FOLDER + sys.exit(ERROR_CODE) + # Convert all paths in absolute paths + for i in (0,2): + args[i] = os.path.abspath(args[i]) + def run(self): + optParser = OptionParser(usage=GeneratorScript.__doc__) + optParser.add_option("-c", "--i18n-clean", action='store_true', + dest='i18nClean', default=False, help=C_OPTION) + optParser.add_option("-s", "--i18n-sort", action='store_true', + dest='i18nSort', default=False, help=S_OPTION) + (options, args) = optParser.parse_args() + try: + self.manageArgs(optParser, options, args) + print 'Generating %s product in %s...' % (args[1], args[2]) + self.generateProduct(options, *args) + except GeneratorError, ge: + sys.stderr.write(str(ge)) + sys.stderr.write('\n') + optParser.print_help() + sys.exit(ERROR_CODE) + +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + GeneratorScript().run() +# ------------------------------------------------------------------------------ diff --git a/gen/odt/__init__.py b/gen/odt/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gen/odt/generator.py b/gen/odt/generator.py new file mode 100755 index 0000000..c1f4ad2 --- /dev/null +++ b/gen/odt/generator.py @@ -0,0 +1,54 @@ +'''This file contains the main Generator class used for generating an + ODT file from an Appy application.''' + +# ------------------------------------------------------------------------------ +import os, os.path +from appy.gen import Page +from appy.gen.utils import produceNiceMessage +from appy.gen.generator import Generator as AbstractGenerator + +# ------------------------------------------------------------------------------ +class Generator(AbstractGenerator): + '''This generator generates ODT files from an Appy application.''' + + def __init__(self, *args, **kwargs): + AbstractGenerator.__init__(self, *args, **kwargs) + self.repls = {'generator': self} + + def finalize(self): + pass + + def getOdtFieldLabel(self, fieldName): + '''Given a p_fieldName, this method creates the label as it will appear + in the ODT file.''' + return '%s' % \ + (fieldName, produceNiceMessage(fieldName)) + + def generateClass(self, classDescr): + '''Is called each time an Appy class is found in the application.''' + repls = self.repls.copy() + repls['classDescr'] = classDescr + self.copyFile('basic.odt', repls, + destName='%sEdit.odt' % classDescr.klass.__name__, isPod=True) + + def fieldIsStaticallyInvisible(self, field): + '''This method determines if p_field is always invisible. It can be + verified for example if field.type.show is the boolean value False or + if the page where the field must be displayed has a boolean attribute + "show" having the boolean value False.''' + if (type(field.show) == bool) and not field.show: return True + if (type(field.pageShow) == bool) and not field.pageShow: return True + return False + + undumpable = ('Ref', 'Action', 'File', 'Computed') + def getRelevantAttributes(self, classDescr): + '''Some fields, like computed fields or actions, should not be dumped + into the ODT file. This method returns the list of "dumpable" + fields.''' + res = [] + for fieldName, field in classDescr.getOrderedAppyAttributes(): + if (field.type not in self.undumpable) and \ + (not self.fieldIsStaticallyInvisible(field)): + res.append((fieldName, field)) + return res +# ------------------------------------------------------------------------------ diff --git a/gen/odt/templates/basic.odt b/gen/odt/templates/basic.odt new file mode 100644 index 0000000..7f3816a Binary files /dev/null and b/gen/odt/templates/basic.odt differ diff --git a/gen/plone25/__init__.py b/gen/plone25/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py new file mode 100755 index 0000000..a3595a0 --- /dev/null +++ b/gen/plone25/descriptors.py @@ -0,0 +1,666 @@ +'''Descriptor classes defined in this file are "intermediary" classes that + gather, from the user application, information about concepts (like Archetype + classes or DC workflow definitions) that will eventually be dumped into the + generated application. Typically they have methods named "generate..." that + produce generated code.''' + +# ------------------------------------------------------------------------------ +import types, copy +from model import ModelClass, Flavour, flavourAttributePrefixes +from utils import stringify +import appy.gen +import appy.gen.descriptors +from appy.gen.po import PoMessage +from appy.gen import Date, String, State, Transition, Type +from appy.gen.utils import GroupDescr, PageDescr, produceNiceMessage +TABS = 4 # Number of blanks in a Python indentation. + +# ------------------------------------------------------------------------------ +class ArchetypeFieldDescriptor: + '''This class allows to gather information needed to generate an Archetypes + definition (field + widget) from an Appy type. An Appy type is used for + defining the type of attributes defined in the user application.''' + + singleValuedTypes = ('Integer', 'Float', 'Boolean', 'Date', 'File') + # Although Appy allows to specify a multiplicity[0]>1 for those types, it is + # not supported by Archetypes. So we will always generate single-valued type + # definitions for them. + specialParams = ('title', 'description') + + 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 + self.walkAppyType() + + def __repr__(self): + return '' % (self.fieldName, self.classDescr) + + def getFlavourAttributeMessage(self, fieldName): + '''Some attributes generated on the Flavour class need a specific + default message, returned by this method.''' + res = fieldName + for prefix in flavourAttributePrefixes: + if fieldName.startswith(prefix): + messageId = 'MSG_%s' % prefix + res = getattr(PoMessage, messageId) + if res.find('%s') != -1: + # I must complete the message with the field name. + res = res % fieldName.split('_')[-1] + break + return res + + def produceMessage(self, msgId, isLabel=True): + '''Gets the default label or description (if p_isLabel is False) for + i18n message p_msgId.''' + default = ' ' + produceNice = False + if isLabel: + produceNice = True + default = self.fieldName + # Some attributes need a specific predefined message + if isinstance(self.classDescr, FlavourClassDescriptor): + default = self.getFlavourAttributeMessage(self.fieldName) + if default != self.fieldName: produceNice = False + msg = PoMessage(msgId, '', default) + if produceNice: + msg.produceNiceDefault() + return msg + + def walkBasicType(self): + '''How to dump a basic type?''' + self.fieldType = '%sField' % self.appyType.type + self.widgetType = "%sWidget" % self.appyType.type + if self.appyType.type == 'Date': + self.fieldType = 'DateTimeField' + self.widgetType = 'CalendarWidget' + if self.appyType.format == Date.WITHOUT_HOUR: + self.widgetParams['show_hm'] = False + elif self.appyType.type == 'Float': + self.widgetType = 'DecimalWidget' + elif self.appyType.type == 'File': + if self.appyType.isImage: + self.fieldType = 'ImageField' + self.widgetType = 'ImageWidget' + self.fieldParams['storage'] = 'python:AttributeStorage()' + + def walkString(self): + '''How to generate an Appy String?''' + if self.appyType.format == String.LINE: + if self.appyType.isSelection(): + if self.appyType.isMultiValued(): + self.fieldType = 'LinesField' + self.widgetType = 'MultiSelectionWidget' + self.fieldParams['multiValued'] = True + else: + self.fieldType = 'StringField' + self.widgetType = 'SelectionWidget' + self.widgetParams['format'] = 'select' + # Elements common to all selection fields + methodName = 'list_%s_values' % self.fieldName + self.fieldParams['vocabulary'] = methodName + self.classDescr.addSelectMethod( + methodName, self, self.appyType.isMultiValued()) + self.fieldParams['enforceVocabulary'] = True + else: + self.fieldType = 'StringField' + self.widgetType = 'StringWidget' + self.widgetParams['size'] = 50 + if self.appyType.width: + self.widgetParams['size'] = self.appyType.width + # Manage index + if self.appyType.searchable: + self.fieldParams['index'] = 'FieldIndex' + elif self.appyType.format == String.TEXT: + self.fieldType = 'TextField' + self.widgetType = 'TextAreaWidget' + if self.appyType.height: + self.widgetParams['rows'] = self.appyType.height + elif self.appyType.format == String.XHTML: + self.fieldType = 'TextField' + self.widgetType = 'RichWidget' + self.fieldParams['allowable_content_types'] = ('text/html',) + self.fieldParams['default_output_type'] = "text/html" + else: + self.fieldType = 'StringField' + self.widgetType = 'StringWidget' + # Manage searchability + if self.appyType.searchable: + self.fieldParams['searchable'] = True + + def walkComputed(self): + '''How to generate a computed field? We generate an Archetypes String + field.''' + self.fieldType = 'StringField' + self.widgetType = 'StringWidget' + self.widgetParams['visible'] = False # Archetypes will believe the + # field is invisible; we will display it ourselves (like for Ref fields) + + def walkAction(self): + '''How to generate an action field ? We generate an Archetypes String + field.''' + self.fieldType = 'StringField' + self.widgetType = 'StringWidget' + self.widgetParams['visible'] = False # Archetypes will believe the + # field is invisible; we will display it ourselves (like for Ref fields) + # Add action-specific i18n messages + for suffix in ('ok', 'ko'): + label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName, + suffix) + msg = PoMessage(label, '', + getattr(PoMessage, 'ACTION_%s' % suffix.upper())) + self.generator.labels.append(msg) + self.classDescr.labelsToPropagate.append(msg) + + def walkRef(self): + '''How to generate a Ref?''' + relationship = '%s_%s_rel' % (self.classDescr.name, self.fieldName) + self.fieldType = 'ReferenceField' + self.widgetType = 'ReferenceWidget' + self.fieldParams['relationship'] = relationship + if self.appyType.isMultiValued(): + self.fieldParams['multiValued'] = True + self.widgetParams['visible'] = False + # Update the list of referers + self.generator.addReferer(self, relationship) + # Add the widget label for the back reference + refClassName = ArchetypesClassDescriptor.getClassName( + self.appyType.klass) + if issubclass(self.appyType.klass, ModelClass): + refClassName = self.applicationName + self.appyType.klass.__name__ + elif issubclass(self.appyType.klass, appy.gen.Tool): + refClassName = '%sTool' % self.applicationName + elif issubclass(self.appyType.klass, appy.gen.Flavour): + refClassName = '%sFlavour' % self.applicationName + backLabel = "%s_%s_back" % (refClassName, self.appyType.back.attribute) + poMsg = PoMessage(backLabel, '', self.appyType.back.attribute) + poMsg.produceNiceDefault() + self.generator.labels.append(poMsg) + + def walkInfo(self): + '''How to generate an Info field? We generate an Archetypes String + field.''' + self.fieldType = 'StringField' + self.widgetType = 'StringWidget' + self.widgetParams['visible'] = False # Archetypes will believe the + # field is invisible; we will display it ourselves (like for Ref fields) + + alwaysAValidatorFor = ('Ref', 'Integer', 'Float') + def walkAppyType(self): + '''Walks into the Appy type definition and gathers data about the + Archetype elements to generate.''' + # Manage things common to all Appy types + # - special accessor for fields "title" and "description" + if self.fieldName in self.specialParams: + self.fieldParams['accessor'] = self.fieldName.capitalize() + # - default value + if self.appyType.default != None: + self.fieldParams['default'] = self.appyType.default + # - required? + if self.appyType.multiplicity[0] >= 1: + if self.appyType.type != 'Ref': + # Indeed, if it is a ref appy will manage itself field updates + # in at_post_create_script, so Archetypes must not enforce + # required=True + self.fieldParams['required'] = True + # - optional ? + if self.appyType.optional: + Flavour._appy_addOptionalField(self) + self.widgetParams['condition'] = ' python: ' \ + 'here.fieldIsUsed("%s")'% self.fieldName + # - edit default value ? + if self.appyType.editDefault: + Flavour._appy_addDefaultField(self) + methodName = 'getDefaultValueFor%s' % self.fieldName + self.fieldParams['default_method'] = methodName + self.classDescr.addDefaultMethod(methodName, self) + # - searchable ? + if self.appyType.searchable and (self.appyType.type != 'String'): + self.fieldParams['index'] = 'FieldIndex' + # - slaves ? + if self.appyType.slaves: + self.widgetParams['visible'] = False # Archetypes will believe the + # field is invisible; we will display it ourselves (like for Ref + # fields) + # - need to generate a field validator? + # In all cases, add an i18n message for the validation error for this + # field. + label = '%s_%s_valid' % (self.classDescr.name, self.fieldName) + poMsg = PoMessage(label, '', PoMessage.DEFAULT_VALID_ERROR) + self.generator.labels.append(poMsg) + if (type(self.appyType.validator) == types.FunctionType) or \ + (type(self.appyType.validator) == type(String.EMAIL)) or \ + (self.appyType.type in self.alwaysAValidatorFor): + # For references, we always add a validator because gen validates + # itself things like multiplicities; + # For integers and floats, we also need validators because, by + # default, Archetypes produces an exception if the field value does + # not have the correct type, for example. + methodName = 'validate_%s' % self.fieldName + # Add a validate method for this + specificType = None + if self.appyType.type in self.alwaysAValidatorFor: + specificType = self.appyType.type + self.classDescr.addValidateMethod(methodName, label, self, + specificType=specificType) + # Manage specific permissions + permFieldName = '%s %s' % (self.classDescr.name, self.fieldName) + if self.appyType.specificReadPermission: + self.fieldParams['read_permission'] = '%s: Read %s' % \ + (self.generator.applicationName, permFieldName) + if self.appyType.specificWritePermission: + self.fieldParams['write_permission'] = '%s: Write %s' % \ + (self.generator.applicationName, permFieldName) + # i18n labels + i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName) + wp = self.widgetParams + wp['label'] = self.fieldName + wp['label_msgid'] = '%s' % i18nPrefix + wp['description'] = '%sDescr' % i18nPrefix + wp['description_msgid'] = '%s_descr' % i18nPrefix + wp['i18n_domain'] = self.applicationName + # Create labels for generating them in i18n files. + messages = self.generator.labels + messages.append(self.produceMessage(wp['label_msgid'])) + messages.append(self.produceMessage(wp['description_msgid'], + isLabel=False)) + # Create i18n messages linked to pages and phases + messages = self.generator.labels + pageMsgId = '%s_page_%s' % (self.classDescr.name, self.appyType.page) + phaseMsgId = '%s_phase_%s' % (self.classDescr.name, self.appyType.phase) + pagePoMsg = PoMessage(pageMsgId, '', + produceNiceMessage(self.appyType.page)) + phasePoMsg = PoMessage(phaseMsgId, '', + produceNiceMessage(self.appyType.phase)) + for poMsg in (pagePoMsg, phasePoMsg): + if poMsg not in messages: + messages.append(poMsg) + self.classDescr.labelsToPropagate.append(poMsg) + # Create i18n messages linked to groups + if self.appyType.group: + groupName, cols = GroupDescr.getGroupInfo(self.appyType.group) + msgId = '%s_group_%s' % (self.classDescr.name, groupName) + poMsg = PoMessage(msgId, '', groupName) + poMsg.produceNiceDefault() + if poMsg not in messages: + messages.append(poMsg) + self.classDescr.labelsToPropagate.append(poMsg) + # Manage schemata + if self.appyType.page != 'main': + self.fieldParams['schemata'] = self.appyType.page + # Manage things which are specific to basic types + if self.appyType.type in self.singleValuedTypes: self.walkBasicType() + # Manage things which are specific to String types + elif self.appyType.type == 'String': self.walkString() + # Manage things which are specific to Computed types + elif self.appyType.type == 'Computed': self.walkComputed() + # Manage things which are specific to Actions + elif self.appyType.type == 'Action': self.walkAction() + # Manage things which are specific to reference types + elif self.appyType.type == 'Ref': self.walkRef() + # Manage things which are specific to info types + elif self.appyType.type == 'Info': self.walkInfo() + + def generate(self): + '''Produces the Archetypes field definition as a string.''' + res = '' + s = stringify + spaces = TABS + # Generate field name + res += ' '*spaces + self.fieldType + '(\n' + # Generate field parameters + spaces += TABS + for fParamName, fParamValue in self.fieldParams.iteritems(): + res += ' '*spaces + fParamName + '=' + s(fParamValue) + ',\n' + # Generate widget + res += ' '*spaces + 'widget=%s(\n' % self.widgetType + spaces += TABS + for wParamName, wParamValue in self.widgetParams.iteritems(): + res += ' '*spaces + wParamName + '=' + s(wParamValue) + ',\n' + # End of widget definition + spaces -= TABS + res += ' '*spaces + ')\n' + # End of field definition + spaces -= TABS + res += ' '*spaces + '),\n' + return res + +class ClassDescriptor(appy.gen.descriptors.ClassDescriptor): + '''Represents an Archetypes-compliant class.''' + def __init__(self, klass, orderedAttributes, generator): + appy.gen.descriptors.ClassDescriptor.__init__(self, klass, + orderedAttributes, generator) + self.schema = '' # The archetypes schema will be generated here + self.methods = '' # Needed method definitions will be generated here + # We remember here encountered pages and groups defined in the Appy + # type. Indeed, after having parsed all application classes, we will + # need to generate i18n labels for every child class of the class + # that declared pages and groups. + self.labelsToPropagate = [] #~[PoMessage]~ Some labels (like page, + # group or action names) need to be propagated in children classes + # (because they contain the class name). But at this time we don't know + # yet every sub-class. So we store those labels here; the Generator + # will propagate them later. + self.flavourFieldsToPropagate = [] # For this class, some fields have + # been defined on the Flavour class. Those fields need to be defined + # for child classes of this class as well, but at this time we don't + # know yet every sub-class. So we store field definitions here; the + # Generator will propagate them later. + + def generateSchema(self): + '''Generates the corresponding Archetypes schema in self.schema.''' + for attrName in self.orderedAttributes: + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, Type): + field = ArchetypeFieldDescriptor(attrName, attrValue, self) + self.schema += '\n' + field.generate() + + def addSelectMethod(self, methodName, fieldDescr, isMultivalued=False): + '''For the selection field p_fieldDescr I need to generate a method + named p_methodName that will generate the vocabulary for + p_fieldDescr.''' + # Generate the method signature + m = self.methods + s = stringify + spaces = TABS + m += '\n' + ' '*spaces + 'def %s(self):\n' % methodName + spaces += TABS + appyType = fieldDescr.appyType + if type(appyType.validator) in (list, tuple): + # Generate i18n messages for every possible value + f = fieldDescr + labels = [] + for value in appyType.validator: + msgLabel = '%s_%s_list_%s' % (f.classDescr.name, f.fieldName, + value) + labels.append(msgLabel) # I will need it later + poMsg = PoMessage(msgLabel, '', value) + poMsg.produceNiceDefault() + self.generator.labels.append(poMsg) + # Generate a method that returns a DisplayList + appName = self.generator.applicationName + allValues = appyType.validator + if not isMultivalued: + allValues = [''] + appyType.validator + labels.insert(0, 'choose_a_value') + m += ' '*spaces + 'return self._appy_getDisplayList' \ + '(%s, %s, %s)\n' % (s(allValues), s(labels), s(appName)) + self.methods = m + + def addValidateMethod(self, methodName, label, fieldDescr, + specificType=None): + '''For the field p_fieldDescr I need to generate a validation method. + If p_specificType is not None, it corresponds to the name of a type + like Ref, Integer or Float, for which specific validation is needed, + beyond the potential custom validation specified by a user-defined + validator method.''' + # Generate the method signature + m = self.methods + s = stringify + spaces = TABS + m += '\n' + ' '*spaces + 'def %s(self, value):\n' % methodName + spaces += TABS + m += ' '*spaces + 'return self._appy_validateField(%s, value, %s, ' \ + '%s)\n' % (s(fieldDescr.fieldName), s(label), s(specificType)) + self.methods = m + + def addDefaultMethod(self, methodName, fieldDescr): + '''When the default value of a field may be edited, we must add a method + that will gather the default value from the flavour.''' + m = self.methods + spaces = TABS + m += '\n' + ' '*spaces + 'def %s(self):\n' % methodName + spaces += TABS + m += ' '*spaces + 'return self.getDefaultValueFor("%s")\n' % \ + fieldDescr.fieldName + self.methods = m + +class ArchetypesClassDescriptor(ClassDescriptor): + '''Represents an Archetypes-compliant class that corresponds to an + application class.''' + predefined = False + def __init__(self, klass, orderedAttributes, generator): + ClassDescriptor.__init__(self, klass, orderedAttributes, generator) + if not hasattr(self, 'name'): + self.name = self.getClassName(klass) + self.generateSchema() + + def getClassName(klass): + '''Generates the name of the corresponding Archetypes class.''' + return klass.__module__.replace('.', '_') + '_' + klass.__name__ + getClassName = staticmethod(getClassName) + + def isAbstract(self): + '''Is self.klass abstract?''' + res = False + if self.klass.__dict__.has_key('abstract'): + 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 self.klass.__dict__.has_key('root'): + res = self.klass.__dict__['root'] + return res + + def isPod(self): + '''May this class be associated with POD templates?.''' + res = False + if self.klass.__dict__.has_key('pod') and self.klass.__dict__['pod']: + res = True + 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 theClass.__dict__.has_key('folder'): + res = theClass.__dict__['folder'] + else: + if theClass.__bases__: + res = self.isFolder(theClass.__bases__[0]) + return res + + def addGenerateDocMethod(self): + m = self.methods + spaces = TABS + m += '\n' + ' '*spaces + 'def generateDocument(self):\n' + spaces += TABS + m += ' '*spaces + "'''Generates a document from p_self.'''\n" + m += ' '*spaces + 'return self._appy_generateDocument()\n' + self.methods = m + +class ToolClassDescriptor(ClassDescriptor): + '''Represents the POD-specific fields that must be added to the tool.''' + predefined = True + def __init__(self, klass, generator): + ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator) + self.name = '%sTool' % generator.applicationName + def isFolder(self, klass=None): return True + def isRoot(self): return False + def addUnoValidator(self): + m = self.methods + spaces = TABS + m += '\n' + ' '*spaces + 'def validate_unoEnabledPython(self, value):\n' + spaces += TABS + m += ' '*spaces + 'return self._appy_validateUnoEnabledPython(value)\n' + self.methods = m + def generateSchema(self): + ClassDescriptor.generateSchema(self) + self.addUnoValidator() + +class FlavourClassDescriptor(ClassDescriptor): + '''Represents an Archetypes-compliant class that corresponds to the Flavour + for the generated application.''' + predefined = True + def __init__(self, klass, generator): + ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator) + self.name = '%sFlavour' % generator.applicationName + self.attributesByClass = klass._appy_classes + # We don't generate the schema automatically here because we need to + # add more fields. + def isFolder(self, klass=None): return True + def isRoot(self): return False + +class PodTemplateClassDescriptor(ClassDescriptor): + '''Represents a POD template.''' + predefined = True + def __init__(self, klass, generator): + ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator) + self.name = '%sPodTemplate' % generator.applicationName + def isRoot(self): return False + +class CustomToolClassDescriptor(ArchetypesClassDescriptor): + '''If the user defines a class that inherits from Tool, we will add those + fields to the tool.''' + predefined = False + def __init__(self, *args): + self.name = '%sTool' % args[2].applicationName + ArchetypesClassDescriptor.__init__(self, *args) + def generateSchema(self): + '''Custom tool fields may not use the variability mechanisms, ie + 'optional' or 'editDefault' attributes.''' + for attrName in self.orderedAttributes: + attrValue = getattr(self.klass, attrName) + if isinstance(attrValue, Type): + attrValue = copy.copy(attrValue) + attrValue.optional = False + attrValue.editDefault = False + field = ArchetypeFieldDescriptor(attrName, attrValue, self) + self.schema += '\n' + field.generate() + +class CustomFlavourClassDescriptor(CustomToolClassDescriptor): + def __init__(self, *args): + self.name = '%sFlavour' % args[2].applicationName + ArchetypesClassDescriptor.__init__(self, *args) + +class WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor): + '''Represents a workflow.''' + # How to map Appy permissions to Plone permissions ? + appyToPlonePermissions = { + 'read': ('View', 'Access contents information'), + 'write': ('Modify portal content',), + 'delete': ('Delete objects',), + } + def getPlonePermissions(self, permission): + '''Returns the Plone permission(s) that correspond to + Appy p_permission.''' + if self.appyToPlonePermissions.has_key(permission): + res = self.appyToPlonePermissions[permission] + elif isinstance(permission, basestring): + res = [permission] + else: + # Permission if an Appy permission declaration + className, fieldName = permission.fieldDescriptor.rsplit('.', 1) + if className.find('.') == -1: + # The related class resides in the same module as the workflow + fullClassName = '%s_%s' % ( + self.klass.__module__.replace('.', '_'), className) + else: + # className contains the full package name of the class + fullClassName = className.replace('.', '_') + # Read or Write ? + if permission.__class__.__name__ == 'ReadPermission': + access = 'Read' + else: + access = 'Write' + permName = '%s: %s %s %s' % (self.generator.applicationName, + access, fullClassName, fieldName) + res = [permName] + return res + + def getWorkflowName(klass): + '''Generates the name of the corresponding Archetypes workflow.''' + res = klass.__module__.replace('.', '_') + '_' + klass.__name__ + return res.lower() + getWorkflowName = staticmethod(getWorkflowName) + + def getStatesInfo(self, asDumpableCode=False): + '''Gets, in a dict, information for configuring states of the workflow. + If p_asDumpableCode is True, instead of returning a dict, this + method will return a string containing the dict that can be dumped + into a Python code file.''' + res = {} + transitions = self.getTransitions() + for state in self.getStates(): + stateName = self.getNameOf(state) + # We need the list of transitions that start from this state + outTransitions = state.getTransitions(transitions, + selfIsFromState=True) + tNames = self.getTransitionNames(outTransitions, + limitToFromState=state) + # Compute the permissions/roles mapping for this state + permissionsMapping = {} + for permission, roles in state.getPermissions().iteritems(): + for plonePerm in self.getPlonePermissions(permission): + permissionsMapping[plonePerm] = roles + # Add 'Review portal content' to anyone; this is not a security + # problem because we limit the triggering of every transition + # individually. + allRoles = self.generator.getAllUsedRoles() + if 'Manager' not in allRoles: allRoles.append('Manager') + permissionsMapping['Review portal content'] = allRoles + res[stateName] = (tNames, permissionsMapping) + if not asDumpableCode: + return res + # We must create the "Python code" version of this dict + newRes = '{' + for stateName, stateInfo in res.iteritems(): + transitions = ','.join(['"%s"' % tn for tn in stateInfo[0]]) + # Compute permissions + permissions = '' + for perm, roles in stateInfo[1].iteritems(): + theRoles = ','.join(['"%s"' % r for r in roles]) + permissions += '"%s": [%s],' % (perm, theRoles) + newRes += '\n "%s": ([%s], {%s}),' % \ + (stateName, transitions, permissions) + return newRes + '}' + + def getTransitionsInfo(self, asDumpableCode=False): + '''Gets, in a dict, information for configuring transitions of the + workflow. If p_asDumpableCode is True, instead of returning a dict, + this method will return a string containing the dict that can be + dumped into a Python code file.''' + res = {} + for tName in self.getTransitionNames(): + res[tName] = self.getEndStateName(tName) + if not asDumpableCode: + return res + # We must create the "Python code" version of this dict + newRes = '{' + for transitionName, endStateName in res.iteritems(): + newRes += '\n "%s": "%s",' % (transitionName, endStateName) + return newRes + '}' + + def getManagedPermissions(self): + '''Returns the Plone permissions of all Appy permissions managed by this + workflow.''' + res = set() + res.add('Review portal content') + for state in self.getStates(): + for permission in state.permissions.iterkeys(): + for plonePerm in self.getPlonePermissions(permission): + res.add(plonePerm) + return res + + def getScripts(self): + res = '' + wfName = WorkflowDescriptor.getWorkflowName(self.klass) + for tName in self.getTransitionNames(): + scriptName = '%s_do%s%s' % (wfName, tName[0].upper(), tName[1:]) + res += 'def %s(self, stateChange, **kw): do("%s", ' \ + 'stateChange, logger)\n' % (scriptName, tName) + return res +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py new file mode 100755 index 0000000..5ca1265 --- /dev/null +++ b/gen/plone25/generator.py @@ -0,0 +1,712 @@ +'''This file contains the main Generator class used for generating a + Plone 2.5-compliant product.''' + +# ------------------------------------------------------------------------------ +import os, os.path, re, sys +import appy.gen +from appy.gen import * +from appy.gen.po import PoMessage, PoFile, PoParser +from appy.gen.generator import Generator as AbstractGenerator +from model import ModelClass, PodTemplate, Flavour, Tool +from descriptors import ArchetypeFieldDescriptor, ArchetypesClassDescriptor, \ + WorkflowDescriptor, ToolClassDescriptor, \ + FlavourClassDescriptor, PodTemplateClassDescriptor, \ + CustomToolClassDescriptor, CustomFlavourClassDescriptor + +# Common methods that need to be defined on every Archetype class -------------- +COMMON_METHODS = ''' + def at_post_create_script(self): self._appy_onEdit(True) + def at_post_edit_script(self): self._appy_onEdit(False) + def post_validate(self, REQUEST=None, errors=None): + if not errors: self._appy_validateAllFields(REQUEST, errors) + def getTool(self): return self.%s + def getProductConfig(self): return Products.%s.config +''' +# ------------------------------------------------------------------------------ +class Generator(AbstractGenerator): + '''This generator generates a Plone 2.5-compliant product from a given + appy application.''' + poExtensions = ('.po', '.pot') + + def __init__(self, *args, **kwargs): + Flavour._appy_clean() + AbstractGenerator.__init__(self, *args, **kwargs) + # i18n labels to generate + self.labels = [] # i18n labels + self.toolName = '%sTool' % self.applicationName + self.flavourName = '%sFlavour' % self.applicationName + self.toolInstanceName = 'portal_%s' % self.applicationName.lower() + self.podTemplateName = '%sPodTemplate' % self.applicationName + self.portletName = '%s_portlet' % self.applicationName.lower() + self.queryName = '%s_query' % self.applicationName.lower() + self.skinsFolder = 'skins/%s' % self.applicationName + # The following dict, pre-filled in the abstract generator, contains a + # series of replacements that need to be applied to file templates to + # generate files. + commonMethods = COMMON_METHODS % \ + (self.toolInstanceName, self.applicationName) + self.repls.update( + {'toolName': self.toolName, 'flavourName': self.flavourName, + 'portletName': self.portletName, 'queryName': self.queryName, + 'toolInstanceName': self.toolInstanceName, + 'podTemplateName': self.podTemplateName, + 'macros': '%s_macros' % self.applicationName.lower(), + 'commonMethods': commonMethods}) + # Predefined class descriptors + self.toolDescr = ToolClassDescriptor(Tool, self) + self.flavourDescr = FlavourClassDescriptor(Flavour, self) + self.podTemplateDescr = PodTemplateClassDescriptor(PodTemplate,self) + self.referers = {} + + versionRex = re.compile('(.*?\s+build)\s+(\d+)') + def initialize(self): + # Use customized class descriptors + self.classDescriptor = ArchetypesClassDescriptor + self.workflowDescriptor = WorkflowDescriptor + self.customToolClassDescriptor = CustomToolClassDescriptor + self.customFlavourClassDescriptor = CustomFlavourClassDescriptor + # Determine version number of the Plone product + self.version = '0.1 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.outputFolder, 'i18n') + if os.path.exists(i18nFolder): + for fileName in os.listdir(i18nFolder): + name, ext = os.path.splitext(fileName) + if ext in self.poExtensions: + poParser = PoParser(os.path.join(i18nFolder, fileName)) + self.i18nFiles[fileName] = poParser.parse() + + def finalize(self): + # Some useful aliases + msg = PoMessage + app = self.applicationName + # Some global i18n messages + poMsg = msg(app, '', app); poMsg.produceNiceDefault() + self.labels += [poMsg, + msg('workflow_state', '', msg.WORKFLOW_STATE), + msg('phase', '', msg.PHASE), + msg('root_type', '', msg.ROOT_TYPE), + msg('workflow_comment', '', msg.WORKFLOW_COMMENT), + msg('choose_a_value', '', msg.CHOOSE_A_VALUE), + msg('choose_a_doc', '', msg.CHOOSE_A_DOC), + msg('min_ref_violated', '', msg.MIN_REF_VIOLATED), + msg('max_ref_violated', '', msg.MAX_REF_VIOLATED), + msg('no_ref', '', msg.REF_NO), + msg('add_ref', '', msg.REF_ADD), + msg('ref_name', '', msg.REF_NAME), + msg('ref_actions', '', msg.REF_ACTIONS), + msg('move_up', '', msg.REF_MOVE_UP), + msg('move_down', '', msg.REF_MOVE_DOWN), + msg('query_create', '', msg.QUERY_CREATE), + msg('query_no_result', '', msg.QUERY_NO_RESULT), + msg('query_consult_all', '', msg.QUERY_CONSULT_ALL), + msg('ref_invalid_index', '', msg.REF_INVALID_INDEX), + msg('bad_int', '', msg.BAD_INT), + msg('bad_float', '', msg.BAD_FLOAT), + msg('bad_email', '', msg.BAD_EMAIL), + msg('bad_url', '', msg.BAD_URL), + msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC), + ] + # Create basic files (config.py, Install.py, etc) + self.generateAppyReference() + self.generateTool() + self.generateConfig() + self.generateInit() + self.generateInstall() + self.generateWorkflows() + self.generateWrappers() + self.generatePortlet() + if self.config.frontPage == True: + self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT)) + self.copyFile('frontPage.pt', self.repls, + destFolder=self.skinsFolder, + destName='%sFrontPage.pt' % self.applicationName) + self.copyFile('configure.zcml', self.repls) + self.copyFile('import_steps.xml', self.repls, + destFolder='profiles/default') + self.copyFile('ProfileInit.py', self.repls, destFolder='profiles', + destName='__init__.py') + self.copyFile('tool.gif', {}) + self.copyFile('Macros.pt', self.repls, destFolder=self.skinsFolder, + destName='%s_macros.pt' % self.applicationName.lower()) + self.copyFile('appy_view.pt', self.repls, destFolder=self.skinsFolder, + destName='%s_appy_view.pt' % self.applicationName) + self.copyFile('appy_edit.cpt', self.repls, destFolder=self.skinsFolder, + destName='%s_appy_edit.cpt' % self.applicationName) + self.copyFile('appy_edit.cpt.metadata', self.repls, + destFolder=self.skinsFolder, + destName='%s_appy_edit.cpt.metadata'%self.applicationName) + self.copyFile('Styles.css.dtml', self.repls, destFolder=self.skinsFolder, + destName = '%s.css.dtml' % self.applicationName) + self.copyFile('do.py', self.repls, destFolder=self.skinsFolder, + destName='%s_do.py' % self.applicationName) + self.copyFile('colophon.pt', self.repls, destFolder=self.skinsFolder) + self.copyFile('footer.pt', self.repls, destFolder=self.skinsFolder) + # Create version.txt + f = open(os.path.join(self.outputFolder, 'version.txt'), 'w') + f.write(self.version) + f.close() + # Make Extensions a Python package + for moduleFolder in ('Extensions',): + initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder) + if not os.path.isfile(initFile): + f = open(initFile, 'w') + f.write('') + f.close() + # Decline i18n labels into versions for child classes + for classDescr in self.classes: + for poMsg in classDescr.labelsToPropagate: + for childDescr in classDescr.getChildren(): + childMsg = poMsg.clone(classDescr.name, childDescr.name) + if childMsg not in self.labels: + self.labels.append(childMsg) + # Generate i18n pot file + potFileName = '%s.pot' % self.applicationName + if self.i18nFiles.has_key(potFileName): + potFile = self.i18nFiles[potFileName] + else: + fullName = os.path.join(self.outputFolder, 'i18n/%s' % potFileName) + potFile = PoFile(fullName) + self.i18nFiles[potFileName] = potFile + removedLabels = potFile.update(self.labels, self.options.i18nClean, + not self.options.i18nSort) + if removedLabels: + print 'Warning: %d messages were removed from translation ' \ + 'files: %s' % (len(removedLabels), str(removedLabels)) + potFile.generate() + # 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 self.i18nFiles.has_key(poFileName): + poFile = self.i18nFiles[poFileName] + else: + fullName = os.path.join(self.outputFolder, + 'i18n/%s' % poFileName) + poFile = PoFile(fullName) + self.i18nFiles[poFileName] = poFile + poFile.update(potFile.messages, self.options.i18nClean, + not self.options.i18nSort) + poFile.generate() + # Generate i18n po files for other potential files + for poFile in self.i18nFiles.itervalues(): + if not poFile.generated: + poFile.generate() + + ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer') + def getAllUsedRoles(self, appOnly=False): + '''Produces a list of all the roles used within all workflows defined + in this application. If p_appOnly is True, it returns only roles + which are specific to this application (ie it removes predefined + Plone roles like Member, Manager, etc.''' + res = [] + for wfDescr in self.workflows: + # Browse states and transitions + for attr in dir(wfDescr.klass): + attrValue = getattr(wfDescr.klass, attr) + if isinstance(attrValue, State) or \ + isinstance(attrValue, Transition): + res += attrValue.getUsedRoles() + res = list(set(res)) + if appOnly: + for ploneRole in self.ploneRoles: + if ploneRole in res: + res.remove(ploneRole) + return res + + def addReferer(self, fieldDescr, relationship): + '''p_fieldDescr is a Ref type definition. We will create in config.py a + dict that lists all back references, by type.''' + k = fieldDescr.appyType.klass + if issubclass(k, ModelClass): + refClassName = self.applicationName + k.__name__ + elif issubclass(k, appy.gen.Tool): + refClassName = '%sTool' % self.applicationName + elif issubclass(k, appy.gen.Flavour): + refClassName = '%sFlavour' % self.applicationName + else: + refClassName = ArchetypesClassDescriptor.getClassName(k) + if not self.referers.has_key(refClassName): + self.referers[refClassName] = [] + self.referers[refClassName].append( (fieldDescr, relationship)) + + def generatePortlet(self): + rootClasses = '' + for classDescr in self.classes: + if classDescr.isRoot(): + rootClasses += "'%s'," % classDescr.name + repls = self.repls.copy() + repls['rootClasses'] = rootClasses + self.copyFile('Portlet.pt', repls, destName='%s.pt' % self.portletName, + destFolder=self.skinsFolder) + self.copyFile('Query.pt', repls, destName='%s.pt' % self.queryName, + destFolder=self.skinsFolder) + + def generateConfig(self): + # Compute referers + referers = '' + for className, refInfo in self.referers.iteritems(): + referers += '"%s":[' % className + for fieldDescr, relationship in refInfo: + refClass = fieldDescr.classDescr.klass + if issubclass(refClass, ModelClass): + refClassName = 'Extensions.appyWrappers.%s' % \ + refClass.__name__ + else: + refClassName = '%s.%s' % (refClass.__module__, + refClass.__name__) + referers += '(%s.%s' % (refClassName, fieldDescr.fieldName) + referers += ',"%s"' % relationship + referers += '),' + referers += '],\n' + # Compute workflow instances initialisation + wfInit = '' + for workflowDescr in self.workflows: + k = workflowDescr.klass + className = '%s.%s' % (k.__module__, k.__name__) + wfInit += 'wf = %s()\n' % className + wfInit += 'wf._transitionsMapping = {}\n' + for transition in workflowDescr.getTransitions(): + tName = workflowDescr.getNameOf(transition) + tNames = workflowDescr.getTransitionNamesOf(tName, transition) + for trName in tNames: + wfInit += 'wf._transitionsMapping["%s"] = wf.%s\n' % \ + (trName, tName) + # We need a new attribute that stores states in order + wfInit += 'wf._states = []\n' + for stateName in workflowDescr.getStateNames(ordered=True): + wfInit += 'wf._states.append("%s")\n' % stateName + wfInit += 'workflowInstances[%s] = wf\n' % className + # Compute imports + imports = ['import %s' % self.applicationName] + classDescrs = self.classes[:] + if self.customToolDescr: + classDescrs.append(self.customToolDescr) + if self.customFlavourDescr: + classDescrs.append(self.customFlavourDescr) + for classDescr in (classDescrs + self.workflows): + theImport = 'import %s' % classDescr.klass.__module__ + if theImport not in imports: + imports.append(theImport) + # Compute list of add permissions + addPermissions = '' + for classDescr in self.classes: + addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name, + self.applicationName, classDescr.name) + repls = self.repls.copy() + # Compute list of used roles for registering them if needed + repls['roles'] = ','.join(['"%s"' % r for r in \ + self.getAllUsedRoles(appOnly=True)]) + repls['referers'] = referers + repls['workflowInstancesInit'] = wfInit + repls['imports'] = '\n'.join(imports) + repls['defaultAddRoles'] = ','.join( + ['"%s"' % r for r in self.config.defaultCreators]) + repls['addPermissions'] = addPermissions + self.copyFile('config.py', repls) + + def generateInit(self): + # Compute imports + imports = [' import %s' % self.toolName, + ' import %s' % self.flavourName, + ' import %s' % self.podTemplateName] + for c in self.classes: + importDef = ' import %s' % c.name + if importDef not in imports: + imports.append(importDef) + repls = self.repls.copy() + repls['imports'] = '\n'.join(imports) + self.copyFile('__init__.py', repls) + + def generateInstall(self): + # Compute lists of class names + allClassNames = '"%s",' % self.flavourName + allClassNames += '"%s",' % self.podTemplateName + appClassNames = ','.join(['"%s"' % c.name for c in self.classes]) + allClassNames += appClassNames + # Compute imports + imports = [] + for classDescr in self.classes: + theImport = 'import %s' % classDescr.klass.__module__ + if theImport not in imports: + imports.append(theImport) + # Compute list of application classes + appClasses = [] + for classDescr in self.classes: + k = classDescr.klass + appClasses.append('%s.%s' % (k.__module__, k.__name__)) + # Compute classes whose instances must not be catalogued. + catalogMap = '' + blackClasses = [self.toolName, self.flavourName, self.podTemplateName] + for blackClass in blackClasses: + catalogMap += "catalogMap['%s'] = {}\n" % blackClass + catalogMap += "catalogMap['%s']['black'] = " \ + "['portal_catalog']\n" % blackClass + # Compute workflows + workflows = '' + for classDescr in self.classes: + if hasattr(classDescr.klass, 'workflow'): + wfName = WorkflowDescriptor.getWorkflowName( + classDescr.klass.workflow) + className = ArchetypesClassDescriptor.getClassName( + classDescr.klass) + workflows += '\n "%s":"%s",' % (className, wfName) + # Generate the resulting file. + repls = self.repls.copy() + repls['allClassNames'] = allClassNames + repls['appClassNames'] = appClassNames + repls['catalogMap'] = catalogMap + repls['imports'] = '\n'.join(imports) + repls['appClasses'] = "[%s]" % ','.join(appClasses) + repls['minimalistPlone'] = self.config.minimalistPlone + repls['appFrontPage'] = self.config.frontPage == True + repls['workflows'] = workflows + self.copyFile('Install.py', repls, destFolder='Extensions') + + def generateWorkflows(self): + '''Generates the file that contains one function by workflow. + Those functions are called by Plone for registering the workflows.''' + workflows = '' + for wfDescr in self.workflows: + # Compute state names & info, transition names & infos, managed + # permissions + stateNames=','.join(['"%s"' % sn for sn in wfDescr.getStateNames()]) + stateInfos = wfDescr.getStatesInfo(asDumpableCode=True) + transitionNames = ','.join(['"%s"' % tn for tn in \ + wfDescr.getTransitionNames()]) + transitionInfos = wfDescr.getTransitionsInfo(asDumpableCode=True) + managedPermissions = ','.join(['"%s"' % tn for tn in \ + wfDescr.getManagedPermissions()]) + wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass) + workflows += '%s\ndef create_%s(self, id):\n ' \ + 'stateNames = [%s]\n ' \ + 'stateInfos = %s\n ' \ + 'transitionNames = [%s]\n ' \ + 'transitionInfos = %s\n ' \ + 'managedPermissions = [%s]\n ' \ + 'return WorkflowCreator("%s", DCWorkflowDefinition, ' \ + 'stateNames, "%s", stateInfos, transitionNames, ' \ + 'transitionInfos, managedPermissions, PROJECTNAME, ' \ + 'ExternalMethod).run()\n' \ + 'addWorkflowFactory(create_%s,\n id="%s",\n ' \ + 'title="%s")\n\n' % (wfDescr.getScripts(), wfName, stateNames, + stateInfos, transitionNames, transitionInfos, + managedPermissions, wfName, wfDescr.getInitialStateName(), + wfName, wfName, wfName) + repls = self.repls.copy() + repls['workflows'] = workflows + self.copyFile('workflows.py', repls, destFolder='Extensions') + + def generateWrapperProperty(self, attrName, appyType): + # Generate getter + res = ' def get_%s(self):\n' % attrName + blanks = ' '*8 + if isinstance(appyType, Ref): + res += blanks + 'return self.o._appy_getRefs("%s", ' \ + 'noListIfSingleObj=True)\n' % attrName + elif isinstance(appyType, Computed): + res += blanks + 'appyType = getattr(self.klass, "%s")\n' % attrName + res += blanks + 'return self.o.getComputedValue(' \ + 'appyType.__dict__)\n' + else: + getterName = 'get%s%s' % (attrName[0].upper(), attrName[1:]) + if attrName in ArchetypeFieldDescriptor.specialParams: + getterName = attrName.capitalize() + res += blanks + 'return self.o.%s()\n' % getterName + res += ' %s = property(get_%s)\n\n' % (attrName, attrName) + return res + + def generateWrapperPropertyBack(self, attrName, rel): + '''Generates a wrapper property for accessing the back reference named + p_attrName through Archetypes relationship p_rel.''' + res = ' def get_%s(self):\n' % attrName + blanks = ' '*8 + res += blanks + 'return self.o._appy_getRefsBack("%s", "%s", ' \ + 'noListIfSingleObj=True)\n' % (attrName, rel) + res += ' %s = property(get_%s)\n\n' % (attrName, attrName) + return res + + 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.maxint + for resClass in resClasses: + if klass in resClass.__bases__: + lowestChildIndex = min(lowestChildIndex, + resClasses.index(resClass)) + if lowestChildIndex != sys.maxint: + 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.classes[:] + # Add predefined classes (Tool, Flavour, PodTemplate) + allClasses += [self.toolDescr, self.flavourDescr, self.podTemplateDescr] + if self.customToolDescr: + allClasses.append(self.customToolDescr) + if self.customFlavourDescr: + allClasses.append(self.customFlavourDescr) + for c in self.getClassesInOrder(allClasses): + if not c.predefined: + moduleImport = 'import %s' % c.klass.__module__ + if moduleImport not in imports: + imports.append(moduleImport) + # Determine parent wrapper and class + parentWrapper = 'AbstractWrapper' + parentClass = '%s.%s' % (c.klass.__module__, c.klass.__name__) + if c.predefined: + parentClass = c.klass.__name__ + if c.klass.__bases__: + baseClassName = c.klass.__bases__[0].__name__ + for k in allClasses: + if k.klass.__name__ == baseClassName: + parentWrapper = '%s_Wrapper' % k.name + wrapperDef = 'class %s_Wrapper(%s, %s):\n' % \ + (c.name, parentWrapper, parentClass) + titleFound = False + for attrName in c.orderedAttributes: + if attrName == 'title': + titleFound = True + attrValue = getattr(c.klass, attrName) + if isinstance(attrValue, Type): + wrapperDef += self.generateWrapperProperty(attrName, + attrValue) + # Generate properties for back references + if self.referers.has_key(c.name): + for refDescr, rel in self.referers[c.name]: + attrName = refDescr.appyType.back.attribute + wrapperDef += self.generateWrapperPropertyBack(attrName,rel) + if not titleFound: + # Implicitly, the title will be added by Archetypes. So I need + # to define a property for it. + wrapperDef += self.generateWrapperProperty('title', String()) + wrappers.append(wrapperDef) + repls = self.repls.copy() + repls['imports'] = '\n'.join(imports) + repls['wrappers'] = '\n'.join(wrappers) + repls['toolBody'] = Tool._appy_getBody() + repls['flavourBody'] = Flavour._appy_getBody() + repls['podTemplateBody'] = PodTemplate._appy_getBody() + self.copyFile('appyWrappers.py', repls, destFolder='Extensions') + + def generateTool(self): + '''Generates the Plone tool that corresponds to this application.''' + # Generate the tool class in itself and related i18n messages + t = self.toolName + Msg = PoMessage + repls = self.repls.copy() + # Manage predefined fields + Tool.flavours.klass = Flavour + if self.customFlavourDescr: + Tool.flavours.klass = self.customFlavourDescr.klass + self.toolDescr.generateSchema() + repls['predefinedFields'] = self.toolDescr.schema + repls['predefinedMethods'] = self.toolDescr.methods + # Manage custom fields + repls['fields'] = '' + repls['methods'] = '' + repls['wrapperClass'] = '%s_Wrapper' % self.toolDescr.name + if self.customToolDescr: + repls['fields'] = self.customToolDescr.schema + repls['methods'] = self.customToolDescr.methods + wrapperClass = '%s_Wrapper' % self.customToolDescr.name + repls['wrapperClass'] = wrapperClass + self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName) + repls = self.repls.copy() + # Create i18n-related messages + self.labels += [ + Msg(self.toolName, '', Msg.CONFIG % self.applicationName), + Msg('%s_edit_descr' % self.toolName, '', ' ')] + # Before generating the Flavour class, finalize it with query result + # columns, with fields to propagate, workflow-related fields. + for classDescr in self.classes: + for fieldName, fieldType in classDescr.flavourFieldsToPropagate: + for childDescr in classDescr.getChildren(): + childFieldName = fieldName % childDescr.name + fieldType.group = childDescr.klass.__name__ + Flavour._appy_addField(childFieldName,fieldType,childDescr) + if classDescr.isRoot(): + # We must be able to configure query results from the + # flavour. + Flavour._appy_addQueryResultColumns(classDescr) + Flavour._appy_addWorkflowFields(self.flavourDescr) + Flavour._appy_addWorkflowFields(self.podTemplateDescr) + # Generate the flavour class and related i18n messages + self.flavourDescr.generateSchema() + self.labels += [ Msg(self.flavourName, '', Msg.FLAVOUR), + Msg('%s_edit_descr' % self.flavourName, '', ' ')] + repls = self.repls.copy() + repls['predefinedFields'] = self.flavourDescr.schema + repls['predefinedMethods'] = self.flavourDescr.methods + # Manage custom fields + repls['fields'] = '' + repls['methods'] = '' + repls['wrapperClass'] = '%s_Wrapper' % self.flavourDescr.name + if self.customFlavourDescr: + repls['fields'] = self.customFlavourDescr.schema + repls['methods'] = self.customFlavourDescr.methods + wrapperClass = '%s_Wrapper' % self.customFlavourDescr.name + repls['wrapperClass'] = wrapperClass + repls['metaTypes'] = [c.name for c in self.classes] + self.copyFile('FlavourTemplate.py', repls, + destName='%s.py'% self.flavourName) + # Generate the PodTemplate class + self.podTemplateDescr.generateSchema() + self.labels += [ Msg(self.podTemplateName, '', Msg.POD_TEMPLATE), + Msg('%s_edit_descr' % self.podTemplateName, '', ' ')] + repls = self.repls.copy() + repls['fields'] = self.podTemplateDescr.schema + repls['methods'] = self.podTemplateDescr.methods + repls['wrapperClass'] = '%s_Wrapper' % self.podTemplateDescr.name + self.copyFile('PodTemplate.py', repls, + destName='%s.py' % self.podTemplateName) + for imgName in PodTemplate.podFormat.validator: + self.copyFile('%s.png' % imgName, {}, + destFolder=self.skinsFolder) + + refFiles = ('createAppyObject.cpy', 'createAppyObject.cpy.metadata', + 'arrowUp.png', 'arrowDown.png', 'plus.png', 'appyConfig.gif', + 'nextPhase.png', 'nextState.png', 'done.png', 'current.png') + prefixedRefFiles = ('AppyReference.pt',) + def generateAppyReference(self): + '''Generates what is needed to use Appy-specific references.''' + # Some i18n messages + Msg = PoMessage + for refFile in self.prefixedRefFiles: + self.copyFile(refFile, self.repls, destFolder=self.skinsFolder, + destName='%s%s' % (self.applicationName, refFile)) + for refFile in self.refFiles: + self.copyFile(refFile, self.repls, destFolder=self.skinsFolder) + + def generateClass(self, classDescr): + '''Is called each time an Appy class is found in the application, for + generating the corresponding Archetype class and schema.''' + k = classDescr.klass + print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) + # Add, for this class, the needed configuration attributes on Flavour + if classDescr.isPod(): + Flavour._appy_addPodField(classDescr) + if not classDescr.isAbstract(): + Flavour._appy_addWorkflowFields(classDescr) + # Determine base archetypes schema and class + baseClass = 'BaseContent' + baseSchema = 'BaseSchema' + if classDescr.isFolder(): + baseClass = 'OrderedBaseFolder' + baseSchema = 'OrderedBaseFolderSchema' + parents = [baseClass, 'ClassMixin'] + imports = [] + implements = [baseClass] + for baseClass in classDescr.klass.__bases__: + if self.determineAppyType(baseClass) == 'class': + bcName = ArchetypesClassDescriptor.getClassName(baseClass) + parents.remove('ClassMixin') + parents.append(bcName) + implements.append(bcName) + imports.append('from %s import %s' % (bcName, bcName)) + baseSchema = '%s.schema' % bcName + break + parents = ','.join(parents) + implements = '+'.join(['(getattr(%s,"__implements__",()),)' % i \ + for i in implements]) + classDoc = classDescr.klass.__doc__ + if not classDoc: + classDoc = 'Class generated with appy.gen.' + # If the class is abstract I will not register it + register = "registerType(%s, '%s')" % (classDescr.name, + self.applicationName) + if classDescr.isAbstract(): + register = '' + classDescr.addGenerateDocMethod() # For POD + repls = self.repls.copy() + repls.update({ + 'imports': '\n'.join(imports), 'parents': parents, + 'className': classDescr.klass.__name__, + 'genClassName': classDescr.name, + 'classDoc': classDoc, 'applicationName': self.applicationName, + 'fields': classDescr.schema, 'methods': classDescr.methods, + 'implements': implements, 'baseSchema': baseSchema, + 'register': register, 'toolInstanceName': self.toolInstanceName}) + fileName = '%s.py' % classDescr.name + # Remember i18n labels that will be generated in the i18n file + poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ') + self.labels.append(poMsgDescr) + # Remember i18n labels for flavoured variants + for i in range(2,10): + poMsg = PoMessage('%s_%d' % (classDescr.name, i), '', + classDescr.klass.__name__) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i), + '', ' ') + self.labels.append(poMsgDescr) + # Generate the resulting Archetypes class and schema. + self.copyFile('ArchetypesTemplate.py', repls, destName=fileName) + + def generateWorkflow(self, wfDescr): + '''This method does not generate the workflow definition, which is done + in self.generateWorkflows. This method just 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 Plone workflow name + wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass) + # Add i18n messages for states and transitions + for sName in wfDescr.getStateNames(): + poMsg = PoMessage('%s_%s' % (wfName, sName), '', sName) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + for tName, tLabel in wfDescr.getTransitionNames(withLabels=True): + poMsg = PoMessage('%s_%s' % (wfName, tName), '', tLabel) + poMsg.produceNiceDefault() + self.labels.append(poMsg) + for transition in wfDescr.getTransitions(): + if transition.notify: + # Appy will send a mail when this transition is triggered. + # So we need 2 i18n labels for every DC transition corresponding + # to this Appy transition: one for the mail subject and one for + # the mail body. + tName = wfDescr.getNameOf(transition) # Appy name + tNames = wfDescr.getTransitionNamesOf(tName, transition) # DC + # name(s) + for tn in tNames: + subjectLabel = '%s_%s_mail_subject' % (wfName, tn) + poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT) + self.labels.append(poMsg) + bodyLabel = '%s_%s_mail_body' % (wfName, tn) + poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY) + self.labels.append(poMsg) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py new file mode 100644 index 0000000..b4e5219 --- /dev/null +++ b/gen/plone25/installer.py @@ -0,0 +1,496 @@ +'''This package contains stuff used at run-time for installing a generated + Plone product.''' + +# ------------------------------------------------------------------------------ +import os, os.path +from StringIO import StringIO +from sets import Set +from appy.gen.utils import produceNiceMessage +from appy.gen.plone25.utils import updateRolesForPermission + +class PloneInstaller: + '''This Plone installer runs every time the generated Plone product is + installed or uninstalled (in the Plone configuration interface).''' + def __init__(self, reinstall, productName, ploneSite, minimalistPlone, + appClasses, appClassNames, allClassNames, catalogMap, applicationRoles, + defaultAddRoles, workflows, appFrontPage, ploneStuff): + self.reinstall = reinstall # Is it a fresh install or a re-install? + self.productName = productName + self.ploneSite = ploneSite + self.minimalistPlone = minimalistPlone # If True, lots of basic Plone + # stuff will be hidden. + self.appClasses = appClasses # The list of classes declared in the + # gen-application. + self.appClassNames = appClassNames # Names of those classes + self.allClassNames = allClassNames # Includes Flavour and PodTemplate + self.catalogMap = catalogMap # Indicates classes to be indexed or not + self.applicationRoles = applicationRoles # Roles defined in the app + self.defaultAddRoles = defaultAddRoles # The default roles that can add + # content + self.workflows = workflows # Dict whose keys are class names and whose + # values are workflow names (=the workflow + # used by the content type) + self.appFrontPage = appFrontPage # Does this app define a site-wide + # front page? + self.ploneStuff = ploneStuff # A dict of some Plone functions or vars + self.toLog = StringIO() + self.typeAliases = {'sharing': '', 'gethtml': '', + '(Default)': '%s_appy_view' % self.productName, + 'edit': '%s_appy_edit' % self.productName, + 'index.html': '', 'properties': '', 'view': ''} + self.tool = None # The Plone version of the application tool + self.appyTool = None # The Appy version of the application tool + self.toolName = '%sTool' % self.productName + self.toolInstanceName = 'portal_%s' % self.productName.lower() + + + actionsToHide = { + 'portal_actions': ('sitemap', 'accessibility', 'change_state','sendto'), + 'portal_membership': ('mystuff', 'preferences'), + 'portal_undo': ('undo',) + } + def customizePlone(self): + '''Hides some UI elements that appear by default in Plone.''' + for portalName, toHide in self.actionsToHide.iteritems(): + portal = getattr(self.ploneSite, portalName) + portalActions = portal.listActions() + for action in portalActions: + if action.id in toHide: action.visible = False + + appyFolderType = 'AppyFolder' + def registerAppyFolderType(self): + '''We need a specific content type for the folder that will hold all + objects created from this application, in order to remove it from + Plone navigation settings. We will create a new content type based + on Large Plone Folder.''' + if not hasattr(self.ploneSite.portal_types, self.appyFolderType): + portal_types = self.ploneSite.portal_types + lpf = 'Large Plone Folder' + largePloneFolder = getattr(portal_types, lpf) + typeInfoName = 'ATContentTypes: ATBTreeFolder (ATBTreeFolder)' + portal_types.manage_addTypeInformation( + largePloneFolder.meta_type, id=self.appyFolderType, + typeinfo_name=typeInfoName) + appyFolder = getattr(portal_types, self.appyFolderType) + appyFolder.title = 'Appy folder' + #appyFolder.factory = largePloneFolder.factory + #appyFolder.product = largePloneFolder.product + # Copy actions and aliases + appyFolder._actions = tuple(largePloneFolder._cloneActions()) + # Copy aliases from the base portal type + appyFolder.setMethodAliases(largePloneFolder.getMethodAliases()) + # Prevent Appy folders to be visible in standard Plone navigation + nv = self.ploneSite.portal_properties.navtree_properties + metaTypesNotToList = list(nv.getProperty('metaTypesNotToList')) + if self.appyFolderType not in metaTypesNotToList: + metaTypesNotToList.append(self.appyFolderType) + nv.manage_changeProperties( + metaTypesNotToList=tuple(metaTypesNotToList)) + + def getAddPermission(self, className): + '''What is the name of the permission allowing to create instances of + class whose name is p_className?''' + return self.productName + ': Add ' + className + + def installRootFolder(self): + '''Creates and/or configures, at the root of the Plone site and if + needed, the folder where the application will store instances of + root classes.''' + # Register first our own Appy folder type if needed. + site = self.ploneSite + if not hasattr(site.portal_types, self.appyFolderType): + self.registerAppyFolderType() + # Create the folder + if not hasattr(site.aq_base, self.productName): + # Temporarily allow me to create Appy large plone folders + getattr(site.portal_types, self.appyFolderType).global_allow = 1 + site.invokeFactory(self.appyFolderType, self.productName, + title=self.productName) + getattr(site.portal_types, self.appyFolderType).global_allow = 0 + appFolder = getattr(site, self.productName) + # All roles defined as creators should be able to create the + # corresponding root content types in this folder. + i = -1 + allCreators = set() + for klass in self.appClasses: + i += 1 + if klass.__dict__.has_key('root') and klass.__dict__['root']: + # It is a root class. + creators = getattr(klass, 'creators', None) + if not creators: creators = self.defaultAddRoles + allCreators = allCreators.union(creators) + className = self.appClassNames[i] + updateRolesForPermission(self.getAddPermission(className), + tuple(creators), appFolder) + # Beyond content-type-specific "add" permissions, creators must also + # have the main permission "Add portal content". + updateRolesForPermission('Add portal content', tuple(allCreators), + appFolder) + + def installTypes(self): + '''Registers and configures the Plone content types that correspond to + gen-classes.''' + site = self.ploneSite + # Do Plone-based type registration + classes = self.ploneStuff['listTypes'](self.productName) + self.ploneStuff['installTypes'](site, self.toLog, classes, + self.productName) + self.ploneStuff['install_subskin'](site, self.toLog, + self.ploneStuff['GLOBALS']) + # Set appy view/edit pages for every created type + for className in self.allClassNames + ['%sTool' % self.productName]: + # I did not put the app tool in self.allClassNames because it + # must not be registered in portal_factory + if hasattr(site.portal_types, className): + # className may correspond to an abstract class that has no + # corresponding Plone content type + typeInfo = getattr(site.portal_types, className) + typeInfo.setMethodAliases(self.typeAliases) + # Update edit and view actions + typeActions = typeInfo.listActions() + for action in typeActions: + if action.id == 'view': + page = '%s_appy_view' % self.productName + action.edit(action='string:${object_url}/%s' % page) + elif action.id == 'edit': + page = '%s_appy_edit' % self.productName + action.edit(action='string:${object_url}/%s' % page) + + # Configure types for instance creation through portal_factory + factoryTool = site.portal_factory + factoryTypes = self.allClassNames + factoryTool.getFactoryTypes().keys() + factoryTool.manage_setPortalFactoryTypes(listOfTypeIds=factoryTypes) + + # Configure CatalogMultiplex: tell what types will be catalogued or not. + atTool = getattr(site, self.ploneStuff['ARCHETYPETOOLNAME']) + for meta_type in self.catalogMap: + submap = self.catalogMap[meta_type] + current_catalogs = Set( + [c.id for c in atTool.getCatalogsByType(meta_type)]) + if 'white' in submap: + for catalog in submap['white']: + current_catalogs.update([catalog]) + if 'black' in submap: + for catalog in submap['black']: + if catalog in current_catalogs: + current_catalogs.remove(catalog) + atTool.setCatalogsByType(meta_type, list(current_catalogs)) + + def findPodFile(self, klass, podTemplateName): + '''Finds the file that corresponds to p_podTemplateName for p_klass.''' + res = None + exec 'import %s' % klass.__module__ + exec 'moduleFile = %s.__file__' % klass.__module__ + folderName = os.path.dirname(moduleFile) + fileName = os.path.join(folderName, '%s.odt' % podTemplateName) + if os.path.isfile(fileName): + res = fileName + return res + + def updatePodTemplates(self): + '''Creates or updates the POD templates in flavours according to pod + declarations in the application classes.''' + i = -1 + for klass in self.appClasses: + i += 1 + if klass.__dict__.has_key('pod'): + pod = getattr(klass, 'pod') + if isinstance(pod, bool): + podTemplates = [klass.__name__] + else: + podTemplates = pod + for templateName in podTemplates: + fileName = self.findPodFile(klass, templateName) + if fileName: + # Create the corresponding PodTemplate in all flavours + for flavour in self.appyTool.flavours: + podId='%s_%s' % (self.appClassNames[i],templateName) + podAttr = 'podTemplatesFor%s'% self.appClassNames[i] + allPodTemplates = getattr(flavour, podAttr) + if allPodTemplates: + if isinstance(allPodTemplates, list): + allIds = [p.id for p in allPodTemplates] + else: + allIds = [allPodTemplates.id] + else: + allIds = [] + if podId not in allIds: + # Create a PodTemplate instance + f = file(fileName) + flavour.create(podAttr, id=podId, podTemplate=f, + title=produceNiceMessage(templateName)) + f.close() + + def installTool(self): + '''Configures the application tool and flavours.''' + # Register the tool in Plone + try: + self.ploneSite.manage_addProduct[ + self.productName].manage_addTool(self.toolName) + except self.ploneStuff['BadRequest']: + # If an instance with the same name already exists, this error will + # be unelegantly raised by Zope. + pass + except: + e = sys.exc_info() + if e[0] != 'Bad Request': raise + + # Hide the tool from the search form + portalProperties = self.ploneSite.portal_properties + if portalProperties is not None: + siteProperties = getattr(portalProperties, 'site_properties', None) + if siteProperties is not None and \ + siteProperties.hasProperty('types_not_searched'): + current = list(siteProperties.getProperty('types_not_searched')) + if self.toolName not in current: + current.append(self.toolName) + siteProperties.manage_changeProperties( + **{'types_not_searched' : current}) + + # Hide the tool in the navigation + if portalProperties is not None: + nvProps = getattr(portalProperties, 'navtree_properties', None) + if nvProps is not None and nvProps.hasProperty('idsNotToList'): + current = list(nvProps.getProperty('idsNotToList')) + if self.toolInstanceName not in current: + current.append(self.toolInstanceName) + nvProps.manage_changeProperties(**{'idsNotToList': current}) + + # Remove workflow for the tool + wfTool = self.ploneSite.portal_workflow + wfTool.setChainForPortalTypes([self.toolName], '') + + # Create the default flavour + self.tool = getattr(self.ploneSite, self.toolInstanceName) + self.appyTool = self.tool._appy_getWrapper(force=True) + if self.reinstall: + self.tool.at_post_edit_script() + else: + self.tool.at_post_create_script() + if not self.appyTool.flavours: + self.appyTool.create('flavours', title=self.productName, number=1) + self.updatePodTemplates() + + # Uncatalog tool + self.tool.unindexObject() + + # Register tool as configlet + portalControlPanel = self.ploneSite.portal_controlpanel + portalControlPanel.unregisterConfiglet(self.toolName) + portalControlPanel.registerConfiglet( + self.toolName, self.productName, + 'string:${portal_url}/%s' % self.toolInstanceName, 'python:True', + 'Manage portal', # Access permission + 'Products', # Section to which the configlet should be added: + # (Plone, Products (default) or Member) + 1, # Visibility + '%sID' % self.toolName, 'site_icon.gif', # Icon in control_panel + self.productName, None) + + def installRolesAndGroups(self): + '''Registers roles used by workflows defined in this application if + they are not registered yet. Creates the corresponding groups if + needed.''' + site = self.ploneSite + data = list(site.__ac_roles__) + for role in self.applicationRoles: + if not role in data: + data.append(role) + # Add to portal_role_manager + # First, try to fetch it. If it's not there, we probaly have no + # PAS or another way to deal with roles was configured. + try: + prm = site.acl_users.get('portal_role_manager', None) + if prm is not None: + try: + prm.addRole(role, role, + "Added by product '%s'" % self.productName) + except KeyError: # Role already exists + pass + except AttributeError: + pass + # Create a specific group and grant him this role + group = '%s_group' % role + if not site.portal_groups.getGroupById(group): + site.portal_groups.addGroup(group, title=group) + site.portal_groups.setRolesForGroup(group, [role]) + site.__ac_roles__ = tuple(data) + + def installWorkflows(self): + '''Creates or updates the workflows defined in the application.''' + wfTool = self.ploneSite.portal_workflow + for contentType, workflowName in self.workflows.iteritems(): + # Register the workflow if needed + if workflowName not in wfTool.listWorkflows(): + wfMethod = self.ploneStuff['ExternalMethod']('temp', 'temp', + self.productName + '.workflows', 'create_%s' % workflowName) + workflow = wfMethod(self, workflowName) + wfTool._setObject(workflowName, workflow) + else: + self.log('%s already in workflows.' % workflowName) + # Link the workflow to the current content type + wfTool.setChainForPortalTypes([contentType], workflowName) + return wfTool + + def installStyleSheet(self): + '''Registers In Plone the stylesheet linked to this application.''' + cssName = self.productName + '.css' + cssTitle = self.productName + ' CSS styles' + cssInfo = {'id': cssName, 'title': cssTitle} + try: + portalCss = self.ploneSite.portal_css + try: + portalCss.unregisterResource(cssInfo['id']) + except: + pass + defaults = {'id': '', 'media': 'all', 'enabled': True} + defaults.update(cssInfo) + portalCss.registerStylesheet(**defaults) + except: + # No portal_css registry + pass + + def installPortlet(self): + '''Adds the application-specific portlet and configure other Plone + portlets if relevant.''' + portletName= 'here/%s_portlet/macros/portlet' % self.productName.lower() + site = self.ploneSite + # This is the name of the application-specific portlet + leftPortlets = site.getProperty('left_slots') + if not leftPortlets: leftPortlets = [] + else: leftPortlets = list(leftPortlets) + if portletName not in leftPortlets: + leftPortlets.insert(0, portletName) + # Remove some basic Plone portlets that make less sense when building + # web applications. + portletsToRemove = ["here/portlet_navigation/macros/portlet", + "here/portlet_recent/macros/portlet", + "here/portlet_related/macros/portlet"] + if not self.minimalistPlone: portletsToRemove = [] + for p in portletsToRemove: + if p in leftPortlets: + leftPortlets.remove(p) + site.manage_changeProperties(left_slots=tuple(leftPortlets)) + if self.minimalistPlone: + site.manage_changeProperties(right_slots=()) + + def finalizeInstallation(self): + '''Performs some final installation steps.''' + site = self.ploneSite + # Do not generate an action (tab) for each root folder + if self.minimalistPlone: + site.portal_properties.site_properties.manage_changeProperties( + disable_folder_sections=True) + # Do not allow an anonymous user to register himself as new user + site.manage_permission('Add portal member', ('Manager',), acquire=0) + # Call custom installer if any + if hasattr(self.appyTool, 'install'): + self.tool.executeAppyAction('install', reindex=False) + # Replace Plone front-page with an application-specific page if needed + if self.appFrontPage: + frontPageName = self.productName + 'FrontPage' + site.manage_changeProperties(default_page=frontPageName) + + def log(self, msg): print >> self.toLog, msg + + def install(self): + self.log("Installation of %s:" % self.productName) + if self.minimalistPlone: self.customizePlone() + self.installRootFolder() + self.installTypes() + self.installTool() + self.installRolesAndGroups() + self.installWorkflows() + self.installStyleSheet() + self.installPortlet() + self.finalizeInstallation() + self.log("Installation of %s done." % self.productName) + return self.toLog.getvalue() + + def uninstallTool(self): + site = self.ploneSite + # Unmention tool in the search form + portalProperties = getattr(site, 'portal_properties', None) + if portalProperties is not None: + siteProperties = getattr(portalProperties, 'site_properties', None) + if siteProperties is not None and \ + siteProperties.hasProperty('types_not_searched'): + current = list(siteProperties.getProperty('types_not_searched')) + if self.toolName in current: + current.remove(self.toolName) + siteProperties.manage_changeProperties( + **{'types_not_searched' : current}) + + # Unmention tool in the navigation + if portalProperties is not None: + nvProps = getattr(portalProperties, 'navtree_properties', None) + if nvProps is not None and nvProps.hasProperty('idsNotToList'): + current = list(nvProps.getProperty('idsNotToList')) + if self.toolInstanceName in current: + current.remove(self.toolInstanceName) + nvProps.manage_changeProperties(**{'idsNotToList': current}) + + def uninstall(self): + self.log("Uninstallation of %s:" % self.productName) + self.uninstallTool() + self.log("Uninstallation of %s done." % self.productName) + return self.toLog.getvalue() + +# ------------------------------------------------------------------------------ +class ZopeInstaller: + '''This Zope installer runs every time Zope starts and encounters this + generated Zope product.''' + def __init__(self, zopeContext, productName, toolClass, + defaultAddContentPermission, addContentPermissions, + logger, ploneStuff): + self.zopeContext = zopeContext + self.productName = productName + self.toolClass = toolClass + self.defaultAddContentPermission = defaultAddContentPermission + self.addContentPermissions = addContentPermissions + self.logger = logger + self.ploneStuff = ploneStuff # A dict of some Plone functions or vars + + def installApplication(self): + '''Performs some application-wide installation steps.''' + self.ploneStuff['DirectoryView'].registerDirectory('skins', + self.ploneStuff['product_globals']) + + def installTool(self): + '''Installs the tool.''' + self.ploneStuff['ToolInit'](self.productName + ' Tools', + tools = [self.toolClass], icon='tool.gif').initialize( + self.zopeContext) + + def installTypes(self): + '''Installs and configures the types defined in the application.''' + contentTypes, constructors, ftis = self.ploneStuff['process_types']( + self.ploneStuff['listTypes'](self.productName), self.productName) + + self.ploneStuff['cmfutils'].ContentInit(self.productName + ' Content', + content_types = contentTypes, + permission = self.defaultAddContentPermission, + extra_constructors = constructors, fti = ftis).initialize( + self.zopeContext) + + # Define content-specific "add" permissions + for i in range(0, len(contentTypes)): + className = contentTypes[i].__name__ + if not className in self.addContentPermissions: continue + self.zopeContext.registerClass(meta_type = ftis[i]['meta_type'], + constructors = (constructors[i],), + permission = self.addContentPermissions[className]) + + def finalizeInstallation(self): + '''Performs some final installation steps.''' + # Apply customization policy if any + cp = self.ploneStuff['CustomizationPolicy'] + if cp and hasattr(cp, 'register'): cp.register(context) + + def install(self): + self.logger.info('is being installed...') + self.installApplication() + self.installTool() + self.installTypes() + self.finalizeInstallation() +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ClassMixin.py b/gen/plone25/mixins/ClassMixin.py new file mode 100644 index 0000000..22190a7 --- /dev/null +++ b/gen/plone25/mixins/ClassMixin.py @@ -0,0 +1,42 @@ +# ------------------------------------------------------------------------------ +from appy.gen.plone25.mixins import AbstractMixin + +# ------------------------------------------------------------------------------ +class ClassMixin(AbstractMixin): + _appy_meta_type = 'class' + def _appy_fieldIsUsed(self, portalTypeName, fieldName): + tool = self.getTool() + flavour = tool.getFlavour(portalTypeName) + optionalFieldsAccessor = 'getOptionalFieldsFor%s' % self.meta_type + exec 'usedFields = flavour.%s()' % optionalFieldsAccessor + res = False + if fieldName in usedFields: + res = True + return res + + def _appy_getDefaultValueFor(self, portalTypeName, fieldName): + tool = self.getTool() + flavour = tool.getFlavour(portalTypeName) + fieldFound = False + klass = self.__class__ + while not fieldFound: + metaType = klass.meta_type + defValueAccessor = 'getDefaultValueFor%s_%s' % (metaType, fieldName) + if not hasattr(flavour, defValueAccessor): + # The field belongs to a super-class. + klass = klass.__bases__[-1] + else: + fieldFound = True + exec 'res = flavour.%s()' % defValueAccessor + return res + + def fieldIsUsed(self, fieldName): + '''Checks in the corresponding flavour if p_fieldName is used.''' + portalTypeName = self._appy_getPortalType(self.REQUEST) + return self._appy_fieldIsUsed(portalTypeName, fieldName) + + def getDefaultValueFor(self, fieldName): + '''Gets in the flavour the default value for p_fieldName.''' + portalTypeName = self._appy_getPortalType(self.REQUEST) + return self._appy_getDefaultValueFor(portalTypeName,fieldName) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/FlavourMixin.py b/gen/plone25/mixins/FlavourMixin.py new file mode 100644 index 0000000..dc5986f --- /dev/null +++ b/gen/plone25/mixins/FlavourMixin.py @@ -0,0 +1,113 @@ +# ------------------------------------------------------------------------------ +import appy.gen +from appy.gen.plone25.mixins import AbstractMixin +from appy.gen.plone25.descriptors import ArchetypesClassDescriptor + +# ------------------------------------------------------------------------------ +class FlavourMixin(AbstractMixin): + _appy_meta_type = 'flavour' + def getPortalType(self, metaTypeOrAppyType): + '''Returns the name of the portal_type that is based on + p_metaTypeOrAppyType in this flavour.''' + res = metaTypeOrAppyType + isPredefined = False + isAppy = False + appName = self.getProductConfig().PROJECTNAME + if not isinstance(res, basestring): + res = ArchetypesClassDescriptor.getClassName(res) + isAppy = True + if res.find('Extensions_appyWrappers') != -1: + isPredefined = True + elems = res.split('_') + res = '%s%s' % (elems[1], elems[4]) + elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Tool): + # This is the custom tool + isPredefined = True + res = '%sTool' % appName + elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Flavour): + # This is the custom Flavour + isPredefined = True + res = '%sFlavour' % appName + if not isPredefined: + if self.getNumber() != 1: + res = '%s_%d' % (res, self.number) + return res + + def registerPortalTypes(self): + '''Registers, into portal_types, the portal types which are specific + to this flavour.''' + i = -1 + registeredFactoryTypes = self.portal_factory.getFactoryTypes().keys() + factoryTypesToRegister = [] + appName = self.getProductConfig().PROJECTNAME + for metaTypeName in self.allMetaTypes: + i += 1 + portalTypeName = '%s_%d' % (metaTypeName, self.number) + # If the portal type corresponding to the meta type is + # registered in portal_factory (in the model: + # use_portal_factory=True), we must also register the new + # portal_type we are currently creating. + if metaTypeName in registeredFactoryTypes: + factoryTypesToRegister.append(portalTypeName) + if not hasattr(self.portal_types, portalTypeName) and \ + hasattr(self.portal_types, metaTypeName): + # Indeed abstract meta_types have no associated portal_type + typeInfoName = "%s: %s (%s)" % (appName, metaTypeName, + metaTypeName) + self.portal_types.manage_addTypeInformation( + getattr(self.portal_types, metaTypeName).meta_type, + id=portalTypeName, typeinfo_name=typeInfoName) + # Set the human readable title explicitly + portalType = getattr(self.portal_types, portalTypeName) + portalType.title = portalTypeName + # Associate a workflow for this new portal type. + pf = self.portal_workflow + workflowChain = pf.getChainForPortalType(metaTypeName) + pf.setChainForPortalTypes([portalTypeName],workflowChain) + # Copy actions from the base portal type + basePortalType = getattr(self.portal_types, metaTypeName) + portalType._actions = tuple(basePortalType._cloneActions()) + # Copy aliases from the base portal type + portalType.setMethodAliases(basePortalType.getMethodAliases()) + # Update the factory tool with the list of types to register + self.portal_factory.manage_setPortalFactoryTypes( + listOfTypeIds=factoryTypesToRegister+registeredFactoryTypes) + + def getClassFolder(self, className): + '''Return the folder related to p_className.''' + return getattr(self, className) + + def getAvailablePodTemplates(self, obj, phase='main'): + '''Returns the POD templates which are available for generating a + document from p_obj.''' + appySelf = self._appy_getWrapper() + fieldName = 'podTemplatesFor%s' % obj.meta_type + res = [] + podTemplates = getattr(appySelf, fieldName, []) + if not isinstance(podTemplates, list): + podTemplates = [podTemplates] + res = [r.o for r in podTemplates if r.phase==phase] + hasParents = True + klass = obj.__class__ + while hasParents: + parent = klass.__bases__[-1] + if hasattr(parent, 'wrapperClass'): + fieldName = 'podTemplatesFor%s' % parent.meta_type + podTemplates = getattr(appySelf, fieldName, []) + if not isinstance(podTemplates, list): + podTemplates = [podTemplates] + res += [r.o for r in podTemplates if r.phase==phase] + klass = parent + else: + hasParents = False + return res + + def getMaxShownTemplates(self, obj): + attrName = 'podMaxShownTemplatesFor%s' % obj.meta_type + return getattr(self, attrName) + + def getAttr(self, attrName): + '''Gets on this flavour attribute named p_attrName. Useful because we + can't use getattr directly in Zope Page Templates.''' + return getattr(self, attrName, None) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/PodTemplateMixin.py b/gen/plone25/mixins/PodTemplateMixin.py new file mode 100644 index 0000000..85a9e3a --- /dev/null +++ b/gen/plone25/mixins/PodTemplateMixin.py @@ -0,0 +1,97 @@ +# ------------------------------------------------------------------------------ +import os, os.path, time +from appy.shared import mimeTypes +from appy.gen.plone25.mixins import AbstractMixin +from StringIO import StringIO + +# ------------------------------------------------------------------------------ +class PodError(Exception): pass + +# ------------------------------------------------------------------------------ +def getOsTempFolder(): + tmp = '/tmp' + if os.path.exists(tmp) and os.path.isdir(tmp): + res = tmp + elif os.environ.has_key('TMP'): + res = os.environ['TMP'] + elif os.environ.has_key('TEMP'): + res = os.environ['TEMP'] + else: + raise "Sorry, I can't find a temp folder on your machine." + return res + +# Error-related constants ------------------------------------------------------ +POD_ERROR = 'An error occurred while generating the document. Please check ' \ + 'the following things if you wanted to generate the document in ' \ + 'PDF, DOC or RTF: (1) OpenOffice is started in server mode on ' \ + 'the port you should have specified in the PloneMeeting ' \ + 'configuration (go to Site setup-> PloneMeeting configuration); ' \ + '(2) if the Python interpreter running Zope and ' \ + 'Plone is not able to discuss with OpenOffice (it does not have ' \ + '"uno" installed - check it by typing "import uno" at the Python ' \ + 'prompt) please specify, in the PloneMeeting configuration, ' \ + 'the path to a UNO-enabled Python interpreter (ie, the Python ' \ + 'interpreter included in the OpenOffice distribution, or, if ' \ + 'your server runs Ubuntu, the standard Python interpreter ' \ + 'installed in /usr/bin/python). Here is the error as reported ' \ + 'by the appy.pod library:\n\n %s' +DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' + +# ------------------------------------------------------------------------------ +class PodTemplateMixin(AbstractMixin): + _appy_meta_type = 'podtemplate' + def generateDocument(self, obj): + '''Generates a document from this template, for object p_obj.''' + appySelf = self._appy_getWrapper(force=True) + appName = self.getProductConfig().PROJECTNAME + appModule = getattr(self.getProductConfig(), appName) + # Temporary file where to generate the result + tempFileName = '%s/%s_%f.%s' % ( + getOsTempFolder(), obj.UID(), time.time(), self.getPodFormat()) + # Define parameters to pass to the appy.pod renderer + currentUser = self.portal_membership.getAuthenticatedMember() + podContext = {'self': obj._appy_getWrapper(force=True), + 'user': currentUser, + 'podTemplate': appySelf, + 'now': self.getProductConfig().DateTime(), + 'projectFolder': os.path.dirname(appModule.__file__) + } + rendererParams = {'template': StringIO(appySelf.podTemplate), + 'context': podContext, + 'result': tempFileName } + if appySelf.tool.unoEnabledPython: + rendererParams['pythonWithUnoPath'] = appySelf.tool.unoEnabledPython + if appySelf.tool.openOfficePort: + rendererParams['ooPort'] = appySelf.tool.openOfficePort + # Launch the renderer + import appy.pod + try: + renderer = appy.pod.renderer.Renderer(**rendererParams) + renderer.run() + except appy.pod.PodError, pe: + if not os.path.exists(tempFileName): + # In some (most?) cases, when OO returns an error, the result is + # nevertheless generated. + raise PodError(POD_ERROR % str(pe)) + # Open the temp file on the filesystem + f = file(tempFileName, 'rb') + forBrowser = True + if forBrowser: + # Create a OFS.Image.File object that will manage correclty HTTP + # headers, etc. + theFile = self.getProductConfig().File('dummyId', 'dummyTitle', f, + content_type=mimeTypes[appySelf.podFormat]) + res = theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE) + else: + # I must return the raw document content. + res = f.read() + f.close() + # Returns the doc and removes the temp file + try: + os.remove(tempFileName) + except OSError, oe: + self.getProductConfig().logger.warn(DELETE_TEMP_DOC_ERROR % str(oe)) + except IOError, ie: + self.getProductConfig().logger.warn(DELETE_TEMP_DOC_ERROR % str(ie)) + return res +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py new file mode 100644 index 0000000..cdd3fa5 --- /dev/null +++ b/gen/plone25/mixins/ToolMixin.py @@ -0,0 +1,192 @@ +# ------------------------------------------------------------------------------ +import re, os, os.path +from appy.gen.utils import FieldDescr +from appy.gen.plone25.mixins import AbstractMixin +from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin +from appy.gen.plone25.wrappers import AbstractWrapper + +_PY = 'Please specify a file corresponding to a Python interpreter ' \ + '(ie "/usr/bin/python").' +FILE_NOT_FOUND = 'Path "%s" was not found.' +VALUE_NOT_FILE = 'Path "%s" is not a file. ' + _PY +NO_PYTHON = "Name '%s' does not starts with 'python'. " + _PY +NOT_UNO_ENABLED_PYTHON = '"%s" is not a UNO-enabled Python interpreter. ' \ + 'To check if a Python interpreter is UNO-enabled, ' \ + 'launch it and type "import uno". If you have no ' \ + 'ImportError exception it is ok.' + +# ------------------------------------------------------------------------------ +class ToolMixin(AbstractMixin): + _appy_meta_type = 'tool' + def _appy_validateUnoEnabledPython(self, value): + '''This method represents the validator for field unoEnabledPython. + This field is present on the Tool only if POD is needed.''' + if value: + if not os.path.exists(value): + return FILE_NOT_FOUND % value + if not os.path.isfile(value): + return VALUE_NOT_FILE % value + if not os.path.basename(value).startswith('python'): + return NO_PYTHON % value + if os.system('%s -c "import uno"' % value): + return NOT_UNO_ENABLED_PYTHON % value + return None + + def getFlavour(self, contextObjOrPortalType, appy=False): + '''Gets the flavour that corresponds to p_contextObjOrPortalType.''' + if isinstance(contextObjOrPortalType, basestring): + portalTypeName = contextObjOrPortalType + else: + # It is the contextObj, not a portal type name + portalTypeName = contextObjOrPortalType.portal_type + res = None + appyTool = self._appy_getWrapper(force=True) + flavourNumber = None + nameElems = portalTypeName.split('_') + if len(nameElems) > 1: + try: + flavourNumber = int(nameElems[-1]) + except ValueError: + pass + appName = self.getProductConfig().PROJECTNAME + if flavourNumber != None: + for flavour in appyTool.flavours: + if flavourNumber == flavour.number: + res = flavour + elif portalTypeName == ('%sFlavour' % appName): + # Current object is the Flavour itself. In this cas we simply + # return the wrapped contextObj. Here we are sure that + # contextObjOrPortalType is an object, not a portal type. + res = contextObjOrPortalType._appy_getWrapper(force=True) + if not res and appyTool.flavours: + res = appyTool.flavours[0] + # If appy=False, return the Plone object and not the Appy wrapper + # (this way, we avoid Zope security/access-related problems while + # using this object in Zope Page Templates) + if res and not appy: + res = res.o + return res + + def getFlavoursInfo(self): + '''Returns information about flavours.''' + res = [] + appyTool = self._appy_getWrapper(force=True) + for flavour in appyTool.flavours: + if isinstance(flavour.o, FlavourMixin): + # This is a bug: sometimes other objects are associated as + # flavours. + res.append({'title': flavour.title, 'number':flavour.number}) + return res + + def getAppFolder(self): + '''Returns the folder at the root of the Plone site that is dedicated + to this application.''' + portal = self.getProductConfig().getToolByName( + self, 'portal_url').getPortalObject() + appName = self.getProductConfig().PROJECTNAME + return getattr(portal, appName) + + def showPortlet(self): + return not self.portal_membership.isAnonymousUser() + + def executeQuery(self, queryName, flavourNumber): + if queryName.find(',') != -1: + # Several content types are specified + portalTypes = queryName.split(',') + if flavourNumber != 1: + portalTypes = ['%s_%d' % (pt, flavourNumber) \ + for pt in portalTypes] + else: + portalTypes = queryName + params = {'portal_type': portalTypes, 'batch': True} + res = self.portal_catalog.searchResults(**params) + batchStart = self.REQUEST.get('b_start', 0) + res = self.getProductConfig().Batch(res, + self.getNumberOfResultsPerPage(), int(batchStart), orphan=0) + return res + + def getResultColumnsNames(self, queryName): + contentTypes = queryName.strip(',').split(',') + resSet = None # Temporary set for computing intersections. + res = [] # Final, sorted result. + flavour = None + fieldNames = None + for cType in contentTypes: + # Get the flavour tied to those content types + if not flavour: + flavour = self.getFlavour(cType, appy=True) + if flavour.number != 1: + cType = cType.rsplit('_', 1)[0] + fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType) + if not resSet: + resSet = set(fieldNames) + else: + resSet = resSet.intersection(fieldNames) + # By converting to set, we've lost order. Let's put things in the right + # order. + for fieldName in fieldNames: + if fieldName in resSet: + res.append(fieldName) + return res + + def getResultColumns(self, anObject, queryName): + '''What columns must I show when displaying a list of root class + instances? Result is a list of tuples containing the name of the + column (=name of the field) and a FieldDescr instance.''' + res = [] + for fieldName in self.getResultColumnsNames(queryName): + if fieldName == 'workflowState': + # We do not return a FieldDescr instance if the attributes is + # not a *real* attribute but the workfow state. + res.append(fieldName) + else: + # Create a FieldDescr instance + appyType = anObject.getAppyType(fieldName) + atField = anObject.schema.get(fieldName) + fieldDescr = FieldDescr(atField, appyType, None) + res.append(fieldDescr.get()) + return res + + xhtmlToText = re.compile('<.*?>', re.S) + def getReferenceLabel(self, brain, appyType): + '''p_appyType is a Ref with link=True. I need to display, on an edit + view, the referenced object p_brain in the listbox that will allow + the user to choose which object(s) to link through the Ref. + According to p_appyType, the label may only be the object title, + or more if parameter appyType.shownInfo is used.''' + res = brain.Title + if 'title' in appyType['shownInfo']: + # We may place it at another place + res = '' + appyObj = brain.getObject()._appy_getWrapper(force=True) + for fieldName in appyType['shownInfo']: + value = getattr(appyObj, fieldName) + if isinstance(value, AbstractWrapper): + value = value.title.decode('utf-8') + elif isinstance(value, basestring): + value = value.decode('utf-8') + refAppyType = appyObj.o.getAppyType(fieldName) + if refAppyType and (refAppyType['type'] == 'String') and \ + (refAppyType['format'] == 2): + value = self.xhtmlToText.sub(' ', value) + else: + value = str(value) + prefix = '' + if res: + prefix = ' | ' + res += prefix + value.encode('utf-8') + maxWidth = self.getListBoxesMaximumWidth() + if len(res) > maxWidth: + res = res[:maxWidth-2] + '...' + return res + + translationMapping = {'portal_path': ''} + def translateWithMapping(self, label): + '''Translates p_label in the application domain, with a default + translation mapping.''' + if not self.translationMapping['portal_path']: + self.translationMapping['portal_path'] = \ + self.portal_url.getPortalPath() + appName = self.getProductConfig().PROJECTNAME + return self.utranslate(label, self.translationMapping, domain=appName) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py new file mode 100644 index 0000000..f748b5d --- /dev/null +++ b/gen/plone25/mixins/__init__.py @@ -0,0 +1,840 @@ +'''This package contains mixin classes that are mixed in with generated classes: + - mixins/ClassMixin is mixed in with Standard Archetypes classes; + - mixins/ToolMixin is mixed in with the generated application Tool class; + - mixins/FlavourMixin is mixed in with the generated application Flavour + class. + The AbstractMixin defined hereafter is the base class of any mixin.''' + +# ------------------------------------------------------------------------------ +import os, os.path, sys, types +import appy.gen +from appy.gen import String +from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \ + ValidationErrors, sequenceTypes +from appy.gen.plone25.descriptors import ArchetypesClassDescriptor +from appy.gen.plone25.utils import updateRolesForPermission, getAppyRequest + +# ------------------------------------------------------------------------------ +class AbstractMixin: + '''Every Archetype class generated by appy.gen inherits from a mixin that + inherits from this class. It contains basic functions allowing to + minimize the amount of generated code.''' + + def getAppyType(self, fieldName): + '''Returns the Appy type corresponding to p_fieldName.''' + res = None + if fieldName == 'id': return res + if self.wrapperClass: + baseClass = self.wrapperClass.__bases__[-1] + try: + # If I get the attr on self instead of baseClass, I get the + # property field that is redefined at the wrapper level. + appyType = getattr(baseClass, fieldName) + res = self._appy_getTypeAsDict(fieldName, appyType, baseClass) + except AttributeError: + # Check for another parent + if self.wrapperClass.__bases__[0].__bases__: + baseClass = self.wrapperClass.__bases__[0].__bases__[-1] + try: + appyType = getattr(baseClass, fieldName) + res = self._appy_getTypeAsDict(fieldName, appyType, + baseClass) + except AttributeError: + pass + return res + + def _appy_getRefs(self, fieldName, ploneObjects=False, + noListIfSingleObj=False): + '''p_fieldName is the name of a Ref field. This method returns an + ordered list containing the objects linked to p_self through this + field. If p_ploneObjects is True, the method returns the "true" + Plone objects instead of the Appy wrappers.''' + res = [] + sortedFieldName = '_appy_%s' % fieldName + exec 'objs = self.get%s%s()' % (fieldName[0].upper(), fieldName[1:]) + if objs: + if type(objs) != list: + objs = [objs] + objectsUids = [o.UID() for o in objs] + sortedObjectsUids = getattr(self, sortedFieldName) + # The list of UIDs may contain too much UIDs; indeed, when deleting + # objects, the list of UIDs are not updated. + uidsToDelete = [] + for uid in sortedObjectsUids: + try: + uidIndex = objectsUids.index(uid) + obj = objs[uidIndex] + if not ploneObjects: + obj = obj._appy_getWrapper(force=True) + res.append(obj) + except ValueError: + uidsToDelete.append(uid) + # Delete unused UIDs + for uid in uidsToDelete: + sortedObjectsUids.remove(uid) + if res and noListIfSingleObj: + appyType = self.getAppyType(fieldName) + if appyType['multiplicity'][1] == 1: + res = res[0] + return res + + def getAppyRefs(self, fieldName): + '''Gets the objects linked to me through p_fieldName.''' + return self._appy_getRefs(fieldName, ploneObjects=True) + + def getAppyRefIndex(self, fieldName, obj): + '''Gets the position of p_obj within Ref field named p_fieldName.''' + sortedFieldName = '_appy_%s' % fieldName + sortedObjectsUids = getattr(self, sortedFieldName) + res = sortedObjectsUids.index(obj.UID()) + return res + + def getAppyBackRefs(self): + '''Returns the list of back references (=types) that are defined for + this class.''' + className = self.__class__.__name__ + referers = self.getProductConfig().referers + res = [] + if referers.has_key(className): + for appyType, relationship in referers[className]: + d = appyType.__dict__ + d['backd'] = appyType.back.__dict__ + res.append((d, relationship)) + return res + + def getAppyRefPortalType(self, fieldName): + '''Gets the portal type of objects linked to me through Ref field named + p_fieldName.''' + appyType = self.getAppyType(fieldName) + tool = self.getTool() + if self._appy_meta_type == 'flavour': + flavour = self._appy_getWrapper(force=True) + else: + portalTypeName = self._appy_getPortalType(self.REQUEST) + flavour = tool.getFlavour(portalTypeName) + return self._appy_getAtType(appyType['klass'], flavour) + + def _appy_getOrderedFields(self, isEdit): + '''Gets all fields (normal fields, back references, fields to show, + fields to hide) in order, in the form of a list of FieldDescr + instances.''' + orderedFields = [] + # Browse Archetypes fields + for atField in self.Schema().filterFields(isMetadata=0): + fieldName = atField.getName() + appyType = self.getAppyType(fieldName) + if not appyType: + if isEdit and (fieldName == 'title'): + # We must provide a dummy appy type for it. Else, it will + # not be rendered in the "edit" form. + appyType = String(multiplicity=(1,1)).__dict__ + else: + continue # Special fields like 'id' are not relevant + # Do not display title on view page; it is already in the header + if not isEdit and (fieldName=='title'): pass + else: + orderedFields.append(FieldDescr(atField, appyType, None)) + # Browse back references + for appyType, fieldRel in self.getAppyBackRefs(): + orderedFields.append(FieldDescr(None, appyType, fieldRel)) + # If some fields must be moved, do it now + res = [] + for fieldDescr in orderedFields: + if fieldDescr.appyType['move']: + newPosition = len(res) - abs(fieldDescr.appyType['move']) + if newPosition <= 0: + newPosition = 0 + res.insert(newPosition, fieldDescr) + else: + res.append(fieldDescr) + return res + + def showField(self, fieldDescr, isEdit=False): + '''Must I show field corresponding to p_fieldDescr?''' + if isinstance(fieldDescr, FieldDescr): + fieldDescr = fieldDescr.__dict__ + appyType = fieldDescr['appyType'] + if isEdit and (appyType['type']=='Ref') and appyType['add']: + return False + if (fieldDescr['widgetType'] == 'backField') and \ + not self.getBRefs(fieldDescr['fieldRel']): + return False + # Do not show field if it is optional and not selected in flavour + if appyType['optional']: + tool = self.getTool() + flavour = tool.getFlavour(self, appy=True) + flavourAttrName = 'optionalFieldsFor%s' % self.meta_type + flavourAttrValue = getattr(flavour, flavourAttrName, ()) + if fieldDescr['atField'].getName() not in flavourAttrValue: + return False + # Check if the user has the permission to view or edit the field + if fieldDescr['widgetType'] != 'backField': + user = self.portal_membership.getAuthenticatedMember() + if isEdit: + perm = fieldDescr['atField'].write_permission + else: + perm = fieldDescr['atField'].read_permission + if not user.has_permission(perm, self): + return False + # Evaluate fieldDescr['show'] + if callable(fieldDescr['show']): + obj = self._appy_getWrapper(force=True) + res = fieldDescr['show'](obj) + else: + res = fieldDescr['show'] + return res + + def getAppyFields(self, isEdit, page): + '''Returns the fields sorted by group. For every field, a dict + containing the relevant info needed by the view or edit templates is + given.''' + res = [] + groups = {} # The already encountered groups + for fieldDescr in self._appy_getOrderedFields(isEdit): + # Select only widgets shown on current page + if fieldDescr.page != page: + continue + # Do not take into account hidden fields and fields that can't be + # edited through the edit view + if not self.showField(fieldDescr, isEdit): continue + if not fieldDescr.group: + res.append(fieldDescr.get()) + else: + # Have I already met this group? + groupName, cols = GroupDescr.getGroupInfo(fieldDescr.group) + if not groups.has_key(groupName): + groupDescr = GroupDescr(groupName, cols, + fieldDescr.appyType['page']).get() + groups[groupName] = groupDescr + res.append(groupDescr) + else: + groupDescr = groups[groupName] + groupDescr['fields'].append(fieldDescr.get()) + if groups: + for groupDict in groups.itervalues(): + GroupDescr.computeRows(groupDict) + return res + + def getAppyStates(self, phase, currentOnly=False): + '''Returns information about the states that are related to p_phase. + If p_currentOnly is True, we return the current state, even if not + related to p_phase.''' + res = [] + dcWorkflow = self.getWorkflow(appy=False) + if not dcWorkflow: return res + currentState = self.portal_workflow.getInfoFor(self, 'review_state') + if currentOnly: + return [StateDescr(currentState,'current').get()] + workflow = self.getWorkflow(appy=True) + if workflow: + stateStatus = 'done' + for stateName in workflow._states: + if stateName == currentState: + stateStatus = 'current' + elif stateStatus != 'done': + stateStatus = 'future' + state = getattr(workflow, stateName) + if (state.phase == phase) and \ + (self._appy_showState(workflow, state.show)): + res.append(StateDescr(stateName, stateStatus).get()) + return res + + def getAppyPage(self, isEdit, phaseInfo, appyName=True): + '''On which page am I? p_isEdit indicates if the current page is an + edit or consult view. p_phaseInfo indicates the current phase.''' + pageAttr = 'pageName' + if isEdit: + pageAttr = 'fieldset' # Archetypes page name + default = phaseInfo['pages'][0] + # Default page is the first page of the current phase + res = self.REQUEST.get(pageAttr, default) + if appyName and (res == 'default'): + res = 'main' + return res + + def getAppyPages(self, phase='main'): + '''Gets the list of pages that are defined for this content type.''' + res = [] + for atField in self.Schema().filterFields(isMetadata=0): + appyType = self.getAppyType(atField.getName()) + if not appyType: continue + if (appyType['phase'] == phase) and (appyType['page'] not in res) \ + and self._appy_showPage(appyType['page'], appyType['pageShow']): + res.append(appyType['page']) + for appyType, fieldRel in self.getAppyBackRefs(): + if (appyType['backd']['phase'] == phase) and \ + (appyType['backd']['page'] not in res) and \ + self._appy_showPage(appyType['backd']['page'], + appyType['backd']['pageShow']): + res.append(appyType['backd']['page']) + return res + + def getAppyPhases(self, currentOnly=False, fieldset=None, forPlone=False): + '''Gets the list of phases that are defined for this content type. If + p_currentOnly is True, the search is limited to the current phase. + If p_fieldset is not None, the search is limited to the phase + corresponding the Plone fieldset whose name is given in this + parameter. If p_forPlone=True, among phase info we write Plone + fieldset names, which are a bit different from Appy page names.''' + # Get the list of phases + res = [] # Ordered list of phases + phases = {} # Dict of phases + for atField in self.Schema().filterFields(isMetadata=0): + appyType = self.getAppyType(atField.getName()) + if not appyType: continue + if appyType['phase'] not in phases: + phase = PhaseDescr(appyType['phase'], + self.getAppyStates(appyType['phase']), forPlone, self) + res.append(phase.__dict__) + phases[appyType['phase']] = phase + else: + phase = phases[appyType['phase']] + phase.addPage(appyType, self) + for appyType, fieldRel in self.getAppyBackRefs(): + if appyType['backd']['phase'] not in phases: + phase = PhaseDescr(appyType['backd']['phase'], + self.getAppyStates(appyType['backd']['phase']), + forPlone, self) + res.append(phase.__dict__) + phases[appyType['phase']] = phase + else: + phase = phases[appyType['backd']['phase']] + phase.addPage(appyType['backd'], 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] + # Then, compute status of phases + for ph in phases.itervalues(): + ph.computeStatus() + ph.totalNbOfPhases = len(res) + # Restrict the result if we must not produce the whole list of phases + if currentOnly: + for phaseInfo in res: + if phaseInfo['phaseStatus'] == 'Current': + return phaseInfo + elif fieldset: + for phaseInfo in res: + if fieldset in phaseInfo['pages']: + return phaseInfo + else: + return res + + def changeAppyRefOrder(self, fieldName, objectUid, newIndex, isDelta): + '''This method changes the position of object with uid p_objectUid in + reference field p_fieldName to p_newIndex i p_isDelta is False, or + to actualIndex+p_newIndex if p_isDelta is True.''' + sortedFieldName = '_appy_%s' % fieldName + sortedObjectsUids = getattr(self, sortedFieldName) + oldIndex = sortedObjectsUids.index(objectUid) + sortedObjectsUids.remove(objectUid) + if isDelta: + newIndex = oldIndex + newIndex + else: + pass # To implement later on + sortedObjectsUids.insert(newIndex, objectUid) + + def getWorkflow(self, appy=True): + '''Returns the Appy workflow instance that is relevant for this + object. If p_appy is False, it returns the DC workflow.''' + res = None + if appy: + # Get the workflow class first + workflowClass = None + if self.wrapperClass: + appyClass = self.wrapperClass.__bases__[1] + if hasattr(appyClass, 'workflow'): + workflowClass = appyClass.workflow + if workflowClass: + # Get the corresponding prototypical workflow instance + res = self.getProductConfig().workflowInstances[workflowClass] + else: + dcWorkflows = self.portal_workflow.getWorkflowsFor(self) + if dcWorkflows: + res = dcWorkflows[0] + return res + + def getWorkflowLabel(self, stateName=None): + '''Gets the i18n label for the workflow current state. If no p_stateName + is given, workflow label is given for the current state.''' + res = '' + wf = self.getWorkflow(appy=False) + if wf: + res = stateName + if not res: + res = self.portal_workflow.getInfoFor(self, 'review_state') + appyWf = self.getWorkflow(appy=True) + if appyWf: + res = '%s_%s' % (wf.id, res) + return res + + def getComputedValue(self, appyType): + '''Computes on p_self the value of the Computed field corresponding to + p_appyType.''' + res = '' + obj = self._appy_getWrapper(force=True) + if appyType['method']: + try: + res = appyType['method'](obj) + if not isinstance(res, basestring): + res = repr(res) + except Exception, e: + res = str(e) + return res + + def may(self, transitionName): + '''May the user execute transition named p_transitionName?''' + # Get the Appy workflow instance + workflow = self.getWorkflow() + res = False + if workflow: + # Get the corresponding Appy transition + transition = workflow._transitionsMapping[transitionName] + user = self.portal_membership.getAuthenticatedMember() + if isinstance(transition.condition, basestring): + # It is a role. Transition may be triggered if the user has this + # role. + res = user.has_role(transition.condition, self) + elif type(transition.condition) == types.FunctionType: + obj = self._appy_getWrapper() + res = transition.condition(workflow, obj) + elif type(transition.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 roleOrFunction in transition.condition: + if isinstance(roleOrFunction, basestring): + if hasRole == None: + hasRole = False + if user.has_role(roleOrFunction, self): + hasRole = True + elif type(roleOrFunction) == types.FunctionType: + obj = self._appy_getWrapper() + if not roleOrFunction(workflow, obj): + return False + if hasRole != False: + res = True + return res + + def executeAppyAction(self, actionName, reindex=True): + '''Executes action with p_fieldName on this object.''' + appyClass = self.wrapperClass.__bases__[1] + res = getattr(appyClass, actionName)(self._appy_getWrapper(force=True)) + self.reindexObject() + return res + + def callAppySelect(self, selectMethod, brains): + '''Selects objects from a Reference field.''' + if selectMethod: + obj = self._appy_getWrapper(force=True) + allObjects = [b.getObject()._appy_getWrapper() \ + for b in brains] + filteredObjects = selectMethod(obj, allObjects) + filteredUids = [o.o.UID() for o in filteredObjects] + res = [] + for b in brains: + if b.UID in filteredUids: + res.append(b) + else: + res = brains + return res + + def getCssClasses(self, appyType, asSlave=True): + '''Gets the CSS classes (used for master/slave relationships) for this + object, either as slave (p_asSlave=True) either as master. The HTML + element on which to define the CSS class for a slave or a master is + different. So this method is called either for getting CSS classes + as slave or as master.''' + res = '' + if not asSlave and appyType['slaves']: + res = 'appyMaster master_%s' % appyType['id'] + elif asSlave and appyType['master']: + res = 'slave_%s' % appyType['master'].id + res += ' slaveValue_%s_%s' % (appyType['master'].id, + appyType['masterValue']) + return res + + def fieldValueSelected(self, fieldName, value, vocabValue): + '''When displaying a selection box (ie a String with a validator being a + list), must the _vocabValue appear as selected?''' + # Check according to database value + if (type(value) in sequenceTypes): + if vocabValue in value: return True + else: + if vocabValue == value: return True + # Check according to value in request + valueInReq = self.REQUEST.get(fieldName, None) + if type(valueInReq) in sequenceTypes: + if vocabValue in valueInReq: return True + else: + if vocabValue == valueInReq: return True + return False + + def checkboxChecked(self, fieldName, value): + '''When displaying a checkbox, must it be checked or not?''' + valueInReq = self.REQUEST.get(fieldName, None) + if valueInReq != None: + return valueInReq in ('True', 1, '1') + else: + return value + + def getLabelPrefix(self, fieldName=None): + '''For some i18n labels, wee need to determine a prefix, which may be + linked to p_fieldName. Indeed, the prefix may be based on the name + of the (super-)class where p_fieldName is defined.''' + res = self.meta_type + if fieldName: + appyType = self.getAppyType(fieldName) + res = '%s_%s' % (self._appy_getAtType(appyType['selfClass']), + fieldName) + return res + + def _appy_getWrapper(self, force=False): + '''Returns the wrapper object for p_self. It is created if it did not + exist.''' + if (not hasattr(self.aq_base, 'appyWrapper')) or force: + # In some cases (p_force=True), we need to re-generate the + # wrapper object. Else, acquisition may be lost on wrapper.o. + self.appyWrapper = self.wrapperClass(self) + return self.appyWrapper + + def _appy_getSourceClass(self, fieldName, baseClass): + '''We know that p_fieldName was defined on Python class p_baseClass or + one of its parents. This method returns the exact class (p_baseClass + or a parent) where it was defined.''' + if fieldName in baseClass.__dict__: + return baseClass + else: + return self._appy_getSourceClass(fieldName, baseClass.__bases__[0]) + + def _appy_getTypeAsDict(self, fieldName, appyType, baseClass): + '''Within page templates, the appyType is given as a dict instead of + an object in order to avoid security problems.''' + appyType.selfClass = self._appy_getSourceClass(fieldName, baseClass) + res = appyType.__dict__ + if res.has_key('back') and res['back'] and (not res.has_key('backd')): + res['backd'] = res['back'].__dict__ + # I create a new entry "backd"; if I put the dict in "back" I + # really modify the initial appyType object and I don't want to do + # this. + return res + + def _appy_getAtType(self, appyClass, flavour=None): + '''Gets the name of the Archetypes class that corresponds to + p_appyClass (which is a Python class coming from the user + application). If p_flavour is specified, the method returns the name + of the specific Archetypes class in this flavour (ie suffixed with + the flavour number).''' + res = ArchetypesClassDescriptor.getClassName(appyClass) + appName = self.getProductConfig().PROJECTNAME + if res.find('Extensions_appyWrappers') != -1: + # This is not a content type defined Maybe I am a tool or flavour + res = appName + appyClass.__name__ + elif issubclass(appyClass, appy.gen.Tool): + # This is the custom tool + res = '%sTool' % appName + elif issubclass(appyClass, appy.gen.Flavour): + # This is the custom Flavour + res = '%sFlavour' % appName + else: + if flavour and flavour.number != 1: + res += '_%d' % flavour.number + return res + + def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False, + noListIfSingleObj=False): + '''This method returns the list of objects linked to this one + through the BackRef corresponding to the Archetypes + relationship named p_relName.''' + res = [] + referers = self.getProductConfig().referers + objs = self.getBRefs(relName) + for obj in objs: + if not ploneObjects: + obj = obj._appy_getWrapper(force=True) + res.append(obj) + if res and noListIfSingleObj: + className = self.__class__.__name__ + appyType = None + for anAppyType, rel in referers[className]: + if rel == relName: + appyType = anAppyType + break + if appyType.back.multiplicity[1] == 1: + res = res[0] + return res + + def _appy_showPage(self, page, pageShow): + '''Must I show p_page?''' + if callable(pageShow): + return pageShow(self._appy_getWrapper(force=True)) + else: return pageShow + + def _appy_showState(self, workflow, stateShow): + '''Must I show a state whose "show value" is p_stateShow?''' + if callable(stateShow): + return stateShow(workflow, self._appy_getWrapper()) + else: return stateShow + + def _appy_managePermissions(self): + '''When an object is created or updated, we must update "add" + permissions accordingly: if the object is a folder, we must set on + it permissions that will allow to create, inside it, objects through + Ref fields; if it is not a folder, we must update permissions on its + parent folder instead.''' + # Determine on which folder we need to set "add" permissions + folder = self + if not self.isPrincipiaFolderish: + folder = self.getParentNode() + # On this folder, set "add" permissions for every content type that will + # be created through reference fields + allCreators = set() + for field in self.schema.fields(): + if field.type == 'reference': + refContentTypeName= self.getAppyRefPortalType(field.getName()) + refContentType = getattr(self.portal_types, refContentTypeName) + refMetaType = refContentType.content_meta_type + if refMetaType in self.getProductConfig(\ + ).ADD_CONTENT_PERMISSIONS: + # No specific "add" permission is defined for tool and + # flavour, for example. + appyClass = refContentType.wrapperClass.__bases__[-1] + # Get roles that may add this content type + creators = getattr(appyClass, 'creators', None) + if not creators: + creators = self.getProductConfig().defaultAddRoles + allCreators = allCreators.union(creators) + # Grant this "add" permission to those roles + updateRolesForPermission( + self.getProductConfig().ADD_CONTENT_PERMISSIONS[\ + refMetaType], creators, folder) + # Beyond content-type-specific "add" permissions, creators must also + # have the main permission "Add portal content". + if allCreators: + updateRolesForPermission('Add portal content', tuple(allCreators), + folder) + + def _appy_onEdit(self, created): + '''What happens when an object is created (p_created=True) or edited?''' + # Manage references + self._appy_manageRefs(created) + if self.wrapperClass: + # Get the wrapper first + appyWrapper = self._appy_getWrapper(force=True) + # Call the custom "onEdit" if available + try: + appyWrapper.onEdit(created) + except AttributeError, ae: + pass + # Manage "add" permissions + self._appy_managePermissions() + # Re/unindex object + if self._appy_meta_type == 'tool': self.unindexObject() + else: self.reindexObject() + + def _appy_getDisplayList(self, values, labels, domain): + '''Creates a DisplayList given a list of p_values and corresponding + i18n p_labels.''' + res = [] + i = -1 + for v in values: + i += 1 + res.append( (v, self.utranslate(labels[i], domain=domain))) + return self.getProductConfig().DisplayList(tuple(res)) + + nullValues = (None, '', ' ') + numbersMap = {'Integer': 'int', 'Float': 'float'} + validatorTypes = (types.FunctionType, type(String.EMAIL)) + def _appy_validateField(self, fieldName, value, label, specificType): + '''Checks whether the p_value entered in field p_fieldName is + correct.''' + appyType = self.getAppyType(fieldName) + msgId = None + if (specificType == 'Ref') and appyType['link']: + # 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. + # If the field is a reference, appy must ensure itself that + # multiplicities are enforced. + fieldValue = self.REQUEST.get('appy_ref_%s' % fieldName, '') + if not fieldValue: + nbOfRefs = 0 + elif isinstance(fieldValue, basestring): + nbOfRefs = 1 + else: + nbOfRefs = len(fieldValue) + minRef = appyType['multiplicity'][0] + maxRef = appyType['multiplicity'][1] + if maxRef == None: + maxRef = sys.maxint + if nbOfRefs < minRef: + msgId = 'min_ref_violated' + elif nbOfRefs > maxRef: + msgId = 'max_ref_violated' + elif specificType in self.numbersMap: # Float, Integer + pyType = self.numbersMap[specificType] + # Validate only if input value is there. + # By the way, we also convert the value. + if value not in self.nullValues: + try: + exec 'value = %s(value)' % pyType + except ValueError: + msgId = 'bad_%s' % pyType + else: + value = None + # Apply the custom validator if it exists + validator = appyType['validator'] + if not msgId and (type(validator) in self.validatorTypes): + obj = self._appy_getWrapper(force=True) + if type(validator) == self.validatorTypes[0]: + # It is a custom function. Execute it. + try: + validValue = validator(obj, value) + if isinstance(validValue, basestring) and validValue: + # Validation failed; and p_validValue contains an error + # message. + return validValue + else: + if not validValue: + msgId = label + except Exception, e: + return str(e) + except: + msgId = label + elif type(validator) == self.validatorTypes[1]: + # It is a regular expression + if (value not in self.nullValues) and \ + not validator.match(value): + # If the regular expression is among the default ones, we + # generate a specific error message. + if validator == String.EMAIL: + msgId = 'bad_email' + elif validator == String.URL: + msgId = 'bad_url' + elif validator == String.ALPHANUMERIC: + msgId = 'bad_alphanumeric' + else: + msgId = label + res = msgId + if msgId: + res = self.utranslate(msgId, domain=self.i18nDomain) + return res + + def _appy_validateAllFields(self, REQUEST, errors): + '''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-fields validation errors.''' + obj = self._appy_getWrapper() + appyRequest = getAppyRequest(REQUEST, obj) + try: + appyErrors = ValidationErrors() + obj.validate(appyRequest, appyErrors) + # This custom "validate" method may have added fields in the given + # ValidationErrors instance. Now we must fill the Zope "errors" dict + # based on it. 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 appyErrors.__dict__.iteritems(): + resValue = value + if not isinstance(resValue, basestring): + msgId = '%s_valid' % self.getLabelPrefix(key) + resValue = self.utranslate(msgId, domain=self.i18nDomain) + errors[key] = resValue + except AttributeError: + pass + + def _appy_getPortalType(self, request): + '''Guess the portal_type of p_self from info about p_self and + p_request.''' + res = None + # If the object is being created, self.portal_type is not correctly + # initialized yet. + if request.has_key('__factory__info__'): + factoryInfo = request['__factory__info__'] + if factoryInfo.has_key('stack'): + res = factoryInfo['stack'][0] + if not res: + res = self.portal_type + return res + + def _appy_generateDocument(self): + '''Generates the document from a template whose UID is specified in the + request for a given object whose UID is also in the request.''' + # Get the object + objectUid = self.REQUEST.get('objectUid') + obj = self.uid_catalog(UID=objectUid)[0].getObject() + # Get the POD template + templateUid = self.REQUEST.get('templateUid') + podTemplate = self.uid_catalog(UID=templateUid)[0].getObject() + return podTemplate.generateDocument(obj) + + def _appy_manageSortedRefs(self): + '''For every reference field, this method creates the additional + reference lists that are ordered (if it did not already exist).''' + for field in self.schema.fields(): + if field.type == 'reference': + sortedRefField = '_appy_%s' % field.getName() + if not hasattr(self.aq_base, sortedRefField): + pList = self.getProductConfig().PersistentList + exec 'self.%s = pList()' % sortedRefField + + def _appy_manageRefs(self, created): + '''Every time an object is created or updated, this method updates + the Reference fields accordingly.''' + self._appy_manageSortedRefs() + self._appy_manageRefsFromRequest() + # If the creation was initiated by another object, update the + # reference. + if created: + session = self.REQUEST.SESSION + initiatorUid = session.get('initiator', None) + initiator = None + if initiatorUid: + initiatorRes = self.uid_catalog.searchResults(UID=initiatorUid) + if initiatorRes: + initiator = initiatorRes[0].getObject() + if initiator: + fieldName = session.get('initiatorField') + initiator._appy_getWrapper(force=True).link(fieldName, self) + # Re-initialise the session + session['initiator'] = None + + def _appy_manageRefsFromRequest(self): + '''Appy manages itself some Ref fields (with link=True). So here we must + update the Ref fields.''' + fieldsInRequest = [] # Fields present in the request + for requestKey in self.REQUEST.keys(): + if requestKey.startswith('appy_ref_'): + fieldName = requestKey[9:] + fieldsInRequest.append(fieldName) + fieldValue = self.REQUEST[requestKey] + sortedRefField = getattr(self, '_appy_%s' % fieldName) + del sortedRefField[:] + if isinstance(fieldValue, basestring): + fieldValue = [fieldValue] + refObjects = [] + for uid in fieldValue: + obj = self.uid_catalog(UID=uid)[0].getObject() + refObjects.append(obj) + sortedRefField.append(uid) + exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(), + fieldName[1:]) + # Manage Ref fields that are not present in the request + currentFieldset = self.REQUEST.get('fieldset', 'default') + for field in self.schema.fields(): + if (field.type == 'reference') and \ + (field.schemata == currentFieldset) and \ + (field.getName() not in fieldsInRequest): + # If this field is visible, it was not present in the request: + # it means that we must remove any Ref from it. + fieldName = field.getName() + appyType = self.getAppyType(fieldName) + fieldDescr = FieldDescr(field, appyType, None) + if self.showField(fieldDescr, isEdit=True): + exec 'self.set%s%s([])' % (fieldName[0].upper(), + fieldName[1:]) +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/model.py b/gen/plone25/model.py new file mode 100755 index 0000000..f59cfc6 --- /dev/null +++ b/gen/plone25/model.py @@ -0,0 +1,226 @@ +'''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. The "Tool" is + available to administrators under the standard Plone link "site setup". Plone + itself is shipped with several tools used for conguring the various parts of + Plone (content types, catalogs, workflows, etc.)''' + +# ------------------------------------------------------------------------------ +import copy, types +from appy.gen import Type, Integer, String, File, Ref, Boolean + +# ------------------------------------------------------------------------------ +class ModelClass: + '''This class is the abstract class of all predefined application classes + used in the Appy model: Tool, Flavour, PodTemplate, 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.''' + _appy_attributes = [] # We need to keep track of attributes order. + _appy_notinit = ('id', 'type', 'pythonType', 'slaves', 'selfClass', + 'phase', 'pageShow') # When creating a new instance of a + # ModelClass, those attributes must not be given in the + # constructor. + + def _appy_addField(klass, fieldName, fieldType, classDescr): + exec "klass.%s = fieldType" % fieldName + klass._appy_attributes.append(fieldName) + if hasattr(klass, '_appy_classes'): + klass._appy_classes[fieldName] = classDescr.name + _appy_addField = classmethod(_appy_addField) + + def _appy_getTypeBody(klass, appyType): + '''This method returns the code declaration for p_appyType.''' + typeArgs = '' + for attrName, attrValue in appyType.__dict__.iteritems(): + if attrName in ModelClass._appy_notinit: + continue + if isinstance(attrValue, basestring): + attrValue = '"%s"' % attrValue + elif isinstance(attrValue, Type): + attrValue = klass._appy_getTypeBody(attrValue) + elif type(attrValue) == type(ModelClass): + moduleName = attrValue.__module__ + if moduleName.startswith('appy.gen'): + attrValue = attrValue.__name__ + else: + attrValue = '%s.%s' % (moduleName, attrValue.__name__) + typeArgs += '%s=%s,' % (attrName, attrValue) + return '%s(%s)' % (appyType.__class__.__name__, typeArgs) + _appy_getTypeBody = classmethod(_appy_getTypeBody) + + def _appy_getBody(klass): + '''This method returns the code declaration of this class. We will dump + this in appyWrappers.py in the resulting product.''' + res = '' + for attrName in klass._appy_attributes: + exec 'appyType = klass.%s' % attrName + res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType)) + return res + _appy_getBody = classmethod(_appy_getBody) + +class PodTemplate(ModelClass): + description = String(format=String.TEXT) + podTemplate = File(multiplicity=(1,1)) + podFormat = String(validator=['odt', 'pdf', 'rtf', 'doc'], + multiplicity=(1,1), default='odt') + phase = String(default='main') + _appy_attributes = ['description', 'podTemplate', 'podFormat', 'phase'] + +defaultFlavourAttrs = ('number', 'enableNotifications') +flavourAttributePrefixes = ('optionalFieldsFor', 'defaultValueFor', + 'podTemplatesFor', 'podMaxShownTemplatesFor', 'resultColumnsFor', + 'showWorkflowFor', 'showWorkflowCommentFieldFor', 'showAllStatesInPhaseFor') +# Attribute prefixes of the fields generated on the Flavour for configuring +# the application classes. + +class Flavour(ModelClass): + '''For every application, the Flavour may be different (it depends on the + fields declared as optional, etc). Instead of creating a new way to + generate the Archetypes Flavour class, we create a silly + FlavourStub instance and we will use the standard Archetypes + generator that generates classes from the application to generate the + flavour class.''' + number = Integer(default=1, show=False) + enableNotifications = Boolean(default=True, page='notifications') + _appy_classes = {} # ~{s_attributeName: s_className}~ + # We need to remember the original classes related to the flavour attributes + _appy_attributes = list(defaultFlavourAttrs) + + def _appy_clean(klass): + toClean = [] + for k, v in klass.__dict__.iteritems(): + if not k.startswith('__') and (not k.startswith('_appy_')): + if k not in defaultFlavourAttrs: + toClean.append(k) + for k in toClean: + exec 'del klass.%s' % k + klass._appy_attributes = list(defaultFlavourAttrs) + klass._appy_classes = {} + _appy_clean = classmethod(_appy_clean) + + def _appy_copyField(klass, appyType): + '''From a given p_appyType, produce a type definition suitable for + storing the default value for this field.''' + res = copy.copy(appyType) + res.editDefault = False + res.optional = False + res.show = True + res.phase = 'main' + res.specificReadPermission = False + res.specificWritePermission = False + res.multiplicity = (0, appyType.multiplicity[1]) + if type(res.validator) == types.FunctionType: + # We will not be able to call this function from the flavour. + res.validator = None + if isinstance(appyType, Ref): + res.link = True + res.add = False + res.back = copy.copy(appyType.back) + res.back.attribute += 'DefaultValue' + res.back.show = False + res.select = None # Not callable from flavour + return res + _appy_copyField = classmethod(_appy_copyField) + + def _appy_addOptionalField(klass, fieldDescr): + className = fieldDescr.classDescr.name + fieldName = 'optionalFieldsFor%s' % className + fieldType = getattr(klass, fieldName, None) + if not fieldType: + fieldType = String(multiplicity=(0,None)) + fieldType.validator = [] + klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr) + fieldType.validator.append(fieldDescr.fieldName) + fieldType.page = 'data' + fieldType.group = fieldDescr.classDescr.klass.__name__ + _appy_addOptionalField = classmethod(_appy_addOptionalField) + + def _appy_addDefaultField(klass, fieldDescr): + className = fieldDescr.classDescr.name + fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName) + fieldType = klass._appy_copyField(fieldDescr.appyType) + klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr) + fieldType.page = 'data' + fieldType.group = fieldDescr.classDescr.klass.__name__ + _appy_addDefaultField = classmethod(_appy_addDefaultField) + + def _appy_addPodField(klass, classDescr): + '''Adds a POD field to the flavour and also an integer field that will + determine the maximum number of documents to show at once on consult + views. If this number is reached, a list is displayed.''' + # First, add the POD field that will hold PodTemplates. + fieldType = Ref(PodTemplate, multiplicity=(0,None), add=True, + link=False, back = Ref(attribute='flavour'), + page="documentGeneration", + group=classDescr.klass.__name__) + fieldName = 'podTemplatesFor%s' % classDescr.name + klass._appy_addField(fieldName, fieldType, classDescr) + # Then, add the integer field + fieldType = Integer(default=1, page='userInterface', + group=classDescr.klass.__name__) + fieldName = 'podMaxShownTemplatesFor%s' % classDescr.name + klass._appy_addField(fieldName, fieldType, classDescr) + classDescr.flavourFieldsToPropagate.append( + ('podMaxShownTemplatesFor%s', copy.copy(fieldType)) ) + _appy_addPodField = classmethod(_appy_addPodField) + + def _appy_addQueryResultColumns(klass, classDescr): + className = classDescr.name + fieldName = 'resultColumnsFor%s' % className + attrNames = [a[0] for a in classDescr.getOrderedAppyAttributes()] + attrNames.append('workflowState') # Object state from workflow + if 'title' in attrNames: + attrNames.remove('title') # Included by default. + fieldType = String(multiplicity=(0,None), validator=attrNames, + page='userInterface', + group=classDescr.klass.__name__) + klass._appy_addField(fieldName, fieldType, classDescr) + _appy_addQueryResultColumns = classmethod(_appy_addQueryResultColumns) + + def _appy_addWorkflowFields(klass, classDescr): + '''Adds, for a given p_classDescr, the workflow-related fields.''' + className = classDescr.name + groupName = classDescr.klass.__name__ + # Adds a field allowing to show/hide completely any workflow-related + # information for a given class. + defaultValue = False + if classDescr.isRoot() or issubclass(classDescr.klass, ModelClass): + defaultValue = True + fieldName = 'showWorkflowFor%s' % className + fieldType = Boolean(default=defaultValue, page='userInterface', + group=groupName) + klass._appy_addField(fieldName, fieldType, classDescr) + # Adds the boolean field for showing or not the field "enter comments". + fieldName = 'showWorkflowCommentFieldFor%s' % className + fieldType = Boolean(default=defaultValue, page='userInterface', + group=groupName) + klass._appy_addField(fieldName, fieldType, classDescr) + # Adds the boolean field for showing all states in current state or not. + # If this boolean is True but the current phase counts only one state, + # we will not show the state at all: the fact of knowing in what phase + # we are is sufficient. If this boolean is False, we simply show the + # current state. + defaultValue = False + if len(classDescr.getPhases()) > 1: + defaultValue = True + fieldName = 'showAllStatesInPhaseFor%s' % className + fieldType = Boolean(default=defaultValue, page='userInterface', + group=groupName) + klass._appy_addField(fieldName, fieldType, classDescr) + + _appy_addWorkflowFields = classmethod(_appy_addWorkflowFields) + +class Tool(ModelClass): + flavours = Ref(None, multiplicity=(1,None), add=True, link=False, + back=Ref(attribute='tool')) + # First arg is None because we don't know yet if it will link + # to the predefined Flavour class or a custom class defined + # in the application. + unoEnabledPython = String(group="connectionToOpenOffice") + openOfficePort = Integer(default=2002, group="connectionToOpenOffice") + numberOfResultsPerPage = Integer(default=30) + listBoxesMaximumWidth = Integer(default=100) + _appy_attributes = ['flavours', 'unoEnabledPython', 'openOfficePort', + 'numberOfResultsPerPage', 'listBoxesMaximumWidth'] +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/notifier.py b/gen/plone25/notifier.py new file mode 100644 index 0000000..b205f18 --- /dev/null +++ b/gen/plone25/notifier.py @@ -0,0 +1,105 @@ +'''This package contains functions for sending email notifications.''' + +# ------------------------------------------------------------------------------ +def getEmailAddress(name, email, encoding='utf-8'): + '''Creates a full email address from a p_name and p_email.''' + res = email + if name: res = name.decode(encoding) + ' <%s>' % email + return res + +def convertRolesToEmails(users, portal): + '''p_users is a list of emails and/or roles. This function returns the same + list, where all roles have been expanded to emails of users having this + role (more precisely, users belonging to the group Appy created for the + given role).''' + res = [] + for mailOrRole in users: + if mailOrRole.find('@') != -1: + # It is an email. Append it directly to the result. + res.append(mailOrRole) + else: + # It is a role. Find the corresponding group (Appy creates + # one group for every role defined in the application). + groupId = mailOrRole + '_group' + group = portal.acl_users.getGroupById(groupId) + if group: + for user in group.getAllGroupMembers(): + userMail = user.getProperty('email') + if userMail and (userMail not in res): + res.append(userMail) + return res + +# ------------------------------------------------------------------------------ +SENDMAIL_ERROR = 'Error while sending mail: %s.' +ENCODING_ERROR = 'Encoding error while sending mail: %s.' + +from appy.gen.utils import sequenceTypes +from appy.gen.plone25.descriptors import WorkflowDescriptor +import socket + +def sendMail(obj, transition, transitionName, workflow, logger): + '''Sends mail about p_transition that has been triggered on p_obj that is + controlled by p_workflow.''' + wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__) + ploneObj = obj.o + portal = ploneObj.portal_url.getPortalObject() + mailInfo = transition.notify(workflow, obj) + if not mailInfo[0]: return # Send a mail to nobody. + # mailInfo may be one of the following: + # (to,) + # (to, cc) + # (to, mailSubject, mailBody) + # (to, cc, mailSubject, mailBody) + # "to" and "cc" maybe simple strings (one simple string = one email + # address or one role) or sequences of strings. + # Determine mail subject and body. + if len(mailInfo) <= 2: + # The user didn't mention mail body and subject. We will use + # those defined from i18n labels. + wfHistory = ploneObj.getWorkflowHistory() + labelPrefix = '%s_%s' % (wfName, transitionName) + tName = obj.translate(labelPrefix) + keys = {'siteUrl': portal.absolute_url(), + 'siteTitle': portal.Title(), + 'objectUrl': ploneObj.absolute_url(), + 'objectTitle': ploneObj.Title(), + 'transitionName': tName, + 'transitionComment': wfHistory[0]['comments']} + mailSubject = obj.translate(labelPrefix + '_mail_subject', keys) + mailBody = obj.translate(labelPrefix + '_mail_body', keys) + else: + mailSubject = mailInfo[-1] + mailBody = mailInfo[-2] + # Determine "to" and "cc". + to = mailInfo[0] + cc = [] + if (len(mailInfo) in (2,4)) and mailInfo[1]: cc = mailInfo[1] + if type(to) not in sequenceTypes: to = [to] + if type(cc) not in sequenceTypes: cc = [cc] + # Among "to" and "cc", convert all roles to concrete email addresses + to = convertRolesToEmails(to, portal) + cc = convertRolesToEmails(cc, portal) + # Determine "from" address + enc= portal.portal_properties.site_properties.getProperty('default_charset') + fromAddress = getEmailAddress( + portal.getProperty('email_from_name'), + portal.getProperty('email_from_address'), enc) + # Send the mail + i = 0 + for recipient in to: + i += 1 + try: + if i != 1: cc = [] + portal.MailHost.secureSend(mailBody.encode(enc), + recipient.encode(enc), fromAddress.encode(enc), + mailSubject.encode(enc), mcc=cc, charset='utf-8') + except socket.error, sg: + logger.warn(SENDMAIL_ERROR % str(sg)) + break + except UnicodeDecodeError, ue: + logger.warn(ENCODING_ERROR % str(ue)) + break + except Exception, e: + logger.warn(SENDMAIL_ERROR % str(e)) + break +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/AppyReference.pt b/gen/plone25/templates/AppyReference.pt new file mode 100755 index 0000000..28be62e --- /dev/null +++ b/gen/plone25/templates/AppyReference.pt @@ -0,0 +1,228 @@ + We begin with some sub-macros used within + macro "showReference" defined below. + + + 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. + + + + + Displays icons for triggering actions on a given + referenced object (edit, delete, etc). + + + Edit the element + + Delete the element + + Arrows for moving objects up or down + + +
+ + + + +
+ + + + Arrow up + + + + Arrow down + + + +
+
+
+ + + Displays the "plus" icon that allows to add new object + through a reference widget. Indeed, If field was declared as "addable", we must provide + an icon for creating a new linked object (at least if multiplicities allow it). + + + +
+ + This macro displays the Reference widget on a "consult" page. + + The definition of "atMostOneRef" above may sound strange: we shouldn't check the actual number + of referenced objects. But for back references people often forget to specify multiplicities. + So concretely, multiplicities (0,None) are coded as (0,1). + + + Display a simplified widget if maximum number of + referenced objects is 1. + + + + If there is no object... + + + + + + If there is an object... + + + + + + +
no_ref + +
+
+ + Display a fieldset in all other cases. + +
+ + + + + + Object description +

+ + No object is present +

no_ref

+ + + +
+ + Show backward reference(s) + + + + +
+
+ + Show forward reference(s) + + + + + + + + Object title, shown here if not specified somewhere + else in appyType.shownInfo. + + Additional fields that must be shown + + Actions + + +
+ + + + +
  + + + + + + + + + + + + + + + + + + + +   + + +
+ +
+
+ A carriage return needed in some cases. +
+
+
+ +
+ + This macro displays the Reference widget on an "edit" page + + +
+
+ +
diff --git a/gen/plone25/templates/ArchetypesTemplate.py b/gen/plone25/templates/ArchetypesTemplate.py new file mode 100755 index 0000000..a7bb780 --- /dev/null +++ b/gen/plone25/templates/ArchetypesTemplate.py @@ -0,0 +1,34 @@ + +from AccessControl import ClassSecurityInfo +from Products.Archetypes.atapi import * +import Products..config +from Extensions.appyWrappers import _Wrapper +from appy.gen.plone25.mixins.ClassMixin import ClassMixin + + +schema = Schema(( +),) +fullSchema = .copy() + schema.copy() + +class (): + '''''' + security = ClassSecurityInfo() + __implements__ = + archetype_name = '' + meta_type = '' + portal_type = '' + allowed_content_types = [] + filter_content_types = 0 + global_allow = 1 + immediate_view = '_appy_view' + default_view = '_appy_view' + suppl_views = () + typeDescription = '' + typeDescMsgId = '_edit_descr' + _at_rename_after_creation = True + i18nDomain = '' + schema = fullSchema + wrapperClass = _Wrapper + + + diff --git a/gen/plone25/templates/FlavourTemplate.py b/gen/plone25/templates/FlavourTemplate.py new file mode 100755 index 0000000..4d1ec10 --- /dev/null +++ b/gen/plone25/templates/FlavourTemplate.py @@ -0,0 +1,38 @@ + +from AccessControl import ClassSecurityInfo +from Products.Archetypes.atapi import * +import Products..config +from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin +from Extensions.appyWrappers import + +predefinedSchema = Schema(( +),) +schema = Schema(( +),) +fullSchema = OrderedBaseFolderSchema.copy() + predefinedSchema.copy() + schema.copy() + +class (OrderedBaseFolder, FlavourMixin): + '''Configuration flavour class for .''' + security = ClassSecurityInfo() + __implements__ = (getattr(OrderedBaseFolderSchema,'__implements__',()),) + archetype_name = '' + meta_type = '' + portal_type = '' + allowed_content_types = [] + filter_content_types = 0 + global_allow = 1 + #content_icon = '.gif' + immediate_view = '_appy_view' + default_view = '_appy_view' + suppl_views = () + typeDescription = "" + typeDescMsgId = '_edit_descr' + i18nDomain = '' + schema = fullSchema + allMetaTypes = + wrapperClass = + _at_rename_after_creation = True + + + +registerType(, '') diff --git a/gen/plone25/templates/Install.py b/gen/plone25/templates/Install.py new file mode 100755 index 0000000..7408dd4 --- /dev/null +++ b/gen/plone25/templates/Install.py @@ -0,0 +1,36 @@ + +from zExceptions import BadRequest +from Products.ExternalMethod.ExternalMethod import ExternalMethod +from Products.Archetypes.Extensions.utils import installTypes +from Products.Archetypes.Extensions.utils import install_subskin +from Products.Archetypes.config import TOOL_NAME as ARCHETYPETOOLNAME +from Products.Archetypes.atapi import listTypes +from Products..config import applicationRoles,defaultAddRoles +from Products..config import product_globals as GLOBALS +import appy.gen +from appy.gen.plone25.installer import PloneInstaller + +catalogMap = {} + +appClasses = +appClassNames = [] +allClassNames = [] +workflows = {} +# ------------------------------------------------------------------------------ +def install(self, reinstall=False): + '''Installation of product ""''' + ploneInstaller = PloneInstaller(reinstall, "", self, + , appClasses, appClassNames, allClassNames, + catalogMap, applicationRoles, defaultAddRoles, workflows, + , globals()) + return ploneInstaller.install() + +# ------------------------------------------------------------------------------ +def uninstall(self, reinstall=False): + '''Uninstallation of product ""''' + ploneInstaller = PloneInstaller(reinstall, "", self, + , appClasses, appClassNames, allClassNames, + catalogMap, applicationRoles, defaultAddRoles, workflows, + , globals()) + return ploneInstaller.uninstall() +# ------------------------------------------------------------------------------ diff --git a/gen/plone25/templates/Macros.pt b/gen/plone25/templates/Macros.pt new file mode 100755 index 0000000..3bb670d --- /dev/null +++ b/gen/plone25/templates/Macros.pt @@ -0,0 +1,765 @@ +
+ + + Form submitted when an object needs to be generated as a document. +
+ + +
+ + + + Display templates as links if a few number of templates must be shown + + + + + +   + Display templates as a list if a lot of templates must be shown + + +
+ + +