Initial import

This commit is contained in:
Gaetan Delannay 2009-06-29 14:06:01 +02:00
commit 4043163fc4
427 changed files with 18387 additions and 0 deletions

459
gen/__init__.py Executable file
View file

@ -0,0 +1,459 @@
# ------------------------------------------------------------------------------
import re
from appy.gen.utils import sequenceTypes, PageDescr
# Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete')
# Descriptor classes used for refining descriptions of elements in types
# (pages, groups,...) ----------------------------------------------------------
class Page:
def __init__(self, name, phase='main', show=True):
self.name = name
self.phase = phase
self.show = show
# ------------------------------------------------------------------------------
class Type:
'''Basic abstract class for defining any appy type.'''
def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue):
# The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc.
self.validator = validator
# Multiplicity is a tuple indicating the minimum and maximum
# occurrences of values.
self.multiplicity = multiplicity
# Type of the index on the values. If you want to represent a simple
# (ordered) list of values, specify None. If you want to
# index your values with unordered integers or with other types like
# strings (thus creating a dictionary of values instead of a list),
# specify a type specification for the index, like Integer() or
# String(). Note that this concept of "index" has nothing to do with
# the concept of "database index".
self.index = index
# Default value
self.default = default
# Is the field optional or not ?
self.optional = optional
# May the user configure a default value ?
self.editDefault = editDefault
# Must the field be visible or not?
self.show = show
# When displaying/editing the whole object, on what page and phase must
# this field value appear? Default is ('main', 'main'). pageShow
# indicates if the page must be shown or not.
self.page, self.phase, self.pageShow = PageDescr.getPageInfo(page, Page)
# Within self.page, in what group of fields must this field value
# appear?
self.group = group
# The following attribute allows to move a field back to a previous
# position (useful for content types that inherit from others).
self.move = move
# If specified "searchable", the field will be referenced in low-level
# indexing mechanisms for fast access and search functionalities.
self.searchable = searchable
# Normally, permissions to read or write every attribute in a type are
# granted if the user has the global permission to read or
# create/edit instances of the whole type. If you want a given attribute
# to be protected by specific permissions, set one or the 2 next boolean
# values to "True".
self.specificReadPermission = specificReadPermission
self.specificWritePermission = specificWritePermission
# Widget width and height
self.width = width
self.height = height
# The behaviour of this field may depend on another, "master" field
self.master = master
if master:
self.master.slaves.append(self)
# When master has some value(s), there is impact on this field.
self.masterValue = masterValue
self.id = id(self)
self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type
self.slaves = [] # The list of slaves of this field
self.selfClass = None # The Python class to which this Type definition
# is linked. This will be computed at runtime.
def isMultiValued(self):
'''Does this type definition allow to define multiple values?'''
res = False
maxOccurs = self.multiplicity[1]
if (maxOccurs == None) or (maxOccurs > 1):
res = True
return res
class Integer(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.pythonType = long
class Float(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.pythonType = float
class String(Type):
# Some predefined regular expressions that may be used as validators
c = re.compile
EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
'[a-zA-Z][a-zA-Z\.]*[a-zA-Z]')
ALPHANUMERIC = c('[\w-]+')
URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
'(([0-9]{1,5})?\/.*)?')
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, format=LINE,
show=True, page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.format = format
def isSelection(self):
'''Does the validator of this type definition define a list of values
into which the user must select one or more values?'''
res = True
if type(self.validator) in (list, tuple):
for elem in self.validator:
if not isinstance(elem, basestring):
res = False
break
else:
if not isinstance(self.validator, Selection):
res = False
return res
class Boolean(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.pythonType = bool
class Date(Type):
# Possible values for "format"
WITH_HOUR = 0
WITHOUT_HOUR = 1
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False,
format=WITH_HOUR, show=True, page='main', group=None, move=0,
searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.format = format
class File(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None,
isImage=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.isImage = isImage
class Ref(Type):
def __init__(self, klass=None, attribute=None, validator=None,
multiplicity=(0,1), index=None, default=None, optional=False,
editDefault=False, add=False, link=True, unlink=False,
back=None, isBack=False, show=True, page='main', group=None,
showHeaders=False, shownInfo=(), wide=False, select=None,
move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.klass = klass
self.attribute = attribute
self.add = add # May the user add new objects through this ref ?
self.link = link # May the user link existing objects through this ref?
self.unlink = unlink # May the user unlink existing objects?
self.back = back
self.isBack = isBack # Should always be False
self.showHeaders = showHeaders # When displaying a tabular list of
# referenced objects, must we show the table headers?
self.shownInfo = shownInfo # When displaying referenced object(s),
# we will display its title + all other fields whose names are listed
# in this attribute.
self.wide = wide # If True, the table of references will be as wide
# as possible
self.select = select # If a method is defined here, it will be used to
# filter the list of available tied objects.
class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, method=None, plainText=True,
master=None, masterValue=None):
Type.__init__(self, None, multiplicity, index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.method = method # The method used for computing the field value
self.plainText = plainText # Does field computation produce pain text
# or XHTML?
class Action(Type):
'''An action is a workflow-independent Python method that can be triggered
by the user on a given gen-class. For example, the custom installation
procedure of a gen-application is implemented by an action on the custom
tool class. An action is rendered as a button.'''
def __init__(self, validator=None, multiplicity=(1,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, action=None, master=None,
masterValue=None):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
self.action = action # Can be a single method or a list/tuple of methods
def __call__(self, obj):
'''Calls the action on p_obj.'''
try:
if type(self.action) in sequenceTypes:
# There are multiple Python methods
res = [True, '']
for act in self.action:
actRes = act(obj)
if type(actRes) in sequenceTypes:
res[0] = res[0] and actRes[0]
res[1] = res[1] + '\n' + actRes[1]
else:
res[0] = res[0] and actRes
else:
# There is only one Python method
actRes = self.action(obj)
if type(actRes) in sequenceTypes:
res = list(actRes)
else:
res = [actRes, '']
# If res is None (ie the user-defined action did not return anything)
# we consider the action as successfull.
if res[0] == None: res[0] = True
except Exception, e:
res = (False, str(e))
return res
class Info(Type):
'''An info is a field whose purpose is to present information
(text, html...) to the user.'''
def __init__(self, validator=None, multiplicity=(1,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
# Workflow-specific types ------------------------------------------------------
class State:
def __init__(self, permissions, initial=False, phase='main', show=True):
self.permissions = permissions #~{s_permissionName:[s_roleName]}~ This
# dict gives, for every permission managed by a workflow, the list of
# roles for which the permission is granted in this state.
# Standard permissions are 'read', 'write' and 'delete'.
self.initial = initial
self.phase = phase
self.show = show
def getUsedRoles(self):
res = set()
for roleValue in self.permissions.itervalues():
if isinstance(roleValue, basestring):
res.add(roleValue)
elif roleValue:
for role in roleValue:
res.add(role)
return list(res)
def getTransitions(self, transitions, selfIsFromState=True):
'''Among p_transitions, returns those whose fromState is p_self (if
p_selfIsFromState is True) or those whose toState is p_self (if
p_selfIsFromState is False).'''
res = []
for t in transitions:
if self in t.getStates(selfIsFromState):
res.append(t)
return res
def getPermissions(self):
'''If you get the permissions mapping through self.permissions, dict
values may be of different types (a list of roles, a single role or
None). Iy you call this method, you will always get a list which
may be empty.'''
res = {}
for permission, roleValue in self.permissions.iteritems():
if roleValue == None:
res[permission] = []
elif isinstance(roleValue, basestring):
res[permission] = [roleValue]
else:
res[permission] = roleValue
return res
class Transition:
def __init__(self, states, condition=True, action=None, notify=None):
self.states = states # In its simpler form, it is a tuple with 2
# states: (fromState, toState). But it can also be a tuple of several
# (fromState, toState) sub-tuples. This way, you may define only 1
# transition at several places in the state-transition diagram. It may
# be useful for "undo" transitions, for example.
self.condition = condition
self.action = action
self.notify = notify # If not None, it is a method telling who must be
# notified by email after the transition has been executed.
def getUsedRoles(self):
'''If self.condition is specifies a role.'''
res = []
if isinstance(self.condition, basestring):
res = [self.condition]
return res
def isSingle(self):
'''If this transitions is only define between 2 states, returns True.
Else, returns False.'''
return isinstance(self.states[0], State)
def getStates(self, fromStates=True):
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
else. If you want to get the states grouped in tuples
(fromState, toState), simply use self.states.'''
res = []
stateIndex = 1
if fromStates:
stateIndex = 0
if self.isSingle():
res.append(self.states[stateIndex])
else:
for states in self.states:
theState = states[stateIndex]
if theState not in res:
res.append(theState)
return res
def hasState(self, state, isFrom):
'''If p_isFrom is True, this method returns True if p_state is a
starting state for p_self. If p_isFrom is False, this method returns
True if p_state is an ending state for p_self.'''
stateIndex = 1
if isFrom:
stateIndex = 0
if self.isSingle():
res = state == self.states[stateIndex]
else:
res = False
for states in self.states:
if states[stateIndex] == state:
res = True
break
return res
class Permission:
'''If you need to define a specific read or write permission of a given
attribute of an Appy type, you use the specific boolean parameters
"specificReadPermission" or "specificWritePermission" for this attribute.
When you want to refer to those specific read or write permissions when
defining a workflow, for example, you need to use instances of
"ReadPermission" and "WritePermission", the 2 children classes of this
class. For example, if you need to refer to write permission of
attribute "t1" of class A, write: "WritePermission("A.t1") or
WritePermission("x.y.A.t1") if class A is not in the same module as
where you instantiate the class.'''
def __init__(self, fieldDescriptor):
self.fieldDescriptor = fieldDescriptor
class ReadPermission(Permission): pass
class WritePermission(Permission): pass
# ------------------------------------------------------------------------------
class Selection:
'''Instances of this class may be given as validator of a String, in order
to tell Appy that the validator is a selection that will be computed
dynamically.'''
pass
# ------------------------------------------------------------------------------
class Tool:
'''If you want so define a custom tool class, she must inherit from this
class.'''
class Flavour:
'''A flavour represents a given group of configuration options. If you want
to define a custom flavour class, she must inherit from this class.'''
def __init__(self, name): self.name = name
# ------------------------------------------------------------------------------
class Config:
'''If you want to specify some configuration parameters for appy.gen and
your application, please create an instance of this class and modify its
attributes. You may put your instance anywhere in your application
(main package, sub-package, etc).'''
# The default Config instance, used if the application does not give one.
defaultConfig = None
def getDefault():
if not Config.defaultConfig:
Config.defaultConfig = Config()
return Config.defaultConfig
getDefault = staticmethod(getDefault)
def __init__(self):
# For every language code that you specify in this list, appy.gen will
# produce and maintain translation files.
self.languages = ['en']
# People having one of these roles will be able to create instances
# of classes defined in your application.
self.defaultCreators = ['Manager', 'Owner']
# If True, the following flag will produce a minimalist Plone, where
# some actions, portlets or other stuff less relevant for building
# web applications, are removed or hidden. Using this produces
# effects on your whole Plone site!
self.minimalistPlone = False
# If you want to replace the Plone front page with a page coming from
# your application, use the following parameter. Setting
# frontPage = True will replace the Plone front page with a page
# whose content will come fron i18n label "front_page_text".
self.frontPage = False
# ------------------------------------------------------------------------------

193
gen/descriptors.py Normal file
View file

@ -0,0 +1,193 @@
# ------------------------------------------------------------------------------
from appy.gen import State, Transition, Type
# ------------------------------------------------------------------------------
class Descriptor: # Abstract
def __init__(self, klass, orderedAttributes, generator):
self.klass = klass # The corresponding Python class
self.orderedAttributes = orderedAttributes # Names of the static appy-
# compliant attributes declared in self.klass
self.generator = generator # A reference to the code generator.
def __repr__(self): return '<Class %s>' % self.klass.__name__
class ClassDescriptor(Descriptor):
'''This class gives information about an Appy class.'''
def getOrderedAppyAttributes(self):
'''Returns the appy types for all attributes of this class and parent
class(es).'''
res = []
# First, get the attributes for the current class
for attrName in self.orderedAttributes:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Type):
res.append( (attrName, attrValue) )
# Then, add attributes from parent classes
for baseClass in self.klass.__bases__:
# Find the classDescr that corresponds to baseClass
baseClassDescr = None
for classDescr in self.generator.classes:
if classDescr.klass == baseClass:
baseClassDescr = classDescr
break
if baseClassDescr:
res = baseClassDescr.getOrderedAppyAttributes() + res
return res
def getChildren(self):
'''Returns, among p_allClasses, the classes that inherit from p_self.'''
res = []
for classDescr in self.generator.classes:
if (classDescr.klass != self.klass) and \
issubclass(classDescr.klass, self.klass):
res.append(classDescr)
return res
def getPhases(self):
'''Gets the phases defined on fields of this class.'''
res = []
for fieldName, appyType in self.getOrderedAppyAttributes():
if appyType.phase not in res:
res.append(appyType.phase)
return res
class WorkflowDescriptor(Descriptor):
'''This class gives information about an Appy workflow.'''
def _getWorkflowElements(self, elemType):
res = []
for attrName in dir(self.klass):
attrValue = getattr(self.klass, attrName)
condition = False
if elemType == 'states':
condition = isinstance(attrValue, State)
elif elemType == 'transitions':
condition = isinstance(attrValue, Transition)
elif elemType == 'all':
condition = isinstance(attrValue, State) or \
isinstance(attrValue, Transition)
if condition:
res.append(attrValue)
return res
def getStates(self):
return self._getWorkflowElements('states')
def getTransitions(self):
return self._getWorkflowElements('transitions')
def getStateNames(self, ordered=False):
res = []
attrs = dir(self.klass)
allAttrs = attrs
if ordered:
attrs = self.orderedAttributes
allAttrs = dir(self.klass)
for attrName in attrs:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, State):
res.append(attrName)
# Complete the list with inherited states. For the moment, we are unable
# to sort inherited states.
for attrName in allAttrs:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, State) and (attrName not in attrs):
res.insert(0, attrName)
return res
def getInitialStateName(self):
res = None
for attrName in dir(self.klass):
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, State) and attrValue.initial:
res = attrName
break
return res
def getTransitionNamesOf(self, transitionName, transition,
limitToFromState=None):
'''Appy p_transition may correspond to several transitions of the
concrete workflow engine used. This method returns in a list the
name(s) of the "concrete" transition(s) corresponding to
p_transition.'''
res = []
if transition.isSingle():
res.append(transitionName)
else:
for fromState, toState in transition.states:
if not limitToFromState or \
(limitToFromState and (fromState == limitToFromState)):
fromStateName = self.getNameOf(fromState)
toStateName = self.getNameOf(toState)
res.append('%s%s%sTo%s%s' % (transitionName,
fromStateName[0].upper(), fromStateName[1:],
toStateName[0].upper(), toStateName[1:]))
return res
def getTransitionNames(self, limitToTransitions=None, limitToFromState=None,
withLabels=False):
'''Returns the name of all "concrete" transitions corresponding to the
Appy transitions of this worlflow. If p_limitToTransitions is not
None, it represents a list of Appy transitions and the result is a
list of the names of the "concrete" transitions that correspond to
those transitions only. If p_limitToFromState is not None, it
represents an Appy state; only transitions having this state as start
state will be taken into account. If p_withLabels is True, the method
returns a list of tuples (s_transitionName, s_transitionLabel); the
label being the name of the Appy transition.'''
res = []
for attrName in dir(self.klass):
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Transition):
# We encountered a transition.
t = attrValue
tName = attrName
if not limitToTransitions or \
(limitToTransitions and t in limitToTransitions):
# We must take this transition into account according to
# param "limitToTransitions".
if (not limitToFromState) or \
(limitToFromState and \
t.hasState(limitToFromState, isFrom=True)):
# We must take this transition into account according
# to param "limitToFromState"
tNames = self.getTransitionNamesOf(
tName, t, limitToFromState)
if not withLabels:
res += tNames
else:
for tn in tNames:
res.append((tn, tName))
return res
def getEndStateName(self, transitionName):
'''Returns the name of the state where the "concrete" transition named
p_transitionName ends.'''
res = None
for attrName in dir(self.klass):
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Transition):
# We got a transition.
t = attrValue
tName = attrName
if t.isSingle():
if transitionName == tName:
endState = t.states[1]
res = self.getNameOf(endState)
else:
transNames = self.getTransitionNamesOf(tName, t)
if transitionName in transNames:
endState = t.states[transNames.index(transitionName)][1]
res = self.getNameOf(endState)
return res
def getNameOf(self, stateOrTransition):
'''Gets the Appy name of a p_stateOrTransition.'''
res = None
for attrName in dir(self.klass):
attrValue = getattr(self.klass, attrName)
if attrValue == stateOrTransition:
res = attrName
break
return res
# ------------------------------------------------------------------------------

420
gen/generator.py Executable file
View file

@ -0,0 +1,420 @@
# ------------------------------------------------------------------------------
import os, os.path, sys, parser, symbol, token
from optparse import OptionParser
from appy.gen import Type, State, Config, Tool, Flavour
from appy.gen.descriptors import *
from appy.gen.utils import produceNiceMessage
import appy.pod, appy.pod.renderer
from appy.shared.utils import FolderDeleter
# ------------------------------------------------------------------------------
class GeneratorError(Exception): pass
# I need the following classes to parse Python classes and find in which
# order the attributes are defined. --------------------------------------------
class AstMatcher:
'''Allows to find a given pattern within an ast (part).'''
def _match(pattern, node):
res = None
if pattern[0] == node[0]:
# This level matches
if len(pattern) == 1:
return node
else:
if type(node[1]) == tuple:
return AstMatcher._match(pattern[1:], node[1])
return res
_match = staticmethod(_match)
def match(pattern, node):
res = []
for subNode in node[1:]:
# Do I find the pattern among the subnodes ?
occurrence = AstMatcher._match(pattern, subNode)
if occurrence:
res.append(occurrence)
return res
match = staticmethod(match)
# ------------------------------------------------------------------------------
class AstClass:
'''Python class.'''
def __init__(self, node):
# Link to the Python ast node
self.node = node
self.name = node[2][1]
self.attributes = [] # We are only interested in parsing static
# attributes to now their order
if sys.version_info[:2] >= (2,5):
self.statementPattern = (
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.or_test,
symbol.and_test, symbol.not_test, symbol.comparison, symbol.expr,
symbol.xor_expr, symbol.and_expr, symbol.shift_expr,
symbol.arith_expr, symbol.term, symbol.factor, symbol.power)
else:
self.statementPattern = (
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.and_test,
symbol.not_test, symbol.comparison, symbol.expr, symbol.xor_expr,
symbol.and_expr, symbol.shift_expr, symbol.arith_expr,
symbol.term, symbol.factor, symbol.power)
for subNode in node[1:]:
if subNode[0] == symbol.suite:
# We are in the class body
self.getStaticAttributes(subNode)
def getStaticAttributes(self, classBody):
statements = AstMatcher.match(self.statementPattern, classBody)
for statement in statements:
if len(statement) == 2 and statement[1][0] == symbol.atom and \
statement[1][1][0] == token.NAME:
attrName = statement[1][1][1]
self.attributes.append(attrName)
def __repr__(self):
return '<class %s has attrs %s>' % (self.name, str(self.attributes))
# ------------------------------------------------------------------------------
class Ast:
'''Python AST.'''
classPattern = (symbol.stmt, symbol.compound_stmt, symbol.classdef)
def __init__(self, pyFile):
f = file(pyFile)
fContent = f.read()
f.close()
fContent = fContent.replace('\r', '')
ast = parser.suite(fContent).totuple()
# Get all the classes defined within this module.
self.classes = {}
classNodes = AstMatcher.match(self.classPattern, ast)
for node in classNodes:
astClass = AstClass(node)
self.classes[astClass.name] = astClass
# ------------------------------------------------------------------------------
WARN_NO_TEMPLATE = 'Warning: the code generator should have a folder "%s" ' \
'containing all code templates.'
CODE_HEADER = '''# -*- coding: utf-8 -*-
#
# GNU General Public License (GPL)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
'''
class Generator:
'''Abstract base class for building a generator.'''
def __init__(self, application, outputFolder, options):
self.application = application
# Determine application name
self.applicationName = os.path.basename(application)
if application.endswith('.py'):
self.applicationName = self.applicationName[:-3]
# Determine output folder (where to store the generated product)
self.outputFolder = '%s/%s' % (outputFolder, self.applicationName)
self.options = options
# Determine templates folder
exec 'import %s as genModule' % self.__class__.__module__
self.templatesFolder = os.path.join(os.path.dirname(genModule.__file__),
'templates')
if not os.path.exists(self.templatesFolder):
print WARN_NO_TEMPLATE % self.templatesFolder
# Default descriptor classes
self.classDescriptor = ClassDescriptor
self.workflowDescriptor = WorkflowDescriptor
self.customToolClassDescriptor = ClassDescriptor
self.customFlavourClassDescriptor = ClassDescriptor
# Custom tool and flavour classes, if they are defined in the
# application
self.customToolDescr = None
self.customFlavourDescr = None
# The following dict contains a series of replacements that need to be
# applied to file templates to generate files.
self.repls = {'applicationName': self.applicationName,
'applicationPath': os.path.dirname(self.application),
'codeHeader': CODE_HEADER}
# List of Appy classes and workflows found in the application
self.classes = []
self.workflows = []
self.initialize()
self.config = Config.getDefault()
def determineAppyType(self, klass):
'''Is p_klass an Appy class ? An Appy workflow? None of this ?
If it (or a parent) declares at least one appy type definition,
it will be considered an Appy class. If it (or a parent) declares at
least one state definition, it will be considered an Appy
workflow.'''
res = 'none'
for attrValue in klass.__dict__.itervalues():
if isinstance(attrValue, Type):
res = 'class'
elif isinstance(attrValue, State):
res = 'workflow'
if not res:
for baseClass in klass.__bases__:
baseClassType = self.determineAppyType(baseClass)
if baseClassType != 'none':
res = baseClassType
break
return res
IMPORT_ERROR = 'Warning: error while importing module %s (%s)'
SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)'
def walkModule(self, moduleName):
'''Visits a given (sub-*)module into the application.'''
try:
exec 'import %s' % moduleName
exec 'moduleObj = %s' % moduleName
moduleFile = moduleObj.__file__
if moduleFile.endswith('.pyc'):
moduleFile = moduleFile[:-1]
astClasses = Ast(moduleFile).classes
except ImportError, ie:
# True import error or, simply, this is a simple folder within
# the application, not a sub-module.
print self.IMPORT_ERROR % (moduleName, str(ie))
return
except SyntaxError, se:
print self.SYNTAX_ERROR % (moduleName, str(se))
return
classType = type(Generator)
# Find all classes in this module
for moduleElemName in moduleObj.__dict__.keys():
exec 'moduleElem = moduleObj.%s' % moduleElemName
if (type(moduleElem) == classType) and \
(moduleElem.__module__ == moduleObj.__name__):
# We have found a Python class definition in this module.
appyType = self.determineAppyType(moduleElem)
if appyType != 'none':
# Produce a list of static class attributes (in the order
# of their definition).
attrs = astClasses[moduleElem.__name__].attributes
if appyType == 'class':
if issubclass(moduleElem, Tool):
descrClass = self.customToolClassDescriptor
self.customToolDescr = descrClass(
moduleElem, attrs, self)
elif issubclass(moduleElem, Flavour):
descrClass = self.customFlavourClassDescriptor
self.customFlavourDescr = descrClass(
moduleElem, attrs, self)
else:
descrClass = self.classDescriptor
self.classes.append(
descrClass(moduleElem, attrs, self))
elif appyType == 'workflow':
descrClass = self.workflowDescriptor
self.workflows.append(
descrClass(moduleElem, attrs, self))
elif isinstance(moduleElem, Config):
self.config = moduleElem
# Walk potential sub-modules
if moduleFile.find('__init__.py') != -1:
# Potentially, sub-modules exist
moduleFolder = os.path.dirname(moduleFile)
for elem in os.listdir(moduleFolder):
subModuleName, ext = os.path.splitext(elem)
if ((ext == '.py') and (subModuleName != '__init__')) or \
os.path.isdir(os.path.join(moduleFolder, subModuleName)):
# Submodules may be sub-folders or Python files
subModuleName = '%s.%s' % (moduleName, subModuleName)
self.walkModule(subModuleName)
def walkApplication(self):
'''This method walks into the application and creates the corresponding
meta-classes in self.classes, self.workflows, etc.'''
# Where is the application located ?
containingFolder = os.path.dirname(self.application)
sys.path.append(containingFolder)
# What is the name of the application ?
appName = os.path.basename(self.application)
if os.path.isfile(self.application):
appName = os.path.splitext(appName)[0]
self.walkModule(appName)
sys.path.pop()
def generateClass(self, classDescr):
'''This method is called whenever a Python class declaring Appy type
definition(s) is encountered within the application.'''
def generateWorkflow(self, workflowDescr):
'''This method is called whenever a Python class declaring states and
transitions is encountered within the application.'''
def initialize(self):
'''Called before the old product is removed (if any), in __init__.'''
def finalize(self):
'''Called at the end of the generation process.'''
def copyFile(self, fileName, replacements, destName=None, destFolder=None,
isPod=False):
'''This method will copy p_fileName from self.templatesFolder to
self.outputFolder (or in a subFolder if p_destFolder is given)
after having replaced all p_replacements. If p_isPod is True,
p_fileName is a POD template and the copied file is the result of
applying p_fileName with context p_replacements.'''
# Get the path of the template file to copy
templatePath = os.path.join(self.templatesFolder, fileName)
# Get (or create if needed) the path of the result file
destFile = fileName
if destName: destFile = destName
if destFolder: destFile = '%s/%s' % (destFolder, destFile)
absDestFolder = self.outputFolder
if destFolder:
absDestFolder = os.path.join(self.outputFolder, destFolder)
if not os.path.exists(absDestFolder):
os.makedirs(absDestFolder)
resultPath = os.path.join(self.outputFolder, destFile)
if os.path.exists(resultPath): os.remove(resultPath)
if not isPod:
# Copy the template file to result file after having performed some
# replacements
f = file(templatePath)
fileContent = f.read()
f.close()
if not fileName.endswith('.png'):
for rKey, rValue in replacements.iteritems():
fileContent = fileContent.replace(
'<!%s!>' % rKey, str(rValue))
f = file(resultPath, 'w')
f.write(fileContent)
f.close()
else:
# Call the POD renderer to produce the result
rendererParams = {'template': templatePath,
'context': replacements,
'result': resultPath}
renderer = appy.pod.renderer.Renderer(**rendererParams)
renderer.run()
def run(self):
self.walkApplication()
for classDescr in self.classes: self.generateClass(classDescr)
for wfDescr in self.workflows: self.generateWorkflow(wfDescr)
self.finalize()
print 'Done.'
# ------------------------------------------------------------------------------
ERROR_CODE = 1
VALID_PRODUCT_TYPES = ('plone25', 'odt')
APP_NOT_FOUND = 'Application not found at %s.'
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.'
PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \
'following: %s' % str(VALID_PRODUCT_TYPES)
C_OPTION = 'Removes from i18n files all labels that are not automatically ' \
'generated from your gen-application. It can be useful during ' \
'development, when you do lots of name changes (classes, ' \
'attributes, states, transitions, etc): in this case, the Appy ' \
'i18n label generation machinery produces lots of labels that ' \
'then become obsolete.'
S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \
'generated i18n files, you will find first all labels ' \
'that are automatically generated by appy.gen, in some logical ' \
'order (ie: field-related labels appear together, in the order ' \
'they are declared in the gen-class). Then, if you have added ' \
'labels manually, they will appear afterwards. Sorting labels ' \
'may not be desired under development. Indeed, when no sorting ' \
'occurs, every time you add or modify a field, class, state, etc, ' \
'newly generated labels will all appear together at the end of ' \
'the file; so it will be easy to translate them all. When sorting ' \
'occurs, those elements may be spread at different places in the ' \
'i18n file. When the development is finished, it may be a good ' \
'idea to sort the labels to get a clean and logically ordered ' \
'set of translation files.'
class GeneratorScript:
'''usage: %prog [options] app productType outputFolder
"app" is the path to your Appy application, which may be a
Python module (= a file than ends with .py) or a Python
package (= a folder containing a file named __init__.py).
Your app may reside anywhere (but it needs to be
accessible by the underlying application server, ie Zope),
excepted within the generated product. Typically, if you
generate a Plone product, it may reside within
<yourZopeInstance>/lib/python, but not within the
generated product (typically stored in
<yourZopeInstance>/Products).
"productType" is the kind of product you want to generate
(currently, only "plone25" and 'odt' are supported;
in the near future, the "plone25" target will also produce
Plone 3-compliant code that will still work with
Plone 2.5).
"outputFolder" is the folder where the product will be generated.
For example, if you specify /my/output/folder for your
application /home/gde/MyApp.py, this script will create
a folder /my/output/folder/MyApp and put in it the
generated product.
Example: generating a Plone product
-----------------------------------
In your Zope instance named myZopeInstance, create a folder
"myZopeInstance/lib/python/MyApp". Create into it your Appy application
(we suppose here that it is a Python package, containing a __init__.py
file and other files). Then, chdir into this folder and type
"python <appyPath>/gen/generator.py . plone25 ../../../Products" and the
product will be generated in myZopeInstance/Products/MyApp.
"python" must refer to a Python interpreter that knows package appy.'''
def generateProduct(self, options, application, productType, outputFolder):
exec 'from appy.gen.%s.generator import Generator' % productType
Generator(application, outputFolder, options).run()
def manageArgs(self, parser, options, args):
# Check number of args
if len(args) != 3:
print WRONG_NG_OF_ARGS
parser.print_help()
sys.exit(ERROR_CODE)
# Check productType
if args[1] not in VALID_PRODUCT_TYPES:
print PRODUCT_TYPE_ERROR
sys.exit(ERROR_CODE)
# Check existence of application
if not os.path.exists(args[0]):
print APP_NOT_FOUND % args[0]
sys.exit(ERROR_CODE)
# Check existence of outputFolder basic type
if not os.path.exists(args[2]):
print WRONG_OUTPUT_FOLDER
sys.exit(ERROR_CODE)
# Convert all paths in absolute paths
for i in (0,2):
args[i] = os.path.abspath(args[i])
def run(self):
optParser = OptionParser(usage=GeneratorScript.__doc__)
optParser.add_option("-c", "--i18n-clean", action='store_true',
dest='i18nClean', default=False, help=C_OPTION)
optParser.add_option("-s", "--i18n-sort", action='store_true',
dest='i18nSort', default=False, help=S_OPTION)
(options, args) = optParser.parse_args()
try:
self.manageArgs(optParser, options, args)
print 'Generating %s product in %s...' % (args[1], args[2])
self.generateProduct(options, *args)
except GeneratorError, ge:
sys.stderr.write(str(ge))
sys.stderr.write('\n')
optParser.print_help()
sys.exit(ERROR_CODE)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
GeneratorScript().run()
# ------------------------------------------------------------------------------

0
gen/odt/__init__.py Executable file
View file

54
gen/odt/generator.py Executable file
View file

@ -0,0 +1,54 @@
'''This file contains the main Generator class used for generating an
ODT file from an Appy application.'''
# ------------------------------------------------------------------------------
import os, os.path
from appy.gen import Page
from appy.gen.utils import produceNiceMessage
from appy.gen.generator import Generator as AbstractGenerator
# ------------------------------------------------------------------------------
class Generator(AbstractGenerator):
'''This generator generates ODT files from an Appy application.'''
def __init__(self, *args, **kwargs):
AbstractGenerator.__init__(self, *args, **kwargs)
self.repls = {'generator': self}
def finalize(self):
pass
def getOdtFieldLabel(self, fieldName):
'''Given a p_fieldName, this method creates the label as it will appear
in the ODT file.'''
return '<text:p><text:bookmark text:name="%s"/>%s</text:p>' % \
(fieldName, produceNiceMessage(fieldName))
def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application.'''
repls = self.repls.copy()
repls['classDescr'] = classDescr
self.copyFile('basic.odt', repls,
destName='%sEdit.odt' % classDescr.klass.__name__, isPod=True)
def fieldIsStaticallyInvisible(self, field):
'''This method determines if p_field is always invisible. It can be
verified for example if field.type.show is the boolean value False or
if the page where the field must be displayed has a boolean attribute
"show" having the boolean value False.'''
if (type(field.show) == bool) and not field.show: return True
if (type(field.pageShow) == bool) and not field.pageShow: return True
return False
undumpable = ('Ref', 'Action', 'File', 'Computed')
def getRelevantAttributes(self, classDescr):
'''Some fields, like computed fields or actions, should not be dumped
into the ODT file. This method returns the list of "dumpable"
fields.'''
res = []
for fieldName, field in classDescr.getOrderedAppyAttributes():
if (field.type not in self.undumpable) and \
(not self.fieldIsStaticallyInvisible(field)):
res.append((fieldName, field))
return res
# ------------------------------------------------------------------------------

BIN
gen/odt/templates/basic.odt Normal file

Binary file not shown.

0
gen/plone25/__init__.py Executable file
View file

666
gen/plone25/descriptors.py Executable file
View file

@ -0,0 +1,666 @@
'''Descriptor classes defined in this file are "intermediary" classes that
gather, from the user application, information about concepts (like Archetype
classes or DC workflow definitions) that will eventually be dumped into the
generated application. Typically they have methods named "generate..." that
produce generated code.'''
# ------------------------------------------------------------------------------
import types, copy
from model import ModelClass, Flavour, flavourAttributePrefixes
from utils import stringify
import appy.gen
import appy.gen.descriptors
from appy.gen.po import PoMessage
from appy.gen import Date, String, State, Transition, Type
from appy.gen.utils import GroupDescr, PageDescr, produceNiceMessage
TABS = 4 # Number of blanks in a Python indentation.
# ------------------------------------------------------------------------------
class ArchetypeFieldDescriptor:
'''This class allows to gather information needed to generate an Archetypes
definition (field + widget) from an Appy type. An Appy type is used for
defining the type of attributes defined in the user application.'''
singleValuedTypes = ('Integer', 'Float', 'Boolean', 'Date', 'File')
# Although Appy allows to specify a multiplicity[0]>1 for those types, it is
# not supported by Archetypes. So we will always generate single-valued type
# definitions for them.
specialParams = ('title', 'description')
def __init__(self, fieldName, appyType, classDescriptor):
self.appyType = appyType
self.classDescr = classDescriptor
self.generator = classDescriptor.generator
self.applicationName = classDescriptor.generator.applicationName
self.fieldName = fieldName
self.fieldParams = {'name': fieldName}
self.widgetParams = {}
self.fieldType = None
self.widgetType = None
self.walkAppyType()
def __repr__(self):
return '<Field %s, %s>' % (self.fieldName, self.classDescr)
def getFlavourAttributeMessage(self, fieldName):
'''Some attributes generated on the Flavour class need a specific
default message, returned by this method.'''
res = fieldName
for prefix in flavourAttributePrefixes:
if fieldName.startswith(prefix):
messageId = 'MSG_%s' % prefix
res = getattr(PoMessage, messageId)
if res.find('%s') != -1:
# I must complete the message with the field name.
res = res % fieldName.split('_')[-1]
break
return res
def produceMessage(self, msgId, isLabel=True):
'''Gets the default label or description (if p_isLabel is False) for
i18n message p_msgId.'''
default = ' '
produceNice = False
if isLabel:
produceNice = True
default = self.fieldName
# Some attributes need a specific predefined message
if isinstance(self.classDescr, FlavourClassDescriptor):
default = self.getFlavourAttributeMessage(self.fieldName)
if default != self.fieldName: produceNice = False
msg = PoMessage(msgId, '', default)
if produceNice:
msg.produceNiceDefault()
return msg
def walkBasicType(self):
'''How to dump a basic type?'''
self.fieldType = '%sField' % self.appyType.type
self.widgetType = "%sWidget" % self.appyType.type
if self.appyType.type == 'Date':
self.fieldType = 'DateTimeField'
self.widgetType = 'CalendarWidget'
if self.appyType.format == Date.WITHOUT_HOUR:
self.widgetParams['show_hm'] = False
elif self.appyType.type == 'Float':
self.widgetType = 'DecimalWidget'
elif self.appyType.type == 'File':
if self.appyType.isImage:
self.fieldType = 'ImageField'
self.widgetType = 'ImageWidget'
self.fieldParams['storage'] = 'python:AttributeStorage()'
def walkString(self):
'''How to generate an Appy String?'''
if self.appyType.format == String.LINE:
if self.appyType.isSelection():
if self.appyType.isMultiValued():
self.fieldType = 'LinesField'
self.widgetType = 'MultiSelectionWidget'
self.fieldParams['multiValued'] = True
else:
self.fieldType = 'StringField'
self.widgetType = 'SelectionWidget'
self.widgetParams['format'] = 'select'
# Elements common to all selection fields
methodName = 'list_%s_values' % self.fieldName
self.fieldParams['vocabulary'] = methodName
self.classDescr.addSelectMethod(
methodName, self, self.appyType.isMultiValued())
self.fieldParams['enforceVocabulary'] = True
else:
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
self.widgetParams['size'] = 50
if self.appyType.width:
self.widgetParams['size'] = self.appyType.width
# Manage index
if self.appyType.searchable:
self.fieldParams['index'] = 'FieldIndex'
elif self.appyType.format == String.TEXT:
self.fieldType = 'TextField'
self.widgetType = 'TextAreaWidget'
if self.appyType.height:
self.widgetParams['rows'] = self.appyType.height
elif self.appyType.format == String.XHTML:
self.fieldType = 'TextField'
self.widgetType = 'RichWidget'
self.fieldParams['allowable_content_types'] = ('text/html',)
self.fieldParams['default_output_type'] = "text/html"
else:
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
# Manage searchability
if self.appyType.searchable:
self.fieldParams['searchable'] = True
def walkComputed(self):
'''How to generate a computed field? We generate an Archetypes String
field.'''
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
self.widgetParams['visible'] = False # Archetypes will believe the
# field is invisible; we will display it ourselves (like for Ref fields)
def walkAction(self):
'''How to generate an action field ? We generate an Archetypes String
field.'''
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
self.widgetParams['visible'] = False # Archetypes will believe the
# field is invisible; we will display it ourselves (like for Ref fields)
# Add action-specific i18n messages
for suffix in ('ok', 'ko'):
label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName,
suffix)
msg = PoMessage(label, '',
getattr(PoMessage, 'ACTION_%s' % suffix.upper()))
self.generator.labels.append(msg)
self.classDescr.labelsToPropagate.append(msg)
def walkRef(self):
'''How to generate a Ref?'''
relationship = '%s_%s_rel' % (self.classDescr.name, self.fieldName)
self.fieldType = 'ReferenceField'
self.widgetType = 'ReferenceWidget'
self.fieldParams['relationship'] = relationship
if self.appyType.isMultiValued():
self.fieldParams['multiValued'] = True
self.widgetParams['visible'] = False
# Update the list of referers
self.generator.addReferer(self, relationship)
# Add the widget label for the back reference
refClassName = ArchetypesClassDescriptor.getClassName(
self.appyType.klass)
if issubclass(self.appyType.klass, ModelClass):
refClassName = self.applicationName + self.appyType.klass.__name__
elif issubclass(self.appyType.klass, appy.gen.Tool):
refClassName = '%sTool' % self.applicationName
elif issubclass(self.appyType.klass, appy.gen.Flavour):
refClassName = '%sFlavour' % self.applicationName
backLabel = "%s_%s_back" % (refClassName, self.appyType.back.attribute)
poMsg = PoMessage(backLabel, '', self.appyType.back.attribute)
poMsg.produceNiceDefault()
self.generator.labels.append(poMsg)
def walkInfo(self):
'''How to generate an Info field? We generate an Archetypes String
field.'''
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
self.widgetParams['visible'] = False # Archetypes will believe the
# field is invisible; we will display it ourselves (like for Ref fields)
alwaysAValidatorFor = ('Ref', 'Integer', 'Float')
def walkAppyType(self):
'''Walks into the Appy type definition and gathers data about the
Archetype elements to generate.'''
# Manage things common to all Appy types
# - special accessor for fields "title" and "description"
if self.fieldName in self.specialParams:
self.fieldParams['accessor'] = self.fieldName.capitalize()
# - default value
if self.appyType.default != None:
self.fieldParams['default'] = self.appyType.default
# - required?
if self.appyType.multiplicity[0] >= 1:
if self.appyType.type != 'Ref':
# Indeed, if it is a ref appy will manage itself field updates
# in at_post_create_script, so Archetypes must not enforce
# required=True
self.fieldParams['required'] = True
# - optional ?
if self.appyType.optional:
Flavour._appy_addOptionalField(self)
self.widgetParams['condition'] = ' python: ' \
'here.fieldIsUsed("%s")'% self.fieldName
# - edit default value ?
if self.appyType.editDefault:
Flavour._appy_addDefaultField(self)
methodName = 'getDefaultValueFor%s' % self.fieldName
self.fieldParams['default_method'] = methodName
self.classDescr.addDefaultMethod(methodName, self)
# - searchable ?
if self.appyType.searchable and (self.appyType.type != 'String'):
self.fieldParams['index'] = 'FieldIndex'
# - slaves ?
if self.appyType.slaves:
self.widgetParams['visible'] = False # Archetypes will believe the
# field is invisible; we will display it ourselves (like for Ref
# fields)
# - need to generate a field validator?
# In all cases, add an i18n message for the validation error for this
# field.
label = '%s_%s_valid' % (self.classDescr.name, self.fieldName)
poMsg = PoMessage(label, '', PoMessage.DEFAULT_VALID_ERROR)
self.generator.labels.append(poMsg)
if (type(self.appyType.validator) == types.FunctionType) or \
(type(self.appyType.validator) == type(String.EMAIL)) or \
(self.appyType.type in self.alwaysAValidatorFor):
# For references, we always add a validator because gen validates
# itself things like multiplicities;
# For integers and floats, we also need validators because, by
# default, Archetypes produces an exception if the field value does
# not have the correct type, for example.
methodName = 'validate_%s' % self.fieldName
# Add a validate method for this
specificType = None
if self.appyType.type in self.alwaysAValidatorFor:
specificType = self.appyType.type
self.classDescr.addValidateMethod(methodName, label, self,
specificType=specificType)
# Manage specific permissions
permFieldName = '%s %s' % (self.classDescr.name, self.fieldName)
if self.appyType.specificReadPermission:
self.fieldParams['read_permission'] = '%s: Read %s' % \
(self.generator.applicationName, permFieldName)
if self.appyType.specificWritePermission:
self.fieldParams['write_permission'] = '%s: Write %s' % \
(self.generator.applicationName, permFieldName)
# i18n labels
i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName)
wp = self.widgetParams
wp['label'] = self.fieldName
wp['label_msgid'] = '%s' % i18nPrefix
wp['description'] = '%sDescr' % i18nPrefix
wp['description_msgid'] = '%s_descr' % i18nPrefix
wp['i18n_domain'] = self.applicationName
# Create labels for generating them in i18n files.
messages = self.generator.labels
messages.append(self.produceMessage(wp['label_msgid']))
messages.append(self.produceMessage(wp['description_msgid'],
isLabel=False))
# Create i18n messages linked to pages and phases
messages = self.generator.labels
pageMsgId = '%s_page_%s' % (self.classDescr.name, self.appyType.page)
phaseMsgId = '%s_phase_%s' % (self.classDescr.name, self.appyType.phase)
pagePoMsg = PoMessage(pageMsgId, '',
produceNiceMessage(self.appyType.page))
phasePoMsg = PoMessage(phaseMsgId, '',
produceNiceMessage(self.appyType.phase))
for poMsg in (pagePoMsg, phasePoMsg):
if poMsg not in messages:
messages.append(poMsg)
self.classDescr.labelsToPropagate.append(poMsg)
# Create i18n messages linked to groups
if self.appyType.group:
groupName, cols = GroupDescr.getGroupInfo(self.appyType.group)
msgId = '%s_group_%s' % (self.classDescr.name, groupName)
poMsg = PoMessage(msgId, '', groupName)
poMsg.produceNiceDefault()
if poMsg not in messages:
messages.append(poMsg)
self.classDescr.labelsToPropagate.append(poMsg)
# Manage schemata
if self.appyType.page != 'main':
self.fieldParams['schemata'] = self.appyType.page
# Manage things which are specific to basic types
if self.appyType.type in self.singleValuedTypes: self.walkBasicType()
# Manage things which are specific to String types
elif self.appyType.type == 'String': self.walkString()
# Manage things which are specific to Computed types
elif self.appyType.type == 'Computed': self.walkComputed()
# Manage things which are specific to Actions
elif self.appyType.type == 'Action': self.walkAction()
# Manage things which are specific to reference types
elif self.appyType.type == 'Ref': self.walkRef()
# Manage things which are specific to info types
elif self.appyType.type == 'Info': self.walkInfo()
def generate(self):
'''Produces the Archetypes field definition as a string.'''
res = ''
s = stringify
spaces = TABS
# Generate field name
res += ' '*spaces + self.fieldType + '(\n'
# Generate field parameters
spaces += TABS
for fParamName, fParamValue in self.fieldParams.iteritems():
res += ' '*spaces + fParamName + '=' + s(fParamValue) + ',\n'
# Generate widget
res += ' '*spaces + 'widget=%s(\n' % self.widgetType
spaces += TABS
for wParamName, wParamValue in self.widgetParams.iteritems():
res += ' '*spaces + wParamName + '=' + s(wParamValue) + ',\n'
# End of widget definition
spaces -= TABS
res += ' '*spaces + ')\n'
# End of field definition
spaces -= TABS
res += ' '*spaces + '),\n'
return res
class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
'''Represents an Archetypes-compliant class.'''
def __init__(self, klass, orderedAttributes, generator):
appy.gen.descriptors.ClassDescriptor.__init__(self, klass,
orderedAttributes, generator)
self.schema = '' # The archetypes schema will be generated here
self.methods = '' # Needed method definitions will be generated here
# We remember here encountered pages and groups defined in the Appy
# type. Indeed, after having parsed all application classes, we will
# need to generate i18n labels for every child class of the class
# that declared pages and groups.
self.labelsToPropagate = [] #~[PoMessage]~ Some labels (like page,
# group or action names) need to be propagated in children classes
# (because they contain the class name). But at this time we don't know
# yet every sub-class. So we store those labels here; the Generator
# will propagate them later.
self.flavourFieldsToPropagate = [] # For this class, some fields have
# been defined on the Flavour class. Those fields need to be defined
# for child classes of this class as well, but at this time we don't
# know yet every sub-class. So we store field definitions here; the
# Generator will propagate them later.
def generateSchema(self):
'''Generates the corresponding Archetypes schema in self.schema.'''
for attrName in self.orderedAttributes:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Type):
field = ArchetypeFieldDescriptor(attrName, attrValue, self)
self.schema += '\n' + field.generate()
def addSelectMethod(self, methodName, fieldDescr, isMultivalued=False):
'''For the selection field p_fieldDescr I need to generate a method
named p_methodName that will generate the vocabulary for
p_fieldDescr.'''
# Generate the method signature
m = self.methods
s = stringify
spaces = TABS
m += '\n' + ' '*spaces + 'def %s(self):\n' % methodName
spaces += TABS
appyType = fieldDescr.appyType
if type(appyType.validator) in (list, tuple):
# Generate i18n messages for every possible value
f = fieldDescr
labels = []
for value in appyType.validator:
msgLabel = '%s_%s_list_%s' % (f.classDescr.name, f.fieldName,
value)
labels.append(msgLabel) # I will need it later
poMsg = PoMessage(msgLabel, '', value)
poMsg.produceNiceDefault()
self.generator.labels.append(poMsg)
# Generate a method that returns a DisplayList
appName = self.generator.applicationName
allValues = appyType.validator
if not isMultivalued:
allValues = [''] + appyType.validator
labels.insert(0, 'choose_a_value')
m += ' '*spaces + 'return self._appy_getDisplayList' \
'(%s, %s, %s)\n' % (s(allValues), s(labels), s(appName))
self.methods = m
def addValidateMethod(self, methodName, label, fieldDescr,
specificType=None):
'''For the field p_fieldDescr I need to generate a validation method.
If p_specificType is not None, it corresponds to the name of a type
like Ref, Integer or Float, for which specific validation is needed,
beyond the potential custom validation specified by a user-defined
validator method.'''
# Generate the method signature
m = self.methods
s = stringify
spaces = TABS
m += '\n' + ' '*spaces + 'def %s(self, value):\n' % methodName
spaces += TABS
m += ' '*spaces + 'return self._appy_validateField(%s, value, %s, ' \
'%s)\n' % (s(fieldDescr.fieldName), s(label), s(specificType))
self.methods = m
def addDefaultMethod(self, methodName, fieldDescr):
'''When the default value of a field may be edited, we must add a method
that will gather the default value from the flavour.'''
m = self.methods
spaces = TABS
m += '\n' + ' '*spaces + 'def %s(self):\n' % methodName
spaces += TABS
m += ' '*spaces + 'return self.getDefaultValueFor("%s")\n' % \
fieldDescr.fieldName
self.methods = m
class ArchetypesClassDescriptor(ClassDescriptor):
'''Represents an Archetypes-compliant class that corresponds to an
application class.'''
predefined = False
def __init__(self, klass, orderedAttributes, generator):
ClassDescriptor.__init__(self, klass, orderedAttributes, generator)
if not hasattr(self, 'name'):
self.name = self.getClassName(klass)
self.generateSchema()
def getClassName(klass):
'''Generates the name of the corresponding Archetypes class.'''
return klass.__module__.replace('.', '_') + '_' + klass.__name__
getClassName = staticmethod(getClassName)
def isAbstract(self):
'''Is self.klass abstract?'''
res = False
if self.klass.__dict__.has_key('abstract'):
res = self.klass.__dict__['abstract']
return res
def isRoot(self):
'''Is self.klass root? A root class represents some kind of major
concept into the application. For example, creating instances
of such classes will be easy from the user interface.'''
res = False
if self.klass.__dict__.has_key('root'):
res = self.klass.__dict__['root']
return res
def isPod(self):
'''May this class be associated with POD templates?.'''
res = False
if self.klass.__dict__.has_key('pod') and self.klass.__dict__['pod']:
res = True
return res
def isFolder(self, klass=None):
'''Must self.klass be a folder? If klass is not None, this method tests
it on p_klass instead of self.klass.'''
res = False
theClass = self.klass
if klass:
theClass = klass
if theClass.__dict__.has_key('folder'):
res = theClass.__dict__['folder']
else:
if theClass.__bases__:
res = self.isFolder(theClass.__bases__[0])
return res
def addGenerateDocMethod(self):
m = self.methods
spaces = TABS
m += '\n' + ' '*spaces + 'def generateDocument(self):\n'
spaces += TABS
m += ' '*spaces + "'''Generates a document from p_self.'''\n"
m += ' '*spaces + 'return self._appy_generateDocument()\n'
self.methods = m
class ToolClassDescriptor(ClassDescriptor):
'''Represents the POD-specific fields that must be added to the tool.'''
predefined = True
def __init__(self, klass, generator):
ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator)
self.name = '%sTool' % generator.applicationName
def isFolder(self, klass=None): return True
def isRoot(self): return False
def addUnoValidator(self):
m = self.methods
spaces = TABS
m += '\n' + ' '*spaces + 'def validate_unoEnabledPython(self, value):\n'
spaces += TABS
m += ' '*spaces + 'return self._appy_validateUnoEnabledPython(value)\n'
self.methods = m
def generateSchema(self):
ClassDescriptor.generateSchema(self)
self.addUnoValidator()
class FlavourClassDescriptor(ClassDescriptor):
'''Represents an Archetypes-compliant class that corresponds to the Flavour
for the generated application.'''
predefined = True
def __init__(self, klass, generator):
ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator)
self.name = '%sFlavour' % generator.applicationName
self.attributesByClass = klass._appy_classes
# We don't generate the schema automatically here because we need to
# add more fields.
def isFolder(self, klass=None): return True
def isRoot(self): return False
class PodTemplateClassDescriptor(ClassDescriptor):
'''Represents a POD template.'''
predefined = True
def __init__(self, klass, generator):
ClassDescriptor.__init__(self, klass, klass._appy_attributes, generator)
self.name = '%sPodTemplate' % generator.applicationName
def isRoot(self): return False
class CustomToolClassDescriptor(ArchetypesClassDescriptor):
'''If the user defines a class that inherits from Tool, we will add those
fields to the tool.'''
predefined = False
def __init__(self, *args):
self.name = '%sTool' % args[2].applicationName
ArchetypesClassDescriptor.__init__(self, *args)
def generateSchema(self):
'''Custom tool fields may not use the variability mechanisms, ie
'optional' or 'editDefault' attributes.'''
for attrName in self.orderedAttributes:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Type):
attrValue = copy.copy(attrValue)
attrValue.optional = False
attrValue.editDefault = False
field = ArchetypeFieldDescriptor(attrName, attrValue, self)
self.schema += '\n' + field.generate()
class CustomFlavourClassDescriptor(CustomToolClassDescriptor):
def __init__(self, *args):
self.name = '%sFlavour' % args[2].applicationName
ArchetypesClassDescriptor.__init__(self, *args)
class WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor):
'''Represents a workflow.'''
# How to map Appy permissions to Plone permissions ?
appyToPlonePermissions = {
'read': ('View', 'Access contents information'),
'write': ('Modify portal content',),
'delete': ('Delete objects',),
}
def getPlonePermissions(self, permission):
'''Returns the Plone permission(s) that correspond to
Appy p_permission.'''
if self.appyToPlonePermissions.has_key(permission):
res = self.appyToPlonePermissions[permission]
elif isinstance(permission, basestring):
res = [permission]
else:
# Permission if an Appy permission declaration
className, fieldName = permission.fieldDescriptor.rsplit('.', 1)
if className.find('.') == -1:
# The related class resides in the same module as the workflow
fullClassName = '%s_%s' % (
self.klass.__module__.replace('.', '_'), className)
else:
# className contains the full package name of the class
fullClassName = className.replace('.', '_')
# Read or Write ?
if permission.__class__.__name__ == 'ReadPermission':
access = 'Read'
else:
access = 'Write'
permName = '%s: %s %s %s' % (self.generator.applicationName,
access, fullClassName, fieldName)
res = [permName]
return res
def getWorkflowName(klass):
'''Generates the name of the corresponding Archetypes workflow.'''
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
return res.lower()
getWorkflowName = staticmethod(getWorkflowName)
def getStatesInfo(self, asDumpableCode=False):
'''Gets, in a dict, information for configuring states of the workflow.
If p_asDumpableCode is True, instead of returning a dict, this
method will return a string containing the dict that can be dumped
into a Python code file.'''
res = {}
transitions = self.getTransitions()
for state in self.getStates():
stateName = self.getNameOf(state)
# We need the list of transitions that start from this state
outTransitions = state.getTransitions(transitions,
selfIsFromState=True)
tNames = self.getTransitionNames(outTransitions,
limitToFromState=state)
# Compute the permissions/roles mapping for this state
permissionsMapping = {}
for permission, roles in state.getPermissions().iteritems():
for plonePerm in self.getPlonePermissions(permission):
permissionsMapping[plonePerm] = roles
# Add 'Review portal content' to anyone; this is not a security
# problem because we limit the triggering of every transition
# individually.
allRoles = self.generator.getAllUsedRoles()
if 'Manager' not in allRoles: allRoles.append('Manager')
permissionsMapping['Review portal content'] = allRoles
res[stateName] = (tNames, permissionsMapping)
if not asDumpableCode:
return res
# We must create the "Python code" version of this dict
newRes = '{'
for stateName, stateInfo in res.iteritems():
transitions = ','.join(['"%s"' % tn for tn in stateInfo[0]])
# Compute permissions
permissions = ''
for perm, roles in stateInfo[1].iteritems():
theRoles = ','.join(['"%s"' % r for r in roles])
permissions += '"%s": [%s],' % (perm, theRoles)
newRes += '\n "%s": ([%s], {%s}),' % \
(stateName, transitions, permissions)
return newRes + '}'
def getTransitionsInfo(self, asDumpableCode=False):
'''Gets, in a dict, information for configuring transitions of the
workflow. If p_asDumpableCode is True, instead of returning a dict,
this method will return a string containing the dict that can be
dumped into a Python code file.'''
res = {}
for tName in self.getTransitionNames():
res[tName] = self.getEndStateName(tName)
if not asDumpableCode:
return res
# We must create the "Python code" version of this dict
newRes = '{'
for transitionName, endStateName in res.iteritems():
newRes += '\n "%s": "%s",' % (transitionName, endStateName)
return newRes + '}'
def getManagedPermissions(self):
'''Returns the Plone permissions of all Appy permissions managed by this
workflow.'''
res = set()
res.add('Review portal content')
for state in self.getStates():
for permission in state.permissions.iterkeys():
for plonePerm in self.getPlonePermissions(permission):
res.add(plonePerm)
return res
def getScripts(self):
res = ''
wfName = WorkflowDescriptor.getWorkflowName(self.klass)
for tName in self.getTransitionNames():
scriptName = '%s_do%s%s' % (wfName, tName[0].upper(), tName[1:])
res += 'def %s(self, stateChange, **kw): do("%s", ' \
'stateChange, logger)\n' % (scriptName, tName)
return res
# ------------------------------------------------------------------------------

