Various improvements in both pod and gen.

This commit is contained in:
Gaetan Delannay 2009-11-06 11:33:56 +01:00
parent 1c0744da85
commit 37cf9e7a4f
27 changed files with 227 additions and 49 deletions

View file

@ -1,3 +1,7 @@
0.4.1 (2009-11-03)
- Ajax framework within appy.gen
- More improvements in XHTML->ODT conversion within appy.pod
0.4.0 (2009-08-12)
- Alpha version.

View file

@ -30,8 +30,9 @@ class Import:
class Search:
'''Used for specifying a search for a given type.'''
def __init__(self, name, sortBy='title', limit=None, **fields):
def __init__(self, name, group=None, sortBy='', limit=None, **fields):
self.name = name
self.group = group # Searches may be visually grouped in the portlet
self.sortBy = sortBy
self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field
@ -420,7 +421,8 @@ class State:
return res
class Transition:
def __init__(self, states, condition=True, action=None, notify=None):
def __init__(self, states, condition=True, action=None, notify=None,
show=True):
self.states = states # In its simpler form, it is a tuple with 2
# states: (fromState, toState). But it can also be a tuple of several
# (fromState, toState) sub-tuples. This way, you may define only 1
@ -430,6 +432,8 @@ class Transition:
self.action = action
self.notify = notify # If not None, it is a method telling who must be
# notified by email after the transition has been executed.
self.show = show # If False, the end user will not be able to trigger
# the transition. It will only be possible by code.
def getUsedRoles(self):
'''If self.condition is specifies a role.'''

View file

