appy.gen: added a widget 'List' for rendering grids of data.

This commit is contained in:
Gaetan Delannay 2011-10-19 09:37:44 +02:00
parent f1136eb786
commit c11378c747
11 changed files with 315 additions and 41 deletions

View file

@ -5,7 +5,7 @@ from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \ from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
getClassName, SomeObjects getClassName, SomeObjects, AppyObject
import appy.pod import appy.pod
from appy.pod.renderer import Renderer from appy.pod.renderer import Renderer
from appy.shared.data import countries from appy.shared.data import countries
@ -517,6 +517,11 @@ class Type:
# We must initialise the corresponding back reference # We must initialise the corresponding back reference
self.back.klass = klass self.back.klass = klass
self.back.init(self.back.attribute, self.klass, appName) self.back.init(self.back.attribute, self.klass, appName)
if isinstance(self, List):
for subName, subField in self.fields:
fullName = '%s_%s' % (name, subName)
subField.init(fullName, klass, appName)
subField.name = '%s*%s' % (name, subName)
def reload(self, klass, obj): def reload(self, klass, obj):
'''In debug mode, we want to reload layouts without restarting Zope. '''In debug mode, we want to reload layouts without restarting Zope.
@ -1534,7 +1539,6 @@ class File(Type):
return value._atFile return value._atFile
def getRequestValue(self, request): def getRequestValue(self, request):
res = request.get('%s_file' % self.name)
return request.get('%s_file' % self.name) return request.get('%s_file' % self.name)
def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'} def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'}
@ -1784,9 +1788,6 @@ class Ref(Type):
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
return value return value
def getRequestValue(self, request):
return request.get('appy_ref_%s' % self.name, None)
def validateValue(self, obj, value): def validateValue(self, obj, value):
if not self.link: return None if not self.link: return None
# We only check "link" Refs because in edit views, "add" Refs are # We only check "link" Refs because in edit views, "add" Refs are
@ -2210,6 +2211,94 @@ class Pod(Type):
value = value._atFile value = value._atFile
setattr(obj, self.name, value) setattr(obj, self.name, value)
class List(Type):
'''A list.'''
def __init__(self, fields, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
subLayouts=Table('fv', width=None)):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission,
width, height, None, colspan, master, masterValue, focus,
historized, True, mapping, label)
self.validable = True
# Tuples of (names, Type instances) determining the format of every
# element in the list.
self.fields = fields
self.fieldsd = [(n, f.__dict__) for (n,f) in self.fields]
# Force some layouting for sub-fields, if subLayouts are given. So the
# one who wants freedom on tuning layouts at the field level must
# specify subLayouts=None.
if subLayouts:
for name, field in self.fields:
field.layouts = field.formatLayouts(subLayouts)
def getField(self, name):
'''Gets the field definition whose name is p_name.'''
for n, field in self.fields:
if n == name: return field
def isEmptyValue(self, value, obj=None):
'''Returns True if the p_value must be considered as an empty value.'''
return not value
def getRequestValue(self, request):
'''Concatenates the list from distinct form elements in the request.'''
prefix = self.name + '*' + self.fields[0][0] + '*'
res = {}
for key in request.keys():
if not key.startswith(prefix): continue
# I have found a row. Gets its index
row = AppyObject()
rowIndex = int(key.split('*')[-1])
if rowIndex == -1: continue # Ignore the template row.
for name, field in self.fields:
keyName = '%s*%s*%s' % (self.name, name, rowIndex)
if request.has_key(keyName):
# Simulate the request as if it was for a single value
request.set(field.name, request[keyName])
v = field.getRequestValue(request)
else:
v = None
setattr(row, name, v)
res[rowIndex] = row
# Produce a sorted list.
keys = res.keys()
keys.sort()
res = [res[key] for key in keys]
print 'REQUEST VALUE FOR LIST (%d)' % len(res)
for value in res:
for k, v in value.__dict__.iteritems():
print k, '=', v
# I store in the request this computed value. This way, when individual
# subFields will need to get their value, they will take it from here,
# instead of taking it from the specific request key. Indeed, specific
# request keys contain row indexes that may be wrong after row deletions
# by the user.
request.set(self.name, res)
return res
def getStorableValue(self, value):
'''Gets p_value in a form that can be stored in the database.'''
for v in value:
for name, field in self.fields:
setattr(v, name, field.getStorableValue(getattr(v, name)))
return value
def getInnerValue(self, obj, name, i):
'''Returns the value of inner field named p_name in row number p_i
with the list of values from this field on p_obj.'''
if i == -1: return ''
value = getattr(obj, self.name, None)
if not value: return ''
if i >= len(value): return ''
return getattr(value[i], name, '')
# Workflow-specific types and default workflows -------------------------------- # Workflow-specific types and default workflows --------------------------------
appyToZopePermissions = { appyToZopePermissions = {
'read': ('View', 'Access contents information'), 'read': ('View', 'Access contents information'),

View file

@ -120,6 +120,14 @@ class FieldDescriptor:
# Add the POD-related fields on the Tool # Add the POD-related fields on the Tool
self.generator.tool.addPodRelatedFields(self) self.generator.tool.addPodRelatedFields(self)
def walkList(self):
# Add i18n-specific messages
for name, field in self.appyType.fields:
label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, name)
msg = PoMessage(label, '', name)
msg.produceNiceDefault()
self.generator.labels.append(msg)
def walkAppyType(self): def walkAppyType(self):
'''Walks into the Appy type definition and gathers data about the '''Walks into the Appy type definition and gathers data about the
i18n labels.''' i18n labels.'''
@ -176,6 +184,8 @@ class FieldDescriptor:
elif self.appyType.type == 'Ref': self.walkRef() elif self.appyType.type == 'Ref': self.walkRef()
# Manage things which are specific to Pod types # Manage things which are specific to Pod types
elif self.appyType.type == 'Pod': self.walkPod() elif self.appyType.type == 'Pod': self.walkPod()
# Manage things which are specific to List types
elif self.appyType.type == 'List': self.walkList()
def generate(self): def generate(self):
'''Generates the i18n labels for this type.''' '''Generates the i18n labels for this type.'''

View file

@ -142,6 +142,15 @@ class BaseMixin:
setattr(errors, appyType.name, message) setattr(errors, appyType.name, message)
else: else:
setattr(values, appyType.name, appyType.getStorableValue(value)) setattr(values, appyType.name, appyType.getStorableValue(value))
# Validate sub-fields within Lists
if appyType.type != 'List': continue
i = -1
for row in value:
i += 1
for name, field in appyType.fields:
message = field.validate(self, getattr(row,name,None))
if message:
setattr(errors, '%s*%d' % (field.name, i), message)
def interFieldValidation(self, errors, values): def interFieldValidation(self, errors, values):
'''This method is called when individual validation of all fields '''This method is called when individual validation of all fields
@ -432,13 +441,32 @@ class BaseMixin:
retrieved in synchronous mode.''' retrieved in synchronous mode.'''
appyType = self.getAppyType(name) appyType = self.getAppyType(name)
if not onlyIfSync or (onlyIfSync and appyType.sync[layoutType]): if not onlyIfSync or (onlyIfSync and appyType.sync[layoutType]):
return appyType.getValue(self) # We must really get the field value.
if '*' not in name: return appyType.getValue(self)
# The field is an inner field from a List.
listName, name, i = name.split('*')
return self.getAppyType(listName).getInnerValue(self, name, int(i))
def getFormattedFieldValue(self, name, value): def getFormattedFieldValue(self, name, value):
'''Gets a nice, string representation of p_value which is a value from '''Gets a nice, string representation of p_value which is a value from
field named p_name.''' field named p_name.'''
return self.getAppyType(name).getFormattedValue(self, value) return self.getAppyType(name).getFormattedValue(self, value)
def getRequestFieldValue(self, name):
'''Gets the value of field p_name as may be present in the request.'''
# Return the request value for standard fields.
if '*' not in name:
return self.getAppyType(name).getRequestValue(self.REQUEST)
# For sub-fields within Lists, the corresponding request values have
# already been computed in the request key corresponding to the whole
# List.
listName, name, rowIndex = name.split('*')
rowIndex = int(rowIndex)
if rowIndex == -1: return ''
allValues = self.REQUEST.get(listName)
if not allValues: return ''
return getattr(allValues[rowIndex], name, '')
def getFileInfo(self, fileObject): def getFileInfo(self, fileObject):
'''Returns filename and size of p_fileObject.''' '''Returns filename and size of p_fileObject.'''
if not fileObject: return {'filename': '', 'size': 0} if not fileObject: return {'filename': '', 'size': 0}
@ -533,11 +561,15 @@ class BaseMixin:
def getAppyType(self, name, asDict=False, className=None): def getAppyType(self, name, asDict=False, className=None):
'''Returns the Appy type named p_name. If no p_className is defined, the '''Returns the Appy type named p_name. If no p_className is defined, the
field is supposed to belong to self's class.''' field is supposed to belong to self's class.'''
isInnerType = '*' in name # An inner type lies within a List type.
subName = None
if isInnerType: name, subName, i = name.split('*')
if not className: if not className:
klass = self.__class__.wrapperClass klass = self.__class__.wrapperClass
else: else:
klass = self.getTool().getAppyClass(className, wrapper=True) klass = self.getTool().getAppyClass(className, wrapper=True)
res = getattr(klass, name, None) res = getattr(klass, name, None)
if res and isInnerType: res = res.getField(subName)
if res and asDict: return res.__dict__ if res and asDict: return res.__dict__
return res return res

View file

@ -65,6 +65,9 @@ img {border: 0;}
.list td, .list th { border: 1px solid grey; .list td, .list th { border: 1px solid grey;
padding-left: 5px; padding-right: 5px; padding-top: 3px;} padding-left: 5px; padding-right: 5px; padding-top: 3px;}
.list th { background-color: #cbcbcb; font-style: italic; font-weight: normal;} .list th { background-color: #cbcbcb; font-style: italic; font-weight: normal;}
.grid th { font-style: italic; font-weight: normal;
border-bottom: 2px solid grey; padding: 2px 2px;}
.grid td { padding-right: 5px; }
.noStyle { border: 0 !important; padding: 0 !important; margin: 0 !important; } .noStyle { border: 0 !important; padding: 0 !important; margin: 0 !important; }
.noStyle td { border:0 !important; padding:0 !important; margin:0 !important; } .noStyle td { border:0 !important; padding:0 !important; margin:0 !important; }
.translationLabel { background-color: #EAEAEA; border-bottom: 1px dashed grey; .translationLabel { background-color: #EAEAEA; border-bottom: 1px dashed grey;

View file

@ -492,3 +492,59 @@ function initTab(cookieId, defaultValue) {
if (!toSelect) { showTab(defaultValue) } if (!toSelect) { showTab(defaultValue) }
else { showTab(toSelect); } else { showTab(toSelect); }
} }
// List-related Javascript functions
function updateRowNumber(row, rowIndex, action) {
/* Within p_row, we update every field whose name and id include the row index
with new p_rowIndex. If p_action is 'set', p_rowIndex becomes the new
index. If p_action is 'add', new index becomes:
existing index + p_rowIndex. */
tagTypes = ['input', 'select'];
currentIndex = -1;
for (var i=0; i < tagTypes.length; i++) {
widgets = row.getElementsByTagName(tagTypes[i]);
for (var j=0; j < widgets.length; j++) {
id = widgets[j].id;
name = widgets[j].name;
idNbIndex = id.lastIndexOf('*') + 1;
nameNbIndex = name.lastIndexOf('*') + 1;
// Compute the current row index if not already done.
if (currentIndex == -1) {
currentIndex = parseInt(id.substring(idNbIndex));
}
// Compute the new values for attributes "id" and "name".
newId = id.substring(0, idNbIndex);
newName = id.substring(0, nameNbIndex);
newIndex = rowIndex;
if (action == 'add') newIndex = newIndex + currentIndex;
widgets[j].id = newId + String(newIndex);
widgets[j].name = newName + String(newIndex);
}
}
}
function insertRow(tableId) {
// This function adds a new row in table with ID p_tableId.
table = document.getElementById(tableId);
newRow = table.rows[1].cloneNode(true);
newRow.style.display = 'table-row';
// Within newRow, I must include in field names and ids the row number
updateRowNumber(newRow, table.rows.length-3, 'set');
table.tBodies[0].appendChild(newRow);
}
function deleteRow(tableId, deleteImg) {
row = deleteImg.parentNode.parentNode;
table = document.getElementById(tableId);
allRows = table.rows;
toDeleteIndex = -1; // Will hold the index of the row to delete.
for (var i=0; i < allRows.length; i++) {
if (toDeleteIndex == -1) {
if (row == allRows[i]) toDeleteIndex = i;
}
else {
// Decrement higher row numbers by 1 because of the deletion
updateRowNumber(allRows[i], -1, 'add');
}
}
table.deleteRow(toDeleteIndex);
}

View file

@ -0,0 +1,72 @@
<tal:comment replace="nothing">Single row.</tal:comment>
<tr metal:define-macro="row"
tal:attributes="style python: (rowIndex==-1) and 'display: none' or ''">
<td align="center" tal:repeat="fieldInfo widget/fieldsd">
<tal:show define="widget python: fieldInfo[1];
tagCss python: 'noStyle';
widgetName python: '%s*%d' % (widget['name'], rowIndex)">
<metal:call use-macro="app/skyn/widgets/show/macros/field"/>
</tal:show>
</td>
<tal:comment replace="nothing">Icon for removing the row</tal:comment>
<td align="right" tal:condition="python: layoutType=='edit'">
<img style="cursor:pointer"
tal:attributes="src string:$appUrl/skyn/delete.png;
title python: 'Delete';
onClick python: 'deleteRow(\'list_%s\',this)' % name"/>
</td>
</tr>
<tal:comment replace="nothing">The whole table, edit or view.</tal:comment>
<table metal:define-macro="table" class="grid"
tal:define="isEdit python: layoutType == 'edit'"
tal:condition="python: isEdit or value"
tal:attributes="id python: 'list_%s' % name">
<tal:comment replace="nothing">Header</tal:comment>
<tr>
<th tal:repeat="fieldInfo widget/fieldsd"
tal:content="python: _(fieldInfo[1]['labelId'])">
</th>
<tal:comment replace="nothing">Icon for adding a new row.</tal:comment>
<th tal:condition="isEdit">
<img style="cursor:pointer"
tal:attributes="src string:$appUrl/skyn/plus.png;
title python: _('add_ref');
onClick python: 'insertRow(\'list_%s\')' % name"/>
</th>
</tr>
<tal:comment replace="nothing">Template row (edit only)</tal:comment>
<tal:templateRow define="rowIndex python:-1" condition="isEdit">
<metal:call use-macro="app/skyn/widgets/list/macros/row"/>
</tal:templateRow>
<tr height="7px"><td></td></tr>
<tal:comment replace="nothing">Rows of data</tal:comment>
<tal:rows define="rows python: inRequest and requestValue or value"
repeat="row rows">
<tal:row define="rowIndex repeat/row/index">
<metal:call use-macro="app/skyn/widgets/list/macros/row"/>
</tal:row>
</tal:rows>
</table>
<tal:comment replace="nothing">View</tal:comment>
<metal:view define-macro="view">
<metal:call use-macro="app/skyn/widgets/list/macros/table"/>
</metal:view>
<tal:comment replace="nothing">Edit</tal:comment>
<metal:edit define-macro="edit">
<tal:comment replace="nothing">
The following input makes Appy aware that this field is in the request.
</tal:comment>
<input type="hidden" tal:attributes="name name" value="">
<metal:call use-macro="app/skyn/widgets/list/macros/table"/>
</metal:edit>
<tal:comment replace="nothing">Cell</tal:comment>
<metal:cell define-macro="cell">
<metal:call use-macro="app/skyn/widgets/list/macros/table"/>
</metal:cell>
<tal:comment replace="nothing">Search</tal:comment>
<metal:search define-macro="search"></metal:search>

View file

@ -28,13 +28,13 @@
<tal:comment replace="nothing">Move up</tal:comment> <tal:comment replace="nothing">Move up</tal:comment>
<img tal:condition="python: objectIndex &gt; 0" <img tal:condition="python: objectIndex &gt; 0"
tal:attributes="src string: $appUrl/skyn/arrowUp.png; tal:attributes="src string: $appUrl/skyn/arrowUp.png;
title python: tool.translate('move_up'); title python: _('move_up');
onClick python: ajaxBaseCall.replace('**v**', 'up')" onClick python: ajaxBaseCall.replace('**v**', 'up')"
style="cursor:pointer"/> style="cursor:pointer"/>
<tal:comment replace="nothing">Move down</tal:comment> <tal:comment replace="nothing">Move down</tal:comment>
<img tal:condition="python: objectIndex &lt; (totalNumber-1)" <img tal:condition="python: objectIndex &lt; (totalNumber-1)"
tal:attributes="src string: $appUrl/skyn/arrowDown.png; tal:attributes="src string: $appUrl/skyn/arrowDown.png;
title python: tool.translate('move_down'); title python: _('move_down');
onClick python: ajaxBaseCall.replace('**v**', 'down')" onClick python: ajaxBaseCall.replace('**v**', 'down')"
style="cursor:pointer"/> style="cursor:pointer"/>
</tal:moveRef> </tal:moveRef>
@ -69,7 +69,7 @@
noFormCall python: navBaseCall.replace('**v**', '%d, \'CreateWithoutForm\'' % startNumber); noFormCall python: navBaseCall.replace('**v**', '%d, \'CreateWithoutForm\'' % startNumber);
noFormCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', &quot;%s&quot;, &quot;%s&quot;)' % (noFormCall, addConfirmMsg), noFormCall)" noFormCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', &quot;%s&quot;, &quot;%s&quot;)' % (noFormCall, addConfirmMsg), noFormCall)"
tal:attributes="src string:$appUrl/skyn/plus.png; tal:attributes="src string:$appUrl/skyn/plus.png;
title python: tool.translate('add_ref'); title python: _('add_ref');
onClick python: test(appyType['noForm'], noFormCall, formCall)"/> onClick python: test(appyType['noForm'], noFormCall, formCall)"/>
</metal:plusIcon> </metal:plusIcon>
@ -108,6 +108,7 @@
ajaxHookId python: contextObj.UID()+fieldName; ajaxHookId python: contextObj.UID()+fieldName;
startNumber python: int(request.get('%s_startNumber' % ajaxHookId, 0)); startNumber python: int(request.get('%s_startNumber' % ajaxHookId, 0));
tool contextObj/getTool; tool contextObj/getTool;
_ python: tool.translate;
refObjects python:contextObj.getAppyRefs(fieldName, startNumber); refObjects python:contextObj.getAppyRefs(fieldName, startNumber);
objs refObjects/objects; objs refObjects/objects;
totalNumber refObjects/totalNumber; totalNumber refObjects/totalNumber;
@ -121,7 +122,7 @@
showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and user.has_permission(addPermission, folder) and canWrite; showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and user.has_permission(addPermission, folder) and canWrite;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1); atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: contextObj.translate('label', field=appyType); label python: contextObj.translate('label', field=appyType);
addConfirmMsg python: appyType['addConfirm'] and tool.translate('%s_addConfirm' % appyType['labelId']) or ''; addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)"> navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "view" page. <tal:comment replace="nothing">This macro displays the Reference widget on a "view" page.
@ -139,7 +140,7 @@
<tal:comment replace="nothing">If there is no object...</tal:comment> <tal:comment replace="nothing">If there is no object...</tal:comment>
<tal:noObject condition="not:objs"> <tal:noObject condition="not:objs">
<td tal:content="python: tool.translate('no_ref')"></td> <td tal:content="python: _('no_ref')"></td>
<td><metal:plusIcon use-macro="app/skyn/widgets/ref/macros/plusIcon"/></td> <td><metal:plusIcon use-macro="app/skyn/widgets/ref/macros/plusIcon"/></td>
</tal:noObject> </tal:noObject>
@ -162,7 +163,7 @@
<tal:comment replace="nothing">The search icon if field is queryable</tal:comment> <tal:comment replace="nothing">The search icon if field is queryable</tal:comment>
<a tal:condition="appyType/queryable" <a tal:condition="appyType/queryable"
tal:attributes="href python: '%s/skyn/search?type_name=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])"> tal:attributes="href python: '%s/skyn/search?type_name=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])">
<img src="search.gif" tal:attributes="title python: tool.translate('search_objects')"/></a> <img src="search.gif" tal:attributes="title python: _('search_objects')"/></a>
</legend> </legend>
<tal:comment replace="nothing">Object description</tal:comment> <tal:comment replace="nothing">Object description</tal:comment>
@ -173,7 +174,7 @@
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/> <metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
<tal:comment replace="nothing">No object is present</tal:comment> <tal:comment replace="nothing">No object is present</tal:comment>
<p tal:condition="not:objs" tal:content="python: tool.translate('no_ref')"></p> <p tal:condition="not:objs" tal:content="python: _('no_ref')"></p>
<table width="100%" tal:condition="python: objs" <table width="100%" tal:condition="python: objs"
tal:attributes="class python:test(innerRef, 'innerAppyTable', '')"> tal:attributes="class python:test(innerRef, 'innerAppyTable', '')">
@ -185,10 +186,10 @@
<tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])"> <tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])">
<tr tal:condition="appyType/showHeaders"> <tr tal:condition="appyType/showHeaders">
<th tal:repeat="widget widgets"> <th tal:repeat="widget widgets">
<span tal:content="python: tool.translate(widget['labelId'])"></span> <span tal:content="python: _(widget['labelId'])"></span>
<metal:sortIcons use-macro="app/skyn/widgets/ref/macros/sortIcons" /> <metal:sortIcons use-macro="app/skyn/widgets/ref/macros/sortIcons" />
</th> </th>
<th tal:content="python: tool.translate('ref_actions')"></th> <th tal:content="python: _('ref_actions')"></th>
</tr> </tr>
<tal:row repeat="obj objs"> <tal:row repeat="obj objs">
<tr valign="middle" tal:define="odd repeat/obj/odd" <tr valign="middle" tal:define="odd repeat/obj/odd"
@ -199,7 +200,7 @@
<metal:showObjectTitle use-macro="app/skyn/widgets/ref/macros/objectTitle"/> <metal:showObjectTitle use-macro="app/skyn/widgets/ref/macros/objectTitle"/>
</tal:title> </tal:title>
<tal:state condition="python: widget['name'] == 'state'" <tal:state condition="python: widget['name'] == 'state'"
content="python: tool.translate(obj.getWorkflowLabel())"> content="python: _(obj.getWorkflowLabel())">
</tal:state> </tal:state>
<tal:other condition="python: widget['name'] not in ('title', 'state')"> <tal:other condition="python: widget['name'] not in ('title', 'state')">
<tal:field define="contextObj python:obj; <tal:field define="contextObj python:obj;
@ -231,14 +232,13 @@
<tal:comment replace="nothing">Edit macro for an Ref.</tal:comment> <tal:comment replace="nothing">Edit macro for an Ref.</tal:comment>
<metal:editRef define-macro="edit" <metal:editRef define-macro="edit"
tal:condition="widget/link" tal:condition="widget/link"
tal:define="rname python: 'appy_ref_%s' % name; tal:define="requestValue python: request.get(name, []);
requestValue python: request.get(rname, []); inRequest python: request.has_key(name);
inRequest python: request.has_key(rname);
allObjects python: contextObj.getSelectableAppyRefs(name); allObjects python: contextObj.getSelectableAppyRefs(name);
refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']]; refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']];
isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())"> isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())">
<select tal:attributes="name rname; <select tal:attributes="name name;
size python: test(isMultiple, widget['height'], ''); size python: test(isMultiple, widget['height'], '');
multiple python: test(isMultiple, 'multiple', '')"> multiple python: test(isMultiple, 'multiple', '')">
<option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option> <option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option>