712
gen/plone25/generator.py Executable file
View file

@ -0,0 +1,712 @@
'''This file contains the main Generator class used for generating a
Plone 2.5-compliant product.'''
# ------------------------------------------------------------------------------
import os, os.path, re, sys
import appy.gen
from appy.gen import *
from appy.gen.po import PoMessage, PoFile, PoParser
from appy.gen.generator import Generator as AbstractGenerator
from model import ModelClass, PodTemplate, Flavour, Tool
from descriptors import ArchetypeFieldDescriptor, ArchetypesClassDescriptor, \
WorkflowDescriptor, ToolClassDescriptor, \
FlavourClassDescriptor, PodTemplateClassDescriptor, \
CustomToolClassDescriptor, CustomFlavourClassDescriptor
# Common methods that need to be defined on every Archetype class --------------
COMMON_METHODS = '''
def at_post_create_script(self): self._appy_onEdit(True)
def at_post_edit_script(self): self._appy_onEdit(False)
def post_validate(self, REQUEST=None, errors=None):
if not errors: self._appy_validateAllFields(REQUEST, errors)
def getTool(self): return self.%s
def getProductConfig(self): return Products.%s.config
'''
# ------------------------------------------------------------------------------
class Generator(AbstractGenerator):
'''This generator generates a Plone 2.5-compliant product from a given
appy application.'''
poExtensions = ('.po', '.pot')
def __init__(self, *args, **kwargs):
Flavour._appy_clean()
AbstractGenerator.__init__(self, *args, **kwargs)
# i18n labels to generate
self.labels = [] # i18n labels
self.toolName = '%sTool' % self.applicationName
self.flavourName = '%sFlavour' % self.applicationName
self.toolInstanceName = 'portal_%s' % self.applicationName.lower()
self.podTemplateName = '%sPodTemplate' % self.applicationName
self.portletName = '%s_portlet' % self.applicationName.lower()
self.queryName = '%s_query' % self.applicationName.lower()
self.skinsFolder = 'skins/%s' % self.applicationName
# The following dict, pre-filled in the abstract generator, contains a
# series of replacements that need to be applied to file templates to
# generate files.
commonMethods = COMMON_METHODS % \
(self.toolInstanceName, self.applicationName)
self.repls.update(
{'toolName': self.toolName, 'flavourName': self.flavourName,
'portletName': self.portletName, 'queryName': self.queryName,
'toolInstanceName': self.toolInstanceName,
'podTemplateName': self.podTemplateName,
'macros': '%s_macros' % self.applicationName.lower(),
'commonMethods': commonMethods})
# Predefined class descriptors
self.toolDescr = ToolClassDescriptor(Tool, self)
self.flavourDescr = FlavourClassDescriptor(Flavour, self)
self.podTemplateDescr = PodTemplateClassDescriptor(PodTemplate,self)
self.referers = {}
versionRex = re.compile('(.*?\s+build)\s+(\d+)')
def initialize(self):
# Use customized class descriptors
self.classDescriptor = ArchetypesClassDescriptor
self.workflowDescriptor = WorkflowDescriptor
self.customToolClassDescriptor = CustomToolClassDescriptor
self.customFlavourClassDescriptor = CustomFlavourClassDescriptor
# Determine version number of the Plone product
self.version = '0.1 build 1'
versionTxt = os.path.join(self.outputFolder, 'version.txt')
if os.path.exists(versionTxt):
f = file(versionTxt)
oldVersion = f.read().strip()
f.close()
res = self.versionRex.search(oldVersion)
self.version = res.group(1) + ' ' + str(int(res.group(2))+1)
# Existing i18n files
self.i18nFiles = {} #~{p_fileName: PoFile}~
# Retrieve existing i18n files if any
i18nFolder = os.path.join(self.outputFolder, 'i18n')
if os.path.exists(i18nFolder):
for fileName in os.listdir(i18nFolder):
name, ext = os.path.splitext(fileName)
if ext in self.poExtensions:
poParser = PoParser(os.path.join(i18nFolder, fileName))
self.i18nFiles[fileName] = poParser.parse()
def finalize(self):
# Some useful aliases
msg = PoMessage
app = self.applicationName
# Some global i18n messages
poMsg = msg(app, '', app); poMsg.produceNiceDefault()
self.labels += [poMsg,
msg('workflow_state', '', msg.WORKFLOW_STATE),
msg('phase', '', msg.PHASE),
msg('root_type', '', msg.ROOT_TYPE),
msg('workflow_comment', '', msg.WORKFLOW_COMMENT),
msg('choose_a_value', '', msg.CHOOSE_A_VALUE),
msg('choose_a_doc', '', msg.CHOOSE_A_DOC),
msg('min_ref_violated', '', msg.MIN_REF_VIOLATED),
msg('max_ref_violated', '', msg.MAX_REF_VIOLATED),
msg('no_ref', '', msg.REF_NO),
msg('add_ref', '', msg.REF_ADD),
msg('ref_name', '', msg.REF_NAME),
msg('ref_actions', '', msg.REF_ACTIONS),
msg('move_up', '', msg.REF_MOVE_UP),
msg('move_down', '', msg.REF_MOVE_DOWN),
msg('query_create', '', msg.QUERY_CREATE),
msg('query_no_result', '', msg.QUERY_NO_RESULT),
msg('query_consult_all', '', msg.QUERY_CONSULT_ALL),
msg('ref_invalid_index', '', msg.REF_INVALID_INDEX),
msg('bad_int', '', msg.BAD_INT),
msg('bad_float', '', msg.BAD_FLOAT),
msg('bad_email', '', msg.BAD_EMAIL),
msg('bad_url', '', msg.BAD_URL),
msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC),
]
# Create basic files (config.py, Install.py, etc)
self.generateAppyReference()
self.generateTool()
self.generateConfig()
self.generateInit()
self.generateInstall()
self.generateWorkflows()
self.generateWrappers()
self.generatePortlet()
if self.config.frontPage == True:
self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT))
self.copyFile('frontPage.pt', self.repls,
destFolder=self.skinsFolder,
destName='%sFrontPage.pt' % self.applicationName)
self.copyFile('configure.zcml', self.repls)
self.copyFile('import_steps.xml', self.repls,
destFolder='profiles/default')
self.copyFile('ProfileInit.py', self.repls, destFolder='profiles',
destName='__init__.py')
self.copyFile('tool.gif', {})
self.copyFile('Macros.pt', self.repls, destFolder=self.skinsFolder,
destName='%s_macros.pt' % self.applicationName.lower())
self.copyFile('appy_view.pt', self.repls, destFolder=self.skinsFolder,
destName='%s_appy_view.pt' % self.applicationName)
self.copyFile('appy_edit.cpt', self.repls, destFolder=self.skinsFolder,
destName='%s_appy_edit.cpt' % self.applicationName)
self.copyFile('appy_edit.cpt.metadata', self.repls,
destFolder=self.skinsFolder,
destName='%s_appy_edit.cpt.metadata'%self.applicationName)
self.copyFile('Styles.css.dtml', self.repls, destFolder=self.skinsFolder,
destName = '%s.css.dtml' % self.applicationName)
self.copyFile('do.py', self.repls, destFolder=self.skinsFolder,
destName='%s_do.py' % self.applicationName)
self.copyFile('colophon.pt', self.repls, destFolder=self.skinsFolder)
self.copyFile('footer.pt', self.repls, destFolder=self.skinsFolder)
# Create version.txt
f = open(os.path.join(self.outputFolder, 'version.txt'), 'w')
f.write(self.version)
f.close()
# Make Extensions a Python package
for moduleFolder in ('Extensions',):
initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder)
if not os.path.isfile(initFile):
f = open(initFile, 'w')
f.write('')
f.close()
# Decline i18n labels into versions for child classes
for classDescr in self.classes:
for poMsg in classDescr.labelsToPropagate:
for childDescr in classDescr.getChildren():
childMsg = poMsg.clone(classDescr.name, childDescr.name)
if childMsg not in self.labels:
self.labels.append(childMsg)
# Generate i18n pot file
potFileName = '%s.pot' % self.applicationName
if self.i18nFiles.has_key(potFileName):
potFile = self.i18nFiles[potFileName]
else:
fullName = os.path.join(self.outputFolder, 'i18n/%s' % potFileName)
potFile = PoFile(fullName)
self.i18nFiles[potFileName] = potFile
removedLabels = potFile.update(self.labels, self.options.i18nClean,
not self.options.i18nSort)
if removedLabels:
print 'Warning: %d messages were removed from translation ' \
'files: %s' % (len(removedLabels), str(removedLabels))
potFile.generate()
# Generate i18n po files
for language in self.config.languages:
# I must generate (or update) a po file for the language(s)
# specified in the configuration.
poFileName = potFile.getPoFileName(language)
if self.i18nFiles.has_key(poFileName):
poFile = self.i18nFiles[poFileName]
else:
fullName = os.path.join(self.outputFolder,
'i18n/%s' % poFileName)
poFile = PoFile(fullName)
self.i18nFiles[poFileName] = poFile
poFile.update(potFile.messages, self.options.i18nClean,
not self.options.i18nSort)
poFile.generate()
# Generate i18n po files for other potential files
for poFile in self.i18nFiles.itervalues():
if not poFile.generated:
poFile.generate()
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer')
def getAllUsedRoles(self, appOnly=False):
'''Produces a list of all the roles used within all workflows defined
in this application. If p_appOnly is True, it returns only roles
which are specific to this application (ie it removes predefined
Plone roles like Member, Manager, etc.'''
res = []
for wfDescr in self.workflows:
# Browse states and transitions
for attr in dir(wfDescr.klass):
attrValue = getattr(wfDescr.klass, attr)
if isinstance(attrValue, State) or \
isinstance(attrValue, Transition):
res += attrValue.getUsedRoles()
res = list(set(res))
if appOnly:
for ploneRole in self.ploneRoles:
if ploneRole in res:
res.remove(ploneRole)
return res
def addReferer(self, fieldDescr, relationship):
'''p_fieldDescr is a Ref type definition. We will create in config.py a
dict that lists all back references, by type.'''
k = fieldDescr.appyType.klass
if issubclass(k, ModelClass):
refClassName = self.applicationName + k.__name__
elif issubclass(k, appy.gen.Tool):
refClassName = '%sTool' % self.applicationName
elif issubclass(k, appy.gen.Flavour):
refClassName = '%sFlavour' % self.applicationName
else:
refClassName = ArchetypesClassDescriptor.getClassName(k)
if not self.referers.has_key(refClassName):
self.referers[refClassName] = []
self.referers[refClassName].append( (fieldDescr, relationship))
def generatePortlet(self):
rootClasses = ''
for classDescr in self.classes:
if classDescr.isRoot():
rootClasses += "'%s'," % classDescr.name
repls = self.repls.copy()
repls['rootClasses'] = rootClasses
self.copyFile('Portlet.pt', repls, destName='%s.pt' % self.portletName,
destFolder=self.skinsFolder)
self.copyFile('Query.pt', repls, destName='%s.pt' % self.queryName,
destFolder=self.skinsFolder)
def generateConfig(self):
# Compute referers
referers = ''
for className, refInfo in self.referers.iteritems():
referers += '"%s":[' % className
for fieldDescr, relationship in refInfo:
refClass = fieldDescr.classDescr.klass
if issubclass(refClass, ModelClass):
refClassName = 'Extensions.appyWrappers.%s' % \
refClass.__name__
else:
refClassName = '%s.%s' % (refClass.__module__,
refClass.__name__)
referers += '(%s.%s' % (refClassName, fieldDescr.fieldName)
referers += ',"%s"' % relationship
referers += '),'
referers += '],\n'
# Compute workflow instances initialisation
wfInit = ''
for workflowDescr in self.workflows:
k = workflowDescr.klass
className = '%s.%s' % (k.__module__, k.__name__)
wfInit += 'wf = %s()\n' % className
wfInit += 'wf._transitionsMapping = {}\n'
for transition in workflowDescr.getTransitions():
tName = workflowDescr.getNameOf(transition)
tNames = workflowDescr.getTransitionNamesOf(tName, transition)
for trName in tNames:
wfInit += 'wf._transitionsMapping["%s"] = wf.%s\n' % \
(trName, tName)
# We need a new attribute that stores states in order
wfInit += 'wf._states = []\n'
for stateName in workflowDescr.getStateNames(ordered=True):
wfInit += 'wf._states.append("%s")\n' % stateName
wfInit += 'workflowInstances[%s] = wf\n' % className
# Compute imports
imports = ['import %s' % self.applicationName]
classDescrs = self.classes[:]
if self.customToolDescr:
classDescrs.append(self.customToolDescr)
if self.customFlavourDescr:
classDescrs.append(self.customFlavourDescr)
for classDescr in (classDescrs + self.workflows):
theImport = 'import %s' % classDescr.klass.__module__
if theImport not in imports:
imports.append(theImport)
# Compute list of add permissions
addPermissions = ''
for classDescr in self.classes:
addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name,
self.applicationName, classDescr.name)
repls = self.repls.copy()
# Compute list of used roles for registering them if needed
repls['roles'] = ','.join(['"%s"' % r for r in \
self.getAllUsedRoles(appOnly=True)])
repls['referers'] = referers
repls['workflowInstancesInit'] = wfInit
repls['imports'] = '\n'.join(imports)
repls['defaultAddRoles'] = ','.join(
['"%s"' % r for r in self.config.defaultCreators])
repls['addPermissions'] = addPermissions
self.copyFile('config.py', repls)
def generateInit(self):
# Compute imports
imports = [' import %s' % self.toolName,
' import %s' % self.flavourName,
' import %s' % self.podTemplateName]
for c in self.classes:
importDef = ' import %s' % c.name
if importDef not in imports:
imports.append(importDef)
repls = self.repls.copy()
repls['imports'] = '\n'.join(imports)
self.copyFile('__init__.py', repls)
def generateInstall(self):
# Compute lists of class names
allClassNames = '"%s",' % self.flavourName
allClassNames += '"%s",' % self.podTemplateName
appClassNames = ','.join(['"%s"' % c.name for c in self.classes])
allClassNames += appClassNames
# Compute imports
imports = []
for classDescr in self.classes:
theImport = 'import %s' % classDescr.klass.__module__
if theImport not in imports:
imports.append(theImport)
# Compute list of application classes
appClasses = []
for classDescr in self.classes:
k = classDescr.klass
appClasses.append('%s.%s' % (k.__module__, k.__name__))
# Compute classes whose instances must not be catalogued.
catalogMap = ''
blackClasses = [self.toolName, self.flavourName, self.podTemplateName]
for blackClass in blackClasses:
catalogMap += "catalogMap['%s'] = {}\n" % blackClass
catalogMap += "catalogMap['%s']['black'] = " \
"['portal_catalog']\n" % blackClass
# Compute workflows
workflows = ''
for classDescr in self.classes:
if hasattr(classDescr.klass, 'workflow'):
wfName = WorkflowDescriptor.getWorkflowName(
classDescr.klass.workflow)
className = ArchetypesClassDescriptor.getClassName(
classDescr.klass)
workflows += '\n "%s":"%s",' % (className, wfName)
# Generate the resulting file.
repls = self.repls.copy()
repls['allClassNames'] = allClassNames
repls['appClassNames'] = appClassNames
repls['catalogMap'] = catalogMap
repls['imports'] = '\n'.join(imports)
repls['appClasses'] = "[%s]" % ','.join(appClasses)
repls['minimalistPlone'] = self.config.minimalistPlone
repls['appFrontPage'] = self.config.frontPage == True
repls['workflows'] = workflows
self.copyFile('Install.py', repls, destFolder='Extensions')
def generateWorkflows(self):
'''Generates the file that contains one function by workflow.
Those functions are called by Plone for registering the workflows.'''
workflows = ''
for wfDescr in self.workflows:
# Compute state names & info, transition names & infos, managed
# permissions
stateNames=','.join(['"%s"' % sn for sn in wfDescr.getStateNames()])
stateInfos = wfDescr.getStatesInfo(asDumpableCode=True)
transitionNames = ','.join(['"%s"' % tn for tn in \
wfDescr.getTransitionNames()])
transitionInfos = wfDescr.getTransitionsInfo(asDumpableCode=True)
managedPermissions = ','.join(['"%s"' % tn for tn in \
wfDescr.getManagedPermissions()])
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
workflows += '%s\ndef create_%s(self, id):\n ' \
'stateNames = [%s]\n ' \
'stateInfos = %s\n ' \
'transitionNames = [%s]\n ' \
'transitionInfos = %s\n ' \
'managedPermissions = [%s]\n ' \
'return WorkflowCreator("%s", DCWorkflowDefinition, ' \
'stateNames, "%s", stateInfos, transitionNames, ' \
'transitionInfos, managedPermissions, PROJECTNAME, ' \
'ExternalMethod).run()\n' \
'addWorkflowFactory(create_%s,\n id="%s",\n ' \
'title="%s")\n\n' % (wfDescr.getScripts(), wfName, stateNames,
stateInfos, transitionNames, transitionInfos,
managedPermissions, wfName, wfDescr.getInitialStateName(),
wfName, wfName, wfName)
repls = self.repls.copy()
repls['workflows'] = workflows
self.copyFile('workflows.py', repls, destFolder='Extensions')
def generateWrapperProperty(self, attrName, appyType):
# Generate getter
res = ' def get_%s(self):\n' % attrName
blanks = ' '*8
if isinstance(appyType, Ref):
res += blanks + 'return self.o._appy_getRefs("%s", ' \
'noListIfSingleObj=True)\n' % attrName
elif isinstance(appyType, Computed):
res += blanks + 'appyType = getattr(self.klass, "%s")\n' % attrName
res += blanks + 'return self.o.getComputedValue(' \
'appyType.__dict__)\n'
else:
getterName = 'get%s%s' % (attrName[0].upper(), attrName[1:])
if attrName in ArchetypeFieldDescriptor.specialParams:
getterName = attrName.capitalize()
res += blanks + 'return self.o.%s()\n' % getterName
res += ' %s = property(get_%s)\n\n' % (attrName, attrName)
return res
def generateWrapperPropertyBack(self, attrName, rel):
'''Generates a wrapper property for accessing the back reference named
p_attrName through Archetypes relationship p_rel.'''
res = ' def get_%s(self):\n' % attrName
blanks = ' '*8
res += blanks + 'return self.o._appy_getRefsBack("%s", "%s", ' \
'noListIfSingleObj=True)\n' % (attrName, rel)
res += ' %s = property(get_%s)\n\n' % (attrName, attrName)
return res
def getClassesInOrder(self, allClasses):
'''When generating wrappers, classes mut be dumped in order (else, it
generates forward references in the Python file, that does not
compile).'''
res = [] # Appy class descriptors
resClasses = [] # Corresponding real Python classes
for classDescr in allClasses:
klass = classDescr.klass
if not klass.__bases__ or \
(klass.__bases__[0].__name__ == 'ModelClass'):
# This is a root class. We dump it at the begin of the file.
res.insert(0, classDescr)
resClasses.insert(0, klass)
else:
# If a child of this class is already present, we must insert
# this klass before it.
lowestChildIndex = sys.maxint
for resClass in resClasses:
if klass in resClass.__bases__:
lowestChildIndex = min(lowestChildIndex,
resClasses.index(resClass))
if lowestChildIndex != sys.maxint:
res.insert(lowestChildIndex, classDescr)
resClasses.insert(lowestChildIndex, klass)
else:
res.append(classDescr)
resClasses.append(klass)
return res
def generateWrappers(self):
# We must generate imports and wrapper definitions
imports = []
wrappers = []
allClasses = self.classes[:]
# Add predefined classes (Tool, Flavour, PodTemplate)
allClasses += [self.toolDescr, self.flavourDescr, self.podTemplateDescr]
if self.customToolDescr:
allClasses.append(self.customToolDescr)
if self.customFlavourDescr:
allClasses.append(self.customFlavourDescr)
for c in self.getClassesInOrder(allClasses):
if not c.predefined:
moduleImport = 'import %s' % c.klass.__module__
if moduleImport not in imports:
imports.append(moduleImport)
# Determine parent wrapper and class
parentWrapper = 'AbstractWrapper'
parentClass = '%s.%s' % (c.klass.__module__, c.klass.__name__)
if c.predefined:
parentClass = c.klass.__name__
if c.klass.__bases__:
baseClassName = c.klass.__bases__[0].__name__
for k in allClasses:
if k.klass.__name__ == baseClassName:
parentWrapper = '%s_Wrapper' % k.name
wrapperDef = 'class %s_Wrapper(%s, %s):\n' % \
(c.name, parentWrapper, parentClass)
titleFound = False
for attrName in c.orderedAttributes:
if attrName == 'title':
titleFound = True
attrValue = getattr(c.klass, attrName)
if isinstance(attrValue, Type):
wrapperDef += self.generateWrapperProperty(attrName,
attrValue)
# Generate properties for back references
if self.referers.has_key(c.name):
for refDescr, rel in self.referers[c.name]:
attrName = refDescr.appyType.back.attribute
wrapperDef += self.generateWrapperPropertyBack(attrName,rel)
if not titleFound:
# Implicitly, the title will be added by Archetypes. So I need
# to define a property for it.
wrapperDef += self.generateWrapperProperty('title', String())
wrappers.append(wrapperDef)
repls = self.repls.copy()
repls['imports'] = '\n'.join(imports)
repls['wrappers'] = '\n'.join(wrappers)
repls['toolBody'] = Tool._appy_getBody()
repls['flavourBody'] = Flavour._appy_getBody()
repls['podTemplateBody'] = PodTemplate._appy_getBody()
self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
def generateTool(self):
'''Generates the Plone tool that corresponds to this application.'''
# Generate the tool class in itself and related i18n messages
t = self.toolName
Msg = PoMessage
repls = self.repls.copy()
# Manage predefined fields
Tool.flavours.klass = Flavour
if self.customFlavourDescr:
Tool.flavours.klass = self.customFlavourDescr.klass
self.toolDescr.generateSchema()
repls['predefinedFields'] = self.toolDescr.schema
repls['predefinedMethods'] = self.toolDescr.methods
# Manage custom fields
repls['fields'] = ''
repls['methods'] = ''
repls['wrapperClass'] = '%s_Wrapper' % self.toolDescr.name
if self.customToolDescr:
repls['fields'] = self.customToolDescr.schema
repls['methods'] = self.customToolDescr.methods
wrapperClass = '%s_Wrapper' % self.customToolDescr.name
repls['wrapperClass'] = wrapperClass
self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName)
repls = self.repls.copy()
# Create i18n-related messages
self.labels += [
Msg(self.toolName, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.toolName, '', ' ')]
# Before generating the Flavour class, finalize it with query result
# columns, with fields to propagate, workflow-related fields.
for classDescr in self.classes:
for fieldName, fieldType in classDescr.flavourFieldsToPropagate:
for childDescr in classDescr.getChildren():
childFieldName = fieldName % childDescr.name
fieldType.group = childDescr.klass.__name__
Flavour._appy_addField(childFieldName,fieldType,childDescr)
if classDescr.isRoot():
# We must be able to configure query results from the
# flavour.
Flavour._appy_addQueryResultColumns(classDescr)
Flavour._appy_addWorkflowFields(self.flavourDescr)
Flavour._appy_addWorkflowFields(self.podTemplateDescr)
# Generate the flavour class and related i18n messages
self.flavourDescr.generateSchema()
self.labels += [ Msg(self.flavourName, '', Msg.FLAVOUR),
Msg('%s_edit_descr' % self.flavourName, '', ' ')]
repls = self.repls.copy()
repls['predefinedFields'] = self.flavourDescr.schema
repls['predefinedMethods'] = self.flavourDescr.methods
# Manage custom fields
repls['fields'] = ''
repls['methods'] = ''
repls['wrapperClass'] = '%s_Wrapper' % self.flavourDescr.name
if self.customFlavourDescr:
repls['fields'] = self.customFlavourDescr.schema
repls['methods'] = self.customFlavourDescr.methods
wrapperClass = '%s_Wrapper' % self.customFlavourDescr.name
repls['wrapperClass'] = wrapperClass
repls['metaTypes'] = [c.name for c in self.classes]
self.copyFile('FlavourTemplate.py', repls,
destName='%s.py'% self.flavourName)
# Generate the PodTemplate class
self.podTemplateDescr.generateSchema()
self.labels += [ Msg(self.podTemplateName, '', Msg.POD_TEMPLATE),
Msg('%s_edit_descr' % self.podTemplateName, '', ' ')]
repls = self.repls.copy()
repls['fields'] = self.podTemplateDescr.schema
repls['methods'] = self.podTemplateDescr.methods
repls['wrapperClass'] = '%s_Wrapper' % self.podTemplateDescr.name
self.copyFile('PodTemplate.py', repls,
destName='%s.py' % self.podTemplateName)
for imgName in PodTemplate.podFormat.validator:
self.copyFile('%s.png' % imgName, {},
destFolder=self.skinsFolder)
refFiles = ('createAppyObject.cpy', 'createAppyObject.cpy.metadata',
'arrowUp.png', 'arrowDown.png', 'plus.png', 'appyConfig.gif',
'nextPhase.png', 'nextState.png', 'done.png', 'current.png')
prefixedRefFiles = ('AppyReference.pt',)
def generateAppyReference(self):
'''Generates what is needed to use Appy-specific references.'''
# Some i18n messages
Msg = PoMessage
for refFile in self.prefixedRefFiles:
self.copyFile(refFile, self.repls, destFolder=self.skinsFolder,
destName='%s%s' % (self.applicationName, refFile))
for refFile in self.refFiles:
self.copyFile(refFile, self.repls, destFolder=self.skinsFolder)
def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for
generating the corresponding Archetype class and schema.'''
k = classDescr.klass
print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__)
# Add, for this class, the needed configuration attributes on Flavour
if classDescr.isPod():
Flavour._appy_addPodField(classDescr)
if not classDescr.isAbstract():
Flavour._appy_addWorkflowFields(classDescr)
# Determine base archetypes schema and class
baseClass = 'BaseContent'
baseSchema = 'BaseSchema'
if classDescr.isFolder():
baseClass = 'OrderedBaseFolder'
baseSchema = 'OrderedBaseFolderSchema'
parents = [baseClass, 'ClassMixin']
imports = []
implements = [baseClass]
for baseClass in classDescr.klass.__bases__:
if self.determineAppyType(baseClass) == 'class':
bcName = ArchetypesClassDescriptor.getClassName(baseClass)
parents.remove('ClassMixin')
parents.append(bcName)
implements.append(bcName)
imports.append('from %s import %s' % (bcName, bcName))
baseSchema = '%s.schema' % bcName
break
parents = ','.join(parents)
implements = '+'.join(['(getattr(%s,"__implements__",()),)' % i \
for i in implements])
classDoc = classDescr.klass.__doc__
if not classDoc:
classDoc = 'Class generated with appy.gen.'
# If the class is abstract I will not register it
register = "registerType(%s, '%s')" % (classDescr.name,
self.applicationName)
if classDescr.isAbstract():
register = ''
classDescr.addGenerateDocMethod() # For POD
repls = self.repls.copy()
repls.update({
'imports': '\n'.join(imports), 'parents': parents,
'className': classDescr.klass.__name__,
'genClassName': classDescr.name,
'classDoc': classDoc, 'applicationName': self.applicationName,
'fields': classDescr.schema, 'methods': classDescr.methods,
'implements': implements, 'baseSchema': baseSchema,
'register': register, 'toolInstanceName': self.toolInstanceName})
fileName = '%s.py' % classDescr.name
# Remember i18n labels that will be generated in the i18n file
poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ')
self.labels.append(poMsgDescr)
# Remember i18n labels for flavoured variants
for i in range(2,10):
poMsg = PoMessage('%s_%d' % (classDescr.name, i), '',
classDescr.klass.__name__)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i),
'', ' ')
self.labels.append(poMsgDescr)
# Generate the resulting Archetypes class and schema.
self.copyFile('ArchetypesTemplate.py', repls, destName=fileName)
def generateWorkflow(self, wfDescr):
'''This method does not generate the workflow definition, which is done
in self.generateWorkflows. This method just creates the i18n labels
related to the workflow described by p_wfDescr.'''
k = wfDescr.klass
print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)
# Identify Plone workflow name
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
# Add i18n messages for states and transitions
for sName in wfDescr.getStateNames():
poMsg = PoMessage('%s_%s' % (wfName, sName), '', sName)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
for tName, tLabel in wfDescr.getTransitionNames(withLabels=True):
poMsg = PoMessage('%s_%s' % (wfName, tName), '', tLabel)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
for transition in wfDescr.getTransitions():
if transition.notify:
# Appy will send a mail when this transition is triggered.
# So we need 2 i18n labels for every DC transition corresponding
# to this Appy transition: one for the mail subject and one for
# the mail body.
tName = wfDescr.getNameOf(transition) # Appy name
tNames = wfDescr.getTransitionNamesOf(tName, transition) # DC
# name(s)
for tn in tNames:
subjectLabel = '%s_%s_mail_subject' % (wfName, tn)
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
self.labels.append(poMsg)
bodyLabel = '%s_%s_mail_body' % (wfName, tn)
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
self.labels.append(poMsg)
# ------------------------------------------------------------------------------

496
gen/plone25/installer.py Normal file
View file

@ -0,0 +1,496 @@
'''This package contains stuff used at run-time for installing a generated
Plone product.'''
# ------------------------------------------------------------------------------
import os, os.path
from StringIO import StringIO
from sets import Set
from appy.gen.utils import produceNiceMessage
from appy.gen.plone25.utils import updateRolesForPermission
class PloneInstaller:
'''This Plone installer runs every time the generated Plone product is
installed or uninstalled (in the Plone configuration interface).'''
def __init__(self, reinstall, productName, ploneSite, minimalistPlone,
appClasses, appClassNames, allClassNames, catalogMap, applicationRoles,
defaultAddRoles, workflows, appFrontPage, ploneStuff):
self.reinstall = reinstall # Is it a fresh install or a re-install?
self.productName = productName
self.ploneSite = ploneSite
self.minimalistPlone = minimalistPlone # If True, lots of basic Plone
# stuff will be hidden.
self.appClasses = appClasses # The list of classes declared in the
# gen-application.
self.appClassNames = appClassNames # Names of those classes
self.allClassNames = allClassNames # Includes Flavour and PodTemplate
self.catalogMap = catalogMap # Indicates classes to be indexed or not
self.applicationRoles = applicationRoles # Roles defined in the app
self.defaultAddRoles = defaultAddRoles # The default roles that can add
# content
self.workflows = workflows # Dict whose keys are class names and whose
# values are workflow names (=the workflow
# used by the content type)
self.appFrontPage = appFrontPage # Does this app define a site-wide
# front page?
self.ploneStuff = ploneStuff # A dict of some Plone functions or vars
self.toLog = StringIO()
self.typeAliases = {'sharing': '', 'gethtml': '',
'(Default)': '%s_appy_view' % self.productName,
'edit': '%s_appy_edit' % self.productName,
'index.html': '', 'properties': '', 'view': ''}
self.tool = None # The Plone version of the application tool
self.appyTool = None # The Appy version of the application tool
self.toolName = '%sTool' % self.productName
self.toolInstanceName = 'portal_%s' % self.productName.lower()
actionsToHide = {
'portal_actions': ('sitemap', 'accessibility', 'change_state','sendto'),
'portal_membership': ('mystuff', 'preferences'),
'portal_undo': ('undo',)
}
def customizePlone(self):
'''Hides some UI elements that appear by default in Plone.'''
for portalName, toHide in self.actionsToHide.iteritems():
portal = getattr(self.ploneSite, portalName)
portalActions = portal.listActions()
for action in portalActions:
if action.id in toHide: action.visible = False
appyFolderType = 'AppyFolder'
def registerAppyFolderType(self):
'''We need a specific content type for the folder that will hold all
objects created from this application, in order to remove it from
Plone navigation settings. We will create a new content type based
on Large Plone Folder.'''
if not hasattr(self.ploneSite.portal_types, self.appyFolderType):
portal_types = self.ploneSite.portal_types
lpf = 'Large Plone Folder'
largePloneFolder = getattr(portal_types, lpf)
typeInfoName = 'ATContentTypes: ATBTreeFolder (ATBTreeFolder)'
portal_types.manage_addTypeInformation(
largePloneFolder.meta_type, id=self.appyFolderType,
typeinfo_name=typeInfoName)
appyFolder = getattr(portal_types, self.appyFolderType)
appyFolder.title = 'Appy folder'
#appyFolder.factory = largePloneFolder.factory
#appyFolder.product = largePloneFolder.product
# Copy actions and aliases
appyFolder._actions = tuple(largePloneFolder._cloneActions())
# Copy aliases from the base portal type
appyFolder.setMethodAliases(largePloneFolder.getMethodAliases())
# Prevent Appy folders to be visible in standard Plone navigation
nv = self.ploneSite.portal_properties.navtree_properties
metaTypesNotToList = list(nv.getProperty('metaTypesNotToList'))
if self.appyFolderType not in metaTypesNotToList:
metaTypesNotToList.append(self.appyFolderType)
nv.manage_changeProperties(
metaTypesNotToList=tuple(metaTypesNotToList))
def getAddPermission(self, className):
'''What is the name of the permission allowing to create instances of
class whose name is p_className?'''
return self.productName + ': Add ' + className
def installRootFolder(self):
'''Creates and/or configures, at the root of the Plone site and if
needed, the folder where the application will store instances of
root classes.'''
# Register first our own Appy folder type if needed.
site = self.ploneSite
if not hasattr(site.portal_types, self.appyFolderType):
self.registerAppyFolderType()
# Create the folder
if not hasattr(site.aq_base, self.productName):
# Temporarily allow me to create Appy large plone folders
getattr(site.portal_types, self.appyFolderType).global_allow = 1
site.invokeFactory(self.appyFolderType, self.productName,
title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0
appFolder = getattr(site, self.productName)
# All roles defined as creators should be able to create the
# corresponding root content types in this folder.
i = -1
allCreators = set()
for klass in self.appClasses:
i += 1
if klass.__dict__.has_key('root') and klass.__dict__['root']:
# It is a root class.
creators = getattr(klass, 'creators', None)
if not creators: creators = self.defaultAddRoles
allCreators = allCreators.union(creators)
className = self.appClassNames[i]
updateRolesForPermission(self.getAddPermission(className),
tuple(creators), appFolder)
# Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content".
updateRolesForPermission('Add portal content', tuple(allCreators),
appFolder)
def installTypes(self):
'''Registers and configures the Plone content types that correspond to
gen-classes.'''
site = self.ploneSite
# Do Plone-based type registration
classes = self.ploneStuff['listTypes'](self.productName)
self.ploneStuff['installTypes'](site, self.toLog, classes,
self.productName)
self.ploneStuff['install_subskin'](site, self.toLog,
self.ploneStuff['GLOBALS'])
# Set appy view/edit pages for every created type
for className in self.allClassNames + ['%sTool' % self.productName]:
# I did not put the app tool in self.allClassNames because it
# must not be registered in portal_factory
if hasattr(site.portal_types, className):
# className may correspond to an abstract class that has no
# corresponding Plone content type
typeInfo = getattr(site.portal_types, className)
typeInfo.setMethodAliases(self.typeAliases)
# Update edit and view actions
typeActions = typeInfo.listActions()
for action in typeActions:
if action.id == 'view':
page = '%s_appy_view' % self.productName
action.edit(action='string:${object_url}/%s' % page)
elif action.id == 'edit':
page = '%s_appy_edit' % self.productName
action.edit(action='string:${object_url}/%s' % page)
# Configure types for instance creation through portal_factory
factoryTool = site.portal_factory
factoryTypes = self.allClassNames + factoryTool.getFactoryTypes().keys()
factoryTool.manage_setPortalFactoryTypes(listOfTypeIds=factoryTypes)
# Configure CatalogMultiplex: tell what types will be catalogued or not.
atTool = getattr(site, self.ploneStuff['ARCHETYPETOOLNAME'])
for meta_type in self.catalogMap:
submap = self.catalogMap[meta_type]
current_catalogs = Set(
[c.id for c in atTool.getCatalogsByType(meta_type)])
if 'white' in submap:
for catalog in submap['white']:
current_catalogs.update([catalog])
if 'black' in submap:
for catalog in submap['black']:
if catalog in current_catalogs:
current_catalogs.remove(catalog)
atTool.setCatalogsByType(meta_type, list(current_catalogs))
def findPodFile(self, klass, podTemplateName):
'''Finds the file that corresponds to p_podTemplateName for p_klass.'''
res = None
exec 'import %s' % klass.__module__
exec 'moduleFile = %s.__file__' % klass.__module__
folderName = os.path.dirname(moduleFile)
fileName = os.path.join(folderName, '%s.odt' % podTemplateName)
if os.path.isfile(fileName):
res = fileName
return res
def updatePodTemplates(self):
'''Creates or updates the POD templates in flavours according to pod
declarations in the application classes.'''
i = -1
for klass in self.appClasses:
i += 1
if klass.__dict__.has_key('pod'):
pod = getattr(klass, 'pod')
if isinstance(pod, bool):
podTemplates = [klass.__name__]
else:
podTemplates = pod
for templateName in podTemplates:
fileName = self.findPodFile(klass, templateName)
if fileName:
# Create the corresponding PodTemplate in all flavours
for flavour in self.appyTool.flavours:
podId='%s_%s' % (self.appClassNames[i],templateName)
podAttr = 'podTemplatesFor%s'% self.appClassNames[i]
allPodTemplates = getattr(flavour, podAttr)
if allPodTemplates:
if isinstance(allPodTemplates, list):
allIds = [p.id for p in allPodTemplates]
else:
allIds = [allPodTemplates.id]
else:
allIds = []
if podId not in allIds:
# Create a PodTemplate instance
f = file(fileName)
flavour.create(podAttr, id=podId, podTemplate=f,
title=produceNiceMessage(templateName))
f.close()
def installTool(self):
'''Configures the application tool and flavours.'''
# Register the tool in Plone
try:
self.ploneSite.manage_addProduct[
self.productName].manage_addTool(self.toolName)
except self.ploneStuff['BadRequest']:
# If an instance with the same name already exists, this error will
# be unelegantly raised by Zope.
pass
except:
e = sys.exc_info()
if e[0] != 'Bad Request': raise
# Hide the tool from the search form
portalProperties = self.ploneSite.portal_properties
if portalProperties is not None:
siteProperties = getattr(portalProperties, 'site_properties', None)
if siteProperties is not None and \
siteProperties.hasProperty('types_not_searched'):
current = list(siteProperties.getProperty('types_not_searched'))
if self.toolName not in current:
current.append(self.toolName)
siteProperties.manage_changeProperties(
**{'types_not_searched' : current})
# Hide the tool in the navigation
if portalProperties is not None:
nvProps = getattr(portalProperties, 'navtree_properties', None)
if nvProps is not None and nvProps.hasProperty('idsNotToList'):
current = list(nvProps.getProperty('idsNotToList'))
if self.toolInstanceName not in current:
current.append(self.toolInstanceName)
nvProps.manage_changeProperties(**{'idsNotToList': current})
# Remove workflow for the tool
wfTool = self.ploneSite.portal_workflow
wfTool.setChainForPortalTypes([self.toolName], '')
# Create the default flavour
self.tool = getattr(self.ploneSite, self.toolInstanceName)
self.appyTool = self.tool._appy_getWrapper(force=True)
if self.reinstall:
self.tool.at_post_edit_script()
else:
self.tool.at_post_create_script()
if not self.appyTool.flavours:
self.appyTool.create('flavours', title=self.productName, number=1)
self.updatePodTemplates()
# Uncatalog tool
self.tool.unindexObject()
# Register tool as configlet
portalControlPanel = self.ploneSite.portal_controlpanel
portalControlPanel.unregisterConfiglet(self.toolName)
portalControlPanel.registerConfiglet(
self.toolName, self.productName,
'string:${portal_url}/%s' % self.toolInstanceName, 'python:True',
'Manage portal', # Access permission
'Products', # Section to which the configlet should be added:
# (Plone, Products (default) or Member)
1, # Visibility
'%sID' % self.toolName, 'site_icon.gif', # Icon in control_panel
self.productName, None)
def installRolesAndGroups(self):
'''Registers roles used by workflows defined in this application if
they are not registered yet. Creates the corresponding groups if
needed.'''
site = self.ploneSite
data = list(site.__ac_roles__)
for role in self.applicationRoles:
if not role in data:
data.append(role)
# Add to portal_role_manager
# First, try to fetch it. If it's not there, we probaly have no
# PAS or another way to deal with roles was configured.
try:
prm = site.acl_users.get('portal_role_manager', None)
if prm is not None:
try:
prm.addRole(role, role,
"Added by product '%s'" % self.productName)
except KeyError: # Role already exists
pass
except AttributeError:
pass
# Create a specific group and grant him this role
group = '%s_group' % role
if not site.portal_groups.getGroupById(group):
site.portal_groups.addGroup(group, title=group)
site.portal_groups.setRolesForGroup(group, [role])
site.__ac_roles__ = tuple(data)
def installWorkflows(self):
'''Creates or updates the workflows defined in the application.'''
wfTool = self.ploneSite.portal_workflow
for contentType, workflowName in self.workflows.iteritems():
# Register the workflow if needed
if workflowName not in wfTool.listWorkflows():
wfMethod = self.ploneStuff['ExternalMethod']('temp', 'temp',
self.productName + '.workflows', 'create_%s' % workflowName)
workflow = wfMethod(self, workflowName)
wfTool._setObject(workflowName, workflow)
else:
self.log('%s already in workflows.' % workflowName)
# Link the workflow to the current content type
wfTool.setChainForPortalTypes([contentType], workflowName)
return wfTool
def installStyleSheet(self):
'''Registers In Plone the stylesheet linked to this application.'''
cssName = self.productName + '.css'
cssTitle = self.productName + ' CSS styles'
cssInfo = {'id': cssName, 'title': cssTitle}
try:
portalCss = self.ploneSite.portal_css
try:
portalCss.unregisterResource(cssInfo['id'])
except:
pass
defaults = {'id': '', 'media': 'all', 'enabled': True}
defaults.update(cssInfo)
portalCss.registerStylesheet(**defaults)
except:
# No portal_css registry
pass
def installPortlet(self):
'''Adds the application-specific portlet and configure other Plone
portlets if relevant.'''
portletName= 'here/%s_portlet/macros/portlet' % self.productName.lower()
site = self.ploneSite
# This is the name of the application-specific portlet
leftPortlets = site.getProperty('left_slots')
if not leftPortlets: leftPortlets = []
else: leftPortlets = list(leftPortlets)
if portletName not in leftPortlets:
leftPortlets.insert(0, portletName)
# Remove some basic Plone portlets that make less sense when building
# web applications.
portletsToRemove = ["here/portlet_navigation/macros/portlet",
"here/portlet_recent/macros/portlet",
"here/portlet_related/macros/portlet"]
if not self.minimalistPlone: portletsToRemove = []
for p in portletsToRemove:
if p in leftPortlets:
leftPortlets.remove(p)
site.manage_changeProperties(left_slots=tuple(leftPortlets))
if self.minimalistPlone:
site.manage_changeProperties(right_slots=())
def finalizeInstallation(self):
'''Performs some final installation steps.'''
site = self.ploneSite
# Do not generate an action (tab) for each root folder
if self.minimalistPlone:
site.portal_properties.site_properties.manage_changeProperties(
disable_folder_sections=True)
# Do not allow an anonymous user to register himself as new user
site.manage_permission('Add portal member', ('Manager',), acquire=0)
# Call custom installer if any
if hasattr(self.appyTool, 'install'):
self.tool.executeAppyAction('install', reindex=False)
# Replace Plone front-page with an application-specific page if needed
if self.appFrontPage:
frontPageName = self.productName + 'FrontPage'
site.manage_changeProperties(default_page=frontPageName)
def log(self, msg): print >> self.toLog, msg
def install(self):
self.log("Installation of %s:" % self.productName)
if self.minimalistPlone: self.customizePlone()
self.installRootFolder()
self.installTypes()
self.installTool()
self.installRolesAndGroups()
self.installWorkflows()
self.installStyleSheet()
self.installPortlet()
self.finalizeInstallation()
self.log("Installation of %s done." % self.productName)
return self.toLog.getvalue()
def uninstallTool(self):
site = self.ploneSite
# Unmention tool in the search form
portalProperties = getattr(site, 'portal_properties', None)
if portalProperties is not None:
siteProperties = getattr(portalProperties, 'site_properties', None)
if siteProperties is not None and \
siteProperties.hasProperty('types_not_searched'):
current = list(siteProperties.getProperty('types_not_searched'))
if self.toolName in current:
current.remove(self.toolName)
siteProperties.manage_changeProperties(
**{'types_not_searched' : current})
# Unmention tool in the navigation
if portalProperties is not None:
nvProps = getattr(portalProperties, 'navtree_properties', None)
if nvProps is not None and nvProps.hasProperty('idsNotToList'):
current = list(nvProps.getProperty('idsNotToList'))
if self.toolInstanceName in current:
current.remove(self.toolInstanceName)
nvProps.manage_changeProperties(**{'idsNotToList': current})
def uninstall(self):
self.log("Uninstallation of %s:" % self.productName)
self.uninstallTool()
self.log("Uninstallation of %s done." % self.productName)
return self.toLog.getvalue()
# ------------------------------------------------------------------------------
class ZopeInstaller:
'''This Zope installer runs every time Zope starts and encounters this
generated Zope product.'''
def __init__(self, zopeContext, productName, toolClass,
defaultAddContentPermission, addContentPermissions,
logger, ploneStuff):
self.zopeContext = zopeContext
self.productName = productName
self.toolClass = toolClass
self.defaultAddContentPermission = defaultAddContentPermission
self.addContentPermissions = addContentPermissions
self.logger = logger
self.ploneStuff = ploneStuff # A dict of some Plone functions or vars
def installApplication(self):
'''Performs some application-wide installation steps.'''
self.ploneStuff['DirectoryView'].registerDirectory('skins',
self.ploneStuff['product_globals'])
def installTool(self):
'''Installs the tool.'''
self.ploneStuff['ToolInit'](self.productName + ' Tools',
tools = [self.toolClass], icon='tool.gif').initialize(
self.zopeContext)
def installTypes(self):
'''Installs and configures the types defined in the application.'''
contentTypes, constructors, ftis = self.ploneStuff['process_types'](
self.ploneStuff['listTypes'](self.productName), self.productName)
self.ploneStuff['cmfutils'].ContentInit(self.productName + ' Content',
content_types = contentTypes,
permission = self.defaultAddContentPermission,
extra_constructors = constructors, fti = ftis).initialize(
self.zopeContext)
# Define content-specific "add" permissions
for i in range(0, len(contentTypes)):
className = contentTypes[i].__name__
if not className in self.addContentPermissions: continue
self.zopeContext.registerClass(meta_type = ftis[i]['meta_type'],
constructors = (constructors[i],),
permission = self.addContentPermissions[className])
def finalizeInstallation(self):
'''Performs some final installation steps.'''
# Apply customization policy if any
cp = self.ploneStuff['CustomizationPolicy']
if cp and hasattr(cp, 'register'): cp.register(context)
def install(self):
self.logger.info('is being installed...')
self.installApplication()
self.installTool()
self.installTypes()
self.finalizeInstallation()
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,42 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class ClassMixin(AbstractMixin):
_appy_meta_type = 'class'
def _appy_fieldIsUsed(self, portalTypeName, fieldName):
tool = self.getTool()
flavour = tool.getFlavour(portalTypeName)
optionalFieldsAccessor = 'getOptionalFieldsFor%s' % self.meta_type
exec 'usedFields = flavour.%s()' % optionalFieldsAccessor
res = False
if fieldName in usedFields:
res = True
return res
def _appy_getDefaultValueFor(self, portalTypeName, fieldName):
tool = self.getTool()
flavour = tool.getFlavour(portalTypeName)
fieldFound = False
klass = self.__class__
while not fieldFound:
metaType = klass.meta_type
defValueAccessor = 'getDefaultValueFor%s_%s' % (metaType, fieldName)
if not hasattr(flavour, defValueAccessor):
# The field belongs to a super-class.
klass = klass.__bases__[-1]
else:
fieldFound = True
exec 'res = flavour.%s()' % defValueAccessor
return res
def fieldIsUsed(self, fieldName):
'''Checks in the corresponding flavour if p_fieldName is used.'''
portalTypeName = self._appy_getPortalType(self.REQUEST)
return self._appy_fieldIsUsed(portalTypeName, fieldName)
def getDefaultValueFor(self, fieldName):
'''Gets in the flavour the default value for p_fieldName.'''
portalTypeName = self._appy_getPortalType(self.REQUEST)
return self._appy_getDefaultValueFor(portalTypeName,fieldName)
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,113 @@
# ------------------------------------------------------------------------------
import appy.gen
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
# ------------------------------------------------------------------------------
class FlavourMixin(AbstractMixin):
_appy_meta_type = 'flavour'
def getPortalType(self, metaTypeOrAppyType):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
res = metaTypeOrAppyType
isPredefined = False
isAppy = False
appName = self.getProductConfig().PROJECTNAME
if not isinstance(res, basestring):
res = ArchetypesClassDescriptor.getClassName(res)
isAppy = True
if res.find('Extensions_appyWrappers') != -1:
isPredefined = True
elems = res.split('_')
res = '%s%s' % (elems[1], elems[4])
elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Tool):
# This is the custom tool
isPredefined = True
res = '%sTool' % appName
elif isAppy and issubclass(metaTypeOrAppyType, appy.gen.Flavour):
# This is the custom Flavour
isPredefined = True
res = '%sFlavour' % appName
if not isPredefined:
if self.getNumber() != 1:
res = '%s_%d' % (res, self.number)
return res
def registerPortalTypes(self):
'''Registers, into portal_types, the portal types which are specific
to this flavour.'''
i = -1
registeredFactoryTypes = self.portal_factory.getFactoryTypes().keys()
factoryTypesToRegister = []
appName = self.getProductConfig().PROJECTNAME
for metaTypeName in self.allMetaTypes:
i += 1
portalTypeName = '%s_%d' % (metaTypeName, self.number)
# If the portal type corresponding to the meta type is
# registered in portal_factory (in the model:
# use_portal_factory=True), we must also register the new
# portal_type we are currently creating.
if metaTypeName in registeredFactoryTypes:
factoryTypesToRegister.append(portalTypeName)
if not hasattr(self.portal_types, portalTypeName) and \
hasattr(self.portal_types, metaTypeName):
# Indeed abstract meta_types have no associated portal_type
typeInfoName = "%s: %s (%s)" % (appName, metaTypeName,
metaTypeName)
self.portal_types.manage_addTypeInformation(
getattr(self.portal_types, metaTypeName).meta_type,
id=portalTypeName, typeinfo_name=typeInfoName)
# Set the human readable title explicitly
portalType = getattr(self.portal_types, portalTypeName)
portalType.title = portalTypeName
# Associate a workflow for this new portal type.
pf = self.portal_workflow
workflowChain = pf.getChainForPortalType(metaTypeName)
pf.setChainForPortalTypes([portalTypeName],workflowChain)
# Copy actions from the base portal type
basePortalType = getattr(self.portal_types, metaTypeName)
portalType._actions = tuple(basePortalType._cloneActions())
# Copy aliases from the base portal type
portalType.setMethodAliases(basePortalType.getMethodAliases())
# Update the factory tool with the list of types to register
self.portal_factory.manage_setPortalFactoryTypes(
listOfTypeIds=factoryTypesToRegister+registeredFactoryTypes)
def getClassFolder(self, className):
'''Return the folder related to p_className.'''
return getattr(self, className)
def getAvailablePodTemplates(self, obj, phase='main'):
'''Returns the POD templates which are available for generating a
document from p_obj.'''
appySelf = self._appy_getWrapper()
fieldName = 'podTemplatesFor%s' % obj.meta_type
res = []
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res = [r.o for r in podTemplates if r.phase==phase]
hasParents = True
klass = obj.__class__
while hasParents:
parent = klass.__bases__[-1]
if hasattr(parent, 'wrapperClass'):
fieldName = 'podTemplatesFor%s' % parent.meta_type
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res += [r.o for r in podTemplates if r.phase==phase]
klass = parent
else:
hasParents = False
return res
def getMaxShownTemplates(self, obj):
attrName = 'podMaxShownTemplatesFor%s' % obj.meta_type
return getattr(self, attrName)
def getAttr(self, attrName):
'''Gets on this flavour attribute named p_attrName. Useful because we
can't use getattr directly in Zope Page Templates.'''
return getattr(self, attrName, None)
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,97 @@
# ------------------------------------------------------------------------------
import os, os.path, time
from appy.shared import mimeTypes
from appy.gen.plone25.mixins import AbstractMixin
from StringIO import StringIO
# ------------------------------------------------------------------------------
class PodError(Exception): pass
# ------------------------------------------------------------------------------
def getOsTempFolder():
tmp = '/tmp'
if os.path.exists(tmp) and os.path.isdir(tmp):
res = tmp
elif os.environ.has_key('TMP'):
res = os.environ['TMP']
elif os.environ.has_key('TEMP'):
res = os.environ['TEMP']
else:
raise "Sorry, I can't find a temp folder on your machine."
return res
# Error-related constants ------------------------------------------------------
POD_ERROR = 'An error occurred while generating the document. Please check ' \
'the following things if you wanted to generate the document in ' \
'PDF, DOC or RTF: (1) OpenOffice is started in server mode on ' \
'the port you should have specified in the PloneMeeting ' \
'configuration (go to Site setup-> PloneMeeting configuration); ' \
'(2) if the Python interpreter running Zope and ' \
'Plone is not able to discuss with OpenOffice (it does not have ' \
'"uno" installed - check it by typing "import uno" at the Python ' \
'prompt) please specify, in the PloneMeeting configuration, ' \
'the path to a UNO-enabled Python interpreter (ie, the Python ' \
'interpreter included in the OpenOffice distribution, or, if ' \
'your server runs Ubuntu, the standard Python interpreter ' \
'installed in /usr/bin/python). Here is the error as reported ' \
'by the appy.pod library:\n\n %s'
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
# ------------------------------------------------------------------------------
class PodTemplateMixin(AbstractMixin):
_appy_meta_type = 'podtemplate'
def generateDocument(self, obj):
'''Generates a document from this template, for object p_obj.'''
appySelf = self._appy_getWrapper(force=True)
appName = self.getProductConfig().PROJECTNAME
appModule = getattr(self.getProductConfig(), appName)
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.UID(), time.time(), self.getPodFormat())
# Define parameters to pass to the appy.pod renderer
currentUser = self.portal_membership.getAuthenticatedMember()
podContext = {'self': obj._appy_getWrapper(force=True),
'user': currentUser,
'podTemplate': appySelf,
'now': self.getProductConfig().DateTime(),
'projectFolder': os.path.dirname(appModule.__file__)
}
rendererParams = {'template': StringIO(appySelf.podTemplate),
'context': podContext,
'result': tempFileName }
if appySelf.tool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = appySelf.tool.unoEnabledPython
if appySelf.tool.openOfficePort:
rendererParams['ooPort'] = appySelf.tool.openOfficePort
# Launch the renderer
import appy.pod
try:
renderer = appy.pod.renderer.Renderer(**rendererParams)
renderer.run()
except appy.pod.PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
raise PodError(POD_ERROR % str(pe))
# Open the temp file on the filesystem
f = file(tempFileName, 'rb')
forBrowser = True
if forBrowser:
# Create a OFS.Image.File object that will manage correclty HTTP
# headers, etc.
theFile = self.getProductConfig().File('dummyId', 'dummyTitle', f,
content_type=mimeTypes[appySelf.podFormat])
res = theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE)
else:
# I must return the raw document content.
res = f.read()
f.close()
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
self.getProductConfig().logger.warn(DELETE_TEMP_DOC_ERROR % str(oe))
except IOError, ie:
self.getProductConfig().logger.warn(DELETE_TEMP_DOC_ERROR % str(ie))
return res
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,192 @@
# ------------------------------------------------------------------------------
import re, os, os.path
from appy.gen.utils import FieldDescr
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.wrappers import AbstractWrapper
_PY = 'Please specify a file corresponding to a Python interpreter ' \
'(ie "/usr/bin/python").'
FILE_NOT_FOUND = 'Path "%s" was not found.'
VALUE_NOT_FILE = 'Path "%s" is not a file. ' + _PY
NO_PYTHON = "Name '%s' does not starts with 'python'. " + _PY
NOT_UNO_ENABLED_PYTHON = '"%s" is not a UNO-enabled Python interpreter. ' \
'To check if a Python interpreter is UNO-enabled, ' \
'launch it and type "import uno". If you have no ' \
'ImportError exception it is ok.'
# ------------------------------------------------------------------------------
class ToolMixin(AbstractMixin):
_appy_meta_type = 'tool'
def _appy_validateUnoEnabledPython(self, value):
'''This method represents the validator for field unoEnabledPython.
This field is present on the Tool only if POD is needed.'''
if value:
if not os.path.exists(value):
return FILE_NOT_FOUND % value
if not os.path.isfile(value):
return VALUE_NOT_FILE % value
if not os.path.basename(value).startswith('python'):
return NO_PYTHON % value
if os.system('%s -c "import uno"' % value):
return NOT_UNO_ENABLED_PYTHON % value
return None
def getFlavour(self, contextObjOrPortalType, appy=False):
'''Gets the flavour that corresponds to p_contextObjOrPortalType.'''
if isinstance(contextObjOrPortalType, basestring):
portalTypeName = contextObjOrPortalType
else:
# It is the contextObj, not a portal type name
portalTypeName = contextObjOrPortalType.portal_type
res = None
appyTool = self._appy_getWrapper(force=True)
flavourNumber = None
nameElems = portalTypeName.split('_')
if len(nameElems) > 1:
try:
flavourNumber = int(nameElems[-1])
except ValueError:
pass
appName = self.getProductConfig().PROJECTNAME
if flavourNumber != None:
for flavour in appyTool.flavours:
if flavourNumber == flavour.number:
res = flavour
elif portalTypeName == ('%sFlavour' % appName):
# Current object is the Flavour itself. In this cas we simply
# return the wrapped contextObj. Here we are sure that
# contextObjOrPortalType is an object, not a portal type.
res = contextObjOrPortalType._appy_getWrapper(force=True)
if not res and appyTool.flavours:
res = appyTool.flavours[0]
# If appy=False, return the Plone object and not the Appy wrapper
# (this way, we avoid Zope security/access-related problems while
# using this object in Zope Page Templates)
if res and not appy:
res = res.o
return res
def getFlavoursInfo(self):
'''Returns information about flavours.'''
res = []
appyTool = self._appy_getWrapper(force=True)
for flavour in appyTool.flavours:
if isinstance(flavour.o, FlavourMixin):
# This is a bug: sometimes other objects are associated as
# flavours.
res.append({'title': flavour.title, 'number':flavour.number})
return res
def getAppFolder(self):
'''Returns the folder at the root of the Plone site that is dedicated
to this application.'''
portal = self.getProductConfig().getToolByName(
self, 'portal_url').getPortalObject()
appName = self.getProductConfig().PROJECTNAME
return getattr(portal, appName)
def showPortlet(self):
return not self.portal_membership.isAnonymousUser()
def executeQuery(self, queryName, flavourNumber):
if queryName.find(',') != -1:
# Several content types are specified
portalTypes = queryName.split(',')
if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes]
else:
portalTypes = queryName
params = {'portal_type': portalTypes, 'batch': True}
res = self.portal_catalog.searchResults(**params)
batchStart = self.REQUEST.get('b_start', 0)
res = self.getProductConfig().Batch(res,
self.getNumberOfResultsPerPage(), int(batchStart), orphan=0)
return res
def getResultColumnsNames(self, queryName):
contentTypes = queryName.strip(',').split(',')
resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result.
flavour = None
fieldNames = None
for cType in contentTypes:
# Get the flavour tied to those content types
if not flavour:
flavour = self.getFlavour(cType, appy=True)
if flavour.number != 1:
cType = cType.rsplit('_', 1)[0]
fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType)
if not resSet:
resSet = set(fieldNames)
else:
resSet = resSet.intersection(fieldNames)
# By converting to set, we've lost order. Let's put things in the right
# order.
for fieldName in fieldNames:
if fieldName in resSet:
res.append(fieldName)
return res
def getResultColumns(self, anObject, queryName):
'''What columns must I show when displaying a list of root class
instances? Result is a list of tuples containing the name of the
column (=name of the field) and a FieldDescr instance.'''
res = []
for fieldName in self.getResultColumnsNames(queryName):
if fieldName == 'workflowState':
# We do not return a FieldDescr instance if the attributes is
# not a *real* attribute but the workfow state.
res.append(fieldName)
else:
# Create a FieldDescr instance
appyType = anObject.getAppyType(fieldName)
atField = anObject.schema.get(fieldName)
fieldDescr = FieldDescr(atField, appyType, None)
res.append(fieldDescr.get())
return res
xhtmlToText = re.compile('<.*?>', re.S)
def getReferenceLabel(self, brain, appyType):
'''p_appyType is a Ref with link=True. I need to display, on an edit
view, the referenced object p_brain in the listbox that will allow
the user to choose which object(s) to link through the Ref.
According to p_appyType, the label may only be the object title,
or more if parameter appyType.shownInfo is used.'''
res = brain.Title
if 'title' in appyType['shownInfo']:
# We may place it at another place
res = ''
appyObj = brain.getObject()._appy_getWrapper(force=True)
for fieldName in appyType['shownInfo']:
value = getattr(appyObj, fieldName)
if isinstance(value, AbstractWrapper):
value = value.title.decode('utf-8')
elif isinstance(value, basestring):
value = value.decode('utf-8')
refAppyType = appyObj.o.getAppyType(fieldName)
if refAppyType and (refAppyType['type'] == 'String') and \
(refAppyType['format'] == 2):
value = self.xhtmlToText.sub(' ', value)
else:
value = str(value)
prefix = ''
if res:
prefix = ' | '
res += prefix + value.encode('utf-8')
maxWidth = self.getListBoxesMaximumWidth()
if len(res) > maxWidth:
res = res[:maxWidth-2] + '...'
return res
translationMapping = {'portal_path': ''}
def translateWithMapping(self, label):
'''Translates p_label in the application domain, with a default
translation mapping.'''
if not self.translationMapping['portal_path']:
self.translationMapping['portal_path'] = \
self.portal_url.getPortalPath()
appName = self.getProductConfig().PROJECTNAME
return self.utranslate(label, self.translationMapping, domain=appName)
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,840 @@
'''This package contains mixin classes that are mixed in with generated classes:
- mixins/ClassMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class;
- mixins/FlavourMixin is mixed in with the generated application Flavour
class.
The AbstractMixin defined hereafter is the base class of any mixin.'''
# ------------------------------------------------------------------------------
import os, os.path, sys, types
import appy.gen
from appy.gen import String
from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \
ValidationErrors, sequenceTypes
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission, getAppyRequest
# ------------------------------------------------------------------------------
class AbstractMixin:
'''Every Archetype class generated by appy.gen inherits from a mixin that
inherits from this class. It contains basic functions allowing to
minimize the amount of generated code.'''
def getAppyType(self, fieldName):
'''Returns the Appy type corresponding to p_fieldName.'''
res = None
if fieldName == 'id': return res
if self.wrapperClass:
baseClass = self.wrapperClass.__bases__[-1]
try:
# If I get the attr on self instead of baseClass, I get the
# property field that is redefined at the wrapper level.
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType, baseClass)
except AttributeError:
# Check for another parent
if self.wrapperClass.__bases__[0].__bases__:
baseClass = self.wrapperClass.__bases__[0].__bases__[-1]
try:
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType,
baseClass)
except AttributeError:
pass
return res
def _appy_getRefs(self, fieldName, ploneObjects=False,
noListIfSingleObj=False):
'''p_fieldName is the name of a Ref field. This method returns an
ordered list containing the objects linked to p_self through this
field. If p_ploneObjects is True, the method returns the "true"
Plone objects instead of the Appy wrappers.'''
res = []
sortedFieldName = '_appy_%s' % fieldName
exec 'objs = self.get%s%s()' % (fieldName[0].upper(), fieldName[1:])
if objs:
if type(objs) != list:
objs = [objs]
objectsUids = [o.UID() for o in objs]
sortedObjectsUids = getattr(self, sortedFieldName)
# The list of UIDs may contain too much UIDs; indeed, when deleting
# objects, the list of UIDs are not updated.
uidsToDelete = []
for uid in sortedObjectsUids:
try:
uidIndex = objectsUids.index(uid)
obj = objs[uidIndex]
if not ploneObjects:
obj = obj._appy_getWrapper(force=True)
res.append(obj)
except ValueError:
uidsToDelete.append(uid)
# Delete unused UIDs
for uid in uidsToDelete:
sortedObjectsUids.remove(uid)
if res and noListIfSingleObj:
appyType = self.getAppyType(fieldName)
if appyType['multiplicity'][1] == 1:
res = res[0]
return res
def getAppyRefs(self, fieldName):
'''Gets the objects linked to me through p_fieldName.'''
return self._appy_getRefs(fieldName, ploneObjects=True)
def getAppyRefIndex(self, fieldName, obj):
'''Gets the position of p_obj within Ref field named p_fieldName.'''
sortedFieldName = '_appy_%s' % fieldName
sortedObjectsUids = getattr(self, sortedFieldName)
res = sortedObjectsUids.index(obj.UID())
return res
def getAppyBackRefs(self):
'''Returns the list of back references (=types) that are defined for
this class.'''
className = self.__class__.__name__
referers = self.getProductConfig().referers
res = []
if referers.has_key(className):
for appyType, relationship in referers[className]:
d = appyType.__dict__
d['backd'] = appyType.back.__dict__
res.append((d, relationship))
return res
def getAppyRefPortalType(self, fieldName):
'''Gets the portal type of objects linked to me through Ref field named
p_fieldName.'''
appyType = self.getAppyType(fieldName)
tool = self.getTool()
if self._appy_meta_type == 'flavour':
flavour = self._appy_getWrapper(force=True)
else:
portalTypeName = self._appy_getPortalType(self.REQUEST)
flavour = tool.getFlavour(portalTypeName)
return self._appy_getAtType(appyType['klass'], flavour)
def _appy_getOrderedFields(self, isEdit):
'''Gets all fields (normal fields, back references, fields to show,
fields to hide) in order, in the form of a list of FieldDescr
instances.'''
orderedFields = []
# Browse Archetypes fields
for atField in self.Schema().filterFields(isMetadata=0):
fieldName = atField.getName()
appyType = self.getAppyType(fieldName)
if not appyType:
if isEdit and (fieldName == 'title'):
# We must provide a dummy appy type for it. Else, it will
# not be rendered in the "edit" form.
appyType = String(multiplicity=(1,1)).__dict__
else:
continue # Special fields like 'id' are not relevant
# Do not display title on view page; it is already in the header
if not isEdit and (fieldName=='title'): pass
else:
orderedFields.append(FieldDescr(atField, appyType, None))
# Browse back references
for appyType, fieldRel in self.getAppyBackRefs():
orderedFields.append(FieldDescr(None, appyType, fieldRel))
# If some fields must be moved, do it now
res = []
for fieldDescr in orderedFields:
if fieldDescr.appyType['move']:
newPosition = len(res) - abs(fieldDescr.appyType['move'])
if newPosition <= 0:
newPosition = 0
res.insert(newPosition, fieldDescr)
else:
res.append(fieldDescr)
return res
def showField(self, fieldDescr, isEdit=False):
'''Must I show field corresponding to p_fieldDescr?'''
if isinstance(fieldDescr, FieldDescr):
fieldDescr = fieldDescr.__dict__
appyType = fieldDescr['appyType']
if isEdit and (appyType['type']=='Ref') and appyType['add']:
return False
if (fieldDescr['widgetType'] == 'backField') and \
not self.getBRefs(fieldDescr['fieldRel']):
return False
# Do not show field if it is optional and not selected in flavour
if appyType['optional']:
tool = self.getTool()
flavour = tool.getFlavour(self, appy=True)
flavourAttrName = 'optionalFieldsFor%s' % self.meta_type
flavourAttrValue = getattr(flavour, flavourAttrName, ())
if fieldDescr['atField'].getName() not in flavourAttrValue:
return False
# Check if the user has the permission to view or edit the field
if fieldDescr['widgetType'] != 'backField':
user = self.portal_membership.getAuthenticatedMember()
if isEdit:
perm = fieldDescr['atField'].write_permission
else:
perm = fieldDescr['atField'].read_permission
if not user.has_permission(perm, self):
return False
# Evaluate fieldDescr['show']
if callable(fieldDescr['show']):
obj = self._appy_getWrapper(force=True)
res = fieldDescr['show'](obj)
else:
res = fieldDescr['show']
return res
def getAppyFields(self, isEdit, page):
'''Returns the fields sorted by group. For every field, a dict
containing the relevant info needed by the view or edit templates is
given.'''
res = []
groups = {} # The already encountered groups
for fieldDescr in self._appy_getOrderedFields(isEdit):
# Select only widgets shown on current page
if fieldDescr.page != page:
continue
# Do not take into account hidden fields and fields that can't be
# edited through the edit view
if not self.showField(fieldDescr, isEdit): continue
if not fieldDescr.group:
res.append(fieldDescr.get())
else:
# Have I already met this group?
groupName, cols = GroupDescr.getGroupInfo(fieldDescr.group)
if not groups.has_key(groupName):
groupDescr = GroupDescr(groupName, cols,
fieldDescr.appyType['page']).get()
groups[groupName] = groupDescr
res.append(groupDescr)
else:
groupDescr = groups[groupName]
groupDescr['fields'].append(fieldDescr.get())
if groups:
for groupDict in groups.itervalues():
GroupDescr.computeRows(groupDict)
return res
def getAppyStates(self, phase, currentOnly=False):
'''Returns information about the states that are related to p_phase.
If p_currentOnly is True, we return the current state, even if not
related to p_phase.'''
res = []
dcWorkflow = self.getWorkflow(appy=False)
if not dcWorkflow: return res
currentState = self.portal_workflow.getInfoFor(self, 'review_state')
if currentOnly:
return [StateDescr(currentState,'current').get()]
workflow = self.getWorkflow(appy=True)
if workflow:
stateStatus = 'done'
for stateName in workflow._states:
if stateName == currentState:
stateStatus = 'current'
elif stateStatus != 'done':
stateStatus = 'future'
state = getattr(workflow, stateName)
if (state.phase == phase) and \
(self._appy_showState(workflow, state.show)):
res.append(StateDescr(stateName, stateStatus).get())
return res
def getAppyPage(self, isEdit, phaseInfo, appyName=True):
'''On which page am I? p_isEdit indicates if the current page is an
edit or consult view. p_phaseInfo indicates the current phase.'''
pageAttr = 'pageName'
if isEdit:
pageAttr = 'fieldset' # Archetypes page name
default = phaseInfo['pages'][0]
# Default page is the first page of the current phase
res = self.REQUEST.get(pageAttr, default)
if appyName and (res == 'default'):
res = 'main'
return res
def getAppyPages(self, phase='main'):
'''Gets the list of pages that are defined for this content type.'''
res = []
for atField in self.Schema().filterFields(isMetadata=0):
appyType = self.getAppyType(atField.getName())
if not appyType: continue
if (appyType['phase'] == phase) and (appyType['page'] not in res) \
and self._appy_showPage(appyType['page'], appyType['pageShow']):
res.append(appyType['page'])
for appyType, fieldRel in self.getAppyBackRefs():
if (appyType['backd']['phase'] == phase) and \
(appyType['backd']['page'] not in res) and \
self._appy_showPage(appyType['backd']['page'],
appyType['backd']['pageShow']):
res.append(appyType['backd']['page'])
return res
def getAppyPhases(self, currentOnly=False, fieldset=None, forPlone=False):
'''Gets the list of phases that are defined for this content type. If
p_currentOnly is True, the search is limited to the current phase.
If p_fieldset is not None, the search is limited to the phase
corresponding the Plone fieldset whose name is given in this
parameter. If p_forPlone=True, among phase info we write Plone
fieldset names, which are a bit different from Appy page names.'''
# Get the list of phases
res = [] # Ordered list of phases
phases = {} # Dict of phases
for atField in self.Schema().filterFields(isMetadata=0):
appyType = self.getAppyType(atField.getName())
if not appyType: continue
if appyType['phase'] not in phases:
phase = PhaseDescr(appyType['phase'],
self.getAppyStates(appyType['phase']), forPlone, self)
res.append(phase.__dict__)
phases[appyType['phase']] = phase
else:
phase = phases[appyType['phase']]
phase.addPage(appyType, self)
for appyType, fieldRel in self.getAppyBackRefs():
if appyType['backd']['phase'] not in phases:
phase = PhaseDescr(appyType['backd']['phase'],
self.getAppyStates(appyType['backd']['phase']),
forPlone, self)
res.append(phase.__dict__)
phases[appyType['phase']] = phase
else:
phase = phases[appyType['backd']['phase']]
phase.addPage(appyType['backd'], self)
# Remove phases that have no visible page
for i in range(len(res)-1, -1, -1):
if not res[i]['pages']:
del phases[res[i]['name']]
del res[i]
# Then, compute status of phases
for ph in phases.itervalues():
ph.computeStatus()
ph.totalNbOfPhases = len(res)
# Restrict the result if we must not produce the whole list of phases
if currentOnly:
for phaseInfo in res:
if phaseInfo['phaseStatus'] == 'Current':
return phaseInfo
elif fieldset:
for phaseInfo in res:
if fieldset in phaseInfo['pages']:
return phaseInfo
else:
return res
def changeAppyRefOrder(self, fieldName, objectUid, newIndex, isDelta):
'''This method changes the position of object with uid p_objectUid in
reference field p_fieldName to p_newIndex i p_isDelta is False, or
to actualIndex+p_newIndex if p_isDelta is True.'''
sortedFieldName = '_appy_%s' % fieldName
sortedObjectsUids = getattr(self, sortedFieldName)
oldIndex = sortedObjectsUids.index(objectUid)
sortedObjectsUids.remove(objectUid)
if isDelta:
newIndex = oldIndex + newIndex
else:
pass # To implement later on
sortedObjectsUids.insert(newIndex, objectUid)
def getWorkflow(self, appy=True):
'''Returns the Appy workflow instance that is relevant for this
object. If p_appy is False, it returns the DC workflow.'''
res = None
if appy:
# Get the workflow class first
workflowClass = None
if self.wrapperClass:
appyClass = self.wrapperClass.__bases__[1]
if hasattr(appyClass, 'workflow'):
workflowClass = appyClass.workflow
if workflowClass:
# Get the corresponding prototypical workflow instance
res = self.getProductConfig().workflowInstances[workflowClass]
else:
dcWorkflows = self.portal_workflow.getWorkflowsFor(self)
if dcWorkflows:
res = dcWorkflows[0]
return res
def getWorkflowLabel(self, stateName=None):
'''Gets the i18n label for the workflow current state. If no p_stateName
is given, workflow label is given for the current state.'''
res = ''
wf = self.getWorkflow(appy=False)
if wf:
res = stateName
if not res:
res = self.portal_workflow.getInfoFor(self, 'review_state')
appyWf = self.getWorkflow(appy=True)
if appyWf:
res = '%s_%s' % (wf.id, res)
return res
def getComputedValue(self, appyType):
'''Computes on p_self the value of the Computed field corresponding to
p_appyType.'''
res = ''
obj = self._appy_getWrapper(force=True)
if appyType['method']:
try:
res = appyType['method'](obj)
if not isinstance(res, basestring):
res = repr(res)
except Exception, e:
res = str(e)
return res
def may(self, transitionName):
'''May the user execute transition named p_transitionName?'''
# Get the Appy workflow instance
workflow = self.getWorkflow()
res = False
if workflow:
# Get the corresponding Appy transition
transition = workflow._transitionsMapping[transitionName]
user = self.portal_membership.getAuthenticatedMember()
if isinstance(transition.condition, basestring):
# It is a role. Transition may be triggered if the user has this
# role.
res = user.has_role(transition.condition, self)
elif type(transition.condition) == types.FunctionType:
obj = self._appy_getWrapper()
res = transition.condition(workflow, obj)
elif type(transition.condition) in (tuple, list):
# It is a list of roles and or functions. Transition may be
# triggered if user has at least one of those roles and if all
# functions return True.
hasRole = None
for roleOrFunction in transition.condition:
if isinstance(roleOrFunction, basestring):
if hasRole == None:
hasRole = False
if user.has_role(roleOrFunction, self):
hasRole = True
elif type(roleOrFunction) == types.FunctionType:
obj = self._appy_getWrapper()
if not roleOrFunction(workflow, obj):
return False
if hasRole != False:
res = True
return res
def executeAppyAction(self, actionName, reindex=True):
'''Executes action with p_fieldName on this object.'''
appyClass = self.wrapperClass.__bases__[1]
res = getattr(appyClass, actionName)(self._appy_getWrapper(force=True))
self.reindexObject()
return res
def callAppySelect(self, selectMethod, brains):
'''Selects objects from a Reference field.'''
if selectMethod:
obj = self._appy_getWrapper(force=True)
allObjects = [b.getObject()._appy_getWrapper() \
for b in brains]
filteredObjects = selectMethod(obj, allObjects)
filteredUids = [o.o.UID() for o in filteredObjects]
res = []
for b in brains:
if b.UID in filteredUids:
res.append(b)
else:
res = brains
return res
def getCssClasses(self, appyType, asSlave=True):
'''Gets the CSS classes (used for master/slave relationships) for this
object, either as slave (p_asSlave=True) either as master. The HTML
element on which to define the CSS class for a slave or a master is
different. So this method is called either for getting CSS classes
as slave or as master.'''
res = ''
if not asSlave and appyType['slaves']:
res = 'appyMaster master_%s' % appyType['id']
elif asSlave and appyType['master']:
res = 'slave_%s' % appyType['master'].id
res += ' slaveValue_%s_%s' % (appyType['master'].id,
appyType['masterValue'])
return res
def fieldValueSelected(self, fieldName, value, vocabValue):
'''When displaying a selection box (ie a String with a validator being a
list), must the _vocabValue appear as selected?'''
# Check according to database value
if (type(value) in sequenceTypes):
if vocabValue in value: return True
else:
if vocabValue == value: return True
# Check according to value in request
valueInReq = self.REQUEST.get(fieldName, None)
if type(valueInReq) in sequenceTypes:
if vocabValue in valueInReq: return True
else:
if vocabValue == valueInReq: return True
return False
def checkboxChecked(self, fieldName, value):
'''When displaying a checkbox, must it be checked or not?'''
valueInReq = self.REQUEST.get(fieldName, None)
if valueInReq != None:
return valueInReq in ('True', 1, '1')
else:
return value
def getLabelPrefix(self, fieldName=None):
'''For some i18n labels, wee need to determine a prefix, which may be
linked to p_fieldName. Indeed, the prefix may be based on the name
of the (super-)class where p_fieldName is defined.'''
res = self.meta_type
if fieldName:
appyType = self.getAppyType(fieldName)
res = '%s_%s' % (self._appy_getAtType(appyType['selfClass']),
fieldName)
return res
def _appy_getWrapper(self, force=False):
'''Returns the wrapper object for p_self. It is created if it did not
exist.'''
if (not hasattr(self.aq_base, 'appyWrapper')) or force:
# In some cases (p_force=True), we need to re-generate the
# wrapper object. Else, acquisition may be lost on wrapper.o.
self.appyWrapper = self.wrapperClass(self)
return self.appyWrapper
def _appy_getSourceClass(self, fieldName, baseClass):
'''We know that p_fieldName was defined on Python class p_baseClass or
one of its parents. This method returns the exact class (p_baseClass
or a parent) where it was defined.'''
if fieldName in baseClass.__dict__:
return baseClass
else:
return self._appy_getSourceClass(fieldName, baseClass.__bases__[0])
def _appy_getTypeAsDict(self, fieldName, appyType, baseClass):
'''Within page templates, the appyType is given as a dict instead of
an object in order to avoid security problems.'''
appyType.selfClass = self._appy_getSourceClass(fieldName, baseClass)
res = appyType.__dict__
if res.has_key('back') and res['back'] and (not res.has_key('backd')):
res['backd'] = res['back'].__dict__
# I create a new entry "backd"; if I put the dict in "back" I
# really modify the initial appyType object and I don't want to do
# this.
return res
def _appy_getAtType(self, appyClass, flavour=None):
'''Gets the name of the Archetypes class that corresponds to
p_appyClass (which is a Python class coming from the user
application). If p_flavour is specified, the method returns the name
of the specific Archetypes class in this flavour (ie suffixed with
the flavour number).'''
res = ArchetypesClassDescriptor.getClassName(appyClass)
appName = self.getProductConfig().PROJECTNAME
if res.find('Extensions_appyWrappers') != -1:
# This is not a content type defined Maybe I am a tool or flavour
res = appName + appyClass.__name__
elif issubclass(appyClass, appy.gen.Tool):
# This is the custom tool
res = '%sTool' % appName
elif issubclass(appyClass, appy.gen.Flavour):
# This is the custom Flavour
res = '%sFlavour' % appName
else:
if flavour and flavour.number != 1:
res += '_%d' % flavour.number
return res
def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False,
noListIfSingleObj=False):
'''This method returns the list of objects linked to this one
through the BackRef corresponding to the Archetypes
relationship named p_relName.'''
res = []
referers = self.getProductConfig().referers
objs = self.getBRefs(relName)
for obj in objs:
if not ploneObjects:
obj = obj._appy_getWrapper(force=True)
res.append(obj)
if res and noListIfSingleObj:
className = self.__class__.__name__
appyType = None
for anAppyType, rel in referers[className]:
if rel == relName:
appyType = anAppyType
break
if appyType.back.multiplicity[1] == 1:
res = res[0]
return res
def _appy_showPage(self, page, pageShow):
'''Must I show p_page?'''
if callable(pageShow):
return pageShow(self._appy_getWrapper(force=True))
else: return pageShow
def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):
return stateShow(workflow, self._appy_getWrapper())
else: return stateShow
def _appy_managePermissions(self):
'''When an object is created or updated, we must update "add"
permissions accordingly: if the object is a folder, we must set on
it permissions that will allow to create, inside it, objects through
Ref fields; if it is not a folder, we must update permissions on its
parent folder instead.'''
# Determine on which folder we need to set "add" permissions
folder = self
if not self.isPrincipiaFolderish:
folder = self.getParentNode()
# On this folder, set "add" permissions for every content type that will
# be created through reference fields
allCreators = set()
for field in self.schema.fields():
if field.type == 'reference':
refContentTypeName= self.getAppyRefPortalType(field.getName())
refContentType = getattr(self.portal_types, refContentTypeName)
refMetaType = refContentType.content_meta_type
if refMetaType in self.getProductConfig(\
).ADD_CONTENT_PERMISSIONS:
# No specific "add" permission is defined for tool and
# flavour, for example.
appyClass = refContentType.wrapperClass.__bases__[-1]
# Get roles that may add this content type
creators = getattr(appyClass, 'creators', None)
if not creators:
creators = self.getProductConfig().defaultAddRoles
allCreators = allCreators.union(creators)
# Grant this "add" permission to those roles
updateRolesForPermission(
self.getProductConfig().ADD_CONTENT_PERMISSIONS[\
refMetaType], creators, folder)
# Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content".
if allCreators:
updateRolesForPermission('Add portal content', tuple(allCreators),
folder)
def _appy_onEdit(self, created):
'''What happens when an object is created (p_created=True) or edited?'''
# Manage references
self._appy_manageRefs(created)
if self.wrapperClass:
# Get the wrapper first
appyWrapper = self._appy_getWrapper(force=True)
# Call the custom "onEdit" if available
try:
appyWrapper.onEdit(created)
except AttributeError, ae:
pass
# Manage "add" permissions
self._appy_managePermissions()
# Re/unindex object
if self._appy_meta_type == 'tool': self.unindexObject()
else: self.reindexObject()
def _appy_getDisplayList(self, values, labels, domain):
'''Creates a DisplayList given a list of p_values and corresponding
i18n p_labels.'''
res = []
i = -1
for v in values:
i += 1
res.append( (v, self.utranslate(labels[i], domain=domain)))
return self.getProductConfig().DisplayList(tuple(res))
nullValues = (None, '', ' ')
numbersMap = {'Integer': 'int', 'Float': 'float'}
validatorTypes = (types.FunctionType, type(String.EMAIL))
def _appy_validateField(self, fieldName, value, label, specificType):
'''Checks whether the p_value entered in field p_fieldName is
correct.'''
appyType = self.getAppyType(fieldName)
msgId = None
if (specificType == 'Ref') and appyType['link']:
# We only check "link" Refs because in edit views, "add" Refs are
# not visible. So if we check "add" Refs, on an "edit" view we will
# believe that that there is no referred object even if there is.
# If the field is a reference, appy must ensure itself that
# multiplicities are enforced.
fieldValue = self.REQUEST.get('appy_ref_%s' % fieldName, '')
if not fieldValue:
nbOfRefs = 0
elif isinstance(fieldValue, basestring):
nbOfRefs = 1
else:
nbOfRefs = len(fieldValue)
minRef = appyType['multiplicity'][0]
maxRef = appyType['multiplicity'][1]
if maxRef == None:
maxRef = sys.maxint
if nbOfRefs < minRef:
msgId = 'min_ref_violated'
elif nbOfRefs > maxRef:
msgId = 'max_ref_violated'
elif specificType in self.numbersMap: # Float, Integer
pyType = self.numbersMap[specificType]
# Validate only if input value is there.
# By the way, we also convert the value.
if value not in self.nullValues:
try:
exec 'value = %s(value)' % pyType
except ValueError:
msgId = 'bad_%s' % pyType
else:
value = None
# Apply the custom validator if it exists
validator = appyType['validator']
if not msgId and (type(validator) in self.validatorTypes):
obj = self._appy_getWrapper(force=True)
if type(validator) == self.validatorTypes[0]:
# It is a custom function. Execute it.
try:
validValue = validator(obj, value)
if isinstance(validValue, basestring) and validValue:
# Validation failed; and p_validValue contains an error
# message.
return validValue
else:
if not validValue:
msgId = label
except Exception, e:
return str(e)
except:
msgId = label
elif type(validator) == self.validatorTypes[1]:
# It is a regular expression
if (value not in self.nullValues) and \
not validator.match(value):
# If the regular expression is among the default ones, we
# generate a specific error message.
if validator == String.EMAIL:
msgId = 'bad_email'
elif validator == String.URL:
msgId = 'bad_url'
elif validator == String.ALPHANUMERIC:
msgId = 'bad_alphanumeric'
else:
msgId = label
res = msgId
if msgId:
res = self.utranslate(msgId, domain=self.i18nDomain)
return res
def _appy_validateAllFields(self, REQUEST, errors):
'''This method is called when individual validation of all fields
succeed (when editing or creating an object). Then, this method
performs inter-field validation. This way, the user must first
correct individual fields before being confronted to potential
inter-fields validation errors.'''
obj = self._appy_getWrapper()
appyRequest = getAppyRequest(REQUEST, obj)
try:
appyErrors = ValidationErrors()
obj.validate(appyRequest, appyErrors)
# This custom "validate" method may have added fields in the given
# ValidationErrors instance. Now we must fill the Zope "errors" dict
# based on it. For every error message that is not a string,
# we replace it with the standard validation error for the
# corresponding field.
for key, value in appyErrors.__dict__.iteritems():
resValue = value
if not isinstance(resValue, basestring):
msgId = '%s_valid' % self.getLabelPrefix(key)
resValue = self.utranslate(msgId, domain=self.i18nDomain)
errors[key] = resValue
except AttributeError:
pass
def _appy_getPortalType(self, request):
'''Guess the portal_type of p_self from info about p_self and
p_request.'''
res = None
# If the object is being created, self.portal_type is not correctly
# initialized yet.
if request.has_key('__factory__info__'):
factoryInfo = request['__factory__info__']
if factoryInfo.has_key('stack'):
res = factoryInfo['stack'][0]
if not res:
res = self.portal_type
return res
def _appy_generateDocument(self):
'''Generates the document from a template whose UID is specified in the
request for a given object whose UID is also in the request.'''
# Get the object
objectUid = self.REQUEST.get('objectUid')
obj = self.uid_catalog(UID=objectUid)[0].getObject()
# Get the POD template
templateUid = self.REQUEST.get('templateUid')
podTemplate = self.uid_catalog(UID=templateUid)[0].getObject()
return podTemplate.generateDocument(obj)
def _appy_manageSortedRefs(self):
'''For every reference field, this method creates the additional
reference lists that are ordered (if it did not already exist).'''
for field in self.schema.fields():
if field.type == 'reference':
sortedRefField = '_appy_%s' % field.getName()
if not hasattr(self.aq_base, sortedRefField):
pList = self.getProductConfig().PersistentList
exec 'self.%s = pList()' % sortedRefField
def _appy_manageRefs(self, created):
'''Every time an object is created or updated, this method updates
the Reference fields accordingly.'''
self._appy_manageSortedRefs()
self._appy_manageRefsFromRequest()
# If the creation was initiated by another object, update the
# reference.
if created:
session = self.REQUEST.SESSION
initiatorUid = session.get('initiator', None)
initiator = None
if initiatorUid:
initiatorRes = self.uid_catalog.searchResults(UID=initiatorUid)
if initiatorRes:
initiator = initiatorRes[0].getObject()
if initiator:
fieldName = session.get('initiatorField')
initiator._appy_getWrapper(force=True).link(fieldName, self)
# Re-initialise the session
session['initiator'] = None
def _appy_manageRefsFromRequest(self):
'''Appy manages itself some Ref fields (with link=True). So here we must
update the Ref fields.'''
fieldsInRequest = [] # Fields present in the request
for requestKey in self.REQUEST.keys():
if requestKey.startswith('appy_ref_'):
fieldName = requestKey[9:]
fieldsInRequest.append(fieldName)
fieldValue = self.REQUEST[requestKey]
sortedRefField = getattr(self, '_appy_%s' % fieldName)
del sortedRefField[:]
if isinstance(fieldValue, basestring):
fieldValue = [fieldValue]
refObjects = []
for uid in fieldValue:
obj = self.uid_catalog(UID=uid)[0].getObject()
refObjects.append(obj)
sortedRefField.append(uid)
exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(),
fieldName[1:])
# Manage Ref fields that are not present in the request
currentFieldset = self.REQUEST.get('fieldset', 'default')
for field in self.schema.fields():
if (field.type == 'reference') and \
(field.schemata == currentFieldset) and \
(field.getName() not in fieldsInRequest):
# If this field is visible, it was not present in the request:
# it means that we must remove any Ref from it.
fieldName = field.getName()
appyType = self.getAppyType(fieldName)
fieldDescr = FieldDescr(field, appyType, None)
if self.showField(fieldDescr, isEdit=True):
exec 'self.set%s%s([])' % (fieldName[0].upper(),
fieldName[1:])
# ------------------------------------------------------------------------------