@ -145,8 +145,9 @@ class Generator(AbstractGenerator):
self.copyFile('Portlet.pt', self.repls,
destName='%s.pt' % self.portletName, destFolder=self.skinsFolder)
self.copyFile('tool.gif', {})
self.copyFile('Styles.css.dtml', self.repls, destFolder=self.skinsFolder,
self.copyFile('Styles.css.dtml',self.repls, destFolder=self.skinsFolder,
destName = '%s.css.dtml' % self.applicationName)
self.copyFile('IEFixes.css.dtml',self.repls,destFolder=self.skinsFolder)
if self.config.minimalistPlone:
self.copyFile('colophon.pt', self.repls,destFolder=self.skinsFolder)
self.copyFile('footer.pt', self.repls, destFolder=self.skinsFolder)
@ -685,13 +686,17 @@ class Generator(AbstractGenerator):
self.labels.append(poMsgPl)
# Create i18n labels for searches
for search in classDescr.getSearches(classDescr.klass):
searchLabelId = '%s_search_%s' % (classDescr.name, search.name)
searchDescrId = '%s_descr' % searchLabelId
for label in (searchLabelId, searchDescrId):
searchLabel = '%s_search_%s' % (classDescr.name, search.name)
labels = [searchLabel, '%s_descr' % searchLabel]
if search.group:
grpLabel = '%s_searchgroup_%s' % (classDescr.name, search.group)
labels += [grpLabel, '%s_descr' % grpLabel]
for label in labels:
default = ' '
if label == searchLabelId: default = search.name
if label == searchLabel: default = search.name
poMsg = PoMessage(label, '', default)
poMsg.produceNiceDefault()
if poMsg not in self.labels:
self.labels.append(poMsg)
# Generate the resulting Archetypes class and schema.
self.copyFile('ArchetypesTemplate.py', repls, destName=fileName)

View file

@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
import re, os, os.path
import re, os, os.path, Cookie
from appy.gen.utils import FieldDescr, SomeObjects
from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
@ -98,14 +98,18 @@ class ToolMixin(AbstractMixin):
def showPortlet(self):
return not self.portal_membership.isAnonymousUser()
_sortFields = {'title': 'sortable_title'}
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0):
startNumber=0, search=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 a search defined on p_contentType: additional search
criteria will be added to the query. We will retrieve objects from
p_startNumber.'''
p_startNumber. If p_search is defined, it corresponds to a custom
Search instance (instead of a predefined named search like in
p_searchName). If both p_searchName and p_search are given, p_search
is ignored.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
# Several content types are specified
@ -117,23 +121,32 @@ class ToolMixin(AbstractMixin):
portalTypes = contentType
params = {'portal_type': portalTypes, 'batch': True}
# Manage additional criteria from a search when relevant
if searchName:
if searchName or search:
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
# Find the search
search = ArchetypesClassDescriptor.getSearch(appyClass, searchName)
if searchName:
search = ArchetypesClassDescriptor.getSearch(
appyClass, searchName)
if search:
# Add additional search criteria
for fieldName, fieldValue in search.fields.iteritems():
appyType = getattr(appyClass, fieldName)
attrName = fieldName
if (appyType.type == 'String') and appyType.isMultiValued():
attrName = 'get%s%s' % (fieldName[0].upper(), fieldName[1:])
if attrName == 'title': attrName = 'Title'
elif attrName == 'description': attrName = 'Description'
elif attrName == 'state': attrName = 'review_state'
else: attrName = 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
params[attrName] = fieldValue
# Add a sort order if specified
sb = search.sortBy
if sb:
# For field 'title', Plone has created a specific index
# 'sortable_title', because index 'Title' is a ZCTextIndex
# (for searchability) and can't be used for sorting.
if self._sortFields.has_key(sb): sb = self._sortFields[sb]
params['sort_on'] = sb
brains = self.portal_catalog.searchResults(**params)
print 'Number of results per page is', self.getNumberOfResultsPerPage()
print 'StartNumber is', startNumber
res = SomeObjects(brains, self.getNumberOfResultsPerPage(), startNumber)
res.brainsToObjects()
print 'Res?', res.totalNumber, res.batchSize, res.startNumber
return res.__dict__
def getResultColumnsNames(self, contentType):
@ -322,10 +335,43 @@ class ToolMixin(AbstractMixin):
return res
def getSearches(self, contentType):
'''Returns the searches that are defined for p_contentType.'''
'''Returns the list of searches that are defined for p_contentType.
Every list item is a dict that contains info about a search or about
a group of searches.'''
appyClass = self.getAppyClass(contentType)
return [s.__dict__ for s in \
ArchetypesClassDescriptor.getSearches(appyClass)]
res = []
visitedGroups = {} # Names of already visited search groups
for search in ArchetypesClassDescriptor.getSearches(appyClass):
# Determine first group label, we will need it.
groupLabel = ''
if search.group:
groupLabel = '%s_searchgroup_%s' % (contentType, search.group)
# Add an item representing the search group if relevant
if search.group and (search.group not in visitedGroups):
group = {'name': search.group, 'isGroup': True,
'labelId': groupLabel, 'searches': [],
'label': self.translate(groupLabel),
'descr': self.translate('%s_descr' % groupLabel),
}
res.append(group)
visitedGroups[search.group] = group
# Add the search itself
searchLabel = '%s_search_%s' % (contentType, search.name)
dSearch = {'name': search.name, 'isGroup': False,
'label': self.translate(searchLabel),
'descr': self.translate('%s_descr' % searchLabel)}
if search.group:
visitedGroups[search.group]['searches'].append(dSearch)
else:
res.append(dSearch)
return res
def getCookieValue(self, cookieId, default=''):
'''Server-side code for getting the value of a cookie entry.'''
cookie = Cookie.SimpleCookie(self.REQUEST['HTTP_COOKIE'])
cookieValue = cookie.get(cookieId)
if cookieValue: return cookieValue.value
return default
def getQueryUrl(self, contentType, flavourNumber, searchName):
'''This method creates the URL that allows to perform an ajax GET
@ -335,5 +381,4 @@ class ToolMixin(AbstractMixin):
'&page=macros&macro=queryResult&contentType=%s&flavourNumber=%s' \
'&searchName=%s&startNumber=' % (self.UID(), contentType,
flavourNumber, searchName)
# ------------------------------------------------------------------------------

View file