View file

@ -9,23 +9,24 @@
contextMacro The base folder containing the macros to call for contextMacro The base folder containing the macros to call for
rendering the elements within the layout. rendering the elements within the layout.
Defaults to app.skyn Defaults to app.skyn
slaveId The name and id of the main tag for this layout (used tagId The name and id of the main tag for this layout (used
for master/slave relationships). a.o. for master/slave relationships).
slaveCss The CSS class for a slave. tagCss Some additional CSS class for the main tag
(ie, the CSS class for a slave).
</tal:comment> </tal:comment>
<metal:show define-macro="layout" <metal:show define-macro="layout"
tal:define="contextMacro contextMacro| python: app.skyn; tal:define="contextMacro contextMacro| python: app.skyn;
slaveId slaveId|python:''; tagId tagId|python:'';
slaveCss slaveCss|python:''; tagCss tagCss|python:'';
layoutCss layout/css_class;"> layoutCss layout/css_class;">
<table tal:attributes="cellpadding layout/cellpadding; <table tal:attributes="cellpadding layout/cellpadding;
cellspacing layout/cellspacing; cellspacing layout/cellspacing;
width layout/width; width layout/width;
align layout/align; align layout/align;
class python: slaveCss and ('%s %s' % (slaveCss, layoutCss)) or layoutCss; class python: tagCss and ('%s %s' % (tagCss, layoutCss)) or layoutCss;
style layout/style; style layout/style;
id slaveId; id tagId;
name slaveId;"> name tagId;">
<tal:comment replace="nothing">The table header row</tal:comment> <tal:comment replace="nothing">The table header row</tal:comment>
<tr tal:condition="layout/headerRow" tal:attributes="valign layout/headerRow/valign"> <tr tal:condition="layout/headerRow" tal:attributes="valign layout/headerRow/valign">
<th tal:repeat="cell layout/headerRow/cells" <th tal:repeat="cell layout/headerRow/cells"
@ -48,23 +49,27 @@
page The page where the widget lies page The page where the widget lies
layoutType "edit"? "view"? "cell?" layoutType "edit"? "view"? "cell?"
widget The widget to render widget The widget to render
Optionally:
widgetName If the field to show is within a List, we cheat and
include, within the widgetName, the row index.
</tal:comment> </tal:comment>
<metal:field define-macro="field" <metal:field define-macro="field"
tal:define="contextMacro python: app.skyn.widgets; tal:define="contextMacro python: app.skyn.widgets;
layout python: widget['layouts'][layoutType]; layout python: widget['layouts'][layoutType];
name widget/name; name widgetName| widget/name;
sync python: widget['sync'][layoutType]; sync python: widget['sync'][layoutType];
rawValue python: contextObj.getFieldValue(name, onlyIfSync=True, layoutType=layoutType); rawValue python: contextObj.getFieldValue(name, onlyIfSync=True, layoutType=layoutType);
value python: contextObj.getFormattedFieldValue(name, rawValue); value python: contextObj.getFormattedFieldValue(name, rawValue);
requestValue python: request.get(name, None); requestValue python: contextObj.getRequestFieldValue(name);
inRequest python: request.has_key(name); inRequest python: request.has_key(name);
errors errors | python: (); errors errors | python: ();
inError python: (widget['name'] in errors) and True or False; inError python: name in errors;
isMultiple python: (widget['multiplicity'][1] == None) or (widget['multiplicity'][1] &gt; 1); isMultiple python: (widget['multiplicity'][1] == None) or (widget['multiplicity'][1] &gt; 1);
masterCss python: widget['slaves'] and ('master_%s' % name) or ''; masterCss python: widget['slaves'] and ('master_%s' % name) or '';
slaveCss python: widget['master'] and ('slave_%s_%s' % (widget['masterName'], '_'.join(widget['masterValue']))) or ''; slaveCss python: widget['master'] and ('slave_%s_%s' % (widget['masterName'], '_'.join(widget['masterValue']))) or '';
slaveId python: widget['master'] and 'slave' or ''"> tagCss tagCss | slaveCss;
tagId python: widget['master'] and 'slave' or ''">
<metal:layout use-macro="here/skyn/widgets/show/macros/layout"/> <metal:layout use-macro="here/skyn/widgets/show/macros/layout"/>
</metal:field> </metal:field>
@ -77,10 +82,10 @@
widget The widget to render widget The widget to render
</tal:comment> </tal:comment>
<metal:group define-macro="group" <metal:group define-macro="group"
tal:define="slaveCss python: widget['master'] and ('slave_%s_%s' % (widget['masterName'], '_'.join(widget['masterValue']))) or ''; tal:define="tagCss python: widget['master'] and ('slave_%s_%s' % (widget['masterName'], '_'.join(widget['masterValue']))) or '';
widgetCss widget/css_class; widgetCss widget/css_class;
groupCss python: slaveCss and ('%s %s' % (slaveCss, widgetCss)) or widgetCss; groupCss python: tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
slaveId python: widget['master'] and 'slave' or ''"> tagId python: widget['master'] and 'slave' or ''">
<fieldset tal:condition="python: widget['style'] == 'fieldset'"> <fieldset tal:condition="python: widget['style'] == 'fieldset'">
<legend tal:condition="widget/hasLabel"> <legend tal:condition="widget/hasLabel">
<i tal:content="structure python: contextObj.translate(widget['labelId'])"></i> <i tal:content="structure python: contextObj.translate(widget['labelId'])"></i>
@ -98,7 +103,7 @@
<tal:asTabs condition="python: widget['style'] == 'tabs'"> <tal:asTabs condition="python: widget['style'] == 'tabs'">
<table tal:attributes="width widget/wide; <table tal:attributes="width widget/wide;
class groupCss; class groupCss;
id slaveId; name slaveId"> id tagId; name tagId">
<tal:comment replace="nothing">First row: the tabs.</tal:comment> <tal:comment replace="nothing">First row: the tabs.</tal:comment>
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040"> <tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table style="position:relative; bottom:-1px;"> <table style="position:relative; bottom:-1px;">
@ -150,7 +155,7 @@
class groupCss; class groupCss;
cellspacing widget/cellspacing; cellspacing widget/cellspacing;
cellpadding widget/cellpadding; cellpadding widget/cellpadding;
id slaveId; name slaveId"> id tagId; name tagId">
<tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment> <tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment>
<tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']"> <tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']">
<td tal:attributes="colspan python: len(widget['columnsWidths']); <td tal:attributes="colspan python: len(widget['columnsWidths']);