226
gen/plone25/model.py Executable file
View file

@ -0,0 +1,226 @@
'''This file contains basic classes that will be added into any user
application for creating the basic structure of the application "Tool" which
is the set of web pages used for configuring the application. The "Tool" is
available to administrators under the standard Plone link "site setup". Plone
itself is shipped with several tools used for conguring the various parts of
Plone (content types, catalogs, workflows, etc.)'''
# ------------------------------------------------------------------------------
import copy, types
from appy.gen import Type, Integer, String, File, Ref, Boolean
# ------------------------------------------------------------------------------
class ModelClass:
'''This class is the abstract class of all predefined application classes
used in the Appy model: Tool, Flavour, PodTemplate, etc. All methods and
attributes of those classes are part of the Appy machinery and are
prefixed with _appy_ in order to avoid name conflicts with user-defined
parts of the application model.'''
_appy_attributes = [] # We need to keep track of attributes order.
_appy_notinit = ('id', 'type', 'pythonType', 'slaves', 'selfClass',
'phase', 'pageShow') # When creating a new instance of a
# ModelClass, those attributes must not be given in the
# constructor.
def _appy_addField(klass, fieldName, fieldType, classDescr):
exec "klass.%s = fieldType" % fieldName
klass._appy_attributes.append(fieldName)
if hasattr(klass, '_appy_classes'):
klass._appy_classes[fieldName] = classDescr.name
_appy_addField = classmethod(_appy_addField)
def _appy_getTypeBody(klass, appyType):
'''This method returns the code declaration for p_appyType.'''
typeArgs = ''
for attrName, attrValue in appyType.__dict__.iteritems():
if attrName in ModelClass._appy_notinit:
continue
if isinstance(attrValue, basestring):
attrValue = '"%s"' % attrValue
elif isinstance(attrValue, Type):
attrValue = klass._appy_getTypeBody(attrValue)
elif type(attrValue) == type(ModelClass):
moduleName = attrValue.__module__
if moduleName.startswith('appy.gen'):
attrValue = attrValue.__name__
else:
attrValue = '%s.%s' % (moduleName, attrValue.__name__)
typeArgs += '%s=%s,' % (attrName, attrValue)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
_appy_getTypeBody = classmethod(_appy_getTypeBody)
def _appy_getBody(klass):
'''This method returns the code declaration of this class. We will dump
this in appyWrappers.py in the resulting product.'''
res = ''
for attrName in klass._appy_attributes:
exec 'appyType = klass.%s' % attrName
res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType))
return res
_appy_getBody = classmethod(_appy_getBody)
class PodTemplate(ModelClass):
description = String(format=String.TEXT)
podTemplate = File(multiplicity=(1,1))
podFormat = String(validator=['odt', 'pdf', 'rtf', 'doc'],
multiplicity=(1,1), default='odt')
phase = String(default='main')
_appy_attributes = ['description', 'podTemplate', 'podFormat', 'phase']
defaultFlavourAttrs = ('number', 'enableNotifications')
flavourAttributePrefixes = ('optionalFieldsFor', 'defaultValueFor',
'podTemplatesFor', 'podMaxShownTemplatesFor', 'resultColumnsFor',
'showWorkflowFor', 'showWorkflowCommentFieldFor', 'showAllStatesInPhaseFor')
# Attribute prefixes of the fields generated on the Flavour for configuring
# the application classes.
class Flavour(ModelClass):
'''For every application, the Flavour may be different (it depends on the
fields declared as optional, etc). Instead of creating a new way to
generate the Archetypes Flavour class, we create a silly
FlavourStub instance and we will use the standard Archetypes
generator that generates classes from the application to generate the
flavour class.'''
number = Integer(default=1, show=False)
enableNotifications = Boolean(default=True, page='notifications')
_appy_classes = {} # ~{s_attributeName: s_className}~
# We need to remember the original classes related to the flavour attributes
_appy_attributes = list(defaultFlavourAttrs)
def _appy_clean(klass):
toClean = []
for k, v in klass.__dict__.iteritems():
if not k.startswith('__') and (not k.startswith('_appy_')):
if k not in defaultFlavourAttrs:
toClean.append(k)
for k in toClean:
exec 'del klass.%s' % k
klass._appy_attributes = list(defaultFlavourAttrs)
klass._appy_classes = {}
_appy_clean = classmethod(_appy_clean)
def _appy_copyField(klass, appyType):
'''From a given p_appyType, produce a type definition suitable for
storing the default value for this field.'''
res = copy.copy(appyType)
res.editDefault = False
res.optional = False
res.show = True
res.phase = 'main'
res.specificReadPermission = False
res.specificWritePermission = False
res.multiplicity = (0, appyType.multiplicity[1])
if type(res.validator) == types.FunctionType:
# We will not be able to call this function from the flavour.
res.validator = None
if isinstance(appyType, Ref):
res.link = True
res.add = False
res.back = copy.copy(appyType.back)
res.back.attribute += 'DefaultValue'
res.back.show = False
res.select = None # Not callable from flavour
return res
_appy_copyField = classmethod(_appy_copyField)
def _appy_addOptionalField(klass, fieldDescr):
className = fieldDescr.classDescr.name
fieldName = 'optionalFieldsFor%s' % className
fieldType = getattr(klass, fieldName, None)
if not fieldType:
fieldType = String(multiplicity=(0,None))
fieldType.validator = []
klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr)
fieldType.validator.append(fieldDescr.fieldName)
fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addOptionalField = classmethod(_appy_addOptionalField)
def _appy_addDefaultField(klass, fieldDescr):
className = fieldDescr.classDescr.name
fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName)
fieldType = klass._appy_copyField(fieldDescr.appyType)
klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr)
fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addDefaultField = classmethod(_appy_addDefaultField)
def _appy_addPodField(klass, classDescr):
'''Adds a POD field to the flavour and also an integer field that will
determine the maximum number of documents to show at once on consult
views. If this number is reached, a list is displayed.'''
# First, add the POD field that will hold PodTemplates.
fieldType = Ref(PodTemplate, multiplicity=(0,None), add=True,
link=False, back = Ref(attribute='flavour'),
page="documentGeneration",
group=classDescr.klass.__name__)
fieldName = 'podTemplatesFor%s' % classDescr.name
klass._appy_addField(fieldName, fieldType, classDescr)
# Then, add the integer field
fieldType = Integer(default=1, page='userInterface',
group=classDescr.klass.__name__)
fieldName = 'podMaxShownTemplatesFor%s' % classDescr.name
klass._appy_addField(fieldName, fieldType, classDescr)
classDescr.flavourFieldsToPropagate.append(
('podMaxShownTemplatesFor%s', copy.copy(fieldType)) )
_appy_addPodField = classmethod(_appy_addPodField)
def _appy_addQueryResultColumns(klass, classDescr):
className = classDescr.name
fieldName = 'resultColumnsFor%s' % className
attrNames = [a[0] for a in classDescr.getOrderedAppyAttributes()]
attrNames.append('workflowState') # Object state from workflow
if 'title' in attrNames:
attrNames.remove('title') # Included by default.
fieldType = String(multiplicity=(0,None), validator=attrNames,
page='userInterface',
group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr)
_appy_addQueryResultColumns = classmethod(_appy_addQueryResultColumns)
def _appy_addWorkflowFields(klass, classDescr):
'''Adds, for a given p_classDescr, the workflow-related fields.'''
className = classDescr.name
groupName = classDescr.klass.__name__
# Adds a field allowing to show/hide completely any workflow-related
# information for a given class.
defaultValue = False
if classDescr.isRoot() or issubclass(classDescr.klass, ModelClass):
defaultValue = True
fieldName = 'showWorkflowFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr)
# Adds the boolean field for showing or not the field "enter comments".
fieldName = 'showWorkflowCommentFieldFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr)
# Adds the boolean field for showing all states in current state or not.
# If this boolean is True but the current phase counts only one state,
# we will not show the state at all: the fact of knowing in what phase
# we are is sufficient. If this boolean is False, we simply show the
# current state.
defaultValue = False
if len(classDescr.getPhases()) > 1:
defaultValue = True
fieldName = 'showAllStatesInPhaseFor%s' % className
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr)
_appy_addWorkflowFields = classmethod(_appy_addWorkflowFields)
class Tool(ModelClass):
flavours = Ref(None, multiplicity=(1,None), add=True, link=False,
back=Ref(attribute='tool'))
# First arg is None because we don't know yet if it will link
# to the predefined Flavour class or a custom class defined
# in the application.
unoEnabledPython = String(group="connectionToOpenOffice")
openOfficePort = Integer(default=2002, group="connectionToOpenOffice")
numberOfResultsPerPage = Integer(default=30)
listBoxesMaximumWidth = Integer(default=100)
_appy_attributes = ['flavours', 'unoEnabledPython', 'openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth']
# ------------------------------------------------------------------------------

