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:
parent
9f4db88bdf
commit
990e16c6e7
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
158
gen/__init__.py
158
gen/__init__.py
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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.'''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.mixins import AbstractMixin
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ClassMixin(AbstractMixin):
|
||||
_appy_meta_type = 'Class'
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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]
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,7 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.mixins import AbstractMixin
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class PodTemplateMixin(AbstractMixin):
|
||||
_appy_meta_type = 'PodTemplate'
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.mixins import AbstractMixin
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class UserMixin(AbstractMixin):
|
||||
_appy_meta_type = 'UserMixin'
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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.'''
|
||||
|
|
|
@ -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']
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)<=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>
|
||||
</span>
|
||||
<tal:comment replace="nothing">Display templates as a list if a lot of templates must be shown</tal:comment>
|
||||
<select tal:condition="python: len(podTemplates)>maxShownTemplates">
|
||||
<option value="" 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] > 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)>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++){
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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\', "%s")' % (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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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\', "%s", "%s")' % (formCall, addConfirmMsg), formCall);
|
||||
noFormCall python: navBaseCall.replace('**v**', '%d, \'CreateWithoutForm\'' % startNumber);
|
||||
noFormCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', "%s", "%s")' % (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)<=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">
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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!>
|
||||
|
|
|
@ -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!>')
|
|
@ -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!>')
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!>]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,6 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.plone25.wrappers import AbstractWrapper
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class PodTemplateWrapper(AbstractWrapper): pass
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
28
gen/po.py
28
gen/po.py
|
@ -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.'
|
||||
|
|
|
@ -32,6 +32,3 @@ class StandardRadio(Radio):
|
|||
|
||||
c = Config()
|
||||
c.languages = ('en', 'fr')
|
||||
|
||||
class CarFlavour(Flavour):
|
||||
explanation = String(group="userInterface")
|
||||
|
|
|
@ -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
107
shared/dav.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
149
shared/utils.py
149
shared/utils.py
|
@ -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()
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in a new issue