diff --git a/fields/__init__.py b/fields/__init__.py index 077839f..eea2bc4 100644 --- a/fields/__init__.py +++ b/fields/__init__.py @@ -51,7 +51,7 @@ class Field: # * showChanges If True, a variant of the field showing successive changes # made to it is shown. pxRender = Px(''' - ''') # Button for showing changes to the field. - pxChanges = Px('''''') + pxChanges = Px(''' + + + + + + + ''') def __init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, diff --git a/fields/string.py b/fields/string.py index f2e78ff..75a858c 100644 --- a/fields/string.py +++ b/fields/string.py @@ -549,22 +549,25 @@ class String(Field): if Field.isEmptyValue(self, v, obj): return return True - def getDiffValue(self, obj, value): + def getDiffValue(self, obj, value, language): '''Returns a version of p_value that includes the cumulative diffs - between successive versions.''' + between successive versions. If the field is non-multilingual, it + must be called with p_language being None. Else, p_language + identifies the language-specific part we will work on.''' res = None lastEvent = None - for event in obj.workflow_history.values()[0]: + name = language and ('%s-%s' % (self.name, language)) or self.name + for event in obj.workflow_history['appy']: if event['action'] != '_datachange_': continue - if self.name not in event['changes']: continue + if name not in event['changes']: continue if res == None: # We have found the first version of the field - res = event['changes'][self.name][0] or '' + res = event['changes'][name][0] or '' else: # We need to produce the difference between current result and # this version. iMsg, dMsg = obj.getHistoryTexts(lastEvent) - thisVersion = event['changes'][self.name][0] or '' + thisVersion = event['changes'][name][0] or '' comparator = HtmlDiff(res, thisVersion, iMsg, dMsg) res = comparator.get() lastEvent = event @@ -573,7 +576,12 @@ class String(Field): comparator = HtmlDiff(res, value or '', iMsg, dMsg) return comparator.get() - def getUnilingualFormattedValue(self, obj, value, showChanges=False): + def getUnilingualFormattedValue(self, obj, value, showChanges=False, + language=None): + '''If no p_language is specified, this method is called by + m_getFormattedValue for getting a non-multilingual value (ie, in + most cases). Else, this method returns a formatted value for the + p_language-specific part of a multilingual value.''' if Field.isEmptyValue(self, value): return '' res = value if self.isSelect: @@ -594,7 +602,7 @@ class String(Field): res = t('%s_list_%s' % (self.labelId, value)) elif (self.format == String.XHTML) and showChanges: # Compute the successive changes that occurred on p_value. - res = self.getDiffValue(obj, res) + res = self.getDiffValue(obj, res, language) # If value starts with a carriage return, add a space; else, it will # be ignored. if isinstance(res, basestring) and \ @@ -609,7 +617,8 @@ class String(Field): if not value: return value res = {} for lg in self.languages: - res[lg]=self.getUnilingualFormattedValue(obj,value[lg],showChanges) + res[lg] = self.getUnilingualFormattedValue(obj, value[lg], + showChanges, lg) return res def extractText(self, value): @@ -773,8 +782,8 @@ class String(Field): # Apply transform if required if isString and not isEmpty and (self.transform != 'none'): value = self.applyTransform(value) - # Clean XHTML if format is XHTML - if (self.format == String.XHTML) and not isEmpty: + # Clean XHTML strings + if not isEmpty and (self.format == String.XHTML): # When image upload is allowed, ckeditor inserts some "style" attrs # (ie for image size when images are resized). So in this case we # can't remove style-related information. @@ -784,6 +793,9 @@ class String(Field): # Errors while parsing p_value can't prevent the user from # storing it. pass + # Clean TEXT strings + if not isEmpty and (self.format == String.TEXT): + value = value.replace('\r', '') # Truncate the result if longer than self.maxChars if isString and self.maxChars and (len(value) > self.maxChars): value = value[:self.maxChars] @@ -819,6 +831,11 @@ class String(Field): rq = obj.REQUEST if rq.get('cancel') == 'True': return requestValue = rq['fieldContent'] + # Remember previous value if the field is historized. + previousData = obj.rememberPreviousData([self]) + # We take a copy because the data is mutable (ie, a dict). + if previousData: + previousData[self.name] = previousData[self.name].copy() if self.isMultilingual(): # We get a partial value, for one language only. language = rq['languageOnly'] @@ -828,6 +845,12 @@ class String(Field): else: self.store(obj, self.getStorableValue(requestValue)) part = '' + # Update the object history when relevant + if previousData: obj.historizeData(previousData) + # Update obj's last modification date + from DateTime import DateTime + obj.modified = DateTime() + obj.reindex() obj.log('Ajax-edited %s%s on %s.' % (self.name, part, obj.id)) def getIndexType(self): diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 34d5b93..f086123 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -152,10 +152,12 @@ class ToolMixin(BaseMixin): '''Returns the supported languages. First one is the default.''' return self.getProductConfig(True).languages - def getLanguageName(self, code): + def getLanguageName(self, code, lowerize=False): '''Gets the language name (in this language) from a 2-chars language p_code.''' - return languages.get(code)[2] + res = languages.get(code)[2] + if not lowerize: return res + return res.lower() def changeLanguage(self): '''Sets the language cookie with the new desired language code that is diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 61727a5..474fa9a 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -336,7 +336,7 @@ class BaseMixin: value.''' rq = self.REQUEST for field in self.getAppyTypes('edit', rq.form.get('page')): - if not field.validable: continue + if not field.validable or not field.isClientVisible(self): continue value = field.getRequestValue(rq) message = field.validate(self, value) if message: @@ -592,14 +592,8 @@ class BaseMixin: ~{s_fieldName: previousFieldValue}~''' res = {} for field in fields: - if not field.historized: continue - # appyType.historized can be a method or a boolean. - if callable(field.historized): - historized = field.callMethod(self, field.historized) - else: - historized = field.historized - if historized: - res[field.name] = field.getValue(self) + if not field.getAttribute(self, 'historized'): continue + res[field.name] = field.getValue(self) return res def addHistoryEvent(self, action, **kw): @@ -622,15 +616,29 @@ class BaseMixin: a field. The method is also called by m_historizeData below, that performs "automatic" recording when a HTTP form is uploaded. Field changes for which the previous value was empty are not recorded into - the history if p_notForPreviouslyEmptyValues is True.''' + the history if p_notForPreviouslyEmptyValues is True. + + For a multilingual string field, p_changes can contain a key for + every language, of the form -.''' # Add to the p_changes dict the field labels - for fieldName in changes.keys(): - appyType = self.getAppyType(fieldName) - if notForPreviouslyEmptyValues and \ - appyType.isEmptyValue(changes[fieldName], self): - del changes[fieldName] + for name in changes.keys(): + # "name" can contain the language for multilingual fields. + if '-' in name: + fieldName, lg = name.split('-') else: - changes[fieldName] = (changes[fieldName], appyType.labelId) + fieldName = name + lg = None + field = self.getAppyType(fieldName) + if notForPreviouslyEmptyValues: + # Check if the previous field value was empty + if lg: + isEmpty = not changes[name] or not changes[name].get(lg) + else: + isEmpty = field.isEmptyValue(changes[name], self) + if isEmpty: + del changes[name] + else: + changes[name] = (changes[name], field.labelId) # Add an event in the history self.addHistoryEvent('_datachange_', changes=changes) @@ -640,20 +648,30 @@ class BaseMixin: historized fields, while p_self already contains the (potentially) modified values.''' # Remove from previousData all values that were not changed - for field in previousData.keys(): - prev = previousData[field] - appyType = self.getAppyType(field) - curr = appyType.getValue(self) + for name in previousData.keys(): + field = self.getAppyType(name) + prev = previousData[name] + curr = field.getValue(self) try: if (prev == curr) or ((prev == None) and (curr == '')) or \ ((prev == '') and (curr == None)): - del previousData[field] + del previousData[name] + continue except UnicodeDecodeError, ude: # The string comparisons above may imply silent encoding-related # conversions that may produce this exception. - pass - if (appyType.type == 'Ref') and (field in previousData): - previousData[field] = [r.title for r in previousData[field]] + continue + # In some cases the old value must be formatted. + if field.type == 'Ref': + previousData[name] = [r.title for r in previousData[name]] + elif (field.type == 'String') and field.isMultilingual(): + # Consider every language-specific value as a first-class value + del previousData[name] + for lg in field.languages: + lgPrev = prev and prev.get(lg) or None + lgCurr = curr and curr.get(lg) or None + if lgPrev == lgCurr: continue + previousData['%s-%s' % (name, lg)] = lgPrev if previousData: self.addDataChange(previousData) @@ -1020,56 +1038,73 @@ class BaseMixin: return 1 return 0 - def findNewValue(self, field, history, stopIndex): + def findNewValue(self, field, language, history, stopIndex): '''This function tries to find a more recent version of value of p_field - on p_self. It first tries to find it in history[:stopIndex+1]. If - it does not find it there, it returns the current value on p_obj.''' + on p_self. In the case of a multilingual field, p_language is + specified. The method first tries to find it in + history[:stopIndex+1]. If it does not find it there, it returns the + current value on p_obj.''' i = stopIndex + 1 + name = language and ('%s-%s' % (field.name, language)) or field.name while (i-1) >= 0: i -= 1 if history[i]['action'] != '_datachange_': continue - if field.name not in history[i]['changes']: continue + if name not in history[i]['changes']: continue # We have found it! - return history[i]['changes'][field.name][0] or '' - return field.getValue(self) or '' + return history[i]['changes'][name][0] or '' + # A most recent version was not found in the history: return the current + # field value. + val = field.getValue(self) + if not language: return val or '' + return val and val.get(language) or '' def getHistoryTexts(self, event): '''Returns a tuple (insertText, deleteText) containing texts to show on, respectively, inserted and deleted chunks of text in a XHTML diff.''' tool = self.getTool() - userName = tool.getUserName(event['actor']) - mapping = {'userName': userName.decode('utf-8')} + mapping = {'userName': tool.getUserName(event['actor'])} res = [] for type in ('insert', 'delete'): msg = self.translate('history_%s' % type, mapping=mapping) date = tool.formatDate(event['time'], withHour=True) msg = '%s: %s' % (date, msg) - res.append(msg.encode('utf-8')) + res.append(msg) return res - def hasHistory(self, fieldName=None): - '''Has this object an history? If p_fieldName is specified, the question - becomes: has this object an history for field p_fieldName?''' - if hasattr(self.aq_base, 'workflow_history') and self.workflow_history: - history = self.workflow_history.values()[0] - if not fieldName: - for event in history: - if event['action'] and (event['comments'] != '_invisible_'): + def hasHistory(self, name=None): + '''Has this object an history? If p_name is specified, the question + becomes: has this object an history for field p_name?''' + if not hasattr(self.aq_base, 'workflow_history') or \ + not self.workflow_history: return + history = self.workflow_history['appy'] + if not name: + for event in history: + if event['action'] and (event['comments'] != '_invisible_'): + return True + else: + field = self.getAppyType(name) + multilingual = (field.type == 'String') and field.isMultilingual() + for event in history: + if event['action'] != '_datachange_': continue + # Is there a value present for this field in this data change? + if not multilingual: + if (name in event['changes']) and \ + (event['changes'][name][0]): return True - else: - for event in history: - if (event['action'] == '_datachange_') and \ - (fieldName in event['changes']) and \ - event['changes'][fieldName][0]: return True + else: + # At least one language-specific value must be present + for lg in field.languages: + lgName = '%s-%s' % (field.name, lg) + if (lgName in event['changes']) and \ + event['changes'][lgName][0]: + return True def getHistory(self, startNumber=0, reverse=True, includeInvisible=False, batchSize=5): - '''Returns the history for this object, sorted in reverse order (most - recent change first) if p_reverse is True.''' - # Get a copy of the history, reversed if needed, whose invisible events - # have been removed if needed. - key = self.workflow_history.keys()[0] - history = list(self.workflow_history[key][1:]) + '''Returns a copy of the history for this object, sorted in p_reverse + order if specified (most recent change first), whose invisible events + have been removed if p_includeInvisible is True.''' + history = list(self.workflow_history['appy'][1:]) if not includeInvisible: history = [e for e in history if e['comments'] != '_invisible_'] if reverse: history.reverse() @@ -1088,8 +1123,15 @@ class BaseMixin: event = history[i].copy() event['changes'] = {} for name, oldValue in history[i]['changes'].iteritems(): - # oldValue is a tuple (value, fieldName). - field = self.getAppyType(name) + # "name" can specify a language-specific part in a + # multilingual field. "oldValue" is a tuple + # (value, fieldName). + if '-' in name: + fieldName, lg = name.split('-') + else: + fieldName = name + lg = None + field = self.getAppyType(fieldName) # Field 'name' may not exist, if the history has been # transferred from another site. In this case we can't show # this data change. @@ -1099,21 +1141,24 @@ class BaseMixin: # For rich text fields, instead of simply showing the # previous value, we propose a diff with the next # version, excepted if the previous value is empty. - if field.isEmptyValue(oldValue[0]): + if lg: isEmpty = not oldValue[0] + else: isEmpty = field.isEmptyValue(oldValue[0]) + if isEmpty: val = '-' else: - newValue = self.findNewValue(field, history, i-1) + newValue= self.findNewValue(field, lg, history, i-1) # Compute the diff between oldValue and newValue iMsg, dMsg = self.getHistoryTexts(event) comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg) val = comparator.get() - event['changes'][name] = (val, oldValue[1]) else: - val = field.getFormattedValue(self, oldValue[0]) or '-' + fmt = lg and 'getUnilingualFormattedValue' or \ + 'getFormattedValue' + val = getattr(field, fmt)(self, oldValue[0]) or '-' if isinstance(val, list) or isinstance(val, tuple): val = '
    %s