105
gen/plone25/notifier.py Normal file
View file

@ -0,0 +1,105 @@
'''This package contains functions for sending email notifications.'''
# ------------------------------------------------------------------------------
def getEmailAddress(name, email, encoding='utf-8'):
'''Creates a full email address from a p_name and p_email.'''
res = email
if name: res = name.decode(encoding) + ' <%s>' % email
return res
def convertRolesToEmails(users, portal):
'''p_users is a list of emails and/or roles. This function returns the same
list, where all roles have been expanded to emails of users having this
role (more precisely, users belonging to the group Appy created for the
given role).'''
res = []
for mailOrRole in users:
if mailOrRole.find('@') != -1:
# It is an email. Append it directly to the result.
res.append(mailOrRole)
else:
# It is a role. Find the corresponding group (Appy creates
# one group for every role defined in the application).
groupId = mailOrRole + '_group'
group = portal.acl_users.getGroupById(groupId)
if group:
for user in group.getAllGroupMembers():
userMail = user.getProperty('email')
if userMail and (userMail not in res):
res.append(userMail)
return res
# ------------------------------------------------------------------------------
SENDMAIL_ERROR = 'Error while sending mail: %s.'
ENCODING_ERROR = 'Encoding error while sending mail: %s.'
from appy.gen.utils import sequenceTypes
from appy.gen.plone25.descriptors import WorkflowDescriptor
import socket
def sendMail(obj, transition, transitionName, workflow, logger):
'''Sends mail about p_transition that has been triggered on p_obj that is
controlled by p_workflow.'''
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
ploneObj = obj.o
portal = ploneObj.portal_url.getPortalObject()
mailInfo = transition.notify(workflow, obj)
if not mailInfo[0]: return # Send a mail to nobody.
# mailInfo may be one of the following:
# (to,)
# (to, cc)
# (to, mailSubject, mailBody)
# (to, cc, mailSubject, mailBody)
# "to" and "cc" maybe simple strings (one simple string = one email
# address or one role) or sequences of strings.
# Determine mail subject and body.
if len(mailInfo) <= 2:
# The user didn't mention mail body and subject. We will use
# those defined from i18n labels.
wfHistory = ploneObj.getWorkflowHistory()
labelPrefix = '%s_%s' % (wfName, transitionName)
tName = obj.translate(labelPrefix)
keys = {'siteUrl': portal.absolute_url(),
'siteTitle': portal.Title(),
'objectUrl': ploneObj.absolute_url(),
'objectTitle': ploneObj.Title(),
'transitionName': tName,
'transitionComment': wfHistory[0]['comments']}
mailSubject = obj.translate(labelPrefix + '_mail_subject', keys)
mailBody = obj.translate(labelPrefix + '_mail_body', keys)
else:
mailSubject = mailInfo[-1]
mailBody = mailInfo[-2]
# Determine "to" and "cc".
to = mailInfo[0]
cc = []
if (len(mailInfo) in (2,4)) and mailInfo[1]: cc = mailInfo[1]
if type(to) not in sequenceTypes: to = [to]
if type(cc) not in sequenceTypes: cc = [cc]
# Among "to" and "cc", convert all roles to concrete email addresses
to = convertRolesToEmails(to, portal)
cc = convertRolesToEmails(cc, portal)
# Determine "from" address
enc= portal.portal_properties.site_properties.getProperty('default_charset')
fromAddress = getEmailAddress(
portal.getProperty('email_from_name'),
portal.getProperty('email_from_address'), enc)
# Send the mail
i = 0
for recipient in to:
i += 1
try:
if i != 1: cc = []
portal.MailHost.secureSend(mailBody.encode(enc),
recipient.encode(enc), fromAddress.encode(enc),
mailSubject.encode(enc), mcc=cc, charset='utf-8')
except socket.error, sg:
logger.warn(SENDMAIL_ERROR % str(sg))
break
except UnicodeDecodeError, ue:
logger.warn(ENCODING_ERROR % str(ue))
break
except Exception, e:
logger.warn(SENDMAIL_ERROR % str(e))
break
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,228 @@
<tal:comment replace="nothing"> We begin with some sub-macros used within
macro "showReference" defined below.</tal:comment>
<metal:objectTitle define-macro="objectTitle" i18n:domain="<!applicationName!>">
<tal:comment replace="nothing">Displays the title of a referenced object, with a link on
it to reach the consult view for this object. If we are on a back reference, the link
allows to reach the correct page where the forward reference is defined.</tal:comment>
<a tal:define="viewUrl obj/absolute_url;
fullUrl python: test(isBack, viewUrl + '/?pageName=%s&phase=%s' % (appyType['page'], appyType['phase']), viewUrl)"
tal:attributes="href fullUrl" tal:content="obj/Title"></a>
</metal:objectTitle>
<metal:objectActions define-macro="objectActions" i18n:domain="<!applicationName!>">
<tal:comment replace="nothing">Displays icons for triggering actions on a given
referenced object (edit, delete, etc).</tal:comment>
<table class="no-style-table" cellpadding="0" cellspacing="0">
<tr>
<tal:comment replace="nothing">Edit the element</tal:comment>
<td class="noPadding"><a tal:attributes="href python: obj.absolute_url() + '/edit'"
tal:condition="python: member.has_permission('Modify portal content', obj)">
<img src="edit.gif" title="label_edit" i18n:domain="plone" i18n:attributes="title" />
</a></td>
<tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"><a tal:attributes="href python: obj.absolute_url() + '/delete_confirmation'"
tal:condition="python: member.has_permission('Delete objects', obj)">
<img src="delete_icon.gif" title="label_remove" i18n:domain="plone" i18n:attributes="title" />
</a></td>
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment>
<td class="noPadding" tal:condition="python: len(objs)>1">
<form tal:condition="python: member.has_permission('Modify portal content', obj)"
tal:attributes="action python: contextObj.absolute_url() + '/<!applicationName!>_do'"
tal:define="objectIndex python:contextObj.getAppyRefIndex(field.getName(), obj)">
<input type="hidden" name="actionType" value="changeRefOrder"/>
<input type="hidden" name="fieldName" tal:attributes="value field/getName"/>
<input type="hidden" name="objectUid" tal:attributes="value obj/UID"/>
<tal:comment replace="nothing">Arrow up</tal:comment>
<span tal:condition="python: objectIndex > 0">
<input type="image" name="moveUp" tal:attributes="src python: contextObj.absolute_url() + '/arrowUp.png'"
title="move_up" i18n:attributes="title" class="imageInput"/>
</span>
<tal:comment replace="nothing">Arrow down</tal:comment>
<span tal:condition="python: objectIndex < (len(objs)-1)">
<input type="image" name="moveDown" tal:attributes="src python: contextObj.absolute_url() + '/arrowDown.png'"
title="move_down" i18n:attributes="title" class="imageInput"/>
</span>
</form>
</td>
</tr>
</table>
</metal:objectActions>
<metal:plusIcon define-macro="plusIcon" i18n:domain="<!applicationName!>">
<tal:comment replace="nothing">Displays the "plus" icon that allows to add new object
through a reference widget. Indeed, If field was declared as "addable", we must provide
an icon for creating a new linked object (at least if multiplicities allow it).</tal:comment>
<img src="plus.png" i18n:attributes="title" style="cursor:pointer"
tal:condition="showPlusIcon" title="add_ref"
tal:attributes="onClick python: 'href: window.location=\'%s/createAppyObject?initiator=%s&field=%s&type_name=%s\'' % (folder.absolute_url(), contextObj.UID(), field.getName(), linkedPortalType)"/>
</metal:plusIcon>
<div metal:define-macro="showReference" i18n:domain="<!applicationName!>"
tal:define="tool python: contextObj.<!toolInstanceName!>;
flavour python:tool.getFlavour(contextObj);
folder python: test(contextObj.isPrincipiaFolderish, contextObj, contextObj.getParentNode());
linkedPortalType python:flavour.getPortalType(appyType['klass']);
addPermission python: '<!applicationName!>: Add %s' % linkedPortalType;
multiplicity python:test(isBack, appyType['backd']['multiplicity'], appyType['multiplicity']);
maxReached python:(multiplicity[1] != None) and (len(objs) >= multiplicity[1]);
showPlusIcon python:not isBack and appyType['add'] and not maxReached and member.has_permission(addPermission, folder);
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.
The definition of "atMostOneRef" above may sound strange: we shouldn't check the actual number
of referenced objects. But for back references people often forget to specify multiplicities.
So concretely, multiplicities (0,None) are coded as (0,1).</tal:comment>
<tal:atMostOneReference condition="atMostOneRef">
<tal:comment replace="nothing">Display a simplified widget if maximum number of
referenced objects is 1.</tal:comment>
<table class="no-style-table" cellpadding="0" cellspacing="0"><tr valign="top">
<td><span class="appyLabel" tal:condition="not: innerRef"
i18n:translate="" tal:content="labelMsgId"></span></td>
<tal:comment replace="nothing">If there is no object...</tal:comment>
<tal:noObject condition="not:objs">
<td i18n:translate="">no_ref</td>
<td><metal:plusIcon use-macro="here/<!applicationName!>AppyReference/macros/plusIcon"/></td>
</tal:noObject>
<tal:comment replace="nothing">If there is an object...</tal:comment>
<tal:objectIsPresent condition="python: len(objs) == 1">
<tal:obj define="obj python:objs[0]">
<td><metal:showObjectTitle use-macro="here/<!applicationName!>AppyReference/macros/objectTitle" /></td>
<td tal:condition="not: isBack">
<metal:showObjectActions use-macro="here/<!applicationName!>AppyReference/macros/objectActions" />
</td>
</tal:obj>
</tal:objectIsPresent>
</tr></table>
</tal:atMostOneReference>
<tal:comment replace="nothing">Display a fieldset in all other cases.</tal:comment>
<tal:anyNumberOfReferences condition="not: atMostOneRef">
<fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')">
<legend tal:condition="python: not innerRef or showPlusIcon">
<span tal:condition="not: innerRef" i18n:translate="" tal:content="labelMsgId"/>
<metal:plusIcon use-macro="here/<!applicationName!>AppyReference/macros/plusIcon"/>
</legend>
<tal:comment replace="nothing">Object description</tal:comment>
<p tal:define="descr python:contextObj.utranslate(descrMsgId, domain='<!applicationName!>')"
tal:condition="python: not innerRef and descr.strip()" tal:content="descr" class="discreet" ></p>
<tal:comment replace="nothing">No object is present</tal:comment>
<p tal:condition="not:objs" i18n:translate="">no_ref</p>
<table width="100%" cellspacing="0" cellpadding="0" tal:condition="objs"
tal:attributes="class python:test(innerRef, 'innerAppyTable', '')">
<tr valign="bottom"><td>
<tal:comment replace="nothing">Show backward reference(s)</tal:comment>
<table class="no-style-table" cellspacing="0" cellpadding="0"
tal:condition="python: isBack and objs">
<tr tal:repeat="obj objs">
<td><metal:showObjectTitle use-macro="here/<!applicationName!>AppyReference/macros/objectTitle" />
</td>
</tr>
</table>
<tal:comment replace="nothing">Show forward reference(s)</tal:comment>
<table tal:attributes="class python:test(innerRef, '', 'vertical listing');
width python:test(innerRef, '100%', test(appyType['wide'], '100%', ''))"
align="right" tal:condition="python: not isBack and objs" cellpadding="0" cellspacing="0">
<tr tal:condition="appyType/showHeaders">
<th tal:condition="python: 'title' not in appyType['shownInfo']" i18n:translate="ref_name"></th>
<th tal:repeat="shownField appyType/shownInfo">
<tal:showHeader condition="python: objs[0].getField(shownField)">
<tal:titleHeader condition="python: shownField == 'title'" i18n:translate="ref_name"/>
<tal:otherHeader condition="python: shownField != 'title'"
define="labelId python: objs[0].getField(shownField).widget.label_msgid"
content="labelId" i18n:translate=""/>
</tal:showHeader>
</th>
<th i18n:translate="ref_actions"></th>
</tr>
<tr tal:repeat="obj objs" valign="top">
<tal:comment replace="nothing">Object title, shown here if not specified somewhere
else in appyType.shownInfo.</tal:comment>
<td tal:condition="python: 'title' not in appyType['shownInfo']"><metal:showObjectTitle
use-macro="here/<!applicationName!>AppyReference/macros/objectTitle"/>&nbsp;
</td>
<tal:comment replace="nothing">Additional fields that must be shown</tal:comment>
<td tal:repeat="shownField appyType/shownInfo">
<tal:showTitle condition="python: shownField == 'title'">
<metal:showObjectTitle use-macro="here/<!applicationName!>AppyReference/macros/objectTitle"/>
</tal:showTitle>
<tal:showOtherField define="appyType python: obj.getAppyType(shownField);
field python:obj.getField(shownField);
contextObj python:obj;"
condition="python: appyType and (shownField != 'title')">
<tal:showNormalField condition="python: appyType['type'] not in ('Ref', 'Computed', 'Action')">
<metal:viewField use-macro="python: obj.widget(shownField, 'view', use_label=0)"/>
</tal:showNormalField>
<tal:showRef condition="python: appyType['type'] == 'Ref'">
<tal:ref tal:define="isBack python:appyType['isBack'];
fieldRel python:field.relationship;
objs python:contextObj.getAppyRefs(field.getName());
labelMsgId field/widget/label_msgid;
descrMsgId field/widget/description_msgid;
innerRef python:True">
<metal:showField use-macro="here/<!applicationName!>AppyReference/macros/showReference" />
</tal:ref>
</tal:showRef>
<tal:showComputed condition="python: appyType['type'] == 'Computed'">
<tal:computed content="python: obj.getComputedValue(appyType)"/>
</tal:showComputed>
<tal:showAction condition="python: appyType['type'] == 'Action'">
<metal:action use-macro="here/<!macros!>/macros/showActionField" />
</tal:showAction>
</tal:showOtherField>&nbsp;
</td>
<tal:comment replace="nothing">Actions</tal:comment>
<td align="right">
<metal:showObjectActions use-macro="here/<!applicationName!>AppyReference/macros/objectActions" />
</td>
</tr>
</table>
</td></tr>
</table>
</fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment>
<br tal:define="widgetDescr widgetDescr|nothing"
tal:condition="python: not widgetDescr or (widgetDescr['widgetType'] != 'group')"/>
</tal:anyNumberOfReferences>
</div>
<div metal:define-macro="editReference" i18n:domain="<!applicationName!>"
tal:define="refPortalType python:here.getAppyRefPortalType(field.getName());
appyType python:here.getAppyType(field.getName());
allBrains python:here.uid_catalog(portal_type=refPortalType);
brains python:here.callAppySelect(appyType['select'], allBrains);
refUids python: [o.UID() for o in here.getAppyRefs(field.getName())];
isMultiple python:test(appyType['multiplicity'][1]!=1, 'multiple', '');
appyFieldName python: 'appy_ref_%s' % field.getName();
inError python:test(errors.has_key(field.getName()), True, False);
defaultValue python: here.getDefault(field.getName());
defaultValueUID defaultValue/UID|nothing;
isBeingCreated python: context.portal_factory.isTemporary(context) or ('/portal_factory/' in context.absolute_url())"
tal:attributes="class python:'appyRefEdit field' + test(inError, ' error', '')">
<tal:comment replace="nothing">This macro displays the Reference widget on an "edit" page</tal:comment>
<label tal:attributes="for python:appyFieldName"
i18n:translate="" tal:content="field/widget/label_msgid"></label>
<span class="fieldRequired" tal:condition="python: appyType['multiplicity'][0]>0"></span><br/>
<div tal:condition="inError" tal:content="python: errors[field.getName()]"></div>
<select tal:define="valueIsInReq python:test(request.get(appyFieldName, None) != None, True, False)"
tal:attributes="name python:'appy_ref_%s' % field.getName();
multiple isMultiple">
<option tal:condition="not: isMultiple" value="" i18n:translate="choose_a_value"/>
<option tal:repeat="brain brains"
tal:content="python: here.<!toolInstanceName!>.getReferenceLabel(brain, appyType)"
tal:attributes="value brain/UID;
selected python:test((valueIsInReq and (brain.UID in request.get(appyFieldName, []))) or (not valueIsInReq and ((brain.UID in refUids) or (isBeingCreated and (brain.UID==defaultValueUID)))), True, False)"/>
</select>
</div>

View file

@ -0,0 +1,34 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from Extensions.appyWrappers import <!genClassName!>_Wrapper
from appy.gen.plone25.mixins.ClassMixin import ClassMixin
<!imports!>
schema = Schema((<!fields!>
),)
fullSchema = <!baseSchema!>.copy() + schema.copy()
class <!genClassName!>(<!parents!>):
'''<!classDoc!>'''
security = ClassSecurityInfo()
__implements__ = <!implements!>
archetype_name = '<!genClassName!>'
meta_type = '<!genClassName!>'
portal_type = '<!genClassName!>'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
immediate_view = '<!applicationName!>_appy_view'
default_view = '<!applicationName!>_appy_view'
suppl_views = ()
typeDescription = '<!genClassName!>'
typeDescMsgId = '<!genClassName!>_edit_descr'
_at_rename_after_creation = True
i18nDomain = '<!applicationName!>'
schema = fullSchema
wrapperClass = <!genClassName!>_Wrapper
<!commonMethods!>
<!methods!>
<!register!>

View file

@ -0,0 +1,38 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from Extensions.appyWrappers import <!wrapperClass!>
predefinedSchema = Schema((<!predefinedFields!>
),)
schema = Schema((<!fields!>
),)
fullSchema = OrderedBaseFolderSchema.copy() + predefinedSchema.copy() + schema.copy()
class <!flavourName!>(OrderedBaseFolder, FlavourMixin):
'''Configuration flavour class for <!applicationName!>.'''
security = ClassSecurityInfo()
__implements__ = (getattr(OrderedBaseFolderSchema,'__implements__',()),)
archetype_name = '<!flavourName!>'
meta_type = '<!flavourName!>'
portal_type = '<!flavourName!>'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
#content_icon = '<!flavourName!>.gif'
immediate_view = '<!applicationName!>_appy_view'
default_view = '<!applicationName!>_appy_view'
suppl_views = ()
typeDescription = "<!flavourName!>"
typeDescMsgId = '<!flavourName!>_edit_descr'
i18nDomain = '<!applicationName!>'
schema = fullSchema
allMetaTypes = <!metaTypes!>
wrapperClass = <!wrapperClass!>
_at_rename_after_creation = True
<!commonMethods!>
<!predefinedMethods!>
<!methods!>
registerType(<!flavourName!>, '<!applicationName!>')