View file

@ -8,10 +8,10 @@ from appy.gen.plone25.mixins.TestMixin import TestMixin, beforeTest, afterTest
<!imports!> <!imports!>
# Initialize Zope & Plone test systems ----------------------------------------- # Initialize Zope & Plone test systems -----------------------------------------
ZopeTestCase.installProduct('<!applicationName!>')
ZopeTestCase.installProduct('PloneLanguageTool') ZopeTestCase.installProduct('PloneLanguageTool')
PloneTestCase.setupPloneSite(products=['<!applicationName!>', ZopeTestCase.installProduct('<!applicationName!>')
'PloneLanguageTool']) PloneTestCase.setupPloneSite(products=['PloneLanguageTool',
'<!applicationName!>'])
class Test(PloneTestCase.PloneTestCase, TestMixin): class Test(PloneTestCase.PloneTestCase, TestMixin):
'''Base test class for <!applicationName!> test cases.''' '''Base test class for <!applicationName!> test cases.'''

View file

@ -189,6 +189,7 @@ class StylesManager:
self.textStyles = self.styles.getStyles('text') self.textStyles = self.styles.getStyles('text')
# List of paragraph styles derived from self.styles # List of paragraph styles derived from self.styles
self.paragraphStyles = self.styles.getStyles('paragraph') self.paragraphStyles = self.styles.getStyles('paragraph')
def checkStylesAdequation(self, htmlStyle, odtStyle): def checkStylesAdequation(self, htmlStyle, odtStyle):
'''Checks that p_odtStyle my be used for style p_htmlStyle.''' '''Checks that p_odtStyle my be used for style p_htmlStyle.'''
if (htmlStyle in XHTML_PARAGRAPH_TAGS_NO_LISTS) and \ if (htmlStyle in XHTML_PARAGRAPH_TAGS_NO_LISTS) and \
@ -199,6 +200,7 @@ class StylesManager:
(odtStyle in self.paragraphStyles): (odtStyle in self.paragraphStyles):
raise PodError(HTML_TEXT_ODT_PARA % ( raise PodError(HTML_TEXT_ODT_PARA % (
htmlStyle, odtStyle.displayName)) htmlStyle, odtStyle.displayName))
def checkStylesMapping(self, stylesMapping): def checkStylesMapping(self, stylesMapping):
'''Checks that the given p_stylesMapping is correct. Returns the same '''Checks that the given p_stylesMapping is correct. Returns the same
dict as p_stylesMapping, but with Style instances as values, instead dict as p_stylesMapping, but with Style instances as values, instead

