appy.gen: refactoring due to De-Plonization.

This commit is contained in:
Gaetan Delannay 2011-12-05 10:52:18 +01:00
parent 6733f4c7dc
commit d934f49a99
31 changed files with 1220 additions and 1325 deletions

View file

@ -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))
# ------------------------------------------------------------------------------

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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))
# ------------------------------------------------------------------------------

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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
# ------------------------------------------------------------------------------

View file

@ -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!>

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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;
}

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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!>
# ------------------------------------------------------------------------------

View file

@ -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!>'
# ------------------------------------------------------------------------------

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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])
# ------------------------------------------------------------------------------

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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('<','&lt;').replace('>','&gt;')
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')
# ------------------------------------------------------------------------------

View file

@ -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
# ------------------------------------------------------------------------------

View file

@ -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)
# ------------------------------------------------------------------------------