appy.gen: refactoring due to De-Plonization.
This commit is contained in:
parent
6733f4c7dc
commit
d934f49a99
31 changed files with 1220 additions and 1325 deletions
|
@ -1,599 +0,0 @@
|
|||
'''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, toolFieldPrefixes
|
||||
import appy.gen
|
||||
import appy.gen.descriptors
|
||||
from appy.gen.po import PoMessage
|
||||
from appy.gen import *
|
||||
from appy.gen.utils import produceNiceMessage, getClassName
|
||||
TABS = 4 # Number of blanks in a Python indentation.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class FieldDescriptor:
|
||||
'''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
|
||||
|
||||
def __repr__(self):
|
||||
return '<Field %s, %s>' % (self.fieldName, self.classDescr)
|
||||
|
||||
def getToolFieldMessage(self, fieldName):
|
||||
'''Some attributes generated on the Tool class need a specific
|
||||
default message, returned by this method.'''
|
||||
res = fieldName
|
||||
for prefix in toolFieldPrefixes:
|
||||
fullPrefix = prefix + 'For'
|
||||
if fieldName.startswith(fullPrefix):
|
||||
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, description or help (depending on p_msgType)
|
||||
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, ToolClassDescriptor):
|
||||
default = self.getToolFieldMessage(self.fieldName)
|
||||
if default != self.fieldName: produceNice = False
|
||||
msg = PoMessage(msgId, '', default)
|
||||
if produceNice:
|
||||
msg.produceNiceDefault()
|
||||
return msg
|
||||
|
||||
def walkString(self):
|
||||
'''How to generate an Appy String?'''
|
||||
if self.appyType.isSelect and \
|
||||
(type(self.appyType.validator) in (list, tuple)):
|
||||
# Generate i18n messages for every possible value if the list
|
||||
# of values is fixed.
|
||||
for value in self.appyType.validator:
|
||||
msgLabel = '%s_%s_list_%s' % (self.classDescr.name,
|
||||
self.fieldName, value)
|
||||
poMsg = PoMessage(msgLabel, '', value)
|
||||
poMsg.produceNiceDefault()
|
||||
self.generator.labels.append(poMsg)
|
||||
|
||||
def walkAction(self):
|
||||
'''Generates the i18n-related label.'''
|
||||
if self.appyType.confirm:
|
||||
label = '%s_%s_confirm' % (self.classDescr.name, self.fieldName)
|
||||
msg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||
self.generator.labels.append(msg)
|
||||
|
||||
def walkRef(self):
|
||||
'''How to generate a Ref?'''
|
||||
# Update the list of referers
|
||||
self.generator.addReferer(self)
|
||||
# Add the widget label for the back reference
|
||||
back = self.appyType.back
|
||||
refClassName = getClassName(self.appyType.klass, self.applicationName)
|
||||
if back.hasLabel:
|
||||
backLabel = "%s_%s" % (refClassName, self.appyType.back.attribute)
|
||||
poMsg = PoMessage(backLabel, '', self.appyType.back.attribute)
|
||||
poMsg.produceNiceDefault()
|
||||
self.generator.labels.append(poMsg)
|
||||
# Add the label for the confirm message if relevant
|
||||
if self.appyType.addConfirm:
|
||||
label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName)
|
||||
msg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||
self.generator.labels.append(msg)
|
||||
|
||||
def walkPod(self):
|
||||
# Add i18n-specific messages
|
||||
if self.appyType.askAction:
|
||||
label = '%s_%s_askaction' % (self.classDescr.name, self.fieldName)
|
||||
msg = PoMessage(label, '', PoMessage.POD_ASKACTION)
|
||||
self.generator.labels.append(msg)
|
||||
self.classDescr.labelsToPropagate.append(msg)
|
||||
# Add the POD-related fields on the Tool
|
||||
self.generator.tool.addPodRelatedFields(self)
|
||||
|
||||
def walkList(self):
|
||||
# Add i18n-specific messages
|
||||
for name, field in self.appyType.fields:
|
||||
label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, name)
|
||||
msg = PoMessage(label, '', name)
|
||||
msg.produceNiceDefault()
|
||||
self.generator.labels.append(msg)
|
||||
|
||||
def walkAppyType(self):
|
||||
'''Walks into the Appy type definition and gathers data about the
|
||||
i18n labels.'''
|
||||
# Manage things common to all Appy types
|
||||
# - optional ?
|
||||
if self.appyType.optional:
|
||||
self.generator.tool.addOptionalField(self)
|
||||
# - edit default value ?
|
||||
if self.appyType.editDefault:
|
||||
self.generator.tool.addDefaultField(self)
|
||||
# - put an index on this field?
|
||||
if self.appyType.indexed and \
|
||||
(self.fieldName not in ('title', 'description')):
|
||||
self.classDescr.addIndexMethod(self)
|
||||
# i18n labels
|
||||
messages = self.generator.labels
|
||||
if not self.appyType.label:
|
||||
# Create labels for generating them in i18n files, only if required.
|
||||
i18nPrefix = "%s_%s" % (self.classDescr.name, self.fieldName)
|
||||
if self.appyType.hasLabel:
|
||||
messages.append(self.produceMessage(i18nPrefix))
|
||||
if self.appyType.hasDescr:
|
||||
descrId = i18nPrefix + '_descr'
|
||||
messages.append(self.produceMessage(descrId,isLabel=False))
|
||||
if self.appyType.hasHelp:
|
||||
helpId = i18nPrefix + '_help'
|
||||
messages.append(self.produceMessage(helpId, isLabel=False))
|
||||
# Create i18n messages linked to pages and phases, only if there is more
|
||||
# than one page/phase for the class.
|
||||
ppMsgs = []
|
||||
if len(self.classDescr.getPhases()) > 1:
|
||||
# Create the message for the name of the phase
|
||||
phaseName = self.appyType.page.phase
|
||||
msgId = '%s_phase_%s' % (self.classDescr.name, phaseName)
|
||||
ppMsgs.append(PoMessage(msgId, '', produceNiceMessage(phaseName)))
|
||||
if len(self.classDescr.getPages()) > 1:
|
||||
# Create the message for the name of the page
|
||||
pageName = self.appyType.page.name
|
||||
msgId = '%s_page_%s' % (self.classDescr.name, pageName)
|
||||
ppMsgs.append(PoMessage(msgId, '', produceNiceMessage(pageName)))
|
||||
for poMsg in ppMsgs:
|
||||
if poMsg not in messages:
|
||||
messages.append(poMsg)
|
||||
self.classDescr.labelsToPropagate.append(poMsg)
|
||||
# Create i18n messages linked to groups
|
||||
group = self.appyType.group
|
||||
if group and not group.label:
|
||||
group.generateLabels(messages, self.classDescr, set())
|
||||
# Manage things which are specific to String types
|
||||
if self.appyType.type == 'String': self.walkString()
|
||||
# Manage things which are specific to Actions
|
||||
elif self.appyType.type == 'Action': self.walkAction()
|
||||
# Manage things which are specific to Ref types
|
||||
elif self.appyType.type == 'Ref': self.walkRef()
|
||||
# Manage things which are specific to Pod types
|
||||
elif self.appyType.type == 'Pod': self.walkPod()
|
||||
# Manage things which are specific to List types
|
||||
elif self.appyType.type == 'List': self.walkList()
|
||||
|
||||
def generate(self):
|
||||
'''Generates the i18n labels for this type.'''
|
||||
self.walkAppyType()
|
||||
|
||||
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.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.toolFieldsToPropagate = [] # For this class, some fields have
|
||||
# been defined on the Tool 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.
|
||||
self.name = getClassName(self.klass, generator.applicationName)
|
||||
self.predefined = False
|
||||
self.customized = False
|
||||
# Phase and page names will be calculated later, when first required.
|
||||
self.phases = None
|
||||
self.pages = None
|
||||
|
||||
def getParents(self, allClasses):
|
||||
parentWrapper = 'AbstractWrapper'
|
||||
parentClass = '%s.%s' % (self.klass.__module__, self.klass.__name__)
|
||||
if self.klass.__bases__:
|
||||
baseClassName = self.klass.__bases__[0].__name__
|
||||
for k in allClasses:
|
||||
if self.klass.__name__ == baseClassName:
|
||||
parentWrapper = '%s_Wrapper' % k.name
|
||||
return (parentWrapper, parentClass)
|
||||
|
||||
def generateSchema(self, configClass=False):
|
||||
'''Generates i18n and other related stuff for this class. If this class
|
||||
is in the configuration (tool, user, etc) we must avoid having
|
||||
attributes that rely on the configuration (ie attributes that are
|
||||
optional, with editDefault=True, etc).'''
|
||||
for attrName in self.orderedAttributes:
|
||||
try:
|
||||
attrValue = getattr(self.klass, attrName)
|
||||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
if isinstance(attrValue, Type):
|
||||
if configClass:
|
||||
attrValue = copy.copy(attrValue)
|
||||
attrValue.optional = False
|
||||
attrValue.editDefault = False
|
||||
FieldDescriptor(attrName, attrValue, self).generate()
|
||||
|
||||
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 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 getCreators(self):
|
||||
'''Gets the specific creators defined for this class.'''
|
||||
res = []
|
||||
if self.klass.__dict__.has_key('creators') and self.klass.creators:
|
||||
for creator in self.klass.creators:
|
||||
if isinstance(creator, Role):
|
||||
if creator.local:
|
||||
raise 'Local role "%s" cannot be used as a creator.' % \
|
||||
creator.name
|
||||
res.append(creator)
|
||||
else:
|
||||
res.append(Role(creator))
|
||||
return res
|
||||
|
||||
def getCreateMean(self, type='Import'):
|
||||
'''Returns the mean for this class that corresponds to p_type, or
|
||||
None if the class does not support this create mean.'''
|
||||
if not self.klass.__dict__.has_key('create'): return None
|
||||
else:
|
||||
means = self.klass.create
|
||||
if not means: return None
|
||||
if not isinstance(means, tuple) and not isinstance(means, list):
|
||||
means = [means]
|
||||
for mean in means:
|
||||
exec 'found = isinstance(mean, %s)' % type
|
||||
if found: return mean
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getSearches(klass):
|
||||
'''Returns the list of searches that are defined on this class.'''
|
||||
res = []
|
||||
if klass.__dict__.has_key('search'):
|
||||
searches = klass.__dict__['search']
|
||||
if isinstance(searches, basestring): res.append(Search(searches))
|
||||
elif isinstance(searches, Search): res.append(searches)
|
||||
else:
|
||||
# It must be a list of searches.
|
||||
for search in searches:
|
||||
if isinstance(search, basestring):res.append(Search(search))
|
||||
else: res.append(search)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def getSearch(klass, searchName):
|
||||
'''Gets the search named p_searchName.'''
|
||||
for search in ClassDescriptor.getSearches(klass):
|
||||
if search.name == searchName:
|
||||
return search
|
||||
return None
|
||||
|
||||
def addIndexMethod(self, field):
|
||||
'''For indexed p_field, this method generates a method that allows to
|
||||
get the value of the field as must be copied into the corresponding
|
||||
index.'''
|
||||
m = self.methods
|
||||
spaces = TABS
|
||||
n = field.fieldName
|
||||
m += '\n' + ' '*spaces + 'def get%s%s(self):\n' % (n[0].upper(), n[1:])
|
||||
spaces += TABS
|
||||
m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n
|
||||
m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \
|
||||
'self)\n' % n
|
||||
self.methods = m
|
||||
|
||||
def addField(self, fieldName, fieldType):
|
||||
'''Adds a new field to the Tool.'''
|
||||
exec "self.modelClass.%s = fieldType" % fieldName
|
||||
if fieldName in self.modelClass._appy_attributes:
|
||||
print 'Warning, field "%s" is already existing on class "%s"' % \
|
||||
(fieldName, self.modelClass.__name__)
|
||||
return
|
||||
self.modelClass._appy_attributes.append(fieldName)
|
||||
self.orderedAttributes.append(fieldName)
|
||||
|
||||
class ToolClassDescriptor(ClassDescriptor):
|
||||
'''Represents the POD-specific fields that must be added to the tool.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['Tool']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom tool
|
||||
definition. We must then add the custom tool elements in this default
|
||||
Tool descriptor.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
|
||||
def isFolder(self, klass=None): return True
|
||||
def isRoot(self): return False
|
||||
def generateSchema(self):
|
||||
ClassDescriptor.generateSchema(self, configClass=True)
|
||||
|
||||
def addOptionalField(self, fieldDescr):
|
||||
className = fieldDescr.classDescr.name
|
||||
fieldName = 'optionalFieldsFor%s' % className
|
||||
fieldType = getattr(self.modelClass, fieldName, None)
|
||||
if not fieldType:
|
||||
fieldType = String(multiplicity=(0,None))
|
||||
fieldType.validator = []
|
||||
self.addField(fieldName, fieldType)
|
||||
fieldType.validator.append(fieldDescr.fieldName)
|
||||
fieldType.page.name = 'data'
|
||||
fieldType.group = Group(fieldDescr.classDescr.klass.__name__)
|
||||
|
||||
def addDefaultField(self, fieldDescr):
|
||||
className = fieldDescr.classDescr.name
|
||||
fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName)
|
||||
fieldType = fieldDescr.appyType.clone()
|
||||
self.addField(fieldName, fieldType)
|
||||
fieldType.page.name = 'data'
|
||||
fieldType.group = Group(fieldDescr.classDescr.klass.__name__)
|
||||
|
||||
def addPodRelatedFields(self, fieldDescr):
|
||||
'''Adds the fields needed in the Tool for configuring a Pod field.'''
|
||||
className = fieldDescr.classDescr.name
|
||||
# On what page and group to display those fields ?
|
||||
pg = {'page': 'documentGeneration',
|
||||
'group': Group(fieldDescr.classDescr.klass.__name__, ['50%']*2)}
|
||||
# Add the field that will store the pod template.
|
||||
fieldName = 'podTemplateFor%s_%s' % (className, fieldDescr.fieldName)
|
||||
fieldType = File(**pg)
|
||||
self.addField(fieldName, fieldType)
|
||||
# Add the field that will store the output format(s)
|
||||
fieldName = 'formatsFor%s_%s' % (className, fieldDescr.fieldName)
|
||||
fieldType = String(validator=Selection('getPodOutputFormats'),
|
||||
multiplicity=(1,None), default=('odt',), **pg)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
def addQueryResultColumns(self, classDescr):
|
||||
'''Adds, for class p_classDescr, the attribute in the tool that allows
|
||||
to select what default columns will be shown on query results.'''
|
||||
className = classDescr.name
|
||||
fieldName = 'resultColumnsFor%s' % className
|
||||
fieldType = String(multiplicity=(0,None), validator=Selection(
|
||||
'_appy_getAllFields*%s' % className), page='userInterface',
|
||||
group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
def addSearchRelatedFields(self, classDescr):
|
||||
'''Adds, for class p_classDescr, attributes related to the search
|
||||
functionality for class p_classDescr.'''
|
||||
className = classDescr.name
|
||||
# Field that defines if advanced search is enabled for class
|
||||
# p_classDescr or not.
|
||||
fieldName = 'enableAdvancedSearchFor%s' % className
|
||||
fieldType = Boolean(default=True, page='userInterface',
|
||||
group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
# Field that defines how many columns are shown on the custom search
|
||||
# screen.
|
||||
fieldName = 'numberOfSearchColumnsFor%s' % className
|
||||
fieldType = Integer(default=3, page='userInterface',
|
||||
group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
# Field that allows to select, among all indexed fields, what fields
|
||||
# must really be used in the search screen.
|
||||
fieldName = 'searchFieldsFor%s' % className
|
||||
defaultValue = [a[0] for a in classDescr.getOrderedAppyAttributes(
|
||||
condition='attrValue.indexed')]
|
||||
fieldType = String(multiplicity=(0,None), validator=Selection(
|
||||
'_appy_getSearchableFields*%s' % className), default=defaultValue,
|
||||
page='userInterface', group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
def addImportRelatedFields(self, classDescr):
|
||||
'''Adds, for class p_classDescr, attributes related to the import
|
||||
functionality for class p_classDescr.'''
|
||||
className = classDescr.name
|
||||
# Field that defines the path of the files to import.
|
||||
fieldName = 'importPathFor%s' % className
|
||||
defValue = classDescr.getCreateMean('Import').path
|
||||
fieldType = String(page='data', multiplicity=(1,1), default=defValue,
|
||||
group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
def addWorkflowFields(self, 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)
|
||||
self.addField(fieldName, fieldType)
|
||||
# Adds the boolean field for showing or not the field "enter comments".
|
||||
fieldName = 'showWorkflowCommentFieldFor%s' % className
|
||||
fieldType = Boolean(default=defaultValue, page='userInterface',
|
||||
group=groupName)
|
||||
self.addField(fieldName, fieldType)
|
||||
# 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)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
class UserClassDescriptor(ClassDescriptor):
|
||||
'''Represents an Archetypes-compliant class that corresponds to the User
|
||||
for the generated application.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['User']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom user
|
||||
definition. We must then add the custom user elements in this
|
||||
default User descriptor.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
def isFolder(self, klass=None): return False
|
||||
def generateSchema(self):
|
||||
ClassDescriptor.generateSchema(self, configClass=True)
|
||||
|
||||
class GroupClassDescriptor(ClassDescriptor):
|
||||
'''Represents the class that corresponds to the Group for the generated
|
||||
application.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['Group']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom group
|
||||
definition. We must then add the custom group elements in this
|
||||
default Group descriptor.
|
||||
|
||||
NOTE: currently, it is not possible to define a custom Group
|
||||
class.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
def isFolder(self, klass=None): return False
|
||||
def generateSchema(self):
|
||||
ClassDescriptor.generateSchema(self, configClass=True)
|
||||
|
||||
class TranslationClassDescriptor(ClassDescriptor):
|
||||
'''Represents the set of translation ids for a gen-application.'''
|
||||
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
|
||||
def getParents(self, allClasses=()): return ('Translation',)
|
||||
|
||||
def generateSchema(self):
|
||||
ClassDescriptor.generateSchema(self, configClass=True)
|
||||
|
||||
def addLabelField(self, messageId, page):
|
||||
'''Adds a Computed field that will display, in the source language, the
|
||||
content of the text to translate.'''
|
||||
field = Computed(method=self.modelClass.label, plainText=False,
|
||||
page=page, show=self.modelClass.show, layouts='f')
|
||||
self.addField('%s_label' % messageId, field)
|
||||
|
||||
def addMessageField(self, messageId, page, i18nFiles):
|
||||
'''Adds a message field corresponding to p_messageId to the Translation
|
||||
class, on a given p_page. We need i18n files p_i18nFiles for
|
||||
fine-tuning the String type to generate for this field (one-line?
|
||||
several lines?...)'''
|
||||
params = {'page':page, 'layouts':'f', 'show': self.modelClass.show}
|
||||
appName = self.generator.applicationName
|
||||
# Scan all messages corresponding to p_messageId from all translation
|
||||
# files. We will define field length from the longer found message
|
||||
# content.
|
||||
maxLine = 100 # We suppose a line is 100 characters long.
|
||||
width = 0
|
||||
height = 0
|
||||
for fileName, poFile in i18nFiles.iteritems():
|
||||
if not fileName.startswith('%s-' % appName) or \
|
||||
not i18nFiles[fileName].messagesDict.has_key(messageId):
|
||||
# In this case this is not one of our Appy-managed translation
|
||||
# files.
|
||||
continue
|
||||
msgContent = i18nFiles[fileName].messagesDict[messageId].msg
|
||||
# Compute width
|
||||
width = max(width, len(msgContent))
|
||||
# Compute height (a "\n" counts for one line)
|
||||
mHeight = int(len(msgContent)/maxLine) + msgContent.count('<br/>')
|
||||
height = max(height, mHeight)
|
||||
if height < 1:
|
||||
# This is a one-line field.
|
||||
params['width'] = width
|
||||
else:
|
||||
# This is a multi-line field, or a very-long-single-lined field
|
||||
params['format'] = String.TEXT
|
||||
params['height'] = height
|
||||
self.addField(messageId, String(**params))
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,608 +0,0 @@
|
|||
'''This file contains the main Generator class used for generating a Zope
|
||||
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 appy.gen.utils import getClassName
|
||||
from appy.gen.descriptors import WorkflowDescriptor
|
||||
from descriptors import *
|
||||
from model import ModelClass, User, Group, Tool, Translation
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
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):
|
||||
Tool._appy_clean()
|
||||
AbstractGenerator.__init__(self, *args, **kwargs)
|
||||
# Set our own Descriptor classes
|
||||
self.descriptorClasses['class'] = ClassDescriptor
|
||||
# Create our own Tool, User, Group and Translation instances
|
||||
self.tool = ToolClassDescriptor(Tool, self)
|
||||
self.user = UserClassDescriptor(User, self)
|
||||
self.group = GroupClassDescriptor(Group, self)
|
||||
self.translation = TranslationClassDescriptor(Translation, self)
|
||||
# i18n labels to generate
|
||||
self.labels = [] # i18n labels
|
||||
self.toolInstanceName = 'portal_%s' % self.applicationName.lower()
|
||||
# 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.
|
||||
self.repls.update({'toolInstanceName': self.toolInstanceName})
|
||||
self.referers = {}
|
||||
|
||||
versionRex = re.compile('(.*?\s+build)\s+(\d+)')
|
||||
def initialize(self):
|
||||
# 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('appy_title', '', msg.APPY_TITLE),
|
||||
msg('data_change', '', msg.DATA_CHANGE),
|
||||
msg('modified_field', '', msg.MODIFIED_FIELD),
|
||||
msg('previous_value', '', msg.PREVIOUS_VALUE),
|
||||
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_actions', '', msg.REF_ACTIONS),
|
||||
msg('action_ok', '', msg.ACTION_OK),
|
||||
msg('action_ko', '', msg.ACTION_KO),
|
||||
msg('move_up', '', msg.REF_MOVE_UP),
|
||||
msg('move_down', '', msg.REF_MOVE_DOWN),
|
||||
msg('query_create', '', msg.QUERY_CREATE),
|
||||
msg('query_import', '', msg.QUERY_IMPORT),
|
||||
msg('query_no_result', '', msg.QUERY_NO_RESULT),
|
||||
msg('query_consult_all', '', msg.QUERY_CONSULT_ALL),
|
||||
msg('import_title', '', msg.IMPORT_TITLE),
|
||||
msg('import_show_hide', '', msg.IMPORT_SHOW_HIDE),
|
||||
msg('import_already', '', msg.IMPORT_ALREADY),
|
||||
msg('import_many', '', msg.IMPORT_MANY),
|
||||
msg('import_done', '', msg.IMPORT_DONE),
|
||||
msg('search_title', '', msg.SEARCH_TITLE),
|
||||
msg('search_button', '', msg.SEARCH_BUTTON),
|
||||
msg('search_objects', '', msg.SEARCH_OBJECTS),
|
||||
msg('search_results', '', msg.SEARCH_RESULTS),
|
||||
msg('search_results_descr', '', ' '),
|
||||
msg('search_new', '', msg.SEARCH_NEW),
|
||||
msg('search_from', '', msg.SEARCH_FROM),
|
||||
msg('search_to', '', msg.SEARCH_TO),
|
||||
msg('search_or', '', msg.SEARCH_OR),
|
||||
msg('search_and', '', msg.SEARCH_AND),
|
||||
msg('ref_invalid_index', '', msg.REF_INVALID_INDEX),
|
||||
msg('bad_long', '', msg.BAD_LONG),
|
||||
msg('bad_float', '', msg.BAD_FLOAT),
|
||||
msg('bad_date', '', msg.BAD_DATE),
|
||||
msg('bad_email', '', msg.BAD_EMAIL),
|
||||
msg('bad_url', '', msg.BAD_URL),
|
||||
msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC),
|
||||
msg('bad_select_value', '', msg.BAD_SELECT_VALUE),
|
||||
msg('select_delesect', '', msg.SELECT_DESELECT),
|
||||
msg('no_elem_selected', '', msg.NO_SELECTION),
|
||||
msg('delete_confirm', '', msg.DELETE_CONFIRM),
|
||||
msg('delete_done', '', msg.DELETE_DONE),
|
||||
msg('goto_first', '', msg.GOTO_FIRST),
|
||||
msg('goto_previous', '', msg.GOTO_PREVIOUS),
|
||||
msg('goto_next', '', msg.GOTO_NEXT),
|
||||
msg('goto_last', '', msg.GOTO_LAST),
|
||||
msg('goto_source', '', msg.GOTO_SOURCE),
|
||||
msg('whatever', '', msg.WHATEVER),
|
||||
msg('yes', '', msg.YES),
|
||||
msg('no', '', msg.NO),
|
||||
msg('field_required', '', msg.FIELD_REQUIRED),
|
||||
msg('field_invalid', '', msg.FIELD_INVALID),
|
||||
msg('file_required', '', msg.FILE_REQUIRED),
|
||||
msg('image_required', '', msg.IMAGE_REQUIRED),
|
||||
msg('odt', '', msg.FORMAT_ODT),
|
||||
msg('pdf', '', msg.FORMAT_PDF),
|
||||
msg('doc', '', msg.FORMAT_DOC),
|
||||
msg('rtf', '', msg.FORMAT_RTF),
|
||||
msg('front_page_text', '', msg.FRONT_PAGE_TEXT),
|
||||
]
|
||||
# Create a label for every role added by this application
|
||||
for role in self.getAllUsedRoles():
|
||||
self.labels.append(msg('role_%s' % role.name,'', role.name,
|
||||
niceDefault=True))
|
||||
# Create basic files (config.py, Install.py, etc)
|
||||
self.generateTool()
|
||||
self.generateInit()
|
||||
self.generateTests()
|
||||
self.generateConfigureZcml()
|
||||
self.copyFile('import_steps.xml', self.repls,
|
||||
destFolder='profiles/default')
|
||||
self.copyFile('ProfileInit.py', self.repls, destFolder='profiles',
|
||||
destName='__init__.py')
|
||||
# Create version.txt
|
||||
f = open(os.path.join(self.outputFolder, 'version.txt'), 'w')
|
||||
f.write(self.version)
|
||||
f.close()
|
||||
# Make Extensions and tests Python packages
|
||||
for moduleFolder in ('Extensions', 'tests'):
|
||||
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
|
||||
# We update the POT file with our list of automatically managed labels.
|
||||
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))
|
||||
# Before generating the POT file, we still need to add one label for
|
||||
# every page for the Translation class. We've not done it yet because
|
||||
# the number of pages depends on the total number of labels in the POT
|
||||
# file.
|
||||
pageLabels = []
|
||||
nbOfPages = int(len(potFile.messages)/self.config.translationsPerPage)+1
|
||||
for i in range(nbOfPages):
|
||||
msgId = '%s_page_%d' % (self.translation.name, i+2)
|
||||
pageLabels.append(msg(msgId, '', 'Page %d' % (i+2)))
|
||||
potFile.update(pageLabels, keepExistingOrder=False)
|
||||
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 corresponding fields on the Translation class
|
||||
page = 'main'
|
||||
i = 0
|
||||
for message in potFile.messages:
|
||||
i += 1
|
||||
# A computed field is used for displaying the text to translate.
|
||||
self.translation.addLabelField(message.id, page)
|
||||
# A String field will hold the translation in itself.
|
||||
self.translation.addMessageField(message.id, page, self.i18nFiles)
|
||||
if (i % self.config.translationsPerPage) == 0:
|
||||
# A new page must be defined.
|
||||
if page == 'main':
|
||||
page = '2'
|
||||
else:
|
||||
page = str(int(page)+1)
|
||||
# Generate i18n po files for other potential files
|
||||
for poFile in self.i18nFiles.itervalues():
|
||||
if not poFile.generated:
|
||||
poFile.generate()
|
||||
self.generateWrappers()
|
||||
self.generateConfig()
|
||||
|
||||
def getAllUsedRoles(self, plone=None, local=None, grantable=None):
|
||||
'''Produces a list of all the roles used within all workflows and
|
||||
classes defined in this application.
|
||||
|
||||
If p_plone is True, it keeps only Plone-standard roles; if p_plone
|
||||
is False, it keeps only roles which are specific to this application;
|
||||
if p_plone is None it has no effect (so it keeps both roles).
|
||||
|
||||
If p_local is True, it keeps only local roles (ie, roles that can
|
||||
only be granted locally); if p_local is False, it keeps only "global"
|
||||
roles; if p_local is None it has no effect (so it keeps both roles).
|
||||
|
||||
If p_grantable is True, it keeps only roles that the admin can
|
||||
grant; if p_grantable is False, if keeps only ungrantable roles (ie
|
||||
those that are implicitly granted by the system like role
|
||||
"Authenticated"); if p_grantable is None it keeps both roles.'''
|
||||
allRoles = {} # ~{s_roleName:Role_role}~
|
||||
# Gather roles from workflow states and transitions
|
||||
for wfDescr in self.workflows:
|
||||
for attr in dir(wfDescr.klass):
|
||||
attrValue = getattr(wfDescr.klass, attr)
|
||||
if isinstance(attrValue, State) or \
|
||||
isinstance(attrValue, Transition):
|
||||
for role in attrValue.getUsedRoles():
|
||||
if role.name not in allRoles:
|
||||
allRoles[role.name] = role
|
||||
# Gather roles from "creators" attributes from every class
|
||||
for cDescr in self.getClasses(include='all'):
|
||||
for role in cDescr.getCreators():
|
||||
if role.name not in allRoles:
|
||||
allRoles[role.name] = role
|
||||
res = allRoles.values()
|
||||
# Filter the result according to parameters
|
||||
for p in ('plone', 'local', 'grantable'):
|
||||
if eval(p) != None:
|
||||
res = [r for r in res if eval('r.%s == %s' % (p, p))]
|
||||
return res
|
||||
|
||||
def addReferer(self, fieldDescr):
|
||||
'''p_fieldDescr is a Ref type definition.'''
|
||||
k = fieldDescr.appyType.klass
|
||||
refClassName = getClassName(k, self.applicationName)
|
||||
if not self.referers.has_key(refClassName):
|
||||
self.referers[refClassName] = []
|
||||
self.referers[refClassName].append(fieldDescr)
|
||||
|
||||
def getAppyTypePath(self, name, appyType, klass, isBack=False):
|
||||
'''Gets the path to the p_appyType when a direct reference to an
|
||||
appyType must be generated in a Python file.'''
|
||||
if issubclass(klass, ModelClass):
|
||||
res = 'wraps.%s.%s' % (klass.__name__, name)
|
||||
else:
|
||||
res = '%s.%s.%s' % (klass.__module__, klass.__name__, name)
|
||||
if isBack: res += '.back'
|
||||
return res
|
||||
|
||||
def getClasses(self, include=None):
|
||||
'''Returns the descriptors for all the classes in the generated
|
||||
gen-application. If p_include is:
|
||||
* "all" it includes the descriptors for the config-related
|
||||
classes (tool, user, group, translation)
|
||||
* "allButTool" it includes the same descriptors, the tool excepted
|
||||
* "custom" it includes descriptors for the config-related classes
|
||||
for which the user has created a sub-class.'''
|
||||
if not include: return self.classes
|
||||
res = self.classes[:]
|
||||
configClasses = [self.tool, self.user, self.group, self.translation]
|
||||
if include == 'all':
|
||||
res += configClasses
|
||||
elif include == 'allButTool':
|
||||
res += configClasses[1:]
|
||||
elif include == 'custom':
|
||||
res += [c for c in configClasses if c.customized]
|
||||
elif include == 'predefined':
|
||||
res = configClasses
|
||||
return res
|
||||
|
||||
def generateConfigureZcml(self):
|
||||
'''Generates file configure.zcml.'''
|
||||
repls = self.repls.copy()
|
||||
# Note every class as "deprecated".
|
||||
depr = ''
|
||||
for klass in self.getClasses(include='all'):
|
||||
depr += '<five:deprecatedManageAddDelete class=".%s.%s"/>\n' % \
|
||||
(klass.name, klass.name)
|
||||
repls['deprecated'] = depr
|
||||
self.copyFile('configure.zcml', repls)
|
||||
|
||||
def generateConfig(self):
|
||||
repls = self.repls.copy()
|
||||
# Get some lists of classes
|
||||
classes = self.getClasses()
|
||||
classesWithCustom = self.getClasses(include='custom')
|
||||
classesButTool = self.getClasses(include='allButTool')
|
||||
classesAll = self.getClasses(include='all')
|
||||
# Compute imports
|
||||
imports = ['import %s' % self.applicationName]
|
||||
for classDescr in (classesWithCustom + self.workflows):
|
||||
theImport = 'import %s' % classDescr.klass.__module__
|
||||
if theImport not in imports:
|
||||
imports.append(theImport)
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
# Compute default add roles
|
||||
repls['defaultAddRoles'] = ','.join(
|
||||
['"%s"' % r for r in self.config.defaultCreators])
|
||||
# Compute list of add permissions
|
||||
addPermissions = ''
|
||||
for classDescr in classesAll:
|
||||
addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name,
|
||||
self.applicationName, classDescr.name)
|
||||
repls['addPermissions'] = addPermissions
|
||||
# Compute root classes
|
||||
repls['rootClasses'] = ','.join(["'%s'" % c.name \
|
||||
for c in classesButTool if c.isRoot()])
|
||||
# Compute list of class definitions
|
||||
repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \
|
||||
c.klass.__name__) for c in classes])
|
||||
# Compute lists of class names
|
||||
repls['appClassNames'] = ','.join(['"%s"' % c.name \
|
||||
for c in classes])
|
||||
repls['allClassNames'] = ','.join(['"%s"' % c.name \
|
||||
for c in classesButTool])
|
||||
# Compute the list of ordered attributes (forward and backward,
|
||||
# inherited included) for every Appy class.
|
||||
attributes = []
|
||||
for classDescr in classesAll:
|
||||
titleFound = False
|
||||
names = []
|
||||
for name, appyType, klass in classDescr.getOrderedAppyAttributes():
|
||||
names.append(name)
|
||||
if name == 'title': titleFound = True
|
||||
# Add the "title" mandatory field if not found
|
||||
if not titleFound: names.insert(0, 'title')
|
||||
# Any backward attributes to append?
|
||||
if classDescr.name in self.referers:
|
||||
for field in self.referers[classDescr.name]:
|
||||
names.append(field.appyType.back.attribute)
|
||||
qNames = ['"%s"' % name for name in names]
|
||||
attributes.append('"%s":[%s]' % (classDescr.name, ','.join(qNames)))
|
||||
repls['attributes'] = ',\n '.join(attributes)
|
||||
# Compute list of used roles for registering them if needed
|
||||
specificRoles = self.getAllUsedRoles(plone=False)
|
||||
repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles])
|
||||
globalRoles = self.getAllUsedRoles(plone=False, local=False)
|
||||
repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles])
|
||||
grantableRoles = self.getAllUsedRoles(local=False, grantable=True)
|
||||
repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles])
|
||||
# Generate configuration options
|
||||
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
|
||||
repls['languageSelector'] = self.config.languageSelector
|
||||
repls['appFrontPage'] = bool(self.config.frontPage)
|
||||
repls['sourceLanguage'] = self.config.sourceLanguage
|
||||
self.copyFile('config.py', repls)
|
||||
|
||||
def generateInit(self):
|
||||
# Compute imports
|
||||
imports = []
|
||||
classNames = []
|
||||
for c in self.getClasses(include='all'):
|
||||
importDef = ' import %s' % c.name
|
||||
if importDef not in imports:
|
||||
imports.append(importDef)
|
||||
classNames.append("%s.%s" % (c.name, c.name))
|
||||
repls = self.repls.copy()
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
repls['classes'] = ','.join(classNames)
|
||||
repls['totalNumberOfTests'] = self.totalNumberOfTests
|
||||
self.copyFile('__init__.py', repls)
|
||||
|
||||
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.getClasses(include='all')
|
||||
for c in self.getClassesInOrder(allClasses):
|
||||
if not c.predefined or c.customized:
|
||||
moduleImport = 'import %s' % c.klass.__module__
|
||||
if moduleImport not in imports:
|
||||
imports.append(moduleImport)
|
||||
# Determine parent wrapper and class
|
||||
parentClasses = c.getParents(allClasses)
|
||||
wrapperDef = 'class %s_Wrapper(%s):\n' % \
|
||||
(c.name, ','.join(parentClasses))
|
||||
wrapperDef += ' security = ClassSecurityInfo()\n'
|
||||
if c.customized:
|
||||
# For custom tool, add a call to a method that allows to
|
||||
# customize elements from the base class.
|
||||
wrapperDef += " if hasattr(%s, 'update'):\n " \
|
||||
"%s.update(%s)\n" % (parentClasses[1], parentClasses[1],
|
||||
parentClasses[0])
|
||||
# For custom tool, add security declaration that will allow to
|
||||
# call their methods from ZPTs.
|
||||
for parentClass in parentClasses:
|
||||
wrapperDef += " for elem in dir(%s):\n " \
|
||||
"if not elem.startswith('_'): security.declarePublic" \
|
||||
"(elem)\n" % (parentClass)
|
||||
# Register the class in Zope.
|
||||
wrapperDef += 'InitializeClass(%s_Wrapper)\n' % c.name
|
||||
wrappers.append(wrapperDef)
|
||||
repls = self.repls.copy()
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
repls['wrappers'] = '\n'.join(wrappers)
|
||||
for klass in self.getClasses(include='predefined'):
|
||||
modelClass = klass.modelClass
|
||||
repls['%s' % modelClass.__name__] = modelClass._appy_getBody()
|
||||
self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
|
||||
|
||||
def generateTests(self):
|
||||
'''Generates the file needed for executing tests.'''
|
||||
repls = self.repls.copy()
|
||||
modules = self.modulesWithTests
|
||||
repls['imports'] = '\n'.join(['import %s' % m for m in modules])
|
||||
repls['modulesWithTests'] = ','.join(modules)
|
||||
self.copyFile('testAll.py', repls, destFolder='tests')
|
||||
|
||||
def generateTool(self):
|
||||
'''Generates the Plone tool that corresponds to this application.'''
|
||||
Msg = PoMessage
|
||||
# Create Tool-related i18n-related messages
|
||||
msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName)
|
||||
self.labels.append(msg)
|
||||
|
||||
# Tune the Ref field between Tool->User and Group->User
|
||||
Tool.users.klass = User
|
||||
if self.user.customized:
|
||||
Tool.users.klass = self.user.klass
|
||||
Group.users.klass = self.user.klass
|
||||
|
||||
# Generate the Tool-related classes (User, Group, Translation)
|
||||
for klass in (self.user, self.group, self.translation):
|
||||
klassType = klass.name[len(self.applicationName):]
|
||||
klass.generateSchema()
|
||||
self.labels += [ Msg(klass.name, '', klassType),
|
||||
Msg('%s_plural' % klass.name,'', klass.name+'s')]
|
||||
repls = self.repls.copy()
|
||||
repls.update({'methods': klass.methods, 'genClassName': klass.name,
|
||||
'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem',
|
||||
'classDoc': 'Standard Appy class', 'icon':'object.gif'})
|
||||
self.copyFile('Class.py', repls, destName='%s.py' % klass.name)
|
||||
|
||||
# Before generating the Tool class, finalize it with query result
|
||||
# columns, with fields to propagate, workflow-related fields.
|
||||
for classDescr in self.getClasses(include='allButTool'):
|
||||
for fieldName, fieldType in classDescr.toolFieldsToPropagate:
|
||||
for childDescr in classDescr.getChildren():
|
||||
childFieldName = fieldName % childDescr.name
|
||||
fieldType.group = childDescr.klass.__name__
|
||||
self.tool.addField(childFieldName, fieldType)
|
||||
if classDescr.isRoot():
|
||||
# We must be able to configure query results from the tool.
|
||||
self.tool.addQueryResultColumns(classDescr)
|
||||
# Add the search-related fields.
|
||||
self.tool.addSearchRelatedFields(classDescr)
|
||||
importMean = classDescr.getCreateMean('Import')
|
||||
if importMean:
|
||||
self.tool.addImportRelatedFields(classDescr)
|
||||
self.tool.addWorkflowFields(self.user)
|
||||
self.tool.generateSchema()
|
||||
|
||||
# Generate the Tool class
|
||||
repls = self.repls.copy()
|
||||
repls.update({'methods': self.tool.methods,
|
||||
'genClassName': self.tool.name, 'baseMixin':'ToolMixin',
|
||||
'parents': 'ToolMixin, Folder', 'icon': 'folder.gif',
|
||||
'classDoc': 'Tool class for %s' % self.applicationName})
|
||||
self.copyFile('Class.py', repls, destName='%s.py' % self.tool.name)
|
||||
|
||||
def generateClass(self, classDescr):
|
||||
'''Is called each time an Appy class is found in the application, for
|
||||
generating the corresponding Archetype class.'''
|
||||
k = classDescr.klass
|
||||
print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__)
|
||||
if not classDescr.isAbstract():
|
||||
self.tool.addWorkflowFields(classDescr)
|
||||
# Determine base Zope class
|
||||
isFolder = classDescr.isFolder()
|
||||
baseClass = isFolder and 'Folder' or 'SimpleItem'
|
||||
icon = isFolder and 'folder.gif' or 'object.gif'
|
||||
parents = 'BaseMixin, %s' % baseClass
|
||||
classDoc = classDescr.klass.__doc__ or 'Appy class.'
|
||||
repls = self.repls.copy()
|
||||
classDescr.generateSchema()
|
||||
repls.update({
|
||||
'parents': parents, 'className': classDescr.klass.__name__,
|
||||
'genClassName': classDescr.name, 'baseMixin':'BaseMixin',
|
||||
'classDoc': classDoc, 'applicationName': self.applicationName,
|
||||
'methods': classDescr.methods, 'icon':icon})
|
||||
fileName = '%s.py' % classDescr.name
|
||||
# Create i18n labels (class name and plural form)
|
||||
poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__)
|
||||
poMsg.produceNiceDefault()
|
||||
self.labels.append(poMsg)
|
||||
poMsgPl = PoMessage('%s_plural' % classDescr.name, '',
|
||||
classDescr.klass.__name__+'s')
|
||||
poMsgPl.produceNiceDefault()
|
||||
self.labels.append(poMsgPl)
|
||||
# Create i18n labels for searches
|
||||
for search in classDescr.getSearches(classDescr.klass):
|
||||
searchLabel = '%s_search_%s' % (classDescr.name, search.name)
|
||||
labels = [searchLabel, '%s_descr' % searchLabel]
|
||||
if search.group:
|
||||
grpLabel = '%s_searchgroup_%s' % (classDescr.name, search.group)
|
||||
labels += [grpLabel, '%s_descr' % grpLabel]
|
||||
for label in labels:
|
||||
default = ' '
|
||||
if label == searchLabel: default = search.name
|
||||
poMsg = PoMessage(label, '', default)
|
||||
poMsg.produceNiceDefault()
|
||||
if poMsg not in self.labels:
|
||||
self.labels.append(poMsg)
|
||||
# Generate the resulting Archetypes class.
|
||||
self.copyFile('Class.py', repls, destName=fileName)
|
||||
|
||||
def generateWorkflow(self, wfDescr):
|
||||
'''This method creates the i18n labels related to the workflow described
|
||||
by p_wfDescr.'''
|
||||
k = wfDescr.klass
|
||||
print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)
|
||||
# Identify workflow name
|
||||
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
||||
# Add i18n messages for states
|
||||
for name in dir(wfDescr.klass):
|
||||
if not isinstance(getattr(wfDescr.klass, name), State): continue
|
||||
poMsg = PoMessage('%s_%s' % (wfName, name), '', name)
|
||||
poMsg.produceNiceDefault()
|
||||
self.labels.append(poMsg)
|
||||
# Add i18n messages for transitions
|
||||
for name in dir(wfDescr.klass):
|
||||
transition = getattr(wfDescr.klass, name)
|
||||
if not isinstance(transition, Transition): continue
|
||||
poMsg = PoMessage('%s_%s' % (wfName, name), '', name)
|
||||
poMsg.produceNiceDefault()
|
||||
self.labels.append(poMsg)
|
||||
if transition.confirm:
|
||||
# We need to generate a label for the message that will be shown
|
||||
# in the confirm popup.
|
||||
label = '%s_%s_confirm' % (wfName, name)
|
||||
poMsg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||
self.labels.append(poMsg)
|
||||
if transition.notify:
|
||||
# Appy will send a mail when this transition is triggered.
|
||||
# So we need 2 i18n labels: one for the mail subject and one for
|
||||
# the mail body.
|
||||
subjectLabel = '%s_%s_mail_subject' % (wfName, name)
|
||||
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
|
||||
self.labels.append(poMsg)
|
||||
bodyLabel = '%s_%s_mail_body' % (wfName, name)
|
||||
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
|
||||
self.labels.append(poMsg)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,403 +0,0 @@
|
|||
'''This package contains stuff used at run-time for installing a generated
|
||||
Zope product.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, time
|
||||
import appy
|
||||
import appy.version
|
||||
from appy.gen import Type, Ref, String, File
|
||||
from appy.gen.po import PoParser
|
||||
from appy.gen.utils import updateRolesForPermission, createObject
|
||||
from appy.shared.data import languages
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
homePage = '''
|
||||
<tal:main define="tool python: context.config">
|
||||
<html metal:use-macro="context/ui/template/macros/main">
|
||||
<div metal:fill-slot="content">
|
||||
<span tal:replace="structure python: tool.translate('front_page_text')"/>
|
||||
</div>
|
||||
</html>
|
||||
</tal:main>
|
||||
'''
|
||||
errorPage = '''
|
||||
<tal:main define="tool python: context.config">
|
||||
<html metal:use-macro="context/ui/template/macros/main">
|
||||
<div metal:fill-slot="content" tal:define="o python:options">
|
||||
<p tal:condition="o/error_message"
|
||||
tal:content="structure o/error_message"></p>
|
||||
<p>Error type: <b><span tal:replace="o/error_type"/></b></p>
|
||||
<p>Error value: <b><span tal:replace="o/error_value"/></b></p>
|
||||
<p tal:content="structure o/error_tb"></p>
|
||||
</div>
|
||||
</html>
|
||||
</tal:main>
|
||||
'''
|
||||
|
||||
# Stuff for tracking user activity ---------------------------------------------
|
||||
loggedUsers = {}
|
||||
originalTraverse = None
|
||||
doNotTrack = ('.jpg','.gif','.png','.js','.class','.css')
|
||||
|
||||
def traverseWrapper(self, path, response=None, validated_hook=None):
|
||||
'''This function is called every time a users gets a URL, this is used for
|
||||
tracking user activity. self is a BaseRequest'''
|
||||
res = originalTraverse(self, path, response, validated_hook)
|
||||
t = time.time()
|
||||
if os.path.splitext(path)[-1].lower() not in doNotTrack:
|
||||
# Do nothing when the user gets non-pages
|
||||
userId = self['AUTHENTICATED_USER'].getId()
|
||||
if userId:
|
||||
loggedUsers[userId] = t
|
||||
# "Touch" the SESSION object. Else, expiration won't occur.
|
||||
session = self.SESSION
|
||||
return res
|
||||
|
||||
def onDelSession(sessionObject, container):
|
||||
'''This function is called when a session expires.'''
|
||||
rq = container.REQUEST
|
||||
if rq.cookies.has_key('__ac') and rq.cookies.has_key('_ZopeId') and \
|
||||
(rq['_ZopeId'] == sessionObject.token):
|
||||
# The request comes from a guy whose session has expired.
|
||||
resp = rq.RESPONSE
|
||||
resp.expireCookie('__ac', path='/')
|
||||
resp.write('<center>For security reasons, your session has ' \
|
||||
'expired.</center>')
|
||||
|
||||
class ZCTextIndexInfo:
|
||||
'''Silly class used for storing information about a ZCTextIndex.'''
|
||||
lexicon_id = "lexicon"
|
||||
index_type = 'Okapi BM25 Rank'
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZopeInstaller:
|
||||
'''This Zope installer runs every time Zope starts and encounters this
|
||||
generated Zope product.'''
|
||||
def __init__(self, zopeContext, config, classes):
|
||||
self.zopeContext = zopeContext
|
||||
self.app = zopeContext._ProductContext__app # The root of the Zope tree
|
||||
self.config = config
|
||||
self.classes = classes
|
||||
# Unwrap some useful config variables
|
||||
self.productName = config.PROJECTNAME
|
||||
self.languages = config.languages
|
||||
self.logger = config.logger
|
||||
self.addContentPermissions = config.ADD_CONTENT_PERMISSIONS
|
||||
|
||||
def installUi(self):
|
||||
'''Installs the user interface.'''
|
||||
# Delete the existing folder if it existed.
|
||||
zopeContent = self.app.objectIds()
|
||||
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
|
||||
self.app.manage_addFolder('ui')
|
||||
# Some useful imports
|
||||
from Products.PythonScripts.PythonScript import PythonScript
|
||||
from Products.PageTemplates.ZopePageTemplate import \
|
||||
manage_addPageTemplate
|
||||
# Browse the physical folder and re-create it in the Zope folder
|
||||
j = os.path.join
|
||||
ui = j(j(appy.getPath(), 'gen'), 'ui')
|
||||
for root, dirs, files in os.walk(ui):
|
||||
folderName = root[len(ui):]
|
||||
# Get the Zope folder that corresponds to this name
|
||||
zopeFolder = self.app.ui
|
||||
if folderName:
|
||||
for name in folderName.strip(os.sep).split(os.sep):
|
||||
zopeFolder = zopeFolder._getOb(name)
|
||||
# Create sub-folders at this level
|
||||
for name in dirs: zopeFolder.manage_addFolder(name)
|
||||
# Create files at this level
|
||||
for name in files:
|
||||
baseName, ext = os.path.splitext(name)
|
||||
f = file(j(root, name))
|
||||
if ext in File.imageExts:
|
||||
zopeFolder.manage_addImage(name, f)
|
||||
elif ext == '.pt':
|
||||
manage_addPageTemplate(zopeFolder, baseName, '', f.read())
|
||||
elif ext == '.py':
|
||||
obj = PythonScript(baseName)
|
||||
zopeFolder._setObject(baseName, obj)
|
||||
zopeFolder._getOb(baseName).write(f.read())
|
||||
else:
|
||||
zopeFolder.manage_addFile(name, f)
|
||||
f.close()
|
||||
# Update the home page
|
||||
if 'index_html' in zopeContent:
|
||||
self.app.manage_delObjects(['index_html'])
|
||||
manage_addPageTemplate(self.app, 'index_html', '', homePage)
|
||||
# Update the error page
|
||||
if 'standard_error_message' in zopeContent:
|
||||
self.app.manage_delObjects(['standard_error_message'])
|
||||
manage_addPageTemplate(self.app, 'standard_error_message', '',errorPage)
|
||||
|
||||
def installIndexes(self, indexInfo):
|
||||
'''Updates indexes in the catalog.'''
|
||||
catalog = self.app.catalog
|
||||
logger = self.logger
|
||||
for indexName, indexType in indexInfo.iteritems():
|
||||
# If this index already exists but with a different type, remove it.
|
||||
if indexName in catalog.indexes():
|
||||
oldType = catalog.Indexes[indexName].__class__.__name__
|
||||
if oldType != indexType:
|
||||
catalog.delIndex(indexName)
|
||||
logger.info('Existing index "%s" of type "%s" was removed:'\
|
||||
' we need to recreate it with type "%s".' % \
|
||||
(indexName, oldType, indexType))
|
||||
if indexName not in catalog.indexes():
|
||||
# We need to create this index
|
||||
if indexType != 'ZCTextIndex':
|
||||
catalog.addIndex(indexName, indexType)
|
||||
else:
|
||||
catalog.addIndex(indexName, indexType,extra=ZCTextIndexInfo)
|
||||
catalog.reindexIndex(indexName, self.app.REQUEST)
|
||||
logger.info('Created index "%s" of type "%s"...' % \
|
||||
(indexName, indexType))
|
||||
# Indexing database content based on this index.
|
||||
|
||||
lexiconInfos = [
|
||||
appy.Object(group='Case Normalizer', name='Case Normalizer'),
|
||||
appy.Object(group='Stop Words', name=" Don't remove stop words"),
|
||||
appy.Object(group='Word Splitter', name='Whitespace splitter')
|
||||
]
|
||||
def installCatalog(self):
|
||||
'''Create the catalog at the root of Zope if id does not exist.'''
|
||||
if 'catalog' not in self.app.objectIds():
|
||||
# Create the catalog
|
||||
from Products.ZCatalog.ZCatalog import manage_addZCatalog
|
||||
manage_addZCatalog(self.app, 'catalog', '')
|
||||
self.logger.info('Appy catalog created.')
|
||||
|
||||
# Create a lexicon for ZCTextIndexes
|
||||
if 'lexicon' not in self.app.catalog.objectIds():
|
||||
from Products.ZCTextIndex.ZCTextIndex import manage_addLexicon
|
||||
manage_addLexicon(self.app.catalog, 'lexicon',
|
||||
elements=self.lexiconInfos)
|
||||
|
||||
# Create or update Appy-wide indexes and field-related indexes
|
||||
indexInfo = {'State': 'FieldIndex', 'UID': 'FieldIndex',
|
||||
'Title': 'ZCTextIndex', 'SortableTitle': 'FieldIndex',
|
||||
'SearchableText': 'ZCTextIndex', 'Creator': 'FieldIndex',
|
||||
'Created': 'DateIndex', 'ClassName': 'FieldIndex',
|
||||
'Allowed': 'KeywordIndex'}
|
||||
tool = self.app.config
|
||||
for className in self.config.attributes.iterkeys():
|
||||
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
||||
for appyType in wrapperClass.__fields__:
|
||||
if not appyType.indexed or (appyType.name == 'title'): continue
|
||||
n = appyType.name
|
||||
indexName = 'get%s%s' % (n[0].upper(), n[1:])
|
||||
indexInfo[indexName] = appyType.getIndexType()
|
||||
self.installIndexes(indexInfo)
|
||||
|
||||
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 installBaseObjects(self):
|
||||
'''Creates the tool and the root data folder if they do not exist.'''
|
||||
# Create or update the base folder for storing data
|
||||
zopeContent = self.app.objectIds()
|
||||
|
||||
if 'data' not in zopeContent:
|
||||
self.app.manage_addFolder('data')
|
||||
data = self.app.data
|
||||
# Manager has been granted Add permissions for all root classes.
|
||||
# This may not be desired, so remove this.
|
||||
for className in self.config.rootClasses:
|
||||
permission = self.getAddPermission(className)
|
||||
data.manage_permission(permission, (), acquire=0)
|
||||
# All roles defined as creators should be able to create the
|
||||
# corresponding root classes in this folder.
|
||||
i = -1
|
||||
for klass in self.config.appClasses:
|
||||
i += 1
|
||||
if not klass.__dict__.has_key('root') or \
|
||||
not klass.__dict__['root']:
|
||||
continue # It is not a root class
|
||||
creators = getattr(klass, 'creators', None)
|
||||
if not creators: creators = self.config.defaultAddRoles
|
||||
className = self.config.appClassNames[i]
|
||||
permission = self.getAddPermission(className)
|
||||
updateRolesForPermission(permission, tuple(creators), data)
|
||||
|
||||
if 'config' not in zopeContent:
|
||||
toolName = '%sTool' % self.productName
|
||||
createObject(self.app, 'config', toolName,self.productName,wf=False)
|
||||
# Remove some default objects created by Zope but not useful to Appy
|
||||
for name in ('standard_html_footer', 'standard_html_header',\
|
||||
'standard_template.pt'):
|
||||
if name in zopeContent: self.app.manage_delObjects([name])
|
||||
|
||||
def installTool(self):
|
||||
'''Updates the tool (now that the catalog is created) and updates its
|
||||
inner objects (users, groups, translations, documents).'''
|
||||
tool = self.app.config
|
||||
tool.createOrUpdate(True, None)
|
||||
tool.refreshSecurity()
|
||||
appyTool = tool.appy()
|
||||
appyTool.log('Appy version is "%s".' % appy.version.short)
|
||||
|
||||
# Create the admin user if no user exists.
|
||||
if not self.app.acl_users.getUsers():
|
||||
self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ())
|
||||
appyTool.log('Admin user "admin" created.')
|
||||
|
||||
# Create group "admins" if it does not exist
|
||||
if not appyTool.count('Group', login='admins'):
|
||||
appyTool.create('groups', login='admins', title='Administrators',
|
||||
roles=['Manager'])
|
||||
appyTool.log('Group "admins" created.')
|
||||
|
||||
# Create a group for every global role defined in the application
|
||||
for role in self.config.applicationGlobalRoles:
|
||||
relatedGroup = '%s_group' % role
|
||||
if appyTool.count('Group', login=relatedGroup): continue
|
||||
appyTool.create('groups', login=relatedGroup, title=relatedGroup,
|
||||
roles=[role])
|
||||
appyTool.log('Group "%s", related to global role "%s", was ' \
|
||||
'created.' % (relatedGroup, role))
|
||||
|
||||
# Create POD templates within the tool if required
|
||||
for contentType in self.config.attributes.iterkeys():
|
||||
appyClass = tool.getAppyClass(contentType)
|
||||
if not appyClass: continue # May be an abstract class
|
||||
wrapperClass = tool.getAppyClass(contentType, wrapper=True)
|
||||
for appyType in wrapperClass.__fields__:
|
||||
if appyType.type != 'Pod': continue
|
||||
# Find the attribute that stores the template, and store on
|
||||
# it the default one specified in the appyType if no
|
||||
# template is stored yet.
|
||||
attrName = appyTool.getAttributeName('podTemplate', appyClass,
|
||||
appyType.name)
|
||||
fileObject = getattr(appyTool, attrName)
|
||||
if not fileObject or (fileObject.size == 0):
|
||||
# There is no file. Put the one specified in the appyType.
|
||||
fileName = os.path.join(appyTool.getDiskFolder(),
|
||||
appyType.template)
|
||||
if os.path.exists(fileName):
|
||||
setattr(appyTool, attrName, fileName)
|
||||
appyTool.log('Imported "%s" in the tool in ' \
|
||||
'attribute "%s"'% (fileName, attrName))
|
||||
else:
|
||||
appyTool.log('Template "%s" was not found!' % \
|
||||
fileName, type='error')
|
||||
|
||||
# Create or update Translation objects
|
||||
translations = [t.o.id for t in appyTool.translations]
|
||||
# We browse the languages supported by this application and check
|
||||
# whether we need to create the corresponding Translation objects.
|
||||
for language in self.languages:
|
||||
if language in translations: continue
|
||||
# We will create, in the tool, the translation object for this
|
||||
# language. Determine first its title.
|
||||
langId, langEn, langNat = languages.get(language)
|
||||
if langEn != langNat:
|
||||
title = '%s (%s)' % (langEn, langNat)
|
||||
else:
|
||||
title = langEn
|
||||
appyTool.create('translations', id=language, title=title)
|
||||
appyTool.log('Translation object created for "%s".' % language)
|
||||
# Now, we synchronise every Translation object with the corresponding
|
||||
# "po" file on disk.
|
||||
appFolder = self.config.diskFolder
|
||||
appName = self.config.PROJECTNAME
|
||||
dn = os.path.dirname
|
||||
jn = os.path.join
|
||||
i18nFolder = jn(jn(jn(dn(dn(dn(appFolder))),'Products'),appName),'i18n')
|
||||
for translation in appyTool.translations:
|
||||
# Get the "po" file
|
||||
poName = '%s-%s.po' % (appName, translation.id)
|
||||
poFile = PoParser(jn(i18nFolder, poName)).parse()
|
||||
for message in poFile.messages:
|
||||
setattr(translation, message.id, message.getMessage())
|
||||
appyTool.log('Translation "%s" updated from "%s".' % \
|
||||
(translation.id, poName))
|
||||
|
||||
# Execute custom installation code if any
|
||||
if hasattr(appyTool, 'install'):
|
||||
tool.executeAppyAction('install', reindex=False)
|
||||
|
||||
def configureSessions(self):
|
||||
'''Configure the session machinery.'''
|
||||
# Register a function warning us when a session object is deleted. When
|
||||
# launching Zope, the temp folder does not exist.
|
||||
if not hasattr(self.app, 'temp_folder'): return
|
||||
self.app.temp_folder.session_data.setDelNotificationTarget(onDelSession)
|
||||
|
||||
def enableUserTracking(self):
|
||||
'''Enables the machinery allowing to know who is currently logged in.
|
||||
Information about logged users will be stored in RAM, in the variable
|
||||
named loggedUsers defined above.'''
|
||||
global originalTraverse
|
||||
if not originalTraverse:
|
||||
# User tracking is not enabled yet. Do it now.
|
||||
BaseRequest = self.config.BaseRequest
|
||||
originalTraverse = BaseRequest.traverse
|
||||
BaseRequest.traverse = traverseWrapper
|
||||
|
||||
def installZopeClasses(self):
|
||||
'''Zope-level class registration.'''
|
||||
for klass in self.classes:
|
||||
name = klass.__name__
|
||||
module = klass.__module__
|
||||
wrapper = klass.wrapperClass
|
||||
exec 'from %s import manage_add%s as ctor' % (module, name)
|
||||
self.zopeContext.registerClass(meta_type=name,
|
||||
constructors = (ctor,),
|
||||
permission = self.addContentPermissions[name])
|
||||
# Create workflow prototypical instances in __instance__ attributes
|
||||
wf = getattr(klass.wrapperClass, 'workflow', None)
|
||||
if wf and not hasattr(wf, '__instance__'): wf.__instance__ = wf()
|
||||
|
||||
def installAppyTypes(self):
|
||||
'''We complete here the initialisation process of every Appy type of
|
||||
every gen-class of the application.'''
|
||||
appName = self.productName
|
||||
for klass in self.classes:
|
||||
# Store on wrapper class the ordered list of Appy types
|
||||
wrapperClass = klass.wrapperClass
|
||||
if not hasattr(wrapperClass, 'title'):
|
||||
# Special field "type" is mandatory for every class.
|
||||
title = String(multiplicity=(1,1), show='edit', indexed=True)
|
||||
title.init('title', None, 'appy')
|
||||
setattr(wrapperClass, 'title', title)
|
||||
names = self.config.attributes[wrapperClass.__name__[:-8]]
|
||||
wrapperClass.__fields__ = [getattr(wrapperClass, n) for n in names]
|
||||
# Post-initialise every Appy type
|
||||
for baseClass in klass.wrapperClass.__bases__:
|
||||
if baseClass.__name__ == 'AbstractWrapper': continue
|
||||
for name, appyType in baseClass.__dict__.iteritems():
|
||||
if not isinstance(appyType, Type) or \
|
||||
(isinstance(appyType, Ref) and appyType.isBack):
|
||||
continue # Back refs are initialised within fw refs
|
||||
appyType.init(name, baseClass, appName)
|
||||
|
||||
def installRoles(self):
|
||||
'''Installs the application-specific roles if not already done.'''
|
||||
roles = list(self.app.__ac_roles__)
|
||||
for role in self.config.applicationRoles:
|
||||
if role not in roles: roles.append(role)
|
||||
self.app.__ac_roles__ = tuple(roles)
|
||||
|
||||
def installDependencies(self):
|
||||
'''Zope products are installed in alphabetical order. But here, we need
|
||||
ZCTextIndex to be installed before our Appy application. So, we cheat
|
||||
and force Zope to install it now.'''
|
||||
from OFS.Application import install_product
|
||||
import Products
|
||||
install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {})
|
||||
|
||||
def install(self):
|
||||
self.logger.info('is being installed...')
|
||||
self.installDependencies()
|
||||
self.installRoles()
|
||||
self.installAppyTypes()
|
||||
self.installZopeClasses()
|
||||
self.enableUserTracking()
|
||||
self.configureSessions()
|
||||
self.installBaseObjects()
|
||||
self.installCatalog()
|
||||
self.installTool()
|
||||
self.installUi()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,63 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import time
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Migrator:
|
||||
'''This class is responsible for performing migrations, when, on
|
||||
installation, we've detected a new Appy version.'''
|
||||
def __init__(self, installer):
|
||||
self.installer = installer
|
||||
|
||||
def migrateTo_0_7_1(self):
|
||||
'''Appy 0.7.1 has its own management of Ref fields and does not use
|
||||
Archetypes references and the reference catalog anymore. So we must
|
||||
update data structures that store Ref info on instances.'''
|
||||
ins = self.installer
|
||||
ins.info('Migrating to Appy 0.7.1...')
|
||||
allClassNames = [ins.tool.__class__.__name__] + ins.config.allClassNames
|
||||
for className in allClassNames:
|
||||
i = -1
|
||||
updated = 0
|
||||
ins.info('Analysing class "%s"...' % className)
|
||||
refFields = None
|
||||
for obj in ins.tool.executeQuery(className,\
|
||||
noSecurity=True)['objects']:
|
||||
i += 1
|
||||
if i == 0:
|
||||
# Get the Ref fields for objects of this class
|
||||
refFields = [f for f in obj.getAllAppyTypes() \
|
||||
if (f.type == 'Ref') and not f.isBack]
|
||||
if refFields:
|
||||
refNames = ', '.join([rf.name for rf in refFields])
|
||||
ins.info(' Ref fields found: %s' % refNames)
|
||||
else:
|
||||
ins.info(' No Ref field found.')
|
||||
break
|
||||
isUpdated = False
|
||||
for field in refFields:
|
||||
# Attr for storing UIDs of referred objects has moved
|
||||
# from _appy_[fieldName] to [fieldName].
|
||||
refs = getattr(obj, '_appy_%s' % field.name)
|
||||
if refs:
|
||||
isUpdated = True
|
||||
setattr(obj, field.name, refs)
|
||||
exec 'del obj._appy_%s' % field.name
|
||||
# Set the back references
|
||||
for refObject in field.getValue(obj):
|
||||
refObject.link(field.back.name, obj, back=True)
|
||||
if isUpdated: updated += 1
|
||||
if updated:
|
||||
ins.info(' %d/%d object(s) updated.' % (updated, i+1))
|
||||
|
||||
def run(self):
|
||||
i = self.installer
|
||||
installedVersion = i.appyTool.appyVersion
|
||||
startTime = time.time()
|
||||
migrationRequired = False
|
||||
if not installedVersion or (installedVersion <= '0.7.0'):
|
||||
migrationRequired = True
|
||||
self.migrateTo_0_7_1()
|
||||
stopTime = time.time()
|
||||
if migrationRequired:
|
||||
i.info('Migration done in %d minute(s).'% ((stopTime-startTime)/60))
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,91 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TestMixin:
|
||||
'''This class is mixed in with any PloneTestCase.'''
|
||||
def createUser(self, userId, roles):
|
||||
'''Creates a user with id p_userId with some p_roles.'''
|
||||
self.acl_users.addMember(userId, 'password', [], [])
|
||||
self.setRoles(roles, name=userId)
|
||||
|
||||
def changeUser(self, userId):
|
||||
'''Logs out currently logged user and logs in p_loginName.'''
|
||||
self.logout()
|
||||
self.login(userId)
|
||||
|
||||
def getNonEmptySubModules(self, moduleName):
|
||||
'''Returns the list of sub-modules of p_app that are non-empty.'''
|
||||
res = []
|
||||
try:
|
||||
exec 'import %s' % moduleName
|
||||
exec 'moduleObj = %s' % moduleName
|
||||
moduleFile = moduleObj.__file__
|
||||
if moduleFile.endswith('.pyc'):
|
||||
moduleFile = moduleFile[:-1]
|
||||
except ImportError, ie:
|
||||
return res
|
||||
except SyntaxError, se:
|
||||
return res
|
||||
# Include the module if not empty. "Emptyness" is determined by the
|
||||
# absence of names beginning with other chars than "__".
|
||||
for elem in moduleObj.__dict__.iterkeys():
|
||||
if not elem.startswith('__'):
|
||||
res.append(moduleObj)
|
||||
break
|
||||
# Include sub-modules if any
|
||||
if moduleFile.find("__init__.py") != -1:
|
||||
# Potentially, sub-modules exist.
|
||||
moduleFolder = os.path.dirname(moduleFile)
|
||||
for elem in os.listdir(moduleFolder):
|
||||
if elem.startswith('.'): continue
|
||||
subModuleName, ext = os.path.splitext(elem)
|
||||
if ((ext == '.py') and (subModuleName != '__init__')) or \
|
||||
os.path.isdir(os.path.join(moduleFolder, subModuleName)):
|
||||
# Submodules may be sub-folders or Python files
|
||||
subModuleName = '%s.%s' % (moduleName, subModuleName)
|
||||
res += self.getNonEmptySubModules(subModuleName)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def getCovFolder():
|
||||
'''Returns the folder where to put the coverage folder if needed.'''
|
||||
for arg in sys.argv:
|
||||
if arg.startswith('[coverage'):
|
||||
return arg[10:].strip(']')
|
||||
return None
|
||||
|
||||
# Functions executed before and after every test -------------------------------
|
||||
def beforeTest(test):
|
||||
'''Is executed before every test.'''
|
||||
g = test.globs
|
||||
g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy()
|
||||
cfg = g['tool'].o.getProductConfig()
|
||||
g['appFolder'] = cfg.diskFolder
|
||||
moduleOrClassName = g['test'].name # Not used yet.
|
||||
# Initialize the test
|
||||
test.createUser('admin', ('Member','Manager'))
|
||||
test.login('admin')
|
||||
g['t'] = g['test']
|
||||
|
||||
def afterTest(test):
|
||||
'''Is executed after every test.'''
|
||||
g = test.globs
|
||||
appName = g['tool'].o.getAppName()
|
||||
exec 'from Products.%s import cov, covFolder, totalNumberOfTests, ' \
|
||||
'countTest' % appName
|
||||
countTest()
|
||||
exec 'from Products.%s import numberOfExecutedTests' % appName
|
||||
if cov and (numberOfExecutedTests == totalNumberOfTests):
|
||||
cov.stop()
|
||||
appModules = test.getNonEmptySubModules(appName)
|
||||
# Dumps the coverage report
|
||||
# HTML version
|
||||
cov.html_report(directory=covFolder, morfs=appModules)
|
||||
# Summary in a text file
|
||||
f = file('%s/summary.txt' % covFolder, 'w')
|
||||
cov.report(file=f, morfs=appModules)
|
||||
f.close()
|
||||
# Annotated modules
|
||||
cov.annotate(directory=covFolder, morfs=appModules)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,964 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import re, os, os.path, time, random, types, base64, urllib
|
||||
from appy.shared import mimeTypes
|
||||
from appy.shared.utils import getOsTempFolder
|
||||
from appy.shared.data import languages
|
||||
import appy.gen
|
||||
from appy.gen import Type, Search, Selection
|
||||
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName
|
||||
from appy.gen.plone25.mixins import BaseMixin
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
from appy.gen.plone25.descriptors import ClassDescriptor
|
||||
try:
|
||||
from AccessControl.ZopeSecurityPolicy import _noroles
|
||||
except ImportError:
|
||||
_noroles = []
|
||||
|
||||
# Errors -----------------------------------------------------------------------
|
||||
jsMessages = ('no_elem_selected', 'delete_confirm')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ToolMixin(BaseMixin):
|
||||
_appy_meta_type = 'Tool'
|
||||
def getPortalType(self, metaTypeOrAppyClass):
|
||||
'''Returns the name of the portal_type that is based on
|
||||
p_metaTypeOrAppyType.'''
|
||||
appName = self.getProductConfig().PROJECTNAME
|
||||
res = metaTypeOrAppyClass
|
||||
if not isinstance(metaTypeOrAppyClass, basestring):
|
||||
res = getClassName(metaTypeOrAppyClass, appName)
|
||||
if res.find('Extensions_appyWrappers') != -1:
|
||||
elems = res.split('_')
|
||||
res = '%s%s' % (elems[1], elems[4])
|
||||
if res in ('User', 'Group', 'Translation'): res = appName + res
|
||||
return res
|
||||
|
||||
def getCatalog(self):
|
||||
'''Returns the catalog object.'''
|
||||
return self.getParentNode().catalog
|
||||
|
||||
def getApp(self):
|
||||
'''Returns the root Zope object.'''
|
||||
return self.getPhysicalRoot()
|
||||
|
||||
def getSiteUrl(self):
|
||||
'''Returns the absolute URL of this site.'''
|
||||
return self.getApp().absolute_url()
|
||||
|
||||
def getPodInfo(self, obj, name):
|
||||
'''Gets the available POD formats for Pod field named p_name on
|
||||
p_obj.'''
|
||||
podField = self.getAppyType(name, className=obj.meta_type)
|
||||
return podField.getToolInfo(obj.appy())
|
||||
|
||||
def generateDocument(self):
|
||||
'''Generates the document from field-related info. UID of object that
|
||||
is the template target is given in the request.'''
|
||||
rq = self.REQUEST
|
||||
# Get the object on which a document must be generated.
|
||||
obj = self.getObject(rq.get('objectUid'), appy=True)
|
||||
fieldName = rq.get('fieldName')
|
||||
res = getattr(obj, fieldName)
|
||||
if isinstance(res, basestring):
|
||||
# An error has occurred, and p_res contains the error message
|
||||
obj.say(res)
|
||||
return self.goto(rq.get('HTTP_REFERER'))
|
||||
# res contains a FileWrapper instance.
|
||||
response = rq.RESPONSE
|
||||
response.setHeader('Content-Type', res.mimeType)
|
||||
response.setHeader('Content-Disposition',
|
||||
'inline;filename="%s"' % res.name)
|
||||
return res.content
|
||||
|
||||
def getAttr(self, name):
|
||||
'''Gets attribute named p_name.'''
|
||||
return getattr(self.appy(), name, None)
|
||||
|
||||
def getAppName(self):
|
||||
'''Returns the name of the application.'''
|
||||
return self.getProductConfig().PROJECTNAME
|
||||
|
||||
def getPath(self, path):
|
||||
'''Returns the folder or object whose absolute path p_path.'''
|
||||
res = self.getPhysicalRoot()
|
||||
if path == '/': return res
|
||||
path = path[1:]
|
||||
if '/' not in path: return res._getOb(path) # For performance
|
||||
for elem in path.split('/'): res = res._getOb(elem)
|
||||
return res
|
||||
|
||||
def getLanguages(self):
|
||||
'''Returns the supported languages. First one is the default.'''
|
||||
return self.getProductConfig().languages
|
||||
|
||||
def getLanguageName(self, code):
|
||||
'''Gets the language name (in this language) from a 2-chars language
|
||||
p_code.'''
|
||||
return languages.get(code)[2]
|
||||
|
||||
def getMessages(self):
|
||||
'''Returns the list of messages to return to the user.'''
|
||||
if hasattr(self.REQUEST, 'messages'):
|
||||
# Empty the messages and return it
|
||||
res = self.REQUEST.messages
|
||||
del self.REQUEST.messages
|
||||
else:
|
||||
res = []
|
||||
# Add portal_status_message key if present
|
||||
if 'portal_status_message' in self.REQUEST:
|
||||
res.append( ('info', self.REQUEST['portal_status_message']) )
|
||||
return res
|
||||
|
||||
def getRootClasses(self):
|
||||
'''Returns the list of root classes for this application.'''
|
||||
return self.getProductConfig().rootClasses
|
||||
|
||||
def _appy_getAllFields(self, contentType):
|
||||
'''Returns the (translated) names of fields of p_contentType.'''
|
||||
res = []
|
||||
for appyType in self.getAllAppyTypes(className=contentType):
|
||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
||||
# Add object state
|
||||
res.append(('state', self.translate('workflow_state')))
|
||||
return res
|
||||
|
||||
def _appy_getSearchableFields(self, contentType):
|
||||
'''Returns the (translated) names of fields that may be searched on
|
||||
objects of type p_contentType (=indexed fields).'''
|
||||
res = []
|
||||
for appyType in self.getAllAppyTypes(className=contentType):
|
||||
if appyType.indexed:
|
||||
res.append((appyType.name, self.translate(appyType.labelId)))
|
||||
return res
|
||||
|
||||
def getSearchInfo(self, contentType, refInfo=None):
|
||||
'''Returns, as a dict:
|
||||
- the list of searchable fields (= some fields among all indexed
|
||||
fields);
|
||||
- the number of columns for layouting those fields.'''
|
||||
fields = []
|
||||
fieldDicts = []
|
||||
if refInfo:
|
||||
# The search is triggered from a Ref field.
|
||||
refObject, fieldName = self.getRefInfo(refInfo)
|
||||
refField = refObject.getAppyType(fieldName)
|
||||
fieldNames = refField.queryFields or ()
|
||||
nbOfColumns = refField.queryNbCols
|
||||
else:
|
||||
# The search is triggered from an app-wide search.
|
||||
at = self.appy()
|
||||
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
|
||||
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
|
||||
for name in fieldNames:
|
||||
appyType = self.getAppyType(name,asDict=False,className=contentType)
|
||||
appyDict = self.getAppyType(name, asDict=True,className=contentType)
|
||||
fields.append(appyType)
|
||||
fieldDicts.append(appyDict)
|
||||
return {'fields': fields, 'nbOfColumns': nbOfColumns,
|
||||
'fieldDicts': fieldDicts}
|
||||
|
||||
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
|
||||
'filterKey', 'filterValue')
|
||||
def getQueryInfo(self):
|
||||
'''If we are showing search results, this method encodes in a string all
|
||||
the params in the request that are required for re-triggering the
|
||||
search.'''
|
||||
rq = self.REQUEST
|
||||
res = ''
|
||||
if rq.has_key('search'):
|
||||
res = ';'.join([rq.get(key,'').replace(';','') \
|
||||
for key in self.queryParamNames])
|
||||
return res
|
||||
|
||||
def getImportElements(self, contentType):
|
||||
'''Returns the list of elements that can be imported from p_path for
|
||||
p_contentType.'''
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
importParams = self.getCreateMeans(appyClass)['import']
|
||||
onElement = importParams['onElement'].__get__('')
|
||||
sortMethod = importParams['sort']
|
||||
if sortMethod: sortMethod = sortMethod.__get__('')
|
||||
elems = []
|
||||
importType = self.getAppyType('importPathFor%s' % contentType)
|
||||
importPath = importType.getValue(self)
|
||||
for elem in os.listdir(importPath):
|
||||
elemFullPath = os.path.join(importPath, elem)
|
||||
elemInfo = onElement(elemFullPath)
|
||||
if elemInfo:
|
||||
elemInfo.insert(0, elemFullPath) # To the result, I add the full
|
||||
# path of the elem, which will not be shown.
|
||||
elems.append(elemInfo)
|
||||
if sortMethod:
|
||||
elems = sortMethod(elems)
|
||||
return [importParams['headers'], elems]
|
||||
|
||||
def showPortlet(self, context):
|
||||
if self.userIsAnon(): return False
|
||||
if context.id == 'ui': context = context.getParentNode()
|
||||
res = True
|
||||
if not self.getRootClasses():
|
||||
res = False
|
||||
# If there is no root class, show the portlet only if we are within
|
||||
# the configuration.
|
||||
if (self.id in context.absolute_url()): res = True
|
||||
return res
|
||||
|
||||
def getObject(self, uid, appy=False, brain=False):
|
||||
'''Allows to retrieve an object from its p_uid.'''
|
||||
res = self.getPhysicalRoot().catalog(UID=uid)
|
||||
if not res: return
|
||||
res = res[0]
|
||||
if brain: return res
|
||||
res = res.getObject()
|
||||
if not appy: return res
|
||||
return res.appy()
|
||||
|
||||
def getAllowedValue(self):
|
||||
'''Gets, for the currently logged user, the value for index
|
||||
"Allowed".'''
|
||||
user = self.getUser()
|
||||
res = ['user:%s' % user.getId(), 'Anonymous'] + user.getRoles()
|
||||
try:
|
||||
res += ['user:%s' % g for g in user.groups.keys()]
|
||||
except AttributeError, ae:
|
||||
pass # The Zope admin does not have this attribute.
|
||||
return res
|
||||
|
||||
def executeQuery(self, className, searchName=None, startNumber=0,
|
||||
search=None, remember=False, brainsOnly=False,
|
||||
maxResults=None, noSecurity=False, sortBy=None,
|
||||
sortOrder='asc', filterKey=None, filterValue=None,
|
||||
refObject=None, refField=None):
|
||||
'''Executes a query on instances of a given p_className (or several,
|
||||
separated with commas) in the catalog. If p_searchName is specified,
|
||||
it corresponds to:
|
||||
1) a search defined on p_className: additional search criteria
|
||||
will be added to the query, or;
|
||||
2) "_advanced": in this case, additional search criteria will also
|
||||
be added to the query, but those criteria come from the session
|
||||
(in key "searchCriteria") and were created from search.pt.
|
||||
|
||||
We will retrieve objects from p_startNumber. If p_search is defined,
|
||||
it corresponds to a custom Search instance (instead of a predefined
|
||||
named search like in p_searchName). If both p_searchName and p_search
|
||||
are given, p_search is ignored.
|
||||
|
||||
This method returns a list of objects in the form of the
|
||||
__dict__ attribute of an instance of SomeObjects (see in
|
||||
appy.gen.utils). We return the __dict__ attribute instead of real
|
||||
instance: that way, it can be used in ZPTs without security problems.
|
||||
If p_brainsOnly is True, it returns a list of brains instead (can be
|
||||
useful for some usages like knowing the number of objects without
|
||||
needing to get information about them). If no p_maxResults is
|
||||
specified, the method returns maximum
|
||||
self.numberOfResultsPerPage. The method returns all objects if
|
||||
p_maxResults equals string "NO_LIMIT".
|
||||
|
||||
If p_noSecurity is True, it gets all the objects, even those that the
|
||||
currently logged user can't see.
|
||||
|
||||
The result is sorted according to the potential sort key defined in
|
||||
the Search instance (Search.sortBy). But if parameter p_sortBy is
|
||||
given, it defines or overrides the sort. In this case, p_sortOrder
|
||||
gives the order (*asc*ending or *desc*ending).
|
||||
|
||||
If p_filterKey is given, it represents an additional search parameter
|
||||
to take into account: the corresponding search value is in
|
||||
p_filterValue.
|
||||
|
||||
If p_refObject and p_refField are given, the query is limited to the
|
||||
objects that are referenced from p_refObject through p_refField.'''
|
||||
# Is there one or several content types ?
|
||||
if className.find(',') != -1:
|
||||
classNames = className.split(',')
|
||||
else:
|
||||
classNames = className
|
||||
params = {'ClassName': classNames}
|
||||
if not brainsOnly: params['batch'] = True
|
||||
# Manage additional criteria from a search when relevant
|
||||
if searchName:
|
||||
# In this case, className must contain a single content type.
|
||||
appyClass = self.getAppyClass(className)
|
||||
if searchName != '_advanced':
|
||||
search = ClassDescriptor.getSearch(appyClass, searchName)
|
||||
else:
|
||||
fields = self.REQUEST.SESSION['searchCriteria']
|
||||
search = Search('customSearch', **fields)
|
||||
if search:
|
||||
# Add additional search criteria
|
||||
for fieldName, fieldValue in search.fields.iteritems():
|
||||
# Management of searches restricted to objects linked through a
|
||||
# Ref field: not implemented yet.
|
||||
if fieldName == '_ref': continue
|
||||
# Make the correspondance between the name of the field and the
|
||||
# name of the corresponding index.
|
||||
attrName = Search.getIndexName(fieldName)
|
||||
# Express the field value in the way needed by the index
|
||||
params[attrName] = Search.getSearchValue(fieldName, fieldValue)
|
||||
# Add a sort order if specified
|
||||
sortKey = search.sortBy
|
||||
if sortKey:
|
||||
params['sort_on'] = Search.getIndexName(sortKey, usage='sort')
|
||||
# Determine or override sort if specified.
|
||||
if sortBy:
|
||||
params['sort_on'] = Search.getIndexName(sortBy, usage='sort')
|
||||
if sortOrder == 'desc': params['sort_order'] = 'reverse'
|
||||
else: params['sort_order'] = None
|
||||
# If defined, add the filter among search parameters.
|
||||
if filterKey:
|
||||
filterKey = Search.getIndexName(filterKey)
|
||||
filterValue = Search.getSearchValue(filterKey, filterValue)
|
||||
params[filterKey] = filterValue
|
||||
# TODO This value needs to be merged with an existing one if already
|
||||
# in params, or, in a first step, we should avoid to display the
|
||||
# corresponding filter widget on the screen.
|
||||
if refObject:
|
||||
refField = refObject.getAppyType(refField)
|
||||
params['UID'] = getattr(refObject, refField.name).data
|
||||
# Use index "Allowed" if noSecurity is False
|
||||
if not noSecurity: params['Allowed'] = self.getAllowedValue()
|
||||
brains = self.getPath("/catalog")(**params)
|
||||
if brainsOnly:
|
||||
# Return brains only.
|
||||
if not maxResults: return brains
|
||||
else: return brains[:maxResults]
|
||||
if not maxResults:
|
||||
if refField: maxResults = refField.maxPerPage
|
||||
else: maxResults = self.appy().numberOfResultsPerPage
|
||||
elif maxResults == 'NO_LIMIT': maxResults = None
|
||||
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
|
||||
res.brainsToObjects()
|
||||
# In some cases (p_remember=True), we need to keep some information
|
||||
# about the query results in the current user's session, allowing him
|
||||
# to navigate within elements without re-triggering the query every
|
||||
# time a page for an element is consulted.
|
||||
if remember:
|
||||
if not searchName:
|
||||
# It is the global search for all objects pf p_className
|
||||
searchName = className
|
||||
uids = {}
|
||||
i = -1
|
||||
for obj in res.objects:
|
||||
i += 1
|
||||
uids[startNumber+i] = obj.UID()
|
||||
self.REQUEST.SESSION['search_%s' % searchName] = uids
|
||||
return res.__dict__
|
||||
|
||||
def getResultColumnsNames(self, contentType, refInfo):
|
||||
contentTypes = contentType.strip(',').split(',')
|
||||
resSet = None # Temporary set for computing intersections.
|
||||
res = [] # Final, sorted result.
|
||||
fieldNames = None
|
||||
appyTool = self.appy()
|
||||
refField = None
|
||||
if refInfo[0]: refField = refInfo[0].getAppyType(refInfo[1])
|
||||
for cType in contentTypes:
|
||||
if refField:
|
||||
fieldNames = refField.shownInfo
|
||||
else:
|
||||
fieldNames = getattr(appyTool, '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 truncateValue(self, value, appyType):
|
||||
'''Truncates the p_value according to p_appyType width.'''
|
||||
maxWidth = appyType['width']
|
||||
if isinstance(value, str): value = value.decode('utf-8')
|
||||
if len(value) > maxWidth:
|
||||
return value[:maxWidth] + '...'
|
||||
return value
|
||||
|
||||
def truncateText(self, text, width=15):
|
||||
'''Truncates p_text to max p_width chars. If the text is longer than
|
||||
p_width, the truncated part is put in a "acronym" html tag.'''
|
||||
if isinstance(text, str): text = text.decode('utf-8')
|
||||
if len(text) <= width: return text
|
||||
return '<acronym title="%s">%s</acronym>' % (text, text[:width] + '...')
|
||||
|
||||
def getPublishedObject(self):
|
||||
'''Gets the currently published object, if its meta_class is among
|
||||
application classes.'''
|
||||
req = self.REQUEST
|
||||
# If we are querying object, there is no published object (the truth is:
|
||||
# the tool is the currently published object but we don't want to
|
||||
# consider it this way).
|
||||
if not req['ACTUAL_URL'].endswith('/ui/view'): return
|
||||
obj = self.REQUEST['PUBLISHED']
|
||||
parent = obj.getParentNode()
|
||||
if parent.id == 'ui': obj = parent.getParentNode()
|
||||
if obj.meta_type in self.getProductConfig().attributes: return obj
|
||||
|
||||
def getZopeClass(self, name):
|
||||
'''Returns the Zope class whose name is p_name.'''
|
||||
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
|
||||
return C
|
||||
|
||||
def getAppyClass(self, zopeName, wrapper=False):
|
||||
'''Gets the Appy class corresponding to the Zope class named p_name.
|
||||
If p_wrapper is True, it returns the Appy wrapper. Else, it returns
|
||||
the user-defined class.'''
|
||||
zopeClass = self.getZopeClass(zopeName)
|
||||
if wrapper: return zopeClass.wrapperClass
|
||||
else: return zopeClass.wrapperClass.__bases__[-1]
|
||||
|
||||
def getCreateMeans(self, contentTypeOrAppyClass):
|
||||
'''Gets the different ways objects of p_contentTypeOrAppyClass (which
|
||||
can be a Plone content type or a Appy class) can be created
|
||||
(via a web form, by importing external data, etc). Result is a
|
||||
dict whose keys are strings (ie "form", "import"...) and whose
|
||||
values are additional data bout the particular mean.'''
|
||||
pythonClass = contentTypeOrAppyClass
|
||||
if isinstance(contentTypeOrAppyClass, basestring):
|
||||
pythonClass = self.getAppyClass(pythonClass)
|
||||
res = {}
|
||||
if not pythonClass.__dict__.has_key('create'):
|
||||
res['form'] = None
|
||||
# No additional data for this means, which is the default one.
|
||||
else:
|
||||
means = pythonClass.create
|
||||
if means:
|
||||
if isinstance(means, basestring): res[means] = None
|
||||
elif isinstance(means, list) or isinstance(means, tuple):
|
||||
for mean in means:
|
||||
if isinstance(mean, basestring):
|
||||
res[mean] = None
|
||||
else:
|
||||
res[mean.id] = mean.__dict__
|
||||
else:
|
||||
res[means.id] = means.__dict__
|
||||
return res
|
||||
|
||||
def userMaySearch(self, rootClass):
|
||||
'''This method checks if the currently logged user can trigger searches
|
||||
on a given p_rootClass. This is done by calling method "maySearch"
|
||||
on the class. If no such method exists, we return True.'''
|
||||
# When editign a form, one should avoid annoying the user with this.
|
||||
url = self.REQUEST['ACTUAL_URL']
|
||||
if url.endswith('/edit') or url.endswith('/do'): return
|
||||
pythonClass = self.getAppyClass(rootClass)
|
||||
if 'maySearch' in pythonClass.__dict__:
|
||||
return pythonClass.maySearch(self.appy())
|
||||
return True
|
||||
|
||||
def onImportObjects(self):
|
||||
'''This method is called when the user wants to create objects from
|
||||
external data.'''
|
||||
rq = self.REQUEST
|
||||
appyClass = self.getAppyClass(rq.get('className'))
|
||||
importPaths = rq.get('importPath').split('|')
|
||||
appFolder = self.getPath('/data')
|
||||
for importPath in importPaths:
|
||||
if not importPath: continue
|
||||
objectId = os.path.basename(importPath)
|
||||
self.appy().create(appyClass, id=objectId, _data=importPath)
|
||||
self.say(self.translate('import_done'))
|
||||
return self.goto(rq['HTTP_REFERER'])
|
||||
|
||||
def isAlreadyImported(self, contentType, importPath):
|
||||
data = self.getPath('/data')
|
||||
objectId = os.path.basename(importPath)
|
||||
if hasattr(data.aq_base, objectId):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def isSortable(self, name, className, usage):
|
||||
'''Is field p_name defined on p_className sortable for p_usage purposes
|
||||
(p_usage can be "ref" or "search")?'''
|
||||
if (',' in className) or (name == 'state'): return False
|
||||
appyType = self.getAppyType(name, className=className)
|
||||
if appyType: return appyType.isSortable(usage=usage)
|
||||
|
||||
def _searchValueIsEmpty(self, key):
|
||||
'''Returns True if request value in key p_key can be considered as
|
||||
empty.'''
|
||||
rq = self.REQUEST.form
|
||||
if key.endswith('*int') or key.endswith('*float'):
|
||||
# We return True if "from" AND "to" values are empty.
|
||||
toKey = '%s_to' % key[2:key.find('*')]
|
||||
return not rq[key].strip() and not rq[toKey].strip()
|
||||
elif key.endswith('*date'):
|
||||
# We return True if "from" AND "to" values are empty. A value is
|
||||
# considered as not empty if at least the year is specified.
|
||||
toKey = '%s_to_year' % key[2:-5]
|
||||
return not rq[key] and not rq[toKey]
|
||||
else:
|
||||
return not rq[key]
|
||||
|
||||
def _getDateTime(self, year, month, day, setMin):
|
||||
'''Gets a valid DateTime instance from date information coming from the
|
||||
request as strings in p_year, p_month and p_day. Returns None if
|
||||
p_year is empty. If p_setMin is True, when some
|
||||
information is missing (month or day), we will replace it with the
|
||||
minimum value (=1). Else, we will replace it with the maximum value
|
||||
(=12, =31).'''
|
||||
if not year: return None
|
||||
if not month:
|
||||
if setMin: month = 1
|
||||
else: month = 12
|
||||
if not day:
|
||||
if setMin: day = 1
|
||||
else: day = 31
|
||||
DateTime = self.getProductConfig().DateTime
|
||||
# Set the hour
|
||||
if setMin: hour = '00:00'
|
||||
else: hour = '23:59'
|
||||
# We loop until we find a valid date. For example, we could loop from
|
||||
# 2009/02/31 to 2009/02/28.
|
||||
dateIsWrong = True
|
||||
while dateIsWrong:
|
||||
try:
|
||||
res = DateTime('%s/%s/%s %s' % (year, month, day, hour))
|
||||
dateIsWrong = False
|
||||
except:
|
||||
day = int(day)-1
|
||||
return res
|
||||
|
||||
transformMethods = {'uppercase': 'upper', 'lowercase': 'lower',
|
||||
'capitalize': 'capitalize'}
|
||||
def onSearchObjects(self):
|
||||
'''This method is called when the user triggers a search from
|
||||
search.pt.'''
|
||||
rq = self.REQUEST
|
||||
# Store the search criteria in the session
|
||||
criteria = {}
|
||||
for attrName in rq.form.keys():
|
||||
if attrName.startswith('w_') and \
|
||||
not self._searchValueIsEmpty(attrName):
|
||||
# We have a(n interval of) value(s) that is not empty for a
|
||||
# given field.
|
||||
attrValue = rq.form[attrName]
|
||||
if attrName.find('*') != -1:
|
||||
attrValue = attrValue.strip()
|
||||
# The type of the value is encoded after char "*".
|
||||
attrName, attrType = attrName.split('*')
|
||||
if attrType == 'bool':
|
||||
exec 'attrValue = %s' % attrValue
|
||||
elif attrType in ('int', 'float'):
|
||||
# Get the "from" value
|
||||
if not attrValue: attrValue = None
|
||||
else:
|
||||
exec 'attrValue = %s(attrValue)' % attrType
|
||||
# Get the "to" value
|
||||
toValue = rq.form['%s_to' % attrName[2:]].strip()
|
||||
if not toValue: toValue = None
|
||||
else:
|
||||
exec 'toValue = %s(toValue)' % attrType
|
||||
attrValue = (attrValue, toValue)
|
||||
elif attrType == 'date':
|
||||
prefix = attrName[2:]
|
||||
# Get the "from" value
|
||||
year = attrValue
|
||||
month = rq.form['%s_from_month' % prefix]
|
||||
day = rq.form['%s_from_day' % prefix]
|
||||
fromDate = self._getDateTime(year, month, day, True)
|
||||
# Get the "to" value"
|
||||
year = rq.form['%s_to_year' % prefix]
|
||||
month = rq.form['%s_to_month' % prefix]
|
||||
day = rq.form['%s_to_day' % prefix]
|
||||
toDate = self._getDateTime(year, month, day, False)
|
||||
attrValue = (fromDate, toDate)
|
||||
elif attrType.startswith('string'):
|
||||
# In the case of a string, it could be necessary to
|
||||
# apply some text transform.
|
||||
if len(attrType) > 6:
|
||||
transform = attrType.split('-')[1]
|
||||
if (transform != 'none') and attrValue:
|
||||
exec 'attrValue = attrValue.%s()' % \
|
||||
self.transformMethods[transform]
|
||||
if isinstance(attrValue, list):
|
||||
# It is a list of values. Check if we have an operator for
|
||||
# the field, to see if we make an "and" or "or" for all
|
||||
# those values. "or" will be the default.
|
||||
operKey = 'o_%s' % attrName[2:]
|
||||
oper = ' %s ' % rq.form.get(operKey, 'or').upper()
|
||||
attrValue = oper.join(attrValue)
|
||||
criteria[attrName[2:]] = attrValue
|
||||
# Complete criteria with Ref info if the search is restricted to
|
||||
# referenced objects of a Ref field.
|
||||
refInfo = rq.get('ref', None)
|
||||
if refInfo: criteria['_ref'] = refInfo
|
||||
rq.SESSION['searchCriteria'] = criteria
|
||||
# Go to the screen that displays search results
|
||||
backUrl = '%s/ui/query?className=%s&&search=_advanced' % \
|
||||
(self.absolute_url(), rq['className'])
|
||||
return self.goto(backUrl)
|
||||
|
||||
def getJavascriptMessages(self):
|
||||
'''Returns the translated version of messages that must be shown in
|
||||
Javascript popups.'''
|
||||
res = ''
|
||||
for msg in jsMessages:
|
||||
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
|
||||
return res
|
||||
|
||||
def getRefInfo(self, refInfo=None):
|
||||
'''When a search is restricted to objects referenced through a Ref
|
||||
field, this method returns information about this reference: the
|
||||
source content type and the Ref field (Appy type). If p_refInfo is
|
||||
not given, we search it among search criteria in the session.'''
|
||||
if not refInfo and (self.REQUEST.get('search', None) == '_advanced'):
|
||||
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
|
||||
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
|
||||
if not refInfo: return (None, None)
|
||||
objectUid, fieldName = refInfo.split(':')
|
||||
obj = self.getObject(objectUid)
|
||||
return obj, fieldName
|
||||
|
||||
def getSearches(self, contentType):
|
||||
'''Returns the list of searches that are defined for p_contentType.
|
||||
Every list item is a dict that contains info about a search or about
|
||||
a group of searches.'''
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
res = []
|
||||
visitedGroups = {} # Names of already visited search groups
|
||||
for search in ClassDescriptor.getSearches(appyClass):
|
||||
# Determine first group label, we will need it.
|
||||
groupLabel = ''
|
||||
if search.group:
|
||||
groupLabel = '%s_searchgroup_%s' % (contentType, search.group)
|
||||
# Add an item representing the search group if relevant
|
||||
if search.group and (search.group not in visitedGroups):
|
||||
group = {'name': search.group, 'isGroup': True,
|
||||
'labelId': groupLabel, 'searches': [],
|
||||
'label': self.translate(groupLabel),
|
||||
'descr': self.translate('%s_descr' % groupLabel),
|
||||
}
|
||||
res.append(group)
|
||||
visitedGroups[search.group] = group
|
||||
# Add the search itself
|
||||
searchLabel = '%s_search_%s' % (contentType, search.name)
|
||||
dSearch = {'name': search.name, 'isGroup': False,
|
||||
'label': self.translate(searchLabel),
|
||||
'descr': self.translate('%s_descr' % searchLabel)}
|
||||
if search.group:
|
||||
visitedGroups[search.group]['searches'].append(dSearch)
|
||||
else:
|
||||
res.append(dSearch)
|
||||
return res
|
||||
|
||||
def getQueryUrl(self, contentType, searchName, startNumber=None):
|
||||
'''This method creates the URL that allows to perform a (non-Ajax)
|
||||
request for getting queried objects from a search named p_searchName
|
||||
on p_contentType.'''
|
||||
baseUrl = self.absolute_url() + '/ui'
|
||||
baseParams = 'className=%s' % contentType
|
||||
rq = self.REQUEST
|
||||
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
|
||||
# Manage start number
|
||||
if startNumber != None:
|
||||
baseParams += '&startNumber=%s' % startNumber
|
||||
elif rq.has_key('startNumber'):
|
||||
baseParams += '&startNumber=%s' % rq['startNumber']
|
||||
# Manage search name
|
||||
if searchName: baseParams += '&search=%s' % searchName
|
||||
return '%s/query?%s' % (baseUrl, baseParams)
|
||||
|
||||
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
|
||||
'''Returns the number (start at 0) of the first element in a list
|
||||
containing p_currentNumber (starts at 0) whose total number is
|
||||
p_totalNumber and whose batch size is p_batchSize.'''
|
||||
startNumber = 0
|
||||
res = startNumber
|
||||
while (startNumber < totalNumber):
|
||||
if (currentNumber < startNumber + batchSize):
|
||||
return startNumber
|
||||
else:
|
||||
startNumber += batchSize
|
||||
return startNumber
|
||||
|
||||
def getNavigationInfo(self):
|
||||
'''Extracts navigation information from request/nav and returns a dict
|
||||
with the info that a page can use for displaying object
|
||||
navigation.'''
|
||||
res = {}
|
||||
t,d1,d2,currentNumber,totalNumber = self.REQUEST.get('nav').split('.')
|
||||
res['currentNumber'] = int(currentNumber)
|
||||
res['totalNumber'] = int(totalNumber)
|
||||
# Compute the label of the search, or ref field
|
||||
if t == 'search':
|
||||
searchName = d2
|
||||
if not searchName:
|
||||
# We search all objects of a given type.
|
||||
label = '%s_plural' % d1.split(':')[0]
|
||||
elif searchName == '_advanced':
|
||||
# This is an advanced, custom search.
|
||||
label = 'search_results'
|
||||
else:
|
||||
# This is a named, predefined search.
|
||||
label = '%s_search_%s' % (d1.split(':')[0], searchName)
|
||||
res['backText'] = self.translate(label)
|
||||
else:
|
||||
fieldName, pageName = d2.split(':')
|
||||
sourceObj = self.getObject(d1)
|
||||
label = '%s_%s' % (sourceObj.meta_type, fieldName)
|
||||
res['backText'] = '%s : %s' % (sourceObj.Title(),
|
||||
self.translate(label))
|
||||
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
|
||||
# Among, first, previous, next and last, which one do I need?
|
||||
previousNeeded = False # Previous ?
|
||||
previousIndex = res['currentNumber'] - 2
|
||||
if (previousIndex > -1) and (res['totalNumber'] > previousIndex):
|
||||
previousNeeded = True
|
||||
nextNeeded = False # Next ?
|
||||
nextIndex = res['currentNumber']
|
||||
if nextIndex < res['totalNumber']: nextNeeded = True
|
||||
firstNeeded = False # First ?
|
||||
firstIndex = 0
|
||||
if previousIndex > 0: firstNeeded = True
|
||||
lastNeeded = False # Last ?
|
||||
lastIndex = res['totalNumber'] - 1
|
||||
if (nextIndex < lastIndex): lastNeeded = True
|
||||
# Get the list of available UIDs surrounding the current object
|
||||
if t == 'ref': # Manage navigation from a reference
|
||||
# In the case of a reference, we retrieve ALL surrounding objects.
|
||||
masterObj = self.getObject(d1)
|
||||
batchSize = masterObj.getAppyType(fieldName).maxPerPage
|
||||
uids = getattr(masterObj, fieldName)
|
||||
# Display the reference widget at the page where the current object
|
||||
# lies.
|
||||
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
|
||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
||||
res['totalNumber'], batchSize)
|
||||
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
|
||||
'page':pageName, 'nav':''})
|
||||
else: # Manage navigation from a search
|
||||
contentType = d1
|
||||
searchName = keySuffix = d2
|
||||
batchSize = self.appy().numberOfResultsPerPage
|
||||
if not searchName: keySuffix = contentType
|
||||
s = self.REQUEST.SESSION
|
||||
searchKey = 'search_%s' % keySuffix
|
||||
if s.has_key(searchKey): uids = s[searchKey]
|
||||
else: uids = {}
|
||||
# In the case of a search, we retrieve only a part of all
|
||||
# surrounding objects, those that are stored in the session.
|
||||
if (previousNeeded and not uids.has_key(previousIndex)) or \
|
||||
(nextNeeded and not uids.has_key(nextIndex)):
|
||||
# I do not have this UID in session. I will need to
|
||||
# retrigger the query by querying all objects surrounding
|
||||
# this one.
|
||||
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
|
||||
if newStartNumber < 0: newStartNumber = 0
|
||||
self.executeQuery(contentType, searchName=searchName,
|
||||
startNumber=newStartNumber, remember=True)
|
||||
uids = s[searchKey]
|
||||
# For the moment, for first and last, we get them only if we have
|
||||
# them in session.
|
||||
if not uids.has_key(0): firstNeeded = False
|
||||
if not uids.has_key(lastIndex): lastNeeded = False
|
||||
# Compute URL of source object
|
||||
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
|
||||
res['totalNumber'], batchSize)
|
||||
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
|
||||
startNumber=startNumber)
|
||||
# Compute URLs
|
||||
for urlType in ('previous', 'next', 'first', 'last'):
|
||||
exec 'needIt = %sNeeded' % urlType
|
||||
urlKey = '%sUrl' % urlType
|
||||
res[urlKey] = None
|
||||
if needIt:
|
||||
exec 'index = %sIndex' % urlType
|
||||
uid = None
|
||||
try:
|
||||
uid = uids[index]
|
||||
# uids can be a list (ref) or a dict (search)
|
||||
except KeyError: pass
|
||||
except IndexError: pass
|
||||
if uid:
|
||||
brain = self.getObject(uid, brain=True)
|
||||
if brain:
|
||||
sibling = brain.getObject()
|
||||
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
|
||||
page='main')
|
||||
return res
|
||||
|
||||
def tabularize(self, data, numberOfRows):
|
||||
'''This method transforms p_data, which must be a "flat" list or tuple,
|
||||
into a list of lists, where every sub-list has length p_numberOfRows.
|
||||
This method is typically used for rendering elements in a table of
|
||||
p_numberOfRows rows.'''
|
||||
res = []
|
||||
row = []
|
||||
for elem in data:
|
||||
row.append(elem)
|
||||
if len(row) == numberOfRows:
|
||||
res.append(row)
|
||||
row = []
|
||||
# Complete the last unfinished line if required.
|
||||
if row:
|
||||
while len(row) < numberOfRows: row.append(None)
|
||||
res.append(row)
|
||||
return res
|
||||
|
||||
def truncate(self, value, numberOfChars):
|
||||
'''Truncates string p_value to p_numberOfChars.'''
|
||||
if len(value) > numberOfChars: return value[:numberOfChars] + '...'
|
||||
return value
|
||||
|
||||
monthsIds = {
|
||||
1: 'month_jan', 2: 'month_feb', 3: 'month_mar', 4: 'month_apr',
|
||||
5: 'month_may', 6: 'month_jun', 7: 'month_jul', 8: 'month_aug',
|
||||
9: 'month_sep', 10: 'month_oct', 11: 'month_nov', 12: 'month_dec'}
|
||||
def getMonthName(self, monthNumber):
|
||||
'''Gets the translated month name of month numbered p_monthNumber.'''
|
||||
return self.translate(self.monthsIds[int(monthNumber)], domain='plone')
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Authentication-related methods
|
||||
# --------------------------------------------------------------------------
|
||||
def performLogin(self):
|
||||
'''Logs the user in.'''
|
||||
rq = self.REQUEST
|
||||
jsEnabled = rq.get('js_enabled', False) in ('1', 1)
|
||||
cookiesEnabled = rq.get('cookies_enabled', False) in ('1', 1)
|
||||
urlBack = rq['HTTP_REFERER']
|
||||
|
||||
if jsEnabled and not cookiesEnabled:
|
||||
msg = self.translate(u'You must enable cookies before you can ' \
|
||||
'log in.', domain='plone')
|
||||
return self.goto(urlBack, msg.encode('utf-8'))
|
||||
# Perform the Zope-level authentication
|
||||
login = rq.get('__ac_name', '')
|
||||
password = rq.get('__ac_password', '')
|
||||
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
||||
cookieValue = urllib.quote(cookieValue)
|
||||
rq.RESPONSE.setCookie('__ac', cookieValue, path='/')
|
||||
user = self.acl_users.validate(rq)
|
||||
if self.userIsAnon():
|
||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
||||
msg = self.translate(u'Login failed', domain='plone')
|
||||
logMsg = 'Authentication failed (tried with login "%s")' % login
|
||||
else:
|
||||
msg = self.translate(u'Welcome! You are now logged in.',
|
||||
domain='plone')
|
||||
logMsg = 'User "%s" has been logged in.' % login
|
||||
msg = msg.encode('utf-8')
|
||||
self.log(logMsg)
|
||||
# Bring Managers to the config, leave others on the main page.
|
||||
user = self.getUser()
|
||||
if user.has_role('Manager'):
|
||||
# Bring the user to the configuration
|
||||
url = self.goto(self.absolute_url(), msg)
|
||||
else:
|
||||
url = self.goto(rq['HTTP_REFERER'], msg)
|
||||
return url
|
||||
|
||||
def performLogout(self):
|
||||
'''Logs out the current user when he clicks on "disconnect".'''
|
||||
rq = self.REQUEST
|
||||
userId = self.getUser().getId()
|
||||
# Perform the logout in acl_users
|
||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
||||
# Invalidate existing sessions.
|
||||
sdm = self.session_data_manager
|
||||
session = sdm.getSessionData(create=0)
|
||||
if session is not None:
|
||||
session.invalidate()
|
||||
self.log('User "%s" has been logged out.' % userId)
|
||||
# Remove user from variable "loggedUsers"
|
||||
from appy.gen.plone25.installer import loggedUsers
|
||||
if loggedUsers.has_key(userId): del loggedUsers[userId]
|
||||
return self.goto(self.getApp().absolute_url())
|
||||
|
||||
def validate(self, request, auth='', roles=_noroles):
|
||||
'''This method performs authentication and authorization. It is used as
|
||||
a replacement for Zope's AccessControl.User.BasicUserFolder.validate,
|
||||
that allows to manage cookie-based authentication.'''
|
||||
v = request['PUBLISHED'] # The published object
|
||||
# v is the object (value) we're validating access to
|
||||
# n is the name used to access the object
|
||||
# a is the object the object was accessed through
|
||||
# c is the physical container of the object
|
||||
a, c, n, v = self._getobcontext(v, request)
|
||||
# Try to get user name and password from basic authentication
|
||||
login, password = self.identify(auth)
|
||||
if not login:
|
||||
# Try to get them from a cookie
|
||||
cookie = request.get('__ac', None)
|
||||
login = request.get('__ac_name', None)
|
||||
if login and request.form.has_key('__ac_password'):
|
||||
# The user just entered his credentials. The cookie has not been
|
||||
# set yet (it will come in the upcoming HTTP response when the
|
||||
# current request will be served).
|
||||
login = request.get('__ac_name', '')
|
||||
password = request.get('__ac_password', '')
|
||||
elif cookie and (cookie != 'deleted'):
|
||||
cookieValue = base64.decodestring(urllib.unquote(cookie))
|
||||
login, password = cookieValue.split(':')
|
||||
# Try to authenticate this user
|
||||
user = self.authenticate(login, password, request)
|
||||
emergency = self._emergency_user
|
||||
if emergency and user is emergency:
|
||||
# It is the emergency user.
|
||||
return emergency.__of__(self)
|
||||
elif user is None:
|
||||
# Login and/or password incorrect. Try to authorize and return the
|
||||
# anonymous user.
|
||||
if self.authorize(self._nobody, a, c, n, v, roles):
|
||||
return self._nobody.__of__(self)
|
||||
else:
|
||||
return # Anonymous can't acces this object
|
||||
else:
|
||||
# We found a user and his password was correct. Try to authorize him
|
||||
# against the published object.
|
||||
if self.authorize(user, a, c, n, v, roles):
|
||||
return user.__of__(self)
|
||||
# That didn't work. Try to authorize the anonymous user.
|
||||
elif self.authorize(self._nobody, a, c, n, v, roles):
|
||||
return self._nobody.__of__(self)
|
||||
else:
|
||||
return
|
||||
|
||||
# Patch BasicUserFolder with our version of m_validate above.
|
||||
from AccessControl.User import BasicUserFolder
|
||||
BasicUserFolder.validate = validate
|
||||
|
||||
def tempFile(self):
|
||||
'''A temp file has been created in a temp folder. This method returns
|
||||
this file to the browser.'''
|
||||
rq = self.REQUEST
|
||||
baseFolder = os.path.join(getOsTempFolder(), self.getAppName())
|
||||
baseFolder = os.path.join(baseFolder, rq.SESSION.id)
|
||||
fileName = os.path.join(baseFolder, rq.get('name', ''))
|
||||
if os.path.exists(fileName):
|
||||
f = file(fileName)
|
||||
content = f.read()
|
||||
f.close()
|
||||
# Remove the temp file
|
||||
os.remove(fileName)
|
||||
return content
|
||||
return 'File does not exist'
|
||||
|
||||
def getResultPodFields(self, contentType):
|
||||
'''Finds, among fields defined on p_contentType, which ones are Pod
|
||||
fields that need to be shown on a page displaying query results.'''
|
||||
# Skip this if we are searching multiple content types.
|
||||
if ',' in contentType: return ()
|
||||
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
|
||||
if (f.type == 'Pod') and (f.show == 'result')]
|
||||
|
||||
def getUserLine(self, user):
|
||||
'''Returns a one-line user info as shown on every page.'''
|
||||
res = [user.getId()]
|
||||
rolesToShow = [r for r in user.getRoles() \
|
||||
if r not in ('Authenticated', 'Member')]
|
||||
if rolesToShow:
|
||||
res.append(', '.join([self.translate(r) for r in rolesToShow]))
|
||||
return ' | '.join(res)
|
||||
|
||||
def generateUid(self, className):
|
||||
'''Generates a UID for an instance of p_className.'''
|
||||
name = className.replace('_', '')
|
||||
randomNumber = str(random.random()).split('.')[1]
|
||||
timestamp = ('%f' % time.time()).replace('.', '')
|
||||
return '%s%s%s' % (name, timestamp, randomNumber)
|
||||
# ------------------------------------------------------------------------------
|
File diff suppressed because it is too large
Load diff
|
@ -1,233 +0,0 @@
|
|||
'''This file contains basic classes that will be added into any user
|
||||
application for creating the basic structure of the application "Tool" which
|
||||
is the set of web pages used for configuring the application. 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 types
|
||||
from appy.gen import *
|
||||
Grp=Group # Avoid name clash between appy.gen.Group and class Group below
|
||||
|
||||
# Prototypical instances of every type -----------------------------------------
|
||||
class Protos:
|
||||
protos = {}
|
||||
# List of attributes that can't be given to a Type constructor
|
||||
notInit = ('id', 'type', 'pythonType', 'slaves', 'isSelect', 'hasLabel',
|
||||
'hasDescr', 'hasHelp', 'required', 'filterable', 'validable',
|
||||
'backd', 'isBack', 'sync', 'pageName', 'shownInfoWidths',
|
||||
'masterName')
|
||||
@classmethod
|
||||
def get(self, appyType):
|
||||
'''Returns a prototype instance for p_appyType.'''
|
||||
className = appyType.__class__.__name__
|
||||
isString = (className == 'String')
|
||||
if isString:
|
||||
# For Strings, we create one prototype per format, because default
|
||||
# values may change according to format.
|
||||
className += str(appyType.format)
|
||||
if className in self.protos: return self.protos[className]
|
||||
# The prototype does not exist yet: create it
|
||||
if isString:
|
||||
proto = appyType.__class__(format=appyType.format)
|
||||
# Now, we fake to be able to detect default values
|
||||
proto.format = 0
|
||||
else:
|
||||
proto = appyType.__class__()
|
||||
self.protos[className] = proto
|
||||
return proto
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ModelClass:
|
||||
'''This class is the abstract class of all predefined application classes
|
||||
used in the Appy model: Tool, User, etc. All methods and attributes of
|
||||
those classes are part of the Appy machinery and are prefixed with _appy_
|
||||
in order to avoid name conflicts with user-defined parts of the
|
||||
application model.'''
|
||||
_appy_attributes = [] # We need to keep track of attributes order.
|
||||
|
||||
@classmethod
|
||||
def _appy_getTypeBody(klass, appyType, wrapperName):
|
||||
'''This method returns the code declaration for p_appyType.'''
|
||||
typeArgs = ''
|
||||
proto = Protos.get(appyType)
|
||||
for name, value in appyType.__dict__.iteritems():
|
||||
# Some attrs can't be given to the constructor
|
||||
if name in Protos.notInit: continue
|
||||
# If the given value corresponds to the default value, don't give it
|
||||
if value == getattr(proto, name): continue
|
||||
if name == 'layouts':
|
||||
# For Tool attributes we do not copy layout info. Indeed, most
|
||||
# fields added to the Tool are config-related attributes whose
|
||||
# layouts must be standard.
|
||||
if klass.__name__ == 'Tool': continue
|
||||
layouts = appyType.getInputLayouts()
|
||||
# For the Translation class that has potentially thousands of
|
||||
# attributes, the most used layout is cached in a global var in
|
||||
# named "tfw" in appyWrappers.py.
|
||||
if (klass.__name__ == 'Translation') and \
|
||||
(layouts == '{"edit":"f","cell":"f","view":"f",}'):
|
||||
value = 'tfw'
|
||||
else:
|
||||
value = appyType.getInputLayouts()
|
||||
elif isinstance(value, basestring):
|
||||
value = '"%s"' % value
|
||||
elif isinstance(value, Ref):
|
||||
if not value.isBack: continue
|
||||
value = klass._appy_getTypeBody(value, wrapperName)
|
||||
elif type(value) == type(ModelClass):
|
||||
moduleName = value.__module__
|
||||
if moduleName.startswith('appy.gen'):
|
||||
value = value.__name__
|
||||
else:
|
||||
value = '%s.%s' % (moduleName, value.__name__)
|
||||
elif isinstance(value, Selection):
|
||||
value = 'Selection("%s")' % value.methodName
|
||||
elif isinstance(value, Grp):
|
||||
value = 'Grp("%s")' % value.name
|
||||
elif isinstance(value, Page):
|
||||
value = 'pages["%s"]' % value.name
|
||||
elif callable(value):
|
||||
value = '%s.%s' % (wrapperName, value.__name__)
|
||||
typeArgs += '%s=%s,' % (name, value)
|
||||
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
|
||||
|
||||
@classmethod
|
||||
def _appy_getBody(klass):
|
||||
'''This method returns the code declaration of this class. We will dump
|
||||
this in appyWrappers.py in the resulting product.'''
|
||||
className = klass.__name__
|
||||
# Determine the name of the class and its wrapper. Because so much
|
||||
# attributes can be generated on a TranslationWrapper, shortcutting it
|
||||
# to 'TW' may reduce the generated file from several kilobytes.
|
||||
if className == 'Translation': wrapperName = 'WT'
|
||||
else: wrapperName = 'W%s' % className
|
||||
res = 'class %s(%s):\n' % (className, wrapperName)
|
||||
# Tool must be folderish
|
||||
if className == 'Tool': res += ' folder=True\n'
|
||||
# First, scan all attributes, determine all used pages and create a
|
||||
# dict with it. It will prevent us from creating a new Page instance
|
||||
# for every field.
|
||||
pages = {}
|
||||
layouts = []
|
||||
for name in klass._appy_attributes:
|
||||
exec 'appyType = klass.%s' % name
|
||||
if appyType.page.name not in pages:
|
||||
pages[appyType.page.name] = appyType.page
|
||||
res += ' pages = {'
|
||||
for page in pages.itervalues():
|
||||
# Determine page show
|
||||
pageShow = page.show
|
||||
if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow
|
||||
res += '"%s":Page("%s", show=%s),'% (page.name, page.name, pageShow)
|
||||
res += '}\n'
|
||||
# Secondly, dump every attribute
|
||||
for name in klass._appy_attributes:
|
||||
exec 'appyType = klass.%s' % name
|
||||
typeBody = klass._appy_getTypeBody(appyType, wrapperName)
|
||||
res += ' %s=%s\n' % (name, typeBody)
|
||||
return res
|
||||
|
||||
# The User class ---------------------------------------------------------------
|
||||
class User(ModelClass):
|
||||
# In a ModelClass we need to declare attributes in the following list.
|
||||
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
|
||||
'password2', 'roles']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
title = String(show=False, indexed=True)
|
||||
gm = {'group': 'main', 'multiplicity': (1,1), 'width': 25}
|
||||
name = String(**gm)
|
||||
firstName = String(**gm)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
login = String(show=showLogin, validator=validateLogin, indexed=True, **gm)
|
||||
def showPassword(self): pass
|
||||
def validatePassword(self): pass
|
||||
password1 = String(format=String.PASSWORD, show=showPassword,
|
||||
validator=validatePassword, **gm)
|
||||
password2 = String(format=String.PASSWORD, show=showPassword, **gm)
|
||||
gm['multiplicity'] = (0, None)
|
||||
roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm)
|
||||
|
||||
# The Group class --------------------------------------------------------------
|
||||
class Group(ModelClass):
|
||||
# In a ModelClass we need to declare attributes in the following list.
|
||||
_appy_attributes = ['title', 'login', 'roles', 'users']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
m = {'group': 'main', 'width': 25, 'indexed': True}
|
||||
title = String(multiplicity=(1,1), **m)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
login = String(show=showLogin, validator=validateLogin,
|
||||
multiplicity=(1,1), **m)
|
||||
roles = String(validator=Selection('getGrantableRoles'),
|
||||
multiplicity=(0,None), **m)
|
||||
users = Ref(User, multiplicity=(0,None), add=False, link=True,
|
||||
back=Ref(attribute='groups', show=True),
|
||||
showHeaders=True, shownInfo=('title', 'login'))
|
||||
|
||||
# The Translation class --------------------------------------------------------
|
||||
class Translation(ModelClass):
|
||||
_appy_attributes = ['po', 'title']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
def getPoFile(self): pass
|
||||
po = Action(action=getPoFile, page=Page('actions', show='view'),
|
||||
result='filetmp')
|
||||
title = String(show=False, indexed=True)
|
||||
def label(self): pass
|
||||
def show(self, name): pass
|
||||
|
||||
# The Tool class ---------------------------------------------------------------
|
||||
# Here are the prefixes of the fields generated on the Tool.
|
||||
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
|
||||
'enableAdvancedSearch', 'numberOfSearchColumns',
|
||||
'searchFields', 'optionalFields', 'showWorkflow',
|
||||
'showWorkflowCommentField', 'showAllStatesInPhase')
|
||||
defaultToolFields = ('users', 'groups', 'translations', 'enableNotifications',
|
||||
'unoEnabledPython', 'openOfficePort',
|
||||
'numberOfResultsPerPage', 'listBoxesMaximumWidth',
|
||||
'appyVersion', 'refreshSecurity')
|
||||
|
||||
class Tool(ModelClass):
|
||||
# In a ModelClass we need to declare attributes in the following list.
|
||||
_appy_attributes = list(defaultToolFields)
|
||||
|
||||
# Tool attributes
|
||||
def validPythonWithUno(self, value): pass # Real method in the wrapper
|
||||
unoEnabledPython = String(group="connectionToOpenOffice",
|
||||
validator=validPythonWithUno)
|
||||
openOfficePort = Integer(default=2002, group="connectionToOpenOffice")
|
||||
numberOfResultsPerPage = Integer(default=30, show=False)
|
||||
listBoxesMaximumWidth = Integer(default=100, show=False)
|
||||
appyVersion = String(show=False, layouts='f')
|
||||
def refreshSecurity(self): pass # Real method in the wrapper
|
||||
refreshSecurity = Action(action=refreshSecurity, confirm=True)
|
||||
# Ref(User) will maybe be transformed into Ref(CustomUserClass).
|
||||
users = Ref(User, multiplicity=(0,None), add=True, link=False,
|
||||
back=Ref(attribute='toTool', show=False),
|
||||
page=Page('users', show='view'),
|
||||
queryable=True, queryFields=('title', 'login'),
|
||||
showHeaders=True, shownInfo=('title', 'login', 'roles'))
|
||||
groups = Ref(Group, multiplicity=(0,None), add=True, link=False,
|
||||
back=Ref(attribute='toTool2', show=False),
|
||||
page=Page('groups', show='view'),
|
||||
queryable=True, queryFields=('title', 'login'),
|
||||
showHeaders=True, shownInfo=('title', 'login', 'roles'))
|
||||
translations = Ref(Translation, multiplicity=(0,None),add=False,link=False,
|
||||
back=Ref(attribute='trToTool', show=False), show='view',
|
||||
page=Page('translations', show='view'))
|
||||
enableNotifications = Boolean(default=True,
|
||||
page=Page('notifications', show=False))
|
||||
|
||||
@classmethod
|
||||
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 defaultToolFields:
|
||||
toClean.append(k)
|
||||
for k in toClean:
|
||||
exec 'del klass.%s' % k
|
||||
klass._appy_attributes = list(defaultToolFields)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,105 +0,0 @@
|
|||
'''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.descriptors import WorkflowDescriptor
|
||||
import socket
|
||||
|
||||
def sendMail(obj, transition, transitionName, workflow):
|
||||
'''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:
|
||||
obj.log(SENDMAIL_ERROR % str(sg), type='warning')
|
||||
break
|
||||
except UnicodeDecodeError, ue:
|
||||
obj.log(ENCODING_ERROR % str(ue), type='warning')
|
||||
break
|
||||
except Exception, e:
|
||||
obj.log(SENDMAIL_ERROR % str(e), type='warning')
|
||||
break
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,33 +0,0 @@
|
|||
<!codeHeader!>
|
||||
from OFS.SimpleItem import SimpleItem
|
||||
from OFS.Folder import Folder
|
||||
from appy.gen.utils import createObject
|
||||
from AccessControl import ClassSecurityInfo
|
||||
import Products.<!applicationName!>.config as cfg
|
||||
from appy.gen.plone25.mixins import BaseMixin
|
||||
from appy.gen.plone25.mixins.ToolMixin import ToolMixin
|
||||
from Extensions.appyWrappers import <!genClassName!>_Wrapper as Wrapper
|
||||
|
||||
def manage_add<!genClassName!>(self, id, title='', REQUEST=None):
|
||||
'''Creates instances of this class.'''
|
||||
createObject(self, id, '<!genClassName!>', '<!applicationName!>')
|
||||
if REQUEST is not None: return self.manage_main(self, REQUEST)
|
||||
|
||||
class <!genClassName!>(<!parents!>):
|
||||
'''<!classDoc!>'''
|
||||
security = ClassSecurityInfo()
|
||||
meta_type = '<!genClassName!>'
|
||||
portal_type = '<!genClassName!>'
|
||||
allowed_content_types = ()
|
||||
filter_content_types = 0
|
||||
global_allow = 1
|
||||
icon = "ui/<!icon!>"
|
||||
wrapperClass = Wrapper
|
||||
config = cfg
|
||||
def do(self):
|
||||
'''BaseMixin.do can't be traversed by Zope if this class is the tool.
|
||||
So here, we redefine this method.'''
|
||||
return BaseMixin.do(self)
|
||||
for elem in dir(<!baseMixin!>):
|
||||
if not elem.startswith('__'): security.declarePublic(elem)
|
||||
<!methods!>
|
|
@ -1,19 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def installProduct(context):
|
||||
'''Installs the necessary products for Appy.'''
|
||||
portal = context.getSite()
|
||||
qi = getToolByName(portal, 'portal_quickinstaller')
|
||||
if not qi.isProductInstalled('PloneLanguageTool'):
|
||||
qi.installProduct('PloneLanguageTool')
|
||||
if not qi.isProductInstalled('<!applicationName!>'):
|
||||
qi.installProduct('<!applicationName!>')
|
||||
return "<!applicationName!> installed."
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def install_default(context):
|
||||
# Installation function of default profile.
|
||||
installProduct(context)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,90 +0,0 @@
|
|||
#importedElem { color: grey; font-style: italic; }
|
||||
.appyPod { float:right; }
|
||||
.appyFocus { color: #900101; }
|
||||
|
||||
.appyChanges th {
|
||||
font-style: italic;
|
||||
background-color: transparent;
|
||||
border: 0 none transparent;
|
||||
padding: 0.1em 0.1em 0.1em 0.1em;
|
||||
}
|
||||
|
||||
.appyChanges td {
|
||||
padding: 0.1em 0.2em 0.1em 0.2em !important;
|
||||
border-top: 1px dashed #8CACBB !important;
|
||||
border-right: 0 none transparent !important;
|
||||
border-left: 0 none transparent !important;
|
||||
border-bottom: 0 none transparent !important;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
a.tooltip span {
|
||||
display:none;
|
||||
padding:2px 3px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
a.rtip span { margin-left:3px; }
|
||||
a.ltip span { margin-left:-150px }
|
||||
a.tooltip:hover span {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
border: 1px solid grey;
|
||||
background-color: white;
|
||||
color: #dd;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
fieldset {
|
||||
line-height: 1em;
|
||||
border: 2px solid #8CACBB;
|
||||
margin: 0.5em 0em 0.5em 0em;
|
||||
padding: 0 0.7em 0.5em;
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
|
||||
.fakeButton {
|
||||
background: #ffd5c0 url(&dtml-portal_url;/ui/fakeTransition.gif) 5px 1px no-repeat;
|
||||
padding: 3px 4px 3px 12px;
|
||||
}
|
||||
|
||||
/* Portlet elements */
|
||||
.portletHeader {
|
||||
text-transform: none;
|
||||
padding: 1px 0.5em;
|
||||
}
|
||||
.portletSearch {
|
||||
padding: 0 0 0 0.6em;
|
||||
font-style: normal;
|
||||
font-size: 95%;
|
||||
}
|
||||
.portletGroup {
|
||||
font-variant: small-caps;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.portletGroupItem { padding-left: 0.8em; font-style: italic; }
|
||||
.portletMenu { margin-bottom: 0.4em; }
|
||||
|
||||
/* image-right, but without border */
|
||||
.image-right {
|
||||
border:0px solid Black;
|
||||
clear:both;
|
||||
float:right;
|
||||
margin:0.5em;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<!codeHeader!>
|
||||
# Test coverage-related stuff --------------------------------------------------
|
||||
import sys
|
||||
from appy.gen.plone25.mixins.TestMixin import TestMixin
|
||||
covFolder = TestMixin.getCovFolder()
|
||||
# The previous method checks in sys.argv whether Zope was lauched for performing
|
||||
# coverage tests or not.
|
||||
cov = None # The main Coverage instance as created by the coverage program.
|
||||
totalNumberOfTests = <!totalNumberOfTests!>
|
||||
numberOfExecutedTests = 0
|
||||
if covFolder:
|
||||
try:
|
||||
import coverage
|
||||
from coverage import coverage
|
||||
cov = coverage()
|
||||
cov.start()
|
||||
except ImportError:
|
||||
print 'COVERAGE KO! The "coverage" program is not installed. You can ' \
|
||||
'download it from http://nedbatchelder.com/code/coverage.' \
|
||||
'\nHit <enter> to execute the test suite without coverage.'
|
||||
sys.stdin.readline()
|
||||
|
||||
def countTest():
|
||||
global numberOfExecutedTests
|
||||
numberOfExecutedTests += 1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import config
|
||||
from appy.gen.plone25.installer import ZopeInstaller
|
||||
|
||||
# Zope-level installation of the generated product. ----------------------------
|
||||
def initialize(context):
|
||||
<!imports!>
|
||||
# I need to do those imports here; else, types and add permissions will not
|
||||
# be registered.
|
||||
classes = [<!classes!>]
|
||||
ZopeInstaller(context, config, classes).install()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,19 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen import *
|
||||
Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper as WTool
|
||||
from appy.gen.plone25.wrappers.UserWrapper import UserWrapper as WUser
|
||||
from appy.gen.plone25.wrappers.GroupWrapper import GroupWrapper as WGroup
|
||||
from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper as WT
|
||||
from Globals import InitializeClass
|
||||
from AccessControl import ClassSecurityInfo
|
||||
tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields
|
||||
<!imports!>
|
||||
|
||||
<!User!>
|
||||
<!Group!>
|
||||
<!Translation!>
|
||||
<!Tool!>
|
||||
<!wrappers!>
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,52 +0,0 @@
|
|||
<!codeHeader!>
|
||||
import os, os.path, sys, copy
|
||||
import appy.gen
|
||||
import Extensions.appyWrappers as wraps
|
||||
<!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 zExceptions import BadRequest
|
||||
from ZPublisher.HTTPRequest import BaseRequest
|
||||
from OFS.Image import File
|
||||
from ZPublisher.HTTPRequest import FileUpload
|
||||
from AccessControl import getSecurityManager
|
||||
from AccessControl.PermissionRole import rolesForPermissionOn
|
||||
from DateTime import DateTime
|
||||
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
||||
from Products.Transience.Transience import TransientObjectContainer
|
||||
import appy.gen
|
||||
import logging
|
||||
logger = logging.getLogger('<!applicationName!>')
|
||||
|
||||
# Some global variables --------------------------------------------------------
|
||||
PROJECTNAME = '<!applicationName!>'
|
||||
diskFolder = os.path.dirname(<!applicationName!>.__file__)
|
||||
defaultAddRoles = [<!defaultAddRoles!>]
|
||||
ADD_CONTENT_PERMISSIONS = {
|
||||
<!addPermissions!>}
|
||||
|
||||
# Applications classes, in various formats
|
||||
rootClasses = [<!rootClasses!>]
|
||||
appClasses = [<!appClasses!>]
|
||||
appClassNames = [<!appClassNames!>]
|
||||
allClassNames = [<!allClassNames!>]
|
||||
|
||||
# In the following dict, we store, for every Appy class, the ordered list of
|
||||
# appy types (included inherited ones).
|
||||
attributes = {<!attributes!>}
|
||||
|
||||
# Application roles
|
||||
applicationRoles = [<!roles!>]
|
||||
applicationGlobalRoles = [<!gRoles!>]
|
||||
grantableRoles = [<!grRoles!>]
|
||||
|
||||
# Configuration options
|
||||
languages = [<!languages!>]
|
||||
languageSelector = <!languageSelector!>
|
||||
appFrontPage = <!appFrontPage!>
|
||||
sourceLanguage = '<!sourceLanguage!>'
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,6 +0,0 @@
|
|||
<configure xmlns="http://namespaces.zope.org/zope"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
xmlns:five="http://namespaces.zope.org/five"
|
||||
i18n_domain="<!applicationName!>">
|
||||
<!deprecated!>
|
||||
</configure>
|
|
@ -1,11 +0,0 @@
|
|||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
|
||||
metal:use-macro="here/main_template/macros/master" i18n:domain="<!applicationName!>">
|
||||
|
||||
<metal:disable fill-slot="top_slot"
|
||||
tal:define="dummy python:request.set('disable_border',1)" />
|
||||
<body>
|
||||
<div metal:fill-slot="main" tal:define="tool python: context.<!toolInstanceName!>">
|
||||
<!pageContent!>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||
<tal:main define="tool python: context.<!toolInstanceName!>">
|
||||
<html metal:use-macro="context/ui/template/macros/main">
|
||||
<div metal:fill-slot="content"><!pageContent!></div>
|
||||
</html>
|
||||
</tal:main>
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
|
@ -1,27 +0,0 @@
|
|||
<!codeHeader!>
|
||||
|
||||
from unittest import TestSuite
|
||||
from Testing import ZopeTestCase
|
||||
from Testing.ZopeTestCase import ZopeDocTestSuite
|
||||
from Products.PloneTestCase import PloneTestCase
|
||||
from appy.gen.plone25.mixins.TestMixin import TestMixin, beforeTest, afterTest
|
||||
<!imports!>
|
||||
|
||||
# Initialize Zope & Plone test systems -----------------------------------------
|
||||
ZopeTestCase.installProduct('PloneLanguageTool')
|
||||
ZopeTestCase.installProduct('<!applicationName!>')
|
||||
PloneTestCase.setupPloneSite(products=['PloneLanguageTool',
|
||||
'<!applicationName!>'])
|
||||
|
||||
class Test(PloneTestCase.PloneTestCase, TestMixin):
|
||||
'''Base test class for <!applicationName!> test cases.'''
|
||||
|
||||
# Data needed for defining the tests -------------------------------------------
|
||||
data = {'test_class': Test, 'setUp': beforeTest, 'tearDown': afterTest,
|
||||
'globs': {'appName': '<!applicationName!>'}}
|
||||
modulesWithTests = [<!modulesWithTests!>]
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def test_suite():
|
||||
return TestSuite([ZopeDocTestSuite(m, **data) for m in modulesWithTests])
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,74 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class GroupWrapper(AbstractWrapper):
|
||||
|
||||
def showLogin(self):
|
||||
'''When must we show the login field?'''
|
||||
if self.o.isTemporary(): return 'edit'
|
||||
return 'view'
|
||||
|
||||
def validateLogin(self, login):
|
||||
'''Is this p_login valid?'''
|
||||
return True
|
||||
|
||||
def getGrantableRoles(self):
|
||||
'''Returns the list of roles that the admin can grant to a user.'''
|
||||
res = []
|
||||
for role in self.o.getProductConfig().grantableRoles:
|
||||
res.append( (role, self.translate('role_%s' % role)) )
|
||||
return res
|
||||
|
||||
def validate(self, new, errors):
|
||||
'''Inter-field validation.'''
|
||||
return self._callCustom('validate', new, errors)
|
||||
|
||||
def confirm(self, new):
|
||||
'''Use this method for remembering the previous list of users for this
|
||||
group.'''
|
||||
obj = self.o
|
||||
if hasattr(obj.aq_base, '_oldUsers'): del obj.aq_base._oldUsers
|
||||
obj._oldUsers = self.users
|
||||
|
||||
def addUser(self, user):
|
||||
'''Adds a p_user to this group.'''
|
||||
# Update the Ref field.
|
||||
self.link('users', user)
|
||||
# Update the group-related info on the Zope user.
|
||||
zopeUser = user.getZopeUser()
|
||||
zopeUser.groups[self.login] = self.roles
|
||||
|
||||
def removeUser(self, user):
|
||||
'''Removes a p_user from this group.'''
|
||||
self.unlink('users', user)
|
||||
# Update the group-related info on the Zope user.
|
||||
zopeUser = user.getZopeUser()
|
||||
del zopeUser.groups[self.login]
|
||||
|
||||
def onEdit(self, created):
|
||||
# Create or update, on every Zope user of this group, group-related
|
||||
# information.
|
||||
# 1. Remove reference to this group for users that were removed from it
|
||||
newUsers = self.users
|
||||
# The list of previously existing users does not exist when editing a
|
||||
# group from Python. For updating self.users, it is recommended to use
|
||||
# methods m_addUser and m_removeUser above.
|
||||
oldUsers = getattr(self.o.aq_base, '_oldUsers', ())
|
||||
for user in oldUsers:
|
||||
if user not in newUsers:
|
||||
del user.getZopeUser().groups[self.login]
|
||||
self.log('User "%s" removed from group "%s".' % \
|
||||
(user.login, self.login))
|
||||
# 2. Add reference to this group for users that were added to it
|
||||
for user in newUsers:
|
||||
zopeUser = user.getZopeUser()
|
||||
# We refresh group-related info on the Zope user even if the user
|
||||
# was already in the group.
|
||||
zopeUser.groups[self.login] = self.roles
|
||||
if user not in oldUsers:
|
||||
self.log('User "%s" added to group "%s".' % \
|
||||
(user.login, self.login))
|
||||
if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers
|
||||
return self._callCustom('onEdit', created)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,137 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os.path
|
||||
import appy
|
||||
from appy.shared.utils import executeCommand
|
||||
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 ToolWrapper(AbstractWrapper):
|
||||
def validPythonWithUno(self, value):
|
||||
'''This method represents the validator for field unoEnabledPython.'''
|
||||
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 True
|
||||
|
||||
podOutputFormats = ('odt', 'pdf', 'doc', 'rtf')
|
||||
def getPodOutputFormats(self):
|
||||
'''Gets the available output formats for POD documents.'''
|
||||
return [(of, self.translate(of)) for of in self.podOutputFormats]
|
||||
|
||||
def getInitiator(self):
|
||||
'''Retrieves the object that triggered the creation of the object
|
||||
being currently created (if any).'''
|
||||
nav = self.o.REQUEST.get('nav', '')
|
||||
if nav: return self.getObject(nav.split('.')[1])
|
||||
|
||||
def getObject(self, uid):
|
||||
'''Allow to retrieve an object from its unique identifier p_uid.'''
|
||||
return self.o.getObject(uid, appy=True)
|
||||
|
||||
def getDiskFolder(self):
|
||||
'''Returns the disk folder where the Appy application is stored.'''
|
||||
return self.o.getProductConfig().diskFolder
|
||||
|
||||
def getAttributeName(self, attributeType, klass, attrName=None):
|
||||
'''Some names of Tool attributes are not easy to guess. For example,
|
||||
the attribute that stores the names of the columns to display in
|
||||
query results for class A that is in package x.y is
|
||||
"tool.resultColumnsForx_y_A". Other example: the attribute that
|
||||
stores the editable default value of field "f1" of class x.y.A is
|
||||
"tool.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.
|
||||
|
||||
"podTemplate"
|
||||
Stores the pod template for p_attrName.
|
||||
|
||||
"formats"
|
||||
Stores the output format(s) of a given pod template for
|
||||
p_attrName.
|
||||
|
||||
"resultColumns"
|
||||
Stores the list of columns that must be shown when displaying
|
||||
instances of the a given root p_klass.
|
||||
|
||||
"enableAdvancedSearch"
|
||||
Determines if the advanced search screen must be enabled for
|
||||
p_klass.
|
||||
|
||||
"numberOfSearchColumns"
|
||||
Determines in how many columns the search screen for p_klass
|
||||
is rendered.
|
||||
|
||||
"searchFields"
|
||||
Determines, among all indexed fields for p_klass, which one will
|
||||
really be used in the search screen.
|
||||
|
||||
"optionalFields"
|
||||
Stores the list of optional attributes that are in use in the
|
||||
tool 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 = self.o.getPortalType(klass)
|
||||
res = '%sFor%s' % (attributeType, fullClassName)
|
||||
if attrName: res += '_%s' % attrName
|
||||
return res
|
||||
|
||||
def getAvailableLanguages(self):
|
||||
'''Returns the list of available languages for this application.'''
|
||||
return [(t.id, t.title) for t in self.translations]
|
||||
|
||||
def convert(self, fileName, format):
|
||||
'''Launches a UNO-enabled Python interpreter as defined in the self for
|
||||
converting, using OpenOffice in server mode, a file named p_fileName
|
||||
into an output p_format.'''
|
||||
convScript = '%s/pod/converter.py' % os.path.dirname(appy.__file__)
|
||||
cmd = '%s %s "%s" %s -p%d' % (self.unoEnabledPython, convScript,
|
||||
fileName, format, self.openOfficePort)
|
||||
self.log('Executing %s...' % cmd)
|
||||
return executeCommand(cmd) # The result can contain an error message
|
||||
|
||||
def refreshSecurity(self):
|
||||
'''Refreshes, on every object in the database, security-related,
|
||||
workflow-managed information.'''
|
||||
context = {'nb': 0}
|
||||
for className in self.o.getProductConfig().allClassNames:
|
||||
self.compute(className, context=context, noSecurity=True,
|
||||
expression="ctx['nb'] += int(obj.o.refreshSecurity())")
|
||||
msg = 'Security refresh: %d object(s) updated.' % context['nb']
|
||||
self.log(msg)
|
||||
self.say(msg)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,78 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os.path
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
from appy.gen.po import PoFile, PoMessage
|
||||
from appy.shared.utils import getOsTempFolder
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TranslationWrapper(AbstractWrapper):
|
||||
def label(self, field):
|
||||
'''The label for a text to translate displays the text of the
|
||||
corresponding message in the source translation.'''
|
||||
tool = self.tool
|
||||
sourceLanguage = self.o.getProductConfig().sourceLanguage
|
||||
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
|
||||
# p_field is the Computed field. We need to get the name of the
|
||||
# corresponding field holding the translation message.
|
||||
fieldName = field.name[:-6]
|
||||
# If we are showing the source translation, we do not repeat the message
|
||||
# in the label.
|
||||
if self.id == sourceLanguage:
|
||||
sourceMsg = ''
|
||||
else:
|
||||
sourceMsg = getattr(sourceTranslation,fieldName)
|
||||
# When editing the value, we don't want HTML code to be interpreted.
|
||||
# This way, the translator sees the HTML tags and can reproduce them
|
||||
# in the translation.
|
||||
url = self.request['URL']
|
||||
if url.endswith('/ui/edit') or url.endswith('/do'):
|
||||
sourceMsg = sourceMsg.replace('<','<').replace('>','>')
|
||||
sourceMsg = sourceMsg.replace('\n', '<br/>')
|
||||
return '<div class="translationLabel"><acronym title="%s">' \
|
||||
'<img src="ui/help.png"/></acronym>%s</div>' % \
|
||||
(fieldName, sourceMsg)
|
||||
|
||||
def show(self, field):
|
||||
'''We show a field (or its label) only if the corresponding source
|
||||
message is not empty.'''
|
||||
tool = self.tool
|
||||
if field.type == 'Computed': name = field.name[:-6]
|
||||
else: name = field.name
|
||||
# Get the source message
|
||||
sourceLanguage = self.o.getProductConfig().sourceLanguage
|
||||
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
|
||||
sourceMsg = getattr(sourceTranslation, name)
|
||||
if field.isEmptyValue(sourceMsg): return False
|
||||
return True
|
||||
|
||||
poReplacements = ( ('\r\n', '<br/>'), ('\n', '<br/>'), ('"', '\\"') )
|
||||
def getPoFile(self):
|
||||
'''Computes and returns the PO file corresponding to this
|
||||
translation.'''
|
||||
tool = self.tool
|
||||
fileName = os.path.join(getOsTempFolder(),
|
||||
'%s-%s.po' % (tool.o.getAppName(), self.id))
|
||||
poFile = PoFile(fileName)
|
||||
for field in self.fields:
|
||||
if (field.name == 'title') or (field.type != 'String'): continue
|
||||
# Adds the PO message corresponding to this field
|
||||
msg = field.getValue(self.o) or ''
|
||||
for old, new in self.poReplacements:
|
||||
msg = msg.replace(old, new)
|
||||
poFile.addMessage(PoMessage(field.name, msg, ''))
|
||||
poFile.generate()
|
||||
return True, file(fileName)
|
||||
|
||||
def validate(self, new, errors):
|
||||
# Call a custom "validate" if any.
|
||||
return self._callCustom('validate', new, errors)
|
||||
|
||||
def onEdit(self, created):
|
||||
# Call a custom "onEdit" if any.
|
||||
return self._callCustom('onEdit', created)
|
||||
|
||||
def onDelete(self):
|
||||
# Call a custom "onDelete" if any.
|
||||
self.log('Translation "%s" deleted by "%s".' % (self.id, self.user.id))
|
||||
return self._callCustom('onDelete')
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,158 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class UserWrapper(AbstractWrapper):
|
||||
|
||||
def showLogin(self):
|
||||
'''When must we show the login field?'''
|
||||
if self.o.isTemporary(): return 'edit'
|
||||
return 'view'
|
||||
|
||||
def validateLogin(self, login):
|
||||
'''Is this p_login valid?'''
|
||||
# The login can't be the id of the whole site or "admin"
|
||||
if login == 'admin':
|
||||
return self.translate('This username is reserved.')
|
||||
# Check that no user or group already uses this login.
|
||||
if self.count('User', login=login) or self.count('Group', login=login):
|
||||
return self.translate('This login is already in use.')
|
||||
return True
|
||||
|
||||
def validatePassword(self, password):
|
||||
'''Is this p_password valid?'''
|
||||
# Password must be at least 5 chars length
|
||||
if len(password) < 5:
|
||||
return self.translate('Passwords must contain at least 5 letters.')
|
||||
return True
|
||||
|
||||
def showPassword(self):
|
||||
'''When must we show the 2 fields for entering a password ?'''
|
||||
if self.o.isTemporary(): return 'edit'
|
||||
return False
|
||||
|
||||
def getGrantableRoles(self):
|
||||
'''Returns the list of roles that the admin can grant to a user.'''
|
||||
res = []
|
||||
for role in self.o.getProductConfig().grantableRoles:
|
||||
res.append( (role, self.translate('role_%s' % role)) )
|
||||
return res
|
||||
|
||||
def validate(self, new, errors):
|
||||
'''Inter-field validation.'''
|
||||
page = self.request.get('page', 'main')
|
||||
if page == 'main':
|
||||
if hasattr(new, 'password1') and (new.password1 != new.password2):
|
||||
msg = self.translate('Passwords do not match.')
|
||||
errors.password1 = msg
|
||||
errors.password2 = msg
|
||||
return self._callCustom('validate', new, errors)
|
||||
|
||||
def onEdit(self, created):
|
||||
self.title = self.firstName + ' ' + self.name
|
||||
aclUsers = self.o.acl_users
|
||||
login = self.login
|
||||
if created:
|
||||
# Create the corresponding Zope user
|
||||
aclUsers._doAddUser(login, self.password1, self.roles, ())
|
||||
zopeUser = aclUsers.getUser(login)
|
||||
# Remove our own password copies
|
||||
self.password1 = self.password2 = ''
|
||||
from persistent.mapping import PersistentMapping
|
||||
# The following dict will store, for every group, global roles
|
||||
# granted to it.
|
||||
zopeUser.groups = PersistentMapping()
|
||||
else:
|
||||
# Updates roles at the Zope level.
|
||||
zopeUser = aclUsers.getUserById(login)
|
||||
zopeUser.roles = self.roles
|
||||
# "self" must be owned by its Zope user
|
||||
if 'Owner' not in self.o.get_local_roles_for_userid(login):
|
||||
self.o.manage_addLocalRoles(login, ('Owner',))
|
||||
return self._callCustom('onEdit', created)
|
||||
|
||||
def getZopeUser(self):
|
||||
'''Gets the Zope user corresponding to this user.'''
|
||||
return self.o.acl_users.getUser(self.login)
|
||||
|
||||
def onDelete(self):
|
||||
'''Before deleting myself, I must delete the corresponding Zope user.'''
|
||||
self.o.acl_users._doDelUsers([self.login])
|
||||
self.log('User "%s" deleted.' % self.login)
|
||||
# Call a custom "onDelete" if any.
|
||||
return self._callCustom('onDelete')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
try:
|
||||
from AccessControl.PermissionRole import _what_not_even_god_should_do, \
|
||||
rolesForPermissionOn
|
||||
from Acquisition import aq_base
|
||||
except ImportError:
|
||||
pass # For those using Appy without Zope
|
||||
|
||||
class ZopeUserPatches:
|
||||
'''This class is a fake one that defines Appy variants of some of Zope's
|
||||
AccessControl.User methods. The idea is to implement the notion of group
|
||||
of users.'''
|
||||
|
||||
def getRoles(self):
|
||||
'''Returns the global roles that this user (or any of its groups)
|
||||
possesses.'''
|
||||
res = list(self.roles)
|
||||
# Add group global roles
|
||||
if not hasattr(aq_base(self), 'groups'): return res
|
||||
for roles in self.groups.itervalues():
|
||||
for role in roles:
|
||||
if role not in res: res.append(role)
|
||||
return res
|
||||
|
||||
def getRolesInContext(self, object):
|
||||
'''Return the list of global and local (to p_object) roles granted to
|
||||
this user (or to any of its groups).'''
|
||||
object = getattr(object, 'aq_inner', object)
|
||||
# Start with user global roles
|
||||
res = self.getRoles()
|
||||
# Add local roles
|
||||
localRoles = getattr(object, '__ac_local_roles__', None)
|
||||
if not localRoles: return res
|
||||
userId = self.getId()
|
||||
groups = getattr(self, 'groups', ())
|
||||
for id, roles in localRoles.iteritems():
|
||||
if (id != userId) and (id not in groups): continue
|
||||
for role in roles: res.add(role)
|
||||
return res
|
||||
|
||||
def allowed(self, object, object_roles=None):
|
||||
'''Checks whether the user has access to p_object. The user (or one of
|
||||
its groups) must have one of the roles in p_object_roles.'''
|
||||
if object_roles is _what_not_even_god_should_do: return 0
|
||||
# If "Anonymous" is among p_object_roles, grant access.
|
||||
if (object_roles is None) or ('Anonymous' in object_roles): return 1
|
||||
# If "Authenticated" is among p_object_roles, grant access if the user
|
||||
# is not anonymous.
|
||||
if 'Authenticated' in object_roles and \
|
||||
(self.getUserName() != 'Anonymous User'):
|
||||
if self._check_context(object): return 1
|
||||
# Try first to grant access based on global user roles
|
||||
for role in self.getRoles():
|
||||
if role not in object_roles: continue
|
||||
if self._check_context(object): return 1
|
||||
return
|
||||
# Try then to grant access based on local roles
|
||||
innerObject = getattr(object, 'aq_inner', object)
|
||||
localRoles = getattr(innerObject, '__ac_local_roles__', None)
|
||||
if not localRoles: return
|
||||
userId = self.getId()
|
||||
groups = getattr(self, 'groups', ())
|
||||
for id, roles in localRoles.iteritems():
|
||||
if (id != userId) and (id not in groups): continue
|
||||
for role in roles:
|
||||
if role not in object_roles: continue
|
||||
if self._check_context(object): return 1
|
||||
return
|
||||
|
||||
from AccessControl.User import SimpleUser
|
||||
SimpleUser.getRoles = getRoles
|
||||
SimpleUser.getRolesInContext = getRolesInContext
|
||||
SimpleUser.allowed = allowed
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,361 +0,0 @@
|
|||
'''This package contains base classes for wrappers that hide to the Appy
|
||||
developer the real classes used by the underlying web framework.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, mimetypes
|
||||
import appy.pod
|
||||
from appy.gen import Type, Search, Ref, String
|
||||
from appy.gen.utils import sequenceTypes, createObject
|
||||
from appy.shared.utils import getOsTempFolder, executeCommand, normalizeString
|
||||
from appy.shared.xml_parser import XmlMarshaller
|
||||
from appy.shared.csv_parser import CsvMarshaller
|
||||
|
||||
# Some error messages ----------------------------------------------------------
|
||||
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
|
||||
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
|
||||
'mimeType).'
|
||||
FREEZE_ERROR = 'Error while trying to freeze a "%s" file in POD field ' \
|
||||
'"%s" (%s).'
|
||||
FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \
|
||||
'administrator.'
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class AbstractWrapper(object):
|
||||
'''Any real Zope object has a companion object that is an instance of this
|
||||
class.'''
|
||||
def __init__(self, o): self.__dict__['o'] = o
|
||||
def appy(self): return self
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
appyType = self.o.getAppyType(name)
|
||||
if not appyType:
|
||||
raise 'Attribute "%s" does not exist.' % name
|
||||
appyType.store(self.o, value)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
'''Gets the attribute named p_name. Lot of cheating here.'''
|
||||
if name == 'o': return object.__getattribute__(self, name)
|
||||
elif name == 'tool': return self.o.getTool().appy()
|
||||
elif name == 'request': return self.o.REQUEST
|
||||
elif name == 'session': return self.o.REQUEST.SESSION
|
||||
elif name == 'typeName': return self.__class__.__bases__[-1].__name__
|
||||
elif name == 'id': return self.o.id
|
||||
elif name == 'uid': return self.o.UID()
|
||||
elif name == 'klass': return self.__class__.__bases__[-1]
|
||||
elif name == 'url': return self.o.absolute_url()
|
||||
elif name == 'state': return self.o.State()
|
||||
elif name == 'stateLabel':
|
||||
o = self.o
|
||||
appName = o.getProductConfig().PROJECTNAME
|
||||
return o.translate(o.getWorkflowLabel(), domain=appName)
|
||||
elif name == 'history':
|
||||
o = self.o
|
||||
key = o.workflow_history.keys()[0]
|
||||
return o.workflow_history[key]
|
||||
elif name == 'user':
|
||||
return self.o.getUser()
|
||||
elif name == 'appyUser':
|
||||
return self.search('User', login=self.o.getUser().getId())[0]
|
||||
elif name == 'fields': return self.o.getAllAppyTypes()
|
||||
# Now, let's try to return a real attribute.
|
||||
res = object.__getattribute__(self, name)
|
||||
# If we got an Appy type, return the value of this type for this object
|
||||
if isinstance(res, Type):
|
||||
o = self.o
|
||||
if isinstance(res, Ref):
|
||||
return res.getValue(o, noListIfSingleObj=True)
|
||||
else:
|
||||
return res.getValue(o)
|
||||
return res
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s appyobj at %s>' % (self.klass.__name__, id(self))
|
||||
|
||||
def __cmp__(self, other):
|
||||
if other: return cmp(self.o, other.o)
|
||||
return 1
|
||||
|
||||
def _callCustom(self, methodName, *args, **kwargs):
|
||||
'''This wrapper implements some methods like "validate" and "onEdit".
|
||||
If the user has defined its own wrapper, its methods will not be
|
||||
called. So this method allows, from the methods here, to call the
|
||||
user versions.'''
|
||||
if len(self.__class__.__bases__) > 1:
|
||||
# There is a custom user class
|
||||
customUser = self.__class__.__bases__[-1]
|
||||
if customUser.__dict__.has_key(methodName):
|
||||
return customUser.__dict__[methodName](self, *args, **kwargs)
|
||||
|
||||
def getField(self, name): return self.o.getAppyType(name)
|
||||
|
||||
def link(self, fieldName, obj):
|
||||
'''This method links p_obj (which can be a list of objects) to this one
|
||||
through reference field p_fieldName.'''
|
||||
return self.getField(fieldName).linkObject(self.o, obj)
|
||||
|
||||
def unlink(self, fieldName, obj):
|
||||
'''This method unlinks p_obj (which can be a list of objects) from this
|
||||
one through reference field p_fieldName.'''
|
||||
return self.getField(fieldName).unlinkObject(self.o, obj)
|
||||
|
||||
def sort(self, fieldName, sortKey='title', reverse=False):
|
||||
'''Sorts referred elements linked to p_self via p_fieldName according
|
||||
to a given p_sortKey which must be an attribute set on referred
|
||||
objects ("title", by default).'''
|
||||
refs = getattr(self.o, fieldName, None)
|
||||
if not refs: return
|
||||
tool = self.tool
|
||||
refs.sort(lambda x,y: cmp(getattr(tool.getObject(x), sortKey),
|
||||
getattr(tool.getObject(y), sortKey)))
|
||||
if reverse: refs.reverse()
|
||||
|
||||
def create(self, fieldNameOrClass, **kwargs):
|
||||
'''If p_fieldNameOrClass is the name of a field, this method allows to
|
||||
create an object and link it to the current one (self) through
|
||||
reference field named p_fieldName.
|
||||
If p_fieldNameOrClass is a class from the gen-application, it must
|
||||
correspond to a root class and this method allows to create a
|
||||
root object in the application folder.'''
|
||||
isField = isinstance(fieldNameOrClass, basestring)
|
||||
tool = self.tool.o
|
||||
# Determine the portal type of the object to create
|
||||
if isField:
|
||||
fieldName = fieldNameOrClass
|
||||
appyType = self.o.getAppyType(fieldName)
|
||||
portalType = tool.getPortalType(appyType.klass)
|
||||
else:
|
||||
klass = fieldNameOrClass
|
||||
portalType = tool.getPortalType(klass)
|
||||
# Determine object id
|
||||
if kwargs.has_key('id'):
|
||||
objId = kwargs['id']
|
||||
del kwargs['id']
|
||||
else:
|
||||
objId = tool.generateUid(portalType)
|
||||
# Determine if object must be created from external data
|
||||
externalData = None
|
||||
if kwargs.has_key('_data'):
|
||||
externalData = kwargs['_data']
|
||||
del kwargs['_data']
|
||||
# Where must I create the object?
|
||||
if not isField:
|
||||
folder = tool.getPath('/data')
|
||||
else:
|
||||
if hasattr(self, 'folder') and self.folder:
|
||||
folder = self.o
|
||||
else:
|
||||
folder = self.o.getParentNode()
|
||||
# Create the object
|
||||
zopeObj = createObject(folder, objId,portalType, tool.getAppName())
|
||||
appyObj = zopeObj.appy()
|
||||
# Set object attributes
|
||||
for attrName, attrValue in kwargs.iteritems():
|
||||
setattr(appyObj, attrName, attrValue)
|
||||
if isField:
|
||||
# Link the object to this one
|
||||
appyType.linkObject(self.o, zopeObj)
|
||||
zopeObj._appy_managePermissions()
|
||||
# Call custom initialization
|
||||
if externalData: param = externalData
|
||||
else: param = True
|
||||
if hasattr(appyObj, 'onEdit'): appyObj.onEdit(param)
|
||||
zopeObj.reindex()
|
||||
return appyObj
|
||||
|
||||
def freeze(self, fieldName, doAction=False):
|
||||
'''This method freezes a POD document. TODO: allow to freeze Computed
|
||||
fields.'''
|
||||
rq = self.request
|
||||
field = self.o.getAppyType(fieldName)
|
||||
if field.type != 'Pod': raise 'Cannot freeze non-Pod field.'
|
||||
# Perform the related action if required.
|
||||
if doAction: self.request.set('askAction', True)
|
||||
# Set the freeze format
|
||||
rq.set('podFormat', field.freezeFormat)
|
||||
# Generate the document.
|
||||
doc = field.getValue(self.o)
|
||||
if isinstance(doc, basestring):
|
||||
self.log(FREEZE_ERROR % (field.freezeFormat, field.name, doc),
|
||||
type='error')
|
||||
if field.freezeFormat == 'odt': raise FREEZE_FATAL_ERROR
|
||||
self.log('Trying to freeze the ODT version...')
|
||||
# Try to freeze the ODT version of the document, which does not
|
||||
# require to call OpenOffice/LibreOffice, so the risk of error is
|
||||
# smaller.
|
||||
self.request.set('podFormat', 'odt')
|
||||
doc = field.getValue(self.o)
|
||||
if isinstance(doc, basestring):
|
||||
self.log(FREEZE_ERROR % ('odt', field.name, doc), type='error')
|
||||
raise FREEZE_FATAL_ERROR
|
||||
field.store(self.o, doc)
|
||||
|
||||
def unFreeze(self, fieldName):
|
||||
'''This method un freezes a POD document. TODO: allow to unfreeze
|
||||
Computed fields.'''
|
||||
rq = self.request
|
||||
field = self.o.getAppyType(fieldName)
|
||||
if field.type != 'Pod': raise 'Cannot unFreeze non-Pod field.'
|
||||
field.store(self.o, None)
|
||||
|
||||
def delete(self):
|
||||
'''Deletes myself.'''
|
||||
self.o.delete()
|
||||
|
||||
def translate(self, label, mapping={}, domain=None, language=None,
|
||||
format='html'):
|
||||
'''Check documentation of self.o.translate.'''
|
||||
return self.o.translate(label, mapping, domain, language=language,
|
||||
format=format)
|
||||
|
||||
def do(self, transition, comment='', doAction=True, doNotify=True,
|
||||
doHistory=True):
|
||||
'''This method allows to trigger on p_self a workflow p_transition
|
||||
programmatically. See doc in self.o.do.'''
|
||||
return self.o.trigger(transition, comment, doAction=doAction,
|
||||
doNotify=doNotify, doHistory=doHistory, doSay=False)
|
||||
|
||||
def log(self, message, type='info'): return self.o.log(message, type)
|
||||
def say(self, message, type='info'): return self.o.say(message, type)
|
||||
|
||||
def normalize(self, s, usage='fileName'):
|
||||
'''Returns a version of string p_s whose special chars have been
|
||||
replaced with normal chars.'''
|
||||
return normalizeString(s, usage)
|
||||
|
||||
def search(self, klass, sortBy='', maxResults=None, noSecurity=False,
|
||||
**fields):
|
||||
'''Searches objects of p_klass. p_sortBy must be the name of an indexed
|
||||
field (declared with indexed=True); every param in p_fields must
|
||||
take the name of an indexed field and take a possible value of this
|
||||
field. You can optionally specify a maximum number of results in
|
||||
p_maxResults. If p_noSecurity is specified, you get all objects,
|
||||
even if the logged user does not have the permission to view it.'''
|
||||
# Find the content type corresponding to p_klass
|
||||
tool = self.tool.o
|
||||
contentType = tool.getPortalType(klass)
|
||||
# Create the Search object
|
||||
search = Search('customSearch', sortBy=sortBy, **fields)
|
||||
if not maxResults:
|
||||
maxResults = 'NO_LIMIT'
|
||||
# If I let maxResults=None, only a subset of the results will be
|
||||
# returned by method executeResult.
|
||||
res = tool.executeQuery(contentType, search=search,
|
||||
maxResults=maxResults, noSecurity=noSecurity)
|
||||
return [o.appy() for o in res['objects']]
|
||||
|
||||
def count(self, klass, noSecurity=False, **fields):
|
||||
'''Identical to m_search above, but returns the number of objects that
|
||||
match the search instead of returning the objects themselves. Use
|
||||
this method instead of writing len(self.search(...)).'''
|
||||
tool = self.tool.o
|
||||
contentType = tool.getPortalType(klass)
|
||||
search = Search('customSearch', **fields)
|
||||
res = tool.executeQuery(contentType, search=search, brainsOnly=True,
|
||||
noSecurity=noSecurity)
|
||||
if res: return res._len # It is a LazyMap instance
|
||||
else: return 0
|
||||
|
||||
def compute(self, klass, sortBy='', maxResults=None, context=None,
|
||||
expression=None, noSecurity=False, **fields):
|
||||
'''This method, like m_search and m_count above, performs a query on
|
||||
objects of p_klass. But in this case, instead of returning a list of
|
||||
matching objects (like m_search) or counting elements (like p_count),
|
||||
it evaluates, on every matching object, a Python p_expression (which
|
||||
may be an expression or a statement), and returns, if needed, a
|
||||
result. The result may be initialized through parameter p_context.
|
||||
p_expression is evaluated with 2 variables in its context: "obj"
|
||||
which is the currently walked object, instance of p_klass, and "ctx",
|
||||
which is the context as initialized (or not) by p_context. p_context
|
||||
may be used as
|
||||
(1) a variable or instance that is updated on every call to
|
||||
produce a result;
|
||||
(2) an input variable or instance;
|
||||
(3) both.
|
||||
|
||||
The method returns p_context, modified or not by evaluation of
|
||||
p_expression on every matching object.
|
||||
|
||||
When you need to perform an action or computation on a lot of
|
||||
objects, use this method instead of doing things like
|
||||
|
||||
"for obj in self.search(MyClass,...)"
|
||||
'''
|
||||
tool = self.tool.o
|
||||
contentType = tool.getPortalType(klass)
|
||||
search = Search('customSearch', sortBy=sortBy, **fields)
|
||||
# Initialize the context variable "ctx"
|
||||
ctx = context
|
||||
for brain in tool.executeQuery(contentType, search=search, \
|
||||
brainsOnly=True, maxResults=maxResults, noSecurity=noSecurity):
|
||||
# Get the Appy object from the brain
|
||||
if noSecurity: method = '_unrestrictedGetObject'
|
||||
else: method = 'getObject'
|
||||
exec 'obj = brain.%s().appy()' % method
|
||||
exec expression
|
||||
return ctx
|
||||
|
||||
def reindex(self):
|
||||
'''Asks a direct object reindexing. In most cases you don't have to
|
||||
reindex objects "manually" with this method. When an object is
|
||||
modified after some user action has been performed, Appy reindexes
|
||||
this object automatically. But if your code modifies other objects,
|
||||
Appy may not know that they must be reindexed, too. So use this
|
||||
method in those cases.'''
|
||||
self.o.reindex()
|
||||
|
||||
def export(self, at='string', format='xml', include=None, exclude=None):
|
||||
'''Creates an "exportable" version of this object. p_format is "xml" by
|
||||
default, but can also be "csv". If p_format is:
|
||||
* "xml", if p_at is "string", this method returns the XML version,
|
||||
without the XML prologue. Else, (a) if not p_at, the XML
|
||||
will be exported on disk, in the OS temp folder, with an
|
||||
ugly name; (b) else, it will be exported at path p_at.
|
||||
* "csv", if p_at is "string", this method returns the CSV data as a
|
||||
string. If p_at is an opened file handler, the CSV line will
|
||||
be appended in it.
|
||||
If p_include is given, only fields whose names are in it will be
|
||||
included. p_exclude, if given, contains names of fields that will
|
||||
not be included in the result.
|
||||
'''
|
||||
if format == 'xml':
|
||||
# Todo: take p_include and p_exclude into account.
|
||||
# Determine where to put the result
|
||||
toDisk = (at != 'string')
|
||||
if toDisk and not at:
|
||||
at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
|
||||
# Create the XML version of the object
|
||||
marshaller = XmlMarshaller(cdata=True, dumpUnicode=True,
|
||||
dumpXmlPrologue=toDisk,
|
||||
rootTag=self.klass.__name__)
|
||||
xml = marshaller.marshall(self.o, objectType='appy')
|
||||
# Produce the desired result
|
||||
if toDisk:
|
||||
f = file(at, 'w')
|
||||
f.write(xml.encode('utf-8'))
|
||||
f.close()
|
||||
return at
|
||||
else:
|
||||
return xml
|
||||
elif format == 'csv':
|
||||
if isinstance(at, basestring):
|
||||
marshaller = CsvMarshaller(include=include, exclude=exclude)
|
||||
return marshaller.marshall(self)
|
||||
else:
|
||||
marshaller = CsvMarshaller(at, include=include, exclude=exclude)
|
||||
marshaller.marshall(self)
|
||||
|
||||
def historize(self, data):
|
||||
'''This method allows to add "manually" a "data-change" event into the
|
||||
object's history. Indeed, data changes are "automatically" recorded
|
||||
only when an object is edited through the edit form, not when a
|
||||
setter is called from the code.
|
||||
|
||||
p_data must be a dictionary whose keys are field names (strings) and
|
||||
whose values are the previous field values.'''
|
||||
self.o.addDataChange(data)
|
||||
|
||||
def formatText(self, text, format='html'):
|
||||
'''Produces a representation of p_text into the desired p_format, which
|
||||
is 'html' by default.'''
|
||||
return self.o.formatText(text, format)
|
||||
# ------------------------------------------------------------------------------
|
Loading…
Add table
Add a link
Reference in a new issue