Initial import
1
__init__.py
Executable file
|
@ -0,0 +1 @@
|
||||||
|
|
0
bin/__init__.py
Executable file
33
bin/clean.py
Executable file
|
@ -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()
|
||||||
|
# ------------------------------------------------------------------------------
|
519
bin/publish.py
Executable file
|
@ -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('<html>\n\n<head><title>%s</title></head>\n\n' \
|
||||||
|
'<body>\n' % self.title)
|
||||||
|
self.htmlFile.write(self.getProlog())
|
||||||
|
inList = False
|
||||||
|
for line in self.txtFile:
|
||||||
|
if self.retainLine(line):
|
||||||
|
firstChar = self.getFirstChar(line)
|
||||||
|
if firstChar == '-':
|
||||||
|
if not inList:
|
||||||
|
# Begin a new bulleted list
|
||||||
|
self.htmlFile.write('<ul>\n')
|
||||||
|
inList = True
|
||||||
|
self.htmlFile.write(
|
||||||
|
'<li>%s</li>\n' % self.getCleanLine(line))
|
||||||
|
elif firstChar == ' ':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# It is a title
|
||||||
|
if inList:
|
||||||
|
self.htmlFile.write('</ul>\n')
|
||||||
|
inList = False
|
||||||
|
self.htmlFile.write(
|
||||||
|
'<h1>%s</h1>\n' % self.getCleanLine(line, True))
|
||||||
|
self.htmlFile.write('\n</ul>\n</body>\n</html>')
|
||||||
|
self.txtFile.close()
|
||||||
|
self.htmlFile.close()
|
||||||
|
|
||||||
|
class 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 <a href="versions/appy.%s.zip">zip</a>)' %(
|
||||||
|
line, version)
|
||||||
|
return line
|
||||||
|
def getProlog(self):
|
||||||
|
return '<p>Appy releases are available for download as zip files ' \
|
||||||
|
'below. Under Windows, unzip the file with a tool like ' \
|
||||||
|
'<a href="http://www.7-zip.org/">7zip</a> 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").</p>' \
|
||||||
|
'<!--p>Appy releases are also available as eggs. In order to ' \
|
||||||
|
'install an appy egg, install setuptools ' \
|
||||||
|
'(more info on <a href="%s">%s</a>) and type "sudo ' \
|
||||||
|
'easy_install appy".</p--> ' \
|
||||||
|
'<p> 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".</p>' \
|
||||||
|
'<!--p>You may also access directly the SVN repository of ' \
|
||||||
|
'the project: <a href="%s">%s</a>.</p-->' % (
|
||||||
|
self.setupToolsUrl, self.setupToolsUrl,
|
||||||
|
self.svnUrl, self.svnUrl)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Publisher:
|
||||||
|
'''Publishes Appy on the web.'''
|
||||||
|
pageBody = re.compile('<body.*?>(.*)</body>', 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('<title>'), pageContent.find('</title>')
|
||||||
|
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('<td class="doc"(.*?)</td>')
|
||||||
|
tocLink = re.compile('<a href="(.*?)">(.*?)</a>')
|
||||||
|
subSection = re.compile('<h1>(.*?)</h1>')
|
||||||
|
subSectionContent = re.compile('<a name="(.*?)">.*?</a>(.*)')
|
||||||
|
def createDocToc(self):
|
||||||
|
res = '<table width="100%"><tr>'
|
||||||
|
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 += '</td>'
|
||||||
|
res += '<td>'
|
||||||
|
else:
|
||||||
|
tag = 'p'
|
||||||
|
indent = 2
|
||||||
|
styleBegin = '<i>'
|
||||||
|
styleEnd = '</i>'
|
||||||
|
tabs = ' ' * indent * 2
|
||||||
|
res += '<%s>%s%s<a href="%s">%s</a>%s</%s>\n' % \
|
||||||
|
(tag, tabs, styleBegin, url, self._getPageTitle(url),
|
||||||
|
styleEnd, tag)
|
||||||
|
# Parse each HTML file and retrieve sections title that have an
|
||||||
|
# anchor defined
|
||||||
|
docFile = file('%s/doc/%s' % (appyPath, url))
|
||||||
|
docContent = docFile.read()
|
||||||
|
docFile.close()
|
||||||
|
sections = self.subSection.findall(docContent)
|
||||||
|
for section in sections:
|
||||||
|
r = self.subSectionContent.search(section)
|
||||||
|
if r:
|
||||||
|
sectionNb += 1
|
||||||
|
tabs = ' ' * 8
|
||||||
|
res += '<div>%s%d. <a href="%s#%s">%s</a></div>\n' % \
|
||||||
|
(tabs, sectionNb, url, r.group(1), r.group(2))
|
||||||
|
res += '</td></tr></table>'
|
||||||
|
f = file(docToc)
|
||||||
|
toc = f.read()
|
||||||
|
f.close()
|
||||||
|
toc = toc.replace('{{ doc }}', res)
|
||||||
|
f = file(docToc, 'w')
|
||||||
|
f.write(toc)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
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()
|
||||||
|
# ------------------------------------------------------------------------------
|
4
bin/runOpenOffice.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
/opt/openoffice.org3/program/soffice "-accept=socket,host=localhost,port=2002;urp;"
|
||||||
|
echo "Press <enter>..."
|
||||||
|
read R
|
41
bin/zip.py
Executable file
|
@ -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()
|
||||||
|
# ------------------------------------------------------------------------------
|
150
doc/appy.css
Executable file
|
@ -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;
|
||||||
|
}
|
BIN
doc/artwork.odg
Executable file
9
doc/docToc.html
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<header>
|
||||||
|
<title><b>Appy documentation</b> - Table of contents</title>
|
||||||
|
<link rel="stylesheet" href="appy.css" type="text/css">
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
{{ doc }}
|
||||||
|
</body>
|
||||||
|
</html>
|
166
doc/gen.html
Executable file
|
@ -0,0 +1,166 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>An introduction to <b>gen</b></title>
|
||||||
|
<link rel="stylesheet" href="appy.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>What is gen ?</h1>
|
||||||
|
|
||||||
|
<p><b>gen</b> is a code <b>gen</b>erator that allows you write web applications without having to face and understand the plumbery of a given web framework. <b>gen</b> protects you. Concentrate on functionalities that need to be implemented: <b>gen</b> 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.</p>
|
||||||
|
|
||||||
|
<h1>OK, but concretely, on what technologies is <b>gen</b> built upon?</h1>
|
||||||
|
|
||||||
|
<p><b>gen</b> generates code that will run on <a href="http://plone.org" target="_blank">Plone 2.5</a>. 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 <a href="http://www.upfrontsystems.co.za/Members/roche/where-im-calling-from/meeting-plone-3" target="_blank">this</a>, 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.</p>
|
||||||
|
|
||||||
|
<h1>Before starting, let's get bored by some (counter-)principles that underlie gen</h1>
|
||||||
|
|
||||||
|
<p>If you have strict deadlines, skip this.</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>The code-centric approach</b>. 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 <a href="http://c2.com/cgi/wiki?DontRepeatYourself" target="_blank">DRY principle</a>, which is closely related to our <a href="index.html">null-IT principle</a>. On the contrary, <b>gen</b> 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:</li>
|
||||||
|
<ul>
|
||||||
|
<li>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.</li>
|
||||||
|
<li>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;</li>
|
||||||
|
<li>factoring similar groups of attributes or doing other intelligent treatment on a graphical model is not possible;</li>
|
||||||
|
<li>there is no need to write and maintain a model parser (like XMI);</li>
|
||||||
|
<li>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;</li>
|
||||||
|
</ul>
|
||||||
|
<li><b>Violating the model-view-controller pattern (and a lot of other patterns, too)</b>. 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.
|
||||||
|
</li>
|
||||||
|
<li><b>All-in-one objects</b>. 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.
|
||||||
|
</li>
|
||||||
|
<li><b>Building complex and shared web applications</b>. 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 <i>complex</i> and <i>shared</i> web applications.
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
<li>By <b>"complex"</b>, 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 <a href="pod.html">appy.pod</a>, any gen-powered app is by default ready for document generation. gen also generates paramerized through-the-web views and queries.
|
||||||
|
</li>
|
||||||
|
<li>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 <b>share</b> 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 <a href="http://plonegov.org" target="_blank">PloneGov community</a>, 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 <a href="http://www.fundp.ac.be/en/precise/">PRECISE</a> research center and the <a href="http://moves.vub.ac.be/">MoVES</a> project) exploring Software Product Lines Engineering and inventing new ways to manage variability among software systems.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h1>Getting started with gen</h1>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h2>Note for Windows users</h2>
|
||||||
|
|
||||||
|
<p>I've never installed or tested gen on Windows. Feedback is welcome!</p>
|
||||||
|
|
||||||
|
<h2>First step: download and install Plone</h2>
|
||||||
|
|
||||||
|
<p>You need to get Plone 2.5.5 installed on your system. The simplest way to do it is run the unified installer available <a href="http://plone.org/products/plone/releases/2.5.5" target="_blank">here</a>. 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).</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>Create a new Zope instance by typing <span class="code">/opt/Plone-2.5.5/Python-2.4.4/bin/python /opt/Plone-2.5.5/bin/mkzopeinstance.py</span>. 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 <span class="code">[myZopeInstance]</span>. 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:
|
||||||
|
<table class="appyTable">
|
||||||
|
<tr>
|
||||||
|
<td class="code">bin</td>
|
||||||
|
<td>contains the script for starting/stopping the Zope instance. Go to this folder and type <span class="code">./zopectl fg</span>. 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 <span class="code">./zopectl start</span> and <span class="code">./zopectl stop</span>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">etc</td>
|
||||||
|
<td>contains <span class="code">zope.conf</span>, 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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Extensions</td>
|
||||||
|
<td>I don't care about this.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">import</td>
|
||||||
|
<td>I don't care about this.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">lib</td>
|
||||||
|
<td>, and more specifically <span class="code">lib/python</span>, 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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">log</td>
|
||||||
|
<td>contains the log files of your instance: <span class="code">Z2.log</span> is the web server log (every HTTP request dumps a line into it); <span class="code">event.log</span> 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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Products</td>
|
||||||
|
<td>is 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 <span class="code">lib/python</span>, gen will create and maintain for you the corresponding Zope Product in <span class="code">Products</span>. There is a 1-1 relationship between a gen-application and the generated Zope product.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">var</td>
|
||||||
|
<td>is 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 <span class="code">Data.fs</span>. The DB technology used by Zope is named ZODB (which stands for Zope Object DataBase).</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>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 <span class="code">Products</span> 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 <span class="code">[myZopeInstance]/Products</span> and type <span class="code">cp -R /opt/Plone-2.5.5/zeocluster/Products/* .</span></p>
|
||||||
|
|
||||||
|
<p>Your Zope instance is now ready-to-use. Start it and go to <span class="code">http://localhost:8080/manage</span> 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.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/zmi.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">[myZopeInstance]/log/event.log</span>.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">http://localhost:8080/Appy</span> and you will see the main page of your Plone site, like shown below.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/plone.png" align="center"/></p>
|
||||||
|
|
||||||
|
<h2>Second step: download and install Appy</h2>
|
||||||
|
|
||||||
|
<p>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) <a href="version.html">here</a>. For example, you may unzip it in <span class="code">[myZopeInstance]/lib/python</span> or in <span class="code">/opt/Plone-2.5.5/Python-2.4.4/lib/python2.4/site-packages</span>. 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).</p>
|
||||||
|
|
||||||
|
<h2>Third step: develop the "Hello world" application</h2>
|
||||||
|
|
||||||
|
<p>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 <span class="code">ZopeComponent.py</span>:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
01 <b>from</b> appy.gen <b>import</b> *<br/>
|
||||||
|
02 <br/>
|
||||||
|
03 <b>class</b> ZopeComponent:<br/>
|
||||||
|
04 root = True<br/>
|
||||||
|
05 description = String()<br/>
|
||||||
|
06 technicalDescription = String(format=String.XHTML)<br/>
|
||||||
|
07 status = String(validator=['underDevelopement', 'stillSomeWorkToPerform',<br/>
|
||||||
|
08 'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient'])<br/>
|
||||||
|
09 funeralDate = Date()<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">root</span> makes it a class of special importance to gen; it will be treated with some honors. We will see how in a moment.</p>
|
||||||
|
|
||||||
|
<h2>Fourth step: generate the Zope/Plone product</h2>
|
||||||
|
|
||||||
|
<p>Please put <span class="code">ZopeComponent.py</span> in <span class="code">[myZopeInstance]/lib/python</span>, cd into this folder and type the following line.</p>
|
||||||
|
|
||||||
|
<p class="code">python [whereYouInstalledAppy]/appy/gen/generator.py ZopeComponent.py plone25 ../../Products</p>
|
||||||
|
|
||||||
|
<p>You will get an output similar to this:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
Generating product in /home/gde/ZopeInstance2/Products...<br/>
|
||||||
|
Generating class ZopeComponent.ZopeComponent...<br/>
|
||||||
|
Done.<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>If you are familiar with Zope/Plone products, you may take a look to the one gen has generated in <span class="code">[myZopeInstance]/Products/ZopeComponent</span>.</p>
|
||||||
|
|
||||||
|
<h2>Fifth step: admire the result</h2>
|
||||||
|
|
||||||
|
<p>Restart your Zope instance, go to <span class="code">http://localhost:8080/Appy</span>, 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 <span class="code">ZopeComponent</span>: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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/emptyQuery.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/zopeComponentEdit.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/filledQuery.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/tool.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/portlet.png" align="center"/></p>
|
||||||
|
|
||||||
|
<p>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 <a href="pod.html">appy.pod</a> 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.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
1098
doc/genCreatingAdvancedClasses.html
Executable file
644
doc/genCreatingBasicClasses.html
Executable file
|
@ -0,0 +1,644 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><b>gen</b> - Creating basic classes</title>
|
||||||
|
<link rel="stylesheet" href="appy.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><a name="genApplications"></a>Gen-applications</h1>
|
||||||
|
|
||||||
|
<p>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 <a href="gen.html">main gen presentation page</a>, 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 <a class="code">[myZopeInstance]/lib/python</a> you simply create a folder "MyPackage" at the same place.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">appy.gen.Type</span>. 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.</p>
|
||||||
|
|
||||||
|
<p>What gen tries to do is to be as invisible as possible, by leaving your Python classes as "pure" as possible.</p>
|
||||||
|
|
||||||
|
<h1><a name="classesAndAttributes"></a>Gen-classes and attributes</h1>
|
||||||
|
|
||||||
|
<p>The code below shows an example of a gen-class.</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>from</b> appy.gen <b>import</b> *<br/>
|
||||||
|
<b>class</b> A:<br/>
|
||||||
|
at1 = String()<br/>
|
||||||
|
at2 = Integer()<br/>
|
||||||
|
at3 = Float()<br/>
|
||||||
|
at4 = Date()<br/>
|
||||||
|
at5 = 'Hello'<br/>
|
||||||
|
<b>def</b> sayHello(self):<br/>
|
||||||
|
<b>print</b> self.at5 + str(self.at2)<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><span class="code">String</span>, <span class="code">Integer</span>, <span class="code">Float</span> and <span class="code">Date</span> all inherit from <span class="code">appy.gen.Type</span>. You may still define standard Python attributes like <span class="code">at5</span> and methods like <span class="code">sayHello</span>. The list of basic types provided by gen is shown below.</p>
|
||||||
|
|
||||||
|
<table class="appyTable">
|
||||||
|
<tr>
|
||||||
|
<td class="code">Integer </td>
|
||||||
|
<td>Holds an integer value.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Float </td>
|
||||||
|
<td>Holds a floating-point number.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">String </td>
|
||||||
|
<td>Holds a string value (entered by the user, selected from a drop-down list, etc).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Boolean </td>
|
||||||
|
<td>Holds a boolean value (typically rendered as a checkbox).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Date </td>
|
||||||
|
<td>Holds a date (with or without hours and minutes).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">File </td>
|
||||||
|
<td>Holds a binary file.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Ref </td>
|
||||||
|
<td>Holds references to one or more other objects.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Computed </td>
|
||||||
|
<td>Holds nothing; the field value is computed from a specified Python method.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Action </td>
|
||||||
|
<td>Holds nothing; the field represents a button or icon that triggers a function specified as a Python method.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
<table class="appyTable">
|
||||||
|
<tr>
|
||||||
|
<th>parameter</th>
|
||||||
|
<th>default value</th>
|
||||||
|
<th>explanation</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">validator</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>A validator is something that restricts the valid range of values that the field may hold. It can be:
|
||||||
|
<ul>
|
||||||
|
<li>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 <span class="code">basestring</span>), 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 <span class="code">[full_class_name]_[field_name]_valid</span> in the application-specific i18n domain, where <span class="code">[full_class_name]</span> is the class name including the package prefix where dots have been replaced with underscores. More information about i18n may be found <a href="genCreatingAdvancedClasses.html#i18n">here</a>. Examples are presented hereafter.</li>
|
||||||
|
<li>A list of string values. It only applies for <span class="code">String</span> fields. This list contains the possible values the field may hold.</li>
|
||||||
|
<li>A regular expression (under the form of a compiled regular expression generated with <span class="code">re.compile</span>).</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">multiplicity</td>
|
||||||
|
<td class="code">(0,1)</td>
|
||||||
|
<td>Multiplicity is a 2-tuple that represents the minimum and maximum number of values the field may hold. <span class="code">(0,1)</span> means that the field may be empty or contain one value. <span class="code">(1,1)</span> means that a value is required. For all types excepted <span class="code">Ref</span> and some <span class="code">String</span>s, the maximum value must be 1. The Python value <span class="code">None</span> is used for representing an unbounded value: <span class="code">(1,None)</span> means "at least 1" (no upper bound).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">default</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>The 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:
|
||||||
|
<table class="appyTable" align="center">
|
||||||
|
<tr>
|
||||||
|
<th>Appy type</th>
|
||||||
|
<th>Python type</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Integer</td>
|
||||||
|
<td class="code">int, long</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Float</td>
|
||||||
|
<td class="code">float</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">String</td>
|
||||||
|
<td class="code">str, unicode</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Boolean</td>
|
||||||
|
<td class="code">bool</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Date</td>
|
||||||
|
<td>The Date type shipped with Zope (class <span class="code">DateTime.DateTime</span>). The corresponding type from standard <span class="code">datetime</span> package is less sexy.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">optional</td>
|
||||||
|
<td class="code">False</td>
|
||||||
|
<td>When you define a field as <span class="code">optional</span>, 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 <i>variability</i>: 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 <a href="genCreatingAdvancedClasses.html#optional">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">editDefault</td>
|
||||||
|
<td class="code">False</td>
|
||||||
|
<td>When <span class="code">editDefault = True</span>, 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 <span class="code">default</span>, using <span class="code">editDefault</span> allows to do it through-the-web, flavour by flavour. More information about this parameter may be found <a href="genCreatingAdvancedClasses.html#editDefault">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">show</td>
|
||||||
|
<td class="code">True</td>
|
||||||
|
<td>You may specify here a special condition under which the field will be visible or not. This condition may be:
|
||||||
|
<ul>
|
||||||
|
<li>a simple boolean value;</li>
|
||||||
|
<li>a Python (instance) method that returns True, False (one any other equivalent value). This method does not take any argument.</li>
|
||||||
|
</ul>
|
||||||
|
Note that visibility of a given field does not only depend on this parameter. It also depends on its optionality (see parameter <span class="code">optional</span>) and on the user having or not the permission to view and/or edit the field (see parameters <span class="code">specificReadPermission</span> and <span class="code">specificWritePermission</span> below).
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">page</td>
|
||||||
|
<td class="code">main</td>
|
||||||
|
<td>By 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 <span class="code">[full_class_name]</span> in i18n domain <span class="code">plone</span>, where <span class="code">[full_class_name]</span> 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 <span class="code">[full_class_name]_page_[page_name]</span> again in i18n domain <span class="code">plone</span>. More information about pages may be found <a href="genCreatingAdvancedClasses.html#pagesAndGroups">here</a>; more information about i18n may be found <a href="genCreatingAdvancedClasses.html#i18n">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">group</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>Within 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 <span class="code">[full_class_name]_group_[group_name]</span> in i18n domain <span class="code">plone</span>, where <span class="code">[full_class_name]</span> is the class name including the package prefix where dots have been replaced with underscores. More information about i18n may be found <a href="genCreatingAdvancedClasses.html#i18n">here</a>. If you add something like <span class="code">_[number]</span> to the group name, widgets from the group will be rendered into columns; <span class="code">number</span> being the number of columns in the group. For example, if you define several fields with parameter <span class="code">group="groupA_3"</span>, field values will be put in group <span class="code">groupA</span> 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 <a href="genCreatingAdvancedClasses.html#pagesAndGroups">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">move</td>
|
||||||
|
<td class="code">0</td>
|
||||||
|
<td>Normally, 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 <span class="code">move</span>. For example, specifing <span class="code">move=-2</span> 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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">searchable</td>
|
||||||
|
<td class="code">False</td>
|
||||||
|
<td>When defining a field as <span class="code">searchable</span>, 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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">specificReadPermission</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>By 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 <span class="code">True</span>. More information about security may be found <a href="genSecurityAndWorkflows.html">here</a>; specific details about usage of this field may be found <a href="genSecurityAndWorkflows.html#specificFieldPermissions">here</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">specificWritePermission</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>By 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 <span class="code">True</span>. More information about security may be found <a href="genSecurityAndWorkflows.html">here</a>; specific details about usage of this field may be found <a href="genSecurityAndWorkflows.html#specificFieldPermissions">here</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">width</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>An integer value that represents the width of a widget. For the moment, it is only used for <span class="code">String</span>s whose format is <span class="code">String.LINE</span>. For those Strings, default value is 50.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">master</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>Another 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 <a href="genCreatingAdvancedClasses.html#mastersAndSlaves">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">masterValue</td>
|
||||||
|
<td class="code">None</td>
|
||||||
|
<td>If a <span class="code">master</span> 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 <a href="genCreatingAdvancedClasses.html#mastersAndSlaves">here</a>.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h1><a name="integersAndFloats"></a>Integers and Floats</h1>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p class="code"><b>from</b> appy.gen <b>import</b> *<br/>
|
||||||
|
<b>class</b> Zzz:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
<b>def</b> show_f1(self): <b>return</b> True<br/>
|
||||||
|
<b>def</b> validate_i2(self, value):<br/>
|
||||||
|
<b>if</b> (value != None) <b>and</b> (value < 10):<br/>
|
||||||
|
<b>return</b> 'Value must be higher or equal to 10.'<br/>
|
||||||
|
<b>return</b> True<br/>
|
||||||
|
i1 = Integer(show=False)<br/>
|
||||||
|
i2 = Integer(validator = validate_i2)<br/>
|
||||||
|
f1 = Float(show=show_f1, page='other')<br/>
|
||||||
|
f2 = Float(multiplicity=(1,1))<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Recall from <a href="gen.html">the introduction</a> that a class declared as <span class="code">root</span> 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.</p>
|
||||||
|
|
||||||
|
<p>Because <span class="code">i1</span> is defined with <span class="code">show=False</span>, it will never appear on Appy views. <span class="code">i2</span> 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)), <span class="code">validate_i2</span> will still be called even if <span class="code">value</span> is <span class="code">None</span>. <span class="code">f1</span> illustrates how to define a Python method for the <span class="code">show</span> parameter; because this is a silly method that always return True, <span class="code">f1</span> will always be displayed. <span class="code">f1</span> will be displayed on another page named <span class="code">other</span>. <span class="code">f2</span> will be mandatory. So when creating an instance of <span class="code">Zzz</span> through-the-web, you get the following screen:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/integersFloats1.png"></p>
|
||||||
|
|
||||||
|
<p>Plone needs a field named <span class="code">title</span>; because you did not had any field named <span class="code">title</span> 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 <span class="code">title=String(multiplicity=(0,1), show=False)</span>. 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 <span class="code">i2</span> and nothing in <span class="code">f2</span>:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/integersFloats2.png"></p>
|
||||||
|
|
||||||
|
<p>If we enter a value lower than 10 in i2 we get our specific error message:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/integersFloats3.png"></p>
|
||||||
|
|
||||||
|
<p>After corrections have been made, clicking on "next" will bring us to the second page where <span class="code">f1</span> lies.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/integersFloats4.png"></p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>Beyond validation of specific fields, gen also allows to perform global, "inter-field" validation. More information <a href="genCreatingAdvancedClasses.html#specialMethods">here</a>.</p>
|
||||||
|
|
||||||
|
<h1><a name="strings"></a>Strings</h1>
|
||||||
|
|
||||||
|
<p>Strings have an additional attribute named <span class="code">format</span> which may take the following values:</p>
|
||||||
|
|
||||||
|
<table class="appyTable">
|
||||||
|
<tr>
|
||||||
|
<th>value</th>
|
||||||
|
<th>default?</th>
|
||||||
|
<th>example</th>
|
||||||
|
<th>result (edit view)</th>
|
||||||
|
<th>result (consult view)</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">String.LINE</td>
|
||||||
|
<td>yes</td>
|
||||||
|
<td class="code">oneLineString = String()</td>
|
||||||
|
<td><img src="img/strings1.png"></td>
|
||||||
|
<td><img src="img/strings2.png"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">String.TEXT</td>
|
||||||
|
<td>no</td>
|
||||||
|
<td class="code">textString = String(format=String.TEXT)</td>
|
||||||
|
<td><img src="img/strings3.png"></td>
|
||||||
|
<td><img src="img/strings4.png"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">String.XHTML</td>
|
||||||
|
<td>no</td>
|
||||||
|
<td class="code">textXhtml = String(format=String.XHTML)</td>
|
||||||
|
<td><img src="img/strings5.png"></td>
|
||||||
|
<td><img src="img/strings6.png"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>With <span class="code">String</span>s, adequate use of arguments <span class="code">validator</span> and <span class="code">multiplicity</span> may produce more widgets and/or behaviours. Consider the following class:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> SeveralStrings:<br/>
|
||||||
|
root=True<br/>
|
||||||
|
anEmail = String(validator=String.EMAIL)<br/>
|
||||||
|
anUrl = String(validator=String.URL)<br/>
|
||||||
|
anAlphanumericValue = String(validator=String.ALPHANUMERIC)<br/>
|
||||||
|
aSingleSelectedValue = String(validator=['valueA', 'valueB', 'valueC'])<br/>
|
||||||
|
aSingleMandatorySelectedValue = String(<br/>
|
||||||
|
validator=['valueX', 'valueY', 'valueZ'], multiplicity=(1,1))<br/>
|
||||||
|
aMultipleSelectedValue = String(<br/>
|
||||||
|
validator=['valueS', 'valueT', 'valueU', 'valueV'],<br/>
|
||||||
|
multiplicity=(1,None), searchable=True)<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>The edit view generated from this class looks like this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/strings7.png"></p>
|
||||||
|
|
||||||
|
<p>For field <span class="code">anEmail</span>, a valid email was entered so the validation machinery does not complain. For <span class="code">anUrl</span> and <span class="code">anAlphanumericValue</span> (the 2 other predefined regular expressions for validating String fields shipped with gen) an error message is generated. The field <span class="code">aSingleSelectedValue</span> 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 <span class="code">aSingleMandatorySelectedValue</span> is mandatory because of the <span class="code">multiplicity</span> parameter being <span class="code">(1,1)</span>; a validation error is generated because no value was selected. The field <span class="code">aMultipleSelectedValue</span> does not limit the maximum number of chosen values (<span class="code">multiplicity=(1,None)</span>): the widget is a listbox where several values may be selected.</p>
|
||||||
|
|
||||||
|
<p>Field <span class="code">aMultipleSelectedValue</span> has also been specified as <span class="code">searchable</span>. Suppose we have created this instance of <span class="code">SeveralStrings</span>:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/strings8.png"></p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/strings9.png"></p>
|
||||||
|
|
||||||
|
<p>Entering "Value x" for example will produce no match at all because <span class="code">aSingleMandatorySelectedValue</span> was not specified as <span class="code">searchable</span>. Note that <span class="code">title</span> fields are automatically set as <span class="code">searchable</span>.</p>
|
||||||
|
|
||||||
|
<h1><a name="booleans"></a>Booleans</h1>
|
||||||
|
|
||||||
|
<p>Booleans have no additional parameters. Specifying <span class="code">aBooleanValue = Boolean(default=True)</span> will produce this on the edit view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/booleans1.png"></p>
|
||||||
|
|
||||||
|
<h1><a name="dates"></a>Dates</h1>
|
||||||
|
|
||||||
|
<p>Dates have an additional attribute named <span class="code">format</span> which may take the following values:</p>
|
||||||
|
|
||||||
|
<table class="appyTable">
|
||||||
|
<tr>
|
||||||
|
<th>value</th>
|
||||||
|
<th>default?</th>
|
||||||
|
<th>example</th>
|
||||||
|
<th>result (edit view)</th>
|
||||||
|
<th>result (consult view)</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Date.WITH_HOUR</td>
|
||||||
|
<td>yes</td>
|
||||||
|
<td class="code">dateWithHour = Date()</td>
|
||||||
|
<td><img src="img/dates1.png"></td>
|
||||||
|
<td><img src="img/dates2.png"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">Date.WITHOUT_HOUR</td>
|
||||||
|
<td>no</td>
|
||||||
|
<td class="code">dateWithoutHour = Date(format=Date.WITHOUT_HOUR)</td>
|
||||||
|
<td><img src="img/dates3.png"></td>
|
||||||
|
<td><img src="img/dates4.png"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>When editing a <span class="code">Date</span> 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 <span class="code">Date</span> chooser written in Javascript will pop up as shown above.</p>
|
||||||
|
|
||||||
|
<h1><a name="files"></a>Files</h1>
|
||||||
|
|
||||||
|
<p>When specifying this: <span class="code">anAttachedFile = File()</span> you get this result on the edit view when an object is being created:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/files1.png"></p>
|
||||||
|
|
||||||
|
<p>("Parcourir" means "Browse" in french). You get this result on the consult view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/files2.png"></p>
|
||||||
|
|
||||||
|
<p>If you want to edit the corresponding object, you will get this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/files3.png"></p>
|
||||||
|
|
||||||
|
<p>Any kind of file may be uploaded in <span class="code">File</span> fields, but for png, jpg and gif files, you may specify an additional parameter <span class="code">isImage=True</span> and your gen-ified Plone will render the image. Let's define this field:</p>
|
||||||
|
|
||||||
|
<p class="code">anAttachedImage = File(isImage=True)</p>
|
||||||
|
|
||||||
|
<p>The consult view will render the image like on this example:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/files4.png"></p>
|
||||||
|
|
||||||
|
<p>On the edit view, the widget that allows to modify the image will look like this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/files5.png"></p>
|
||||||
|
|
||||||
|
<h1><a name="references"></a>References</h1>
|
||||||
|
|
||||||
|
<p>References allow to specify associations between classes in order to build webs of interrelated objects. Suppose you want to implement this association:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs1.png"></p>
|
||||||
|
|
||||||
|
<p>The corresponding gen model looks like this:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Order:<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
<br/>
|
||||||
|
<b>class</b> Client:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
title = String(show=False)<br/>
|
||||||
|
firstName = String()<br/>
|
||||||
|
name = String()<br/>
|
||||||
|
orders = Ref(Order, add=True, link=False, multiplicity=(0,None),<br/>
|
||||||
|
back=Ref(attribute='client'))<br/>
|
||||||
|
<b>def</b> onEdit(self, created):<br/>
|
||||||
|
self.title = self.firstName + ' ' + self.name<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Such an association is expressed in gen by 2 "crossed" <span class="code">Ref</span> instances (see definition of attribute <span class="code">orders</span>):
|
||||||
|
<ul>
|
||||||
|
<li>the attribute named <span class="code">orders</span> specifies the "main" or "forward" reference;</li>
|
||||||
|
<li>the attribute named <span class="code">client</span> specifies the "secondary" or "backward" reference.</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">orders</span> allows to create and add new <span class="code">Order</span> instances (<span class="code">add=True</span>). In the generated product, once you have created a <span class="code">Client</span> instance, for field <span class="code">orders</span> you will get the following consult view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs2.png"></p>
|
||||||
|
|
||||||
|
<p>Remember that you can't get rid of the <span class="code">title</span> field. So one elegant solution is to specify it as invisible <span class="code">(show=False)</span> and compute it from other fields every time an object is created or updated (special method <span class="code">onEdit</span>: when an object was just created, parameter <span class="code">created</span> is <span class="code">True</span>; when an object was just modified, <span class="code">created</span> is <span class="code">False</span>). Here, in both cases, we update the value of field <span class="code">title</span> with <span class="code">firstName + ' ' + name</span>.</p>
|
||||||
|
|
||||||
|
<p>On this view, because you specified <span class="code">add=True</span> for field <span class="code">orders</span>, the corresponding widget displays a "plus" icon for creating new <span class="code">Order</span> instances and link them to you this client. Clicking on it will bring you to the edit view for <span class="code">Order</span>:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs3.png"></p>
|
||||||
|
|
||||||
|
<p>Saving the order brings you to the consult view for this order:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs4.png"></p>
|
||||||
|
|
||||||
|
<p>On this view, a specific widget was added (it corresponds to backward reference <span class="code">client</span>) 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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs5.png"></p>
|
||||||
|
|
||||||
|
<p>If you had specified <span class="code">multiplicity=(0,4)</span> for field <span class="code">orders</span>, the "plus" icon would have disappeared, preventing you from creating an invalid fifth order. Unlike standard Plone references, gen <span class="code">Ref</span> fields are <b>ordered</b>; the arrows allow you to move them up or down. The other icons allow you to edit and delete them.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">Product</span>: an order may now specify one or several products. The model would include the new <span class="code">Product</span> class and the <span class="code">Order</span> class would get an additional <span class="code">Ref</span> field:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Product:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
<br/>
|
||||||
|
<b>class</b> Order:<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
products = Ref(Product, add=False, link=True, multiplicity=(1,None),<br/>
|
||||||
|
back=Ref(attribute='orders'))<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>So now you are able to create products. Because you specified class <span class="code">Product</span> with <span class="code">root=True</span>, 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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs6.png"></p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs7.png"></p>
|
||||||
|
|
||||||
|
<p>Clicking on "save" will bring you back to the consult view for this order:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs8.png"></p>
|
||||||
|
|
||||||
|
<p>What is a bit annoying for the moment is that the <span class="code">Ref</span> widget configured with <span class="code">add=True</span> is rendered only on the consult view, while the <span class="code">Ref</span> widget configured with <span class="code">link=True</span> 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 <span class="code">add=True</span> and <span class="code">link=True</span> (this is not possible right now).</p>
|
||||||
|
|
||||||
|
<p>You will also notice that when defining an association, both <span class="code">Ref</span> instances are defined in one place (=at the forward reference, like in <span class="code">products = Ref(Product, add=False, link=True, multiplicity=(1,None),back=Ref(attribute='orders'))</span>). 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 <span class="code">attribute</span> parameter of the backward <span class="code">Ref</span> instance. For example, from a Product instance <span class="code">p</span> you may get all orders related to it by typing <span class="code">p.orders</span>. The consult view uses this feature and displays it:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs9.png"></p>
|
||||||
|
|
||||||
|
<p>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: <span class="code">back=Ref(attribute='orders', show=False)</span></p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>For references, 2 more parameters allow to customize the way they are rendered: the boolean <span class="code">showHeaders</span> and <span class="code">shownInfo</span>. 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 <span class="code">orders</span> this way:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
orders = Ref(Order, add=True, link=False, multiplicity=(0,None),<br/>
|
||||||
|
back=Ref(attribute='client'), showHeaders=True,<br/>
|
||||||
|
shownInfo=('description',))<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><span class="code">showHeaders</span> simply tells gen to display or not headers for the table; <span class="code">shownInfo</span> specifies (in a list or tuple) names of fields to display for the referenced objects. By default, field <span class="code">title</span> is always displayed; you don't have to specify it in <span class="code">shownInfo</span>. Here's the result:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs10.png"></p>
|
||||||
|
|
||||||
|
<p>The <span class="code">shownInfo</span> parameter may also be used with <span class="code">Ref</span>s specifying <span class="code">link=True</span>. For <span class="code">Ref</span> field <span class="code">products</span> of class <span class="code">Order</span>, specifying <span class="code">shownInfo=('description',)</span> will produce this, when creating a new <span class="code">Order</span>:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs10b.png"></p>
|
||||||
|
|
||||||
|
<p>The title/name of the referred object always appears; here, the <span class="code">description</span> also appears. If you want the <span class="code">title</span> to appear at a different place, simply specify it in the <span class="code">shownInfo</span> parameter. For example, specifying <span class="code">shownInfo=('description','title')</span> will produce:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs10c.png"></p>
|
||||||
|
|
||||||
|
<p> By adding parameter <span class="code">wide=True</span>, <span class="code">Ref</span> tables take all available space. Returning to the previous example, specifying this parameter will produce this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs11.png"></p>
|
||||||
|
|
||||||
|
<p>When using <span class="code">shownInfo</span>, you may specify any field name, including <span class="code">Ref</span> fields. If you specify <span class="code">shownInfo=('description', 'products')</span> for the field <span class="code">orders</span> of class <span class="code">Client</span> and modify rendering of field <span class="code">products</span> from class <span class="code">Order</span> this way:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Order:<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
products = Ref(Product, add=False, link=True, multiplicity=(1,None),<br/>
|
||||||
|
back=Ref(attribute='orders'), showHeaders=True,<br/>
|
||||||
|
shownInfo=('description',))
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>You will get this result:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs12.png"></p>
|
||||||
|
|
||||||
|
<p>If, for field <span class="code">products</span>, <span class="code">add=True</span> was specified, on this screen you would have been able to add directly new products to orders through specific "plus" icons:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs13.png"></p>
|
||||||
|
|
||||||
|
<p>Let's consider again attribute <span class="code">products</span> of class <span class="code">Order</span> (with <span class="code">add=False</span> and <span class="code">link=True</span>). When specifying this, gen allows every <span class="code">Order</span> to be associated with any <span class="code">Product</span> defined in the whole Plone site (in this case, <span class="code">Product A</span>, <span class="code">Product B</span> and <span class="code">Product C</span>):</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs14.png"></p>
|
||||||
|
|
||||||
|
<p>You may want to filter only some products instead of gathering all defined products. The <span class="code">select</span> parameter may be used for this. Here is an example:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Order:<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
<b>def</b> filterProducts(self, allProducts):<br/>
|
||||||
|
return [f <b>for</b> f <b>in</b> allProducts <b>if</b> f.description.find('Descr') != -1]<br/>
|
||||||
|
products = Ref(Product, add=False, link=True, multiplicity=(1,None),<br/>
|
||||||
|
back=Ref(attribute='orders'), showHeaders=True,<br/>
|
||||||
|
shownInfo=('description',), select=filterProducts)<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>This silly example only selects products whose description contains the word "Descr", which is only the case for Products <span class="code">Product B</span> and <span class="code">Product C</span>. So the "Products" widget will not contain <span class="code">Product A</span> anymore:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/refs15.png"></p>
|
||||||
|
|
||||||
|
<p>The use of the <span class="code">select</span> attribute may cause performance problems for large numbers of objects; an alternative attribute may appear in the future.</p>
|
||||||
|
|
||||||
|
<h1><a name="computed"></a>Computed fields</h1>
|
||||||
|
|
||||||
|
<p>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 <span class="code">Computed</span> field. Computed fields have two main purposes:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>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);</li>
|
||||||
|
<li>producing nice representations of a field (for example, the computation may produce a graphical representation of the field value).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>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)</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">number</span> that will hold the order number and will be invisible. Then, we will define a <span class="code">Computed</span> field named <span class="code">reference</span> that will produce the reference based on some prefix and the order number. Class <span class="code">Order</span> need to be updated like this:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Order:<br/>
|
||||||
|
...<br/>
|
||||||
|
number = Float(show=False)<br/>
|
||||||
|
<i># Reference field</i><br/>
|
||||||
|
<b>def</b> getReference(self): <b>return</b> 'OR-%f' % self.number<br/>
|
||||||
|
reference = Computed(method=getReference)<br/>
|
||||||
|
...<br/>
|
||||||
|
<b>def</b> onEdit(self, created):<br/>
|
||||||
|
<b>if</b> created:<br/>
|
||||||
|
<b>import</b> random<br/>
|
||||||
|
self.number = random.random()<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Method <span class="code">onEdit</span> is used to generate the order number when the order is created. The <span class="code">reference</span> field is a <span class="code">Computed</span> field: parameter <span class="code">method</span> specifies the Python method that will compute the field value. In this case, this value is simply the order <span class="code">number</span> with some prefix. Now, let's create this order and see what happens:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/computed1.png"></p>
|
||||||
|
|
||||||
|
<p><span class="code">Computed</span> fields do not appear on edit views, only on consult views. Clicking on "Save" will bring you to the following consult view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/computed2.png"></p>
|
||||||
|
|
||||||
|
<p>Like any other field, <span class="code">Computed</span> fields may appear on <span class="code">Ref</span> fields or on dashboards. For example, if we change the definition of <span class="code">Ref</span> field <span class="code">orders</span> on class <span class="code">Client</span> this way:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Client:<br/>
|
||||||
|
...<br/>
|
||||||
|
orders = Ref(Order, add=True, link=False, multiplicity=(0,None),<br/>
|
||||||
|
back=Ref(attribute='client'), showHeaders=True,<br/>
|
||||||
|
shownInfo=('reference', 'description', 'products'), wide=True)<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>order references will appear on the corresponding consult view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/computed3.png"></p>
|
||||||
|
|
||||||
|
<p>Python methods specified in attribute <span class="code">with</span> may return HTML code.</p>
|
||||||
|
|
||||||
|
<h1><a name="actions"></a>Actions</h1>
|
||||||
|
|
||||||
|
<p>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 <span class="code">Product</span> this way:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Product:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
description = String(format=String.TEXT)<br/>
|
||||||
|
stock = Integer()<br/>
|
||||||
|
<b>def</b> needOrder(self): <b>return</b> self.stock < 3<br/>
|
||||||
|
<b>def</b> orderProduct(self): self.stock = 3<br/>
|
||||||
|
order = Action(action=orderProduct, show=needOrder)<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Firstly, we have added attribute <span class="code">stock</span>, that allows us to know how many <span class="code">Product</span> items we have in stock. Then, we have added a dummy action named <span class="code">order</span> that allows us to re-order a product when the stock is too low. Suppose we have defined this product:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/actions1.png"></p>
|
||||||
|
|
||||||
|
<p>Because the stock is lower than 3, the <span class="code">order</span> action (every action is defined as an instance of <span class="code">appy.gen.Action</span>) is visible (because of parameter <span class="code">show</span> of action <span class="code">order</span>). The triggered behaviour is specified by a Python method given in parameter <span class="code">action</span>. In this silly example, the action as the direct effect of setting stock to value 3. Clicking on button "order" will have this effect:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/actions2.png"></p>
|
||||||
|
|
||||||
|
<p><span class="code">stock</span> is equal to 3; the <span class="code">order</span> action is not visible anymore (because the method specified in parameter <span class="code">show</span> returns <span class="code">False</span>.</p>
|
||||||
|
|
||||||
|
<p>Considering actions as "fields" is quite different from other frameworks or standard Plone. This has several advantages:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>You may reuse the security machinery linked to fields;</li>
|
||||||
|
<li>You may render them where you want, on any page/group, on <span class="code">Ref</span> fields, or on dashboards.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Note that the <span class="code">action</span> parameter may hold a list/tuple of Python methods instead of a single method (like in the previous example).</p>
|
||||||
|
|
||||||
|
<p>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 <a href="genCreatingAdvancedClasses.html#i18n">section on i18n</a> first. In fact, for every action field, gen generates 2 i18n labels: <span class="code">[full_class_name]_[field_name]_action_ok</span> and <span class="code">[full_class_name]_[field_name]_action_ko</span>. 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 <span class="code">action</span> parameter:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>returns nothing (or <span class="code">None</span>) (it was the case in the example);<li>
|
||||||
|
<li>returns the boolean value <span class="code">True</span> or any Python equivalent.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>The action fails when the Python method returns <span class="code">False</span> or if it raises an exception. In this latter case, the exception message is part of the rendered message.</p>
|
||||||
|
|
||||||
|
<p>If the <span class="code">action</span> parameter specifies several Python methods, the action succeeds if all Python methods succeed.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">action</span> parameter may return a 2-tuple instead of <span class="code">None</span>, <span class="code">True</span> or <span class="code">False</span>. The first element of this tuple determines if the method succeeded or not (<span class="code">True</span> or <span class="code">False</span>); 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 <span class="code">translate</span> as described <a href="genCreatingAdvancedClasses.html#i18n">here</a> (near the end of the section). In the case of multiple Python methods, messages returned are concatenated.</p>
|
||||||
|
|
||||||
|
<p>The installation procedure for your gen-application is defined as an action. More information about this <a href="genCreatingAdvancedClasses.html#customToolAndFlavour">here</a>.</p>
|
||||||
|
|
||||||
|
<p>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 <i>parameters</i>.</p>
|
||||||
|
|
||||||
|
<h1><a name="objectStorage"></a>Some thoughts about how gen-controlled objects are stored</h1>
|
||||||
|
|
||||||
|
<p>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 <a href="genCreatingAdvancedClasses.html#customToolAndFlavour">here</a>) will be created within this folder.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/objectStorage1.png"></p>
|
||||||
|
|
||||||
|
<p>This screenshot shows the ZMI (Zope Management Interface) available at http://localhost:8080/manage. You see that within the <span class="code">Appy</span> object (which is a Plone site) you have, among some predefined Plone objects like <span class="code">MailHost</span> or <span class="code">Members</span>, 2 folders named <span class="code">ZopeComponent</span> and <span class="code">Zzz</span>. 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 <span class="code">folder=True</span> on your class. Suppose you do this on class <span class="code">Client</span>:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> Client:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
folder = True<br/>
|
||||||
|
title = String(show=False)<br/>
|
||||||
|
firstName = String()<br/>
|
||||||
|
...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>If now I start from a database with 3 products and 1 client and I add a new order I will get this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/objectStorage2.png"></p>
|
||||||
|
|
||||||
|
<p>You see in the "navigation" portlet on the left that "Gaetan Delannay" is now a folder that "contains" the order "First order".</p>
|
||||||
|
|
||||||
|
<p>Note that instances of classes tagged with <span class="code">root=True</span> will always be created in the root application folder.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
371
doc/genSecurityAndWorkflows.html
Executable file
|
@ -0,0 +1,371 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><b>gen</b> - Security and workflows</title>
|
||||||
|
<link rel="stylesheet" href="appy.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><a name="principles"></a>The principles</h1>
|
||||||
|
|
||||||
|
<p>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, <b>users</b> are put into <b>groups</b>; groups have some <b>roles</b>; roles are granted basic <b>permissions</b> on objects (create, read, write, delete, etc). Permissions-to-roles mappings may vary according to the <b>state</b> of objects.</p>
|
||||||
|
|
||||||
|
<h1><a name="noMorePrinciples"></a>Yes! We are done with the principles</h1>
|
||||||
|
|
||||||
|
<p>In this chapter, we will use the <span class="code">ZopeComponent</span> example, first introduced <a href="gen.html">here</a> and refined <a href="genCreatingAdvancedClasses.html">here</a>. 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.</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow1.png"></p>
|
||||||
|
|
||||||
|
<p>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):</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>User Name</th>
|
||||||
|
<th>Password</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>sydney</th>
|
||||||
|
<td>sydney</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ludovic</th>
|
||||||
|
<td>ludovic</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Now, we need groups. Guess what? We will not create groups. Why? Because gen will generate groups automatically for you!</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>role name</th>
|
||||||
|
<th>description</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">ZManager</td>
|
||||||
|
<td>Manager in our company that creates Zope components</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">ZLeader</td>
|
||||||
|
<td>Project leader in our company</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="code">ZDeveloper</td>
|
||||||
|
<td>Zope/Python developer</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>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 <span class="code">ZDeveloper</span>. 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:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>name</th>
|
||||||
|
<th>corresponding code object</th>
|
||||||
|
<th>description</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>create</td>
|
||||||
|
<td class="code">-</td>
|
||||||
|
<td>Permission to create an object</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>read</td>
|
||||||
|
<td class="code">appy.gen.r</td>
|
||||||
|
<td>Permission to access/view the content (=field values) of an object</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>write</td>
|
||||||
|
<td class="code">appy.gen.w</td>
|
||||||
|
<td>Permission to edit/modify the content (=field values) of an object</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>delete</td>
|
||||||
|
<td class="code">appy.gen.d</td>
|
||||||
|
<td>Permission to delete an object</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h1><a name="createPermission"></a>Managing the <span class="code">create</span> permission</h1>
|
||||||
|
|
||||||
|
<p>Permission to <span class="code">create</span> 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, <span class="code">ZLeader</span>s are responsible for creating Zope components. You declare this global list in attribute <span class="code">defaultCreators</span> of your <span class="code">appy.gen.Config</span> instance introduced while <a href="genCreatingAdvancedClasses.html#i18n">presenting i18n</a>:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
c = Config()<br/>
|
||||||
|
c.languages = ('en', 'fr')<br/>
|
||||||
|
c.defaultCreators += ['ZLeader']<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Why do I write <span class="code">+=</span> and not <span class="code">=</span> ? Because the <span class="code">defaultCreators</span> attribute is already initialised with this list of default Plone roles: <span class="code">['Manager', 'Owner']</span>. <span class="code">Manager</span> is the role granted to any Plone/Zope administrator (like the <span class="code">admin</span> user we have used in our examples so far); <span class="code">Owner</span> is a special role that is granted to the user that created a given object.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">creators</span>. For example, you may use this attribute on class ZopeComponent:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
class ZopeComponent:<br/>
|
||||||
|
...<br/>
|
||||||
|
creators = c.defaultCreators + ['ZLeader']<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>With this piece of code, <span class="code">Manager</span>s and <span class="code">ZLeader</span>s will be able to create <span class="code">ZopeComponent</span>s; only <span class="code">Manager</span>s will be able to create instances of other classes in your application (provided no specific <span class="code">creators</span> attribute is defined on them). Note that the <span class="code">creators</span> attribute infringes the classical rules of class inheritance: If you have non abstract classes <span class="code">A</span> and <span class="code">B(A)</span>, defining attribute <span class="code">creators</span> on <span class="code">A</span> will have absolutely no effect on <span class="code">B</span>.
|
||||||
|
|
||||||
|
<h1><a name="workflows"></a>Managing all other permissions: defining workflows</h1>
|
||||||
|
|
||||||
|
<p>For granting all other permissions (like read, write and delete, in short <span class="code">r, w, d</span>), we will not use the same approach as for the <span class="code">create</span> permission. Indeed, the permissions-to-roles mapping for a given object may depend on its <i>state</i>. 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 <i>workflow</I> as provided by Zope and Plone. This concept is simple: for a given gen-class, you may define several <i>states</i> (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 <i>initial</i> state of the object and what are the valid state changes (= <i>transitions</i>).</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">defaultWorkflow</span> in the <span class="code">Config</span> 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".</p>
|
||||||
|
|
||||||
|
<p>So let's define a simple workflow for our class <span class="code">ZopeComponent</span>. Until now our class looks like this:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponent:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
<b>def</b> showDate(self):<br/>
|
||||||
|
<b>return</b> True<br/>
|
||||||
|
<b>def</b> validateDescription(self, value):<br/>
|
||||||
|
res = True<br/>
|
||||||
|
<b>if</b> value.find('simple') != -1:<br/>
|
||||||
|
res = self.translate('zope_3_is_not_simple')<br/>
|
||||||
|
<b>return</b> res<br/>
|
||||||
|
description = String(editDefault=True)<br/>
|
||||||
|
technicalDescription = String(format=String.XHTML,<br/>
|
||||||
|
validator=validateDescription)<br/>
|
||||||
|
status = String(validator=['underDevelopement', 'stillSomeWorkToPerform',<br/>
|
||||||
|
'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient'],<br/>
|
||||||
|
optional=True, editDefault=True)<br/>
|
||||||
|
funeralDate = Date(optional=True)<br/>
|
||||||
|
responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,<br/>
|
||||||
|
link=True, back=Ref(attribute='components'))<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Field <span class="code">status</span> 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:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponentWorkflow:<br/>
|
||||||
|
<i># Roles</i><br/>
|
||||||
|
zManager = 'ZManager'<br/>
|
||||||
|
zLeader = 'ZLeader'<br/>
|
||||||
|
managerM = (zManager, 'Manager')<br/>
|
||||||
|
leaderM = (zLeader, 'Manager')<br/>
|
||||||
|
everybody = (zManager, zLeader, 'Manager')<br/>
|
||||||
|
<i># States</i><br/>
|
||||||
|
created = State({r:leaderM, w:leaderM, d:leaderM}, initial=True)<br/>
|
||||||
|
validated = State({r:everybody, w:everybody, d:None})<br/>
|
||||||
|
underDevelopment = State({r:everybody, w:leaderM, d:None})<br/>
|
||||||
|
whereIsTheClient = State({r:everybody, w:managerM, d:None})<br/>
|
||||||
|
<i># Transitions</i><br/>
|
||||||
|
validate = Transition( (created, validated), condition=managerM )<br/>
|
||||||
|
startDevelopment = Transition( (validated, underDevelopment),<br/>
|
||||||
|
condition=leaderM)<br/>
|
||||||
|
cancelDevelopment = Transition( (underDevelopment, whereIsTheClient),<br/>
|
||||||
|
condition=managerM)<br/>
|
||||||
|
cancel = Transition( ( (whereIsTheClient, underDevelopment),<br/>
|
||||||
|
(underDevelopment, validated),<br/>
|
||||||
|
(validated, created)), condition='Manager')<br/>
|
||||||
|
<br/>
|
||||||
|
<b>class</b> ZopeComponent:<br/>
|
||||||
|
...<br/>
|
||||||
|
workflow = ZopeComponentWorkflow<br/>
|
||||||
|
...<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">field</span>; this is because I felt myself guilty about being so ironic.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">appy.gen.State</span> and <span class="code">appy.gen.Transition</span>. 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 (<span class="code">String</span>, <span class="code">Ref</span>, etc) and workflow definitions (<span class="code">State</span>, <span class="code">Transition</span>, etc).</p>
|
||||||
|
|
||||||
|
<p>As shown in the last lines of the example, associating a gen-workflow to a gen-class is done through the <span class="code">workflow</span> 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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">r</span>, <span class="code">w</span>, and <span class="code">d</span> correspond to read, write and delete permissions; I can use them as is because of the clause <span class="code">from appy.gen import *</span>) and whose values are, as suggested by the example, either a tuple/list of roles, a single role, or <span class="code">None</span>. For example, when the component is <span class="code">underDevelopment</span>, only project leaders (and administrators) may modify them; when it is in state <span class="code">whereIsTheClient</span>, only managers (and administrators) may edit them. As soon as a component is <span class="code">validated</span>, nobody may delete it: permission <span class="code">d</span> is granted to <span class="code">None</span> (=nobody). The parameter <span class="code">initial=True</span> 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.</p>
|
||||||
|
|
||||||
|
<p>Definitions of transitions are based on state definitions. Indeed, when defining a transition, the first parameter is a 2-tuple <span class="code">(startState, endState)</span>. So a transition is simply a specified way to go to from one state to the other. Additional parameter <span class="code">condition</span> specifies under what circumstances the transition may be "triggered". In the example, only persons having roles <span class="code">Manager</span> or <span class="code">ZManager</span> are allowed to trigger transition <span class="code">validate</span>, that will change object state from <span class="code">created</span> to <span class="code">validated</span>. It is also possible to define <i>multi-transitions</i>, which are transitions having multiple 2-tuples <span class="code">(startState, endState)</span> (grouped in one big tuple) like transition <span class="code">cancel</span>. Multi-transitions may be seen as a shortcut that allows you to write several similar transitions in only one. In the example, <span class="code">cancel</span> transitions are used to "go backward", if a user triggered a transition by error.</p>
|
||||||
|
|
||||||
|
<p>Such a workflow is called a <i>state machine</i>. The following diagram represents the workflow defined above.</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow2.png"></p>
|
||||||
|
|
||||||
|
<p>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 <a href="gen.html">already mentioned</a>, 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 <i>communicate</i> your ideas. This is why we plan to integrate in future gen releases the possibility to generate diagrams from gen-workflows and gen-classes.</p>
|
||||||
|
|
||||||
|
<h1><a name="grantingRoles"></a>Granting roles</h1>
|
||||||
|
|
||||||
|
<p>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 <span class="code">sidney</span> and <span class="code">ludovic</span> in the right groups.</p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow3.png"></p>
|
||||||
|
|
||||||
|
<p>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".</p>
|
||||||
|
|
||||||
|
<p>We will first walk through the application as user <span class="code">admin</span>, as usual. According to the workflow, <span class="code">admin</span>, as <span class="code">Manager</span>, is God: he can do everything. Besides this pleasant feeling, it will allow us to trigger all workflow transitions.</p>
|
||||||
|
|
||||||
|
<p>Because role <span class="code">Manager</span> may add <span class="code">ZopeComponent</span> instances (thanks to <span class="code">Config.defaultCreators</span>), on the dashboard, the "plus" icon is available in tab "Zope component". Create a new Zope component: the consult view will look like this:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow4.png"></p>
|
||||||
|
|
||||||
|
<p>Besides the component title, its state appears (here: "Created"). According to the workflow, the only possible transition to trigger from this state is <span class="code">validate</span>; as <span class="code">Manager</span> 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 <span class="code">validated</span> as dictated by the workflow. The consult view has now evolved accordingly:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow5.png"></p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>The following screenshot shows how the dashboard may evolve according to permissions:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow6.png"></p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">ZopeComponent</span>, select "workflow state" in field "Columns to display while showing query results".</p>
|
||||||
|
|
||||||
|
<p>Now, please log out (a link is available in the top-right corner, within the blue strip) and log in as <span class="code">ludovic</span>. Because <span class="code">ZLeader</span>s 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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow7.png"></p>
|
||||||
|
|
||||||
|
<p>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:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow8.png"></p>
|
||||||
|
|
||||||
|
<p>Sidney is not among <span class="code">ZopeComponent</span> <span class="code">creator</span>s, 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 <span class="code">validate</span> it:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow9.png"></p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">created</span> 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 <span class="code">Owner</span>s and <span class="code">Manager</span>s when the component is <span class="code">created</span>, you may modify the workflow state <span class="code">created</span> like this:</p>
|
||||||
|
|
||||||
|
<p class="code">created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM}, initial=True)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>Now, log in as Ludovic. Consider the following dashboard as seen by him:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow10.png"></p>
|
||||||
|
|
||||||
|
<p>Components "ZC1" and "Aaaa" were created by <span class="code">admin</span>: Ludovic may not edit them. He can only edit the one he has created itself (= the last one in the table).</p>
|
||||||
|
|
||||||
|
<p>In future gen releases, you will be able to define and manage "custom" local roles.</p>
|
||||||
|
|
||||||
|
<h1><a name="conditionsAndActions"></a>Conditions and actions linked to transitions</h1>
|
||||||
|
|
||||||
|
<p>Until now, we have seen that, as transition <span class="code">condition</span>, 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 <span class="code">validated</span> only if a funeral date (which is not a mandatory field) has been specified. Transition <span class="code">validate</span> need to evolve:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponentWorkflow:<br/>
|
||||||
|
...<br/>
|
||||||
|
<b>def</b> funeralOk(self, obj): <b>return</b> obj.funeralDate<br/>
|
||||||
|
validate = Transition( (created, validated), condition=managerM + (funeralOk,))<br/>
|
||||||
|
...<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>It means that beyond having one of the roles defined in <span class="code">managerM</span>, method <span class="code">funeralOk</span> must also return <span class="code">True</span> (or an equivalent value) as prerequisite for triggering transition <span class="code">validate</span>. 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 <i>and</i> all specified methods return <span class="code">True</span>. So gen computes an <b>or</b>-operator on roles and an <b>and</b>-operator on methods.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">description</span> 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 <span class="code">startDevelopment</span>:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponentWorkflow:<br/>
|
||||||
|
...<br/>
|
||||||
|
<b>def</b> updateDescription(self, obj):<br/>
|
||||||
|
obj.description = 'Description edited by my manager was silly.'<br/>
|
||||||
|
startDevelopment = Transition( (validated, underDevelopment),<br/>
|
||||||
|
condition=leaderM, action=updateDescription)<br/>
|
||||||
|
...<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>We have specified a Python method in a new parameter named <span class="code">action</span>. Now, try to click on button "startDevelopment" and you will see the <span class="code">description</span> 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 <span class="code">action</span> parameter may also take a list or tuple of methods instead of a single method.</p>
|
||||||
|
|
||||||
|
<h1><a name="specificFieldPermissions"></a>Specific field permissions</h1>
|
||||||
|
|
||||||
|
<p>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 <i>any field</i> 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 <a href="genCreatingBasicClasses.html">already mentioned</a>, this can be done at the time of field definition, with boolean parameters <span class="code">specificReadPermission</span> and <span class="code">specificWritePermission</span>. 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.</p>
|
||||||
|
|
||||||
|
<p>Let's try it on our class <span class="code">ZopeComponent</span>. Suppose we need a specific write permission on field <span class="code">funeralDate</span> and a specific read permission on field <span class="code">responsibleBunch</span>:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponent:<br/>
|
||||||
|
...<br/>
|
||||||
|
funeralDate = Date(optional=True, specificWritePermission=True)<br/>
|
||||||
|
responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,<br/>
|
||||||
|
link=True, back=Ref(attribute='components'),<br/>
|
||||||
|
specificReadPermission=True)<br/>
|
||||||
|
...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">appy.gen.ReadPermission</span> and <span class="code">appy.gen.WritePermission</span> like this:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponentWorkflow:<br/>
|
||||||
|
<i># Specific permissions</i><br/>
|
||||||
|
wf = WritePermission('ZopeComponent.funeralDate')<br/>
|
||||||
|
rb = ReadPermission('ZopeComponent.responsibleBunch')<br/>
|
||||||
|
<i># Roles</i><br/>
|
||||||
|
...<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>When constructing a <span class="code">WritePermission</span> or <span class="code">ReadPermission</span> 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 (<span class="code">ZopeComponent</span> in the example). If the workflow class and the field class are in the same package (like, in our case, <span class="code">ZopeComponentWorkflow</span> and <span class="code">ZopeComponent</span>), you can specify the "relative" class name of the field class (without prefixing it with the package name, ie <span class="code">ZopeComponent</span>). Else, you need to specify the full package name of the class (ie <span class="code">ZopeComponent.ZopeComponent.funeralDate</span>).</p>
|
||||||
|
|
||||||
|
<p>Now let's update every state definition by integrating those 2 permissions in the permissions-to-roles mappings:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> ZopeComponentWorkflow:<br/>
|
||||||
|
...<br/>
|
||||||
|
<i># States</i><br/>
|
||||||
|
created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM, wf:'Owner', rb:everybody}, initial=True)<br/>
|
||||||
|
validated = State({r:everybody, w:everybody, d:None, wf:everybody, rb:everybody})<br/>
|
||||||
|
underDevelopment = State({r:everybody, w:leaderM, d:None, wf:leaderM, rb:everybody})<br/>
|
||||||
|
whereIsTheClient = State({r:everybody, w:managerM, d:None, wf:managerM, rb:everybody})<br/>
|
||||||
|
...<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Now, re-generate your product, restart Zope and re-install the product, update the security settings on <span class="code">portal_workflow</span> and try, as <span class="code">admin</span>, to edit a component that is in state <span class="code">created</span> and was created by Ludovic. Because <span class="code">Manager</span>s 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 <span class="code">funeralDate</span> (you are not the component <span class="code">Owner</span>), the field will not show up:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow11.png"></p>
|
||||||
|
|
||||||
|
<p>Aaaaargh! The field is visible! Impossible! How can user <span class="code">admin</span> bypass our security like this? This is the occasion to learn something about local roles: they <i>propagate</i> 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 <span class="code">admin</span> user: so <span class="code">admin</span> has local role <span class="code">Owner</span> on it (and, by the way, has local role <span class="code">Owner</span> on the Plone site as well). It means that <span class="code">admin</span> will have role <span class="code">Owner</span> on all sub-objects of your Plone site. When you think about this, it is normal: <span class="code">admin</span> is God (and you are <span class="code">admin</span>).</p>
|
||||||
|
|
||||||
|
<p>In order to produce a working example, let's create a new user (let's call it <span class="code">gerard</span>) and grant him role <span class="code">Manager</span>. This way, we will get a <span class="code">Manager</span> that is not <span class="code">Owner</span> of all objects. Log in as <span class="code">gerard</span>, and go the previous edit view:</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="img/workflow12.png"></p>
|
||||||
|
|
||||||
|
<p>Yes! You do not see (so you can't edit) field <span class="code">funeralDate</span>. 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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h1><a name="workflowInheritance"></a>Workflow inheritance</h1>
|
||||||
|
|
||||||
|
<p>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 <span class="code">ZopeComponentWorkflow</span>; a new workflow inheriting from it was created:</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<b>class</b> CobolComponentWorkflow(ZopeComponentWorkflow):<br/>
|
||||||
|
p = ZopeComponentWorkflow <i># Shortcut to workflow parent</i><br/>
|
||||||
|
<i># An additional state</i><br/>
|
||||||
|
finished = State(p.whereIsTheClient.permissions)<br/>
|
||||||
|
<i># Override validate: condition on funeralDate has no sense here</i><br/>
|
||||||
|
validate = Transition(p.validate.states, condition=p.managerM)<br/>
|
||||||
|
<i># Override cancelDevelopment: go to finished instead</i><br/>
|
||||||
|
cancelDevelopment = Transition( (p.underDevelopment, finished),<br/>
|
||||||
|
condition=p.managerM)<br/>
|
||||||
|
<i># Update cancel accordingly</i><br/>
|
||||||
|
cancel = Transition( ((finished, p.underDevelopment),) +p.cancel.states[1:],<br/>
|
||||||
|
condition=p.cancel.condition)<br/>
|
||||||
|
<br/>
|
||||||
|
<b>class</b> CobolComponent:<br/>
|
||||||
|
root = True<br/>
|
||||||
|
workflow = CobolComponentWorkflow<br/>
|
||||||
|
description = String()<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Basically, this new workflow "removes" state <span class="code">whereIsTheClient</span>, creates a more optimistic end state <span class="code">finished</span> 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 <span class="code">whereIsTheClient</span>. Then, we have overridden transition <span class="code">validate</span> because the condition that related to field <span class="code">funeralDate</span> is not relevant anymore (COBOL components have no funeral date). Transition <span class="code">cancelDevelopment</span> was also overridden: the end state is not <span class="code">whereIsTheClient</span> anymore, but <span class="code">finished</span> instead. We also need to override transition <span class="code">cancel</span> for updating the tuple of <span class="code">(startState, endState)</span>.</p>
|
||||||
|
|
||||||
|
<p>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 <span class="code">whereIsTheClient</span> 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 <span class="code">ZopeComponentWorkflow</span> is kept in the child workflow; any change made in the reused part of the parent workflow will automatically impact the child workflow(s).</p>
|
||||||
|
|
||||||
|
<h1><a name="workflowsAndi18n"></a>Workflows and i18n</h1>
|
||||||
|
|
||||||
|
<p>As usual, for every workflow state and transition, i18n labels have been automatically generated (in the <span class="code">plone</span> domain), together with a "nice" default value. The format of those labels is defined <a href="genCreatingAdvancedClasses.html#i18n">here</a>. There is still a small problem with the <span class="code">CobolComponentWorkflow</span>: the transition for finishing the work is called <span class="code">cancelDevelopment</span>. 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):</p>
|
||||||
|
|
||||||
|
<p class="code">
|
||||||
|
<i>#. Default: "Cancel development"</i><br/>
|
||||||
|
<b>msgid</b> "zopecomponent_cobolcomponentworkflow_cancelDevelopment"<br/>
|
||||||
|
<b>msgstr</b> "Finish"<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Note that i18n labels are "duplicated" for every child workflow. Here, I modify label <span class="code">zopecomponent_<b>cobolcomponentworkflow</b>_cancelDevelopment</span> without perturbing parent label for the same transition which is <span class="code">zopecomponent_<b>zopecomponentworkflow</b>_cancelDevelopment</span>.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
BIN
doc/gossips.odt
Executable file
BIN
doc/helloWorld.odt
Executable file
BIN
doc/img/ElseAmbiguous.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
doc/img/ElseAmbiguous.res.png
Executable file
After Width: | Height: | Size: 3.7 KiB |
BIN
doc/img/ElseNotAmbiguous.png
Executable file
After Width: | Height: | Size: 13 KiB |
BIN
doc/img/ElseNotAmbiguous.res.png
Executable file
After Width: | Height: | Size: 314 B |
BIN
doc/img/ElseStatements.png
Executable file
After Width: | Height: | Size: 21 KiB |
BIN
doc/img/ElseStatements.res.png
Executable file
After Width: | Height: | Size: 2.6 KiB |
BIN
doc/img/ErrorExpression.png
Executable file
After Width: | Height: | Size: 803 B |
BIN
doc/img/ErrorExpression.res.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
doc/img/ErrorForParsetime.png
Executable file
After Width: | Height: | Size: 6.2 KiB |
BIN
doc/img/ErrorForParsetime.res.png
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
doc/img/ErrorForRuntime.png
Executable file
After Width: | Height: | Size: 5 KiB |
BIN
doc/img/ErrorForRuntime.res.png
Executable file
After Width: | Height: | Size: 5.5 KiB |
BIN
doc/img/ErrorIf.png
Executable file
After Width: | Height: | Size: 3 KiB |
BIN
doc/img/ErrorIf.res.png
Executable file
After Width: | Height: | Size: 3.6 KiB |
BIN
doc/img/ForCellNotEnough.png
Executable file
After Width: | Height: | Size: 3 KiB |
BIN
doc/img/ForCellNotEnough.res.png
Executable file
After Width: | Height: | Size: 2.4 KiB |
BIN
doc/img/ForCellTooMuch2.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
BIN
doc/img/ForCellTooMuch2.res.png
Executable file
After Width: | Height: | Size: 2.6 KiB |
BIN
doc/img/ForTableMinus.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
doc/img/ForTableMinus.res.png
Executable file
After Width: | Height: | Size: 6.1 KiB |
BIN
doc/img/ForTableMinusError.png
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
doc/img/ForTableMinusError.res.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
doc/img/FromWithFor.png
Executable file
After Width: | Height: | Size: 13 KiB |
BIN
doc/img/FromWithFor.res.png
Executable file
After Width: | Height: | Size: 2.8 KiB |
BIN
doc/img/IfAndFors1.png
Executable file
After Width: | Height: | Size: 6.8 KiB |
BIN
doc/img/IfAndFors1.res.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/IfExpression.png
Executable file
After Width: | Height: | Size: 3.6 KiB |
BIN
doc/img/IfExpression.res.png
Executable file
After Width: | Height: | Size: 2.4 KiB |
BIN
doc/img/OnlyExpressions.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/OnlyExpressions.res.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/SimpleFrom.png
Executable file
After Width: | Height: | Size: 7.6 KiB |
BIN
doc/img/SimpleFrom.res.png
Executable file
After Width: | Height: | Size: 897 B |
BIN
doc/img/SimpleTest.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
doc/img/SimpleTest.res.png
Executable file
After Width: | Height: | Size: 7.9 KiB |
BIN
doc/img/actions1.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/actions2.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
doc/img/advisory.png
Executable file
After Width: | Height: | Size: 2.3 KiB |
BIN
doc/img/booleans1.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
doc/img/builtinFunctionInPodExpression.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/builtinFunctionInPodExpression.res.png
Executable file
After Width: | Height: | Size: 23 KiB |
BIN
doc/img/computed1.png
Executable file
After Width: | Height: | Size: 9.1 KiB |
BIN
doc/img/computed2.png
Executable file
After Width: | Height: | Size: 17 KiB |
BIN
doc/img/computed3.png
Executable file
After Width: | Height: | Size: 33 KiB |
BIN
doc/img/contact.gif
Executable file
After Width: | Height: | Size: 138 B |
BIN
doc/img/dates1.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
doc/img/dates2.png
Executable file
After Width: | Height: | Size: 2.6 KiB |
BIN
doc/img/dates3.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
doc/img/dates4.png
Executable file
After Width: | Height: | Size: 2.5 KiB |
BIN
doc/img/documentFunction1.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
doc/img/documentFunction2.png
Executable file
After Width: | Height: | Size: 9.1 KiB |
BIN
doc/img/documentFunction3.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/download.gif
Executable file
After Width: | Height: | Size: 150 B |
BIN
doc/img/emptyQuery.png
Executable file
After Width: | Height: | Size: 3.3 KiB |
BIN
doc/img/files1.png
Executable file
After Width: | Height: | Size: 5.3 KiB |
BIN
doc/img/files2.png
Executable file
After Width: | Height: | Size: 5 KiB |
BIN
doc/img/files3.png
Executable file
After Width: | Height: | Size: 9.5 KiB |
BIN
doc/img/files4.png
Executable file
After Width: | Height: | Size: 6.9 KiB |
BIN
doc/img/files5.png
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
doc/img/filledQuery.png
Executable file
After Width: | Height: | Size: 6.1 KiB |
BIN
doc/img/flavourOptions.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
doc/img/genpod1.png
Executable file
After Width: | Height: | Size: 6.8 KiB |
BIN
doc/img/genpod10.png
Executable file
After Width: | Height: | Size: 9.7 KiB |
BIN
doc/img/genpod11.png
Executable file
After Width: | Height: | Size: 7.8 KiB |
BIN
doc/img/genpod12.png
Executable file
After Width: | Height: | Size: 7.3 KiB |
BIN
doc/img/genpod13.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/genpod14.png
Executable file
After Width: | Height: | Size: 9.3 KiB |
BIN
doc/img/genpod2.png
Executable file
After Width: | Height: | Size: 4 KiB |
BIN
doc/img/genpod3.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/genpod4.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
doc/img/genpod5.png
Executable file
After Width: | Height: | Size: 4.3 KiB |
BIN
doc/img/genpod6.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
doc/img/genpod7.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/genpod8.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
doc/img/genpod9.png
Executable file
After Width: | Height: | Size: 7.6 KiB |
BIN
doc/img/gnu.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
BIN
doc/img/home.png
Executable file
After Width: | Height: | Size: 751 B |
BIN
doc/img/i18n1.png
Executable file
After Width: | Height: | Size: 4.8 KiB |
BIN
doc/img/inherit1.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
doc/img/inherit2.png
Executable file
After Width: | Height: | Size: 3.6 KiB |
BIN
doc/img/inherit3.png
Executable file
After Width: | Height: | Size: 5.2 KiB |
BIN
doc/img/inherit4.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
doc/img/inherit5.png
Executable file
After Width: | Height: | Size: 4.6 KiB |
BIN
doc/img/inherit6.png
Executable file
After Width: | Height: | Size: 4.6 KiB |
BIN
doc/img/integersFloats1.png
Executable file
After Width: | Height: | Size: 3.4 KiB |
BIN
doc/img/integersFloats2.png
Executable file
After Width: | Height: | Size: 10 KiB |