From 3ab6cec7d6e37dbf1c42a275a028f9a5332cadba Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 26 Oct 2011 10:21:09 +0200 Subject: [PATCH] appy.gen: generalized use of appy.Object; existence of a field value (that triggers search for a default value) is now based on Type.isEmptyValue, and not '==None'; bugfix with default values for List fields; prevent search icon to be shown for a Ref when there is no linked object; appy.pod: added class appy.pod.parts.OdtTable that allows to create a complex (ie, with a dynamic number of columns) table programmatically (to be imported with a statement 'do ... from'); appy.shared.diff: improvements in the multiple XHTML diff; appy.shared.xml_parser.XmlMarshaller: support for default namespaces and 'any' tags. --- gen/__init__.py | 28 ++++------- gen/plone25/mixins/__init__.py | 11 +++-- gen/plone25/skin/widgets/ref.pt | 4 +- gen/plone25/skin/widgets/show.pt | 3 +- gen/utils.py | 3 -- pod/parts.py | 64 ++++++++++++++++++++++++ shared/__init__.py | 3 -- shared/diff.py | 83 +++++++++++++++++++------------- shared/xml_parser.py | 16 ++++-- 9 files changed, 147 insertions(+), 68 deletions(-) create mode 100644 pod/parts.py diff --git a/gen/__init__.py b/gen/__init__.py index b57e552..02b954c 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO +from appy import Object 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, AppyObject + getClassName, SomeObjects import appy.pod from appy.pod.renderer import Renderer from appy.shared.data import countries @@ -16,7 +17,7 @@ r, w, d = ('read', 'write', 'delete') digit = re.compile('[0-9]') alpha = re.compile('[a-zA-Z0-9]') letter = re.compile('[a-zA-Z]') -nullValues = (None, '', ' ') +nullValues = (None, '', []) validatorTypes = (types.FunctionType, types.UnboundMethodType, type(re.compile(''))) emptyTuple = () @@ -729,7 +730,7 @@ class Type: def getValue(self, obj): '''Gets, on_obj, the value conforming to self's type definition.''' value = getattr(obj.aq_base, self.name, None) - if (value == None): + if self.isEmptyValue(value): # If there is no value, get the default value if any if not self.editDefault: # Return self.default, of self.default() if it is a method @@ -2243,10 +2244,6 @@ class List(Type): 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] + '*' @@ -2254,7 +2251,7 @@ class List(Type): for key in request.keys(): if not key.startswith(prefix): continue # I have found a row. Gets its index - row = AppyObject() + row = Object() rowIndex = int(key.split('*')[-1]) if rowIndex == -1: continue # Ignore the template row. for name, field in self.fields: @@ -2271,10 +2268,6 @@ class List(Type): 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 @@ -2290,14 +2283,13 @@ class List(Type): setattr(v, name, field.getStorableValue(getattr(v, name))) return value - def getInnerValue(self, obj, name, i): + def getInnerValue(self, outerValue, 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.''' + within the whole list of values p_outerValue.''' if i == -1: return '' - value = getattr(obj, self.name, None) - if not value: return '' - if i >= len(value): return '' - return getattr(value[i], name, '') + if not outerValue: return '' + if i >= len(outerValue): return '' + return getattr(outerValue[i], name, '') # Workflow-specific types and default workflows -------------------------------- appyToZopePermissions = { diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 1d9fdbc..f653043 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -4,6 +4,7 @@ # ------------------------------------------------------------------------------ import os, os.path, sys, types, mimetypes, urllib, cgi +from appy import Object import appy.gen from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \ Transition, Permission @@ -206,9 +207,9 @@ class BaseMixin: return self.goto(urlBack) # Object for storing validation errors - errors = AppyObject() + errors = Object() # Object for storing the (converted) values from the request - values = AppyObject() + values = Object() # Trigger field-specific validation self.intraFieldValidation(errors, values) @@ -435,7 +436,8 @@ class BaseMixin: # broken on returned object. return getattr(self, methodName, None) - def getFieldValue(self, name, onlyIfSync=False, layoutType=None): + def getFieldValue(self, name, onlyIfSync=False, layoutType=None, + outerValue=None): '''Returns the database value of field named p_name for p_self. If p_onlyIfSync is True, it returns the value only if appyType can be retrieved in synchronous mode.''' @@ -445,7 +447,8 @@ class BaseMixin: 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)) + listType = self.getAppyType(listName) + return listType.getInnerValue(outerValue, name, int(i)) def getFormattedFieldValue(self, name, value): '''Gets a nice, string representation of p_value which is a value from diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index caf0c31..93e72ae 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -145,7 +145,7 @@ If there is an object... - + @@ -161,7 +161,7 @@ () The search icon if field is queryable - diff --git a/gen/plone25/skin/widgets/show.pt b/gen/plone25/skin/widgets/show.pt index 93157df..971a7a0 100644 --- a/gen/plone25/skin/widgets/show.pt +++ b/gen/plone25/skin/widgets/show.pt @@ -59,7 +59,8 @@ layout python: widget['layouts'][layoutType]; name widgetName| widget/name; sync python: widget['sync'][layoutType]; - rawValue python: contextObj.getFieldValue(name, onlyIfSync=True, layoutType=layoutType); + outerValue value|nothing; + rawValue python: contextObj.getFieldValue(name,onlyIfSync=True,layoutType=layoutType,outerValue=outerValue); value python: contextObj.getFormattedFieldValue(name, rawValue); requestValue python: contextObj.getRequestFieldValue(name); inRequest python: request.has_key(name); diff --git a/gen/utils.py b/gen/utils.py index bb5e413..42d18ad 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -158,9 +158,6 @@ def produceNiceMessage(msg): res += c return res -# ------------------------------------------------------------------------------ -class AppyObject: pass - # ------------------------------------------------------------------------------ class SomeObjects: '''Represents a bunch of objects retrieved from a reference or a query in diff --git a/pod/parts.py b/pod/parts.py new file mode 100644 index 0000000..e3349b5 --- /dev/null +++ b/pod/parts.py @@ -0,0 +1,64 @@ +# ------------------------------------------------------------------------------ +class OdtTable: + '''This class allows to construct an ODT table programmatically.''' + # Some namespace definitions + tns = 'table:' + txns = 'text:' + + def __init__(self, name, paraStyle, cellStyle, nbOfCols, + paraHeaderStyle=None, cellHeaderStyle=None): + # An ODT table must have a name. + self.name = name + # The default style of every paragraph within cells + self.paraStyle = paraStyle + # The default style of every cell + self.cellStyle = cellStyle + # The total number of columns + self.nbOfCols = nbOfCols + # The default style of every paragraph within a header cell + self.paraHeaderStyle = paraHeaderStyle or paraStyle + # The default style of every header cell + self.cellHeaderStyle = cellHeaderStyle or cellStyle + # The buffer where the resulting table will be rendered + self.res = '' + + def dumpCell(self, content, span=1, header=False, + paraStyle=None, cellStyle=None): + '''Dumps a cell in the table. If no specific p_paraStyle (p_cellStyle) + is given, self.paraStyle (self.cellStyle) is used, excepted if + p_header is True: in that case, self.paraHeaderStyle + (self.cellHeaderStyle) is used.''' + if not paraStyle: + if header: paraStyle = self.paraHeaderStyle + else: paraStyle = self.paraStyle + if not cellStyle: + if header: cellStyle = self.cellHeaderStyle + else: cellStyle = self.cellStyle + self.res += '<%stable-cell %sstyle-name="%s" ' \ + '%snumber-columns-spanned="%d">' % \ + (self.tns, self.tns, cellStyle, self.tns, span) + self.res += '<%sp %sstyle-name="%s">%s' % \ + (self.txns, self.txns, paraStyle, content, self.txns) + self.res += '' % self.tns + + def startRow(self): + self.res += '<%stable-row>' % self.tns + + def endRow(self): + self.res += '' % self.tns + + def startTable(self): + self.res += '<%stable %sname="%s">' % (self.tns, self.tns, self.name) + self.res += '<%stable-column %snumber-columns-repeated="%d"/>' % \ + (self.tns, self.tns, self.nbOfCols) + + def endTable(self): + self.res += '' % self.tns + + def dumpFloat(self, number): + return str(round(number, 2)) + + def get(self): + '''Returns the whole table.''' + return self.res.decode('utf-8') +# ------------------------------------------------------------------------------ diff --git a/shared/__init__.py b/shared/__init__.py index 9a3dff2..8997905 100644 --- a/shared/__init__.py +++ b/shared/__init__.py @@ -44,7 +44,4 @@ class UnicodeBuffer: self.buffer.append(unicode(s)) def getValue(self): return u''.join(self.buffer) - -# ------------------------------------------------------------------------------ -class Dummy: pass # ------------------------------------------------------------------------------ diff --git a/shared/diff.py b/shared/diff.py index 02e70ea..baf8ce6 100644 --- a/shared/diff.py +++ b/shared/diff.py @@ -212,26 +212,25 @@ class HtmlDiff: ratio = difflib.SequenceMatcher(a=s1.lower(), b=s2.lower()).ratio() return ratio > self.diffRatio - def isEmpty(self, l): - '''Is list p_l empty ?''' - return not l or ( (len(l) == 1) and (l[0] in ('', '\r'))) - - def getTagContent(self, line): - '''p_lines is a XHTML tag with content. This method returns the content - of the tag, removing start and end tags.''' - return line[line.find('>')+1:line.rfind('<')] + def splitTagAndContent(self, line): + '''p_line is a XHTML tag with content. This method returns a tuple + (startTag, content), where p_startTag is the isolated start tag and + content is the tag content.''' + i = line.find('>')+1 + return line[0:i], line[i:line.rfind('<')] def getLineAndType(self, line): '''p_line is a string that can already have been surrounded by an "insert" or "delete" tag. This is what we try to determine here. - This method returns a tuple (type, line, innerDiffs), where "type" - can be: + This method returns a tuple (type, line, innerDiffs, outerTag), + where "type" can be: * "insert" if it has already been flagged as inserted; * "delete" if it has already been flagged as deleted; * None else; "line" holds the original parameter p_line, excepted: * if type="insert". In that case, the surrounding insert tag has been - removed; + removed and placed into "outerTag" (the outer start tag to be more + precise); * if inner diff tags (insert or delete) are found. In that case, - if inner "insert" tags are found, they are removed but their content is kept; @@ -240,10 +239,15 @@ class HtmlDiff: - "innerDiffs" holds the list of re.MatchObjects instances representing the found inner tags. ''' - if line.startswith(self.divDeletePrefix): return ('delete', line, None) + if line.startswith(self.divDeletePrefix): + return ('delete', line, None, None) if line.startswith(self.divInsertPrefix): # Return the line without the surrounding tag. - return ('insert', self.getTagContent(line), None) + action = 'insert' + outerTag, line = self.splitTagAndContent(line) + else: + action = None + outerTag = None # Replace found inner inserts with their content. innerDiffs = [] while True: @@ -256,7 +260,7 @@ class HtmlDiff: if match.group(1) == 'insert': content = match.group(2) line = line[:match.start()] + content + line[match.end():] - return (None, line, innerDiffs) + return (action, line, innerDiffs, outerTag) def getSeqDiff(self, seqA, seqB): '''p_seqA and p_seqB are lists of strings. Here we will try to identify @@ -278,7 +282,7 @@ class HtmlDiff: # Scan every string from p_seqA and try to find a similar string in # p_seqB. while i < len(seqA): - pastAction, lineSeqA, innerDiffs = self.getLineAndType(seqA[i]) + pastAction, lineA, innerDiffs, outerTag=self.getLineAndType(seqA[i]) if pastAction == 'delete': # We will consider this line as "equal" because it already has # been noted as deleted in a previous diff. @@ -293,10 +297,10 @@ class HtmlDiff: # be found in seqB. res.append( ('equal', seqA[i]) ) else: - # Try to find a line in seqB which is similar to lineSeqA. + # Try to find a line in seqB which is similar to lineA. similarFound = False for j in range(k, len(seqB)): - if self.isSimilar(lineSeqA, seqB[j]): + if self.isSimilar(lineA, seqB[j]): similarFound = True # Strings between indices k and j in p_seqB must be # considered as inserted, because no similar line exists @@ -304,16 +308,15 @@ class HtmlDiff: if k < j: for line in seqB[k:j]: res.append(('insert', line)) # Similar strings are appended in a 'replace' entry, - # excepted if lineSeqA is already an insert from a + # excepted if lineA is already an insert from a # previous diff: in this case, we keep the "old" # version: the new one is the same, but for which we # don't remember who updated it. - if (pastAction == 'insert') and (lineSeqA == seqB[j]): + if (pastAction == 'insert') and (lineA == seqB[j]): res.append( ('equal', seqA[i]) ) - # TODO: manage lineSeqA != seqB[j] else: - res.append(('replace', (lineSeqA, seqB[j], - innerDiffs))) + res.append(('replace', (lineA, seqB[j], + innerDiffs, outerTag))) k = j+1 break if not similarFound: res.append( ('delete', seqA[i]) ) @@ -337,6 +340,16 @@ class HtmlDiff: if trailSpace: res[-1] = res[-1] + sep return res + garbage = ('', '\r') + def removeGarbage(self, l): + '''Removes from list p_l elements that have no interest, like blank + strings or considered as is.''' + i = len(l)-1 + while i >= 0: + if l[i] in self.garbage: del l[i] + i -= 1 + return l + def getHtmlDiff(self, old, new, sep): '''Returns the differences between p_old and p_new. Result is a string containing the comparison in HTML format. p_sep is used for turning @@ -352,29 +365,27 @@ class HtmlDiff: matcher = difflib.SequenceMatcher() matcher.set_seqs(a,b) for action, i1, i2, j1, j2 in matcher.get_opcodes(): - chunkA = a[i1:i2] - chunkB = b[j1:j2] - aIsEmpty = self.isEmpty(chunkA) - bIsEmpty = self.isEmpty(chunkB) + chunkA = self.removeGarbage(a[i1:i2]) + chunkB = self.removeGarbage(b[j1:j2]) toAdd = None if action == 'equal': - if not aIsEmpty: toAdd = sep.join(chunkA) + if chunkA: toAdd = sep.join(chunkA) elif action == 'insert': - if not bIsEmpty: + if chunkB: toAdd = self.getModifiedChunk(chunkB, action, sep) elif action == 'delete': - if not aIsEmpty: + if chunkA: toAdd = self.getModifiedChunk(chunkA, action, sep) elif action == 'replace': - if aIsEmpty and bIsEmpty: + if not chunkA and not chunkB: toAdd = '' - elif aIsEmpty: + elif not chunkA: # Was an addition, not a replacement toAdd = self.getModifiedChunk(chunkB, 'insert', sep) - elif bIsEmpty: + elif not chunkB: # Was a deletion, not a replacement toAdd = self.getModifiedChunk(chunkA, 'delete', sep) - else: # At least, a true replacement (grr difflib) + else: # At least, a true replacement if sep == '\n': # We know that some lines have been replaced from a to # b. By identifying similarities between those lines, @@ -387,7 +398,7 @@ class HtmlDiff: elif sAction == 'equal': toAdd += line elif sAction == 'replace': - lineA, lineB, previousDiffsA = line + lineA, lineB, previousDiffsA, outerTag = line # Investigate further here and explore # differences at the *word* level between lineA # and lineB. As a preamble, and in order to @@ -405,6 +416,10 @@ class HtmlDiff: if previousDiffsA: merger= Merger(lineA, toAdd, previousDiffsA) toAdd = merger.merge() + # Rewrap line into outerTag if lineA was a line + # tagged as previously inserted. + if outerTag: + toAdd = outerTag + toAdd + '' else: toAdd = self.getModifiedChunk(chunkA, 'delete', sep) toAdd += self.getModifiedChunk(chunkB, 'insert', sep) diff --git a/shared/xml_parser.py b/shared/xml_parser.py index 8563349..4f3f327 100644 --- a/shared/xml_parser.py +++ b/shared/xml_parser.py @@ -448,7 +448,8 @@ class XmlMarshaller: self.rootElementName = rootTag # The namespaces that will be defined at the root of the XML message. # It is a dict whose keys are namespace prefixes and whose values are - # namespace URLs. + # namespace URLs. If you want to specify a default namespace, specify an + # entry with an empty string as a key. self.namespaces = namespaces # The following dict will tell which XML tags will get which namespace # prefix ({s_tagName: s_prefix}). Special optional dict entry @@ -475,8 +476,12 @@ class XmlMarshaller: res.write('<'); res.write(tagName) # Dumps namespace definitions if any for prefix, url in self.namespaces.iteritems(): - res.write(' xmlns:%s="%s"' % (prefix, url)) - # Dumps Appy- or Plone-specific attributed + if not prefix: + pre = 'xmlns' # The default namespace + else: + pre = 'xmlns:%s' % prefix + res.write(' %s="%s"' % (pre, url)) + # Dumps Appy- or Plone-specific attribute if self.objectType != 'popo': res.write(' type="object" id="%s"' % instance.UID()) res.write('>') @@ -572,6 +577,11 @@ class XmlMarshaller: def dumpField(self, res, fieldName, fieldValue, fieldType='basic'): '''Dumps in p_res, the value of the p_field for p_instance.''' + # As a preamble, manage special case of p_fieldName == "_any". In that + # case, p_fieldValue corresponds to a previously marshalled string that + # must be included as is here, without dumping the tag name. + if fieldName == '_any': self.dumpValue(res, fieldValue, None) + # Now, dump "normal" fields. fieldTag = self.getTagName(fieldName) res.write('<'); res.write(fieldTag) # Dump the type of the field as an XML attribute