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 = '