Eradicated Flavour and PodTemplate classes (for the latter, use Pod fields instead); Added a code analyser; Groups can now be slaves in master/slaves relationships; Refs have more params (show a confirmation popup before adding an object, add an object without creation form); Code for Refs has been refactored to comply with the new way to organize Types; Added a WebDAV client library.

This commit is contained in:
Gaetan Delannay 2010-10-14 14:43:56 +02:00
parent 9f4db88bdf
commit 990e16c6e7
47 changed files with 1006 additions and 1297 deletions

View file

@ -4,6 +4,7 @@
import sys, os.path import sys, os.path
from optparse import OptionParser from optparse import OptionParser
from appy.gen.generator import GeneratorError from appy.gen.generator import GeneratorError
from appy.shared.utils import LinesCounter
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ERROR_CODE = 1 ERROR_CODE = 1
@ -104,6 +105,8 @@ class GeneratorScript:
self.manageArgs(optParser, options, args) self.manageArgs(optParser, options, args)
print 'Generating %s product in %s...' % (args[1], args[2]) print 'Generating %s product in %s...' % (args[1], args[2])
self.generateProduct(options, *args) self.generateProduct(options, *args)
# Give the user some statistics about its code
LinesCounter(args[0]).run()
except GeneratorError, ge: except GeneratorError, ge:
sys.stderr.write(str(ge)) sys.stderr.write(str(ge))
sys.stderr.write('\n') sys.stderr.write('\n')

View file

@ -1,8 +1,9 @@
#!/usr/bin/python2.4.4 #!/usr/bin/python2.4.4
# Imports ---------------------------------------------------------------------- # Imports ----------------------------------------------------------------------
import os, os.path, shutil, re, zipfile, sys, ftplib, time import os, os.path, shutil, re, zipfile, sys, ftplib, time
import appy
from appy.shared import appyPath from appy.shared import appyPath
from appy.shared.utils import FolderDeleter from appy.shared.utils import FolderDeleter, LinesCounter
from appy.bin.clean import Cleaner from appy.bin.clean import Cleaner
from appy.gen.utils import produceNiceMessage from appy.gen.utils import produceNiceMessage
@ -432,6 +433,8 @@ class Publisher:
def run(self): def run(self):
Cleaner().run(verbose=False) Cleaner().run(verbose=False)
# Perform a small analysis on the Appy code
LinesCounter(appy).run()
print 'Generating site in %s...' % self.genFolder print 'Generating site in %s...' % self.genFolder
self.prepareGenFolder() self.prepareGenFolder()
self.createDocToc() self.createDocToc()

View file

@ -5,7 +5,7 @@ from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, PageDescr, GroupDescr, Keywords, \ from appy.gen.utils import sequenceTypes, PageDescr, GroupDescr, Keywords, \
FileWrapper, getClassName FileWrapper, getClassName, SomeObjects
from appy.shared.data import languages from appy.shared.data import languages
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
@ -29,10 +29,10 @@ class Page:
class Group: class Group:
'''Used for describing a group of widgets within a page.''' '''Used for describing a group of widgets within a page.'''
def __init__(self, name, columns=['100%'], wide=True, style='fieldset', def __init__(self, name, columns=['100%'], wide=True, style='section2',
hasLabel=True, hasDescr=False, hasHelp=False, hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1, align='center', hasHeaders=False, group=None, colspan=1, align='center',
valign='top'): valign='top', css_class='', master=None, masterValue=None):
self.name = name self.name = name
# In its simpler form, field "columns" below can hold a list or tuple # In its simpler form, field "columns" below can hold a list or tuple
# of column widths expressed as strings, that will be given as is in # of column widths expressed as strings, that will be given as is in
@ -77,6 +77,26 @@ class Group:
self.columns = self.columns[:1] self.columns = self.columns[:1]
# Header labels will be used as labels for the tabs. # Header labels will be used as labels for the tabs.
self.hasHeaders = True self.hasHeaders = True
self.css_class = css_class
self.master = None
self.masterValue = None
if self.master:
self._addMaster(self, master, masterValue)
def _addMaster(self, master, masterValue):
'''Specifies this group being a slave of another field: we will add css
classes allowing to show/hide, in Javascript, its widget according
to master value.'''
self.master = master
self.masterValue = masterValue
classes = 'slave_%s' % self.master.id
if type(self.masterValue) not in sequenceTypes:
masterValues = [self.masterValue]
else:
masterValues = self.masterValue
for masterValue in masterValues:
classes += ' slaveValue_%s_%s' % (self.master.id, masterValue)
self.css_class += ' ' + classes
def _setColumns(self): def _setColumns(self):
'''Standardizes field "columns" as a list of Column instances. Indeed, '''Standardizes field "columns" as a list of Column instances. Indeed,
@ -416,13 +436,12 @@ class Type:
'''When displaying p_obj on a given p_layoutType, must we show this '''When displaying p_obj on a given p_layoutType, must we show this
field?''' field?'''
isEdit = layoutType == 'edit' isEdit = layoutType == 'edit'
# Do not show field if it is optional and not selected in flavour # Do not show field if it is optional and not selected in tool
if self.optional: if self.optional:
tool = obj.getTool() tool = obj.getTool().appy()
flavour = tool.getFlavour(obj, appy=True) fieldName = 'optionalFieldsFor%s' % obj.meta_type
flavourAttrName = 'optionalFieldsFor%s' % obj.meta_type fieldValue = getattr(tool, fieldName, ())
flavourAttrValue = getattr(flavour, flavourAttrName, ()) if self.name not in fieldValue:
if self.name not in flavourAttrValue:
return False return False
# Check if the user has the permission to view or edit the field # Check if the user has the permission to view or edit the field
user = obj.portal_membership.getAuthenticatedMember() user = obj.portal_membership.getAuthenticatedMember()
@ -568,11 +587,10 @@ class Type:
return self.default(obj.appy()) return self.default(obj.appy())
else: else:
return self.default return self.default
# If value is editable, get the default value from the flavour # If value is editable, get the default value from the tool
portalTypeName = obj._appy_getPortalType(obj.REQUEST) portalTypeName = obj._appy_getPortalType(obj.REQUEST)
tool = obj.getTool() tool = obj.getTool().appy()
flavour = tool.getFlavour(portalTypeName, appy=True) return getattr(tool, 'defaultValueFor%s' % self.labelId)
return getattr(flavour, 'defaultValueFor%s' % self.labelId)
return value return value
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
@ -1188,17 +1206,24 @@ class File(Type):
class Ref(Type): class Ref(Type):
def __init__(self, klass=None, attribute=None, validator=None, def __init__(self, klass=None, attribute=None, validator=None,
multiplicity=(0,1), index=None, default=None, optional=False, multiplicity=(0,1), index=None, default=None, optional=False,
editDefault=False, add=False, link=True, unlink=False, editDefault=False, add=False, addConfirm=False, noForm=False,
back=None, show=True, page='main', group=None, layouts=None, link=True, unlink=False, back=None, show=True, page='main',
showHeaders=False, shownInfo=(), select=None, maxPerPage=30, group=None, layouts=None, showHeaders=False, shownInfo=(),
move=0, indexed=False, searchable=False, select=None, maxPerPage=30, move=0, indexed=False,
specificReadPermission=False, specificWritePermission=False, searchable=False, specificReadPermission=False,
width=None, height=None, colspan=1, master=None, specificWritePermission=False, width=None, height=None,
masterValue=None, focus=False, historized=False): colspan=1, master=None, masterValue=None, focus=False,
historized=False):
self.klass = klass self.klass = klass
self.attribute = attribute self.attribute = attribute
# May the user add new objects through this ref ? # May the user add new objects through this ref ?
self.add = add self.add = add
# When the user adds a new object, must a confirmation popup be shown?
self.addConfirm = addConfirm
# If noForm is True, when clicking to create an object through this ref,
# the object will be created automatically, and no creation form will
# be presented to the user.
self.noForm = noForm
# May the user link existing objects through this ref? # May the user link existing objects through this ref?
self.link = link self.link = link
# May the user unlink existing objects? # May the user unlink existing objects?
@ -1246,12 +1271,70 @@ class Ref(Type):
return obj.getBRefs(self.relationship) return obj.getBRefs(self.relationship)
return res return res
def getValue(self, obj): def getValue(self, obj, type='objects', noListIfSingleObj=False,
startNumber=None, someObjects=False):
'''Returns the objects linked to p_obj through Ref field "self".
- If p_type is "objects", it returns the Appy wrappers;
- If p_type is "zobjects", it returns the Zope objects;
- If p_type is "uids", it returns UIDs of objects (= strings).
* If p_startNumber is None, it returns all referred objects.
* If p_startNumber is a number, it returns self.maxPerPage objects,
starting at p_startNumber.
If p_noListIfSingleObj is True, it returns the single reference as
an object and not as a list.
If p_someObjects is True, it returns an instance of SomeObjects
instead of returning a list of references.'''
if self.isBack: if self.isBack:
return obj._appy_getRefsBack(self.name, self.relationship, getRefs = obj.reference_catalog.getBackReferences
noListIfSingleObj=True) uids = [r.sourceUID for r in getRefs(obj, self.relationship)]
else: else:
return obj._appy_getRefs(self.name, noListIfSingleObj=True).objects uids = obj._appy_getSortedField(self.name)
batchNeeded = startNumber != None
exec 'refUids = obj.getRaw%s%s()' % (self.name[0].upper(),
self.name[1:])
# There may be too much UIDs in sortedField because these fields
# are not updated when objects are deleted. So we do it now.
# TODO: do such cleaning on object deletion ?
toDelete = []
for uid in uids:
if uid not in refUids:
toDelete.append(uid)
for uid in toDelete:
uids.remove(uid)
# Prepare the result: an instance of SomeObjects, that, in this case,
# represent a subset of all referred objects
res = SomeObjects()
res.totalNumber = res.batchSize = len(uids)
batchNeeded = startNumber != None
if batchNeeded:
res.batchSize = self.maxPerPage
if startNumber != None:
res.startNumber = startNumber
# Get the needed referred objects
i = res.startNumber
# Is it possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids?
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
# Retrieve every reference in the correct format according to p_type
if type == 'uids':
ref = uids[i]
else:
ref = obj.uid_catalog(UID=uids[i])[0].getObject()
if type == 'objects':
ref = ref.appy()
res.objects.append(ref)
i += 1
# Manage parameter p_noListIfSingleObj
if res.objects and noListIfSingleObj:
if self.multiplicity[1] == 1:
res.objects = res.objects[0]
if someObjects: return res
return res.objects
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
return value return value
@ -1281,6 +1364,24 @@ class Ref(Type):
elif nbOfRefs > maxRef: elif nbOfRefs > maxRef:
return obj.translate('max_ref_violated') return obj.translate('max_ref_violated')
def store(self, obj, value):
'''Stores on p_obj, the p_value, which can be None, an object UID or a
list of UIDs coming from the request. This method is only called for
Ref fields with link=True.'''
# Security check
if not self.isShowable(obj, 'edit'): return
# Standardize the way p_value is expressed
uids = value
if not value: uids = []
if isinstance(value, basestring): uids = [value]
# Update the field storing on p_obj the ordered list of UIDs
sortedRefs = obj._appy_getSortedField(self.name)
del sortedRefs[:]
for uid in uids: sortedRefs.append(uid)
# Update the refs
refs = [obj.uid_catalog(UID=uid)[0].getObject() for uid in uids]
exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:])
class Computed(Type): class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None, def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show='view', default=None, optional=False, editDefault=False, show='view',
@ -1645,10 +1746,6 @@ class Model: pass
class Tool(Model): class Tool(Model):
'''If you want so define a custom tool class, she must inherit from this '''If you want so define a custom tool class, she must inherit from this
class.''' class.'''
class Flavour(Model):
'''A flavour represents a given group of configuration options. If you want
to define a custom flavour class, she must inherit from this class.'''
def __init__(self, name): self.name = name
class User(Model): class User(Model):
'''If you want to extend or modify the User class, subclass me.''' '''If you want to extend or modify the User class, subclass me.'''
@ -1692,11 +1789,6 @@ class Config:
# If you don't need the portlet that appy.gen has generated for your # If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False. # application, set the following parameter to False.
self.showPortlet = True self.showPortlet = True
# Default number of flavours. It will be used for generating i18n labels
# for classes in every flavour. Indeed, every flavour can name its
# concepts differently. For example, class Thing in flavour 2 may have
# i18n label "MyProject_Thing_2".
self.numberOfFlavours = 2
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Special field "type" is mandatory for every class. If one class does not # Special field "type" is mandatory for every class. If one class does not

View file

@ -1,6 +1,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys, parser, symbol, token, types import os, os.path, sys, parser, symbol, token, types
from appy.gen import Type, State, Config, Tool, Flavour, User from appy.gen import Type, State, Config, Tool, User
from appy.gen.descriptors import * from appy.gen.descriptors import *
from appy.gen.utils import produceNiceMessage from appy.gen.utils import produceNiceMessage
import appy.pod, appy.pod.renderer import appy.pod, appy.pod.renderer
@ -133,8 +133,7 @@ class Generator:
# Default descriptor classes # Default descriptor classes
self.descriptorClasses = { self.descriptorClasses = {
'class': ClassDescriptor, 'tool': ClassDescriptor, 'class': ClassDescriptor, 'tool': ClassDescriptor,
'flavour': ClassDescriptor, 'user': ClassDescriptor, 'user': ClassDescriptor, 'workflow': WorkflowDescriptor}
'workflow': WorkflowDescriptor}
# The following dict contains a series of replacements that need to be # The following dict contains a series of replacements that need to be
# applied to file templates to generate files. # applied to file templates to generate files.
self.repls = {'applicationName': self.applicationName, self.repls = {'applicationName': self.applicationName,
@ -143,7 +142,6 @@ class Generator:
# List of Appy classes and workflows found in the application # List of Appy classes and workflows found in the application
self.classes = [] self.classes = []
self.tool = None self.tool = None
self.flavour = None
self.user = None self.user = None
self.workflows = [] self.workflows = []
self.initialize() self.initialize()
@ -224,19 +222,13 @@ class Generator:
# of their definition). # of their definition).
attrs = astClasses[moduleElem.__name__].attributes attrs = astClasses[moduleElem.__name__].attributes
if appyType == 'class': if appyType == 'class':
# Determine the class type (standard, tool, flavour...) # Determine the class type (standard, tool, user...)
if issubclass(moduleElem, Tool): if issubclass(moduleElem, Tool):
if not self.tool: if not self.tool:
klass = self.descriptorClasses['tool'] klass = self.descriptorClasses['tool']
self.tool = klass(moduleElem, attrs, self) self.tool = klass(moduleElem, attrs, self)
else: else:
self.tool.update(moduleElem, attrs) self.tool.update(moduleElem, attrs)
elif issubclass(moduleElem, Flavour):
if not self.flavour:
klass = self.descriptorClasses['flavour']
self.flavour = klass(moduleElem, attrs, self)
else:
self.flavour.update(moduleElem, attrs)
elif issubclass(moduleElem, User): elif issubclass(moduleElem, User):
if not self.user: if not self.user:
klass = self.descriptorClasses['user'] klass = self.descriptorClasses['user']

View file

@ -6,7 +6,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types, copy import types, copy
from model import ModelClass, Flavour, flavourAttributePrefixes from model import ModelClass, Tool, toolFieldPrefixes
from utils import stringify from utils import stringify
import appy.gen import appy.gen
import appy.gen.descriptors import appy.gen.descriptors
@ -43,12 +43,13 @@ class FieldDescriptor:
def __repr__(self): def __repr__(self):
return '<Field %s, %s>' % (self.fieldName, self.classDescr) return '<Field %s, %s>' % (self.fieldName, self.classDescr)
def getFlavourAttributeMessage(self, fieldName): def getToolFieldMessage(self, fieldName):
'''Some attributes generated on the Flavour class need a specific '''Some attributes generated on the Tool class need a specific
default message, returned by this method.''' default message, returned by this method.'''
res = fieldName res = fieldName
for prefix in flavourAttributePrefixes: for prefix in toolFieldPrefixes:
if fieldName.startswith(prefix): fullPrefix = prefix + 'For'
if fieldName.startswith(fullPrefix):
messageId = 'MSG_%s' % prefix messageId = 'MSG_%s' % prefix
res = getattr(PoMessage, messageId) res = getattr(PoMessage, messageId)
if res.find('%s') != -1: if res.find('%s') != -1:
@ -66,8 +67,8 @@ class FieldDescriptor:
produceNice = True produceNice = True
default = self.fieldName default = self.fieldName
# Some attributes need a specific predefined message # Some attributes need a specific predefined message
if isinstance(self.classDescr, FlavourClassDescriptor): if isinstance(self.classDescr, ToolClassDescriptor):
default = self.getFlavourAttributeMessage(self.fieldName) default = self.getToolFieldMessage(self.fieldName)
if default != self.fieldName: produceNice = False if default != self.fieldName: produceNice = False
msg = PoMessage(msgId, '', default) msg = PoMessage(msgId, '', default)
if produceNice: if produceNice:
@ -88,9 +89,7 @@ class FieldDescriptor:
self.generator.labels.append(poMsg) self.generator.labels.append(poMsg)
def walkAction(self): def walkAction(self):
'''How to generate an action field ? We generate an Archetypes String '''Generates the i18n-related labels.'''
field.'''
# Add action-specific i18n messages
for suffix in ('ok', 'ko'): for suffix in ('ok', 'ko'):
label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName, label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName,
suffix) suffix)
@ -98,6 +97,10 @@ class FieldDescriptor:
getattr(PoMessage, 'ACTION_%s' % suffix.upper())) getattr(PoMessage, 'ACTION_%s' % suffix.upper()))
self.generator.labels.append(msg) self.generator.labels.append(msg)
self.classDescr.labelsToPropagate.append(msg) self.classDescr.labelsToPropagate.append(msg)
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): def walkRef(self):
'''How to generate a Ref?''' '''How to generate a Ref?'''
@ -115,6 +118,11 @@ class FieldDescriptor:
poMsg = PoMessage(backLabel, '', self.appyType.back.attribute) poMsg = PoMessage(backLabel, '', self.appyType.back.attribute)
poMsg.produceNiceDefault() poMsg.produceNiceDefault()
self.generator.labels.append(poMsg) 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): def walkPod(self):
# Add i18n-specific messages # Add i18n-specific messages
@ -123,8 +131,8 @@ class FieldDescriptor:
msg = PoMessage(label, '', PoMessage.POD_ASKACTION) msg = PoMessage(label, '', PoMessage.POD_ASKACTION)
self.generator.labels.append(msg) self.generator.labels.append(msg)
self.classDescr.labelsToPropagate.append(msg) self.classDescr.labelsToPropagate.append(msg)
# Add the POD-related fields on the Flavour # Add the POD-related fields on the Tool
Flavour._appy_addPodRelatedFields(self) Tool._appy_addPodRelatedFields(self)
notToValidateFields = ('Info', 'Computed', 'Action', 'Pod') notToValidateFields = ('Info', 'Computed', 'Action', 'Pod')
def walkAppyType(self): def walkAppyType(self):
@ -133,10 +141,10 @@ class FieldDescriptor:
# Manage things common to all Appy types # Manage things common to all Appy types
# - optional ? # - optional ?
if self.appyType.optional: if self.appyType.optional:
Flavour._appy_addOptionalField(self) Tool._appy_addOptionalField(self)
# - edit default value ? # - edit default value ?
if self.appyType.editDefault: if self.appyType.editDefault:
Flavour._appy_addDefaultField(self) Tool._appy_addDefaultField(self)
# - put an index on this field? # - put an index on this field?
if self.appyType.indexed and \ if self.appyType.indexed and \
(self.fieldName not in ('title', 'description')): (self.fieldName not in ('title', 'description')):
@ -229,8 +237,8 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
# (because they contain the class name). But at this time we don't know # (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 # yet every sub-class. So we store those labels here; the Generator
# will propagate them later. # will propagate them later.
self.flavourFieldsToPropagate = [] # For this class, some fields have self.toolFieldsToPropagate = [] # For this class, some fields have
# been defined on the Flavour class. Those fields need to be defined # 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 # 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 # know yet every sub-class. So we store field definitions here; the
# Generator will propagate them later. # Generator will propagate them later.
@ -251,7 +259,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
def generateSchema(self, configClass=False): def generateSchema(self, configClass=False):
'''Generates the corresponding Archetypes schema in self.schema. If we '''Generates the corresponding Archetypes schema in self.schema. If we
are generating a schema for a class that is in the configuration are generating a schema for a class that is in the configuration
(tool, flavour, etc) we must avoid having attributes that rely on (tool, user, etc) we must avoid having attributes that rely on
the configuration (ie attributes that are optional, with the configuration (ie attributes that are optional, with
editDefault=True, etc).''' editDefault=True, etc).'''
for attrName in self.orderedAttributes: for attrName in self.orderedAttributes:
@ -286,13 +294,6 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
res = self.klass.__dict__['root'] res = self.klass.__dict__['root']
return res return res
def isPod(self):
'''May this class be associated with POD templates?.'''
res = False
if self.klass.__dict__.has_key('pod') and self.klass.__dict__['pod']:
res = True
return res
def isFolder(self, klass=None): def isFolder(self, klass=None):
'''Must self.klass be a folder? If klass is not None, this method tests '''Must self.klass be a folder? If klass is not None, this method tests
it on p_klass instead of self.klass.''' it on p_klass instead of self.klass.'''
@ -375,6 +376,7 @@ class ToolClassDescriptor(ClassDescriptor):
'''Represents the POD-specific fields that must be added to the tool.''' '''Represents the POD-specific fields that must be added to the tool.'''
def __init__(self, klass, generator): def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator) ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.attributesByClass = klass._appy_classes
self.modelClass = self.klass self.modelClass = self.klass
self.predefined = True self.predefined = True
self.customized = False self.customized = False
@ -395,42 +397,6 @@ class ToolClassDescriptor(ClassDescriptor):
def generateSchema(self): def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True) ClassDescriptor.generateSchema(self, configClass=True)
class FlavourClassDescriptor(ClassDescriptor):
'''Represents an Archetypes-compliant class that corresponds to the Flavour
for the generated application.'''
def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.attributesByClass = klass._appy_classes
self.modelClass = self.klass
self.predefined = True
self.customized = False
def getParents(self, allClasses=()):
res = ['Flavour']
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 flavour
definition. We must then add the custom flavour elements in this
default Flavour 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)
class PodTemplateClassDescriptor(ClassDescriptor):
'''Represents a POD template.'''
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 ['PodTemplate']
def isRoot(self): return False
class UserClassDescriptor(ClassDescriptor): class UserClassDescriptor(ClassDescriptor):
'''Represents an Archetypes-compliant class that corresponds to the User '''Represents an Archetypes-compliant class that corresponds to the User
for the generated application.''' for the generated application.'''

