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 up Move 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. -
- +
+ id tagId; + name tagId;"> The table header row
- + tagCss tagCss | slaveCss; + tagId python: widget['master'] and 'slave' or ''"> @@ -77,10 +82,10 @@ widget The widget to render + groupCss python: tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss; + tagId python: widget['master'] and 'slave' or ''">
@@ -98,7 +103,7 @@ + id tagId; name tagId"> First row: the tabs.
@@ -150,7 +155,7 @@ class groupCss; cellspacing widget/cellspacing; cellpadding widget/cellpadding; - id slaveId; name slaveId"> + id tagId; name tagId"> Display the title of the group if it is not rendered a fieldset.