' % \ ''.join(['
  • %s
  • ' % v for v in val]) - event['changes'][name] = (val, oldValue[1]) + event['changes'][name] = (val, oldValue[1]) else: event = history[i] res.append(event) diff --git a/gen/ui/appy.css b/gen/ui/appy.css index d8e0cb3..e0dd0ae 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -58,7 +58,7 @@ img { border: 0; vertical-align: middle } .userStrip a:visited { color: #e7e7e7 } .breadcrumb { font-size: 11pt; padding-bottom: 6px } .login { margin: 3px; color: black } -input.button { color: #666666; height: 20px; margin-bottom: 5px; +input.button { color: #666666; height: 20px; margin-bottom: 5px; margin-top:2px; cursor:pointer; font-size: 90%; padding-left: 10px; background-color: white; background-repeat: no-repeat; background-position: 8px 25%; box-shadow: 2px 2px 2px #888888} @@ -75,7 +75,8 @@ input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px; .message { position: absolute; top: -40px; left: 50%; font-size: 90%; width: 600px; border: 1px #F0C36D solid; padding: 6px; background-color: #F9EDBE; text-align: center; margin-left: -300px; - border-radius: 2px 2px 2px 2px; box-shadow: 0 2px 4px #A9A9A9 } + border-radius: 2px 2px 2px 2px; box-shadow: 0 2px 4px #A9A9A9; + z-index: 10 } .messagePopup { width: 80%; top: 0; left: 0; margin-left: 10px } .focus { font-size: 90%; margin: 7px 0 7px 0; padding: 7px; background-color: #d7dee4; border-radius: 2px 2px 2px 2px; diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index 3e9ed7f..29bfe54 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -375,8 +375,11 @@ class AbstractWrapper(object): :_('previous_value') - ::_(field.labelId) + var2="elems=change[0].split('-'); + field=zobj.getAppyType(elems[0]); + lg=(len(elems) == 2) and elems[1] or ''"> + ::_(field.labelId) + :' (%s)' % ztool.getLanguageName(lg, True) ::change[1][0]