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

View file

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

View file

@ -5,7 +5,7 @@ from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, PageDescr, GroupDescr, Keywords, \
FileWrapper, getClassName
FileWrapper, getClassName, SomeObjects
from appy.shared.data import languages
# Default Appy permissions -----------------------------------------------------
@ -29,10 +29,10 @@ class Page:
class Group:
'''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,
hasHeaders=False, group=None, colspan=1, align='center',
valign='top'):
valign='top', css_class='', master=None, masterValue=None):
self.name = name
# 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
@ -77,6 +77,26 @@ class Group:
self.columns = self.columns[:1]
# Header labels will be used as labels for the tabs.
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):
'''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
field?'''
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:
tool = obj.getTool()
flavour = tool.getFlavour(obj, appy=True)
flavourAttrName = 'optionalFieldsFor%s' % obj.meta_type
flavourAttrValue = getattr(flavour, flavourAttrName, ())
if self.name not in flavourAttrValue:
tool = obj.getTool().appy()
fieldName = 'optionalFieldsFor%s' % obj.meta_type
fieldValue = getattr(tool, fieldName, ())
if self.name not in fieldValue:
return False
# Check if the user has the permission to view or edit the field
user = obj.portal_membership.getAuthenticatedMember()
@ -568,11 +587,10 @@ class Type:
return self.default(obj.appy())
else:
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)
tool = obj.getTool()
flavour = tool.getFlavour(portalTypeName, appy=True)
return getattr(flavour, 'defaultValueFor%s' % self.labelId)
tool = obj.getTool().appy()
return getattr(tool, 'defaultValueFor%s' % self.labelId)
return value
def getFormattedValue(self, obj, value):
@ -1188,17 +1206,24 @@ class File(Type):
class Ref(Type):
def __init__(self, klass=None, attribute=None, validator=None,
multiplicity=(0,1), index=None, default=None, optional=False,
editDefault=False, add=False, link=True, unlink=False,
back=None, show=True, page='main', group=None, layouts=None,
showHeaders=False, shownInfo=(), select=None, maxPerPage=30,
move=0, indexed=False, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, colspan=1, master=None,
masterValue=None, focus=False, historized=False):
editDefault=False, add=False, addConfirm=False, noForm=False,
link=True, unlink=False, back=None, show=True, page='main',
group=None, layouts=None, showHeaders=False, shownInfo=(),
select=None, maxPerPage=30, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
self.klass = klass
self.attribute = attribute
# May the user add new objects through this ref ?
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?
self.link = link
# May the user unlink existing objects?
@ -1246,12 +1271,70 @@ class Ref(Type):
return obj.getBRefs(self.relationship)
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:
return obj._appy_getRefsBack(self.name, self.relationship,
noListIfSingleObj=True)
getRefs = obj.reference_catalog.getBackReferences
uids = [r.sourceUID for r in getRefs(obj, self.relationship)]
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):
return value
@ -1281,6 +1364,24 @@ class Ref(Type):
elif nbOfRefs > maxRef:
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):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show='view',
@ -1645,10 +1746,6 @@ class Model: pass
class Tool(Model):
'''If you want so define a custom tool class, she must inherit from this
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):
'''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
# application, set the following parameter to False.
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

View file

@ -1,6 +1,6 @@
# ------------------------------------------------------------------------------
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.utils import produceNiceMessage
import appy.pod, appy.pod.renderer
@ -133,8 +133,7 @@ class Generator:
# Default descriptor classes
self.descriptorClasses = {
'class': ClassDescriptor, 'tool': ClassDescriptor,
'flavour': ClassDescriptor, 'user': ClassDescriptor,
'workflow': WorkflowDescriptor}
'user': ClassDescriptor, 'workflow': WorkflowDescriptor}
# The following dict contains a series of replacements that need to be
# applied to file templates to generate files.
self.repls = {'applicationName': self.applicationName,
@ -143,7 +142,6 @@ class Generator:
# List of Appy classes and workflows found in the application
self.classes = []
self.tool = None
self.flavour = None
self.user = None
self.workflows = []
self.initialize()
@ -224,19 +222,13 @@ class Generator:
# of their definition).
attrs = astClasses[moduleElem.__name__].attributes
if appyType == 'class':
# Determine the class type (standard, tool, flavour...)
# Determine the class type (standard, tool, user...)
if issubclass(moduleElem, Tool):
if not self.tool:
klass = self.descriptorClasses['tool']
self.tool = klass(moduleElem, attrs, self)
else:
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):
if not self.user:
klass = self.descriptorClasses['user']

View file

@ -6,7 +6,7 @@
# ------------------------------------------------------------------------------
import types, copy
from model import ModelClass, Flavour, flavourAttributePrefixes
from model import ModelClass, Tool, toolFieldPrefixes
from utils import stringify
import appy.gen
import appy.gen.descriptors
@ -43,12 +43,13 @@ class FieldDescriptor:
def __repr__(self):
return '<Field %s, %s>' % (self.fieldName, self.classDescr)
def getFlavourAttributeMessage(self, fieldName):
'''Some attributes generated on the Flavour class need a specific
def getToolFieldMessage(self, fieldName):
'''Some attributes generated on the Tool class need a specific
default message, returned by this method.'''
res = fieldName
for prefix in flavourAttributePrefixes:
if fieldName.startswith(prefix):
for prefix in toolFieldPrefixes:
fullPrefix = prefix + 'For'
if fieldName.startswith(fullPrefix):
messageId = 'MSG_%s' % prefix
res = getattr(PoMessage, messageId)
if res.find('%s') != -1:
@ -66,8 +67,8 @@ class FieldDescriptor:
produceNice = True
default = self.fieldName
# Some attributes need a specific predefined message
if isinstance(self.classDescr, FlavourClassDescriptor):
default = self.getFlavourAttributeMessage(self.fieldName)
if isinstance(self.classDescr, ToolClassDescriptor):
default = self.getToolFieldMessage(self.fieldName)
if default != self.fieldName: produceNice = False
msg = PoMessage(msgId, '', default)
if produceNice:
@ -88,9 +89,7 @@ class FieldDescriptor:
self.generator.labels.append(poMsg)
def walkAction(self):
'''How to generate an action field ? We generate an Archetypes String
field.'''
# Add action-specific i18n messages
'''Generates the i18n-related labels.'''
for suffix in ('ok', 'ko'):
label = '%s_%s_action_%s' % (self.classDescr.name, self.fieldName,
suffix)
@ -98,6 +97,10 @@ class FieldDescriptor:
getattr(PoMessage, 'ACTION_%s' % suffix.upper()))
self.generator.labels.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):
'''How to generate a Ref?'''
@ -115,6 +118,11 @@ class FieldDescriptor:
poMsg = PoMessage(backLabel, '', self.appyType.back.attribute)
poMsg.produceNiceDefault()
self.generator.labels.append(poMsg)
# Add the label for the confirm message if relevant
if self.appyType.addConfirm:
label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName)
msg = PoMessage(label, '', PoMessage.CONFIRM)
self.generator.labels.append(msg)
def walkPod(self):
# Add i18n-specific messages
@ -123,8 +131,8 @@ class FieldDescriptor:
msg = PoMessage(label, '', PoMessage.POD_ASKACTION)
self.generator.labels.append(msg)
self.classDescr.labelsToPropagate.append(msg)
# Add the POD-related fields on the Flavour
Flavour._appy_addPodRelatedFields(self)
# Add the POD-related fields on the Tool
Tool._appy_addPodRelatedFields(self)
notToValidateFields = ('Info', 'Computed', 'Action', 'Pod')
def walkAppyType(self):
@ -133,10 +141,10 @@ class FieldDescriptor:
# Manage things common to all Appy types
# - optional ?
if self.appyType.optional:
Flavour._appy_addOptionalField(self)
Tool._appy_addOptionalField(self)
# - edit default value ?
if self.appyType.editDefault:
Flavour._appy_addDefaultField(self)
Tool._appy_addDefaultField(self)
# - put an index on this field?
if self.appyType.indexed and \
(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
# yet every sub-class. So we store those labels here; the Generator
# will propagate them later.
self.flavourFieldsToPropagate = [] # For this class, some fields have
# been defined on the Flavour class. Those fields need to be defined
self.toolFieldsToPropagate = [] # For this class, some fields have
# been defined on the Tool class. Those fields need to be defined
# for child classes of this class as well, but at this time we don't
# know yet every sub-class. So we store field definitions here; the
# Generator will propagate them later.
@ -251,7 +259,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
def generateSchema(self, configClass=False):
'''Generates the corresponding Archetypes schema in self.schema. If we
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
editDefault=True, etc).'''
for attrName in self.orderedAttributes:
@ -286,13 +294,6 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
res = self.klass.__dict__['root']
return res
def isPod(self):
'''May this class be associated with POD templates?.'''
res = False
if self.klass.__dict__.has_key('pod') and self.klass.__dict__['pod']:
res = True
return res
def isFolder(self, klass=None):
'''Must self.klass be a folder? If klass is not None, this method tests
it on p_klass instead of self.klass.'''
@ -375,6 +376,7 @@ class ToolClassDescriptor(ClassDescriptor):
'''Represents the POD-specific fields that must be added to the tool.'''
def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.attributesByClass = klass._appy_classes
self.modelClass = self.klass
self.predefined = True
self.customized = False
@ -395,42 +397,6 @@ class ToolClassDescriptor(ClassDescriptor):
def generateSchema(self):
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):
'''Represents an Archetypes-compliant class that corresponds to the User
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.generator import Generator as AbstractGenerator
from appy.gen.utils import getClassName
from model import ModelClass, PodTemplate, User, Flavour, Tool
from descriptors import FieldDescriptor, ClassDescriptor, \
WorkflowDescriptor, ToolClassDescriptor, \
FlavourClassDescriptor, PodTemplateClassDescriptor, \
UserClassDescriptor
from model import ModelClass, User, Tool
from descriptors import *
# Common methods that need to be defined on every Archetype class --------------
COMMON_METHODS = '''
@ -29,22 +26,18 @@ class Generator(AbstractGenerator):
poExtensions = ('.po', '.pot')
def __init__(self, *args, **kwargs):
Flavour._appy_clean()
Tool._appy_clean()
AbstractGenerator.__init__(self, *args, **kwargs)
# Set our own Descriptor classes
self.descriptorClasses['class'] = ClassDescriptor
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.flavour = FlavourClassDescriptor(Flavour, self)
self.podTemplate = PodTemplateClassDescriptor(PodTemplate, self)
self.user = UserClassDescriptor(User, self)
# i18n labels to generate
self.labels = [] # i18n labels
self.toolName = '%sTool' % self.applicationName
self.flavourName = '%sFlavour' % self.applicationName
self.toolInstanceName = 'portal_%s' % self.applicationName.lower()
self.podTemplateName = '%sPodTemplate' % self.applicationName
self.userName = '%sUser' % self.applicationName
self.portletName = '%s_portlet' % self.applicationName.lower()
self.queryName = '%s_query' % self.applicationName.lower()
@ -55,10 +48,9 @@ class Generator(AbstractGenerator):
commonMethods = COMMON_METHODS % \
(self.toolInstanceName, self.applicationName)
self.repls.update(
{'toolName': self.toolName, 'flavourName': self.flavourName,
'portletName': self.portletName, 'queryName': self.queryName,
{'toolName': self.toolName, 'portletName': self.portletName,
'queryName': self.queryName, 'userName': self.userName,
'toolInstanceName': self.toolInstanceName,
'podTemplateName': self.podTemplateName, 'userName': self.userName,
'commonMethods': commonMethods})
self.referers = {}
@ -145,7 +137,6 @@ class Generator(AbstractGenerator):
msg('goto_last', '', msg.GOTO_LAST),
msg('goto_source', '', msg.GOTO_SOURCE),
msg('whatever', '', msg.WHATEVER),
msg('confirm', '', msg.CONFIRM),
msg('yes', '', msg.YES),
msg('no', '', msg.NO),
msg('field_required', '', msg.FIELD_REQUIRED),
@ -321,15 +312,14 @@ class Generator(AbstractGenerator):
appClasses.append('%s.%s' % (k.__module__, k.__name__))
repls['appClasses'] = "[%s]" % ','.join(appClasses)
# Compute lists of class names
allClassNames = '"%s",' % self.flavourName
allClassNames += '"%s",' % self.podTemplateName
allClassNames = '"%s",' % self.userName
appClassNames = ','.join(['"%s"' % c.name for c in self.classes])
allClassNames += appClassNames
repls['allClassNames'] = allClassNames
repls['appClassNames'] = appClassNames
# Compute classes whose instances must not be catalogued.
catalogMap = ''
blackClasses = [self.toolName, self.flavourName, self.podTemplateName]
blackClasses = [self.toolName]
for blackClass in blackClasses:
catalogMap += "catalogMap['%s'] = {}\n" % blackClass
catalogMap += "catalogMap['%s']['black'] = " \
@ -464,28 +454,30 @@ class Generator(AbstractGenerator):
repls['workflows'] = workflows
self.copyFile('workflows.py', repls, destFolder='Extensions')
def generateWrapperProperty(self, name):
def generateWrapperProperty(self, name, type):
'''Generates the getter for attribute p_name.'''
res = ' def get_%s(self):\n ' % name
if name == 'title':
res += 'return self.o.Title()\n'
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)
return res
def getClasses(self, include=None):
'''Returns the descriptors for all the classes in the generated
gen-application. If p_include is "all", it includes the descriptors
for the config-related classes (tool, flavour, etc); if
p_include is "allButTool", it includes the same descriptors, the
tool excepted; if p_include is "custom", it includes descriptors
for the config-related classes for which the user has created a
sub-class.'''
for the config-related classes (tool, user, etc); if p_include is
"allButTool", it includes the same descriptors, the tool excepted;
if p_include is "custom", it includes descriptors for the
config-related classes for which the user has created a sub-class.'''
if not include: return self.classes
else:
res = self.classes[:]
configClasses = [self.tool,self.flavour,self.podTemplate,self.user]
configClasses = [self.tool, self.user]
if include == 'all':
res += configClasses
elif include == 'allButTool':
@ -547,24 +539,25 @@ class Generator(AbstractGenerator):
except AttributeError:
attrValue = getattr(c.modelClass, attrName)
if isinstance(attrValue, Type):
wrapperDef += self.generateWrapperProperty(attrName)
wrapperDef += self.generateWrapperProperty(attrName,
attrValue.type)
# Generate properties for back references
if self.referers.has_key(c.name):
for refDescr, rel in self.referers[c.name]:
attrName = refDescr.appyType.back.attribute
wrapperDef += self.generateWrapperProperty(attrName)
wrapperDef += self.generateWrapperProperty(attrName, 'Ref')
if not titleFound:
# Implicitly, the title will be added by Archetypes. So I need
# to define a property for it.
wrapperDef += self.generateWrapperProperty('title')
wrapperDef += self.generateWrapperProperty('title', 'String')
if c.customized:
# For custom tool and flavour, add a call to a method that
# allows to customize elements from the base class.
# For custom tool, add a call to a method that allows to
# customize elements from the base class.
wrapperDef += " if hasattr(%s, 'update'):\n " \
"%s.update(%s)\n" % (parentClasses[1], parentClasses[1],
parentClasses[0])
# For custom tool and flavour, add security declaration that
# will allow to call their methods from ZPTs.
# For custom tool, add security declaration that will allow to
# call their methods from ZPTs.
for parentClass in parentClasses:
wrapperDef += " for elem in dir(%s):\n " \
"if not elem.startswith('_'): security.declarePublic" \
@ -576,8 +569,6 @@ class Generator(AbstractGenerator):
repls['imports'] = '\n'.join(imports)
repls['wrappers'] = '\n'.join(wrappers)
repls['toolBody'] = Tool._appy_getBody()
repls['flavourBody'] = Flavour._appy_getBody()
repls['podTemplateBody'] = PodTemplate._appy_getBody()
repls['userBody'] = User._appy_getBody()
self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
@ -613,72 +604,49 @@ class Generator(AbstractGenerator):
def generateTool(self):
'''Generates the Plone tool that corresponds to this application.'''
# Generate the tool class in itself and related i18n messages
t = self.toolName
Msg = PoMessage
repls = self.repls.copy()
# Manage predefined fields
Tool.flavours.klass = Flavour
if self.flavour.customized:
Tool.flavours.klass = self.flavour.klass
# Create Tool-related i18n-related messages
self.labels += [
Msg(self.toolName, '', Msg.CONFIG % self.applicationName),
Msg('%s_edit_descr' % self.toolName, '', ' ')]
# Tune the Ref field between Tool and User
Tool.users.klass = User
if self.user.customized:
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()
# Generate the Tool class
repls = self.repls.copy()
repls['metaTypes'] = [c.name for c in self.classes]
repls['fields'] = self.tool.schema
repls['methods'] = self.tool.methods
repls['wrapperClass'] = '%s_Wrapper' % self.tool.name
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
self.user.generateSchema()
self.labels += [ Msg(self.userName, '', Msg.USER),
@ -687,32 +655,28 @@ class Generator(AbstractGenerator):
repls['fields'] = self.user.schema
repls['methods'] = self.user.methods
repls['wrapperClass'] = '%s_Wrapper' % self.user.name
self.copyFile('UserTemplate.py', repls,
destName='%s.py' % self.userName)
self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName)
def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for
generating the corresponding Archetype class and schema.'''
k = classDescr.klass
print 'Generating %s.%s (gen-class)...' % (k.__module__, k.__name__)
# Add, for this class, the needed configuration attributes on Flavour
if classDescr.isPod():
Flavour._appy_addPodField(classDescr)
if not classDescr.isAbstract():
Flavour._appy_addWorkflowFields(classDescr)
Tool._appy_addWorkflowFields(classDescr)
# Determine base archetypes schema and class
baseClass = 'BaseContent'
baseSchema = 'BaseSchema'
if classDescr.isFolder():
baseClass = 'OrderedBaseFolder'
baseSchema = 'OrderedBaseFolderSchema'
parents = [baseClass, 'ClassMixin']
parents = [baseClass, 'BaseMixin']
imports = []
implements = [baseClass]
for baseClass in classDescr.klass.__bases__:
if self.determineAppyType(baseClass) == 'class':
bcName = getClassName(baseClass)
parents.remove('ClassMixin')
parents.remove('BaseMixin')
parents.append(bcName)
implements.append(bcName)
imports.append('from %s import %s' % (bcName, bcName))
@ -750,19 +714,6 @@ class Generator(AbstractGenerator):
classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault()
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
for search in classDescr.getSearches(classDescr.klass):
searchLabel = '%s_search_%s' % (classDescr.name, search.name)