@ -320,7 +320,7 @@ class AbstractMixin:
fieldDescr = fieldDescr.__dict__
appyType = fieldDescr['appyType']
if isEdit and (appyType['type']=='Ref') and appyType['add']:return False
if isEdit and appyType['type']=='Action': return False
if isEdit and (appyType['type'] in ('Action', 'Computed')): return False
if (fieldDescr['widgetType'] == 'backField') and \
not self.getBRefs(fieldDescr['fieldRel']): return False
# Do not show field if it is optional and not selected in flavour
@ -405,6 +405,22 @@ class AbstractMixin:
res.append(StateDescr(stateName, stateStatus).get())
return res
def getAppyTransitions(self):
'''Returns the transitions that the user can trigger on p_self.'''
transitions = self.portal_workflow.getTransitionsFor(self)
res = []
if transitions:
# Retrieve the corresponding Appy transition, to check if the user
# may view it.
workflow = self.getWorkflow(appy=True)
if not workflow: return transitions
for transition in transitions:
# Get the corresponding Appy transition
appyTr = workflow._transitionsMapping[transition['id']]
if self._appy_showTransition(workflow, appyTr.show):
res.append(transition)
return res
def getAppyPage(self, isEdit, phaseInfo, appyName=True):
'''On which page am I? p_isEdit indicates if the current page is an
edit or consult view. p_phaseInfo indicates the current phase.'''
@ -809,6 +825,12 @@ class AbstractMixin:
return stateShow(workflow, self._appy_getWrapper())
else: return stateShow
def _appy_showTransition(self, workflow, transitionShow):
'''Must I show a transition whose "show value" is p_transitionShow?'''
if callable(transitionShow):
return transitionShow(workflow, self._appy_getWrapper())
else: return transitionShow
def _appy_managePermissions(self):
'''When an object is created or updated, we must update "add"
permissions accordingly: if the object is a folder, we must set on

View file

@ -197,7 +197,7 @@
<div metal:use-macro="here/skyn/ref/macros/showReference" />
</div>
<span metal:define-macro="showGroup">
<metal:group define-macro="showGroup">
<fieldset class="appyGroup">
<legend><i tal:define="groupDescription python:contextObj.translate('%s_group_%s' % (contextObj.meta_type, widgetDescr['name']))"
tal:content="groupDescription"></i></legend>
@ -222,9 +222,9 @@
</table>
</fieldset>
<br/>
</span>
</metal:group>
<div metal:define-macro="listFields"
<metal:fields define-macro="listFields"
tal:repeat="widgetDescr python: contextObj.getAppyFields(isEdit, pageName)">
<tal:displayArchetypesField condition="python: widgetDescr['widgetType'] == 'field'">
@ -232,19 +232,17 @@
<metal:field use-macro="here/skyn/macros/macros/showArchetypesField" />
</tal:atField>
</tal:displayArchetypesField>
<tal:displayBackwardRef condition="python: (not isEdit) and (widgetDescr['widgetType'] == 'backField')">
<tal:backRef condition="python: widgetDescr['appyType']['backd']['page'] == pageName">
<metal:field metal:use-macro="here/skyn/macros/macros/showBackwardField" />
</tal:backRef>
</tal:displayBackwardRef>
<tal:displayGroup condition="python: widgetDescr['widgetType'] == 'group'">
<tal:displayG condition="python: widgetDescr['page'] == pageName">
<metal:group metal:use-macro="here/skyn/macros/macros/showGroup" />
</tal:displayG>
</tal:displayGroup>
</div>
</metal:fields>
<span metal:define-macro="byline"
tal:condition="python: site_properties.allowAnonymousViewAbout or not isAnon"
@ -574,7 +572,10 @@
onClick python:'javascript:onSort(\'title\')';"
id="arrow_title" style="cursor:pointer"/>
<span tal:content="python: tool.translate('ref_name')"/>
<input id="filter_title" type="text" size="5" onkeyup="javascript:onTextEntered('title')"/>
<!--input id="filter_title" type="text" size="5" onkeyup="javascript:onTextEntered('title')"/-->
<tal:comment replace="nothing">Input fields like this have been commented out because they will
be replaced by Ajax server- searches that will be more relevant (the current Javascript search
is limited to the batch, which has little interest).</tal:comment>
</th>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
@ -592,9 +593,9 @@
<tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState>
<input type="text" size="5"
<!--input type="text" size="5"
tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/>
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/-->
</th>
</tal:columnHeader>
@ -604,8 +605,8 @@
onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/>
<span tal:replace="python: tool.translate('root_type')"/>
<input type="text" size="5" id="filter_root_type"
tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/>
<!--input type="text" size="5" id="filter_root_type"
tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/-->
</th>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
@ -716,7 +717,7 @@
</metal:states>
<metal:transitions define-macro="transitions"
tal:define="transitions python: contextObj.portal_workflow.getTransitionsFor(contextObj);"
tal:define="transitions contextObj/getAppyTransitions"
tal:condition="transitions">
<form id="triggerTransitionForm" method="post"
tal:attributes="action python: contextObj.absolute_url() + '/skyn/do'">
@ -746,6 +747,35 @@
tal:define="queryUrl python: '%s/skyn/query' % appFolder.absolute_url();
currentSearch request/search|nothing;
currentType request/type_name|nothing;">
<script language="javascript">
<!--
function toggleSearchGroup(groupId) {
// What is the state of this toggle?
var state = readCookie(groupId);
if ((state != 'collapsed') && (state != 'expanded')) {
// No cookie yet, create it.
createCookie(groupId, 'expanded');
state = 'expanded';
}
var group = document.getElementById(groupId);
var displayValue = 'none';
var newState = 'collapsed';
var imgSrc = 'skyn/expand.gif';
if (state == 'collapsed') {
// Expand the group
displayValue = 'block';
imgSrc = 'skyn/collapse.gif';
newState = 'expanded';
}
// Update group visibility and img
group.style.display = displayValue;
var img = document.getElementById(groupId + '_img');
img.src = imgSrc;
// Inverse the cookie value
createCookie(groupId, newState);
}
-->
</script>
<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
@ -796,14 +826,40 @@
</table>
</dt>
<tal:comment replace="nothing">Searches for this content type.</tal:comment>
<dt class="portletAppyItem portletSearch" tal:repeat="search python: tool.getSearches(rootClass)">
<a tal:define="searchLabel python: '%s_search_%s' % (rootClass, search['name']);
searchDescr python: '%s_descr' % searchLabel"
tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title python: tool.translate(searchDescr);
class python: test(search['name'] == currentSearch, 'portletCurrent', '')"
tal:content="structure python: tool.translate(searchLabel)"></a>
<tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)">
<tal:group condition="searchOrGroup/isGroup">
<tal:expanded define="group searchOrGroup;
expanded python: tool.getCookieValue(group['labelId']) == 'expanded'">
<tal:comment replace="nothing">Group name</tal:comment>
<dt class="portletAppyItem portletGroup">
<img align="left" style="cursor:pointer"
tal:attributes="id python: '%s_img' % group['labelId'];
src python:test(expanded, 'skyn/collapse.gif', 'skyn/expand.gif');
onClick python:'javascript:toggleSearchGroup(\'%s\')' % group['labelId']"/>&nbsp;
<span tal:replace="group/label"/>
</dt>
<tal:comment replace="nothing">Group searches</tal:comment>
<div tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletAppyItem portletSearch" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
</dt>
</div>
</tal:expanded>
</tal:group>
<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']);
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');
id search/group"
tal:content="structure search/label"></a>
</dt>
</tal:searchOrGroup>
</tal:section>
<tal:comment replace="nothing">All objects in flavour</tal:comment>

0
gen/plone25/templates/ArchetypesTemplate.py Executable file → Normal file
View file

0
gen/plone25/templates/FlavourTemplate.py Executable file → Normal file
View file

0
gen/plone25/templates/Install.py Executable file → Normal file
View file

0
gen/plone25/templates/PodTemplate.py Executable file → Normal file
View file

0
gen/plone25/templates/Portlet.pt Executable file → Normal file
View file

0
gen/plone25/templates/ProfileInit.py Executable file → Normal file
View file

6
gen/plone25/templates/Styles.css.dtml Executable file → Normal file
View file

@ -230,6 +230,12 @@ fieldset {
.portletSearch {
padding: 0 0 0 0.6em;
font-style: italic;
font-size: 95%;
}
.portletGroup {
font-variant: small-caps;
font-weight: bold;
font-style: normal;
}
.portletCurrent {
font-weight: bold;

0
gen/plone25/templates/ToolTemplate.py Executable file → Normal file
View file

0
gen/plone25/templates/__init__.py Executable file → Normal file
View file

0
gen/plone25/templates/appyWrappers.py Executable file → Normal file
View file

0
gen/plone25/templates/colophon.pt Executable file → Normal file
View file

0
gen/plone25/templates/config.py Executable file → Normal file
View file

0
gen/plone25/templates/configure.zcml Executable file → Normal file
View file

0
gen/plone25/templates/footer.pt Executable file → Normal file
View file

0
gen/plone25/templates/frontPage.pt Executable file → Normal file
View file

0
gen/plone25/templates/import_steps.xml Executable file → Normal file
View file

0
gen/plone25/templates/tool.gif Executable file → Normal file
View file

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

View file

@ -7,7 +7,6 @@ class ToolWrapper:
res = None
initiatorUid = self.session['initiator']
if initiatorUid:
res = self.o.uid_catalog(UID=initiatorUid)[0].getObject().\
_appy_getWrapper(force=True)
res = self.o.uid_catalog(UID=initiatorUid)[0].getObject().appy()
return res
# ------------------------------------------------------------------------------

View file

@ -2,7 +2,8 @@
developer the real classes used by the underlying web framework.'''
# ------------------------------------------------------------------------------
import time, os.path, mimetypes
import time, os.path, mimetypes, unicodedata
from appy.gen import Search
from appy.gen.utils import sequenceTypes
from appy.shared.utils import getOsTempFolder
@ -74,6 +75,8 @@ class AbstractWrapper:
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
session = property(get_session)
def get_typeName(self): return self.__class__.__bases__[-1].__name__
@ -89,6 +92,8 @@ class AbstractWrapper:
stateLabel = property(get_stateLabel)
def get_klass(self): return self.__class__.__bases__[1]
klass = property(get_klass)
def get_url(self): return self.o.absolute_url()+'/skyn/view'
url = property(get_url)
def link(self, fieldName, obj):
'''This method links p_obj to this one through reference field
@ -226,6 +231,33 @@ class AbstractWrapper:
else: logMethod = logger.info
logMethod(message)
def normalize(self, s):
'''Returns a version of string p_s whose special chars have been
replaced with normal chars.'''
return unicodedata.normalize('NFKD', s).encode("ascii","ignore")
def search(self, klass, sortBy='', **fields):
'''Searches objects of p_klass. p_sortBy must be the name of an indexed
field (declared with indexed=True); every param in p_fields must
take the name of an indexed field and take a possible value of this
field.'''
# Find the content type corresponding to p_klass
flavour = self.flavour
contentType = flavour.o.getPortalType(klass)
# Create the Search object
search = Search('customSearch', sortBy=sortBy, **fields)
res = self.tool.o.executeQuery(contentType,flavour.number,search=search)
return [o.appy() for o in res['objects']]
def reindex(self):
'''Asks a direct object reindexing. In most cases you don't have to
reindex objects "manually" with this method. When an object is
modified after some user action has been performed, Appy reindexes
this object automatically. But if your code modifies other objects,
Appy may not know that they must be reindexed, too. So use this
method in those cases.'''
self.o.reindexObject()
# ------------------------------------------------------------------------------
class FileWrapper:
'''When you get, from an appy object, the value of a File attribute, you

View file

@ -29,6 +29,7 @@ XHTML_INNER_TAGS = ('b', 'i', 'u', 'em')
XHTML_UNSTYLABLE_TAGS = XHTML_LISTS + ('li', 'a')
XML_SPECIAL_CHARS = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
XML_ENTITIES = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': "'"}
# ------------------------------------------------------------------------------
class PodError(Exception):

View file

@ -59,6 +59,10 @@ HTML_ENTITIES = {
'ucirc':'û', 'uuml':'ü', 'yacute':'ý', 'thorn':'þ', 'yuml':'ÿ',
'euro':'', 'nbsp':' ', "rsquo":"'", "lsquo":"'", "ldquo":"'",
"rdquo":"'", 'ndash': ' ', 'oelig':'oe', 'quot': "'", 'mu': 'µ'}
import htmlentitydefs
for k, v in htmlentitydefs.entitydefs.iteritems():
if not HTML_ENTITIES.has_key(k) and not XML_ENTITIES.has_key(k):
HTML_ENTITIES[k] = ''
# ------------------------------------------------------------------------------
class Entity: