Initial import
0
gen/plone25/__init__.py
Executable file
666
gen/plone25/descriptors.py
Executable 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
|
@ -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
|
@ -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()
|
||||
# ------------------------------------------------------------------------------
|
42
gen/plone25/mixins/ClassMixin.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
113
gen/plone25/mixins/FlavourMixin.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
97
gen/plone25/mixins/PodTemplateMixin.py
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
192
gen/plone25/mixins/ToolMixin.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
840
gen/plone25/mixins/__init__.py
Normal 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
|
@ -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
|
@ -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
|
||||
# ------------------------------------------------------------------------------
|
228
gen/plone25/templates/AppyReference.pt
Executable 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"/>
|
||||
</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>
|
||||
</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>
|
34
gen/plone25/templates/ArchetypesTemplate.py
Executable 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!>
|
38
gen/plone25/templates/FlavourTemplate.py
Executable 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!>')
|
36
gen/plone25/templates/Install.py
Executable 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
|
@ -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)<=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>
|
||||
</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)>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', '<br>')"/>
|
||||
<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"/>
|
||||
—
|
||||
</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']>1">−
|
||||
<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)>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>
|
34
gen/plone25/templates/PodTemplate.py
Executable 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!>')
|
45
gen/plone25/templates/Portlet.pt
Executable 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>
|
||||
<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>
|
19
gen/plone25/templates/ProfileInit.py
Executable 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
|
@ -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>
|
264
gen/plone25/templates/Styles.css.dtml
Executable 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
|
||||
*/
|
||||
|
||||
|
49
gen/plone25/templates/ToolTemplate.py
Executable 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!>')
|
23
gen/plone25/templates/__init__.py
Executable 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()
|
BIN
gen/plone25/templates/appyConfig.gif
Executable file
After Width: | Height: | Size: 899 B |
21
gen/plone25/templates/appyWrappers.py
Executable 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!>
|
||||
# ------------------------------------------------------------------------------
|
155
gen/plone25/templates/appy_edit.cpt
Executable 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 > 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 < 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>
|
13
gen/plone25/templates/appy_edit.cpt.metadata
Executable 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
|
35
gen/plone25/templates/appy_view.pt
Executable 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>
|
BIN
gen/plone25/templates/arrowDown.png
Executable file
After Width: | Height: | Size: 174 B |
BIN
gen/plone25/templates/arrowUp.png
Executable file
After Width: | Height: | Size: 174 B |
7
gen/plone25/templates/colophon.pt
Executable 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
|
@ -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!>
|
||||
# ------------------------------------------------------------------------------
|
14
gen/plone25/templates/configure.zcml
Executable 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>
|
27
gen/plone25/templates/createAppyObject.cpy
Executable 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')
|
2
gen/plone25/templates/createAppyObject.cpy.metadata
Executable 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
After Width: | Height: | Size: 244 B |
51
gen/plone25/templates/do.py
Executable 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
After Width: | Height: | Size: 818 B |
BIN
gen/plone25/templates/done.png
Executable file
After Width: | Height: | Size: 248 B |
7
gen/plone25/templates/footer.pt
Executable 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>
|
15
gen/plone25/templates/frontPage.pt
Executable 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>
|
8
gen/plone25/templates/import_steps.xml
Executable 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>
|
BIN
gen/plone25/templates/nextPhase.png
Executable file
After Width: | Height: | Size: 201 B |
BIN
gen/plone25/templates/nextState.png
Executable file
After Width: | Height: | Size: 192 B |
BIN
gen/plone25/templates/odt.png
Executable file
After Width: | Height: | Size: 754 B |
BIN
gen/plone25/templates/pdf.png
Executable file
After Width: | Height: | Size: 228 B |
BIN
gen/plone25/templates/plus.png
Executable file
After Width: | Height: | Size: 225 B |
BIN
gen/plone25/templates/rtf.png
Executable file
After Width: | Height: | Size: 225 B |
BIN
gen/plone25/templates/tool.gif
Executable file
After Width: | Height: | Size: 339 B |
11
gen/plone25/templates/workflows.py
Normal 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
|
@ -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
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
67
gen/plone25/wrappers/FlavourWrapper.py
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
3
gen/plone25/wrappers/PodTemplateWrapper.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
class PodTemplateWrapper: pass
|
||||
# ------------------------------------------------------------------------------
|
13
gen/plone25/wrappers/ToolWrapper.py
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
136
gen/plone25/wrappers/__init__.py
Normal 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
|
||||
# ------------------------------------------------------------------------------
|