View file

@ -0,0 +1,36 @@
<!codeHeader!>
from zExceptions import BadRequest
from Products.ExternalMethod.ExternalMethod import ExternalMethod
from Products.Archetypes.Extensions.utils import installTypes
from Products.Archetypes.Extensions.utils import install_subskin
from Products.Archetypes.config import TOOL_NAME as ARCHETYPETOOLNAME
from Products.Archetypes.atapi import listTypes
from Products.<!applicationName!>.config import applicationRoles,defaultAddRoles
from Products.<!applicationName!>.config import product_globals as GLOBALS
import appy.gen
from appy.gen.plone25.installer import PloneInstaller
<!imports!>
catalogMap = {}
<!catalogMap!>
appClasses = <!appClasses!>
appClassNames = [<!appClassNames!>]
allClassNames = [<!allClassNames!>]
workflows = {<!workflows!>}
# ------------------------------------------------------------------------------
def install(self, reinstall=False):
'''Installation of product "<!applicationName!>"'''
ploneInstaller = PloneInstaller(reinstall, "<!applicationName!>", self,
<!minimalistPlone!>, appClasses, appClassNames, allClassNames,
catalogMap, applicationRoles, defaultAddRoles, workflows,
<!appFrontPage!>, globals())
return ploneInstaller.install()
# ------------------------------------------------------------------------------
def uninstall(self, reinstall=False):
'''Uninstallation of product "<!applicationName!>"'''
ploneInstaller = PloneInstaller(reinstall, "<!applicationName!>", self,
<!minimalistPlone!>, appClasses, appClassNames, allClassNames,
catalogMap, applicationRoles, defaultAddRoles, workflows,
<!appFrontPage!>, globals())
return ploneInstaller.uninstall()
# ------------------------------------------------------------------------------

765
gen/plone25/templates/Macros.pt Executable file
View file

@ -0,0 +1,765 @@
<div metal:define-macro="listPodTemplates" i18n:domain="<!applicationName!>" class="appyPod"
tal:define="flavour python: context.<!toolInstanceName!>.getFlavour(context);
podTemplates python: flavour.getAvailablePodTemplates(context, phase);"
tal:condition="podTemplates">
<script language="javascript">
<!--
// Function that allows to generate a meeting document containing selected items.
function generatePodDocument(contextUid, templateUid) {
var theForm = document.forms["podTemplateForm"];
theForm.objectUid.value = contextUid;
theForm.templateUid.value = templateUid;
theForm.submit();
}
-->
</script>
<tal:comment replace="nothing">Form submitted when an object needs to be generated as a document.</tal:comment>
<form name="podTemplateForm" method="post"
tal:attributes="action python: context.absolute_url() + '/generateDocument'">
<input type="hidden" name="objectUid"/>
<input type="hidden" name="templateUid"/>
</form>
<tal:podTemplates define="maxShownTemplates python: flavour.getMaxShownTemplates(context)">
<tal:comment replace="nothing">Display templates as links if a few number of templates must be shown</tal:comment>
<span class="discreet" tal:condition="python: len(podTemplates)&lt;=maxShownTemplates"
tal:repeat="podTemplate podTemplates">
<a style="cursor: pointer"
tal:define="podFormat podTemplate/getPodFormat"
tal:attributes="onclick python: 'javascript:generatePodDocument(\'%s\',\'%s\')' % (context.UID(), podTemplate.UID())" >
<img tal:attributes="src string: $portal_url/$podFormat.png"/>
<span tal:replace="podTemplate/Title"/>
</a>
&nbsp;</span>
<tal:comment replace="nothing">Display templates as a list if a lot of templates must be shown</tal:comment>
<select tal:condition="python: len(podTemplates)&gt;maxShownTemplates">
<option value="" i18n:translate="">choose_a_doc</option>
<option tal:repeat="podTemplate podTemplates" tal:content="podTemplate/Title"
tal:attributes="onclick python: 'javascript:generatePodDocument(\'%s\',\'%s\')' % (context.UID(), podTemplate.UID())" />
</select>
</tal:podTemplates>
</div>
<metal:editString define-macro="editString" i18n:domain="<!applicationName!>"
tal:define="vocab python:field.Vocabulary(contextObj);">
<label tal:attributes="for fieldName" tal:condition="showLabel" tal:content="label"/>
<div class="discreet" tal:content="description"/>
<select tal:attributes="name fieldName;
id fieldName;
tabindex tabindex/next;
multiple isMultiple;
onchange python: 'javascript:updateSlaves(getMasterValue(this), \'%s\')' % appyType['id'];
class python: contextObj.getCssClasses(appyType, asSlave=False)">
<option tal:repeat="item vocab" i18n:translate=""
tal:attributes="value item;
selected python:contextObj.fieldValueSelected(fieldName, value, item)"
tal:content="python:here.translate(vocab.getMsgId(item), default=vocab.getValue(item))"/>
</select>
</metal:editString>
<metal:editBoolean define-macro="editBoolean" i18n:domain="<!applicationName!>">
<input type="checkbox"
tal:attributes="tabindex tabindex/next;
name python: fieldName + '_visible';
id fieldName;
checked python:contextObj.checkboxChecked(fieldName, value);
onClick python:'toggleCheckbox(\'%s\', \'%s_hidden\');;updateSlaves(getMasterValue(this), \'%s\')' % (fieldName, fieldName, appyType['id']);
class python: 'noborder ' + contextObj.getCssClasses(appyType, asSlave=False)"/>
<input tal:attributes="name fieldName;
id string:${fieldName}_hidden;
value python: test(contextObj.checkboxChecked(fieldName, value), 'True', 'False')"
type="hidden" />
<label tal:attributes="for fieldName" tal:condition="showLabel" tal:content="label"/>
<div class="discreet" tal:content="description"/>
</metal:editBoolean>
<div metal:define-macro="editField" i18n:domain="<!applicationName!>"
tal:define="fieldName field/getName;
isMultiple python:test(appyType['multiplicity'][1]!=1, 'multiple', '');
inError python:test(errors.has_key(fieldName), True, False);
value python:field.getAccessor(contextObj)();
defaultValue python: contextObj.getDefault(fieldName);
label python: contextObj.utranslate(field.widget.label_msgid, domain='<!applicationName!>');
description python: contextObj.utranslate(field.widget.description_msgid, domain='<!applicationName!>')"
tal:attributes="class python:'field ' + test(inError, ' error', '')">
<span class="fieldRequired" tal:condition="python: appyType['multiplicity'][0]>0"></span>
<div tal:condition="inError" tal:content="python: errors[fieldName]"></div>
<tal:stringField condition="python: appyType['type'] == 'String'">
<metal:edit use-macro="here/<!macros!>/macros/editString"/>
</tal:stringField>
<tal:booleanField condition="python: appyType['type'] == 'Boolean'">
<metal:edit use-macro="here/<!macros!>/macros/editBoolean"/>
</tal:booleanField>
</div>
<div metal:define-macro="showComputedField" i18n:domain="<!applicationName!>">
<span class="appyLabel" tal:condition="showLabel"
tal:content="field/widget/label_msgid" i18n:translate=""></span>
<tal:showValue define="theValue python: contextObj.getComputedValue(appyType)">
<span tal:condition="appyType/plainText" tal:replace="theValue"/>
<span tal:condition="not: appyType/plainText" tal:replace="structure theValue"/>
</tal:showValue>
</div>
<div metal:define-macro="showInfoField" i18n:domain="<!applicationName!>">
<span class="appyLabel" tal:content="field/widget/label_msgid" i18n:translate=""></span>
<span tal:content="field/widget/description_msgid" i18n:translate=""></span>
</div>
<div metal:define-macro="showActionField" i18n:domain="<!applicationName!>">
<form name="executeAppyAction" action="<!applicationName!>_do" method="POST">
<input type="hidden" name="actionType" value="appyAction"/>
<input type="hidden" name="objectUid" tal:attributes="value contextObj/UID"/>
<input type="hidden" name="fieldName" tal:attributes="value field/getName"/>
<input type="submit" name="do" i18n:attributes="value"
tal:attributes="value field/widget/label_msgid"/>
</form>
</div>
<div metal:define-macro="showArchetypesField" i18n:domain="<!applicationName!>"
tal:define="field fieldDescr/atField|widgetDescr/atField;
appyType fieldDescr/appyType|widgetDescr/appyType;
showLabel showLabel|python:True;"
tal:attributes="class python: contextObj.getCssClasses(appyType, asSlave=True)">
<tal:comment replace="nothing">For some fields we simply use the standard Archetypes
macro for showing it. Special Appy field types like Ref and Computed have their
corresponding Archetypes fields set as invisible, so they won't be shown by the following
tal:showField.</tal:comment>
<tal:showField define="mode python:test(isEdit, 'edit', 'view');"
tal:condition="python: test(isEdit, member.has_permission(field.write_permission, contextObj), member.has_permission(field.read_permission, contextObj))">
<tal:editField condition="isEdit">
<metal:editMacro use-macro="python:contextObj.widget(field.getName(), mode='edit', use_label=showLabel)" />
</tal:editField>
<tal:viewField tal:condition="not: isEdit">
<tal:defField>
<tal:fileField condition="python: (appyType['type'] == 'File')">
<span tal:condition="showLabel" tal:content="field/widget/label_msgid" i18n:translate=""></span>
<metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/>
</tal:fileField>
<tal:simpleField condition="python: (appyType['type'] in ('Integer', 'Float', 'Date', 'Boolean')) or (appyType['type'] == 'String' and (appyType['format'] == 0))">
<span tal:condition="showLabel" tal:content="field/widget/label_msgid" i18n:translate=""
tal:attributes="class python: 'appyLabel ' + contextObj.getCssClasses(appyType, asSlave=False);
id python: field.getAccessor(contextObj)()"></span>
<metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/>
</tal:simpleField>
<tal:formattedString condition="python: (appyType['type'] == 'String' and (appyType['format'] != 0))">
<fieldset tal:define="value python:field.getAccessor(contextObj)()">
<legend tal:condition="showLabel" i18n:translate="" tal:content="field/widget/label_msgid"></legend>
<span tal:condition="python: appyType['format'] == 1"
tal:replace="structure python: value.replace('\n', '&lt;br&gt;')"/>
<span tal:condition="python: appyType['format'] == 2" tal:replace="structure value"/>
</fieldset>
</tal:formattedString>
</tal:defField>
</tal:viewField>
</tal:showField>
<tal:comment replace="nothing">For other fields like Refs we use specific view/edit macros.</tal:comment>
<tal:viewRef condition="python: (not isEdit) and (appyType['type'] == 'Ref')">
<tal:ref define="isBack python:False;
fieldRel python:field.relationship;
objs python:contextObj.getAppyRefs(field.getName());
labelMsgId field/widget/label_msgid;
descrMsgId field/widget/description_msgid;
innerRef innerRef|python:False">
<metal:viewRef use-macro="here/<!applicationName!>AppyReference/macros/showReference" />
</tal:ref>
</tal:viewRef>
<tal:editRef condition="python: isEdit and (appyType['type'] == 'Ref')">
<tal:ref define="appyType fieldDescr/appyType|widgetDescr/appyType"
condition="python: appyType['link']==True">
<metal:editRef use-macro="here/<!applicationName!>AppyReference/macros/editReference" />
</tal:ref>
</tal:editRef>
<tal:computedField condition="python: (not isEdit) and (appyType['type'] == 'Computed')">
<metal:cf use-macro="here/<!macros!>/macros/showComputedField" />
</tal:computedField>
<tal:actionField condition="python: (not isEdit) and (appyType['type'] == 'Action')">
<metal:af use-macro="here/<!macros!>/macros/showActionField" />
</tal:actionField>
<tal:masterString condition="python: isEdit and (appyType['type'] in ('String', 'Boolean')) and (appyType['slaves'])">
<metal:mf use-macro="here/<!macros!>/macros/editField" />
</tal:masterString>
<tal:infoField condition="python: (not isEdit) and (appyType['type'] == 'Info')">
<metal:af use-macro="here/<!macros!>/macros/showInfoField" />
</tal:infoField>
</div>
<div metal:define-macro="showBackwardField" i18n:domain="<!applicationName!>"
tal:define="isBack python:True;
appyType widgetDescr/appyType;
fieldRel widgetDescr/fieldRel;
objs python:contextObj.getBRefs(fieldRel);
labelMsgId python:'%s_%s_back' % (contextObj.meta_type, appyType['backd']['attribute']);
descrMsgId python:'';
innerRef innerRef|python:False">
<div metal:use-macro="here/<!applicationName!>AppyReference/macros/showReference" />
</div>
<span metal:define-macro="showGroup" i18n:domain="<!applicationName!>">
<fieldset class="appyGroup">
<legend><i i18n:translate=""
tal:content="python: '%s_group_%s' % (contextObj.meta_type, widgetDescr['name'])"></i></legend>
<table tal:define="global fieldNb python:-1" width="100%">
<tr valign="top" tal:repeat="rowNb python:range(widgetDescr['rows'])">
<td tal:repeat="colNb python:range(widgetDescr['cols'])" tal:attributes="width python: str(100.0/widgetDescr['cols']) + '%'">
<tal:showField define="global fieldNb python:fieldNb+1;
hasFieldDescr python: test(fieldNb < len(widgetDescr['fields']), True, False);"
tal:condition="hasFieldDescr">
<tal:field define="fieldDescr python:widgetDescr['fields'][fieldNb]">
<tal:archetypesField condition="python: fieldDescr['widgetType'] == 'field'">
<metal:atField use-macro="here/<!macros!>/macros/showArchetypesField"/>
</tal:archetypesField>
<tal:backwardRef tal:condition="python: (not isEdit) and (fieldDescr['widgetType'] == 'backField')">
<metal:backRef use-macro="here/<!macros!>/macros/showBackwardField" />
</tal:backwardRef>
</tal:field>
</tal:showField>
</td>
</tr>
</table>
</fieldset>
<br/>
</span>
<div metal:define-macro="listFields" i18n:domain="<!applicationName!>"
tal:repeat="widgetDescr python: contextObj.getAppyFields(isEdit, pageName)">
<tal:displayArchetypesField condition="python: widgetDescr['widgetType'] == 'field'">
<tal:atField condition="python: widgetDescr['page'] == pageName">
<metal:field use-macro="here/<!macros!>/macros/showArchetypesField" />
</tal:atField>
</tal:displayArchetypesField>
<tal:displayBackwardRef condition="python: (not isEdit) and (widgetDescr['widgetType'] == 'backField')">
<tal:backRef condition="python: widgetDescr['appyType']['backd']['page'] == pageName">
<metal:field metal:use-macro="here/<!macros!>/macros/showBackwardField" />
</tal:backRef>
</tal:displayBackwardRef>
<tal:displayGroup condition="python: widgetDescr['widgetType'] == 'group'">
<tal:displayG condition="python: widgetDescr['page'] == pageName">
<metal:group metal:use-macro="here/<!macros!>/macros/showGroup" />
</tal:displayG>
</tal:displayGroup>
</div>
<span metal:define-macro="byline" i18n:domain="<!applicationName!>"
tal:condition="python: site_properties.allowAnonymousViewAbout or not isAnon"
tal:define="creator here/Creator;" class="documentByLine">
<tal:name tal:condition="creator"
tal:define="author python:context.portal_membership.getMemberInfo(creator)">
<span class="documentAuthor" i18n:domain="plone" i18n:translate="label_by_author">
by <a tal:attributes="href string:${portal_url}/author/${creator}"
tal:content="python:author and author['fullname'] or creator"
tal:omit-tag="not:author" i18n:name="author"/>
&mdash;
</span>
</tal:name>
<span class="documentModified">
<span i18n:translate="box_last_modified" i18n:domain="plone"/>
<span tal:replace="python:toLocalizedTime(here.ModificationDate(),long_format=1)"/>
</span>
</span>
<span metal:define-macro="workflowHistory" i18n:domain="<!applicationName!>" class="reviewHistory"
tal:define="history context/getWorkflowHistory" tal:condition="history">
<dl id="history" class="collapsible inline collapsedOnLoad">
<dt class="collapsibleHeader" i18n:translate="label_history" i18n:domain="plone">History</dt>
<dd class="collapsibleContent">
<table width="100%" class="listing nosort" i18n:attributes="summary summary_review_history"
tal:define="review_history python:context.portal_workflow.getInfoFor(context, 'review_history', []);
review_history python:[review for review in review_history if review.get('action','')]"
tal:condition="review_history">
<tr i18n:domain="plone">
<th i18n:translate="listingheader_action"/>
<th i18n:translate="listingheader_performed_by"/>
<th i18n:translate="listingheader_date_and_time"/>
<th i18n:translate="listingheader_comment"/>
</tr>
<metal:block tal:define="review_history python: portal.reverseList(review_history);"
tal:repeat="items review_history">
<tr tal:define="odd repeat/items/odd;
rhComments items/comments|nothing;
state items/review_state|nothing"
tal:attributes="class python:test(odd, 'even', 'odd')" tal:condition="items/action">
<td i18n:translate="" tal:content="python: context.getWorkflowLabel(items['action'])"
tal:attributes="class string:state-${state}"/>
<td tal:define="actorid python:items.get('actor');
actor python:context.portal_membership.getMemberInfo(actorid);
fullname actor/fullname|nothing;
username actor/username|nothing"
tal:content="python:fullname or username or actorid"/>
<td tal:content="python:toLocalizedTime(items['time'],long_format=True)"/>
<td> <tal:comment condition="rhComments" tal:content="rhComments"/>
<tal:noComment condition="not: rhComments" i18n:translate="no_comments" i18n:domain="plone"/>
</td>
</tr>
</metal:block>
</table>
</dd>
</dl>
</span>
<div metal:define-macro="showPagePrologue" i18n:domain="<!applicationName!>">
<tal:comment replace="nothing">Global Javascript functions, used in edit and
consult views, are defined gere.</tal:comment>
<script language="javascript">
<!--
// This function turns a checkbox into a radio button... sort of
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
vis = document.getElementById(visibleCheckbox);
hidden = document.getElementById(hiddenBoolean);
if (vis.checked) hidden.value = 'True';
else hidden.value = 'False';
}
// Returns an array of selected options in a select widget
function getMasterValue(widget) {
res = new Array();
if (widget.type == 'checkbox') {
var mv = widget.checked + '';
mv = mv.charAt(0).toUpperCase() + mv.substr(1);
res.push(mv);
}
else { // SELECT widget
for (var i=0; i < widget.options.length; i++) {
if (widget.options[i].selected) res.push(widget.options[i].value);
}
}
return res;
}
// Given the value(s) selected in a master field, this function updates the
// state of all corresponding slaves.
function updateSlaves(masterValues, appyTypeId) {
var slaves = cssQuery('div.slave_' + appyTypeId);
for (var i=0; i< slaves.length; i++){
slaves[i].style.display = "none";
}
for (var i=0; i < masterValues.length; i++) {
var activeSlaves = cssQuery('div.slaveValue_' + appyTypeId + '_' + masterValues[i]);
for (var j=0; j < activeSlaves.length; j++){
activeSlaves[j].style.display = "";
}
}
}
// Triggers a workflow transition
function triggerTransition(transitionId) {
var theForm = document.getElementById('triggerTransitionForm');
theForm.workflow_action.value = transitionId;
theForm.submit();
}
-->
</script>
</div>
<div metal:define-macro="showPageHeader" i18n:domain="<!applicationName!>"
tal:define="appyPages python: context.getAppyPages(phase);
showCommonInfo python: not isEdit"
tal:condition="python: not context.portal_factory.isTemporary(context)">
<tal:comment replace="nothing">Information that is common to all tabs (object title, state, etc)</tal:comment>
<table width="100%" tal:condition="showCommonInfo" class="appyCommonInfo">
<tr valign="bottom">
<tal:comment replace="nothing">Title, edit icon and state</tal:comment>
<td width="80%">
<b class="appyTitle" tal:content="title_string | here/title_or_id"></b>
<tal:comment replace="nothing">Show the phase name tied to this page</tal:comment>
<span class="discreet" tal:condition="python: phaseInfo['totalNbOfPhases']&gt;1">&minus;
<span i18n:translate="phase"></span>:
<span tal:define="label python:'%s_phase_%s' % (context.meta_type, phase)"
tal:content="label" i18n:translate=""></span>
</span>
<tal:comment replace="nothing">When no tabs are shown, we provide an edit icon.</tal:comment>
<img tal:define="editPageName python:test(pageName=='main', 'default', pageName)"
src="edit.gif" title="Edit" i18n:domain="plone" i18n:attributes="title"
style="cursor:pointer"
tal:attributes="onClick python: 'href: window.location=\'%s/<!applicationName!>_appy_edit?fieldset=%s&phase=%s\'' % (context.absolute_url(), editPageName, phase)"
tal:condition="python: (len(appyPages)==1) and member.has_permission('Modify portal content', context)"/>
</td>
<td><metal:actions use-macro="here/document_actions/macros/document_actions"/>
</td>
</tr>
<tr tal:define="descrLabel python: here.utranslate('%s_edit_descr' % here.portal_type, domain='<!applicationName!>')" tal:condition="descrLabel/strip" >
<tal:comment replace="nothing">Content type description</tal:comment>
<td colspan="2" class="discreet" tal:content="descrLabel"/>
</tr>
<tr>
<td>
<metal:byLine use-macro="here/<!macros!>/macros/byline"/>
<tal:showWorkflow condition="showWorkflow">
<metal:workflowHistory use-macro="here/<!macros!>/macros/workflowHistory"/>
</tal:showWorkflow>
</td>
<td valign="top"><metal:pod use-macro="here/<!macros!>/macros/listPodTemplates"/>
</td>
</tr>
<tal:comment replace="nothing">Workflow-related information and actions</tal:comment>
<tr tal:condition="python: showWorkflow and context.getWorkflowLabel()">
<td colspan="2" class="appyWorkflow">
<table width="100%">
<tr>
<td><metal:states use-macro="here/<!macros!>/macros/states"/></td>
<td align="right"><metal:states use-macro="here/<!macros!>/macros/transitions"/></td>
</tr>
</table>
</td>
</tr>
</table>
<tal:comment replace="nothing">Tabs</tal:comment>
<ul class="contentViews appyTabs" tal:condition="python: len(appyPages)>1">
<li tal:repeat="thePage appyPages"
tal:attributes="class python:test(thePage == pageName, 'selected', 'plain')">
<tal:tab define="pageLabel python: '%s_page_%s' % (here.meta_type, thePage)">
<a i18n:translate="" tal:content="pageLabel"
tal:attributes="href python: here.absolute_url() + '/<!applicationName!>_appy_view?phase=%s&pageName=%s' % (phase, thePage)">
</a>
<img tal:define="editPageName python:test(thePage=='main', 'default', thePage)"
src="edit.gif" title="Edit" i18n:domain="plone" i18n:attributes="title"
style="cursor:pointer" class="appyPlusImg"
tal:attributes="onClick python: 'href: window.location=\'%s/<!applicationName!>_appy_edit?fieldset=%s&phase=%s\'' % (context.absolute_url(), editPageName, phase)"
tal:condition="python: member.has_permission('Modify portal content', context)"/>
</tal:tab>
</li>
</ul>
</div>
<div metal:define-macro="showPageFooter" i18n:domain="<!applicationName!>">
<script language="javascript">
<!--
// When the current page is loaded, we must set the correct state for all slave fields.
var masters = cssQuery('.appyMaster');
for (var i=0; i < masters.length; i++) {
var cssClasses = masters[i].className.split(' ');
for (var j=0; j < cssClasses.length; j++) {
if (cssClasses[j].indexOf('master_') == 0) {
var appyId = cssClasses[j].split('_')[1];
var masterValue = [];
if (masters[i].nodeName == 'SPAN'){
var idField = masters[i].id;
if (idField == '') {
masterValue.push(idField);
}
else {
if (idField[0] == '(') {
// There are multiple values, split it
var subValues = idField.substring(1, idField.length-1).split(',');
for (var k=0; k < subValues.length; k++){
var subValue = subValues[k].strip();
masterValue.push(subValue.substring(1, subValue.length-1));
}
}
else { masterValue.push(masters[i].id);
}
}
}
else { masterValue = getMasterValue(masters[i]);
}
updateSlaves(masterValue, appyId);
}
}
}
-->
</script>
</div>
<div metal:define-macro="queryResult" i18n:domain="<!applicationName!>">
<script language="javascript">
<!--
function getSortValue(row, fieldName) {
// Find, from p_fieldName, the cell that is used for sorting.
var cellId = "field_" + fieldName;
var cells = row.cells;
for (var i=0; i < cells.length; i++) {
if (cells[i].id == cellId) {
// Ok we have the cell on which we must sort.
// Now get the cell content.
// If the cell contains links, content is the 1st link content
var innerLinks = cells[i].getElementsByTagName("a");
if (innerLinks.length > 0) {
return innerLinks[0].innerHTML;
} else {
return cells[i].innerHTML;
}
}
}
}
function sortRows(fieldName, ascending) {
var queryRows = cssQuery('#query_row');
// Create a wrapper for sorting
var RowWrapper = function(row, fieldName) {
this.value = getSortValue(row, fieldName);
this.cloned_node = row.cloneNode(true);
this.toString = function() {
if (this.value.toString) {
return this.value.toString();
} else {
return this.value;
}
}
}
// Wrap nodes
var items = new Array();
for (var i=0; i<queryRows.length; i++) {
items.push(new RowWrapper(queryRows[i], fieldName));
}
// Sort nodes
items.sort();
if (!ascending) {
items.reverse();
}
// Reorder nodes
for (var i=0; i<items.length; i++) {
var dest = queryRows[i];
dest.parentNode.replaceChild(items[i].cloned_node, dest);
}
};
function onSort(fieldName){
// First, switch the sort arrow (up->down or down->up)
var arrow = document.getElementById("arrow_" + fieldName);
var sortAscending = (arrow.src.indexOf('arrowDown.gif') != -1);
if (sortAscending){
// Display "up" image
arrow.src = arrow.src.replace('arrowDown.gif', 'arrowUp.gif')
}
else { // Display "down" image
arrow.src = arrow.src.replace('arrowUp.gif', 'arrowDown.gif')
}
// Then, sort the rows on column "fieldName".
sortRows(fieldName, sortAscending);
}
function cellMatches(cell, searchValue) {
// This function returns true if the HTML p_cell contains p_searchValue
var innerLinks = cell.getElementsByTagName("a");
// If the cell contains links, we search within the link contents
for (var i=0; i < innerLinks.length; i++){
var linkContent = innerLinks[i].innerHTML.toLowerCase();
if (linkContent.indexOf(searchValue) != -1) {
return true;
}
}
// If we are here, we still have no match. Let's search directly within
// the cell.
var cellContent = cell.innerHTML.toLowerCase();
if (cellContent.indexOf(searchValue) != -1) {
return true;
}
return false;
}
function onTextEntered(fieldName) {
// Is called whenever text is entered into field named p_fieldName.
var cellId = "field_" + fieldName
var field = document.getElementById("filter_" + fieldName);
var fieldValue = field.value.toLowerCase();
if (fieldValue.length >= 3) {
// Browse all rows and check if it should be visible or not.
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
// Find the value of the cell.
var queryCells = queryRows[i].cells;
for (var j=0; j < queryCells.length; j++) {
if (queryCells[j].id == cellId) {
if (cellMatches(queryCells[j], fieldValue)) {
queryRows[i].style.display = "";
}
else {
queryRows[i].style.display = "none";
}
}
}
}
}
else {
// Show all rows
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
queryRows[i].style.display = "";
}
}
}
-->
</script>
<table class="vertical listing" width="100%"
tal:define="fieldDescrs python: context.<!toolInstanceName!>.getResultColumns(queryResult[0].getObject(), queryName);">
<tal:comment replace="nothing">Every item in fieldDescr is a FieldDescr instance,
excepted for workflow state (which is not a field): in thi case it is simply string
"workflowState".</tal:comment>
<tal:comment replace="nothing">Headers, with filters and sort arrows</tal:comment>
<tr>
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<th><img tal:attributes= "src python: '%s/arrowDown.gif' % context.absolute_url();
onClick python:'javascript:onSort(\'title\')';"
id="arrow_title" style="cursor:pointer"/>
<span i18n:translate="ref_name"/>
<input id="filter_title" type="text" size="10" onkeyup="javascript:onTextEntered('title')"/>
</th>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:columnHeader repeat="fieldDescr fieldDescrs">
<th tal:define="fieldName fieldDescr/atField/getName|string:workflow_state">
<img tal:attributes= "src string: '$portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'%s\')' % fieldName;
id python: 'arrow_%s' % fieldName"
style="cursor:pointer"/>
<tal:comment replace="nothing">Display header for a "standard" field</tal:comment>
<tal:standardField condition="python: fieldName != 'workflow_state'">
<span i18n:translate="" tal:content="fieldDescr/atField/widget/label_msgid"/>
</tal:standardField>
<tal:comment replace="nothing">Display header for the workflow state</tal:comment>
<tal:workflowState condition="python: fieldName == 'workflow_state'">
<span i18n:translate="workflow_state"/>
</tal:workflowState>
<input type="text" size="10"
tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/>
</th>
</tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment>
<th tal:condition="mainTabSelected"><img
tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/>
<span i18n:translate="root_type"/>
<input type="text" size="10" id="filter_root_type"
tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/>
</th>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
<th i18n:translate="ref_actions"></th>
</tr>
<tal:comment replace="nothing">Results</tal:comment>
<tr tal:repeat="brain queryResult" id="query_row">
<tal:row define="obj brain/getObject">
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<td id="field_title"><a tal:content="brain/Title"
tal:attributes="href python: obj.absolute_url()"></a></td>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:otherFields repeat="fieldDescr fieldDescrs">
<tal:standardField condition="python: fieldDescr != 'workflowState'">
<td tal:attributes="id python:'field_%s' % fieldDescr['atField'].getName()">
<tal:field define="contextObj python:obj;
isEdit python:False;
showLabel python:False;
innerRef python:True"
condition="python: contextObj.showField(fieldDescr)">
<metal:field use-macro="here/<!macros!>/macros/showArchetypesField"/>
</tal:field>
</td>
</tal:standardField>
<tal:workflowState condition="python: fieldDescr == 'workflowState'">
<td id="field_workflow_state" i18n:translate="" tal:content="obj/getWorkflowLabel"></td>
</tal:workflowState>
</tal:otherFields>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment>
<td tal:condition="mainTabSelected" tal:content="obj/portal_type"
i18n:translate="" id="field_root_type"></td>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
<td align="right">
<table class="no-style-table" cellpadding="0" cellspacing="0">
<tr>
<tal:comment replace="nothing">Edit the element</tal:comment>
<td class="noPadding"><a tal:attributes="href python: obj.absolute_url() + '/edit'"
tal:condition="python: member.has_permission('Modify portal content', obj)">
<img src="edit.gif" title="Edit" i18n:domain="plone" i18n:attributes="title" />
</a></td>
<tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"><a tal:attributes="href python: obj.absolute_url() + '/delete_confirmation'"
tal:condition="python: member.has_permission('Delete objects', obj)">
<img src="delete_icon.gif" title="Delete" i18n:domain="plone" i18n:attributes="title" />
</a></td>
</tr>
</table>
</td>
</tal:row>
</tr>
</table>
<div metal:use-macro="context/batch_macros/macros/navigation" />
</div>
<metal:phases define-macro="phases" i18n:domain="<!applicationName!>">
<tal:comment replace="nothing">This macro displays phases defined for a given content type,
only if more than one phase is defined.</tal:comment>
<table width="100%" tal:define="phases context/getAppyPhases|nothing"
tal:condition="python: phases and (len(phases)>1)" cellspacing="1" cellpadding="0">
<tal:phase repeat="phase phases">
<tr>
<td tal:define="label python:'%s_phase_%s' % (context.meta_type, phase['name']);
displayLink python: (phase['phaseStatus'] != 'Future') and ('/portal_factory' not in context.absolute_url())"
tal:attributes="class python: 'appyPhase step' + phase['phaseStatus']">
<a tal:attributes="href python: '%s?phase=%s&pageName=%s' % (context.absolute_url(), phase['name'], phase['pages'][0]);" tal:condition="displayLink"
i18n:translate="" tal:content="label"/>
<span tal:condition="not: displayLink" i18n:translate="" tal:content="label"/>
</td>
</tr>
<tr tal:condition="python: phase['name'] != phases[-1]['name']">
<td align="center"><img tal:attributes="src string: $portal_url/nextPhase.png"/></td>
</tr>
</tal:phase>
</table>
</metal:phases>
<metal:states define-macro="states" i18n:domain="<!applicationName!>"
tal:define="showAllStatesInPhase python: flavour.getAttr('showAllStatesInPhaseFor' + contextObj.meta_type);
states python: context.getAppyStates(phase, currentOnly=not showAllStatesInPhase)"
tal:condition="python: test(showAllStatesInPhase, len(states)&gt;1, True)">
<table>
<tr>
<tal:state repeat="stateInfo states">
<td tal:attributes="class python: 'appyState step' + stateInfo['stateStatus']"
tal:content="python: context.getWorkflowLabel(stateInfo['name'])" i18n:translate="">
</td>
<td tal:condition="python: stateInfo['name'] != states[-1]['name']">
<img tal:attributes="src string: $portal_url/nextState.png"/>
</td>
</tal:state>
</tr>
</table>
</metal:states>
<metal:transitions define-macro="transitions" i18n:domain="<!applicationName!>"
tal:define="transitions python: contextObj.portal_workflow.getTransitionsFor(contextObj);"
tal:condition="transitions">
<form id="triggerTransitionForm" method="post"
tal:attributes="action python: contextObj.absolute_url() + '/<!applicationName!>_do'">
<input type="hidden" name="actionType" value="triggerTransition"/>
<input type="hidden" name="workflow_action"/>
<table>
<tr>
<tal:comment replace="nothing">Input field allowing to enter a comment before triggering a transition</tal:comment>
<td tal:define="showCommentsField python:flavour.getAttr('showWorkflowCommentFieldFor'+context.meta_type)"
align="right" tal:condition="showCommentsField">
<span i18n:translate="workflow_comment" class="discreet"></span>
<input type="text" id="comment" name="comment" size="35"/>
</td>
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
<td align="right" tal:repeat="transition transitions">
<input type="button" i18n:attributes="value" class="context"
tal:attributes="value python: transition['name'];
onClick python: 'javascript: triggerTransition(\'%s\')' % transition['id']"/>
</td>
</tr>
</table>
</form>
</metal:transitions>

View file

@ -0,0 +1,34 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.PodTemplateMixin import PodTemplateMixin
from Extensions.appyWrappers import <!wrapperClass!>
schema = Schema((<!fields!>
),)
fullSchema = BaseSchema.copy() + schema.copy()
class <!applicationName!>PodTemplate(BaseContent, PodTemplateMixin):
'''POD template.'''
security = ClassSecurityInfo()
__implements__ = (getattr(BaseContent,'__implements__',()),)
archetype_name = '<!applicationName!>PodTemplate'
meta_type = '<!applicationName!>PodTemplate'
portal_type = '<!applicationName!>PodTemplate'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
#content_icon = '<!applicationName!>PodTemplate.gif'
immediate_view = '<!applicationName!>_appy_view'
default_view = '<!applicationName!>_appy_view'
suppl_views = ()
typeDescription = "<!applicationName!>PodTemplate"
typeDescMsgId = '<!applicationName!>_edit_descr'
_at_rename_after_creation = True
wrapperClass = <!wrapperClass!>
schema = fullSchema
<!commonMethods!>
<!methods!>
registerType(<!applicationName!>PodTemplate, '<!applicationName!>')

