diff --git a/gen/__init__.py b/gen/__init__.py
index 7916e72..b57e552 100644
--- a/gen/__init__.py
+++ b/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, GroupDescr, Keywords, FileWrapper, \
- getClassName, SomeObjects
+ getClassName, SomeObjects, AppyObject
import appy.pod
from appy.pod.renderer import Renderer
from appy.shared.data import countries
@@ -517,6 +517,11 @@ class Type:
# We must initialise the corresponding back reference
self.back.klass = klass
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):
'''In debug mode, we want to reload layouts without restarting Zope.
@@ -1534,7 +1539,6 @@ class File(Type):
return value._atFile
def getRequestValue(self, request):
- res = request.get('%s_file' % self.name)
return request.get('%s_file' % self.name)
def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'}
@@ -1784,9 +1788,6 @@ class Ref(Type):
def getFormattedValue(self, obj, value):
return value
- def getRequestValue(self, request):
- return request.get('appy_ref_%s' % self.name, None)
-
def validateValue(self, obj, value):
if not self.link: return None
# We only check "link" Refs because in edit views, "add" Refs are
@@ -2210,6 +2211,94 @@ class Pod(Type):
value = value._atFile
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 --------------------------------
appyToZopePermissions = {
'read': ('View', 'Access contents information'),
diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py
index 63945d5..6b14ad7 100644
--- a/gen/plone25/descriptors.py
+++ b/gen/plone25/descriptors.py
@@ -120,6 +120,14 @@ class FieldDescriptor:
# Add the POD-related fields on the Tool
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):
'''Walks into the Appy type definition and gathers data about the
i18n labels.'''
@@ -176,6 +184,8 @@ class FieldDescriptor:
elif self.appyType.type == 'Ref': self.walkRef()
# Manage things which are specific to Pod types
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):
'''Generates the i18n labels for this type.'''
diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py
index a7f789b..1d9fdbc 100644
--- a/gen/plone25/mixins/__init__.py
+++ b/gen/plone25/mixins/__init__.py
@@ -142,6 +142,15 @@ class BaseMixin:
setattr(errors, appyType.name, message)
else:
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):
'''This method is called when individual validation of all fields
@@ -432,13 +441,32 @@ class BaseMixin:
retrieved in synchronous mode.'''
appyType = self.getAppyType(name)
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):
'''Gets a nice, string representation of p_value which is a value from
field named p_name.'''
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):
'''Returns filename and size of p_fileObject.'''
if not fileObject: return {'filename': '', 'size': 0}
@@ -533,11 +561,15 @@ class BaseMixin:
def getAppyType(self, name, asDict=False, className=None):
'''Returns the Appy type named p_name. If no p_className is defined, the
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:
klass = self.__class__.wrapperClass
else:
klass = self.getTool().getAppyClass(className, wrapper=True)
res = getattr(klass, name, None)
+ if res and isInnerType: res = res.getField(subName)
if res and asDict: return res.__dict__
return res
diff --git a/gen/plone25/skin/appy.css b/gen/plone25/skin/appy.css
index 413d905..39007ad 100644
--- a/gen/plone25/skin/appy.css
+++ b/gen/plone25/skin/appy.css
@@ -65,6 +65,9 @@ img {border: 0;}
.list td, .list th { border: 1px solid grey;
padding-left: 5px; padding-right: 5px; padding-top: 3px;}
.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 td { border:0 !important; padding:0 !important; margin:0 !important; }
.translationLabel { background-color: #EAEAEA; border-bottom: 1px dashed grey;
diff --git a/gen/plone25/skin/appy.js b/gen/plone25/skin/appy.js
index ff0c6d7..f744ea9 100644
--- a/gen/plone25/skin/appy.js
+++ b/gen/plone25/skin/appy.js
@@ -492,3 +492,59 @@ function initTab(cookieId, defaultValue) {
if (!toSelect) { showTab(defaultValue) }
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);
+}
diff --git a/gen/plone25/skin/widgets/list.pt b/gen/plone25/skin/widgets/list.pt
new file mode 100644
index 0000000..3b167ba
--- /dev/null
+++ b/gen/plone25/skin/widgets/list.pt
@@ -0,0 +1,72 @@
+Single row.
+
+
+
+
+
+
+ Icon for removing the row
+
+
+
+
+
+The whole table, edit or view.
+
+ Header
+
+
+
+ Icon for adding a new row.
+
+
+
+
+ Template row (edit only)
+
+
+
+
+ Rows of data
+
+
+
+
+
+
+
+View
+
+
+
+
+Edit
+
+
+ The following input makes Appy aware that this field is in the request.
+
+
+
+
+
+Cell
+
+
+
+
+Search
+
diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt
index 02c205d..caf0c31 100644
--- a/gen/plone25/skin/widgets/ref.pt
+++ b/gen/plone25/skin/widgets/ref.pt
@@ -28,13 +28,13 @@
Move upMove down
@@ -69,7 +69,7 @@
noFormCall python: navBaseCall.replace('**v**', '%d, \'CreateWithoutForm\'' % startNumber);
noFormCall python: test(appyType['addConfirm'], 'askConfirm(\'script\', "%s", "%s")' % (noFormCall, addConfirmMsg), noFormCall)"
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)"/>
@@ -108,6 +108,7 @@
ajaxHookId python: contextObj.UID()+fieldName;
startNumber python: int(request.get('%s_startNumber' % ajaxHookId, 0));
tool contextObj/getTool;
+ _ python: tool.translate;
refObjects python:contextObj.getAppyRefs(fieldName, startNumber);
objs refObjects/objects;
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;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1);
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)">
This macro displays the Reference widget on a "view" page.
@@ -139,7 +140,7 @@
If there is no object...
-
+
@@ -162,7 +163,7 @@
The search icon if field is queryable
-
+
Object description
@@ -173,7 +174,7 @@
No object is present
-
+
@@ -185,10 +186,10 @@
-
+
-
+
+ content="python: _(obj.getWorkflowLabel())">
Edit macro for an Ref.
-