View file

@ -8,11 +8,8 @@ from appy.gen import *
from appy.gen.po import PoMessage, PoFile, PoParser from appy.gen.po import PoMessage, PoFile, PoParser
from appy.gen.generator import Generator as AbstractGenerator from appy.gen.generator import Generator as AbstractGenerator
from appy.gen.utils import getClassName from appy.gen.utils import getClassName
from model import ModelClass, PodTemplate, User, Flavour, Tool from model import ModelClass, User, Tool
from descriptors import FieldDescriptor, ClassDescriptor, \ from descriptors import *
WorkflowDescriptor, ToolClassDescriptor, \
FlavourClassDescriptor, PodTemplateClassDescriptor, \
UserClassDescriptor
# Common methods that need to be defined on every Archetype class -------------- # Common methods that need to be defined on every Archetype class --------------
COMMON_METHODS = ''' COMMON_METHODS = '''
@ -29,22 +26,18 @@ class Generator(AbstractGenerator):
poExtensions = ('.po', '.pot') poExtensions = ('.po', '.pot')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
Flavour._appy_clean() Tool._appy_clean()
AbstractGenerator.__init__(self, *args, **kwargs) AbstractGenerator.__init__(self, *args, **kwargs)
# Set our own Descriptor classes # Set our own Descriptor classes
self.descriptorClasses['class'] = ClassDescriptor self.descriptorClasses['class'] = ClassDescriptor
self.descriptorClasses['workflow'] = WorkflowDescriptor self.descriptorClasses['workflow'] = WorkflowDescriptor
# Create our own Tool, Flavour and PodTemplate instances # Create our own Tool and User instances
self.tool = ToolClassDescriptor(Tool, self) self.tool = ToolClassDescriptor(Tool, self)
self.flavour = FlavourClassDescriptor(Flavour, self)
self.podTemplate = PodTemplateClassDescriptor(PodTemplate, self)
self.user = UserClassDescriptor(User, self) self.user = UserClassDescriptor(User, self)
# i18n labels to generate # i18n labels to generate
self.labels = [] # i18n labels self.labels = [] # i18n labels
self.toolName = '%sTool' % self.applicationName self.toolName = '%sTool' % self.applicationName
self.flavourName = '%sFlavour' % self.applicationName
self.toolInstanceName = 'portal_%s' % self.applicationName.lower() self.toolInstanceName = 'portal_%s' % self.applicationName.lower()
self.podTemplateName = '%sPodTemplate' % self.applicationName
self.userName = '%sUser' % self.applicationName self.userName = '%sUser' % self.applicationName
self.portletName = '%s_portlet' % self.applicationName.lower() self.portletName = '%s_portlet' % self.applicationName.lower()
self.queryName = '%s_query' % self.applicationName.lower() self.queryName = '%s_query' % self.applicationName.lower()
@ -55,10 +48,9 @@ class Generator(AbstractGenerator):
commonMethods = COMMON_METHODS % \ commonMethods = COMMON_METHODS % \
(self.toolInstanceName, self.applicationName) (self.toolInstanceName, self.applicationName)
self.repls.update( self.repls.update(
{'toolName': self.toolName, 'flavourName': self.flavourName, {'toolName': self.toolName, 'portletName': self.portletName,
'portletName': self.portletName, 'queryName': self.queryName, 'queryName': self.queryName, 'userName': self.userName,
'toolInstanceName': self.toolInstanceName, 'toolInstanceName': self.toolInstanceName,
'podTemplateName': self.podTemplateName, 'userName': self.userName,
'commonMethods': commonMethods}) 'commonMethods': commonMethods})
self.referers = {} self.referers = {}
@ -145,7 +137,6 @@ class Generator(AbstractGenerator):
msg('goto_last', '', msg.GOTO_LAST), msg('goto_last', '', msg.GOTO_LAST),
msg('goto_source', '', msg.GOTO_SOURCE), msg('goto_source', '', msg.GOTO_SOURCE),
msg('whatever', '', msg.WHATEVER), msg('whatever', '', msg.WHATEVER),
msg('confirm', '', msg.CONFIRM),
msg('yes', '', msg.YES), msg('yes', '', msg.YES),
msg('no', '', msg.NO), msg('no', '', msg.NO),
msg('field_required', '', msg.FIELD_REQUIRED), msg('field_required', '', msg.FIELD_REQUIRED),
@ -321,15 +312,14 @@ class Generator(AbstractGenerator):
appClasses.append('%s.%s' % (k.__module__, k.__name__)) appClasses.append('%s.%s' % (k.__module__, k.__name__))
repls['appClasses'] = "[%s]" % ','.join(appClasses) repls['appClasses'] = "[%s]" % ','.join(appClasses)
# Compute lists of class names # Compute lists of class names
allClassNames = '"%s",' % self.flavourName allClassNames = '"%s",' % self.userName
allClassNames += '"%s",' % self.podTemplateName
appClassNames = ','.join(['"%s"' % c.name for c in self.classes]) appClassNames = ','.join(['"%s"' % c.name for c in self.classes])
allClassNames += appClassNames allClassNames += appClassNames
repls['allClassNames'] = allClassNames repls['allClassNames'] = allClassNames
repls['appClassNames'] = appClassNames repls['appClassNames'] = appClassNames
# Compute classes whose instances must not be catalogued. # Compute classes whose instances must not be catalogued.
catalogMap = '' catalogMap = ''
blackClasses = [self.toolName, self.flavourName, self.podTemplateName] blackClasses = [self.toolName]
for blackClass in blackClasses: for blackClass in blackClasses:
catalogMap += "catalogMap['%s'] = {}\n" % blackClass catalogMap += "catalogMap['%s'] = {}\n" % blackClass
catalogMap += "catalogMap['%s']['black'] = " \ catalogMap += "catalogMap['%s']['black'] = " \
@ -464,28 +454,30 @@ class Generator(AbstractGenerator):
repls['workflows'] = workflows repls['workflows'] = workflows
self.copyFile('workflows.py', repls, destFolder='Extensions') self.copyFile('workflows.py', repls, destFolder='Extensions')
def generateWrapperProperty(self, name): def generateWrapperProperty(self, name, type):
'''Generates the getter for attribute p_name.''' '''Generates the getter for attribute p_name.'''
res = ' def get_%s(self):\n ' % name res = ' def get_%s(self):\n ' % name
if name == 'title': if name == 'title':
res += 'return self.o.Title()\n' res += 'return self.o.Title()\n'
else: else:
res += 'return self.o.getAppyType("%s").getValue(self.o)\n' % name suffix = ''
if type == 'Ref': suffix = ', noListIfSingleObj=True'
res += 'return self.o.getAppyType("%s").getValue(self.o%s)\n' % \
(name, suffix)
res += ' %s = property(get_%s)\n\n' % (name, name) res += ' %s = property(get_%s)\n\n' % (name, name)
return res return res
def getClasses(self, include=None): def getClasses(self, include=None):
'''Returns the descriptors for all the classes in the generated '''Returns the descriptors for all the classes in the generated
gen-application. If p_include is "all", it includes the descriptors gen-application. If p_include is "all", it includes the descriptors
for the config-related classes (tool, flavour, etc); if for the config-related classes (tool, user, etc); if p_include is
p_include is "allButTool", it includes the same descriptors, the "allButTool", it includes the same descriptors, the tool excepted;
tool excepted; if p_include is "custom", it includes descriptors if p_include is "custom", it includes descriptors for the
for the config-related classes for which the user has created a config-related classes for which the user has created a sub-class.'''
sub-class.'''
if not include: return self.classes if not include: return self.classes
else: else:
res = self.classes[:] res = self.classes[:]
configClasses = [self.tool,self.flavour,self.podTemplate,self.user] configClasses = [self.tool, self.user]
if include == 'all': if include == 'all':
res += configClasses res += configClasses
elif include == 'allButTool': elif include == 'allButTool':
@ -547,24 +539,25 @@ class Generator(AbstractGenerator):
except AttributeError: except AttributeError:
attrValue = getattr(c.modelClass, attrName) attrValue = getattr(c.modelClass, attrName)
if isinstance(attrValue, Type): if isinstance(attrValue, Type):
wrapperDef += self.generateWrapperProperty(attrName) wrapperDef += self.generateWrapperProperty(attrName,
attrValue.type)
# Generate properties for back references # Generate properties for back references
if self.referers.has_key(c.name): if self.referers.has_key(c.name):
for refDescr, rel in self.referers[c.name]: for refDescr, rel in self.referers[c.name]:
attrName = refDescr.appyType.back.attribute attrName = refDescr.appyType.back.attribute
wrapperDef += self.generateWrapperProperty(attrName) wrapperDef += self.generateWrapperProperty(attrName, 'Ref')
if not titleFound: if not titleFound:
# Implicitly, the title will be added by Archetypes. So I need # Implicitly, the title will be added by Archetypes. So I need
# to define a property for it. # to define a property for it.
wrapperDef += self.generateWrapperProperty('title') wrapperDef += self.generateWrapperProperty('title', 'String')
if c.customized: if c.customized:
# For custom tool and flavour, add a call to a method that # For custom tool, add a call to a method that allows to
# allows to customize elements from the base class. # customize elements from the base class.
wrapperDef += " if hasattr(%s, 'update'):\n " \ wrapperDef += " if hasattr(%s, 'update'):\n " \
"%s.update(%s)\n" % (parentClasses[1], parentClasses[1], "%s.update(%s)\n" % (parentClasses[1], parentClasses[1],
parentClasses[0]) parentClasses[0])
# For custom tool and flavour, add security declaration that # For custom tool, add security declaration that will allow to
# will allow to call their methods from ZPTs. # call their methods from ZPTs.
for parentClass in parentClasses: for parentClass in parentClasses:
wrapperDef += " for elem in dir(%s):\n " \ wrapperDef += " for elem in dir(%s):\n " \
"if not elem.startswith('_'): security.declarePublic" \ "if not elem.startswith('_'): security.declarePublic" \
@ -576,8 +569,6 @@ class Generator(AbstractGenerator):
repls['imports'] = '\n'.join(imports) repls['imports'] = '\n'.join(imports)
repls['wrappers'] = '\n'.join(wrappers) repls['wrappers'] = '\n'.join(wrappers)
repls['toolBody'] = Tool._appy_getBody() repls['toolBody'] = Tool._appy_getBody()
repls['flavourBody'] = Flavour._appy_getBody()
repls['podTemplateBody'] = PodTemplate._appy_getBody()
repls['userBody'] = User._appy_getBody() repls['userBody'] = User._appy_getBody()
self.copyFile('appyWrappers.py', repls, destFolder='Extensions') self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
@ -613,72 +604,49 @@ class Generator(AbstractGenerator):
def generateTool(self): def generateTool(self):
'''Generates the Plone tool that corresponds to this application.''' '''Generates the Plone tool that corresponds to this application.'''
# Generate the tool class in itself and related i18n messages
t = self.toolName
Msg = PoMessage Msg = PoMessage
repls = self.repls.copy() # Create Tool-related i18n-related messages
# Manage predefined fields self.labels += [
Tool.flavours.klass = Flavour Msg(self.toolName, '', Msg.CONFIG % self.applicationName),
if self.flavour.customized: Msg('%s_edit_descr' % self.toolName, '', ' ')]
Tool.flavours.klass = self.flavour.klass
# Tune the Ref field between Tool and User
Tool.users.klass = User Tool.users.klass = User
if self.user.customized: if self.user.customized:
Tool.users.klass = self.user.klass Tool.users.klass = self.user.klass
# Before generating the Tool class, finalize it with query result
# columns, with fields to propagate, workflow-related fields.
for classDescr in self.classes:
for fieldName, fieldType in classDescr.toolFieldsToPropagate:
for childDescr in classDescr.getChildren():
childFieldName = fieldName % childDescr.name
fieldType.group = childDescr.klass.__name__
Tool._appy_addField(childFieldName, fieldType, childDescr)
if classDescr.isRoot():
# We must be able to configure query results from the tool.
Tool._appy_addQueryResultColumns(classDescr)
# Add the search-related fields.
Tool._appy_addSearchRelatedFields(classDescr)
importMean = classDescr.getCreateMean('Import')
if importMean:
Tool._appy_addImportRelatedFields(classDescr)
Tool._appy_addWorkflowFields(self.user)
# Complete self.tool.orderedAttributes from the attributes that we
# just added to the Tool model class.
for fieldName in Tool._appy_attributes:
if fieldName not in self.tool.orderedAttributes:
self.tool.orderedAttributes.append(fieldName)
self.tool.generateSchema() self.tool.generateSchema()
# Generate the Tool class
repls = self.repls.copy()
repls['metaTypes'] = [c.name for c in self.classes]
repls['fields'] = self.tool.schema repls['fields'] = self.tool.schema
repls['methods'] = self.tool.methods repls['methods'] = self.tool.methods
repls['wrapperClass'] = '%s_Wrapper' % self.tool.name repls['wrapperClass'] = '%s_Wrapper' % self.tool.name
self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName) self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName)
repls = self.repls.copy()
# Create i18n-related messages
self.labels += [
Msg(self.toolName, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.toolName, '', ' ')]
# Before generating the Flavour class, finalize it with query result
# columns, with fields to propagate, workflow-related fields.
for classDescr in self.classes:
for fieldName, fieldType in classDescr.flavourFieldsToPropagate:
for childDescr in classDescr.getChildren():
childFieldName = fieldName % childDescr.name
fieldType.group = childDescr.klass.__name__
Flavour._appy_addField(childFieldName,fieldType,childDescr)
if classDescr.isRoot():
# We must be able to configure query results from the flavour.
Flavour._appy_addQueryResultColumns(classDescr)
# Add the search-related fields.
Flavour._appy_addSearchRelatedFields(classDescr)
importMean = classDescr.getCreateMean('Import')
if importMean:
Flavour._appy_addImportRelatedFields(classDescr)
Flavour._appy_addWorkflowFields(self.flavour)
Flavour._appy_addWorkflowFields(self.podTemplate)
Flavour._appy_addWorkflowFields(self.user)
# Complete self.flavour.orderedAttributes from the attributes that we
# just added to the Flavour model class.
for fieldName in Flavour._appy_attributes:
if fieldName not in self.flavour.orderedAttributes:
self.flavour.orderedAttributes.append(fieldName)
# Generate the flavour class and related i18n messages
self.flavour.generateSchema()
self.labels += [ Msg(self.flavourName, '', Msg.FLAVOUR),
Msg('%s_edit_descr' % self.flavourName, '', ' ')]
repls = self.repls.copy()
repls['fields'] = self.flavour.schema
repls['methods'] = self.flavour.methods
repls['wrapperClass'] = '%s_Wrapper' % self.flavour.name
repls['metaTypes'] = [c.name for c in self.classes]
self.copyFile('FlavourTemplate.py', repls,
destName='%s.py'% self.flavourName)
# Generate the PodTemplate class
self.podTemplate.generateSchema()
self.labels += [ Msg(self.podTemplateName, '', Msg.POD_TEMPLATE),
Msg('%s_edit_descr' % self.podTemplateName, '', ' ')]
repls = self.repls.copy()
repls['fields'] = self.podTemplate.schema
repls['methods'] = self.podTemplate.methods
repls['wrapperClass'] = '%s_Wrapper' % self.podTemplate.name
self.copyFile('PodTemplate.py', repls,
destName='%s.py' % self.podTemplateName)
# Generate the User class # Generate the User class
self.user.generateSchema() self.user.generateSchema()
self.labels += [ Msg(self.userName, '', Msg.USER), self.labels += [ Msg(self.userName, '', Msg.USER),
@ -687,32 +655,28 @@ class Generator(AbstractGenerator):
repls['fields'] = self.user.schema repls['fields'] = self.user.schema
repls['methods'] = self.user.methods repls['methods'] = self.user.methods
repls['wrapperClass'] = '%s_Wrapper' % self.user.name repls['wrapperClass'] = '%s_Wrapper' % self.user.name
self.copyFile('UserTemplate.py', repls, self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName)
destName='%s.py' % self.userName)
def generateClass(self, classDescr): def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for '''Is called each time an Appy class is found in the application, for
generating the corresponding Archetype class and schema.''' generating the corresponding Archetype class and schema.'''
k = classDescr.klass k = classDescr.klass
print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__) print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__)
# Add, for this class, the needed configuration attributes on Flavour
if classDescr.isPod():
Flavour._appy_addPodField(classDescr)
if not classDescr.isAbstract(): if not classDescr.isAbstract():
Flavour._appy_addWorkflowFields(classDescr) Tool._appy_addWorkflowFields(classDescr)
# Determine base archetypes schema and class # Determine base archetypes schema and class
baseClass = 'BaseContent' baseClass = 'BaseContent'
baseSchema = 'BaseSchema' baseSchema = 'BaseSchema'
if classDescr.isFolder(): if classDescr.isFolder():
baseClass = 'OrderedBaseFolder' baseClass = 'OrderedBaseFolder'
baseSchema = 'OrderedBaseFolderSchema' baseSchema = 'OrderedBaseFolderSchema'
parents = [baseClass, 'ClassMixin'] parents = [baseClass, 'BaseMixin']
imports = [] imports = []
implements = [baseClass] implements = [baseClass]
for baseClass in classDescr.klass.__bases__: for baseClass in classDescr.klass.__bases__:
if self.determineAppyType(baseClass) == 'class': if self.determineAppyType(baseClass) == 'class':
bcName = getClassName(baseClass) bcName = getClassName(baseClass)
parents.remove('ClassMixin') parents.remove('BaseMixin')
parents.append(bcName) parents.append(bcName)
implements.append(bcName) implements.append(bcName)
imports.append('from %s import %s' % (bcName, bcName)) imports.append('from %s import %s' % (bcName, bcName))
@ -750,19 +714,6 @@ class Generator(AbstractGenerator):
classDescr.klass.__name__+'s') classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault() poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl) self.labels.append(poMsgPl)
# Create i18n labels for flavoured variants
for i in range(2, self.config.numberOfFlavours+1):
poMsg = PoMessage('%s_%d' % (classDescr.name, i), '',
classDescr.klass.__name__)
poMsg.produceNiceDefault()
self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i),
'', ' ')
self.labels.append(poMsgDescr)
poMsgPl = PoMessage('%s_%d_plural' % (classDescr.name, i), '',
classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl)
# Create i18n labels for searches # Create i18n labels for searches
for search in classDescr.getSearches(classDescr.klass): for search in classDescr.getSearches(classDescr.klass):
searchLabel = '%s_search_%s' % (classDescr.name, search.name) searchLabel = '%s_search_%s' % (classDescr.name, search.name)

View file

@ -144,7 +144,6 @@ class PloneInstaller:
appFolder = getattr(site, self.productName) appFolder = getattr(site, self.productName)
for className in self.config.rootClasses: for className in self.config.rootClasses:
permission = self.getAddPermission(className) permission = self.getAddPermission(className)
print 'Permission is', permission
appFolder.manage_permission(permission, (), acquire=0) appFolder.manage_permission(permission, (), acquire=0)
else: else:
appFolder = getattr(site, self.productName) appFolder = getattr(site, self.productName)
@ -221,78 +220,33 @@ class PloneInstaller:
current_catalogs.remove(catalog) current_catalogs.remove(catalog)
atTool.setCatalogsByType(meta_type, list(current_catalogs)) atTool.setCatalogsByType(meta_type, list(current_catalogs))
def findPodFile(self, klass, podTemplateName):
'''Finds the file that corresponds to p_podTemplateName for p_klass.'''
res = None
exec 'import %s' % klass.__module__
exec 'moduleFile = %s.__file__' % klass.__module__
folderName = os.path.dirname(moduleFile)
fileName = os.path.join(folderName, '%s.odt' % podTemplateName)
if os.path.isfile(fileName):
res = fileName
return res
def updatePodTemplates(self): def updatePodTemplates(self):
'''Creates or updates the POD templates in flavours according to pod '''Creates or updates the POD templates in the tool according to pod
declarations in the application classes.''' declarations in the application classes.'''
# Creates or updates the old-way class-related templates # Creates the templates for Pod fields if they do not exist.
i = -1
for klass in self.appClasses:
i += 1
if klass.__dict__.has_key('pod'):
pod = getattr(klass, 'pod')
if isinstance(pod, bool):
podTemplates = [klass.__name__]
else:
podTemplates = pod
for templateName in podTemplates:
fileName = self.findPodFile(klass, templateName)
if fileName:
# Create the corresponding PodTemplate in all flavours
for flavour in self.appyTool.flavours:
podId='%s_%s' % (self.appClassNames[i],templateName)
podAttr = 'podTemplatesFor%s'% self.appClassNames[i]
allPodTemplates = getattr(flavour, podAttr)
if allPodTemplates:
if isinstance(allPodTemplates, list):
allIds = [p.id for p in allPodTemplates]
else:
allIds = [allPodTemplates.id]
else:
allIds = []
if podId not in allIds:
# Create a PodTemplate instance
f = file(fileName)
flavour.create(podAttr, id=podId, podTemplate=f,
title=produceNiceMessage(templateName))
f.close()
# Creates the new-way templates for Pod fields if they do not exist.
for contentType, appyTypes in self.attributes.iteritems(): for contentType, appyTypes in self.attributes.iteritems():
appyClass = self.tool.getAppyClass(contentType) appyClass = self.tool.getAppyClass(contentType)
if not appyClass: continue # May be an abstract class if not appyClass: continue # May be an abstract class
for appyType in appyTypes: for appyType in appyTypes:
if appyType.type == 'Pod': if appyType.type != 'Pod': continue
# For every flavour, find the attribute that stores the # Find the attribute that stores the template, and store on
# template, and store on it the default one specified in # it the default one specified in the appyType if no
# the appyType if no template is stored yet. # template is stored yet.
for flavour in self.appyTool.flavours: attrName = self.appyTool.getAttributeName(
attrName = flavour.getAttributeName( 'podTemplate', appyClass, appyType.name)
'podTemplate', appyClass, appyType.name) fileObject = getattr(self.appyTool, attrName)
fileObject = getattr(flavour, attrName) if not fileObject or (fileObject.size == 0):
if not fileObject or (fileObject.size == 0): # There is no file. Put the one specified in the appyType.
# There is no file. Put the one specified in the fileName = os.path.join(self.appyTool.getDiskFolder(),
# appyType. appyType.template)
fileName=os.path.join(self.appyTool.getDiskFolder(), if os.path.exists(fileName):
appyType.template) setattr(self.appyTool, attrName, fileName)
if os.path.exists(fileName): else:
setattr(flavour, attrName, fileName) self.appyTool.log('Template "%s" was not found!' % \
else: fileName, type='error')
self.appyTool.log(
'Template "%s" was not found!' % \
fileName, type='error')
def installTool(self): def installTool(self):
'''Configures the application tool and flavours.''' '''Configures the application tool.'''
# Register the tool in Plone # Register the tool in Plone
try: try:
self.ploneSite.manage_addProduct[ self.ploneSite.manage_addProduct[
@ -333,9 +287,6 @@ class PloneInstaller:
else: else:
self.tool.createOrUpdate(True, None) self.tool.createOrUpdate(True, None)
if not self.appyTool.flavours:
# Create the default flavour
self.appyTool.create('flavours', title=self.productName, number=1)
self.updatePodTemplates() self.updatePodTemplates()
# Uncatalog tool # Uncatalog tool

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class ClassMixin(AbstractMixin):
_appy_meta_type = 'Class'
# ------------------------------------------------------------------------------

View file

@ -1,263 +0,0 @@
# ------------------------------------------------------------------------------
import os, os.path, time, types
from StringIO import StringIO
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen
from appy.gen import Type
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.descriptors import ClassDescriptor
# Errors -----------------------------------------------------------------------
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
POD_ERROR = 'An error occurred while generating the document. Please ' \
'contact the system administrator.'
# ------------------------------------------------------------------------------
class FlavourMixin(AbstractMixin):
_appy_meta_type = 'Flavour'
def getPortalType(self, metaTypeOrAppyClass):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
return self.getParentNode().getPortalType(metaTypeOrAppyClass)
def registerPortalTypes(self):
'''Registers, into portal_types, the portal types which are specific
to this flavour.'''
i = -1
registeredFactoryTypes = self.portal_factory.getFactoryTypes().keys()
factoryTypesToRegister = []
appName = self.getProductConfig().PROJECTNAME
for metaTypeName in self.allMetaTypes:
i += 1
portalTypeName = '%s_%d' % (metaTypeName, self.number)
# If the portal type corresponding to the meta type is
# registered in portal_factory (in the model:
# use_portal_factory=True), we must also register the new
# portal_type we are currently creating.
if metaTypeName in registeredFactoryTypes:
factoryTypesToRegister.append(portalTypeName)
if not hasattr(self.portal_types, portalTypeName) and \
hasattr(self.portal_types, metaTypeName):
# Indeed abstract meta_types have no associated portal_type
typeInfoName = "%s: %s (%s)" % (appName, metaTypeName,
metaTypeName)
self.portal_types.manage_addTypeInformation(
getattr(self.portal_types, metaTypeName).meta_type,
id=portalTypeName, typeinfo_name=typeInfoName)
# Set the human readable title explicitly
portalType = getattr(self.portal_types, portalTypeName)
portalType.title = portalTypeName
# Associate a workflow for this new portal type.
pf = self.portal_workflow
workflowChain = pf.getChainForPortalType(metaTypeName)
pf.setChainForPortalTypes([portalTypeName],workflowChain)
# Copy actions from the base portal type
basePortalType = getattr(self.portal_types, metaTypeName)
portalType._actions = tuple(basePortalType._cloneActions())
# Copy aliases from the base portal type
portalType.setMethodAliases(basePortalType.getMethodAliases())
# Update the factory tool with the list of types to register
self.portal_factory.manage_setPortalFactoryTypes(
listOfTypeIds=factoryTypesToRegister+registeredFactoryTypes)
def getClassFolder(self, className):
'''Return the folder related to p_className.'''
return getattr(self, className)
def getAvailablePodTemplates(self, obj, phase='main'):
'''Returns the POD templates which are available for generating a
document from p_obj.'''
appySelf = self.appy()
fieldName = 'podTemplatesFor%s' % obj.meta_type
res = []
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res = [r.o for r in podTemplates if r.podPhase == phase]
hasParents = True
klass = obj.__class__
while hasParents:
parent = klass.__bases__[-1]
if hasattr(parent, 'wrapperClass'):
fieldName = 'podTemplatesFor%s' % parent.meta_type
podTemplates = getattr(appySelf, fieldName, [])
if not isinstance(podTemplates, list):
podTemplates = [podTemplates]
res += [r.o for r in podTemplates if r.podPhase == phase]
klass = parent
else:
hasParents = False
return res
def getMaxShownTemplates(self, obj):
attrName = 'getPodMaxShownTemplatesFor%s' % obj.meta_type
return getattr(self, attrName)()
def getPodInfo(self, ploneObj, fieldName):
'''Returns POD-related information about Pod field p_fieldName defined
on class whose p_ploneObj is an instance of.'''
res = {}
appyClass = self.getParentNode().getAppyClass(ploneObj.meta_type)
appyFlavour = self.appy()
n = appyFlavour.getAttributeName('formats', appyClass, fieldName)
res['formats'] = getattr(appyFlavour, n)
n = appyFlavour.getAttributeName('podTemplate', appyClass, fieldName)
res['template'] = getattr(appyFlavour, n)
appyType = ploneObj.getAppyType(fieldName)
res['title'] = self.translate(appyType.labelId)
res['context'] = appyType.context
res['action'] = appyType.action
return res
def generateDocument(self):
'''Generates the document:
- from a PodTemplate instance if it is a class-wide pod template;
- from field-related info on the flavour if it is a Pod field.
UID of object that is the template target is given in the request.'''
rq = self.REQUEST
appyTool = self.getParentNode().appy()
# Get the object
objectUid = rq.get('objectUid')
obj = self.uid_catalog(UID=objectUid)[0].getObject()
appyObj = obj.appy()
# Get information about the document to render. Information comes from
# a PodTemplate instance or from the flavour itself, depending on
# whether we generate a doc from a class-wide template or from a pod
# field.
templateUid = rq.get('templateUid', None)
specificPodContext = None
if templateUid:
podTemplate = self.uid_catalog(UID=templateUid)[0].getObject()
appyPt = podTemplate.appy()
format = podTemplate.getPodFormat()
template = appyPt.podTemplate.content
podTitle = podTemplate.Title()
doAction = False
else:
fieldName = rq.get('fieldName')
format = rq.get('podFormat')
podInfo = self.getPodInfo(obj, fieldName)
template = podInfo['template'].content
podTitle = podInfo['title']
if podInfo['context']:
if type(podInfo['context']) == types.FunctionType:
specificPodContext = podInfo['context'](appyObj)
else:
specificPodContext = podInfo['context']
doAction = rq.get('askAction') == 'True'
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.UID(), time.time(), format)
# Define parameters to pass to the appy.pod renderer
currentUser = self.portal_membership.getAuthenticatedMember()
podContext = {'tool': appyTool, 'flavour': self.appy(),
'user': currentUser, 'self': appyObj,
'now': self.getProductConfig().DateTime(),
'projectFolder': appyTool.getDiskFolder(),
}
if specificPodContext:
podContext.update(specificPodContext)
if templateUid:
podContext['podTemplate'] = appyPt
rendererParams = {'template': StringIO(template),
'context': podContext,
'result': tempFileName}
if appyTool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython
if appyTool.openOfficePort:
rendererParams['ooPort'] = appyTool.openOfficePort
# Launch the renderer
try:
renderer = Renderer(**rendererParams)
renderer.run()
except appy.pod.PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
appyTool.log(str(pe), type='error')
appyTool.say(POD_ERROR)
return self.goto(rq.get('HTTP_REFERER'))
# Open the temp file on the filesystem
f = file(tempFileName, 'rb')
res = f.read()
# Identify the filename to return
fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle)
fileName = appyTool.normalize(fileName)
response = obj.REQUEST.RESPONSE
response.setHeader('Content-Type', mimeTypes[format])
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\
% (fileName, format))
f.close()
# Execute the related action if relevant
if doAction and podInfo['action']:
podInfo['action'](appyObj, podContext)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning')
except IOError, ie:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res
def getAttr(self, name):
'''Gets on this flavour attribute named p_attrName. Useful because we
can't use getattr directly in Zope Page Templates.'''
return getattr(self.appy(), name, None)
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.name != 'title': # Will be included by default.
label = '%s_%s' % (contentType, appyType.name)
res.append((appyType.name, self.translate(label)))
# Add object state
res.append(('workflowState', 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.getProductConfig().attributes[contentType]:
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
return res
def getSearchableFields(self, contentType):
'''Returns, among the list of all searchable fields (see method above),
the list of fields that the user has configured in the flavour as
being effectively used in the search screen.'''
res = []
fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ())
for name in fieldNames:
appyType = self.getAppyType(name, asDict=True,className=contentType)
res.append(appyType)
return res
def getImportElements(self, contentType):
'''Returns the list of elements that can be imported from p_path for
p_contentType.'''
tool = self.getParentNode()
appyClass = tool.getAppyClass(contentType)
importParams = tool.getCreateMeans(appyClass)['import']
onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sort']
if sortMethod: sortMethod = sortMethod.__get__('')
elems = []
importPath = getattr(self, 'importPathFor%s' % contentType)
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]
# ------------------------------------------------------------------------------

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class PodTemplateMixin(AbstractMixin):
_appy_meta_type = 'PodTemplate'
# ------------------------------------------------------------------------------

View file

@ -1,22 +1,28 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, os, os.path, Cookie import re, os, os.path, time, Cookie, StringIO, types
from appy.shared import mimeTypes
from appy.shared.utils import getOsTempFolder from appy.shared.utils import getOsTempFolder
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen import appy.gen
from appy.gen import Type, Search, Selection from appy.gen import Type, Search, Selection
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName from appy.gen.utils import SomeObjects, sequenceTypes, getClassName
from appy.gen.plone25.mixins import AbstractMixin from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ClassDescriptor from appy.gen.plone25.descriptors import ClassDescriptor
# Errors -----------------------------------------------------------------------
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
POD_ERROR = 'An error occurred while generating the document. Please ' \
'contact the system administrator.'
jsMessages = ('no_elem_selected', 'delete_confirm') jsMessages = ('no_elem_selected', 'delete_confirm')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class ToolMixin(AbstractMixin): class ToolMixin(BaseMixin):
_appy_meta_type = 'Tool' _appy_meta_type = 'Tool'
def getPortalType(self, metaTypeOrAppyClass): def getPortalType(self, metaTypeOrAppyClass):
'''Returns the name of the portal_type that is based on '''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.''' p_metaTypeOrAppyType.'''
appName = self.getProductConfig().PROJECTNAME appName = self.getProductConfig().PROJECTNAME
if not isinstance(metaTypeOrAppyClass, basestring): if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName) res = getClassName(metaTypeOrAppyClass, appName)
@ -25,52 +31,100 @@ class ToolMixin(AbstractMixin):
res = '%s%s' % (elems[1], elems[4]) res = '%s%s' % (elems[1], elems[4])
return res return res
def getFlavour(self, contextObjOrPortalType, appy=False): def getPodInfo(self, ploneObj, fieldName):
'''Gets the flavour that corresponds to p_contextObjOrPortalType.''' '''Returns POD-related information about Pod field p_fieldName defined
if isinstance(contextObjOrPortalType, basestring): on class whose p_ploneObj is an instance of.'''
portalTypeName = contextObjOrPortalType res = {}
else: appyClass = self.getAppyClass(ploneObj.meta_type)
# It is the contextObj, not a portal type name
portalTypeName = contextObjOrPortalType.portal_type
res = None
appyTool = self.appy() appyTool = self.appy()
flavourNumber = None n = appyTool.getAttributeName('formats', appyClass, fieldName)
nameElems = portalTypeName.split('_') res['formats'] = getattr(appyTool, n)
if len(nameElems) > 1: n = appyTool.getAttributeName('podTemplate', appyClass, fieldName)
try: res['template'] = getattr(appyTool, n)
flavourNumber = int(nameElems[-1]) appyType = ploneObj.getAppyType(fieldName)
except ValueError: res['title'] = self.translate(appyType.labelId)
pass res['context'] = appyType.context
appName = self.getProductConfig().PROJECTNAME res['action'] = appyType.action
if flavourNumber != None:
for flavour in appyTool.flavours:
if flavourNumber == flavour.number:
res = flavour
elif portalTypeName == ('%sFlavour' % appName):
# Current object is the Flavour itself. In this cas we simply
# return the wrapped contextObj. Here we are sure that
# contextObjOrPortalType is an object, not a portal type.
res = contextObjOrPortalType.appy()
if not res and appyTool.flavours:
res = appyTool.flavours[0]
# If appy=False, return the Plone object and not the Appy wrapper
# (this way, we avoid Zope security/access-related problems while
# using this object in Zope Page Templates)
if res and not appy:
res = res.o
return res return res
def getFlavoursInfo(self): def generateDocument(self):
'''Returns information about flavours.''' '''Generates the document from field-related info. UID of object that
res = [] is the template target is given in the request.'''
rq = self.REQUEST
appyTool = self.appy() appyTool = self.appy()
for flavour in appyTool.flavours: # Get the object
if isinstance(flavour.o, FlavourMixin): objectUid = rq.get('objectUid')
# This is a bug: sometimes other objects are associated as obj = self.uid_catalog(UID=objectUid)[0].getObject()
# flavours. appyObj = obj.appy()
res.append({'title': flavour.title, 'number':flavour.number}) # Get information about the document to render.
specificPodContext = None
fieldName = rq.get('fieldName')
format = rq.get('podFormat')
podInfo = self.getPodInfo(obj, fieldName)
template = podInfo['template'].content
podTitle = podInfo['title']
if podInfo['context']:
if type(podInfo['context']) == types.FunctionType:
specificPodContext = podInfo['context'](appyObj)
else:
specificPodContext = podInfo['context']
doAction = rq.get('askAction') == 'True'
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
getOsTempFolder(), obj.UID(), time.time(), format)
# Define parameters to pass to the appy.pod renderer
currentUser = self.portal_membership.getAuthenticatedMember()
podContext = {'tool': appyTool, 'user': currentUser, 'self': appyObj,
'now': self.getProductConfig().DateTime(),
'projectFolder': appyTool.getDiskFolder(),
}
if specificPodContext:
podContext.update(specificPodContext)
rendererParams = {'template': StringIO.StringIO(template),
'context': podContext, 'result': tempFileName}
if appyTool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython
if appyTool.openOfficePort:
rendererParams['ooPort'] = appyTool.openOfficePort
# Launch the renderer
try:
renderer = Renderer(**rendererParams)
renderer.run()
except appy.pod.PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
appyTool.log(str(pe), type='error')
appyTool.say(POD_ERROR)
return self.goto(rq.get('HTTP_REFERER'))
# Open the temp file on the filesystem
f = file(tempFileName, 'rb')
res = f.read()
# Identify the filename to return
fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle)
fileName = appyTool.normalize(fileName)
response = obj.REQUEST.RESPONSE
response.setHeader('Content-Type', mimeTypes[format])
response.setHeader('Content-Disposition', 'inline;filename="%s.%s"'\
% (fileName, format))
f.close()
# Execute the related action if relevant
if doAction and podInfo['action']:
podInfo['action'](appyObj, podContext)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning')
except IOError, ie:
appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res return res
def getAttr(self, name):
'''Gets attribute named p_attrName. Useful because we can't use getattr
directly in Zope Page Templates.'''
return getattr(self.appy(), name, None)
def getAppName(self): def getAppName(self):
'''Returns the name of this application.''' '''Returns the name of this application.'''
return self.getProductConfig().PROJECTNAME return self.getProductConfig().PROJECTNAME
@ -86,6 +140,58 @@ class ToolMixin(AbstractMixin):
'''Returns the list of root classes for this application.''' '''Returns the list of root classes for this application.'''
return self.getProductConfig().rootClasses return self.getProductConfig().rootClasses
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
res = []
for appyType in self.getProductConfig().attributes[contentType]:
if appyType.name != 'title': # Will be included by default.
label = '%s_%s' % (contentType, appyType.name)
res.append((appyType.name, self.translate(label)))
# Add object state
res.append(('workflowState', 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.getProductConfig().attributes[contentType]:
if appyType.indexed:
res.append((appyType.name, self.translate(appyType.labelId)))
return res
def getSearchableFields(self, contentType):
'''Returns, among the list of all searchable fields (see method above),
the list of fields that the user has configured as being effectively
used in the search screen.'''
res = []
fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ())
for name in fieldNames:
appyType = self.getAppyType(name, asDict=True,className=contentType)
res.append(appyType)
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 = []
importPath = getattr(self, 'importPathFor%s' % contentType)
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): def showPortlet(self, context):
if self.portal_membership.isAnonymousUser(): return False if self.portal_membership.isAnonymousUser(): return False
if context.id == 'skyn': context = context.getParentNode() if context.id == 'skyn': context = context.getParentNode()
@ -106,15 +212,13 @@ class ToolMixin(AbstractMixin):
res = res.appy() res = res.appy()
return res return res
def executeQuery(self, contentType, flavourNumber=1, searchName=None, def executeQuery(self, contentType, searchName=None, startNumber=0,
startNumber=0, search=None, remember=False, search=None, remember=False, brainsOnly=False,
brainsOnly=False, maxResults=None, noSecurity=False, maxResults=None, noSecurity=False, sortBy=None,
sortBy=None, sortOrder='asc', sortOrder='asc', filterKey=None, filterValue=None):
filterKey=None, filterValue=None):
'''Executes a query on a given p_contentType (or several, separated '''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the with commas) in Plone's portal_catalog. If p_searchName is specified,
flavour numbered p_flavourNumber. If p_searchName is specified, it it corresponds to:
corresponds to:
1) a search defined on p_contentType: additional search criteria 1) a search defined on p_contentType: additional search criteria
will be added to the query, or; will be added to the query, or;
2) "_advanced": in this case, additional search criteria will also 2) "_advanced": in this case, additional search criteria will also
@ -150,11 +254,7 @@ class ToolMixin(AbstractMixin):
p_filterValue.''' p_filterValue.'''
# Is there one or several content types ? # Is there one or several content types ?
if contentType.find(',') != -1: if contentType.find(',') != -1:
# Several content types are specified
portalTypes = contentType.split(',') portalTypes = contentType.split(',')
if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes]
else: else:
portalTypes = contentType portalTypes = contentType
params = {'portal_type': portalTypes} params = {'portal_type': portalTypes}
@ -164,8 +264,7 @@ class ToolMixin(AbstractMixin):
# In this case, contentType must contain a single content type. # In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType) appyClass = self.getAppyClass(contentType)
if searchName != '_advanced': if searchName != '_advanced':
search = ClassDescriptor.getSearch( search = ClassDescriptor.getSearch(appyClass, searchName)
appyClass, searchName)
else: else:
fields = self.REQUEST.SESSION['searchCriteria'] fields = self.REQUEST.SESSION['searchCriteria']
search = Search('customSearch', **fields) search = Search('customSearch', **fields)
@ -220,22 +319,17 @@ class ToolMixin(AbstractMixin):
for obj in res.objects: for obj in res.objects:
i += 1 i += 1
uids[startNumber+i] = obj.UID() uids[startNumber+i] = obj.UID()
s['search_%s_%s' % (flavourNumber, searchName)] = uids s['search_%s' % searchName] = uids
return res.__dict__ return res.__dict__
def getResultColumnsNames(self, contentType): def getResultColumnsNames(self, contentType):
contentTypes = contentType.strip(',').split(',') contentTypes = contentType.strip(',').split(',')
resSet = None # Temporary set for computing intersections. resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result. res = [] # Final, sorted result.
flavour = None
fieldNames = None fieldNames = None
appyTool = self.appy()
for cType in contentTypes: for cType in contentTypes:
# Get the flavour tied to those content types fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType)
if not flavour:
flavour = self.getFlavour(cType, appy=True)
if flavour.number != 1:
cType = cType.rsplit('_', 1)[0]
fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType)
if not resSet: if not resSet:
resSet = set(fieldNames) resSet = set(fieldNames)
else: else:
@ -483,9 +577,9 @@ class ToolMixin(AbstractMixin):
attrValue = oper.join(attrValue) attrValue = oper.join(attrValue)
criteria[attrName[2:]] = attrValue criteria[attrName[2:]] = attrValue
rq.SESSION['searchCriteria'] = criteria rq.SESSION['searchCriteria'] = criteria
# Goto the screen that displays search results # Go to the screen that displays search results
backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \ backUrl = '%s/query?type_name=%s&&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber']) (os.path.dirname(rq['URL']), rq['type_name'])
return self.goto(backUrl) return self.goto(backUrl)
def getJavascriptMessages(self): def getJavascriptMessages(self):
@ -535,13 +629,12 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value if cookieValue: return cookieValue.value
return default return default
def getQueryUrl(self, contentType, flavourNumber, searchName, def getQueryUrl(self, contentType, searchName, startNumber=None):
startNumber=None):
'''This method creates the URL that allows to perform a (non-Ajax) '''This method creates the URL that allows to perform a (non-Ajax)
request for getting queried objects from a search named p_searchName request for getting queried objects from a search named p_searchName
on p_contentType from flavour numbered p_flavourNumber.''' on p_contentType.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn' baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber) baseParams = 'type_name=%s' % contentType
# Manage start number # Manage start number
rq = self.REQUEST rq = self.REQUEST
if startNumber != None: if startNumber != None:
@ -609,11 +702,10 @@ class ToolMixin(AbstractMixin):
if (nextIndex < lastIndex): lastNeeded = True if (nextIndex < lastIndex): lastNeeded = True
# Get the list of available UIDs surrounding the current object # Get the list of available UIDs surrounding the current object
if t == 'ref': # Manage navigation from a reference if t == 'ref': # Manage navigation from a reference
# In the case of a reference, we retrieve ALL surrounding objects.
masterObj = self.getObject(d1) masterObj = self.getObject(d1)
batchSize = masterObj.getAppyType(fieldName).maxPerPage batchSize = masterObj.getAppyType(fieldName).maxPerPage
uids = getattr(masterObj, '_appy_%s' % fieldName) uids = getattr(masterObj, '_appy_%s' % fieldName)
# In the case of a reference, we retrieve ALL surrounding objects.
# Display the reference widget at the page where the current object # Display the reference widget at the page where the current object
# lies. # lies.
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName) startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
@ -622,13 +714,12 @@ class ToolMixin(AbstractMixin):
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber, res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
'page':pageName, 'nav':''}) 'page':pageName, 'nav':''})
else: # Manage navigation from a search else: # Manage navigation from a search
contentType, flavourNumber = d1.split(':') contentType = d1
flavourNumber = int(flavourNumber)
searchName = keySuffix = d2 searchName = keySuffix = d2
batchSize = self.appy().numberOfResultsPerPage batchSize = self.appy().numberOfResultsPerPage
if not searchName: keySuffix = contentType if not searchName: keySuffix = contentType
s = self.REQUEST.SESSION s = self.REQUEST.SESSION
searchKey = 'search_%s_%s' % (flavourNumber, keySuffix) searchKey = 'search_%s' % keySuffix
if s.has_key(searchKey): uids = s[searchKey] if s.has_key(searchKey): uids = s[searchKey]
else: uids = {} else: uids = {}
# In the case of a search, we retrieve only a part of all # In the case of a search, we retrieve only a part of all
@ -640,9 +731,8 @@ class ToolMixin(AbstractMixin):
# this one. # this one.
newStartNumber = (res['currentNumber']-1) - (batchSize / 2) newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
if newStartNumber < 0: newStartNumber = 0 if newStartNumber < 0: newStartNumber = 0
self.executeQuery(contentType, flavourNumber, self.executeQuery(contentType, searchName=searchName,
searchName=searchName, startNumber=newStartNumber, startNumber=newStartNumber, remember=True)
remember=True)
uids = s[searchKey] uids = s[searchKey]
# For the moment, for first and last, we get them only if we have # For the moment, for first and last, we get them only if we have
# them in session. # them in session.
@ -650,9 +740,9 @@ class ToolMixin(AbstractMixin):
if not uids.has_key(lastIndex): lastNeeded = False if not uids.has_key(lastIndex): lastNeeded = False
# Compute URL of source object # Compute URL of source object
startNumber = self.computeStartNumberFrom(res['currentNumber']-1, startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize) res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber, res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
searchName, startNumber=startNumber) startNumber=startNumber)
# Compute URLs # Compute URLs
for urlType in ('previous', 'next', 'first', 'last'): for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType exec 'needIt = %sNeeded' % urlType

View file

@ -1,7 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.mixins import AbstractMixin
# ------------------------------------------------------------------------------
class UserMixin(AbstractMixin):
_appy_meta_type = 'UserMixin'
# ------------------------------------------------------------------------------

View file

@ -1,9 +1,6 @@
'''This package contains mixin classes that are mixed in with generated classes: '''This package contains mixin classes that are mixed in with generated classes:
- mixins/ClassMixin is mixed in with Standard Archetypes classes; - mixins/BaseMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class; - mixins/ToolMixin is mixed in with the generated application Tool class.'''
- mixins/FlavourMixin is mixed in with the generated application Flavour
class.
The AbstractMixin defined hereafter is the base class of any mixin.'''
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, types, mimetypes import os, os.path, types, mimetypes
@ -15,10 +12,10 @@ from appy.gen.plone25.descriptors import ClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission from appy.gen.plone25.utils import updateRolesForPermission
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class AbstractMixin: class BaseMixin:
'''Every Archetype class generated by appy.gen inherits from a mixin that '''Every Archetype class generated by appy.gen inherits from this class or
inherits from this class. It contains basic functions allowing to a subclass of it.'''
minimize the amount of generated code.''' _appy_meta_type = 'Class'
def createOrUpdate(self, created, values): def createOrUpdate(self, created, values):
'''This method creates (if p_created is True) or updates an object. '''This method creates (if p_created is True) or updates an object.
@ -52,17 +49,21 @@ class AbstractMixin:
# Keep in history potential changes on historized fields # Keep in history potential changes on historized fields
self.historizeData(previousData) self.historizeData(previousData)
# Manage references # Manage potential link with an initiator object
obj._appy_manageRefs(created) if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog(UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[0]
initiator.appy().link(fieldName, obj)
# Call the custom "onEdit" if available
if obj.wrapperClass: if obj.wrapperClass:
# Get the wrapper first
appyObject = obj.appy() appyObject = obj.appy()
# Call the custom "onEdit" if available if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created)
if hasattr(appyObject, 'onEdit'): # Manage "add" permissions and reindex the object
appyObject.onEdit(created)
# Manage "add" permissions
obj._appy_managePermissions() obj._appy_managePermissions()
# Reindex object
obj.reindexObject() obj.reindexObject()
return obj return obj
@ -95,6 +96,12 @@ class AbstractMixin:
(baseUrl, typeName, objId) (baseUrl, typeName, objId)
return self.goto(self.getUrl(editUrl, **urlParams)) return self.goto(self.getUrl(editUrl, **urlParams))
def onCreateWithoutForm(self):
'''This method is called when a user wants to create a object from a
reference field, automatically (without displaying a form).'''
rq = self.REQUEST
self.appy().create(rq['fieldName'])
def intraFieldValidation(self, errors, values): def intraFieldValidation(self, errors, values):
'''This method performs field-specific validation for every field from '''This method performs field-specific validation for every field from
the page that is being created or edited. For every field whose the page that is being created or edited. For every field whose
@ -120,7 +127,7 @@ class AbstractMixin:
obj = self.appy() obj = self.appy()
if not hasattr(obj, 'validate'): return if not hasattr(obj, 'validate'): return
obj.validate(values, errors) obj.validate(values, errors)
# This custom "validate" method may have added fields in the given # Those custom validation methods may have added fields in the given
# p_errors object. Within this object, for every error message that is # p_errors object. Within this object, for every error message that is
# not a string, we replace it with the standard validation error for the # not a string, we replace it with the standard validation error for the
# corresponding field. # corresponding field.
@ -241,21 +248,19 @@ class AbstractMixin:
res = {} res = {}
for appyType in self.getAllAppyTypes(): for appyType in self.getAllAppyTypes():
if appyType.historized: if appyType.historized:
res[appyType.name] = (getattr(self, appyType.name), res[appyType.name] = appyType.getValue(self)
appyType.labelId)
return res return res
def addDataChange(self, changes, labels=False): def addDataChange(self, changes):
'''This method allows to add "manually" a data change into the objet's '''This method allows to add "manually" a data change into the objet's
history. Indeed, data changes are "automatically" recorded only when history. Indeed, data changes are "automatically" recorded only when
a HTTP form is uploaded, not if, in the code, a setter is called on a HTTP form is uploaded, not if, in the code, a setter is called on
a field. The method is also called by the method historizeData below, a field. The method is also called by the method historizeData below,
that performs "automatic" recording when a HTTP form is uploaded.''' that performs "automatic" recording when a HTTP form is uploaded.'''
# Add to the p_changes dict the field labels if they are not present # Add to the p_changes dict the field labels
if not labels: for fieldName in changes.iterkeys():
for fieldName in changes.iterkeys(): appyType = self.getAppyType(fieldName)
appyType = self.getAppyType(fieldName) changes[fieldName] = (changes[fieldName], appyType.labelId)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Create the event to record in the history # Create the event to record in the history
DateTime = self.getProductConfig().DateTime DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state') state = self.portal_workflow.getInfoFor(self, 'review_state')
@ -273,14 +278,18 @@ class AbstractMixin:
historized fields, while p_self already contains the (potentially) historized fields, while p_self already contains the (potentially)
modified values.''' modified values.'''
# Remove from previousData all values that were not changed # Remove from previousData all values that were not changed
for fieldName in previousData.keys(): for field in previousData.keys():
prev = previousData[fieldName][0] prev = previousData[field]
curr = getattr(self, fieldName) appyType = self.getAppyType(field)
curr = appyType.getValue(self)
if (prev == curr) or ((prev == None) and (curr == '')) or \ if (prev == curr) or ((prev == None) and (curr == '')) or \
((prev == '') and (curr == None)): ((prev == '') and (curr == None)):
del previousData[fieldName] del previousData[field]
if (appyType.type == 'Ref') and (field in previousData):
titles = [r.title for r in previousData[field]]
previousData[field] = ','.join(titles)
if previousData: if previousData:
self.addDataChange(previousData, labels=True) self.addDataChange(previousData)
def goto(self, url, addParams=False): def goto(self, url, addParams=False):
'''Brings the user to some p_url after an action has been executed.''' '''Brings the user to some p_url after an action has been executed.'''
@ -308,80 +317,14 @@ class AbstractMixin:
field named p_name.''' field named p_name.'''
return self.getAppyType(name).getFormattedValue(self, value) return self.getAppyType(name).getFormattedValue(self, value)
def _appy_getRefs(self, fieldName, ploneObjects=False,
noListIfSingleObj=False, startNumber=None):
'''p_fieldName is the name of a Ref field. This method returns an
ordered list containing the objects linked to p_self through this
field. If p_ploneObjects is True, the method returns the "true"
Plone objects instead of the Appy wrappers.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
appyType = self.getAppyType(fieldName)
sortedUids = self._appy_getSortedField(fieldName)
batchNeeded = startNumber != None
exec 'refUids= self.getRaw%s%s()' % (fieldName[0].upper(),fieldName[1:])
# There may be too much UIDs in sortedUids because these fields
# are not updated when objects are deleted. So we do it now. TODO: do
# such cleaning on object deletion?
toDelete = []
for uid in sortedUids:
if uid not in refUids:
toDelete.append(uid)
for uid in toDelete:
sortedUids.remove(uid)
# Prepare the result
res = SomeObjects()
res.totalNumber = res.batchSize = len(sortedUids)
if batchNeeded:
res.batchSize = appyType.maxPerPage
if startNumber != None:
res.startNumber = startNumber
# Get the needed referred objects
i = res.startNumber
# Is it possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids?
toUnlink = []
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
refUid = sortedUids[i]
refObject = self.uid_catalog(UID=refUid)[0].getObject()
i += 1
tool = self.getTool()
if refObject.meta_type != tool.getPortalType(appyType.klass):
toUnlink.append(refObject)
continue
if not ploneObjects:
refObject = refObject.appy()
res.objects.append(refObject)
# Unlink dummy linked objects
if toUnlink:
suffix = '%s%s' % (fieldName[0].upper(), fieldName[1:])
exec 'linkedObjects = self.get%s()' % suffix
for dummyObject in toUnlink:
linkedObjects.remove(dummyObject)
self.getProductConfig().logger.warn('DB error: Ref %s.%s ' \
'contains a %s instance "%s". It was removed.' % \
(self.meta_type, fieldName, dummyObject.meta_type,
dummyObject.getId()))
exec 'self.set%s(linkedObjects)' % suffix
if res.objects and noListIfSingleObj:
if appyType.multiplicity[1] == 1:
res.objects = res.objects[0]
return res
def getAppyRefs(self, name, startNumber=None): def getAppyRefs(self, name, startNumber=None):
'''Gets the objects linked to me through Ref field named p_name. '''Gets the objects linked to me through Ref field named p_name.
If p_startNumber is None, this method returns all referred objects. If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects, If p_startNumber is a number, this method will return
starting at p_startNumber, x being appyType.maxPerPage.''' appyType.maxPerPage objects, starting at p_startNumber.'''
appyType = self.getAppyType(name) appyType = self.getAppyType(name)
if not appyType.isBack: return appyType.getValue(self, type='zobjects', someObjects=True,
return self._appy_getRefs(name, ploneObjects=True, startNumber=startNumber).__dict__
startNumber=startNumber).__dict__
else:
# Note that pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType.relationship)).__dict__
def getSelectableAppyRefs(self, name): def getSelectableAppyRefs(self, name):
'''p_name is the name of a Ref field. This method returns the list of '''p_name is the name of a Ref field. This method returns the list of
@ -780,10 +723,6 @@ class AbstractMixin:
self.reindexObject() self.reindexObject()
return self.goto(urlBack) return self.goto(urlBack)
def getFlavour(self):
'''Returns the flavour corresponding to this object.'''
return self.getTool().getFlavour(self.portal_type)
def fieldValueSelected(self, fieldName, vocabValue, dbValue): def fieldValueSelected(self, fieldName, vocabValue, dbValue):
'''When displaying a selection box (ie a String with a validator being a '''When displaying a selection box (ie a String with a validator being a
list), must the _vocabValue appear as selected?''' list), must the _vocabValue appear as selected?'''
@ -853,32 +792,6 @@ class AbstractMixin:
rq.appyWrappers[uid] = wrapper rq.appyWrappers[uid] = wrapper
return wrapper return wrapper
def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False,
noListIfSingleObj=False):
'''This method returns the list of objects linked to this one
through the BackRef corresponding to the Archetypes
relationship named p_relName.'''
# Preamble: must I return a list or a single element?
maxOne = False
if noListIfSingleObj:
# I must get the referred appyType to know its maximum multiplicity.
appyType = self.getAppyType(fieldName)
if appyType.multiplicity[1] == 1:
maxOne = True
# Get the referred objects through the Archetypes relationship.
objs = self.getBRefs(relName)
if maxOne:
res = None
if objs:
res = objs[0]
if res and not ploneObjects:
res = res.appy()
else:
res = objs
if not ploneObjects:
res = [o.appy() for o in objs]
return res
def _appy_showState(self, workflow, stateShow): def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?''' '''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow): if callable(stateShow):
@ -955,57 +868,6 @@ class AbstractMixin:
exec 'self.%s = pList()' % sortedFieldName exec 'self.%s = pList()' % sortedFieldName
return getattr(self, sortedFieldName) return getattr(self, sortedFieldName)
def _appy_manageRefs(self, created):
'''Every time an object is created or updated, this method updates
the Reference fields accordingly.'''
self._appy_manageRefsFromRequest()
rq = self.REQUEST
# If the creation was initiated by another object, update the ref.
if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog.searchResults(
UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[1]
initiator.appy().link(fieldName, self)
def _appy_manageRefsFromRequest(self):
'''Appy manages itself some Ref fields (with link=True). So here we must
update the Ref fields.'''
fieldsInRequest = [] # Fields present in the request
for requestKey in self.REQUEST.keys():
if requestKey.startswith('appy_ref_'):
fieldName = requestKey[9:]
# Security check
if not self.getAppyType(fieldName).isShowable(self, 'edit'):
continue
fieldsInRequest.append(fieldName)
fieldValue = self.REQUEST[requestKey]
sortedRefField = self._appy_getSortedField(fieldName)
del sortedRefField[:]
if not fieldValue: fieldValue = []
if isinstance(fieldValue, basestring):
fieldValue = [fieldValue]
refObjects = []
for uid in fieldValue:
obj = self.uid_catalog(UID=uid)[0].getObject()
refObjects.append(obj)
sortedRefField.append(uid)
exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(),
fieldName[1:])
# Manage Ref fields that are not present in the request
currentPage = self.REQUEST.get('page', 'main')
for appyType in self.getAllAppyTypes():
if (appyType.type == 'Ref') and not appyType.isBack and \
(appyType.page == currentPage) and \
(appyType.name not in fieldsInRequest):
# If this field is visible, it was not present in the request:
# it means that we must remove any Ref from it.
if appyType.isShowable(self, 'edit'):
exec 'self.set%s%s([])' % (appyType.name[0].upper(),
appyType.name[1:])
getUrlDefaults = {'page':True, 'nav':True} getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', **kwargs): def getUrl(self, base=None, mode='view', **kwargs):
'''Returns a Appy URL. '''Returns a Appy URL.
@ -1039,12 +901,14 @@ class AbstractMixin:
params = '' params = ''
return '%s%s' % (base, params) return '%s%s' % (base, params)
def translate(self, label, mapping={}, domain=None, default=None): def translate(self, label, mapping={}, domain=None, default=None,
language=None):
'''Translates a given p_label into p_domain with p_mapping.''' '''Translates a given p_label into p_domain with p_mapping.'''
cfg = self.getProductConfig() cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME if not domain: domain = cfg.PROJECTNAME
return self.translation_service.utranslate( return self.Control_Panel.TranslationService.utranslate(
domain, label, mapping, self, default=default) domain, label, mapping, self, default=default,
target_language=language)
def getPageLayout(self, layoutType): def getPageLayout(self, layoutType):
'''Returns the layout corresponding to p_layoutType for p_self.''' '''Returns the layout corresponding to p_layoutType for p_self.'''

View file

@ -12,10 +12,10 @@ from appy.gen import *
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class ModelClass: class ModelClass:
'''This class is the abstract class of all predefined application classes '''This class is the abstract class of all predefined application classes
used in the Appy model: Tool, Flavour, PodTemplate, etc. All methods and used in the Appy model: Tool, User, etc. All methods and attributes of
attributes of those classes are part of the Appy machinery and are those classes are part of the Appy machinery and are prefixed with _appy_
prefixed with _appy_ in order to avoid name conflicts with user-defined in order to avoid name conflicts with user-defined parts of the
parts of the application model.''' application model.'''
_appy_attributes = [] # We need to keep track of attributes order. _appy_attributes = [] # We need to keep track of attributes order.
# When creating a new instance of a ModelClass, the following attributes # When creating a new instance of a ModelClass, the following attributes
# must not be given in the constructor (they are computed attributes). # must not be given in the constructor (they are computed attributes).
@ -70,7 +70,11 @@ class ModelClass:
res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType)) res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType))
return res return res
# The User class ---------------------------------------------------------------
class User(ModelClass): 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. # All methods defined below are fake. Real versions are in the wrapper.
title = String(show=False) title = String(show=False)
gm = {'group': 'main', 'multiplicity': (1,1)} gm = {'group': 'main', 'multiplicity': (1,1)}
@ -86,47 +90,50 @@ class User(ModelClass):
password2 = String(format=String.PASSWORD, show=showPassword, **gm) password2 = String(format=String.PASSWORD, show=showPassword, **gm)
gm['multiplicity'] = (0, None) gm['multiplicity'] = (0, None)
roles = String(validator=Selection('getGrantableRoles'), **gm) roles = String(validator=Selection('getGrantableRoles'), **gm)
_appy_attributes = ['title', 'name', 'firstName', 'login',
'password1', 'password2', 'roles']
class PodTemplate(ModelClass): # The Tool class ---------------------------------------------------------------
description = String(format=String.TEXT)
podTemplate = File(multiplicity=(1,1))
podFormat = String(validator=['odt', 'pdf', 'rtf', 'doc'],
multiplicity=(1,1), default='odt')
podPhase = String(default='main')
_appy_attributes = ['description', 'podTemplate', 'podFormat', 'podPhase']
defaultFlavourAttrs = ('number', 'enableNotifications') # Here are the prefixes of the fields generated on the Tool.
flavourAttributePrefixes = ('optionalFieldsFor', 'defaultValueFor', toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'podTemplatesFor', 'podMaxShownTemplatesFor', 'resultColumnsFor', 'enableAdvancedSearch', 'numberOfSearchColumns',
'showWorkflowFor', 'showWorkflowCommentFieldFor', 'showAllStatesInPhaseFor') 'searchFields', 'optionalFields', 'showWorkflow',
# Attribute prefixes of the fields generated on the Flavour for configuring 'showWorkflowCommentField', 'showAllStatesInPhase')
# the application classes. defaultToolFields = ('users', 'enableNotifications', 'unoEnabledPython',
'openOfficePort', 'numberOfResultsPerPage',
'listBoxesMaximumWidth')
class Flavour(ModelClass): class Tool(ModelClass):
'''For every application, the Flavour may be different (it depends on the # The following dict allows us to remember the original classes related to
fields declared as optional, etc). Instead of creating a new way to # the attributes we will add due to params in user attributes.
generate the Archetypes Flavour class, we create a silly
FlavourStub instance and we will use the standard Archetypes
generator that generates classes from the application to generate the
flavour class.'''
number = Integer(default=1, show=False)
enableNotifications = Boolean(default=True, page='notifications')
_appy_classes = {} # ~{s_attributeName: s_className}~ _appy_classes = {} # ~{s_attributeName: s_className}~
# We need to remember the original classes related to the flavour attributes # In a ModelClass we need to declare attributes in the following list.
_appy_attributes = list(defaultFlavourAttrs) _appy_attributes = list(defaultToolFields)
# Tool attributes
# First arg of Ref field below is None because we don't know yet if it will
# link to the predefined User class or a custom class defined in the
# application.
users = Ref(None, multiplicity=(0,None), add=True, link=False,
back=Ref(attribute='toTool'), page='users',
shownInfo=('login', 'title', 'roles'), showHeaders=True)
enableNotifications = Boolean(default=True, page='notifications')
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)
listBoxesMaximumWidth = Integer(default=100)
@classmethod @classmethod
def _appy_clean(klass): def _appy_clean(klass):
toClean = [] toClean = []
for k, v in klass.__dict__.iteritems(): for k, v in klass.__dict__.iteritems():
if not k.startswith('__') and (not k.startswith('_appy_')): if not k.startswith('__') and (not k.startswith('_appy_')):
if k not in defaultFlavourAttrs: if k not in defaultToolFields:
toClean.append(k) toClean.append(k)
for k in toClean: for k in toClean:
exec 'del klass.%s' % k exec 'del klass.%s' % k
klass._appy_attributes = list(defaultFlavourAttrs) klass._appy_attributes = list(defaultToolFields)
klass._appy_classes = {} klass._appy_classes = {}
@classmethod @classmethod
@ -134,20 +141,20 @@ class Flavour(ModelClass):
'''From a given p_appyType, produce a type definition suitable for '''From a given p_appyType, produce a type definition suitable for
storing the default value for this field.''' storing the default value for this field.'''
res = copy.copy(appyType) res = copy.copy(appyType)
# A fiekd in the flavour can't have parameters that would lead to the # A field added to the tool can't have parameters that would lead to the
# creation of new fields in the flavour. # creation of new fields in the tool.
res.editDefault = False res.editDefault = False
res.optional = False res.optional = False
res.show = True res.show = True
res.group = copy.copy(appyType.group) res.group = copy.copy(appyType.group)
res.phase = 'main' res.phase = 'main'
# Set default layouts for all Flavour fields # Set default layouts for all Tool fields
res.layouts = res.formatLayouts(None) res.layouts = res.formatLayouts(None)
res.specificReadPermission = False res.specificReadPermission = False
res.specificWritePermission = False res.specificWritePermission = False
res.multiplicity = (0, appyType.multiplicity[1]) res.multiplicity = (0, appyType.multiplicity[1])
if type(res.validator) == types.FunctionType: if type(res.validator) == types.FunctionType:
# We will not be able to call this function from the flavour. # We will not be able to call this function from the tool.
res.validator = None res.validator = None
if isinstance(appyType, Ref): if isinstance(appyType, Ref):
res.link = True res.link = True
@ -155,7 +162,7 @@ class Flavour(ModelClass):
res.back = copy.copy(appyType.back) res.back = copy.copy(appyType.back)
res.back.attribute += 'DefaultValue' res.back.attribute += 'DefaultValue'
res.back.show = False res.back.show = False
res.select = None # Not callable from flavour res.select = None # Not callable from tool.
return res return res
@classmethod @classmethod
@ -182,10 +189,7 @@ class Flavour(ModelClass):
@classmethod @classmethod
def _appy_addPodRelatedFields(klass, fieldDescr): def _appy_addPodRelatedFields(klass, fieldDescr):
'''Adds the fields needed in the Flavour for configuring a Pod field. '''Adds the fields needed in the Tool for configuring a Pod field.'''
The following method, m_appy_addPodField, is the previous way to
manage gen-pod integration. For the moment, both approaches coexist.
In the future, only this one may subsist.'''
className = fieldDescr.classDescr.name className = fieldDescr.classDescr.name
# On what page and group to display those fields ? # On what page and group to display those fields ?
pg = {'page': 'documentGeneration', pg = {'page': 'documentGeneration',
@ -200,29 +204,9 @@ class Flavour(ModelClass):
multiplicity=(1,None), default=('odt',), **pg) multiplicity=(1,None), default=('odt',), **pg)
klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr) klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr)
@classmethod
def _appy_addPodField(klass, classDescr):
'''Adds a POD field to the flavour and also an integer field that will
determine the maximum number of documents to show at once on consult
views. If this number is reached, a list is displayed.'''
# First, add the POD field that will hold PodTemplates.
fieldType = Ref(PodTemplate, multiplicity=(0,None), add=True,
link=False, back = Ref(attribute='flavour'),
page="documentGeneration",
group=classDescr.klass.__name__)
fieldName = 'podTemplatesFor%s' % classDescr.name
klass._appy_addField(fieldName, fieldType, classDescr)
# Then, add the integer field
fieldType = Integer(default=1, page='userInterface',
group=classDescr.klass.__name__)
fieldName = 'podMaxShownTemplatesFor%s' % classDescr.name
klass._appy_addField(fieldName, fieldType, classDescr)
classDescr.flavourFieldsToPropagate.append(
('podMaxShownTemplatesFor%s', copy.copy(fieldType)) )
@classmethod @classmethod
def _appy_addQueryResultColumns(klass, classDescr): def _appy_addQueryResultColumns(klass, classDescr):
'''Adds, for class p_classDescr, the attribute in the flavour that '''Adds, for class p_classDescr, the attribute in the tool that
allows to select what default columns will be shown on query allows to select what default columns will be shown on query
results.''' results.'''
className = classDescr.name className = classDescr.name
@ -302,25 +286,4 @@ class Flavour(ModelClass):
fieldType = Boolean(default=defaultValue, page='userInterface', fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName) group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr) klass._appy_addField(fieldName, fieldType, classDescr)
class Tool(ModelClass):
flavours = Ref(None, multiplicity=(1,None), add=True, link=False,
back=Ref(attribute='tool'))
# First arg is None because we don't know yet if it will link
# to the predefined Flavour class or a custom class defined
# in the application.
users = Ref(None, multiplicity=(0,None), add=True, link=False,
back=Ref(attribute='toTool'), page='users',
shownInfo=('login', 'title', 'roles'), showHeaders=True)
# First arg is None because we don't know yet if it will link to the
# predefined User class or a custom class defined in the application.
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)
listBoxesMaximumWidth = Integer(default=100)
_appy_attributes = ['flavours', 'users', 'unoEnabledPython',
'openOfficePort', 'numberOfResultsPerPage',
'listBoxesMaximumWidth']
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -11,7 +11,7 @@
response request/RESPONSE; response request/RESPONSE;
member context/portal_membership/getAuthenticatedMember; member context/portal_membership/getAuthenticatedMember;
portal context/portal_url/getPortalObject; portal context/portal_url/getPortalObject;
portal_url context/portal_url/getPortalPath; portal_url python: context.portal_url();
template python: contextObj.getPageTemplate(portal.skyn, page); template python: contextObj.getPageTemplate(portal.skyn, page);
dummy python: response.setHeader('Content-Type','text/html;;charset=utf-8'); dummy python: response.setHeader('Content-Type','text/html;;charset=utf-8');
dummy2 python: response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT'); dummy2 python: response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');

View file

@ -4,7 +4,6 @@
layoutType python:'edit'; layoutType python:'edit';
layout python: contextObj.getPageLayout(layoutType); layout python: contextObj.getPageLayout(layoutType);
tool contextObj/getTool; tool contextObj/getTool;
flavour python: tool.getFlavour(contextObj);
appFolder tool/getAppFolder; appFolder tool/getAppFolder;
appName appFolder/getId; appName appFolder/getId;
page request/page|python:'main'; page request/page|python:'main';

View file

@ -16,8 +16,7 @@
tal:define="appFolder context/getParentNode; tal:define="appFolder context/getParentNode;
contentType request/type_name; contentType request/type_name;
tool python: portal.get('portal_%s' % appFolder.id.lower()); tool python: portal.get('portal_%s' % appFolder.id.lower());
flavour python: tool.getFlavour(contentType); importElems python: tool.getImportElements(contentType);
importElems python: flavour.getImportElements(contentType);
global allAreImported python:True"> global allAreImported python:True">
<div metal:use-macro="here/skyn/page/macros/prologue"/> <div metal:use-macro="here/skyn/page/macros/prologue"/>

View file

@ -1,7 +1,6 @@
<metal:queryResults define-macro="queryResult" <metal:queryResults define-macro="queryResult"
tal:define="tool python: contextObj; tal:define="tool python: contextObj;
contentType request/type_name; contentType request/type_name;
flavourNumber python: int(request['flavourNumber']);
startNumber request/startNumber|python:'0'; startNumber request/startNumber|python:'0';
startNumber python: int(startNumber); startNumber python: int(startNumber);
searchName request/search; searchName request/search;
@ -12,13 +11,13 @@
sortOrder request/sortOrder| python:'asc'; sortOrder request/sortOrder| python:'asc';
filterKey request/filterKey| python:''; filterKey request/filterKey| python:'';
filterValue request/filterValue | python:''; filterValue request/filterValue | python:'';
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber, remember=True, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue); queryResult python: tool.executeQuery(contentType, searchName, startNumber, remember=True, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue);
objs queryResult/objects; objs queryResult/objects;
totalNumber queryResult/totalNumber; totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize; batchSize queryResult/batchSize;
ajaxHookId python:'queryResult'; ajaxHookId python:'queryResult';
navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, flavourNumber, searchName); navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, searchName);
newSearchUrl python: '%s/skyn/search?type_name=%s&flavourNumber=%d' % (tool.getAppFolder().absolute_url(), contentType, flavourNumber);"> newSearchUrl python: '%s/skyn/search?type_name=%s&' % (tool.getAppFolder().absolute_url(), contentType);">
<tal:result condition="objs"> <tal:result condition="objs">
@ -91,7 +90,7 @@
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment> <tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<td id="field_title"><a <td id="field_title"><a
tal:define="navInfo python:'search.%s:%d.%s.%d.%d' % (contentType, flavourNumber, searchName, repeat['obj'].number()+startNumber, totalNumber);" tal:define="navInfo python:'search.%s.%s.%d.%d' % (contentType, searchName, repeat['obj'].number()+startNumber, totalNumber);"
tal:content="obj/Title" tal:attributes="href python: obj.getUrl(nav=navInfo, page='main')"></a></td> tal:content="obj/Title" tal:attributes="href python: obj.getUrl(nav=navInfo, page='main')"></a></td>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
@ -125,7 +124,7 @@
<tr> <tr>
<tal:comment replace="nothing">Edit the element</tal:comment> <tal:comment replace="nothing">Edit the element</tal:comment>
<td class="noPadding"> <td class="noPadding">
<a tal:define="navInfo python:'search.%s:%d.%s.%d.%d' % (contentType, flavourNumber, searchName, repeat['obj'].number()+startNumber, totalNumber);" <a tal:define="navInfo python:'search.%s.%s.%d.%d' % (contentType, searchName, repeat['obj'].number()+startNumber, totalNumber);"
tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)" tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)"
tal:condition="python: member.has_permission('Modify portal content', obj)"> tal:condition="python: member.has_permission('Modify portal content', obj)">
<img title="Edit" i18n:domain="plone" i18n:attributes="title" <img title="Edit" i18n:domain="plone" i18n:attributes="title"

View file

@ -122,11 +122,11 @@
/* The functions below wrap askAjaxChunk for getting specific content through /* The functions below wrap askAjaxChunk for getting specific content through
an Ajax request. */ an Ajax request. */
function askQueryResult(hookId, objectUrl, contentType, flavourNumber, function askQueryResult(hookId, objectUrl, contentType, searchName,
searchName, startNumber, sortKey, sortOrder, filterKey) { startNumber, sortKey, sortOrder, filterKey) {
// Sends an Ajax request for getting the result of a query. // Sends an Ajax request for getting the result of a query.
var params = {'type_name': contentType, 'flavourNumber': flavourNumber, var params = {'type_name': contentType, 'search': searchName,
'search': searchName, 'startNumber': startNumber}; 'startNumber': startNumber};
if (sortKey) params['sortKey'] = sortKey; if (sortKey) params['sortKey'] = sortKey;
if (sortOrder) params['sortOrder'] = sortOrder; if (sortOrder) params['sortOrder'] = sortOrder;
if (filterKey) { if (filterKey) {
@ -208,11 +208,9 @@
theForm.submit(); theForm.submit();
} }
function onDeleteObject(objectUid) { function onDeleteObject(objectUid) {
if (confirm(delete_confirm)) { f = document.getElementById('deleteForm');
f = document.getElementById('deleteForm'); f.objectUid.value = objectUid;
f.objectUid.value = objectUid; askConfirm('form', 'deleteForm', delete_confirm);
f.submit();
}
} }
function toggleCookie(cookieId) { function toggleCookie(cookieId) {
// What is the state of this boolean (expanded/collapsed) cookie? // What is the state of this boolean (expanded/collapsed) cookie?
@ -241,10 +239,9 @@
createCookie(cookieId, newState); createCookie(cookieId, newState);
} }
// Function that allows to generate a document from a pod template. // Function that allows to generate a document from a pod template.
function generatePodDocument(contextUid, templateUid, fieldName, podFormat) { function generatePodDocument(contextUid, fieldName, podFormat) {
var theForm = document.getElementsByName("podTemplateForm")[0]; var theForm = document.getElementsByName("podTemplateForm")[0];
theForm.objectUid.value = contextUid; theForm.objectUid.value = contextUid;
theForm.templateUid.value = templateUid;
theForm.fieldName.value = fieldName; theForm.fieldName.value = fieldName;
theForm.podFormat.value = podFormat; theForm.podFormat.value = podFormat;
theForm.askAction.value = "False"; theForm.askAction.value = "False";
@ -255,7 +252,10 @@
theForm.submit(); theForm.submit();
} }
// Functions for opening and closing a popup // Functions for opening and closing a popup
function openPopup(popupId) { function openPopup(popupId, msg) {
// Put the message into the popup
var confirmElem = document.getElementById('appyConfirmText');
confirmElem.innerHTML = msg;
// Open the popup // Open the popup
var popup = document.getElementById(popupId); var popup = document.getElementById(popupId);
// Put it at the right place on the screen // Put it at the right place on the screen
@ -276,19 +276,35 @@
greyed.style.display = "none"; greyed.style.display = "none";
} }
// Function triggered when an action needs to be confirmed by the user // Function triggered when an action needs to be confirmed by the user
function askConfirm(formId) { function askConfirm(actionType, action, msg) {
// Store the ID of the form to send if the users confirms. /* Store the actionType (send a form, call an URL or call a script) and the
related action, and shows the confirm popup. If the user confirms, we
will perform the action. */
var confirmForm = document.getElementById('confirmActionForm'); var confirmForm = document.getElementById('confirmActionForm');
confirmForm.actionFormId.value = formId; confirmForm.actionType.value = actionType;
openPopup("confirmActionPopup"); confirmForm.action.value = action;
openPopup("confirmActionPopup", msg);
} }
// Function triggered when an action confirmed by the user must be performed // Function triggered when an action confirmed by the user must be performed
function doConfirm() { function doConfirm() {
// The user confirmed: retrieve the form to send and send it. // The user confirmed: perform the required action.
closePopup('confirmActionPopup');
var confirmForm = document.getElementById('confirmActionForm'); var confirmForm = document.getElementById('confirmActionForm');
var actionFormId = confirmForm.actionFormId.value; var actionType = confirmForm.actionType.value;
var actionForm = document.getElementById(actionFormId); var action = confirmForm.action.value;
actionForm.submit(); if (actionType == 'form') {
// We must submit the form whose id is in "action"
document.getElementById(action).submit();
}
else if (actionType == 'url') {
// We must go to the URL defined in "action"
window.location = action;
}
else if (actionType == 'script') {
// We must execute Javascript code in "action"
eval(action);
}
} }
// Function that shows or hides a tab. p_action is 'show' or 'hide'. // Function that shows or hides a tab. p_action is 'show' or 'hide'.
function manageTab(tabId, action) { function manageTab(tabId, action) {
@ -344,10 +360,8 @@
</form> </form>
<tal:comment replace="nothing">Global form for generating a document from a pod template.</tal:comment> <tal:comment replace="nothing">Global form for generating a document from a pod template.</tal:comment>
<form name="podTemplateForm" method="post" <form name="podTemplateForm" method="post"
tal:attributes="action python: flavour.absolute_url() + '/generateDocument'"> tal:attributes="action python: tool.absolute_url() + '/generateDocument'">
<input type="hidden" name="objectUid"/> <input type="hidden" name="objectUid"/>
<tal:comment replace="nothing">templateUid is given if class-wide pod, fieldName and podFormat are given if podField.</tal:comment>
<input type="hidden" name="templateUid"/>
<input type="hidden" name="fieldName"/> <input type="hidden" name="fieldName"/>
<input type="hidden" name="podFormat"/> <input type="hidden" name="podFormat"/>
<input type="hidden" name="askAction"/> <input type="hidden" name="askAction"/>
@ -384,31 +398,6 @@
</tr> </tr>
</table> </table>
<tal:comment replace="nothing">
This macro lists the POD templates that are available. It is used by macro "header" below.
</tal:comment>
<div metal:define-macro="listPodTemplates" class="appyPod" tal:condition="podTemplates"
tal:define="podTemplates python: flavour.getAvailablePodTemplates(contextObj, phase);">
<tal:podTemplates define="maxShownTemplates python: flavour.getMaxShownTemplates(contextObj)">
<tal:comment replace="nothing">Display templates as links if a few number of templates must be shown</tal:comment>
<span class="discreet" tal:condition="python: len(podTemplates)&lt;=maxShownTemplates"
tal:repeat="podTemplate podTemplates">
<a style="cursor: pointer"
tal:define="podFormat podTemplate/getPodFormat"
tal:attributes="onclick python: 'generatePodDocument(\'%s\',\'%s\', \'\', \'\')' % (contextObj.UID(), podTemplate.UID())" >
<img tal:attributes="src string: $portal_url/skyn/$podFormat.png"/>
<span tal:replace="podTemplate/Title"/>
</a>
&nbsp;</span>
<tal:comment replace="nothing">Display templates as a list if a lot of templates must be shown</tal:comment>
<select tal:condition="python: len(podTemplates)&gt;maxShownTemplates">
<option value="" tal:content="python: tool.translate('choose_a_doc')"></option>
<option tal:repeat="podTemplate podTemplates" tal:content="podTemplate/Title"
tal:attributes="onclick python: 'generatePodDocument(\'%s\',\'%s\', \'\', \'\')' % (contextObj.UID(), podTemplate.UID())" />
</select>
</tal:podTemplates>
</div>
<tal:comment replace="nothing"> <tal:comment replace="nothing">
This macro displays an object's history. It is used by macro "header" below. This macro displays an object's history. It is used by macro "header" below.
</tal:comment> </tal:comment>
@ -460,7 +449,7 @@
<th align="left" width="70%" tal:content="python: tool.translate('previous_value')"></th> <th align="left" width="70%" tal:content="python: tool.translate('previous_value')"></th>
</tr> </tr>
<tr tal:repeat="change event/changes/items" valign="top"> <tr tal:repeat="change event/changes/items" valign="top">
<td tal:content="python: tool.translate(change[1][1])"></td> <td tal:content="structure python: tool.translate(change[1][1])"></td>
<td tal:define="appyValue python: contextObj.getFormattedFieldValue(change[0], change[1][0]); <td tal:define="appyValue python: contextObj.getFormattedFieldValue(change[0], change[1][0]);
appyType python:contextObj.getAppyType(change[0], asDict=True); appyType python:contextObj.getAppyType(change[0], asDict=True);
severalValues python: (appyType['multiplicity'][1] &gt; 1) or (appyType['multiplicity'][1] == None)"> severalValues python: (appyType['multiplicity'][1] &gt; 1) or (appyType['multiplicity'][1] == None)">
@ -482,13 +471,13 @@
This macro displays an object's state(s). It is used by macro "header" below. This macro displays an object's state(s). It is used by macro "header" below.
</tal:comment> </tal:comment>
<metal:states define-macro="states" <metal:states define-macro="states"
tal:define="showAllStatesInPhase python: flavour.getAttr('showAllStatesInPhaseFor' + contextObj.meta_type); tal:define="showAllStatesInPhase python: tool.getAttr('showAllStatesInPhaseFor' + contextObj.meta_type);
states python: contextObj.getAppyStates(phase, currentOnly=not showAllStatesInPhase)" states python: contextObj.getAppyStates(phase, currentOnly=not showAllStatesInPhase)"
tal:condition="python: test(showAllStatesInPhase, len(states)&gt;1, True)"> tal:condition="python: test(showAllStatesInPhase, len(states)&gt;1, True)">
<table> <table>
<tr> <tr>
<tal:state repeat="stateInfo states"> <tal:state repeat="stateInfo states">
<td tal:attributes="class python: 'appyState step' + stateInfo['stateStatus']" <td tal:attributes="class python: 'appyState step%sState' % stateInfo['stateStatus']"
tal:content="python: tool.translate(contextObj.getWorkflowLabel(stateInfo['name']))"> tal:content="python: tool.translate(contextObj.getWorkflowLabel(stateInfo['name']))">
</td> </td>
<td tal:condition="python: stateInfo['name'] != states[-1]['name']"> <td tal:condition="python: stateInfo['name'] != states[-1]['name']">
@ -512,7 +501,7 @@
<table> <table>
<tr> <tr>
<tal:comment replace="nothing">Input field allowing to enter a comment before triggering a transition</tal:comment> <tal:comment replace="nothing">Input field allowing to enter a comment before triggering a transition</tal:comment>
<td tal:define="showCommentsField python:flavour.getAttr('showWorkflowCommentFieldFor'+contextObj.meta_type)" <td tal:define="showCommentsField python:tool.getAttr('showWorkflowCommentFieldFor'+contextObj.meta_type)"
align="right" tal:condition="showCommentsField"> align="right" tal:condition="showCommentsField">
<span tal:content="python: tool.translate('workflow_comment')" class="discreet"></span> <span tal:content="python: tool.translate('workflow_comment')" class="discreet"></span>
<input type="text" id="comment" name="comment" size="35"/> <input type="text" id="comment" name="comment" size="35"/>
@ -520,7 +509,7 @@
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment> <tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
<td align="right" tal:repeat="transition transitions"> <td align="right" tal:repeat="transition transitions">
<input type="button" class="context" <input type="button" class="appyButton"
tal:attributes="value python: tool.translate(transition['name']); tal:attributes="value python: tool.translate(transition['name']);
onClick python: 'triggerTransition(\'%s\')' % transition['id'];"/> onClick python: 'triggerTransition(\'%s\')' % transition['id'];"/>
</td> </td>
@ -556,7 +545,7 @@
<td colspan="2" class="discreet" tal:content="descrLabel"/> <td colspan="2" class="discreet" tal:content="descrLabel"/>
</tr> </tr>
<tr> <tr>
<td class="documentByLine"> <td class="documentByLine" colspan="2">
<tal:comment replace="nothing">Creator and last modification date</tal:comment> <tal:comment replace="nothing">Creator and last modification date</tal:comment>
<tal:comment replace="nothing">Plus/minus icon for accessing history</tal:comment> <tal:comment replace="nothing">Plus/minus icon for accessing history</tal:comment>
<tal:accessHistory condition="hasHistory"> <tal:accessHistory condition="hasHistory">
@ -580,8 +569,6 @@
<span i18n:translate="box_last_modified" i18n:domain="plone"></span> <span i18n:translate="box_last_modified" i18n:domain="plone"></span>
<span tal:replace="python:contextObj.restrictedTraverse('@@plone').toLocalizedTime(contextObj.ModificationDate(),long_format=1)"></span> <span tal:replace="python:contextObj.restrictedTraverse('@@plone').toLocalizedTime(contextObj.ModificationDate(),long_format=1)"></span>
</td> </td>
<td valign="top"><metal:pod use-macro="here/skyn/page/macros/listPodTemplates"/>
</td>
</tr> </tr>
<tal:comment replace="nothing">Object history</tal:comment> <tal:comment replace="nothing">Object history</tal:comment>
<tr tal:condition="hasHistory"> <tr tal:condition="hasHistory">
@ -631,7 +618,7 @@
masterValue.push(idField); masterValue.push(idField);
} }
else { else {
if (idField[0] == '(') { if ((idField[0] == '(') || (idField[0] == '[')) {
// There are multiple values, split it // There are multiple values, split it
var subValues = idField.substring(1, idField.length-1).split(','); var subValues = idField.substring(1, idField.length-1).split(',');
for (var k=0; k < subValues.length; k++){ for (var k=0; k < subValues.length; k++){

View file

@ -8,13 +8,13 @@
contextObj python: tool.getPublishedObject()"> contextObj python: tool.getPublishedObject()">
<tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment> <tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment>
<dt class="portletHeader"> <dt class="portletHeader">
<tal:comment replace="nothing">If there is only one flavour, clicking on the portlet <tal:comment replace="nothing">For the Manager, clicking on the portlet
title allows to see all root objects in the database.</tal:comment> title allows to see all root objects in the database.</tal:comment>
<table cellpadding="0" cellspacing="0" width="100%"> <table cellpadding="0" cellspacing="0" width="100%">
<tr> <tr>
<td tal:define="titleIsClickable python: member.has_role('Manager') and rootClasses"> <td tal:define="titleIsClickable python: member.has_role('Manager') and rootClasses">
<a tal:condition="titleIsClickable" <a tal:condition="titleIsClickable"
tal:attributes="href python:'%s?type_name=%s&flavourNumber=1' % (queryUrl, ','.join(rootClasses))" tal:attributes="href python:'%s?type_name=%s' % (queryUrl, ','.join(rootClasses))"
tal:content="python: tool.translate(appName)"></a> tal:content="python: tool.translate(appName)"></a>
<span tal:condition="not: titleIsClickable" <span tal:condition="not: titleIsClickable"
tal:replace="python: tool.translate(appName)"/> tal:replace="python: tool.translate(appName)"/>
@ -37,16 +37,14 @@
</tal:publishedObject> </tal:publishedObject>
<tal:comment replace="nothing">Create a section for every root class.</tal:comment> <tal:comment replace="nothing">Create a section for every root class.</tal:comment>
<tal:section repeat="rootClass rootClasses" <tal:section repeat="rootClass rootClasses">
define="flavourNumber python:1;
flavour python: tool.getFlavour('Dummy_%d' % flavourNumber)">
<tal:comment replace="nothing">Section title, with action icons</tal:comment> <tal:comment replace="nothing">Section title, with action icons</tal:comment>
<dt tal:condition="python: tool.userMaySearch(rootClass)" <dt tal:condition="python: tool.userMaySearch(rootClass)"
tal:attributes="class python:test((repeat['rootClass'].number()==1) and not contextObj, 'portletAppyItem', 'portletAppyItem portletSep')"> tal:attributes="class python:test((repeat['rootClass'].number()==1) and not contextObj, 'portletAppyItem', 'portletAppyItem portletSep')">
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table"> <table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
<tr> <tr>
<td> <td>
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s' % (queryUrl, rootClass, flavourNumber); <a tal:attributes="href python: '%s?type_name=%s' % (queryUrl, rootClass);
class python:test(not currentSearch and (currentType==rootClass), 'portletCurrent', '')" class python:test(not currentSearch and (currentType==rootClass), 'portletCurrent', '')"
tal:content="python: tool.translate(rootClass + '_plural')"></a> tal:content="python: tool.translate(rootClass + '_plural')"></a>
</td> </td>
@ -66,11 +64,11 @@
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootClass); tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/import.png; src string: $portal_url/skyn/import.png;
title python: tool.translate('query_import')"/> title python: tool.translate('query_import')"/>
<tal:comment replace="nothing">Search objects of this type (todo: update flavourNumber)</tal:comment> <tal:comment replace="nothing">Search objects of this type</tal:comment>
<img style="cursor:pointer" <img style="cursor:pointer"
tal:define="showSearch python: flavour.getAttr('enableAdvancedSearchFor%s' % rootClass)" tal:define="showSearch python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)"
tal:condition="showSearch" tal:condition="showSearch"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/search?type_name=%s&flavourNumber=1\'' % (appFolder.absolute_url(), rootClass); tal:attributes="onClick python: 'href: window.location=\'%s/skyn/search?type_name=%s\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/search.gif; src string: $portal_url/skyn/search.gif;
title python: tool.translate('search_objects')"/> title python: tool.translate('search_objects')"/>
</td> </td>
@ -94,7 +92,7 @@
<span tal:attributes="id group/labelId; <span tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')"> style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletAppyItem portletSearch portletGroupItem" tal:repeat="search group/searches"> <dt class="portletAppyItem portletSearch portletGroupItem" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']); <a tal:attributes="href python: '%s?type_name=%s&search=%s' % (queryUrl, rootClass, search['name']);
title search/descr; title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');" class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a> tal:content="structure search/label"></a>
@ -105,7 +103,7 @@
<dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup" <dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup"
class="portletAppyItem portletSearch"> class="portletAppyItem portletSearch">
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']); <a tal:attributes="href python: '%s?type_name=%s&search=%s' % (queryUrl, rootClass, search['name']);
title search/descr; title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');" class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a> tal:content="structure search/label"></a>
@ -113,16 +111,6 @@
</tal:searchOrGroup> </tal:searchOrGroup>
</tal:section> </tal:section>
<tal:comment replace="nothing">All objects in flavour</tal:comment>
<!--dt class="portletAppyItem" tal:define="flavourInfo python: flavours[0]">
<a tal:define="flavourNumber flavourInfo/number;
rootTypes python: test(flavourNumber==1, rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootTypes)"
tal:content="flavourInfo/title"
tal:attributes="title python: tool.translate('query_consult_all');
href python:'%s?type_name=%s&flavourNumber=%d' % (queryUrl, rootClassesQuery, flavourNumber)"></a>
</dt-->
<tal:comment replace="nothing"> <tal:comment replace="nothing">
Greyed transparent zone that is deployed on the Greyed transparent zone that is deployed on the
whole screen when a popup is displayed. whole screen when a popup is displayed.
@ -133,8 +121,9 @@
<div id="confirmActionPopup" class="appyPopup"> <div id="confirmActionPopup" class="appyPopup">
<form id="confirmActionForm" method="post"> <form id="confirmActionForm" method="post">
<div align="center"> <div align="center">
<p tal:content="python: tool.translate('confirm')"></p> <p id="appyConfirmText"></p>
<input type="hidden" name="actionFormId"/> <input type="hidden" name="actionType"/>
<input type="hidden" name="action"/>
<input type="button" onClick="doConfirm()" <input type="button" onClick="doConfirm()"
tal:attributes="value python:tool.translate('yes')"/> tal:attributes="value python:tool.translate('yes')"/>
<input type="button" value="No" onClick="closePopup('confirmActionPopup')" <input type="button" value="No" onClick="closePopup('confirmActionPopup')"

View file

@ -16,8 +16,6 @@
appName appFolder/id; appName appFolder/id;
tool python: portal.get('portal_%s' % appName.lower()); tool python: portal.get('portal_%s' % appName.lower());
contentType python:context.REQUEST.get('type_name'); contentType python:context.REQUEST.get('type_name');
flavour python: tool.getFlavour(contentType);
flavourNumber python:int(context.REQUEST.get('flavourNumber', 1));
searchName python:context.REQUEST.get('search', '')"> searchName python:context.REQUEST.get('search', '')">
<div metal:use-macro="here/skyn/page/macros/prologue"/> <div metal:use-macro="here/skyn/page/macros/prologue"/>
@ -25,8 +23,8 @@
<div id="queryResult"></div> <div id="queryResult"></div>
<script language="javascript" <script language="javascript"
tal:define="ajaxUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)" tal:define="ajaxUrl python: tool.getQueryUrl(contentType, searchName)"
tal:content="python: 'askQueryResult(\'queryResult\', \'%s\',\'%s\',\'%s\',\'%s\',0)' % (tool.absolute_url(), contentType, flavourNumber, searchName)"> tal:content="python: 'askQueryResult(\'queryResult\', \'%s\',\'%s\',\'%s\',0)' % (tool.absolute_url(), contentType, searchName)">
</script> </script>
</div> </div>
</body> </body>

View file

@ -16,8 +16,7 @@
tal:define="appFolder context/getParentNode; tal:define="appFolder context/getParentNode;
contentType request/type_name; contentType request/type_name;
tool python: portal.get('portal_%s' % appFolder.id.lower()); tool python: portal.get('portal_%s' % appFolder.id.lower());
flavour python: tool.getFlavour('Dummy_%s' % request['flavourNumber']); searchableFields python: tool.getSearchableFields(contentType)">
searchableFields python: flavour.getSearchableFields(contentType)">
<tal:comment replace="nothing">Search title</tal:comment> <tal:comment replace="nothing">Search title</tal:comment>
<h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> — <h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> —
@ -27,10 +26,9 @@
<form name="search" tal:attributes="action python: appFolder.absolute_url()+'/skyn/do'" method="post"> <form name="search" tal:attributes="action python: appFolder.absolute_url()+'/skyn/do'" method="post">
<input type="hidden" name="action" value="SearchObjects"/> <input type="hidden" name="action" value="SearchObjects"/>
<input type="hidden" name="type_name" tal:attributes="value contentType"/> <input type="hidden" name="type_name" tal:attributes="value contentType"/>
<input type="hidden" name="flavourNumber:int" tal:attributes="value request/flavourNumber"/>
<table class="no-style-table" cellpadding="0" cellspacing="0" width="100%" <table class="no-style-table" cellpadding="0" cellspacing="0" width="100%"
tal:define="numberOfColumns python: flavour.getAttr('numberOfSearchColumnsFor%s' % contentType)"> tal:define="numberOfColumns python: tool.getAttr('numberOfSearchColumnsFor%s' % contentType)">
<tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top"> <tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top">
<td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)"> <td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)">
<tal:field condition="widget"> <tal:field condition="widget">

View file

@ -21,13 +21,12 @@
layoutType python:'view'; layoutType python:'view';
layout python: contextObj.getPageLayout(layoutType); layout python: contextObj.getPageLayout(layoutType);
tool contextObj/getTool; tool contextObj/getTool;
flavour python: tool.getFlavour(contextObj);
appFolder tool/getAppFolder; appFolder tool/getAppFolder;
appName appFolder/getId; appName appFolder/getId;
page request/page|python:'main'; page request/page|python:'main';
phaseInfo python: contextObj.getAppyPhases(page=page); phaseInfo python: contextObj.getAppyPhases(page=page);
phase phaseInfo/name; phase phaseInfo/name;
showWorkflow python: flavour.getAttr('showWorkflowFor' + contextObj.meta_type)"> showWorkflow python: tool.getAttr('showWorkflowFor' + contextObj.meta_type)">
<metal:prologue use-macro="here/skyn/page/macros/prologue"/> <metal:prologue use-macro="here/skyn/page/macros/prologue"/>
<metal:show use-macro="here/skyn/page/macros/show"/> <metal:show use-macro="here/skyn/page/macros/show"/>
<metal:footer use-macro="here/skyn/page/macros/footer"/> <metal:footer use-macro="here/skyn/page/macros/footer"/>

View file

@ -2,14 +2,15 @@
<metal:view define-macro="view"> <metal:view define-macro="view">
<form name="executeAppyAction" <form name="executeAppyAction"
tal:define="formId python: '%s_%s' % (contextObj.UID(), name); tal:define="formId python: '%s_%s' % (contextObj.UID(), name);
label python: contextObj.translate(widget['labelId'])" label python: contextObj.translate(widget['labelId']);
labelConfirm python: contextObj.translate(widget['labelId'] + '_confirm')"
tal:attributes="id formId; action python: contextObj.absolute_url()+'/skyn/do'"> tal:attributes="id formId; action python: contextObj.absolute_url()+'/skyn/do'">
<input type="hidden" name="action" value="ExecuteAppyAction"/> <input type="hidden" name="action" value="ExecuteAppyAction"/>
<input type="hidden" name="objectUid" tal:attributes="value contextObj/UID"/> <input type="hidden" name="objectUid" tal:attributes="value contextObj/UID"/>
<input type="hidden" name="fieldName" tal:attributes="value name"/> <input type="hidden" name="fieldName" tal:attributes="value name"/>
<input type="button" tal:condition="widget/confirm" <input type="button" tal:condition="widget/confirm"
tal:attributes="value label; tal:attributes="value label;
onClick python: 'askConfirm(\'%s\')' % formId"/> onClick python: 'askConfirm(\'form\', \'%s\', &quot;%s&quot;)' % (formId, labelConfirm)"/>
<input type="submit" name="do" tal:condition="not: widget/confirm" <input type="submit" name="do" tal:condition="not: widget/confirm"
tal:attributes="value label" onClick="javascript:;"/> tal:attributes="value label" onClick="javascript:;"/>
<tal:comment replace="nothing">The previous onClick is simply used to prevent Plone <tal:comment replace="nothing">The previous onClick is simply used to prevent Plone

View file

@ -7,9 +7,9 @@
<label tal:attributes="for chekboxId" class="discreet" <label tal:attributes="for chekboxId" class="discreet"
tal:content="python: tool.translate(doLabel)"></label> tal:content="python: tool.translate(doLabel)"></label>
</tal:askAction> </tal:askAction>
<img tal:repeat="podFormat python:flavour.getPodInfo(contextObj, name)['formats']" <img tal:repeat="podFormat python: tool.getPodInfo(contextObj, name)['formats']"
tal:attributes="src string: $portal_url/skyn/${podFormat}.png; tal:attributes="src string: $portal_url/skyn/${podFormat}.png;
onClick python: 'generatePodDocument(\'%s\',\'\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat); onClick python: 'generatePodDocument(\'%s\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat);
title podFormat/capitalize" title podFormat/capitalize"
style="cursor:pointer"/> style="cursor:pointer"/>
</metal:view> </metal:view>

View file

@ -10,7 +10,7 @@
from one object to the next/previous on skyn/view.</tal:comment> from one object to the next/previous on skyn/view.</tal:comment>
<a tal:define="includeShownInfo includeShownInfo | python:False; <a tal:define="includeShownInfo includeShownInfo | python:False;
navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], repeat['obj'].number()+startNumber, totalNumber); navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], repeat['obj'].number()+startNumber, totalNumber);
navInfo python: appyType['isBack'] and '' or navInfo; navInfo python: test(appyType['isBack'], '', navInfo);
pageName python: appyType['isBack'] and appyType['backd']['page'] or 'main'; pageName python: appyType['isBack'] and appyType['backd']['page'] or 'main';
fullUrl python: obj.getUrl(page=pageName, nav=navInfo)" fullUrl python: obj.getUrl(page=pageName, nav=navInfo)"
tal:attributes="href fullUrl" tal:content="python: (not includeShownInfo) and obj.Title() or contextObj.getReferenceLabel(fieldName, obj.appy())"></a> tal:attributes="href fullUrl" tal:content="python: (not includeShownInfo) and obj.Title() or contextObj.getReferenceLabel(fieldName, obj.appy())"></a>
@ -40,13 +40,13 @@
</tal:moveRef> </tal:moveRef>
</td> </td>
<tal:comment replace="nothing">Edit the element</tal:comment> <tal:comment replace="nothing">Edit the element</tal:comment>
<td class="noPadding"> <td class="noPadding" tal:condition="python: member.has_permission('Modify portal content', obj) and not appyType['noForm']">
<a tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], repeat['obj'].number()+startNumber, totalNumber);" <a tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], repeat['obj'].number()+startNumber, totalNumber);"
tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)" tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)">
tal:condition="python: member.has_permission('Modify portal content', obj)">
<img title="label_edit" i18n:domain="plone" i18n:attributes="title" <img title="label_edit" i18n:domain="plone" i18n:attributes="title"
tal:attributes="src string: $portal_url/skyn/edit.gif"/> tal:attributes="src string: $portal_url/skyn/edit.gif"/>
</a></td> </a>
</td>
<tal:comment replace="nothing">Delete the element</tal:comment> <tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"> <td class="noPadding">
<img tal:condition="python: member.has_permission('Delete objects', obj)" <img tal:condition="python: member.has_permission('Delete objects', obj)"
@ -63,10 +63,14 @@
through a reference widget. Indeed, If field was declared as "addable", we must provide through a reference widget. Indeed, If field was declared as "addable", we must provide
an icon for creating a new linked object (at least if multiplicities allow it).</tal:comment> an icon for creating a new linked object (at least if multiplicities allow it).</tal:comment>
<img style="cursor:pointer" tal:condition="showPlusIcon" <img style="cursor:pointer" tal:condition="showPlusIcon"
tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], 0, totalNumber);" tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['page'], 0, totalNumber);
formCall python:'window.location=\'%s/skyn/do?action=Create&type_name=%s&nav=%s\'' % (folder.absolute_url(), linkedPortalType, navInfo);
formCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', &quot;%s&quot;, &quot;%s&quot;)' % (formCall, addConfirmMsg), formCall);
noFormCall python: navBaseCall.replace('**v**', '%d, \'CreateWithoutForm\'' % startNumber);
noFormCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', &quot;%s&quot;, &quot;%s&quot;)' % (noFormCall, addConfirmMsg), noFormCall)"
tal:attributes="src string:$portal_url/skyn/plus.png; tal:attributes="src string:$portal_url/skyn/plus.png;
title python: tool.translate('add_ref'); title python: tool.translate('add_ref');
onClick python: 'href: window.location=\'%s/skyn/do?action=Create&type_name=%s&nav=%s\'' % (folder.absolute_url(), linkedPortalType, navInfo)"/> onClick python: test(appyType['noForm'], noFormCall, formCall)"/>
</metal:plusIcon> </metal:plusIcon>
<tal:comment replace="nothing"> <tal:comment replace="nothing">
@ -109,8 +113,7 @@
totalNumber refObjects/totalNumber; totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize; batchSize refObjects/batchSize;
folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode(); folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode();
flavour python:tool.getFlavour(contextObj); linkedPortalType python: tool.getPortalType(appyType['klass']);
linkedPortalType python:flavour.getPortalType(appyType['klass']);
addPermission python: '%s: Add %s' % (tool.getAppName(), linkedPortalType); addPermission python: '%s: Add %s' % (tool.getAppName(), linkedPortalType);
canWrite python: not appyType['isBack'] and member.has_permission(appyType['writePermission'], contextObj); canWrite python: not appyType['isBack'] and member.has_permission(appyType['writePermission'], contextObj);
multiplicity appyType/multiplicity; multiplicity appyType/multiplicity;
@ -118,6 +121,7 @@
showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite; showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1); atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(appyType['labelId']); label python: tool.translate(appyType['labelId']);
addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']);
description python: tool.translate(appyType['descrId']); description python: tool.translate(appyType['descrId']);
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)"> navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
@ -240,13 +244,11 @@
</fieldset> </fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment> <tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment>
<br tal:define="widgetDescr widgetDescr|nothing"
tal:condition="python: not widgetDescr or (widgetDescr['widgetType'] != 'group')"/>
</tal:anyNumberOfReferences> </tal:anyNumberOfReferences>
</div> </div>
<tal:comment replace="nothing">Edit macro for an Ref.</tal:comment> <tal:comment replace="nothing">Edit macro for an Ref.</tal:comment>
<div metal:define-macro="edit" <metal:editRef define-macro="edit"
tal:condition="widget/link" tal:condition="widget/link"
tal:define="rname python: 'appy_ref_%s' % name; tal:define="rname python: 'appy_ref_%s' % name;
requestValue python: request.get(rname, []); requestValue python: request.get(rname, []);
@ -255,18 +257,18 @@
refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']]; refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']];
isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())"> isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())">
<select tal:attributes="name rname; <select tal:attributes="name rname;
multiple python: isMultiple and 'multiple' or ''"> multiple python: isMultiple and 'multiple' or ''">
<option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option> <option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option>
<tal:ref repeat="refObj allObjects"> <tal:ref repeat="refObj allObjects">
<option tal:define="uid python: contextObj.getReferenceUid(refObj)" <option tal:define="uid python: contextObj.getReferenceUid(refObj)"
tal:content="python: contextObj.getReferenceLabel(name, refObj)" tal:content="python: contextObj.getReferenceLabel(name, refObj)"
tal:attributes="value uid; tal:attributes="value uid;
selected python:(inRequest and (uid in requestValue) or (not inRequest and ((uid in refUids)))) and True or False"> selected python:(inRequest and (uid in requestValue) or (not inRequest and ((uid in refUids)))) and True or False">
</option> </option>
</tal:ref> </tal:ref>
</select> </select>
</div> </metal:editRef>
<tal:comment replace="nothing">Cell macro for a Ref.</tal:comment> <tal:comment replace="nothing">Cell macro for a Ref.</tal:comment>
<metal:cell define-macro="cell"> <metal:cell define-macro="cell">

View file

@ -90,7 +90,9 @@
<metal:content use-macro="portal/skyn/widgets/show/macros/groupContent"/> <metal:content use-macro="portal/skyn/widgets/show/macros/groupContent"/>
</tal:asSection> </tal:asSection>
<tal:asTabs condition="python: widget['style'] == 'tabs'"> <tal:asTabs condition="python: widget['style'] == 'tabs'">
<table cellpadding="0" cellspacing="0" tal:attributes="width python: test(widget['wide'], '100%', '')"> <table cellpadding="0" cellspacing="0"
tal:attributes="width python: test(widget['wide'], '100%', '');
class widget/css_class">
<tal:comment replace="nothing">First row: the tabs.</tal:comment> <tal:comment replace="nothing">First row: the tabs.</tal:comment>
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040"> <tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table cellpadding="0" cellspacing="0" style="position:relative; bottom:-1px;"> <table cellpadding="0" cellspacing="0" style="position:relative; bottom:-1px;">
@ -137,7 +139,8 @@
</tal:comment> </tal:comment>
<table metal:define-macro="groupContent" <table metal:define-macro="groupContent"
tal:attributes="width python: test(widget['wide'], '100%', ''); tal:attributes="width python: test(widget['wide'], '100%', '');
align widget/align"> align widget/align;
class widget/css_class">
<tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment> <tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment>
<tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']"> <tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']">
<td tal:attributes="colspan python: len(widget['columnsWidths']); <td tal:attributes="colspan python: len(widget['columnsWidths']);

View file

@ -4,7 +4,7 @@ from DateTime import DateTime
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config import Products.<!applicationName!>.config
from Extensions.appyWrappers import <!genClassName!>_Wrapper from Extensions.appyWrappers import <!genClassName!>_Wrapper
from appy.gen.plone25.mixins.ClassMixin import ClassMixin from appy.gen.plone25.mixins import BaseMixin
<!imports!> <!imports!>
schema = Schema((<!fields!> schema = Schema((<!fields!>
@ -29,7 +29,7 @@ class <!genClassName!>(<!parents!>):
i18nDomain = '<!applicationName!>' i18nDomain = '<!applicationName!>'
schema = fullSchema schema = fullSchema
wrapperClass = <!genClassName!>_Wrapper wrapperClass = <!genClassName!>_Wrapper
for elem in dir(ClassMixin): for elem in dir(BaseMixin):
if not elem.startswith('__'): security.declarePublic(elem) if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!> <!commonMethods!>
<!methods!> <!methods!>

View file

@ -1,36 +0,0 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from Extensions.appyWrappers import <!wrapperClass!>
schema = Schema((<!fields!>
),)
fullSchema = OrderedBaseFolderSchema.copy() + schema.copy()
class <!flavourName!>(OrderedBaseFolder, FlavourMixin):
'''Configuration flavour class for <!applicationName!>.'''
security = ClassSecurityInfo()
__implements__ = (getattr(OrderedBaseFolderSchema,'__implements__',()),)
archetype_name = '<!flavourName!>'
meta_type = '<!flavourName!>'
portal_type = '<!flavourName!>'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
immediate_view = 'skyn/view'
default_view = 'skyn/view'
suppl_views = ()
typeDescription = "<!flavourName!>"
typeDescMsgId = '<!flavourName!>_edit_descr'
i18nDomain = '<!applicationName!>'
schema = fullSchema
allMetaTypes = <!metaTypes!>
wrapperClass = <!wrapperClass!>
for elem in dir(FlavourMixin):
if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!>
<!methods!>
registerType(<!flavourName!>, '<!applicationName!>')

View file

@ -1,34 +0,0 @@
<!codeHeader!>
from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.PodTemplateMixin import PodTemplateMixin
from Extensions.appyWrappers import <!wrapperClass!>
schema = Schema((<!fields!>
),)
fullSchema = BaseSchema.copy() + schema.copy()
class <!applicationName!>PodTemplate(BaseContent, PodTemplateMixin):
'''POD template.'''
security = ClassSecurityInfo()
__implements__ = (getattr(BaseContent,'__implements__',()),)
archetype_name = '<!applicationName!>PodTemplate'
meta_type = '<!applicationName!>PodTemplate'
portal_type = '<!applicationName!>PodTemplate'
allowed_content_types = []
filter_content_types = 0
global_allow = 1
immediate_view = 'skyn/view'
default_view = 'skyn/view'
suppl_views = ()
typeDescription = "<!applicationName!>PodTemplate"
typeDescMsgId = '<!applicationName!>PodTemplate_edit_descr'
wrapperClass = <!wrapperClass!>
schema = fullSchema
for elem in dir(PodTemplateMixin):
if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!>
<!methods!>
registerType(<!applicationName!>PodTemplate, '<!applicationName!>')

View file

@ -3,15 +3,13 @@
i18n:domain="<!applicationName!>"> i18n:domain="<!applicationName!>">
<body> <body>
<div metal:define-macro="portlet" <div metal:define-macro="portlet"
tal:define="tool python: context.<!toolInstanceName!>; tal:define="tool python: context.<!toolInstanceName!>;"
flavour python: tool.getFlavour(tool);"
tal:condition="python: tool.showPortlet(context)"> tal:condition="python: tool.showPortlet(context)">
<metal:block metal:use-macro="here/global_defines/macros/defines" /> <metal:block metal:use-macro="here/global_defines/macros/defines" />
<metal:prologue use-macro="here/skyn/page/macros/prologue"/> <metal:prologue use-macro="here/skyn/page/macros/prologue"/>
<dl tal:define="rootClasses tool/getRootClasses; <dl tal:define="rootClasses tool/getRootClasses;
appName string:<!applicationName!>; appName string:<!applicationName!>;
appFolder tool/getAppFolder; appFolder tool/getAppFolder" class="portlet">
flavours tool/getFlavoursInfo" class="portlet">
<metal:content use-macro="here/skyn/portlet/macros/portletContent"/> <metal:content use-macro="here/skyn/portlet/macros/portletContent"/>
</dl> </dl>
</div> </div>

View file

@ -36,24 +36,22 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
border-width: thin; border-width: thin;
text-align: center; text-align: center;
padding: 0.1em 1em 0.1em 1.3em; padding: 0.1em 1em 0.1em 1.3em;
background-position: -1px 4px;
} }
.appyChanges th { .appyChanges th {
font-style: italic; font-style: italic;
background-color: transparent; background-color: transparent;
border-bottom: 1px dashed #8CACBB; border: 0 none transparent;
border-top: 0 none transparent;
border-left: 0 none transparent;
border-right: 0 none transparent;
padding: 0.1em 0.1em 0.1em 0.1em; padding: 0.1em 0.1em 0.1em 0.1em;
} }
.appyChanges td { .appyChanges td {
padding: 0.1em 0.1em 0.1em 0.1em !important; padding: 0.1em 0.2em 0.1em 0.2em !important;
border-top: 1px dashed #8CACBB !important;
border-right: 0 none transparent !important; border-right: 0 none transparent !important;
border-top: 0 none transparent; border-left: 0 none transparent !important;
border-left: 0 none transparent; border-bottom: 0 none transparent !important;
border-right: 0 none transparent;
} }
.appyHistory { .appyHistory {
@ -70,6 +68,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: -1px 7px; background-position: -1px 7px;
} }
.stepDoneState {
background-color: #cde2a7;
background-image: url(&dtml-portal_url;/skyn/done.png);
background-repeat: no-repeat;
background-position: -1px 4px;
}
.stepCurrent { .stepCurrent {
background-color: #eef3f5; background-color: #eef3f5;
@ -77,6 +81,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: -1px 7px; background-position: -1px 7px;
} }
.stepCurrentState {
background-color: #eef3f5;
background-image: url(&dtml-portal_url;/skyn/current.png);
background-repeat: no-repeat;
background-position: -1px 4px;
}
.stepFuture { .stepFuture {
background-color: #ffffff; background-color: #ffffff;

View file

@ -29,6 +29,7 @@ class <!toolName!>(UniqueObject, OrderedBaseFolder, ToolMixin):
typeDescription = "<!toolName!>" typeDescription = "<!toolName!>"
typeDescMsgId = '<!toolName!>_edit_descr' typeDescMsgId = '<!toolName!>_edit_descr'
i18nDomain = '<!applicationName!>' i18nDomain = '<!applicationName!>'
allMetaTypes = <!metaTypes!>
wrapperClass = <!wrapperClass!> wrapperClass = <!wrapperClass!>
schema = fullSchema schema = fullSchema
schema["id"].widget.visible = False schema["id"].widget.visible = False

View file

@ -2,15 +2,15 @@
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.UserMixin import UserMixin from appy.gen.plone25.mixins import BaseMixin
from Extensions.appyWrappers import <!wrapperClass!> from Extensions.appyWrappers import <!wrapperClass!>
schema = Schema((<!fields!> schema = Schema((<!fields!>
),) ),)
fullSchema = BaseSchema.copy() + schema.copy() fullSchema = BaseSchema.copy() + schema.copy()
class <!applicationName!>User(BaseContent, UserMixin): class <!applicationName!>User(BaseContent, BaseMixin):
'''Configuration flavour class for <!applicationName!>.''' '''User mixin.'''
security = ClassSecurityInfo() security = ClassSecurityInfo()
__implements__ = (getattr(BaseContent,'__implements__',()),) __implements__ = (getattr(BaseContent,'__implements__',()),)
archetype_name = '<!applicationName!>User' archetype_name = '<!applicationName!>User'
@ -27,7 +27,7 @@ class <!applicationName!>User(BaseContent, UserMixin):
i18nDomain = '<!applicationName!>' i18nDomain = '<!applicationName!>'
schema = fullSchema schema = fullSchema
wrapperClass = <!wrapperClass!> wrapperClass = <!wrapperClass!>
for elem in dir(UserMixin): for elem in dir(BaseMixin):
if not elem.startswith('__'): security.declarePublic(elem) if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!> <!commonMethods!>
<!methods!> <!methods!>

View file

@ -2,23 +2,14 @@
from appy.gen import * from appy.gen import *
from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper
from appy.gen.plone25.wrappers.FlavourWrapper import FlavourWrapper
from appy.gen.plone25.wrappers.PodTemplateWrapper import PodTemplateWrapper
from appy.gen.plone25.wrappers.UserWrapper import UserWrapper from appy.gen.plone25.wrappers.UserWrapper import UserWrapper
from Globals import InitializeClass from Globals import InitializeClass
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
<!imports!> <!imports!>
class PodTemplate(PodTemplateWrapper):
'''This class represents a POD template for this application.'''
<!podTemplateBody!>
class User(UserWrapper): class User(UserWrapper):
'''This class represents a user.''' '''This class represents a user.'''
<!userBody!> <!userBody!>
class Flavour(FlavourWrapper):
'''This class represents the Appy class used for defining a flavour.'''
folder=True
<!flavourBody!>
class Tool(ToolWrapper): class Tool(ToolWrapper):
'''This class represents the tool for this application.''' '''This class represents the tool for this application.'''
folder=True folder=True

View file

@ -42,7 +42,7 @@ ADD_CONTENT_PERMISSIONS = {
<!addPermissions!>} <!addPermissions!>}
setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles)) setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, tuple(defaultAddRoles))
# Applications classes, in various formats and flavours # Applications classes, in various formats
rootClasses = [<!rootClasses!>] rootClasses = [<!rootClasses!>]
appClasses = <!appClasses!> appClasses = <!appClasses!>
appClassNames = [<!appClassNames!>] appClassNames = [<!appClassNames!>]

View file

@ -155,9 +155,8 @@ def do(transitionName, stateChange, logger):
if hasattr(ploneObj, '_v_appy_do') and \ if hasattr(ploneObj, '_v_appy_do') and \
not ploneObj._v_appy_do['doNotify']: not ploneObj._v_appy_do['doNotify']:
doNotify = False doNotify = False
elif not ploneObj.getTool().getFlavour( elif not getattr(ploneObj.getTool().appy(), 'enableNotifications'):
ploneObj).getEnableNotifications(): # We do not notify if the "notify" flag in the tool is disabled.
# We do not notify if the "notify" flag in the flavour is disabled.
doNotify = False doNotify = False
if doAction or doNotify: if doAction or doNotify:
obj = ploneObj.appy() obj = ploneObj.appy()

View file

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

View file

@ -1,6 +0,0 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------
class PodTemplateWrapper(AbstractWrapper): pass
# ------------------------------------------------------------------------------

View file

@ -44,4 +44,65 @@ class ToolWrapper(AbstractWrapper):
def getDiskFolder(self): def getDiskFolder(self):
'''Returns the disk folder where the Appy application is stored.''' '''Returns the disk folder where the Appy application is stored.'''
return self.o.getProductConfig().diskFolder 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -3,6 +3,18 @@ from appy.gen.plone25.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class UserWrapper(AbstractWrapper): class UserWrapper(AbstractWrapper):
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):
customUser.__dict__[methodName](self, *args, **kwargs)
def showLogin(self): def showLogin(self):
'''When must we show the login field?''' '''When must we show the login field?'''
if self.o.isTemporary(): return 'edit' if self.o.isTemporary(): return 'edit'
@ -51,6 +63,7 @@ class UserWrapper(AbstractWrapper):
msg = self.translate(u'Passwords do not match.', domain='plone') msg = self.translate(u'Passwords do not match.', domain='plone')
errors.password1 = msg errors.password1 = msg
errors.password2 = msg errors.password2 = msg
self._callCustom('validate', new, errors)
def onEdit(self, created): def onEdit(self, created):
self.title = self.firstName + ' ' + self.name self.title = self.firstName + ' ' + self.name
@ -86,11 +99,5 @@ class UserWrapper(AbstractWrapper):
# Remove the user if it was in the corresponding group # Remove the user if it was in the corresponding group
if groupName in userGroups: if groupName in userGroups:
group.removeMember(self.login) group.removeMember(self.login)
# Call the custom user "onEdit" method if it exists self._callCustom('onEdit', created)
# XXX This code does not work.
if len(self.__class__.__bases__) > 1:
customUser = self.__class__.__bases__[-1]
# There is a custom user class
if customUser.__dict__.has_key('onEdit'):
customUser.__dict__['onEdit'](self, created)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -82,8 +82,6 @@ class AbstractWrapper:
else: return 1 else: return 1
def get_tool(self): return self.o.getTool().appy() def get_tool(self): return self.o.getTool().appy()
tool = property(get_tool) tool = property(get_tool)
def get_flavour(self): return self.o.getTool().getFlavour(self.o, appy=True)
flavour = property(get_flavour)
def get_request(self): return self.o.REQUEST def get_request(self): return self.o.REQUEST
request = property(get_request) request = property(get_request)
def get_session(self): return self.o.REQUEST.SESSION def get_session(self): return self.o.REQUEST.SESSION
@ -204,9 +202,9 @@ class AbstractWrapper:
ploneObj.reindexObject() ploneObj.reindexObject()
return appyObj return appyObj
def translate(self, label, mapping={}, domain=None): def translate(self, label, mapping={}, domain=None, language=None):
'''Check documentation of self.o.translate.''' '''Check documentation of self.o.translate.'''
return self.o.translate(label, mapping, domain) return self.o.translate(label, mapping, domain, language=language)
def do(self, transition, comment='', doAction=False, doNotify=False, def do(self, transition, comment='', doAction=False, doNotify=False,
doHistory=True): doHistory=True):
@ -276,27 +274,25 @@ class AbstractWrapper:
p_maxResults. If p_noSecurity is specified, you get all objects, p_maxResults. If p_noSecurity is specified, you get all objects,
even if the logged user does not have the permission to view it.''' even if the logged user does not have the permission to view it.'''
# Find the content type corresponding to p_klass # Find the content type corresponding to p_klass
flavour = self.flavour contentType = self.tool.o.getPortalType(klass)
contentType = flavour.o.getPortalType(klass)
# Create the Search object # Create the Search object
search = Search('customSearch', sortBy=sortBy, **fields) search = Search('customSearch', sortBy=sortBy, **fields)
if not maxResults: if not maxResults:
maxResults = 'NO_LIMIT' maxResults = 'NO_LIMIT'
# If I let maxResults=None, only a subset of the results will be # If I let maxResults=None, only a subset of the results will be
# returned by method executeResult. # returned by method executeResult.
res = self.tool.o.executeQuery(contentType,flavour.number,search=search, res = self.tool.o.executeQuery(contentType, search=search,
maxResults=maxResults, noSecurity=noSecurity) maxResults=maxResults, noSecurity=noSecurity)
return [o.appy() for o in res['objects']] return [o.appy() for o in res['objects']]
def count(self, klass, noSecurity=False, **fields): def count(self, klass, noSecurity=False, **fields):
'''Identical to m_search above, but returns the number of objects that '''Identical to m_search above, but returns the number of objects that
match the search instead of returning the objects themselves. Use match the search instead of returning the objects themselves. Use
this method instead of writing len(self.search(...)).''' this method instead of writing len(self.search(...)).'''
flavour = self.flavour contentType = self.tool.o.getPortalType(klass)
contentType = flavour.o.getPortalType(klass)
search = Search('customSearch', **fields) search = Search('customSearch', **fields)
res = self.tool.o.executeQuery(contentType,flavour.number,search=search, res = self.tool.o.executeQuery(contentType, search=search,
brainsOnly=True, noSecurity=noSecurity) brainsOnly=True, noSecurity=noSecurity)
if res: return res._len # It is a LazyMap instance if res: return res._len # It is a LazyMap instance
else: return 0 else: return 0
@ -325,14 +321,12 @@ class AbstractWrapper:
"for obj in self.search(MyClass,...)" "for obj in self.search(MyClass,...)"
''' '''
flavour = self.flavour contentType = self.tool.o.getPortalType(klass)
contentType = flavour.o.getPortalType(klass)
search = Search('customSearch', sortBy=sortBy, **fields) search = Search('customSearch', sortBy=sortBy, **fields)
# Initialize the context variable "ctx" # Initialize the context variable "ctx"
ctx = context ctx = context
for brain in self.tool.o.executeQuery(contentType, flavour.number, \ for brain in self.tool.o.executeQuery(contentType, search=search, \
search=search, brainsOnly=True, maxResults=maxResults, brainsOnly=True, maxResults=maxResults, noSecurity=noSecurity):
noSecurity=noSecurity):
# Get the Appy object from the brain # Get the Appy object from the brain
obj = brain.getObject().appy() obj = brain.getObject().appy()
exec expression exec expression
@ -379,5 +373,5 @@ class AbstractWrapper:
p_data must be a dictionary whose keys are field names (strings) and p_data must be a dictionary whose keys are field names (strings) and
whose values are the previous field values.''' whose values are the previous field values.'''
self.o.addDataChange(data, labels=False) self.o.addDataChange(data)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -26,20 +26,22 @@ fallbacks = {'en': 'en-us en-ca',
class PoMessage: class PoMessage:
'''Represents a i18n message (po format).''' '''Represents a i18n message (po format).'''
CONFIG = "Configuration panel for product '%s'" CONFIG = "Configuration panel for product '%s'"
FLAVOUR = "Configuration flavour" # The following messages (starting with MSG_) correspond to tool
# The following messages (starting with MSG_) correspond to flavour
# attributes added for every gen-class (warning: the message IDs correspond # attributes added for every gen-class (warning: the message IDs correspond
# to MSG_<attributePrexix>). # to MSG_<attributePrefix>).
MSG_optionalFieldsFor = 'Optional fields' MSG_defaultValue = "Default value for field '%s'"
MSG_defaultValueFor = "Default value for field '%s'" MSG_podTemplate = "POD template for field '%s'"
MSG_podTemplatesFor = "POD templates" MSG_formats = "Output format(s) for field '%s'"
MSG_podMaxShownTemplatesFor = "Max shown POD templates" MSG_resultColumns = "Columns to display while showing query results"
MSG_resultColumnsFor = "Columns to display while showing query results" MSG_enableAdvancedSearch = "Enable advanced search"
MSG_showWorkflowFor = 'Show workflow-related information' MSG_numberOfSearchColumns = "Number of search columns"
MSG_showWorkflowCommentFieldFor = 'Show field allowing to enter a ' \ MSG_searchFields = "Search fields"
'comment every time a transition is triggered' MSG_optionalFields = 'Optional fields'
MSG_showAllStatesInPhaseFor = 'Show all states in phase' MSG_showWorkflow = 'Show workflow-related information'
POD_TEMPLATE = 'POD template' MSG_showWorkflowCommentField = 'Show field allowing to enter a ' \
'comment every time a transition is ' \
'triggered'
MSG_showAllStatesInPhase = 'Show all states in phase'
USER = 'User' USER = 'User'
POD_ASKACTION = 'Trigger related action' POD_ASKACTION = 'Trigger related action'
DEFAULT_VALID_ERROR = 'Please fill or correct this.' DEFAULT_VALID_ERROR = 'Please fill or correct this.'

View file

@ -32,6 +32,3 @@ class StandardRadio(Radio):
c = Config() c = Config()
c.languages = ('en', 'fr') c.languages = ('en', 'fr')
class CarFlavour(Flavour):
explanation = String(group="userInterface")

View file

@ -9,16 +9,6 @@ class ZopeComponentTool(Tool):
self.someUsefulConfigurationOption = 'My app is configured now!' self.someUsefulConfigurationOption = 'My app is configured now!'
install = Action(action=onInstall) install = Action(action=onInstall)
class ZopeComponentFlavour(Flavour):
anIntegerOption = Integer()
bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
link=False, back=Ref(attribute='backToTool'),
shownInfo=('description',), page='data')
def onEdit(self, created):
if 'Escadron de la mort' not in [b.title for b in self.bunchesOfGeeks]:
self.create('bunchesOfGeeks', title='Escadron de la mort',
description='I want those guys everywhere!')
class ZopeComponentWorkflow: class ZopeComponentWorkflow:
# Specific permissions # Specific permissions
wf = WritePermission('ZopeComponent.funeralDate') wf = WritePermission('ZopeComponent.funeralDate')

107
shared/dav.py Normal file
View file

@ -0,0 +1,107 @@
# ------------------------------------------------------------------------------
import os, re, httplib, sys
from StringIO import StringIO
from mimetypes import guess_type
from base64 import encodestring
# ------------------------------------------------------------------------------
urlRex = re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I)
# ------------------------------------------------------------------------------
class Resource:
'''Every instance of this class represents some web resource accessible
through WebDAV.'''
def __init__(self, url, username=None, password=None):
self.username = username
self.password = password
self.url = url
# Split the URL into its components
res = urlRex.match(url)
if res:
host, port, uri = res.group(1,2,3)
self.host = host
self.port = port and int(port[1:]) or 80
self.uri = uri or '/'
else: raise 'Wrong URL: %s' % str(url)
def updateHeaders(self, headers):
# Add credentials if present
if not (self.username and self.password): return
if headers.has_key('Authorization'): return
credentials = '%s:%s' % (self.username,self.password)
credentials = credentials.replace('\012','')
headers['Authorization'] = "Basic %s" % encodestring(credentials)
headers['User-Agent'] = 'WebDAV.client'
headers['Host'] = self.host
headers['Connection'] = 'close'
headers['Accept'] = '*/*'
return headers
def sendRequest(self, method, uri, body=None, headers={}):
'''Sends a HTTP request with p_method, for p_uri.'''
conn = httplib.HTTP()
conn.connect(self.host, self.port)
conn.putrequest(method, uri)
# Add HTTP headers
self.updateHeaders(headers)
for n, v in headers.items(): conn.putheader(n, v)
conn.endheaders()
if body: conn.send(body)
ver, code, msg = conn.getreply()
data = conn.getfile().read()
conn.close()
return data
def mkdir(self, name):
'''Creates a folder named p_name in this resource.'''
folderUri = self.uri + '/' + name
#body = '<d:propertyupdate xmlns:d="DAV:"><d:set><d:prop>' \
# '<d:displayname>%s</d:displayname></d:prop></d:set>' \
# '</d:propertyupdate>' % name
return self.sendRequest('MKCOL', folderUri)
def delete(self, name):
'''Deletes a file or a folder (and all contained files if any) named
p_name within this resource.'''
toDeleteUri = self.uri + '/' + name
return self.sendRequest('DELETE', toDeleteUri)
def add(self, content, type='fileName', name=''):
'''Adds a file in this resource. p_type can be:
- "fileName" In this case, p_content is the path to a file on disk
and p_name is ignored;
- "zope" In this case, p_content is an instance of
OFS.Image.File and the name of the file is given in
p_name.
'''
if type == 'fileName':
# p_content is the name of a file on disk
f = file(content, 'rb')
body = f.read()
f.close()
fileName = os.path.basename(content)
fileType, encoding = guess_type(fileName)
elif type == 'zope':
# p_content is a "Zope" file, ie a OFS.Image.File instance
fileName = name
fileType = content.content_type
encoding = None
if isinstance(content.data, basestring):
# The file content is here, in one piece
body = content.data
else:
# There are several parts to this file.
body = ''
data = content.data
while data is not None:
body += data.data
data = data.next
fileUri = self.uri + '/' + fileName
headers = {}
if fileType: headers['Content-Type'] = fileType
if encoding: headers['Content-Encoding'] = encoding
headers['Content-Length'] = str(len(body))
return self.sendRequest('PUT', fileUri, body, headers)
# ------------------------------------------------------------------------------

View file

@ -125,4 +125,153 @@ def normalizeString(s, usage='fileName'):
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
typeLetters = {'b': bool, 'i': int, 'j': long, 'f':float, 's':str, 'u':unicode, typeLetters = {'b': bool, 'i': int, 'j': long, 'f':float, 's':str, 'u':unicode,
'l': list, 'd': dict} 'l': list, 'd': dict}
# ------------------------------------------------------------------------------
class CodeAnalysis:
'''This class holds information about some code analysis (line counts) that
spans some folder hierarchy.'''
def __init__(self, name):
self.name = name # Let's give a name for the analysis
self.numberOfFiles = 0 # The total number of analysed files
self.emptyLines = 0 # The number of empty lines within those files
self.commentLines = 0 # The number of comment lines
# A code line is defined as anything that is not an empty or comment
# line.
self.codeLines = 0
def numberOfLines(self):
'''Computes the total number of lines within analysed files.'''
return self.emptyLines + self.commentLines + self.codeLines
def analyseZptFile(self, theFile):
'''Analyses the ZPT file named p_fileName.'''
inDoc = False
for line in theFile:
stripped = line.strip()
# Manage a comment
if not inDoc and (line.find('<tal:comment ') != -1):
inDoc = True
if inDoc:
self.commentLines += 1
if line.find('</tal:comment>') != -1:
inDoc = False
continue
# Manage an empty line
if not stripped:
self.emptyLines += 1
else:
self.codeLines += 1
docSeps = ('"""', "'''")
def isPythonDoc(self, line, start, isStart=False):
'''Returns True if we find, in p_line, the start of a docstring (if
p_start is True) or the end of a docstring (if p_start is False).
p_isStart indicates if p_line is the start of the docstring.'''
if start:
res = line.startswith(self.docSeps[0]) or \
line.startswith(self.docSeps[1])
else:
sepOnly = (line == self.docSeps[0]) or (line == self.docSeps[1])
if sepOnly:
# If the line contains the separator only, is this the start or
# the end of the docstring?
if isStart: res = False
else: res = True
else:
res = line.endswith(self.docSeps[0]) or \
line.endswith(self.docSeps[1])
return res
def analysePythonFile(self, theFile):
'''Analyses the Python file named p_fileName.'''
# Are we in a docstring ?
inDoc = False
for line in theFile:
stripped = line.strip()
# Manage a line that is within a docstring
inDocStart = False
if not inDoc and self.isPythonDoc(stripped, start=True):
inDoc = True
inDocStart = True
if inDoc:
self.commentLines += 1
if self.isPythonDoc(stripped, start=False, isStart=inDocStart):
inDoc = False
continue
# Manage an empty line
if not stripped:
self.emptyLines += 1
continue
# Manage a comment line
if line.startswith('#'):
self.commentLines += 1
continue
# If we are here, we have a code line.
self.codeLines += 1
def analyseFile(self, fileName):
'''Analyses file named p_fileName.'''
self.numberOfFiles += 1
theFile = file(fileName)
if fileName.endswith('.py'):
self.analysePythonFile(theFile)
elif fileName.endswith('.pt'):
self.analyseZptFile(theFile)
theFile.close()
def printReport(self):
'''Returns the analysis report as a string, only if there is at least
one analysed line.'''
lines = self.numberOfLines()
if not lines: return
commentRate = (self.commentLines / float(lines)) * 100.0
blankRate = (self.emptyLines / float(lines)) * 100.0
print '%s: %d files, %d lines (%.0f%% comments, %.0f%% blank)' % \
(self.name, self.numberOfFiles, lines, commentRate, blankRate)
class LinesCounter:
'''Counts and classifies the lines of code within a folder hierarchy.'''
def __init__(self, folderOrModule):
if isinstance(folderOrModule, basestring):
# It is the path of some folder
self.folder = folderOrModule
else:
# It is a Python module
self.folder = os.path.dirname(folderOrModule.__file__)
# These dicts will hold information about analysed files
self.python = {False: CodeAnalysis('Python'),
True: CodeAnalysis('Python (test)')}
self.zpt = {False: CodeAnalysis('ZPT'),
True: CodeAnalysis('ZPT (test)')}
# Are we currently analysing real or test code?
self.inTest = False
def printReport(self):
'''Displays on stdout a small analysis report about self.folder.'''
for zone in (False, True): self.python[zone].printReport()
for zone in (False, True): self.zpt[zone].printReport()
def run(self):
'''Let's start the analysis of self.folder.'''
# The test markers will allow us to know if we are analysing test code
# or real code within a given part of self.folder code hierarchy.
testMarker1 = '%stest%s' % (os.sep, os.sep)
testMarker2 = '%stest' % os.sep
j = os.path.join
for root, folders, files in os.walk(self.folder):
rootName = os.path.basename(root)
if rootName.startswith('.') or \
(rootName in ('tmp', 'temp')):
continue
# Are we in real code or in test code ?
self.inTest = False
if root.endswith(testMarker2) or (root.find(testMarker1) != -1):
self.inTest = True
# Scan the files in this folder
for fileName in files:
if fileName.endswith('.py'):
self.python[self.inTest].analyseFile(j(root, fileName))
elif fileName.endswith('.pt'):
self.zpt[self.inTest].analyseFile(j(root, fileName))
self.printReport()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------