View file

@ -148,11 +148,15 @@ class HtmlDiff:
if action == 'equal': if action == 'equal':
toAdd = sep.join(a[i1:i2]) toAdd = sep.join(a[i1:i2])
elif action == 'insert': elif action == 'insert':
print 'INSERT', b[j1:j2]
toAdd = self.getModifiedChunk(b[j1:j2], action, sep) toAdd = self.getModifiedChunk(b[j1:j2], action, sep)
elif action == 'delete': elif action == 'delete':
print 'DELETE', a[i1:i2]
toAdd = self.getModifiedChunk(a[i1:i2], action, sep) toAdd = self.getModifiedChunk(a[i1:i2], action, sep)
elif action == 'replace': elif action == 'replace':
if sep == '\n': if sep == '\n':
print 'REPLACE', a[i1:i2]
print 'WITH', b[j1:j2]
# We know that some lines have been replaced from a to b. By # We know that some lines have been replaced from a to b. By
# identifying similarities between those lines, consider # identifying similarities between those lines, consider
# some as having been deleted, modified or inserted. # some as having been deleted, modified or inserted.
@ -184,5 +188,6 @@ class HtmlDiff:
def get(self): def get(self):
'''Produces the result.''' '''Produces the result.'''
print 'RUN'
return self.getHtmlDiff(self.old, self.new, '\n') return self.getHtmlDiff(self.old, self.new, '\n')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------