View file

@ -144,7 +144,6 @@ class PloneInstaller:
appFolder = getattr(site, self.productName)
for className in self.config.rootClasses:
permission = self.getAddPermission(className)
print 'Permission is', permission
appFolder.manage_permission(permission, (), acquire=0)
else:
appFolder = getattr(site, self.productName)
@ -221,78 +220,33 @@ class PloneInstaller:
current_catalogs.remove(catalog)
atTool.setCatalogsByType(meta_type, list(current_catalogs))
def findPodFile(self, klass, podTemplateName):
'''Finds the file that corresponds to p_podTemplateName for p_klass.'''
res = None
exec 'import %s' % klass.__module__
exec 'moduleFile = %s.__file__' % klass.__module__
folderName = os.path.dirname(moduleFile)
fileName = os.path.join(folderName, '%s.odt' % podTemplateName)
if os.path.isfile(fileName):
res = fileName
return res
def updatePodTemplates(self):
'''Creates or updates the POD templates in flavours according to pod
'''Creates or updates the POD templates in the tool according to pod
declarations in the application classes.'''
# Creates or updates the old-way class-related templates
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.
# Creates the templates for Pod fields if they do not exist.
for contentType, appyTypes in self.attributes.iteritems():
appyClass = self.tool.getAppyClass(contentType)
if not appyClass: continue # May be an abstract class
for appyType in appyTypes:
if appyType.type == 'Pod':
# For every flavour, find the attribute that stores the
# template, and store on it the default one specified in
# the appyType if no template is stored yet.
for flavour in self.appyTool.flavours:
attrName = flavour.getAttributeName(
'podTemplate', appyClass, appyType.name)
fileObject = getattr(flavour, attrName)
if not fileObject or (fileObject.size == 0):
# There is no file. Put the one specified in the
# appyType.
fileName=os.path.join(self.appyTool.getDiskFolder(),
appyType.template)
if os.path.exists(fileName):
setattr(flavour, attrName, fileName)
else:
self.appyTool.log(
'Template "%s" was not found!' % \
fileName, type='error')
if appyType.type != 'Pod': continue
# Find the attribute that stores the template, and store on
# it the default one specified in the appyType if no
# template is stored yet.
attrName = self.appyTool.getAttributeName(
'podTemplate', appyClass, appyType.name)
fileObject = getattr(self.appyTool, attrName)
if not fileObject or (fileObject.size == 0):
# There is no file. Put the one specified in the appyType.
fileName = os.path.join(self.appyTool.getDiskFolder(),
appyType.template)
if os.path.exists(fileName):
setattr(self.appyTool, attrName, fileName)
else:
self.appyTool.log('Template "%s" was not found!' % \
fileName, type='error')
def installTool(self):
'''Configures the application tool and flavours.'''
'''Configures the application tool.'''
# Register the tool in Plone
try:
self.ploneSite.manage_addProduct[
@ -333,9 +287,6 @@ class PloneInstaller:
else:
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()
# 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
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen
from appy.gen import Type, Search, Selection
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.mixins import BaseMixin
from appy.gen.plone25.wrappers import AbstractWrapper
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')
# ------------------------------------------------------------------------------
class ToolMixin(AbstractMixin):
class ToolMixin(BaseMixin):
_appy_meta_type = 'Tool'
def getPortalType(self, metaTypeOrAppyClass):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType in this flavour.'''
p_metaTypeOrAppyType.'''
appName = self.getProductConfig().PROJECTNAME
if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName)
@ -25,52 +31,100 @@ class ToolMixin(AbstractMixin):
res = '%s%s' % (elems[1], elems[4])
return res
def getFlavour(self, contextObjOrPortalType, appy=False):
'''Gets the flavour that corresponds to p_contextObjOrPortalType.'''
if isinstance(contextObjOrPortalType, basestring):
portalTypeName = contextObjOrPortalType
else:
# It is the contextObj, not a portal type name
portalTypeName = contextObjOrPortalType.portal_type
res = None
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.getAppyClass(ploneObj.meta_type)
appyTool = self.appy()
flavourNumber = None
nameElems = portalTypeName.split('_')
if len(nameElems) > 1:
try:
flavourNumber = int(nameElems[-1])
except ValueError:
pass
appName = self.getProductConfig().PROJECTNAME
if flavourNumber != None:
for flavour in appyTool.flavours:
if flavourNumber == flavour.number:
res = flavour
elif portalTypeName == ('%sFlavour' % appName):
# Current object is the Flavour itself. In this cas we simply
# return the wrapped contextObj. Here we are sure that
# contextObjOrPortalType is an object, not a portal type.
res = contextObjOrPortalType.appy()
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
n = appyTool.getAttributeName('formats', appyClass, fieldName)
res['formats'] = getattr(appyTool, n)
n = appyTool.getAttributeName('podTemplate', appyClass, fieldName)
res['template'] = getattr(appyTool, n)
appyType = ploneObj.getAppyType(fieldName)
res['title'] = self.translate(appyType.labelId)
res['context'] = appyType.context
res['action'] = appyType.action
return res
def getFlavoursInfo(self):
'''Returns information about flavours.'''
res = []
def generateDocument(self):
'''Generates the document from field-related info. UID of object that
is the template target is given in the request.'''
rq = self.REQUEST
appyTool = self.appy()
for flavour in appyTool.flavours:
if isinstance(flavour.o, FlavourMixin):
# This is a bug: sometimes other objects are associated as
# flavours.
res.append({'title': flavour.title, 'number':flavour.number})
# 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.
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
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):
'''Returns the name of this application.'''
return self.getProductConfig().PROJECTNAME
@ -86,6 +140,58 @@ class ToolMixin(AbstractMixin):
'''Returns the list of root classes for this application.'''
return self.getProductConfig().rootClasses
def _appy_getAllFields(self, contentType):
'''Returns the (translated) names of fields of p_contentType.'''
res = []
for appyType in self.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):
if self.portal_membership.isAnonymousUser(): return False
if context.id == 'skyn': context = context.getParentNode()
@ -106,15 +212,13 @@ class ToolMixin(AbstractMixin):
res = res.appy()
return res
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None, remember=False,
brainsOnly=False, maxResults=None, noSecurity=False,
sortBy=None, sortOrder='asc',
filterKey=None, filterValue=None):
def executeQuery(self, contentType, searchName=None, startNumber=0,
search=None, remember=False, brainsOnly=False,
maxResults=None, noSecurity=False, sortBy=None,
sortOrder='asc', filterKey=None, filterValue=None):
'''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the
flavour numbered p_flavourNumber. If p_searchName is specified, it
corresponds to:
with commas) in Plone's portal_catalog. If p_searchName is specified,
it corresponds to:
1) a search defined on p_contentType: additional search criteria
will be added to the query, or;
2) "_advanced": in this case, additional search criteria will also
@ -150,11 +254,7 @@ class ToolMixin(AbstractMixin):
p_filterValue.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
# Several content types are specified
portalTypes = contentType.split(',')
if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes]
else:
portalTypes = contentType
params = {'portal_type': portalTypes}
@ -164,8 +264,7 @@ class ToolMixin(AbstractMixin):
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
if searchName != '_advanced':
search = ClassDescriptor.getSearch(
appyClass, searchName)
search = ClassDescriptor.getSearch(appyClass, searchName)
else:
fields = self.REQUEST.SESSION['searchCriteria']
search = Search('customSearch', **fields)
@ -220,22 +319,17 @@ class ToolMixin(AbstractMixin):
for obj in res.objects:
i += 1
uids[startNumber+i] = obj.UID()
s['search_%s_%s' % (flavourNumber, searchName)] = uids
s['search_%s' % searchName] = uids
return res.__dict__
def getResultColumnsNames(self, contentType):
contentTypes = contentType.strip(',').split(',')
resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result.
flavour = None
fieldNames = None
appyTool = self.appy()
for cType in contentTypes:
# Get the flavour tied to those content types
if not flavour:
flavour = self.getFlavour(cType, appy=True)
if flavour.number != 1:
cType = cType.rsplit('_', 1)[0]
fieldNames = getattr(flavour, 'resultColumnsFor%s' % cType)
fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType)
if not resSet:
resSet = set(fieldNames)
else:
@ -483,9 +577,9 @@ class ToolMixin(AbstractMixin):
attrValue = oper.join(attrValue)
criteria[attrName[2:]] = attrValue
rq.SESSION['searchCriteria'] = criteria
# Goto the screen that displays search results
backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber'])
# Go to the screen that displays search results
backUrl = '%s/query?type_name=%s&&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'])
return self.goto(backUrl)
def getJavascriptMessages(self):
@ -535,13 +629,12 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value
return default
def getQueryUrl(self, contentType, flavourNumber, searchName,
startNumber=None):
def getQueryUrl(self, contentType, searchName, startNumber=None):
'''This method creates the URL that allows to perform a (non-Ajax)
request for getting queried objects from a search named p_searchName
on p_contentType from flavour numbered p_flavourNumber.'''
on p_contentType.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber)
baseParams = 'type_name=%s' % contentType
# Manage start number
rq = self.REQUEST
if startNumber != None:
@ -609,11 +702,10 @@ class ToolMixin(AbstractMixin):
if (nextIndex < lastIndex): lastNeeded = True
# Get the list of available UIDs surrounding the current object
if t == 'ref': # Manage navigation from a reference
# In the case of a reference, we retrieve ALL surrounding objects.
masterObj = self.getObject(d1)
batchSize = masterObj.getAppyType(fieldName).maxPerPage
uids = getattr(masterObj, '_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
# lies.
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
@ -622,13 +714,12 @@ class ToolMixin(AbstractMixin):
res['sourceUrl'] = masterObj.getUrl(**{startNumberKey:startNumber,
'page':pageName, 'nav':''})
else: # Manage navigation from a search
contentType, flavourNumber = d1.split(':')
flavourNumber = int(flavourNumber)
contentType = d1
searchName = keySuffix = d2
batchSize = self.appy().numberOfResultsPerPage
if not searchName: keySuffix = contentType
s = self.REQUEST.SESSION
searchKey = 'search_%s_%s' % (flavourNumber, keySuffix)
searchKey = 'search_%s' % keySuffix
if s.has_key(searchKey): uids = s[searchKey]
else: uids = {}
# In the case of a search, we retrieve only a part of all
@ -640,9 +731,8 @@ class ToolMixin(AbstractMixin):
# this one.
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
if newStartNumber < 0: newStartNumber = 0
self.executeQuery(contentType, flavourNumber,
searchName=searchName, startNumber=newStartNumber,
remember=True)
self.executeQuery(contentType, searchName=searchName,
startNumber=newStartNumber, remember=True)
uids = s[searchKey]
# For the moment, for first and last, we get them only if we have
# them in session.
@ -650,9 +740,9 @@ class ToolMixin(AbstractMixin):
if not uids.has_key(lastIndex): lastNeeded = False
# Compute URL of source object
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber,
searchName, startNumber=startNumber)
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, searchName,
startNumber=startNumber)
# Compute URLs
for urlType in ('previous', 'next', 'first', 'last'):
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:
- mixins/ClassMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class;
- mixins/FlavourMixin is mixed in with the generated application Flavour
class.
The AbstractMixin defined hereafter is the base class of any mixin.'''
- mixins/BaseMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
# ------------------------------------------------------------------------------
import os, os.path, types, mimetypes
@ -15,10 +12,10 @@ from appy.gen.plone25.descriptors import ClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission
# ------------------------------------------------------------------------------
class AbstractMixin:
'''Every Archetype class generated by appy.gen inherits from a mixin that
inherits from this class. It contains basic functions allowing to
minimize the amount of generated code.'''
class BaseMixin:
'''Every Archetype class generated by appy.gen inherits from this class or
a subclass of it.'''
_appy_meta_type = 'Class'
def createOrUpdate(self, created, values):
'''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
self.historizeData(previousData)
# Manage references
obj._appy_manageRefs(created)
# Manage potential link with an initiator object
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:
# Get the wrapper first
appyObject = obj.appy()
# Call the custom "onEdit" if available
if hasattr(appyObject, 'onEdit'):
appyObject.onEdit(created)
# Manage "add" permissions
if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created)
# Manage "add" permissions and reindex the object
obj._appy_managePermissions()
# Reindex object
obj.reindexObject()
return obj
@ -95,6 +96,12 @@ class AbstractMixin:
(baseUrl, typeName, objId)
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):
'''This method performs field-specific validation for every field from
the page that is being created or edited. For every field whose
@ -120,7 +127,7 @@ class AbstractMixin:
obj = self.appy()
if not hasattr(obj, 'validate'): return
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
# not a string, we replace it with the standard validation error for the
# corresponding field.
@ -241,21 +248,19 @@ class AbstractMixin:
res = {}
for appyType in self.getAllAppyTypes():
if appyType.historized:
res[appyType.name] = (getattr(self, appyType.name),
appyType.labelId)
res[appyType.name] = appyType.getValue(self)
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
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 field. The method is also called by the method historizeData below,
that performs "automatic" recording when a HTTP form is uploaded.'''
# Add to the p_changes dict the field labels if they are not present
if not labels:
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Add to the p_changes dict the field labels
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Create the event to record in the history
DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state')
@ -273,14 +278,18 @@ class AbstractMixin:
historized fields, while p_self already contains the (potentially)
modified values.'''
# Remove from previousData all values that were not changed
for fieldName in previousData.keys():
prev = previousData[fieldName][0]
curr = getattr(self, fieldName)
for field in previousData.keys():
prev = previousData[field]
appyType = self.getAppyType(field)
curr = appyType.getValue(self)
if (prev == curr) or ((prev == None) and (curr == '')) or \
((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:
self.addDataChange(previousData, labels=True)
self.addDataChange(previousData)
def goto(self, url, addParams=False):
'''Brings the user to some p_url after an action has been executed.'''
@ -308,80 +317,14 @@ class AbstractMixin:
field named p_name.'''
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):
'''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 a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
If p_startNumber is a number, this method will return
appyType.maxPerPage objects, starting at p_startNumber.'''
appyType = self.getAppyType(name)
if not appyType.isBack:
return self._appy_getRefs(name, ploneObjects=True,
startNumber=startNumber).__dict__
else:
# Note that pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType.relationship)).__dict__
return appyType.getValue(self, type='zobjects', someObjects=True,
startNumber=startNumber).__dict__
def getSelectableAppyRefs(self, name):
'''p_name is the name of a Ref field. This method returns the list of
@ -780,10 +723,6 @@ class AbstractMixin:
self.reindexObject()
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):
'''When displaying a selection box (ie a String with a validator being a
list), must the _vocabValue appear as selected?'''
@ -853,32 +792,6 @@ class AbstractMixin:
rq.appyWrappers[uid] = 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):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):
@ -955,57 +868,6 @@ class AbstractMixin:
exec 'self.%s = pList()' % 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}
def getUrl(self, base=None, mode='view', **kwargs):
'''Returns a Appy URL.
@ -1039,12 +901,14 @@ class AbstractMixin:
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.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
return self.translation_service.utranslate(
domain, label, mapping, self, default=default)
return self.Control_Panel.TranslationService.utranslate(
domain, label, mapping, self, default=default,
target_language=language)
def getPageLayout(self, layoutType):
'''Returns the layout corresponding to p_layoutType for p_self.'''

