From 8e1760842e79b3a58646ecb18552b7772ffc7c4c Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 10 Nov 2011 21:59:02 +0100 Subject: [PATCH] appy.gen: Type 'float': added the possibility to define a separator for thousands; bugfixes in master/slave relationships; permission-related bugfix while creating objects through AbstractWrapper.create; appy.shared.diff: more improvements (still ongoing work). --- gen/__init__.py | 13 ++-- gen/plone25/installer.py | 1 - gen/plone25/skin/appy.js | 29 +++++---- gen/plone25/skin/widgets/boolean.pt | 5 +- gen/plone25/skin/widgets/float.pt | 5 +- gen/plone25/skin/widgets/integer.pt | 5 +- gen/plone25/skin/widgets/ref.pt | 1 - gen/plone25/skin/widgets/string.pt | 5 +- gen/plone25/wrappers/__init__.py | 9 +++ pod/renderer.py | 2 + shared/diff.py | 98 ++++++++++++++++++++++------- shared/utils.py | 21 +++++-- 12 files changed, 139 insertions(+), 55 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index 79ee0ef..487c5ed 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -25,7 +25,7 @@ labelTypes = ('label', 'descr', 'help') def initMasterValue(v): '''Standardizes p_v as a list of strings.''' - if not v: res = [] + if not isinstance(v, bool) and not v: res = [] elif type(v) not in sequenceTypes: res = [v] else: res = v return [str(v) for v in res] @@ -959,6 +959,7 @@ class Integer(Type): class Float(Type): allowedDecimalSeps = (',', '.') + allowedThousandsSeps = (' ', '') def __init__(self, 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, @@ -966,7 +967,7 @@ class Float(Type): specificWritePermission=False, width=6, height=None, maxChars=13, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, - precision=None, sep=(',', '.')): + precision=None, sep=(',', '.'), tsep=' '): # The precision is the number of decimal digits. This number is used # for rendering the float, but the internal float representation is not # rounded. @@ -981,6 +982,7 @@ class Float(Type): for sep in self.sep: if sep not in Float.allowedDecimalSeps: raise 'Char "%s" is not allowed as decimal separator.' % sep + self.tsep = tsep Type.__init__(self, validator, multiplicity, index, default, optional, editDefault, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, @@ -989,11 +991,13 @@ class Float(Type): self.pythonType = float def getFormattedValue(self, obj, value): - return formatNumber(value, sep=self.sep[0], precision=self.precision) + return formatNumber(value, sep=self.sep[0], precision=self.precision, + tsep=self.tsep) def validateValue(self, obj, value): # Replace used separator with the Python separator '.' for sep in self.sep: value = value.replace(sep, '.') + value = value.replace(self.tsep, '') try: value = self.pythonType(value) except ValueError: @@ -1002,6 +1006,7 @@ class Float(Type): def getStorableValue(self, value): if not self.isEmptyValue(value): for sep in self.sep: value = value.replace(sep, '.') + value = value.replace(self.tsep, '') return self.pythonType(value) class String(Type): @@ -1752,7 +1757,7 @@ class Ref(Type): if type == 'uids': ref = uids[i] else: - ref = obj.portal_catalog(UID=uids[i])[0].getObject() + ref = obj.uid_catalog(UID=uids[i])[0].getObject() if type == 'objects': ref = ref.appy() res.objects.append(ref) diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index 70bdbac..a7f2265 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -452,7 +452,6 @@ class ZopeInstaller: self.config.listTypes(self.productName) contentTypes, constructors, ftis = self.config.process_types( self.config.listTypes(self.productName), self.productName) - self.config.cmfutils.ContentInit(self.productName + ' Content', content_types = contentTypes, permission = self.defaultAddContentPermission, diff --git a/gen/plone25/skin/appy.js b/gen/plone25/skin/appy.js index f744ea9..619e126 100644 --- a/gen/plone25/skin/appy.js +++ b/gen/plone25/skin/appy.js @@ -217,8 +217,8 @@ function getSlaveInfo(slave, infoType) { function getMasterValues(master) { // Returns the list of values that p_master currently has. - if (master.tagName == 'SPAN') { - res = master.attributes['value'].value; + if ((master.tagName == 'INPUT') && (master.type != 'checkbox')) { + res = master.value; if ((res[0] == '(') || (res[0] == '[')) { // There are multiple values, split it values = res.substring(1, res.length-1).split(','); @@ -247,17 +247,17 @@ function getMasterValues(master) { function getSlaves(master) { // Gets all the slaves of master. allSlaves = document.getElementsByName('slave'); - res = []; + res = []; masterName = master.attributes['name'].value; if (master.type == 'checkbox') { masterName = masterName.substr(0, masterName.length-8); } slavePrefix = 'slave_' + masterName + '_'; - for (var i=0; i < slaves.length; i++){ - cssClasses = slaves[i].className.split(' '); + for (var i=0; i < allSlaves.length; i++){ + cssClasses = allSlaves[i].className.split(' '); for (var j=0; j < cssClasses.length; j++) { if (cssClasses[j].indexOf(slavePrefix) == 0) { - res.push(slaves[i]); + res.push(allSlaves[i]); break; } } @@ -265,9 +265,13 @@ function getSlaves(master) { return res; } -function updateSlaves(master) { +function updateSlaves(master, slave) { // Given the value(s) in a master field, we must update slave's visibility. - slaves = getSlaves(master); + // If p_slave is given, it updates only this slave. Else, it updates all + // slaves of p_master. + var slaves = null; + if (slave) { slaves = [slave]; } + else { slaves = getSlaves(master); } masterValues = getMasterValues(master); for (var i=0; i < slaves.length; i++) { showSlave = false; @@ -286,13 +290,12 @@ function initSlaves() { // When the current page is loaded, we must set the correct state for all // slave fields. slaves = document.getElementsByName('slave'); - walkedMasters = {}; // Remember the already walked masters. - for (var i=0; i < slaves.length; i++) { + i = slaves.length -1; + while (i >= 0) { masterName = getSlaveInfo(slaves[i], 'masterName'); - if (masterName in walkedMasters) continue; master = document.getElementById(masterName); - updateSlaves(master); - walkedMasters[masterName] = 'walked'; + updateSlaves(master, slaves[i]); + i -= 1; } } diff --git a/gen/plone25/skin/widgets/boolean.pt b/gen/plone25/skin/widgets/boolean.pt index 348ce11..3a1173a 100644 --- a/gen/plone25/skin/widgets/boolean.pt +++ b/gen/plone25/skin/widgets/boolean.pt @@ -1,7 +1,8 @@ View macro for a Boolean. - + + Edit macro for an Boolean. diff --git a/gen/plone25/skin/widgets/float.pt b/gen/plone25/skin/widgets/float.pt index 01710b5..8dc9f14 100644 --- a/gen/plone25/skin/widgets/float.pt +++ b/gen/plone25/skin/widgets/float.pt @@ -1,7 +1,8 @@ View macro for a Float. - + + Edit macro for an Float. diff --git a/gen/plone25/skin/widgets/integer.pt b/gen/plone25/skin/widgets/integer.pt index 904d575..9239335 100644 --- a/gen/plone25/skin/widgets/integer.pt +++ b/gen/plone25/skin/widgets/integer.pt @@ -1,7 +1,8 @@ View macro for an Integer. - + + Edit macro for an Integer. diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index 93e72ae..86cb465 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -130,7 +130,6 @@ The definition of "atMostOneRef" above may sound strange: we shouldn't check the actual number of referenced objects. But for back references people often forget to specify multiplicities. So concretely, multiplicities (0,None) are coded as (0,1). - Display a simplified widget if maximum number of referenced objects is 1. diff --git a/gen/plone25/skin/widgets/string.pt b/gen/plone25/skin/widgets/string.pt index 929f52f..b23ab15 100644 --- a/gen/plone25/skin/widgets/string.pt +++ b/gen/plone25/skin/widgets/string.pt @@ -1,8 +1,7 @@ View macro for a String. - +
@@ -16,6 +15,8 @@ tal:replace="structure python: contextObj.formatText(value, format='html')"/> +
Edit macro for a String. diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 4794d71..2d0c11b 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -148,6 +148,14 @@ class AbstractWrapper(object): else: folder = self.o.getParentNode() # Create the object + # -------------------- Try to replace invokeFactory -------------------- + #folder._objects = folder._objects + ({'id':id,'meta_type':portalType},) + #folder._setOb(id, ob) + #ploneObj = self._getOb(id) + #ob._setPortalTypeName(self.getId()) + #ob.notifyWorkflowCreated() + # + Check what's done in Archetypes/ClassGen.py in m_genCtor + # ------------------------------ Try end ------------------------------- folder.invokeFactory(portalType, objId) ploneObj = getattr(folder, objId) appyObj = ploneObj.appy() @@ -157,6 +165,7 @@ class AbstractWrapper(object): if isField: # Link the object to this one appyType.linkObject(self.o, ploneObj) + ploneObj._appy_managePermissions() # Call custom initialization if externalData: param = externalData else: param = True diff --git a/pod/renderer.py b/pod/renderer.py index f0ddbdb..034cf47 100644 --- a/pod/renderer.py +++ b/pod/renderer.py @@ -237,6 +237,7 @@ class Renderer: ns = self.currentParser.env.namespaces # xhtmlString can only be a chunk of XHTML. So we must surround it a # tag in order to get a XML-compliant file (we need a root tag). + if xhtmlString == None: xhtmlString = '' xhtmlContent = '