View file

@ -0,0 +1,45 @@
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="<!applicationName!>">
<body>
<div metal:define-macro="portlet"
tal:define="tool python: context.<!toolInstanceName!>"
tal:condition="tool/showPortlet">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<dl class="portlet"
tal:define="rootClasses python:[<!rootClasses!>];
appFolder tool/getAppFolder">
<tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment>
<dt class="portletHeader">
<span i18n:translate="" tal:content="python: '<!applicationName!>'">
</span>&nbsp;
<img i18n:attributes="title" style="cursor:pointer" title="<!applicationName!>Tool"
tal:condition="python: member.has_role('Manager')"
tal:attributes="onClick python: 'href: window.location=\'%s/\'' % tool.absolute_url();
src python: here.<!toolInstanceName!>.absolute_url() + '/appyConfig.gif'"/>
</dt>
<tal:comment replace="nothing">Links to flavours</tal:comment>
<dt class="portletAppyItem" tal:repeat="flavourInfo tool/getFlavoursInfo">
<a tal:define="flavourNumber flavourInfo/number;
rootTypes python: test(flavourNumber==1, rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootTypes)"
tal:content="flavourInfo/title"
i18n:translate="" title="query_consult_all" i18n:attributes="title"
tal:attributes="href python:'%s/<!queryName!>?query=%s&flavourNumber=%d' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"></a>
</dt>
<dt class="portletAppyItem" tal:condition="python: context.meta_type in rootClasses">
<metal:phases use-macro="here/<!macros!>/macros/phases"/>
</dt>
</dl>
</div>
</body>
</html>

View file

@ -0,0 +1,19 @@
# ------------------------------------------------------------------------------
from Products.CMFCore.utils import getToolByName
# ------------------------------------------------------------------------------
def installProduct(context):
'''Installs the necessary products for running PloneMeeting.'''
portal = context.getSite()
qi = getToolByName(portal, 'portal_quickinstaller')
if not qi.isProductInstalled('Archetypes'):
qi.installProduct('Archetypes')
if not qi.isProductInstalled('<!applicationName!>'):
qi.installProduct('<!applicationName!>')
return "Product <!applicationName!> installed."
# ------------------------------------------------------------------------------
def install_default(context):
# Installation function of default profile.
installProduct(context)
# ------------------------------------------------------------------------------

63
gen/plone25/templates/Query.pt Executable file
View file

@ -0,0 +1,63 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="<!applicationName!>">
<!-- This page presents results of queries -->
<body>
<div metal:fill-slot="top_slot">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<div tal:define="dummy python:request.set('disable_border', 1)" />
</div>
<div metal:fill-slot="main"
tal:define="tool python: context.<!toolInstanceName!>;
queryName python:context.REQUEST.get('query');
flavourNumber python:context.REQUEST.get('flavourNumber');
rootClasses python:[<!rootClasses!>];
rootTypes python: test(flavourNumber=='1', rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootClasses);
mainTabSelected python: queryName.find(',') != -1;
appFolder tool/getAppFolder">
<span tal:condition="python: queryName and (queryName != 'none')">
<span tal:define="queryResult python: context.<!toolInstanceName!>.executeQuery(queryName, int(flavourNumber));
batch queryResult">
<!-- Tabs -->
<ul class="contentViews appyTabs">
<!-- Tab "All objects" -->
<li tal:define="selected python:mainTabSelected"
tal:attributes="class python:test(selected, 'selected', 'plain')"
tal:condition="python: len(rootClasses)>1">
<a tal:attributes="href python: '%s/<!queryName!>?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"
i18n:translate="">query_consult_all</a>
</li>
<!-- One tab for each root content type -->
<tal:tab repeat="rootContentType rootTypes">
<li tal:define="selected python:queryName == rootContentType"
tal:attributes="class python:test(selected, 'selected', 'plain')">
<a i18n:translate="" tal:content="rootContentType"
tal:attributes="href python: '%s/<!queryName!>?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootContentType, flavourNumber)"/>
<img i18n:attributes="title" style="cursor:pointer" title="query_create" class="appyPlusImg"
tal:define="addPermission python: '<!applicationName!>: Add %s' % rootContentType"
tal:attributes="onClick python: 'href: window.location=\'%s/createObject?type_name=%s\'' % (appFolder.absolute_url(), rootContentType);
src python: here.<!toolInstanceName!>.absolute_url() + '/plus.png'"
tal:condition="python: member.has_permission(addPermission, appFolder)"/>
</li>
</tal:tab>
</ul>
<br/>
<!-- Query result -->
<span tal:condition="queryResult">
<span metal:use-macro="here/<!macros!>/macros/queryResult"></span>
</span>
<span tal:condition="not: queryResult"
i18n:translate="query_no_result">No result.</span>
</span>
</span>
</div>
</body>
</html>

View file

@ -0,0 +1,264 @@
/* <dtml-var "enableHTTPCompression(request=REQUEST, debug=1, css=1)"> (this is for http compression) */
/* <dtml-with base_properties> (do not remove this :) */
/* <dtml-call "REQUEST.set('portal_url', portal_url())"> (not this either :) */
#portal-breadcrumbs {
display: none;
}
.appyPod {
float:right;
}
.appyTabs {
margin-bottom: 1em;
}
.appyTabs li a {
border-bottom:1px solid transparent;
font-size: 90%;
}
.appyTabs li a:visited {
color: #578308;
}
.appyTitle {
padding-top: 0.5em;
font-size: 110%;
}
.appyLabel {
font-weight: bold;
padding-right: 0.4em;
}
.appyPhase {
border-style: solid;
border-width: thin;
text-align: center;
padding: 0 1em 0 1.3em;
}
.appyState {
font-size: 85%;
font-style: normal;
border-style: solid;
border-width: thin;
text-align: center;
padding: 0.1em 1em 0.1em 1.3em;
}
/* stepxx classes are used for displaying status of a phase or state. */
.stepDone {
background-color: #cde2a7;
background-image: url(&dtml-portal_url;/done.png);
background-repeat: no-repeat;
background-position: center left;
}
.stepCurrent {
background-color: #ffce7b;
background-image: url(&dtml-portal_url;/current.png);
background-repeat: no-repeat;
background-position: center left;
}
.stepFuture {
background-color: #ffffff;
color: #C8C8C8;
border-style: dashed;
}
.stepUnselected {
background-color: #ffffff;
}
.appyPlusImg {
vertical-align: top;
position: relative;
left: -1.4em;
top: -0.55em;
}
.appyRefEdit {
line-height: 1.5em;
}
.appyCommonInfo {
border-color: #ffa500;
background-color: &dtml-evenRowBackgroundColor;;
border-style: solid;
border-width: 2px;
margin-bottom: 1em;
}
.appyWorkflow {
text-align: center;
background-color: &dtml-globalBackgroundColor;;
}
dl.expandedInlineCollapsible dt.collapsibleHeader, dl.expandedBlockCollapsible dt.collapsibleHeader {
background:#dee7ec url(treeExpanded.gif) no-repeat scroll 6px 50%;
border-width 1px;
border-color: #8cacbb;
border-style: solid;
border-width: thin;
}
/* With fields layout in columns, standard error frame is too large */
.error {
padding: 0.4em;
}
/* Table styles */
.no-style-table {
border: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
.no-style-table td {
border: 0 !important;
padding-left: 0 !important;
margin: 0 !important;
}
/* Minor layout changes in fieldsets and tables */
fieldset {
margin: 0em 0em;
line-height: 1.0em;
}
/* Group fieldsets */
.appyGroup {
border-width: 2px;
}
.imageInput {
border-width: 0px;
background: none;
}
.noPadding {
padding-right: 0em !important;
padding-left: 0em !important;
padding-top: 0em !important;
padding-bottom: 0em !important;
}
.appyButton {
background: &dtml-globalBackgroundColor; url(&dtml-portal_url;/linkOpaque.gif) 5px 1px no-repeat;
cursor: pointer;
font-size: &dtml-fontSmallSize;;
padding: 1px 1px 1px 12px;
text-transform: &dtml-textTransform;;
/* overflow: visible; IE produces ugly results with this */
}
.listing {
margin: 0em 0em;
}
.listing td, .stx table td {
padding-right: 0.1em;
padding-left: 0.3em;
padding-top: 0.3em;
padding-bottom: 0em;
}
.vertical td {
padding-left: 0.3em;
}
.innerAppyTable {
border-width: 0px;
}
.innerAppyTable td {
border-top: 0px;
border-bottom: 1px solid #8CACBB;
border-right: 0px;
padding: 0.4em 0em 0em 0em;
border-collapse: separate;
}
.innerAppyTable th {
border-right: 0px;
}
.innerAppyFieldset {
margin: 0em 1em;
line-height: 1.0em;
}
/* Portlet elements */
.portletAppyItem {
margin: 0;
padding: 1px 0.5em;
border-left: 1px solid #8cacbb;
border-right: 1px solid #8cacbb;
font-weight: normal;
}
/* Uncomment this if you want to hide breadcrumbs */
/*
#portal-breadcrumbs {
display: none;
}
*/
/* </dtml-with> */
/* image-right, but without border */
.image-right {
border:0px solid Black;
clear:both;
float:right;
margin:0.5em;
}
/* DOCUMENTATION ON PRE-DEFINED PROPERTIES FROM PLONE */
/* You can insert colors and other variables from Plone's
base_properties by doing:
& dtml-variableName ; (without the spaces, excluded here to not make it render)
Example:
myLink {
color: & dtml-fontColor ; (again, without the spaces)
}
This means you can generate your own elements that use Plone's defaults,
and respect any customizations people have done. See base_properties for
the default values.
These are the available properties:
logoName - the file name of the portal logo.
fontFamily - the font family used for all text that is not headers
fontBaseSize - the base font size that everything is calculated from
fontColor - the main font color
backgroundColor - the background color
linkColor - the color used on normal links
linkActiveColor - color used on active links
linkVisitedColor - color used on visited links
borderWidth - the width of most borders in Plone
borderStyle - the style of the border lines, normally solid
borderStyleAnnotations - style of border lines on comments etc
globalBorderColor - the border color used on the main tabs, the portlets etc
globalBackgroundColor - background color for the selected tabs, portlet headings etc
globalFontColor - the color of the font in the tabs and in portlet headings
headingFontFamily - font family for h1/h2/h3/h4/h5/h6 headlines.
headingFontBaseSize - the base size used when calculating the different headline sizes
contentViewBorderColor - the content view tabs border color
contentViewBackgroundColor - the content view tabs background color
contentViewFontColor - the font color used in the content view tabs
textTransform - whether to lowercase text in portlets, tabs etc.
evenRowBackgroundColor - the background color of even rows in listings
oddRowBackgroundColor - the background color of even rows in listings
notifyBorderColor - border color of notification elements like the status message, the calendar focus
notifyBackgroundColor - background color of notification elements like the status message, the calendar focus
discreetColor:string=#999999
helpBackgroundColor:string=#ffffe1
*/

View file

@ -0,0 +1,49 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.CMFCore.utils import UniqueObject
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.ToolMixin import ToolMixin
from Extensions.appyWrappers import AbstractWrapper, <!wrapperClass!>
predefinedSchema = Schema((<!predefinedFields!>
),)
schema = Schema((<!fields!>
),)
fullSchema = OrderedBaseFolderSchema.copy() + predefinedSchema.copy() + schema.copy()
class <!toolName!>(UniqueObject, OrderedBaseFolder, ToolMixin):
'''Tool for <!applicationName!>.'''
security = ClassSecurityInfo()
__implements__ = (getattr(UniqueObject,'__implements__',()),) + (getattr(OrderedBaseFolder,'__implements__',()),)
archetype_name = '<!toolName!>'
meta_type = '<!toolName!>'
portal_type = '<!toolName!>'
allowed_content_types = ()
filter_content_types = 0
global_allow = 0
#content_icon = '<!toolName!>.gif'
immediate_view = '<!applicationName!>_appy_view'
default_view = '<!applicationName!>_appy_view'
suppl_views = ()
typeDescription = "<!toolName!>"
typeDescMsgId = '<!toolName!>_edit_descr'
i18nDomain = '<!applicationName!>'
wrapperClass = <!wrapperClass!>
_at_rename_after_creation = True
schema = fullSchema
schema["id"].widget.visible = False
schema["title"].widget.visible = False
# When browsing into the tool, the 'configure' portlet should be dislayed.
left_slots = ['here/portlet_prefs/macros/portlet']
right_slots = []
# Tool constructor has no id argument, the id is fixed.
def __init__(self, id=None):
OrderedBaseFolder.__init__(self, '<!toolInstanceName!>')
self.setTitle('<!applicationName!>')
<!commonMethods!>
<!predefinedMethods!>
<!methods!>
registerType(<!toolName!>, '<!applicationName!>')

View file

@ -0,0 +1,23 @@
<!codeHeader!>
from config import *
import logging
try:
import CustomizationPolicy
except ImportError:
CustomizationPolicy = None
from Products.CMFCore import utils as cmfutils
from Products.CMFCore import DirectoryView
from Products.CMFPlone.utils import ToolInit
from Products.Archetypes.atapi import *
from Products.Archetypes import listTypes
from appy.gen.plone25.installer import ZopeInstaller
logger = logging.getLogger(PROJECTNAME)
def initialize(context):
<!imports!>
# I need to do those imports here; else, types and add permissions will not
# be registered.
ZopeInstaller(context, PROJECTNAME,
<!applicationName!>Tool.<!applicationName!>Tool,
DEFAULT_ADD_CONTENT_PERMISSION, ADD_CONTENT_PERMISSIONS,
logger, globals()).install()

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View file

@ -0,0 +1,21 @@
# ------------------------------------------------------------------------------
from appy.gen import *
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper
from appy.gen.plone25.wrappers.FlavourWrapper import FlavourWrapper
from appy.gen.plone25.wrappers.PodTemplateWrapper import PodTemplateWrapper
<!imports!>
class PodTemplate(PodTemplateWrapper):
'''This class represents a POD template for this application.'''
<!podTemplateBody!>
class Flavour(FlavourWrapper):
'''This class represents the Appy class used for defining a flavour.'''
folder=True
<!flavourBody!>
class Tool(ToolWrapper):
'''This class represents the tool for this application.'''
folder=True
<!toolBody!>
<!wrappers!>
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,155 @@
<tal:block metal:define-macro="master"
define="errors options/state/getErrors | nothing;
Iterator python:modules['Products.Archetypes'].IndexIterator;
schematas here/Schemata;
fieldsets python:[key for key in schematas.keys() if (key != 'metadata') and (schematas[key].editableFields(here, visible_only=True))];
default_fieldset python:(not schematas or schematas.has_key('default')) and 'default' or fieldsets[0];
fieldset request/fieldset|options/fieldset|default_fieldset;
fields python:schematas[fieldset].editableFields(here);
dummy python:here.at_isEditable(fields);
portal_type python:here.getPortalTypeName().lower().replace(' ', '_');
type_name here/getPortalTypeName|here/archetype_name;
lockable python:hasattr(here, 'wl_isLocked');
isLocked python:lockable and here.wl_isLocked();
tabindex tabindex|python:Iterator(pos=7000);
isEdit python:True;
contextObj python:context;
css python:here.getUniqueWidgetAttr(fields, 'helper_css');
js python:here.getUniqueWidgetAttr(fields, 'helper_js');
phaseInfo python: context.getAppyPhases(fieldset=fieldset, forPlone=True);
phase request/phase|phaseInfo/name;
pageName python: context.getAppyPage(isEdit, phaseInfo);">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="here/main_template/macros/master">
<!-- Disable the Standard Plone green tab -->
<div metal:fill-slot="top_slot">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<div tal:define="dummy python:request.set('disable_border', 1)" />
</div>
<!-- Archetypes stuff for managing Javascript and CSS. If I remove this stuff,
Javascript popup for dates does not work anyore -->
<metal:javascript_head fill-slot="javascript_head_slot">
<tal:block define="macro here/archetypes_custom_js/macros/javascript_head | nothing"
condition="macro">
<metal:block use-macro="macro" />
</tal:block>
<tal:js condition="js" repeat="item js">
<script type="text/javascript" charset="iso-8859-1"
tal:condition="python:exists('portal/%s' % item)"
tal:attributes="src string:$portal_url/$item">
</script>
</tal:js>
<tal:block define="macro edit_macros/javascript_head | nothing" condition="macro">
<metal:block use-macro="macro" />
</tal:block>
</metal:javascript_head>
<metal:css fill-slot="css_slot">
<tal:css condition="css" repeat="item css">
<style type="text/css" media="all"
tal:condition="python:exists('portal/%s' % item)"
tal:content="structure string:<!-- @import url($portal_url/$item); -->">
</style>
</tal:css>
<tal:block define="macro edit_macros/css | nothing" condition="macro">
<metal:block use-macro="macro" />
</tal:block>
</metal:css>
<body>
<metal:fill fill-slot="main">
<div metal:use-macro="here/<!macros!>/macros/showPagePrologue"/>
<div metal:use-macro="here/<!macros!>/macros/showPageHeader"/>
<form name="edit_form" method="post" enctype="multipart/form-data"
class="enableUnloadProtection atBaseEditForm"
tal:attributes="action python:here.absolute_url()+'/'+template.id;
id string:${portal_type}-base-edit">
<div metal:use-macro="here/<!macros!>/macros/listFields" />
<div class="formControls">
<input type="hidden" name="fieldset" value="default" tal:attributes="value fieldset"/>
<input type="hidden" name="form.submitted" value="1"/>
<input type="hidden" name="add_reference.field:record" value=""/>
<input type="hidden" name="add_reference.type:record" value=""/>
<input type="hidden" name="add_reference.destination:record" value=""/>
<tal:env define="env request/controller_state/kwargs">
<tal:loop repeat="varname python:('reference_source_url', 'reference_source_field', 'reference_source_fieldset')">
<tal:reference define="items python:env.get(varname, request.get(varname))"
condition="items">
<input tal:repeat="item items" type="hidden"
tal:attributes="value item;
name string:form_env.${varname}:list:record"/>
</tal:reference>
</tal:loop>
</tal:env>
<tal:comment replace="nothing">Turn 'persistent_' variables from controller_state persistent
</tal:comment>
<tal:env repeat="env request/controller_state/kwargs/items">
<input type="hidden"
tal:define="key python:env[0];
value python:env[1]"
tal:condition="python:key.startswith('persistent_')"
tal:attributes="name string:form_env.${key}:record;
value value"/>
</tal:env>
<tal:comment replace="nothing">Turn 'persistent_' variables from forms (GET/POST) persistent
</tal:comment>
<tal:env repeat="env request/form">
<input type="hidden"
tal:define="key env;
value request/?env"
tal:condition="python:key.startswith('persistent_')"
tal:attributes="name string:form_env.${key}:record;
value value"/>
</tal:env>
<tal:comment replace="nothing">Store referrer to remember where to go back
</tal:comment>
<input type="hidden" name="last_referer"
tal:define="last_referer python:here.session_restore_value('HTTP_REFERER', request.form.get('last_referer', request.get('HTTP_REFERER')))"
tal:attributes="value python:(last_referer and '%s/%s' % (here.absolute_url(), template.id) not in last_referer) and last_referer or (here.getParentNode() and here.getParentNode().absolute_url())"/>
<tal:comment replace="nothing">Buttons (Previous, Next, Save, etc)</tal:comment>
<metal:block define-slot="buttons"
tal:define="fieldset_index python:fieldsets.index(fieldset);
n_fieldsets python:len(fieldsets)">
<input tal:condition="python:(fieldset_index &gt; 0) and (fieldsets[fieldset_index-1] in phaseInfo['pages'])"
class="context" type="submit" name="form_previous" value="Previous"
i18n:attributes="value label_previous;" i18n:domain="plone"
tal:attributes="tabindex tabindex/next;
disabled python:test(isLocked, 'disabled', None);"/>
<input tal:condition="python:(fieldset_index &lt; n_fieldsets - 1) and (fieldsets[fieldset_index+1] in phaseInfo['pages'])"
class="context" type="submit" name="form_next" value="Next"
i18n:attributes="value label_next;" i18n:domain="plone"
tal:attributes="tabindex tabindex/next;
disabled python:test(isLocked, 'disabled', None);"/>
<input class="context" type="submit" name="form_submit" value="Save"
i18n:attributes="value label_save;" i18n:domain="plone"
tal:attributes="tabindex tabindex/next;
disabled python:test(isLocked, 'disabled', None);"/>
<input class="standalone" type="submit" name="form.button.cancel" value="Cancel"
i18n:attributes="value label_cancel;" i18n:domain="plone"
tal:attributes="tabindex tabindex/next"/>
</metal:block>
</div>
</form>
<div metal:use-macro="here/<!macros!>/macros/showPageFooter"/>
</metal:fill>
</body>
</html>
</tal:block>

View file

@ -0,0 +1,13 @@
[default]
title = Edit
[validators]
validators = validate_base
validators..form_add =
validators..cancel =
[actions]
action.success = traverse_to:string:content_edit
action.success..form_add = traverse_to:string:add_reference
action.success..cancel = traverse_to:string:go_back
action.failure = traverse_to_action:string:edit

View file

@ -0,0 +1,35 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
i18n:domain="<!applicationName!>"
lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="here/main_template/macros/master">
<head><title></title></head>
<tal:comment replace="nothing">Disable standard Plone green tabs</tal:comment>
<div metal:fill-slot="top_slot">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<div tal:define="dummy python:request.set('disable_border', 1)" />
</div>
<tal:comment replace="nothing">Fill main slot of Plone main_template</tal:comment>
<body>
<metal:fill fill-slot="main"
tal:define="contextObj python:context;
portal_type python:here.getPortalTypeName().lower().replace(' ', '_');
errors python:request.get('errors', {});
isEdit python:False;
tool contextObj/getTool;
flavour python: tool.getFlavour(contextObj);
phaseInfo python: contextObj.getAppyPhases(currentOnly=True, forPlone=False);
phase request/phase|phaseInfo/name;
pageName python: contextObj.getAppyPage(isEdit, phaseInfo);
showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)">
<div metal:use-macro="here/<!macros!>/macros/showPagePrologue"/>
<div metal:use-macro="here/<!macros!>/macros/showPageHeader"/>
<div metal:use-macro="here/<!macros!>/macros/listFields" />
<div metal:use-macro="here/<!macros!>/macros/showPageFooter"/>
</metal:fill>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

BIN
gen/plone25/templates/arrowUp.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View file

@ -0,0 +1,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
i18n:domain="<!applicationName!>">
<body>
<div id="portal-colophon" metal:define-macro="colophon">
</div>
</body>
</html>

43
gen/plone25/templates/config.py Executable file
View file

@ -0,0 +1,43 @@
<!codeHeader!>
import sys
try: # New CMF
from Products.CMFCore.permissions import setDefaultRoles
except ImportError: # Old CMF
from Products.CMFCore.CMFCorePermissions import setDefaultRoles
import Extensions.appyWrappers
<!imports!>
# The following imports are here for allowing mixin classes to access those
# elements without being statically dependent on Plone/Zope packages. Indeed,
# every Archetype instance has a method "getProductConfig" that returns this
# module.
from persistent.list import PersistentList
from Products.Archetypes.utils import DisplayList
from OFS.Image import File
from DateTime import DateTime
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.PloneBatch import Batch
import logging
logger = logging.getLogger('<!applicationName!>')
# Some global variables --------------------------------------------------------
defaultAddRoles = [<!defaultAddRoles!>]
DEFAULT_ADD_CONTENT_PERMISSION = "Add portal content"
ADD_CONTENT_PERMISSIONS = {
<!addPermissions!>}
setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles))
product_globals = globals()
PROJECTNAME = '<!applicationName!>'
applicationRoles = [<!roles!>]
referers = {
<!referers!>
}
# In the following dict, we keep one instance for every Appy workflow defined
# in the application. Those prototypical instances will be used for executing
# user-defined actions and transitions. For each instance, we add a special
# attribute "_transitionsMapping" that allows to get Appy transitions from the
# names of DC transitions.
workflowInstances = {}
<!workflowInstancesInit!>
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,14 @@
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="<!applicationName!>">
<!--five:deprecatedManageAddDelete class=".Meeting.Meeting"/-->
<genericsetup:registerProfile name="default"
title="<!applicationName!>" description=""
provides="Products.GenericSetup.interfaces.EXTENSION"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"/>
</configure>

View file

@ -0,0 +1,27 @@
## Controller Python Script "createAppyObject"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind state=state
##bind subpath=traverse_subpath
##parameters=initiator, field, type_name
##title=createAppyObject
##
if not initiator or not field:
raise Exception, 'You must specify the uid of the object that initiates ' \
'this creation in the "initiator" parameter and the ' \
'related field in the "field" param.'
if not type_name:
raise Exception, 'You must specify the target type name in the "type_name" ' \
'parameter.'
initiatorRes = context.uid_catalog.searchResults(UID=initiator)
if not initiatorRes:
raise Exception, 'Given initiator UID does not correspond to a valid object.'
context.REQUEST.SESSION['initiator'] = initiator
context.REQUEST.SESSION['initiatorField'] = field
context.REQUEST.SESSION['initiatorTarget'] = type_name
return state.set(status='success')

View file

@ -0,0 +1,2 @@
[actions]
action.success=redirect_to:python:'createObject?type_name=%s' % request.SESSION.get('initiatorTarget')

BIN
gen/plone25/templates/current.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

51
gen/plone25/templates/do.py Executable file
View file

@ -0,0 +1,51 @@
## Python Script "<!applicationName!>_do.py"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=actionType
##title=Executes an action
rq = context.REQUEST
urlBack = rq['HTTP_REFERER']
if actionType == 'appyAction':
obj = context.uid_catalog(UID=rq['objectUid'])[0].getObject()
res, msg = obj.executeAppyAction(rq['fieldName'])
if not msg:
# Use the default i18n messages
suffix = 'ko'
if res:
suffix = 'ok'
label = '%s_action_%s' % (obj.getLabelPrefix(rq['fieldName']), suffix)
msg = context.utranslate(label, domain='<!applicationName!>')
context.plone_utils.addPortalMessage(msg)
elif actionType == 'changeRefOrder':
# Move the item up (-1), down (+1) or at a given position ?
move = -1 # Move up
isDelta = True
if rq.get('moveDown.x', None) != None:
move = 1 # Move down
elif rq.get('moveSeveral.x', None) != None:
try:
move = int(rq.get('moveValue'))
# In this case, it is not a delta value; it is the new position where
# the item must be moved.
isDelta = False
except ValueError:
context.plone_utils.addPortalMessage(
context.utranslate('ref_invalid_index', domain='<!applicationName!>'))
context.changeAppyRefOrder(rq['fieldName'], rq['objectUid'], move, isDelta)
elif actionType == 'triggerTransition':
from Products.CMFPlone import PloneMessageFactory as _
context.portal_workflow.doActionFor(context, rq['workflow_action'],
comment=rq.get('comment', ''))
if urlBack.find('?') != -1:
# Remove params; this way, the user may be redirected to correct phase
# when relevant.
urlBack = urlBack[:urlBack.find('?')]
context.plone_utils.addPortalMessage(_(u'Your content\'s status has been modified.'))
return rq.RESPONSE.redirect(urlBack)

BIN
gen/plone25/templates/doc.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

BIN
gen/plone25/templates/done.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

View file

@ -0,0 +1,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
i18n:domain="<!applicationName!>">
<body>
<div id="portal-footer" metal:define-macro="portal_footer">
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
metal:use-macro="here/main_template/macros/master" i18n:domain="<!applicationName!>">
<!-- Disable standard Plone green tabs -->
<div metal:fill-slot="top_slot">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<div tal:define="dummy python:request.set('disable_border', 1)" />
</div>
<body>
<div metal:fill-slot="main">
<span tal:replace="structure python: context.<!toolInstanceName!>.translateWithMapping('front_page_text')"/>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<import-steps>
<import-step id="install-product-<!applicationName!>" version="1.0"
handler="Products.<!applicationName!>.profiles.install_default"
title="Product <!applicationName!>: installation.">
Product <!applicationName!>: installation.
</import-step>
</import-steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

BIN
gen/plone25/templates/odt.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

BIN
gen/plone25/templates/pdf.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
gen/plone25/templates/plus.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

BIN
gen/plone25/templates/rtf.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

BIN
gen/plone25/templates/tool.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

View file

@ -0,0 +1,11 @@
<!codeHeader!>
from Products.CMFCore.WorkflowTool import addWorkflowFactory
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
from appy.gen.plone25.workflow import WorkflowCreator
from Products.<!applicationName!>.config import PROJECTNAME
from Products.ExternalMethod.ExternalMethod import ExternalMethod
import logging
logger = logging.getLogger('<!applicationName!>')
from appy.gen.plone25.workflow import do
<!workflows!>

40
gen/plone25/utils.py Executable file
View file

@ -0,0 +1,40 @@
# ------------------------------------------------------------------------------
def stringify(value):
'''Transforms p_value such that it can be dumped as a string into a
generated file.'''
if isinstance(value, tuple) or isinstance(value, list):
res = '('
for v in value:
res += '%s,' % stringify(v)
res += ')'
else:
res = str(value)
if isinstance(value, basestring):
if value.startswith('python:'):
res = value[7:]
else:
res = "'%s'" % value.replace("'", "\\'")
return res
# ------------------------------------------------------------------------------
def updateRolesForPermission(permission, roles, obj):
'''Adds roles from list p_roles to the list of roles that are granted
p_permission on p_obj.'''
from AccessControl.Permission import Permission
# Find existing roles that were granted p_permission on p_obj
existingRoles = ()
for p in obj.ac_inherited_permissions(1):
name, value = p[:2]
if name == permission:
perm = Permission(name, value, obj)
existingRoles = perm.getRoles()
allRoles = set(existingRoles).union(roles)
obj.manage_permission(permission, tuple(allRoles), acquire=0)
# ------------------------------------------------------------------------------
from appy.gen.utils import AppyRequest
def getAppyRequest(zopeRequest, obj=None):
'''This method creates a nice (Appy) object representation of a
dictionary-like Zope REQUEST object.'''
return AppyRequest(zopeRequest, obj)
# ------------------------------------------------------------------------------

172
gen/plone25/workflow.py Normal file
View file