View file

@ -12,10 +12,10 @@ from appy.gen import *
# ------------------------------------------------------------------------------
class ModelClass:
'''This class is the abstract class of all predefined application classes
used in the Appy model: Tool, Flavour, PodTemplate, etc. All methods and
attributes of those classes are part of the Appy machinery and are
prefixed with _appy_ in order to avoid name conflicts with user-defined
parts of the application model.'''
used in the Appy model: Tool, User, etc. All methods and attributes of
those classes are part of the Appy machinery and are prefixed with _appy_
in order to avoid name conflicts with user-defined parts of the
application model.'''
_appy_attributes = [] # We need to keep track of attributes order.
# When creating a new instance of a ModelClass, the following 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))
return res
# The User class ---------------------------------------------------------------
class User(ModelClass):
# In a ModelClass we need to declare attributes in the following list.
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
'password2', 'roles']
# All methods defined below are fake. Real versions are in the wrapper.
title = String(show=False)
gm = {'group': 'main', 'multiplicity': (1,1)}
@ -86,47 +90,50 @@ class User(ModelClass):
password2 = String(format=String.PASSWORD, show=showPassword, **gm)
gm['multiplicity'] = (0, None)
roles = String(validator=Selection('getGrantableRoles'), **gm)
_appy_attributes = ['title', 'name', 'firstName', 'login',
'password1', 'password2', 'roles']
class PodTemplate(ModelClass):
description = String(format=String.TEXT)
podTemplate = File(multiplicity=(1,1))
podFormat = String(validator=['odt', 'pdf', 'rtf', 'doc'],
multiplicity=(1,1), default='odt')
podPhase = String(default='main')
_appy_attributes = ['description', 'podTemplate', 'podFormat', 'podPhase']
# The Tool class ---------------------------------------------------------------
defaultFlavourAttrs = ('number', 'enableNotifications')
flavourAttributePrefixes = ('optionalFieldsFor', 'defaultValueFor',
'podTemplatesFor', 'podMaxShownTemplatesFor', 'resultColumnsFor',
'showWorkflowFor', 'showWorkflowCommentFieldFor', 'showAllStatesInPhaseFor')
# Attribute prefixes of the fields generated on the Flavour for configuring
# the application classes.
# Here are the prefixes of the fields generated on the Tool.
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow',
'showWorkflowCommentField', 'showAllStatesInPhase')
defaultToolFields = ('users', 'enableNotifications', 'unoEnabledPython',
'openOfficePort', 'numberOfResultsPerPage',
'listBoxesMaximumWidth')
class Flavour(ModelClass):
'''For every application, the Flavour may be different (it depends on the
fields declared as optional, etc). Instead of creating a new way to
generate the Archetypes Flavour class, we create a silly
FlavourStub instance and we will use the standard Archetypes
generator that generates classes from the application to generate the
flavour class.'''
number = Integer(default=1, show=False)
enableNotifications = Boolean(default=True, page='notifications')
class Tool(ModelClass):
# The following dict allows us to remember the original classes related to
# the attributes we will add due to params in user attributes.
_appy_classes = {} # ~{s_attributeName: s_className}~
# We need to remember the original classes related to the flavour attributes
_appy_attributes = list(defaultFlavourAttrs)
# In a ModelClass we need to declare attributes in the following list.
_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
def _appy_clean(klass):
toClean = []
for k, v in klass.__dict__.iteritems():
if not k.startswith('__') and (not k.startswith('_appy_')):
if k not in defaultFlavourAttrs:
if k not in defaultToolFields:
toClean.append(k)
for k in toClean:
exec 'del klass.%s' % k
klass._appy_attributes = list(defaultFlavourAttrs)
klass._appy_attributes = list(defaultToolFields)
klass._appy_classes = {}
@classmethod
@ -134,20 +141,20 @@ class Flavour(ModelClass):
'''From a given p_appyType, produce a type definition suitable for
storing the default value for this field.'''
res = copy.copy(appyType)
# A fiekd in the flavour can't have parameters that would lead to the
# creation of new fields in the flavour.
# A field added to the tool can't have parameters that would lead to the
# creation of new fields in the tool.
res.editDefault = False
res.optional = False
res.show = True
res.group = copy.copy(appyType.group)
res.phase = 'main'
# Set default layouts for all Flavour fields
# Set default layouts for all Tool fields
res.layouts = res.formatLayouts(None)
res.specificReadPermission = False
res.specificWritePermission = False
res.multiplicity = (0, appyType.multiplicity[1])
if type(res.validator) == types.FunctionType:
# We will not be able to call this function from the flavour.
# We will not be able to call this function from the tool.
res.validator = None
if isinstance(appyType, Ref):
res.link = True
@ -155,7 +162,7 @@ class Flavour(ModelClass):
res.back = copy.copy(appyType.back)
res.back.attribute += 'DefaultValue'
res.back.show = False
res.select = None # Not callable from flavour
res.select = None # Not callable from tool.
return res
@classmethod
@ -182,10 +189,7 @@ class Flavour(ModelClass):
@classmethod
def _appy_addPodRelatedFields(klass, fieldDescr):
'''Adds the fields needed in the Flavour 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.'''
'''Adds the fields needed in the Tool for configuring a Pod field.'''
className = fieldDescr.classDescr.name
# On what page and group to display those fields ?
pg = {'page': 'documentGeneration',
@ -200,29 +204,9 @@ class Flavour(ModelClass):
multiplicity=(1,None), default=('odt',), **pg)
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
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
results.'''
className = classDescr.name
@ -302,25 +286,4 @@ class Flavour(ModelClass):
fieldType = Boolean(default=defaultValue, page='userInterface',
group=groupName)
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;
member context/portal_membership/getAuthenticatedMember;
portal context/portal_url/getPortalObject;
portal_url context/portal_url/getPortalPath;
portal_url python: context.portal_url();
template python: contextObj.getPageTemplate(portal.skyn, page);
dummy python: response.setHeader('Content-Type','text/html;;charset=utf-8');
dummy2 python: response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');