%s

' % xhtmlString return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager, stylesMapping, ns).run() @@ -244,6 +245,7 @@ class Renderer: def renderText(self, text, encoding='utf-8', stylesMapping={}): '''Method that can be used (under the name 'text') into a pod template for inserting a text containing carriage returns.''' + if text == None: text = '' text = cgi.escape(text).replace('\r\n', '
').replace('\n', '
') return self.renderXhtml(text, encoding, stylesMapping) diff --git a/shared/diff.py b/shared/diff.py index baf8ce6..b98c1cc 100644 --- a/shared/diff.py +++ b/shared/diff.py @@ -2,13 +2,14 @@ import re, difflib # ------------------------------------------------------------------------------ -innerDiff = re.compile('(.*?)') +innerDiff = re.compile('' \ + '(.*?)') # ------------------------------------------------------------------------------ class Merger: '''This class allows to merge 2 lines of text, each containing inserts and deletions.''' - def __init__(self, lineA, lineB, previousDiffs): + def __init__(self, lineA, lineB, previousDiffs, differ): # lineA comes "naked": any diff previously found on it was removed from # it (ie, deleted text has been completely removed, while inserted text # has been included, but without its surrounding tag). Info about @@ -25,6 +26,11 @@ class Merger: self.i = 0 # The delta index that must be applied on previous diffs self.deltaPrevious = 0 + # A link to the caller HtmlDiff class. + self.differ = differ + # While "consuming" diffs (see m_getNextDiff), keep here every message + # from every diff. + self.messages = [self.differ.complexMsg] def computeNewDiffs(self): '''lineB may include inner "insert" and/or tags. This function @@ -68,10 +74,31 @@ class Merger: del self.newDiffs[0] return newDiff, newDiffIndex, False + def manageOverlaps(self): + '''We have detected that changes between lineA and lineB include + overlapping inserts and deletions. Our solution: to remember names + of editors and return the whole line in a distinct colour, where + we (unfortunately) can't distinguish editors's specific updates.''' + # First, get a "naked" version of self.lineB, without the latest + # updates. + res = self.lineB + for diff in self.newDiffs: + res = self.differ.applyDiff(res, diff) + # Construct the message explaining the series of updates. + # self.messages already contains messages from the "consumed" diffs + # (see m_getNextDiff). + for type in ('previous', 'new'): + exec 'diffs = self.%sDiffs' % type + for diff in diffs: + self.messages.append(diff.group(2)) + msg = ' -=- '.join(self.messages) + return self.differ.getModifiedChunk(res, 'complex', '\n', msg=msg) + def merge(self): '''Merges self.previousDiffs into self.lineB.''' res = '' diff, diffStart, isPrevious = self.getNextDiff() + if diff: self.messages.append(diff.group(2)) while diff: # Dump the part of lineB between self.i and diffStart res += self.lineB[self.i:diffStart] @@ -80,7 +107,14 @@ class Merger: res += diff.group(0) if isPrevious: if diff.group(1) == 'insert': - self.i += len(diff.group(2)) + # Check if the inserted text is still present in lineB + if self.lineB[self.i:].startswith(diff.group(3)): + # Yes. Go ahead within lineB + self.i += len(diff.group(3)) + else: + # The inserted text can't be found as is in lineB. + # Must have been (partly) re-edited or removed. + return self.manageOverlaps() else: # Update self.i self.i += len(diff.group(0)) @@ -92,9 +126,10 @@ class Merger: # The indexes in lineA do not take the deleted text into # account, because it wasn't deleted at this time. So remove # from self.deltaPrevious the length of removed text. - self.deltaPrevious -= len(diff.group(2)) + self.deltaPrevious -= len(diff.group(3)) # Load next diff diff, diffStart, isPrevious = self.getNextDiff() + if diff: self.messages.append(diff.group(2)) # Dump the end of self.lineB if not completely consumed if self.i < len(self.lineB): res += self.lineB[self.i:] @@ -106,11 +141,14 @@ class HtmlDiff: HTML chunk.''' insertStyle = 'color: blue; cursor: help' deleteStyle = 'color: red; text-decoration: line-through; cursor: help' + complexStyle = 'color: purple; cursor: help' def __init__(self, old, new, insertMsg='Inserted text', deleteMsg='Deleted text', - insertCss=None, deleteCss=None, insertName='insert', - deleteName='delete', diffRatio=0.7): + complexMsg='Multiple inserts and/or deletions', + insertCss=None, deleteCss=None, complexCss=None, + insertName='insert', deleteName='delete', + complexName='complex', diffRatio=0.7): # p_old and p_new are strings containing chunks of HTML. self.old = old.strip() self.new = new.strip() @@ -121,15 +159,18 @@ class HtmlDiff: # (who made it and at what time, for example). self.insertMsg = insertMsg self.deleteMsg = deleteMsg + self.complexMsg = complexMsg # This tag will get a CSS class p_insertCss or p_deleteCss for # highlighting the change. If no class is provided, default styles will # be used (see HtmlDiff.insertStyle and HtmlDiff.deleteStyle). self.insertCss = insertCss self.deleteCss = deleteCss + self.complexCss = complexCss # This tag will get a "name" attribute whose content will be # p_insertName or p_deleteName self.insertName = insertName self.deleteName = deleteName + self.complexName = complexName # The diff algorithm of this class will need to identify similarities # between strings. Similarity ratios will be computed by using method # difflib.SequenceMatcher.ratio (see m_isSimilar below). Strings whose @@ -138,27 +179,33 @@ class HtmlDiff: self.diffRatio = diffRatio # Some computed values for tag in ('div', 'span'): - setattr(self, '%sInsertPrefix' % tag, - '<%s name="%s"' % (tag, self.insertName)) - setattr(self, '%sDeletePrefix' % tag, - '<%s name="%s"' % (tag, self.deleteName)) + for type in ('insert', 'delete', 'complex'): + setattr(self, '%s%sPrefix' % (tag, type.capitalize()), + '<%s name="%s"' % (tag, getattr(self, '%sName' % type))) - def getModifiedChunk(self, seq, type, sep): + def getModifiedChunk(self, seq, type, sep, msg=None): '''p_sep.join(p_seq) (if p_seq is a list) or p_seq (if p_seq is a string) is a chunk that was either inserted (p_type='insert') or - deleted (p_type='delete'). This method will surround this part with - a div or span tag that will get some CSS class allowing to highlight - the difference.''' - # Prepare parts of the surrounding tag. + deleted (p_type='delete'). It can also be a complex, partially + managed combination of inserts/deletions (p_type='insert'). + This method will surround this part with a div or span tag that will + get some CSS class allowing to highlight the update. If p_msg is + given, it will be used instead of the default p_type-related message + stored on p_self.''' + # Will the surrouding tag be a div or a span? if sep == '\n': tag = 'div' else: tag = 'span' - exec 'msg = self.%sMsg' % type + # What message wiill it show in its 'title' attribute? + if not msg: + exec 'msg = self.%sMsg' % type + # What CSS class (or, if none, tag-specific style) will be used ? exec 'cssClass = self.%sCss' % type if cssClass: style = 'class="%s"' % cssClass else: exec 'style = self.%sStyle' % type style = 'style="%s"' % style + # the 'name' attribute of the tag indicates the type of the update. exec 'tagName = self.%sName' % type # The idea is: if there are several lines, every line must be surrounded # by a tag. this way, we know that a surrounding tag can't span several @@ -176,6 +223,16 @@ class HtmlDiff: (sep, tag, tagName, style, msg, line, tag, sep) return res + def applyDiff(self, line, diff): + '''p_diff is a regex containing an insert or delete that was found within + line. This function applies the diff, removing or inserting the diff + into p_line.''' + # Keep content only for "insert" tags. + content = '' + if diff.group(1) == 'insert': + content = diff.group(3) + return line[:diff.start()] + content + line[diff.end():] + def getStringDiff(self, old, new): '''Identifies the differences between strings p_old and p_new by computing: @@ -255,11 +312,7 @@ class HtmlDiff: if not match: break # I found one. innerDiffs.append(match) - # Keep content only for "insert" tags. - content = '' - if match.group(1) == 'insert': - content = match.group(2) - line = line[:match.start()] + content + line[match.end():] + line = self.applyDiff(line, match) return (action, line, innerDiffs, outerTag) def getSeqDiff(self, seqA, seqB): @@ -414,7 +467,8 @@ class HtmlDiff: # Merge potential previous inner diff tags that # were found (but extracted from) lineA. if previousDiffsA: - merger= Merger(lineA, toAdd, previousDiffsA) + merger = Merger(lineA, toAdd, + previousDiffsA, self) toAdd = merger.merge() # Rewrap line into outerTag if lineA was a line # tagged as previously inserted. diff --git a/shared/utils.py b/shared/utils.py index 30340a1..b5c48a5 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -209,18 +209,27 @@ def formatNumber(n, sep=',', precision=2, tsep=' '): res = format % n # Use the correct decimal separator res = res.replace('.', sep) - # Format the integer part with tsep: TODO. + # Insert p_tsep every 3 chars in the integer part of the number splitted = res.split(sep) - # Remove the decimal part if = 0 + res = '' + i = len(splitted[0])-1 + j = 0 + while i >= 0: + j += 1 + res = splitted[0][i] + res + if (j % 3) == 0: + res = tsep + res + i -= 1 + # Add the decimal part if not 0 if len(splitted) > 1: try: decPart = int(splitted[1]) - if decPart == 0: - res = splitted[0] + if decPart != 0: + res += sep + str(decPart) except ValueError: # This exception may occur when the float value has an "exp" - # part, like in this example: 4.345e-05. - pass + # part, like in this example: 4.345e-05 + res += sep + splitted[1] return res # ------------------------------------------------------------------------------