@ -0,0 +1,172 @@
'''This package contains functions for managing workflow events.'''
# ------------------------------------------------------------------------------
class WorkflowCreator:
'''This class allows to construct the Plone workflow that corresponds to a
Appy workflow.'''
def __init__(self, wfName, ploneWorkflowClass, stateNames, initialState,
stateInfos, transitionNames, transitionInfos, managedPermissions,
productName, externalMethodClass):
self.wfName = wfName
self.ploneWorkflowClass = ploneWorkflowClass
self.stateNames = stateNames
self.initialState = initialState # Name of the initial state
self.stateInfos = stateInfos
# stateInfos is a dict giving information about every state. Keys are
# state names, values are lists? Every list contains (in this order):
# - the list of transitions (names) going out from this state;
# - a dict of permissions, whose keys are permission names and whose
# values are lists of roles that are granted this permission. In
# short: ~{s_stateName: ([transitions], {s_permissionName:
# (roleNames)})}~.
self.transitionNames = transitionNames
self.transitionInfos = transitionInfos
# transitionInfos is a dict giving information avout every transition.
# Keys are transition names, values are end states of the transitions.
self.variableInfos = {
'review_history': ("Provides access to workflow history",
'state_change/getHistory', 0, 0, {'guard_permissions':\
'Request review; Review portal content'}),
'comments': ("Comments about the last transition",
'python:state_change.kwargs.get("comment", "")', 1, 1, None),
'time': ("Time of the last transition", "state_change/getDateTime",
1, 1, None),
'actor': ("The ID of the user who performed the last transition",
"user/getId", 1, 1, None),
'action': ("The last transition", "transition/getId|nothing",
1, 1, None)
}
self.managedPermissions = managedPermissions
self.ploneWf = None # The Plone DC workflow definition
self.productName = productName
self.externalMethodClass = externalMethodClass
def createWorkflowDefinition(self):
'''Creates the Plone instance corresponding to this workflow.'''
self.ploneWf = self.ploneWorkflowClass(self.wfName)
self.ploneWf.setProperties(title=self.wfName)
def createWorkflowElements(self):
'''Creates states, transitions, variables and managed permissions and
sets the initial state.'''
wf = self.ploneWf
# Create states
for s in self.stateNames:
try:
wf.states[s]
except KeyError, k:
# It does not exist, so we create it!
wf.states.addState(s)
# Create transitions
for t in self.transitionNames:
try:
wf.transitions[t]
except KeyError, k:
wf.transitions.addTransition(t)
# Create variables
for v in self.variableInfos.iterkeys():
try:
wf.variables[v]
except KeyError, k:
wf.variables.addVariable(v)
# Create managed permissions
for mp in self.managedPermissions:
try:
wf.addManagedPermission(mp)
except ValueError, va:
pass # Already a managed permission
# Set initial state
if not wf.initial_state: wf.states.setInitialState(self.initialState)
def getTransitionScriptName(self, transitionName):
'''Gets the name of the script corresponding to DC p_transitionName.'''
return '%s_do%s%s' % (self.wfName, transitionName[0].upper(),
transitionName[1:])
def configureStatesAndTransitions(self):
'''Configures states and transitions of the Plone workflow.'''
wf = self.ploneWf
# Configure states
for stateName, stateInfo in self.stateInfos.iteritems():
state = wf.states[stateName]
stateTitle = '%s_%s' % (self.wfName, stateName)
state.setProperties(title=stateTitle, description="",
transitions=stateInfo[0])
for permissionName, roles in stateInfo[1].iteritems():
state.setPermission(permissionName, 0, roles)
# Configure transitions
for transitionName, endStateName in self.transitionInfos.iteritems():
# Define the script to call when the transition has been triggered.
scriptName = self.getTransitionScriptName(transitionName)
if not scriptName in wf.scripts.objectIds():
sn = scriptName
wf.scripts._setObject(sn, self.externalMethodClass(
sn, sn, self.productName + '.workflows', sn))
# Configure the transition in itself
transition = wf.transitions[transitionName]
transition.setProperties(
title=transitionName, new_state_id=endStateName, trigger_type=1,
script_name="", after_script_name=scriptName,
actbox_name='%s_%s' % (self.wfName, transitionName),
actbox_url="",
props={'guard_expr': 'python:here.may("%s")' % transitionName})
def configureVariables(self):
'''Configures the variables defined in this workflow.'''
wf = self.ploneWf
# Set the name of the state variable
wf.variables.setStateVar('review_state')
# Configure the variables
for variableName, info in self.variableInfos.iteritems():
var = wf.variables[variableName]
var.setProperties(description=info[0], default_value='',
default_expr=info[1], for_catalog=0, for_status=info[2],
update_always=info[3], props=info[4])
def run(self):
self.createWorkflowDefinition()
self.createWorkflowElements()
self.configureStatesAndTransitions()
self.configureVariables()
return self.ploneWf
# ------------------------------------------------------------------------------
import notifier
def do(transitionName, stateChange, logger):
'''This function is called by a Plone workflow every time a transition named
p_transitionName has been triggered. p_stateChange.objet is the Plone
object on which the transition has been triggered; p_logger is the Zope
logger allowing to dump information, warnings or errors in the log file
or object.'''
ploneObj = stateChange.object
workflow = ploneObj.getWorkflow()
transition = workflow._transitionsMapping[transitionName]
# Must I execute transition-related actions and notifications?
doAction = False
if transition.action:
doAction = True
if hasattr(ploneObj, '_v_appy_do') and \
not ploneObj._v_appy_do['doAction']:
doAction = False
doNotify = False
if transition.notify:
doNotify = True
if hasattr(ploneObj, '_v_appy_do') and \
not ploneObj._v_appy_do['doNotify']:
doNotify = False
elif not ploneObj.getTool().getFlavour(
ploneObj).getEnableNotifications():
# We do not notify if the "notify" flag in the flavour is disabled.
doNotify = False
if doAction or doNotify:
obj = ploneObj._appy_getWrapper(force=True)
if doAction:
if type(transition.action) in (tuple, list):
# We need to execute a list of actions
for act in transition.action: act(workflow, obj)
else: # We execute a single action only.
transition.action(workflow, obj)
if doNotify:
notifier.sendMail(obj, transition, transitionName, workflow, logger)
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,67 @@
# ------------------------------------------------------------------------------
class FlavourWrapper:
def onEdit(self, created):
if created:
nbOfFlavours = len(self.tool.flavours)
if nbOfFlavours != 1:
self.number = nbOfFlavours
self.o.registerPortalTypes()
# Call the custom flavour "onEdit" method if it exists
customFlavour = self.__class__.__bases__[1]
if customFlavour.__name__ != 'Flavour':
# There is a custom flavour
if customFlavour.__dict__.has_key('onEdit'):
customFlavour.__dict__['onEdit'](self, created)
def getAttributeName(self, attributeType, klass, attrName=None):
'''Some names of Flavour attributes are not easy to guess. For example,
the attribute that stores, for a given flavour, the POD templates
for class A that is in package x.y is "flavour.podTemplatesForx_y_A".
Other example: the attribute that stores the editable default value
of field "f1" of class x.y.A is "flavour.defaultValueForx_y_A_f1".
This method generates the attribute name based on p_attributeType,
a p_klass from the application, and a p_attrName (given only if
needed, for example if p_attributeType is "defaultValue").
p_attributeType may be:
"defaultValue"
Stores the editable default value for a given p_attrName of a
given p_klass.
"podTemplates"
Stores the POD templates that are defined for a given p_klass.
"podMaxShownTemplates"
Stores the maximum number of POD templates shown at once. If the
number of available templates is higher, templates are shown in a
drop-down list.
"resultColumns"
Stores the list of columns that must be show when displaying
instances of the a given root p_klass.
"optionalFields"
Stores the list of optional attributes that are in use in the
current flavour for the given p_klass.
"showWorkflow"
Stores the boolean field indicating if we must show workflow-
related information for p_klass or not.
"showWorkflowCommentField"
Stores the boolean field indicating if we must show the field
allowing to enter a comment every time a transition is triggered.
"showAllStatesInPhase"
Stores the boolean field indicating if we must show all states
linked to the current phase or not. If this field is False, we
simply show the current state, be it linked to the current phase
or not.
'''
fullClassName = '%s_%s' % (klass.__module__.replace('.', '_'),
klass.__name__)
res = '%sFor%s' % (attributeType, fullClassName)
if attrName: res += '_%s' % attrName
return res
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,3 @@
# ------------------------------------------------------------------------------
class PodTemplateWrapper: pass
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,13 @@
# ------------------------------------------------------------------------------
class ToolWrapper:
def getInitiator(self):
'''Retrieves the object that triggered the creation of the object
being currently created (if any).'''
res = None
initiatorUid = self.session['initiator']
if initiatorUid:
res = self.o.uid_catalog(UID=initiatorUid)[0].getObject().\
_appy_getWrapper(force=True)
return res
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,136 @@
'''This package contains base classes for wrappers that hide to the Appy
developer the real classes used by the undelrying web framework.'''
# ------------------------------------------------------------------------------
import time
# ------------------------------------------------------------------------------
class AbstractWrapper:
'''Any real web framework object has a companion object that is an instance
of this class.'''
def __init__(self, o):
self.__dict__['o'] = o
def __setattr__(self, name, v):
exec "self.o.set%s%s(v)" % (name[0].upper(), name[1:])
def __cmp__(self, other):
if other:
return cmp(self.o, other.o)
else:
return 1
def get_tool(self):
return self.o.getTool()._appy_getWrapper(force=True)
tool = property(get_tool)
def get_session(self):
return self.o.REQUEST.SESSION
session = property(get_session)
def get_typeName(self):
return self.__class__.__bases__[-1].__name__
typeName = property(get_typeName)
def get_id(self):
return self.o.id
id = property(get_id)
def get_state(self):
return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
state = property(get_state)
def get_stateLabel(self):
appName = self.o.getProductConfig().PROJECTNAME
return self.o.utranslate(self.o.getWorkflowLabel(), domain=appName)
stateLabel = property(get_stateLabel)
def get_klass(self):
return self.__class__.__bases__[1]
klass = property(get_klass)
def link(self, fieldName, obj):
'''This method links p_obj to this one through reference field
p_fieldName.'''
if isinstance(obj, AbstractWrapper):
obj = obj.o
postfix = 'et%s%s' % (fieldName[0].upper(), fieldName[1:])
# Update the Archetypes reference field
exec 'objs = self.o.g%s()' % postfix
if not objs:
objs = []
elif type(objs) not in (list, tuple):
objs = [objs]
objs.append(obj)
exec 'self.o.s%s(objs)' % postfix
# Update the ordered list of references
sortedRefField = '_appy_%s' % fieldName
if not hasattr(self.o.aq_base, sortedRefField):
exec 'self.o.%s = self.o.getProductConfig().PersistentList()' % \
sortedRefField
getattr(self.o, sortedRefField).append(obj.UID())
def create(self, fieldName, **kwargs):
'''This method allows to create an object and link it to the current
one through reference field named p_fieldName.'''
# Determine object id and portal type
portalType = self.o.getAppyRefPortalType(fieldName)
if kwargs.has_key('id'):
objId = kwargs['id']
del kwargs['id']
else:
objId = '%s.%f' % (fieldName, time.time())
# Where must I create te object?
if hasattr(self, 'folder') and self.folder:
folder = self.o
else:
folder = self.o.getParentNode()
# Create the object
folder.invokeFactory(portalType, objId)
ploneObj = getattr(folder, objId)
appyObj = ploneObj._appy_getWrapper(force=True)
# Set object attributes
ploneObj._appy_manageSortedRefs()
for attrName, attrValue in kwargs.iteritems():
setterName = 'set%s%s' % (attrName[0].upper(), attrName[1:])
if isinstance(attrValue, AbstractWrapper):
try:
refAppyType = getattr(appyObj.__class__.__bases__[-1],
attrName)
appyObj.link(attrName, attrValue.o)
except AttributeError, ae:
pass
else:
getattr(ploneObj, setterName)(attrValue)
# Link the object to this one
self.link(fieldName, ploneObj)
try:
appyObj.onEdit(True) # Call custom initialization
except AttributeError:
pass
self.o.reindexObject()
ploneObj.reindexObject()
return appyObj
def translate(self, label, mapping={}, domain=None):
if not domain: domain = self.o.getProductConfig().PROJECTNAME
return self.o.utranslate(label, mapping, domain=domain)
def do(self, transition, comment='', doAction=False, doNotify=False):
'''This method allows to trigger on p_self a workflow p_transition
programmatically. If p_doAction is False, the action that must
normally be executed after the transition has been triggered will
not be executed. If p_doNotify is False, the notifications
(email,...) that must normally be launched after the transition has
been triggered will not be launched.'''
wfTool = self.o.portal_workflow
availableTransitions = [t['id'] for t in \
wfTool.getTransitionsFor(self.o)]
transitionName = transition
if not transitionName in availableTransitions:
# Maybe is is a compound Appy transition. Try to find the
# corresponding DC transition.
state = self.state
transitionPrefix = transition + state[0].upper() + state[1:] + 'To'
for at in availableTransitions:
if at.startswith(transitionPrefix):
transitionName = at
break
# Set in a versatile attribute details about what to execute or not
# (actions, notifications) after the transition has been executed by DC
# workflow.
self.o._v_appy_do = {'doAction': doAction, 'doNotify': doNotify}
wfTool.doActionFor(self.o, transitionName, comment=comment)
del self.o._v_appy_do
# ------------------------------------------------------------------------------

347
gen/po.py Executable file
View file

@ -0,0 +1,347 @@
# ------------------------------------------------------------------------------
import os, re, time, copy
from utils import produceNiceMessage
# ------------------------------------------------------------------------------
poHeader = '''msgid ""
msgstr ""
"Project-Id-Version: %s\\n"
"POT-Creation-Date: %s\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=utf-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=1; plural=0\\n"
"Language-code: %s\\n"
"Language-name: %s\\n"
"Preferred-encodings: utf-8 latin1\\n"
"Domain: %s\\n"
%s
'''
fallbacks = {'en': 'en-us en-ca',
'fr': 'fr-be fr-ca fr-lu fr-mc fr-ch fr-fr'
}
# ------------------------------------------------------------------------------
class PoMessage:
'''Represents a i18n message (po format).'''
CONFIG = "Configuration panel for product '%s'"
FLAVOUR = "Configuration flavour"
# The following messages (starting with MSG_) correspond to flavour
# attributes added for every gen-class (warning: the message IDs correspond
# to MSG_<attributePrexix>).
MSG_optionalFieldsFor = 'Optional fields'
MSG_defaultValueFor = "Default value for field '%s'"
MSG_podTemplatesFor = "POD templates"
MSG_podMaxShownTemplatesFor = "Max shown POD templates"
MSG_resultColumnsFor = "Columns to display while showing query results"
MSG_showWorkflowFor = 'Show workflow-related information'
MSG_showWorkflowCommentFieldFor = 'Show field allowing to enter a ' \
'comment every time a transition is triggered'
MSG_showAllStatesInPhaseFor = 'Show all states in phase'
POD_TEMPLATE = 'POD template'
DEFAULT_VALID_ERROR = 'Please fill or correct this.'
REF_NO = 'No object.'
REF_ADD = 'Add a new one'
REF_NAME = 'Name'
REF_ACTIONS = 'Actions'
REF_MOVE_UP = 'Move up'
REF_MOVE_DOWN = 'Move down'
REF_INVALID_INDEX = 'No move occurred: please specify a valid number.'
QUERY_CREATE = 'create'
QUERY_CONSULT_ALL = 'consult all'
QUERY_NO_RESULT = 'Nothing to see for the moment.'
WORKFLOW_COMMENT = 'Optional comment'
WORKFLOW_STATE = 'state'
PHASE = 'phase'
ROOT_TYPE = 'type'
CHOOSE_A_VALUE = ' - '
CHOOSE_A_DOC = '[ Documents ]'
MIN_REF_VIOLATED = 'You must choose more elements here.'
MAX_REF_VIOLATED = 'Too much elements are selected here.'
BAD_INT = 'An integer value is expected; do not enter any space.'
BAD_FLOAT = 'A floating-point number is expected; use the dot as decimal ' \
'separator, not a comma; do not enter any space.'
BAD_EMAIL = 'Please enter a valid email.'
BAD_URL = 'Please enter a valid URL.'
BAD_ALPHANUMERIC = 'Please enter a valid alphanumeric value.'
ACTION_OK = 'The action has been successfully executed.'
ACTION_KO = 'A problem occurred while executing the action.'
FRONT_PAGE_TEXT = 'Welcome to this Appy-powered Plone site.'
EMAIL_SUBJECT = '${siteTitle} - Action \\"${transitionName}\\" has been ' \
'performed on element entitled \\"${objectTitle}\\".'
EMAIL_BODY = 'You can consult this element at ${objectUrl}.'
def __init__(self, id, msg, default, fuzzy=False, comments=[]):
self.id = id
self.msg = msg
self.default = default
self.fuzzy = fuzzy # True if the default value has changed in the pot
# file: the msg in the po file needs to be translated again.
self.comments = comments
def update(self, newMsg, isPot, language):
'''Updates me with new values from p_newMsg. If p_isPot is True (which
means that the current file is a pot file), I do not care about
filling self.msg.'''
if isPot:
self.msg = ""
if not self.default:
self.default = newMsg.default
# It means that if the default message has changed, we will not
# update it in the pot file. We will always keep the one that
# the user may have changed in the pot file. We will write a
# default message only when no default message is defined.
else:
# newMsg comes from a pot file, we must update the corresponding
# message in the current po file.
oldDefault = self.default
if self.default != newMsg.default:
# The default value has changed in the pot file
self.default = newMsg.default
if self.msg.strip():
self.fuzzy = True
# We mark the message as "fuzzy" (=may need to be rewritten
# because the default value has changed) only if the user
# has already entered a message. Else, this has no sense to
# rewrite the empty message.
else:
self.fuzzy = False
if (language == 'en'):
if not self.msg:
# Put the default message into msg for english
self.msg = self.default
if self.fuzzy and (self.msg == oldDefault):
# The message was equal to the old default value. It means
# that the user did not change it, because for English we
# fill by default the message with the default value (see
# code just above). So in this case, the message was not
# really fuzzy.
self.fuzzy = False
self.msg = self.default
def produceNiceDefault(self):
'''Transforms self.default into a nice msg.'''
self.default = produceNiceMessage(self.default)
def generate(self):
'''Produces myself as I must appear in a po(t) file.'''
res = ''
for comment in self.comments:
res += comment + '\n'
if self.default != None:
res = '#. Default: "%s"\n' % self.default
if self.fuzzy:
res += '#, fuzzy\n'
res += 'msgid "%s"\n' % self.id
res += 'msgstr "%s"\n' % self.msg
return res
def __repr__(self):
return '<i18n msg id="%s", msg="%s", default="%s">' % \
(self.id, self.msg, self.default)
def __cmp__(self, other):
return cmp(self.id, other.id)
def clone(self, oldPrefix, newPrefix):
'''This returns a cloned version of myself. The clone has another id
that includes p_newPrefix.'''
if self.id.startswith(oldPrefix):
newId = newPrefix + self.id[len(oldPrefix):]
else:
newId = '%s_%s' % (newPrefix, self.id.split('_', 1)[1])
return PoMessage(newId, self.msg, self.default, comments=self.comments)
class PoHeader:
def __init__(self, name, value):
self.name = name
self.value = value
def generate(self):
'''Generates the representation of myself into a po(t) file.'''
return '"%s: %s\\n"\n' % (self.name, self.value)
class PoFile:
'''Represents a i18n file.'''
def __init__(self, fileName):
self.fileName = fileName
self.isPot = fileName.endswith('.pot')
self.messages = [] # Ordered list of messages
self.messagesDict = {} # Dict of the same messages, indexed by msgid
self.headers = []
self.headersDict = {}
# Get application name, domain name and language from fileName
self.applicationName = ''
self.language = ''
self.domain = ''
baseName = os.path.splitext(os.path.basename(fileName))[0]
elems = baseName.split('-')
if self.isPot:
if len(elems) == 1:
self.applicationName = baseName
self.domain = baseName
else:
self.applicationName = elems[0]
self.domain = elems[1]
else:
self.applicationName = elems[0]
if len(elems) == 2:
self.domain = self.applicationName
self.language = elems[1]
else:
self.domain = elems[1]
self.language = elems[2]
self.generated = False # Is set to True during the generation process
# when this file has been generated.
def addMessage(self, newMsg):
res = copy.copy(newMsg)
self.messages.append(res)
self.messagesDict[res.id] = res
return res
def addHeader(self, newHeader):
self.headers.append(newHeader)
self.headersDict[newHeader.name] = newHeader
def update(self, newMessages, removeNotNewMessages=False,
keepExistingOrder=True):
'''Updates the existing messages with p_newMessages.
If p_removeNotNewMessages is True, all messages in self.messages
that are not in newMessages will be removed. If p_keepExistingOrder
is False, self.messages will be sorted according to p_newMessages.
Else, newMessages that are not yet in self.messages will be appended
to the end of self.messages.'''
# First, remove not new messages if necessary
newIds = [m.id for m in newMessages]
removedIds = []
if removeNotNewMessages:
i = len(self.messages)-1
while i >= 0:
oldId = self.messages[i].id
if oldId not in newIds:
del self.messages[i]
del self.messagesDict[oldId]
removedIds.append(oldId)
i -= 1
if keepExistingOrder:
# Update existing messages and add inexistent messages to the end.
for newMsg in newMessages:
if self.messagesDict.has_key(newMsg.id):
msg = self.messagesDict[newMsg.id]
else:
msg = self.addMessage(newMsg)
msg.update(newMsg, self.isPot, self.language)
else:
# Keep the list of all old messages not being in new messages.
# We will append them at the end of the new messages.
notNewMessages = [m for m in self.messages if m.id not in newIds]
del self.messages[:]
for newMsg in newMessages:
if self.messagesDict.has_key(newMsg.id):
msg = self.messagesDict[newMsg.id]
self.messages.append(msg)
else:
msg = self.addMessage(newMsg)
msg.update(newMsg, self.isPot, self.language)
# Append the list of old messages to the end
self.messages += notNewMessages
return removedIds
def generateHeaders(self, f):
if not self.headers:
creationTime = time.strftime("%Y-%m-%d %H:%M-%S", time.localtime())
fb = ''
if not self.isPot:
# I must add fallbacks
if fallbacks.has_key(self.language):
fb = '"X-is-fallback-for: %s\\n"' % fallbacks[self.language]
f.write(poHeader % (self.applicationName, creationTime,
self.language, self.language, self.domain, fb))
else:
# Some headers were already found, we dump them as is
f.write('msgid ""\nmsgstr ""\n')
for header in self.headers:
f.write(header.generate())
f.write('\n')
def generate(self):
'''Generates the corresponding po or pot file.'''
folderName = os.path.dirname(self.fileName)
if not os.path.exists(folderName):
os.makedirs(folderName)
f = file(self.fileName, 'w')
self.generateHeaders(f)
for msg in self.messages:
f.write(msg.generate())
f.write('\n')
f.close()
self.generated = True
def getPoFileName(self, language):
'''Gets the name of the po file that corresponds to this pot file and
the given p_language.'''
if self.applicationName == self.domain:
res = '%s-%s.po' % (self.applicationName, language)
else:
res = '%s-%s-%s.po' % (self.applicationName, self.domain, language)
return res
class PoParser:
'''Allows to parse a i18n file. The result is produced in self.res as a
PoFile instance.'''
def __init__(self, fileName):
self.res = PoFile(fileName)
# Regular expressions for msgIds, msgStrs and default values.
re_default = re.compile('#\.\s+Default\s*:\s*"(.*)"')
re_fuzzy = re.compile('#,\s+fuzzy')
re_id = re.compile('msgid\s+"(.*)"')
re_msg = re.compile('msgstr\s+"(.*)"')
def parse(self):
'''Parses all i18n messages in the file, stores it in
self.res.messages and returns it also.'''
f = file(self.res.fileName)
# Currently parsed values
msgDefault = msgFuzzy = msgId = msgStr = None
comments = []
# Walk every line of the po(t) file
for line in f:
lineContent = line.strip()
if lineContent and (not lineContent.startswith('"')):
r = self.re_default.match(lineContent)
if r:
msgDefault = r.group(1)
else:
r = self.re_fuzzy.match(lineContent)
if r:
msgFuzzy = True
else:
r = self.re_id.match(lineContent)
if r:
msgId = r.group(1)
else:
r = self.re_msg.match(lineContent)
if r:
msgStr = r.group(1)
else:
if lineContent.startswith('#'):
comments.append(lineContent.strip())
if msgStr != None:
if not ((msgId == '') and (msgStr == '')):
poMsg = PoMessage(msgId, msgStr, msgDefault, msgFuzzy,
comments)
self.res.addMessage(poMsg)
msgDefault = msgFuzzy = msgId = msgStr = None
comments = []
if lineContent.startswith('"'):
# It is a header value
name, value = lineContent.strip('"').split(':', 1)
if value.endswith('\\n'):
value = value[:-2]
self.res.addHeader(PoHeader(name.strip(), value.strip()))
f.close()
return self.res
# ------------------------------------------------------------------------------

Binary file not shown.

View file

@ -0,0 +1,6 @@
from appy.gen import *
class Engine:
engineType = String()
description = String(format=String.XHTML)
pod = True

View file

@ -0,0 +1,5 @@
from appy.gen import *
class Radio:
abstract = True
name = String()

View file

@ -0,0 +1,6 @@
from appy.gen import *
class Wheel:
title = String(multiplicity=(1,1))
description = String(format=String.XHTML)

View file

@ -0,0 +1,37 @@
from appy.gen import *
from AppyCar.Interior.Radio import Radio
class RallyCarWorkflow:
# Roles
carDriver = 'CarDriver'
driverM = ('Manager', carDriver)
# Specific permissions
readColor = ReadPermission('Car.color')
writeColor = WritePermission('Car.color')
# States
created = State({r:driverM, w:driverM, d:driverM,
readColor: driverM, writeColor: driverM}, initial=True)
running = State({r:driverM, w:driverM, d:driverM,
readColor: 'Manager', writeColor: 'Manager'})
# Transitions
run = Transition( (created, running), condition=driverM)
stop = Transition( (running, created), condition=driverM)
class Car:
sport = Boolean()
color = String(specificReadPermission=True, specificWritePermission=True)
description = String(format=String.TEXT)
class RallyCar(Car):
root = True
workflow = RallyCarWorkflow
test = Integer()
class StandardRadio(Radio):
test1 = Integer()
c = Config()
c.languages = ('en', 'fr')
class CarFlavour(Flavour):
explanation = String(group="userInterface")

View file

@ -0,0 +1,10 @@
from appy.gen import *
class Meeting:
place = String(editDefault=True)
date = Date()
myObservations = String(format=String.XHTML, optional=True)
annex = File(optional=True)
leader = String(validator=['andyStein', 'joelLambillotte'])
root = True
pod = ['Meeting']

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,10 @@
This folder contains some example Appy applications used for testing purposes.
Directly in the folder, you have the application "AppyMeeting" which consists in
a single Python file (module AppyMeeting.py) and a POD template for producing
documents (Meeting.odt). You also have simple applications Zzz.py and
ZopeComponent.py that are one-file applications.
In sub-folder AppyCar, you have a more complex application that is structured
as a Python package (a hierarchy of folders).

Binary file not shown.

View file

@ -0,0 +1,140 @@
from appy.gen import *
class BunchOfGeek:
description = String(format=String.TEXT)
class ZopeComponentTool(Tool):
someUsefulConfigurationOption = String()
def onInstall(self):
self.someUsefulConfigurationOption = 'My app is configured now!'
install = Action(action=onInstall)
class ZopeComponentFlavour(Flavour):
anIntegerOption = Integer()
bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
link=False, back=Ref(attribute='backToTool'),
shownInfo=('description',), page='data')
def onEdit(self, created):
if 'Escadron de la mort' not in [b.title for b in self.bunchesOfGeeks]:
self.create('bunchesOfGeeks', title='Escadron de la mort',
description='I want those guys everywhere!')
class ZopeComponentWorkflow:
# Specific permissions
wf = WritePermission('ZopeComponent.funeralDate')
rb = ReadPermission('ZopeComponent.responsibleBunch')
# Roles
zManager = 'ZManager'
zLeader = 'ZLeader'
managerM = (zManager, 'Manager')
leaderM = (zLeader, 'Manager')
everybody = (zManager, zLeader, 'Manager')
# States
created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM, wf:'Owner',
rb:everybody}, initial=True)
validated = State({r:everybody, w:everybody, d:None, wf:everybody,
rb:everybody})
underDevelopment = State({r:everybody, w:leaderM, d:None, wf:leaderM,
rb:everybody})
whereIsTheClient = State({r:everybody, w:managerM, d:None, wf:managerM,
rb:everybody})
# Transitions
def funeralOk(self, obj): return obj.funeralDate
validate = Transition( (created, validated),
condition=managerM + (funeralOk,))
def updateDescription(self, obj):
obj.description = 'Description edited by my manager was silly.'
startDevelopment = Transition( (validated, underDevelopment),
condition=leaderM, action=updateDescription)
cancelDevelopment = Transition( (underDevelopment, whereIsTheClient),
condition=managerM)
cancel = Transition( ( (whereIsTheClient, underDevelopment),
(underDevelopment, validated),
(validated, created)), condition='Manager')
class ZopeComponent:
root = True
workflow = ZopeComponentWorkflow
def showDate(self):
return True
def validateDescription(self, value):
res = True
if value.find('simple') != -1:
res = self.translate('zope_3_is_not_simple')
return res
description = String(editDefault=True)
technicalDescription = String(format=String.XHTML,
validator=validateDescription)
#status = String(validator=['underDevelopement', 'stillSomeWorkToPerform',
# 'weAreAlmostFinished', 'alphaReleaseIsBugged', 'whereIsTheClient'],
# optional=True, editDefault=True)
funeralDate = Date(optional=True, specificWritePermission=True)
responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,
link=True, back=Ref(attribute='components'),
specificReadPermission=True)
class CobolComponentWorkflow(ZopeComponentWorkflow):
p = ZopeComponentWorkflow # Shortcut to workflow parent
# An additional state
finished = State(p.whereIsTheClient.permissions)
# Override validate: condition on funeralDate has no sense here
validate = Transition(p.validate.states, condition=p.managerM)
# Override cancelDevelopment: go to finished instead
cancelDevelopment = Transition( (p.underDevelopment, finished),
condition=p.managerM)
# Update cancel accordingly
cancel = Transition( ((finished, p.underDevelopment),) +p.cancel.states[1:],
condition=p.cancel.condition)
class CobolComponent:
root = True
workflow = CobolComponentWorkflow
description = String()
class Person:
abstract = True
pod = True
title = String(show=False)
n = 'name_3'
firstName = String(group=n, width=15)
middleInitial = String(group=n, width=3)
name = String(multiplicity=(1,1), group=n, width=30)
contractDetails = String(format=String.XHTML)
cia = {'page': 'contactInformation', 'group': 'address_2'}
street = String(**cia)
number = Integer(**cia)
country = String(**cia)
zipCode = Integer(**cia)
cio = {'page': 'contactInformation', 'group': 'numbers_2', 'width': 20}
phoneNumber = String(**cio)
faxNumber = String(**cio)
mobilePhone = String(**cio)
workPhoneNumber = String(**cio)
workFaxNumber = String(**cio)
workMobilePhone = String(**cio)
def onEdit(self, created):
self.title = self.firstName + ' ' + self.name
class Worker(Person):
root = True
productivity = Integer()
class Parasite(Person):
root = True
pod = ['SordidGossips', 'MoreGossips']
hairColor = String(group='hairyCharacteristics')
sordidGossips = String(format = String.XHTML, page='Gossips')
parasiteIndex = String(validator=['low', 'medium', 'high',
'unquantifiable'],
page='contactInformation', group='numbers')
details = String(page='contactInformation', group='numbers',
master=parasiteIndex, masterValue='unquantifiable')
avoidAnyPhysicalContact = Boolean(page='contactInformation')
def validate(self, new, errors):
if (new.hairColor == 'flashy') and (new.firstName == 'Gerard'):
errors.hairColor = True
errors.firstName = "Flashy Gerards are disgusting."
c = Config()
c.languages = ('en', 'fr')
c.defaultCreators += ['ZLeader']

View file

@ -0,0 +1,67 @@
from appy.gen import *
class Zzz:
root = True
def show_f2(self): return True
def validate_i2(self, value):
if (value != None) and (value < 10):
return 'Value must be higher or equal to 10.'
return True
title=String(multiplicity=(0,1), show=False)
i1 = Integer(show=False)
i2 = Integer(validator = validate_i2)
f1 = Float(show=show_f2, page='other')
f2 = Float(multiplicity=(1,1))
class SeveralStrings:
root=True
anEmail = String(validator=String.EMAIL)
anUrl = String(validator=String.URL)
anAlphanumericValue = String(validator=String.ALPHANUMERIC)
aSingleSelectedValue = String(validator=['valueA', 'valueB', 'valueC'])
aSingleMandatorySelectedValue = String(
validator=['valueX', 'valueY', 'valueZ'], multiplicity=(1,1))
aMultipleSelectedValue = String(
validator=['valueS', 'valueT', 'valueU', 'valueV'],
multiplicity=(1,None), searchable=True)
aBooleanValue = Boolean(default=True)
dateWithHour = Date()
dateWithoutHour = Date(format=Date.WITHOUT_HOUR)
anAttachedFile = File()
anAttachedImage = File(isImage=True)
class Product:
root = True
description = String(format=String.TEXT)
stock = Integer()
def needOrder(self): return self.stock < 3
def orderProduct(self): self.stock = 3
order = Action(action=orderProduct, show=needOrder)
class Order:
description = String(format=String.TEXT)
number = Float(show=False)
# Reference field
def getReference(self): return 'OR-%f' % self.number
reference = Computed(method=getReference)
def filterProducts(self, allProducts):
return [f for f in allProducts if f.description.find('Descr') != -1]
products = Ref(Product, add=False, link=True, multiplicity=(1,None),
back=Ref(attribute='orders'), showHeaders=True,
shownInfo=('description','title', 'order'),
select=filterProducts)
def onEdit(self, created):
if created:
import random
self.number = random.random()
class Client:
root = True
folder = True
title = String(show=False)
firstName = String()
name = String()
orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
back=Ref(attribute='client'), showHeaders=True,
shownInfo=('reference', 'description', 'products'), wide=True)
def onEdit(self, created):
self.title = self.firstName + ' ' + self.name

173
gen/utils.py Executable file
View file

@ -0,0 +1,173 @@
# ------------------------------------------------------------------------------
import re
sequenceTypes = (list, tuple)
# Classes used by edit/view templates for accessing information ----------------
class Descr:
'''Abstract class for description classes.'''
def get(self): return self.__dict__
class FieldDescr(Descr):
def __init__(self, atField, appyType, fieldRel):
self.atField = atField # The corresponding Archetypes field (may be None
# in the case of backward references)
self.appyType = appyType # The corresponding Appy type
self.fieldRel = fieldRel # The field relatonship, needed when the field
# description is a backward reference.
if fieldRel:
self.widgetType = 'backField'
self.group = appyType['backd']['group']
self.show = appyType['backd']['show']
self.page = appyType['backd']['page']
else:
self.widgetType = 'field'
self.group = appyType['group']
self.show = appyType['show']
self.page = appyType['page']
class GroupDescr(Descr):
def __init__(self, name, cols, page):
self.name = name
self.cols = cols # The nb of columns for placing fields into the group
self.rows = None # The number of rows
self.page = page
self.fields = []
self.widgetType = 'group'
def computeRows(groupDict):
'''Computes self.rows. But because at this time the object has already
been converted to a dict (for being maniputated within ZPTs, this
method is a static method that takes the dict as arg.'''
groupDict['rows'] = len(groupDict['fields']) / groupDict['cols']
if len(groupDict['fields']) % groupDict['cols']:
groupDict['rows'] += 1
computeRows = staticmethod(computeRows)
def getGroupInfo(groupName):
'''In the group name, the user may optionally specify at the end the
number of columns for placing fields into the group. This method
returns the real group name and the number of columns.'''
res = groupName.rsplit('_', 1)
if len(res) == 1:
res.append(1) # Append the default numer of columns
else:
try:
res[1] = int(res[1])
except ValueError:
res[1] = 1
return res
getGroupInfo = staticmethod(getGroupInfo)
class PageDescr(Descr):
def getPageInfo(pageOrName, pageKlass):
'''pageOrName can be:
- a string containing the name of the page
- a string containing <pageName>_<phaseName>
- a appy.gen.Page instance for a more detailed page description.
This method returns a normalized tuple containing page-related
information.'''
if isinstance(pageOrName, pageKlass):
res = [pageOrName.name, pageOrName.phase, pageOrName.show]
else:
res = pageOrName.rsplit('_', 1)
if len(res) == 1:
res.append('main')
res.append(True)
return res
getPageInfo = staticmethod(getPageInfo)
class PhaseDescr(Descr):
def __init__(self, name, states, forPlone, ploneObj):
self.name = name
self.states = states
self.forPlone = forPlone
self.ploneObj = ploneObj
self.phaseStatus = None
self.pages = [] # The list of pages in this phase
self.totalNbOfPhases = None
def addPage(self, appyType, obj):
toAdd = appyType['page']
if (toAdd == 'main') and self.forPlone:
toAdd = 'default'
if (toAdd not in self.pages) and \
obj._appy_showPage(appyType['page'], appyType['pageShow']):
self.pages.append(toAdd)
def computeStatus(self):
'''Compute status of whole phase based on individual status of states
in this phase. If this phase includes no state, the concept of phase
is simply used as a tab, and its status depends on the page currently
shown.'''
res = 'Current'
if self.states:
# Compute status base on states
res = self.states[0]['stateStatus']
if len(self.states) > 1:
for state in self.states[1:]:
if res != state['stateStatus']:
res = 'Current'
break
else:
# Compute status based on current page
rq = self.ploneObj.REQUEST
if rq.has_key('fieldset'):
pageName = rq['fieldset']
if not self.forPlone and (pageName == 'default'):
pageName = 'main'
else:
pageName = rq.get('pageName', 'main')
if pageName in self.pages:
res = 'Current'
else:
res = 'Deselected'
self.phaseStatus = res
class StateDescr(Descr):
def __init__(self, name, stateStatus):
self.name = name
self.stateStatus = stateStatus.capitalize()
# ------------------------------------------------------------------------------
upperLetter = re.compile('[A-Z]')
def produceNiceMessage(msg):
'''Transforms p_msg into a nice msg.'''
res = ''
if msg:
res = msg[0].upper()
for c in msg[1:]:
if c == '_':
res += ' '
elif upperLetter.match(c):
res += ' ' + c.lower()
else:
res += c
return res
# ------------------------------------------------------------------------------
class ValidationErrors: pass
class AppyRequest:
def __init__(self, zopeRequest, appyObj=None):
self.zopeRequest = zopeRequest
self.appyObj = appyObj
def __str__(self): return '<AppyRequest object>'
def __repr__(self): return '<AppyRequest object>'
def __getattr__(self, attr):
res = None
if self.appyObj:
# I can retrieve type information from the ploneObj.
appyType = self.appyObj.o.getAppyType(attr)
if appyType['type'] == 'Ref':
res = self.zopeRequest.get('appy_ref_%s' % attr, None)
else:
res = self.zopeRequest.get(attr, None)
if appyType['pythonType']:
try:
exec 'res = %s' % res # bool('False') gives True, so we
# can't write: res = appyType['pythonType'](res)
except SyntaxError, se:
# Can happen when for example, an Integer value is empty
res = None
else:
res = self.zopeRequest.get(attr, None)
return res
# ------------------------------------------------------------------------------