View file

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

View file

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

View file

@ -1,7 +1,6 @@
<metal:queryResults define-macro="queryResult"
tal:define="tool python: contextObj;
contentType request/type_name;
flavourNumber python: int(request['flavourNumber']);
startNumber request/startNumber|python:'0';
startNumber python: int(startNumber);
searchName request/search;
@ -12,13 +11,13 @@
sortOrder request/sortOrder| python:'asc';
filterKey request/filterKey| 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;
totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize;
ajaxHookId python:'queryResult';
navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, flavourNumber, searchName);
newSearchUrl python: '%s/skyn/search?type_name=%s&flavourNumber=%d' % (tool.getAppFolder().absolute_url(), contentType, flavourNumber);">
navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, searchName);
newSearchUrl python: '%s/skyn/search?type_name=%s&' % (tool.getAppFolder().absolute_url(), contentType);">
<tal:result condition="objs">
@ -91,7 +90,7 @@
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<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:comment replace="nothing">Columns corresponding to other fields</tal:comment>
@ -125,7 +124,7 @@
<tr>
<tal:comment replace="nothing">Edit the element</tal:comment>
<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:condition="python: member.has_permission('Modify portal content', obj)">
<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
an Ajax request. */
function askQueryResult(hookId, objectUrl, contentType, flavourNumber,
searchName, startNumber, sortKey, sortOrder, filterKey) {
function askQueryResult(hookId, objectUrl, contentType, searchName,
startNumber, sortKey, sortOrder, filterKey) {
// Sends an Ajax request for getting the result of a query.
var params = {'type_name': contentType, 'flavourNumber': flavourNumber,
'search': searchName, 'startNumber': startNumber};
var params = {'type_name': contentType, 'search': searchName,
'startNumber': startNumber};
if (sortKey) params['sortKey'] = sortKey;
if (sortOrder) params['sortOrder'] = sortOrder;
if (filterKey) {
@ -208,11 +208,9 @@
theForm.submit();
}
function onDeleteObject(objectUid) {
if (confirm(delete_confirm)) {
f = document.getElementById('deleteForm');
f.objectUid.value = objectUid;
f.submit();
}
f = document.getElementById('deleteForm');
f.objectUid.value = objectUid;
askConfirm('form', 'deleteForm', delete_confirm);
}
function toggleCookie(cookieId) {
// What is the state of this boolean (expanded/collapsed) cookie?
@ -241,10 +239,9 @@
createCookie(cookieId, newState);
}
// 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];
theForm.objectUid.value = contextUid;
theForm.templateUid.value = templateUid;
theForm.fieldName.value = fieldName;
theForm.podFormat.value = podFormat;
theForm.askAction.value = "False";
@ -255,7 +252,10 @@
theForm.submit();
}
// 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
var popup = document.getElementById(popupId);
// Put it at the right place on the screen
@ -276,19 +276,35 @@
greyed.style.display = "none";
}
// Function triggered when an action needs to be confirmed by the user
function askConfirm(formId) {
// Store the ID of the form to send if the users confirms.
function askConfirm(actionType, action, msg) {
/* 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');
confirmForm.actionFormId.value = formId;
openPopup("confirmActionPopup");
confirmForm.actionType.value = actionType;
confirmForm.action.value = action;
openPopup("confirmActionPopup", msg);
}
// Function triggered when an action confirmed by the user must be performed
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 actionFormId = confirmForm.actionFormId.value;
var actionForm = document.getElementById(actionFormId);
actionForm.submit();
var actionType = confirmForm.actionType.value;
var action = confirmForm.action.value;
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 manageTab(tabId, action) {
@ -344,10 +360,8 @@
</form>
<tal:comment replace="nothing">Global form for generating a document from a pod template.</tal:comment>
<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"/>
<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="podFormat"/>
<input type="hidden" name="askAction"/>
@ -384,31 +398,6 @@
</tr>
</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">
This macro displays an object's history. It is used by macro "header" below.
</tal:comment>
@ -460,7 +449,7 @@
<th align="left" width="70%" tal:content="python: tool.translate('previous_value')"></th>
</tr>
<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]);
appyType python:contextObj.getAppyType(change[0], asDict=True);
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.
</tal:comment>
<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)"
tal:condition="python: test(showAllStatesInPhase, len(states)&gt;1, True)">
<table>
<tr>
<tal:state repeat="stateInfo states">
<td tal:attributes="class python: 'appyState step' + stateInfo['stateStatus']"
<td tal:attributes="class python: 'appyState step%sState' % stateInfo['stateStatus']"
tal:content="python: tool.translate(contextObj.getWorkflowLabel(stateInfo['name']))">
</td>
<td tal:condition="python: stateInfo['name'] != states[-1]['name']">
@ -512,7 +501,7 @@
<table>
<tr>
<tal:comment replace="nothing">Input field allowing to enter a comment before triggering a transition</tal:comment>
<td tal:define="showCommentsField python:flavour.getAttr('showWorkflowCommentFieldFor'+contextObj.meta_type)"
<td tal:define="showCommentsField python:tool.getAttr('showWorkflowCommentFieldFor'+contextObj.meta_type)"
align="right" tal:condition="showCommentsField">
<span tal:content="python: tool.translate('workflow_comment')" class="discreet"></span>
<input type="text" id="comment" name="comment" size="35"/>
@ -520,7 +509,7 @@
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
<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']);
onClick python: 'triggerTransition(\'%s\')' % transition['id'];"/>
</td>
@ -556,7 +545,7 @@
<td colspan="2" class="discreet" tal:content="descrLabel"/>
</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">Plus/minus icon for accessing history</tal:comment>
<tal:accessHistory condition="hasHistory">
@ -580,8 +569,6 @@
<span i18n:translate="box_last_modified" i18n:domain="plone"></span>
<span tal:replace="python:contextObj.restrictedTraverse('@@plone').toLocalizedTime(contextObj.ModificationDate(),long_format=1)"></span>
</td>
<td valign="top"><metal:pod use-macro="here/skyn/page/macros/listPodTemplates"/>
</td>
</tr>
<tal:comment replace="nothing">Object history</tal:comment>
<tr tal:condition="hasHistory">
@ -631,7 +618,7 @@
masterValue.push(idField);
}
else {
if (idField[0] == '(') {
if ((idField[0] == '(') || (idField[0] == '[')) {
// There are multiple values, split it
var subValues = idField.substring(1, idField.length-1).split(',');
for (var k=0; k < subValues.length; k++){

View file

@ -8,13 +8,13 @@
contextObj python: tool.getPublishedObject()">
<tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment>
<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>
<table cellpadding="0" cellspacing="0" width="100%">
<tr>
<td tal:define="titleIsClickable python: member.has_role('Manager') and rootClasses">
<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>
<span tal:condition="not: titleIsClickable"
tal:replace="python: tool.translate(appName)"/>
@ -37,16 +37,14 @@
</tal:publishedObject>
<tal:comment replace="nothing">Create a section for every root class.</tal:comment>
<tal:section repeat="rootClass rootClasses"
define="flavourNumber python:1;
flavour python: tool.getFlavour('Dummy_%d' % flavourNumber)">
<tal:section repeat="rootClass rootClasses">
<tal:comment replace="nothing">Section title, with action icons</tal:comment>
<dt tal:condition="python: tool.userMaySearch(rootClass)"
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">
<tr>
<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', '')"
tal:content="python: tool.translate(rootClass + '_plural')"></a>
</td>
@ -66,11 +64,11 @@
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/import.png;
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"
tal:define="showSearch python: flavour.getAttr('enableAdvancedSearchFor%s' % rootClass)"
tal:define="showSearch python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)"
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;
title python: tool.translate('search_objects')"/>
</td>
@ -94,7 +92,7 @@
<span tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')">
<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;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
@ -105,7 +103,7 @@
<dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup"
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;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
@ -113,16 +111,6 @@
</tal:searchOrGroup>
</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">
Greyed transparent zone that is deployed on the
whole screen when a popup is displayed.
@ -133,8 +121,9 @@
<div id="confirmActionPopup" class="appyPopup">
<form id="confirmActionForm" method="post">
<div align="center">
<p tal:content="python: tool.translate('confirm')"></p>
<input type="hidden" name="actionFormId"/>
<p id="appyConfirmText"></p>
<input type="hidden" name="actionType"/>
<input type="hidden" name="action"/>
<input type="button" onClick="doConfirm()"
tal:attributes="value python:tool.translate('yes')"/>
<input type="button" value="No" onClick="closePopup('confirmActionPopup')"

View file

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

View file

@ -16,8 +16,7 @@
tal:define="appFolder context/getParentNode;
contentType request/type_name;
tool python: portal.get('portal_%s' % appFolder.id.lower());
flavour python: tool.getFlavour('Dummy_%s' % request['flavourNumber']);
searchableFields python: flavour.getSearchableFields(contentType)">
searchableFields python: tool.getSearchableFields(contentType)">
<tal:comment replace="nothing">Search title</tal:comment>
<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">
<input type="hidden" name="action" value="SearchObjects"/>
<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%"
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">
<td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)">
<tal:field condition="widget">

View file

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

View file

@ -2,14 +2,15 @@
<metal:view define-macro="view">
<form name="executeAppyAction"
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'">
<input type="hidden" name="action" value="ExecuteAppyAction"/>
<input type="hidden" name="objectUid" tal:attributes="value contextObj/UID"/>
<input type="hidden" name="fieldName" tal:attributes="value name"/>
<input type="button" tal:condition="widget/confirm"
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"
tal:attributes="value label" onClick="javascript:;"/>
<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"
tal:content="python: tool.translate(doLabel)"></label>
</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;
onClick python: 'generatePodDocument(\'%s\',\'\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat);
onClick python: 'generatePodDocument(\'%s\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat);
title podFormat/capitalize"
style="cursor:pointer"/>
</metal:view>

View file

@ -10,7 +10,7 @@
from one object to the next/previous on skyn/view.</tal:comment>
<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: appyType['isBack'] and '' or navInfo;
navInfo python: test(appyType['isBack'], '', navInfo);
pageName python: appyType['isBack'] and appyType['backd']['page'] or 'main';
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>
@ -40,13 +40,13 @@
</tal:moveRef>
</td>
<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);"
tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)"
tal:condition="python: member.has_permission('Modify portal content', obj)">
tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)">
<img title="label_edit" i18n:domain="plone" i18n:attributes="title"
tal:attributes="src string: $portal_url/skyn/edit.gif"/>
</a></td>
</a>
</td>
<tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding">
<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
an icon for creating a new linked object (at least if multiplicities allow it).</tal:comment>
<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;
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>
<tal:comment replace="nothing">
@ -109,8 +113,7 @@
totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize;
folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode();
flavour python:tool.getFlavour(contextObj);
linkedPortalType python:flavour.getPortalType(appyType['klass']);
linkedPortalType python: tool.getPortalType(appyType['klass']);
addPermission python: '%s: Add %s' % (tool.getAppName(), linkedPortalType);
canWrite python: not appyType['isBack'] and member.has_permission(appyType['writePermission'], contextObj);
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;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(appyType['labelId']);
addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']);
description python: tool.translate(appyType['descrId']);
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
@ -240,13 +244,11 @@
</fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment>
<br tal:define="widgetDescr widgetDescr|nothing"
tal:condition="python: not widgetDescr or (widgetDescr['widgetType'] != 'group')"/>
</tal:anyNumberOfReferences>
</div>
<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:define="rname python: 'appy_ref_%s' % name;
requestValue python: request.get(rname, []);
@ -255,18 +257,18 @@
refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']];
isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())">
<select tal:attributes="name rname;
multiple python: isMultiple and 'multiple' or ''">
<option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option>
<tal:ref repeat="refObj allObjects">
<option tal:define="uid python: contextObj.getReferenceUid(refObj)"
tal:content="python: contextObj.getReferenceLabel(name, refObj)"
tal:attributes="value uid;
selected python:(inRequest and (uid in requestValue) or (not inRequest and ((uid in refUids)))) and True or False">
</option>
</tal:ref>
</select>
</div>
<select tal:attributes="name rname;
multiple python: isMultiple and 'multiple' or ''">
<option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option>
<tal:ref repeat="refObj allObjects">
<option tal:define="uid python: contextObj.getReferenceUid(refObj)"
tal:content="python: contextObj.getReferenceLabel(name, refObj)"
tal:attributes="value uid;
selected python:(inRequest and (uid in requestValue) or (not inRequest and ((uid in refUids)))) and True or False">
</option>
</tal:ref>
</select>
</metal:editRef>
<tal:comment replace="nothing">Cell macro for a Ref.</tal:comment>
<metal:cell define-macro="cell">

View file

@ -90,7 +90,9 @@
<metal:content use-macro="portal/skyn/widgets/show/macros/groupContent"/>
</tal:asSection>
<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>
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table cellpadding="0" cellspacing="0" style="position:relative; bottom:-1px;">
@ -137,7 +139,8 @@
</tal:comment>
<table metal:define-macro="groupContent"
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>
<tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']">
<td tal:attributes="colspan python: len(widget['columnsWidths']);

View file

@ -4,7 +4,7 @@ from DateTime import DateTime
from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config
from Extensions.appyWrappers import <!genClassName!>_Wrapper
from appy.gen.plone25.mixins.ClassMixin import ClassMixin
from appy.gen.plone25.mixins import BaseMixin
<!imports!>
schema = Schema((<!fields!>
@ -29,7 +29,7 @@ class <!genClassName!>(<!parents!>):
i18nDomain = '<!applicationName!>'
schema = fullSchema
wrapperClass = <!genClassName!>_Wrapper
for elem in dir(ClassMixin):
for elem in dir(BaseMixin):
if not elem.startswith('__'): security.declarePublic(elem)
<!commonMethods!>
<!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!>">
<body>
<div metal:define-macro="portlet"
tal:define="tool python: context.<!toolInstanceName!>;
flavour python: tool.getFlavour(tool);"
tal:define="tool python: context.<!toolInstanceName!>;"
tal:condition="python: tool.showPortlet(context)">
<metal:block metal:use-macro="here/global_defines/macros/defines" />
<metal:prologue use-macro="here/skyn/page/macros/prologue"/>
<dl tal:define="rootClasses tool/getRootClasses;
appName string:<!applicationName!>;
appFolder tool/getAppFolder;
flavours tool/getFlavoursInfo" class="portlet">
appFolder tool/getAppFolder" class="portlet">
<metal:content use-macro="here/skyn/portlet/macros/portletContent"/>
</dl>
</div>

View file

@ -36,24 +36,22 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
border-width: thin;
text-align: center;
padding: 0.1em 1em 0.1em 1.3em;
background-position: -1px 4px;
}
.appyChanges th {
font-style: italic;
background-color: transparent;
border-bottom: 1px dashed #8CACBB;
border-top: 0 none transparent;
border-left: 0 none transparent;
border-right: 0 none transparent;
border: 0 none transparent;
padding: 0.1em 0.1em 0.1em 0.1em;
}
.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-top: 0 none transparent;
border-left: 0 none transparent;
border-right: 0 none transparent;
border-left: 0 none transparent !important;
border-bottom: 0 none transparent !important;
}
.appyHistory {
@ -70,6 +68,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
background-repeat: no-repeat;
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 {
background-color: #eef3f5;
@ -77,6 +81,12 @@ label { font-weight: bold; font-style: italic; line-height: 1.4em;}
background-repeat: no-repeat;
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 {
background-color: #ffffff;

View file

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

View file

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

View file

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

View file

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

View file

@ -155,9 +155,8 @@ def do(transitionName, stateChange, logger):
if hasattr(ploneObj, '_v_appy_do') and \
not ploneObj._v_appy_do['doNotify']:
doNotify = False
elif not ploneObj.getTool().getFlavour(
ploneObj).getEnableNotifications():
# We do not notify if the "notify" flag in the flavour is disabled.
elif not getattr(ploneObj.getTool().appy(), 'enableNotifications'):
# We do not notify if the "notify" flag in the tool is disabled.
doNotify = False
if doAction or doNotify:
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):
'''Returns the disk folder where the Appy application is stored.'''
return self.o.getProductConfig().diskFolder
def getAttributeName(self, attributeType, klass, attrName=None):
'''Some names of Tool attributes are not easy to guess. For example,
the attribute that stores the names of the columns to display in
query results for class A that is in package x.y is
"tool.resultColumnsForx_y_A". Other example: the attribute that
stores the editable default value of field "f1" of class x.y.A is
"tool.defaultValueForx_y_A_f1". This method generates the attribute
name based on p_attributeType, a p_klass from the application, and a
p_attrName (given only if needed, for example if p_attributeType is
"defaultValue"). p_attributeType may be:
"defaultValue"
Stores the editable default value for a given p_attrName of a
given p_klass.
"podTemplate"
Stores the pod template for p_attrName.
"formats"
Stores the output format(s) of a given pod template for
p_attrName.
"resultColumns"
Stores the list of columns that must be shown when displaying
instances of the a given root p_klass.
"enableAdvancedSearch"
Determines if the advanced search screen must be enabled for
p_klass.
"numberOfSearchColumns"
Determines in how many columns the search screen for p_klass
is rendered.
"searchFields"
Determines, among all indexed fields for p_klass, which one will
really be used in the search screen.
"optionalFields"
Stores the list of optional attributes that are in use in the
tool for the given p_klass.
"showWorkflow"
Stores the boolean field indicating if we must show workflow-
related information for p_klass or not.
"showWorkflowCommentField"
Stores the boolean field indicating if we must show the field
allowing to enter a comment every time a transition is triggered.
"showAllStatesInPhase"
Stores the boolean field indicating if we must show all states
linked to the current phase or not. If this field is False, we
simply show the current state, be it linked to the current phase
or not.
'''
fullClassName = self.o.getPortalType(klass)
res = '%sFor%s' % (attributeType, fullClassName)
if attrName: res += '_%s' % attrName
return res
# ------------------------------------------------------------------------------

View file

@ -3,6 +3,18 @@ from appy.gen.plone25.wrappers import 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):
'''When must we show the login field?'''
if self.o.isTemporary(): return 'edit'
@ -51,6 +63,7 @@ class UserWrapper(AbstractWrapper):
msg = self.translate(u'Passwords do not match.', domain='plone')
errors.password1 = msg
errors.password2 = msg
self._callCustom('validate', new, errors)
def onEdit(self, created):
self.title = self.firstName + ' ' + self.name
@ -86,11 +99,5 @@ class UserWrapper(AbstractWrapper):
# Remove the user if it was in the corresponding group
if groupName in userGroups:
group.removeMember(self.login)
# Call the custom user "onEdit" method if it exists
# 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)
self._callCustom('onEdit', created)
# ------------------------------------------------------------------------------

View file

@ -82,8 +82,6 @@ class AbstractWrapper:
else: return 1
def get_tool(self): return self.o.getTool().appy()
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
request = property(get_request)
def get_session(self): return self.o.REQUEST.SESSION
@ -204,9 +202,9 @@ class AbstractWrapper:
ploneObj.reindexObject()
return appyObj
def translate(self, label, mapping={}, domain=None):
def translate(self, label, mapping={}, domain=None, language=None):
'''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,
doHistory=True):
@ -276,27 +274,25 @@ class AbstractWrapper:
p_maxResults. If p_noSecurity is specified, you get all objects,
even if the logged user does not have the permission to view it.'''
# Find the content type corresponding to p_klass
flavour = self.flavour
contentType = flavour.o.getPortalType(klass)
contentType = self.tool.o.getPortalType(klass)
# Create the Search object
search = Search('customSearch', sortBy=sortBy, **fields)
if not maxResults:
maxResults = 'NO_LIMIT'
# If I let maxResults=None, only a subset of the results will be
# returned by method executeResult.
res = self.tool.o.executeQuery(contentType,flavour.number,search=search,
maxResults=maxResults, noSecurity=noSecurity)
res = self.tool.o.executeQuery(contentType, search=search,
maxResults=maxResults, noSecurity=noSecurity)
return [o.appy() for o in res['objects']]
def count(self, klass, noSecurity=False, **fields):
'''Identical to m_search above, but returns the number of objects that
match the search instead of returning the objects themselves. Use
this method instead of writing len(self.search(...)).'''
flavour = self.flavour
contentType = flavour.o.getPortalType(klass)
contentType = self.tool.o.getPortalType(klass)
search = Search('customSearch', **fields)
res = self.tool.o.executeQuery(contentType,flavour.number,search=search,
brainsOnly=True, noSecurity=noSecurity)
res = self.tool.o.executeQuery(contentType, search=search,
brainsOnly=True, noSecurity=noSecurity)
if res: return res._len # It is a LazyMap instance
else: return 0
@ -325,14 +321,12 @@ class AbstractWrapper:
"for obj in self.search(MyClass,...)"
'''
flavour = self.flavour
contentType = flavour.o.getPortalType(klass)
contentType = self.tool.o.getPortalType(klass)
search = Search('customSearch', sortBy=sortBy, **fields)
# Initialize the context variable "ctx"
ctx = context
for brain in self.tool.o.executeQuery(contentType, flavour.number, \
search=search, brainsOnly=True, maxResults=maxResults,
noSecurity=noSecurity):
for brain in self.tool.o.executeQuery(contentType, search=search, \
brainsOnly=True, maxResults=maxResults, noSecurity=noSecurity):
# Get the Appy object from the brain
obj = brain.getObject().appy()
exec expression
@ -379,5 +373,5 @@ class AbstractWrapper:
p_data must be a dictionary whose keys are field names (strings) and
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:
'''Represents a i18n message (po format).'''
CONFIG = "Configuration panel for product '%s'"
FLAVOUR = "Configuration flavour"
# The following messages (starting with MSG_) correspond to flavour
# The following messages (starting with MSG_) correspond to tool
# attributes added for every gen-class (warning: the message IDs correspond
# to MSG_<attributePrexix>).
MSG_optionalFieldsFor = 'Optional fields'
MSG_defaultValueFor = "Default value for field '%s'"
MSG_podTemplatesFor = "POD templates"
MSG_podMaxShownTemplatesFor = "Max shown POD templates"
MSG_resultColumnsFor = "Columns to display while showing query results"
MSG_showWorkflowFor = 'Show workflow-related information'
MSG_showWorkflowCommentFieldFor = 'Show field allowing to enter a ' \
'comment every time a transition is triggered'
MSG_showAllStatesInPhaseFor = 'Show all states in phase'
POD_TEMPLATE = 'POD template'
# to MSG_<attributePrefix>).
MSG_defaultValue = "Default value for field '%s'"
MSG_podTemplate = "POD template for field '%s'"
MSG_formats = "Output format(s) for field '%s'"
MSG_resultColumns = "Columns to display while showing query results"
MSG_enableAdvancedSearch = "Enable advanced search"
MSG_numberOfSearchColumns = "Number of search columns"
MSG_searchFields = "Search fields"
MSG_optionalFields = 'Optional fields'
MSG_showWorkflow = 'Show workflow-related information'
MSG_showWorkflowCommentField = 'Show field allowing to enter a ' \
'comment every time a transition is ' \
'triggered'
MSG_showAllStatesInPhase = 'Show all states in phase'
USER = 'User'
POD_ASKACTION = 'Trigger related action'
DEFAULT_VALID_ERROR = 'Please fill or correct this.'

View file

@ -32,6 +32,3 @@ class StandardRadio(Radio):
c = Config()
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!'
install = Action(action=onInstall)
class ZopeComponentFlavour(Flavour):
anIntegerOption = Integer()
bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
link=False, back=Ref(attribute='backToTool'),
shownInfo=('description',), page='data')
def onEdit(self, created):
if 'Escadron de la mort' not in [b.title for b in self.bunchesOfGeeks]:
self.create('bunchesOfGeeks', title='Escadron de la mort',
description='I want those guys everywhere!')
class ZopeComponentWorkflow:
# Specific permissions
wf = WritePermission('ZopeComponent.funeralDate')

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,
'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()
# ------------------------------------------------------------------------------