diff --git a/fields/__init__.py b/fields/__init__.py index 5a50bcc..e63a701 100644 --- a/fields/__init__.py +++ b/fields/__init__.py @@ -63,7 +63,7 @@ class Field: value=not isSearch and \ field.getFormattedValue(zobj, rawValue, showChanges); requestValue=not isSearch and zobj.getRequestFieldValue(name); - inRequest=req.has_key(name); + inRequest=field.valueIsInRequest(req, name); error=req.get('%s_error' % name); isMultiple=(field.multiplicity[1] == None) or \ (field.multiplicity[1] > 1); @@ -544,6 +544,14 @@ class Field: return res return value + def valueIsInRequest(self, request, name): + '''Is there a value corresponding to this field in the request? p_name + can be different from self.name (ie, if it is a field within another + (List) field). In most cases, checking that this p_name is in the + request is sufficient. But in some cases it may be more complex, ie + for string multilingual fields.''' + return request.has_key(name) + def getRequestValue(self, request, requestName=None): '''Gets a value for this field as carried in the request object. In the simplest cases, the request value is a single value whose name in the @@ -601,6 +609,18 @@ class Field: '''Returns True if the p_value must be considered as an empty value.''' return value in self.nullValues + def isCompleteValue(self, value, obj=None): + '''Returns True if the p_value must be considered as "complete". While, + in most cases, a "complete" value simply means a "non empty" value + (see m_isEmptyValue above), in some special cases it is more subtle. + For example, a multilingual string value is not empty as soon as a + value is given for some language but will not be considered as + complete while a value is missing for some language. Another example: + a Date with the "hour" part required will not be considered as empty + if the "day, month, year" part is present but will not be considered + as complete without the "hour, minute" part.''' + return not self.isEmptyValue(value, obj) + def validateValue(self, obj, value): '''This method may be overridden by child classes and will be called at the right moment by m_validate defined below for triggering @@ -623,8 +643,8 @@ class Field: m_getRequestValue defined above, is valid according to this type definition. If it is the case, None is returned. Else, a translated error message is returned.''' - # Check that a value is given if required. - if self.isEmptyValue(value, obj): + # If the value is required, check that a (complete) value is present. + if not self.isCompleteValue(value, obj): if self.required and self.isClientVisible(obj): # If the field is required, but not visible according to # master/slave relationships, we consider it not to be required. diff --git a/fields/boolean.py b/fields/boolean.py index 9004e89..e99868e 100644 --- a/fields/boolean.py +++ b/fields/boolean.py @@ -35,6 +35,7 @@ class Boolean(Field): cLayouts = {'view': 'lf|', 'edit': 'flrv|'} # Layout for radio buttons (render = "radios") rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'} + rlLayouts = {'edit': 'l-f', 'view': 'lf', 'search': 'l-f'} pxView = pxCell = Px(''':value ::value or '-'
::value or '-'
- + - ''') + # pxEdit part for formats String.LINE (but that are not selections), + # String.PASSWORD and String.CAPTCHA. + pxEditText = Px(''' + + + :_('captcha_text', \ + mapping=field.getCaptchaChallenge(req.SESSION)) + ''') + + # pxEdit part for formats String.TEXT and String.XHTML. + pxEditTextArea = Px(''' + + ''') + + # Mapping formats -> PXs defining the edit widgets. + editPx = {LINE:pxEditText, TEXT:pxEditTextArea, XHTML:pxEditTextArea, + PASSWORD:pxEditText, CAPTCHA:pxEditText} pxEdit = Px(''' + multilingual=field.isMultilingual()"> - - - - :_('captcha_text', \ - mapping=field.getCaptchaChallenge(req.SESSION)) - - - - - - - ''') + + :field.editPx[fmt] + + + + + + + + +
+ :lg.upper():field.editPx[fmt]
''') pxCell = Px(''' @@ -285,12 +315,6 @@ class String(Field): if not alpha.match(c): return False return True - # Possible values for "format" - LINE = 0 - TEXT = 1 - XHTML = 2 - PASSWORD = 3 - CAPTCHA = 4 def __init__(self, validator=None, multiplicity=(0,1), default=None, format=LINE, show=True, page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, @@ -300,7 +324,7 @@ class String(Field): label=None, sdefault='', scolspan=1, swidth=None, sheight=None, persist=True, transform='none', placeholder=None, styles=('p','h1','h2','h3','h4'), allowImageUpload=True, - spellcheck=False, contentLanguage=None, inlineEdit=False): + spellcheck=False, languages=('en',), inlineEdit=False): # According to format, the widget will be different: input field, # textarea, inline editor... Note that there can be only one String # field of format CAPTCHA by page, because the captcha challenge is @@ -314,8 +338,11 @@ class String(Field): self.allowImageUpload = allowImageUpload # When format is XHTML, do we run the CK spellchecker ? self.spellcheck = spellcheck - # What is the language of field content? - self.contentLanguage = contentLanguage + # If "languages" holds more than one language, the field will be + # multi-lingual and several widgets will allow to edit/visualize the + # field content in all the supported languages. The field is also used + # by the CK spell checker. + self.languages = languages # When format in XHTML, can the field be inline-edited (ckeditor)? self.inlineEdit = inlineEdit # The following field has a direct impact on the text entered by the @@ -361,6 +388,12 @@ class String(Field): not self.isSelect self.swidth = self.swidth or self.width self.sheight = self.sheight or self.height + self.checkParameters() + + def checkParameters(self): + '''Ensures this String is correctly defined.''' + if self.isSelect and self.isMultilingual(): + raise Exception("A selection field can't be multilingual.") def isSelection(self): '''Does the validator of this type definition define a list of values @@ -376,6 +409,8 @@ class String(Field): res = False return res + def isMultilingual(self): return len(self.languages) > 1 + def getDefaultLayouts(self): '''Returns the default layouts for this type. Default layouts can vary acccording to format, multiplicity or history.''' @@ -394,7 +429,7 @@ class String(Field): return {'view': 'l-f', 'edit': 'lrv-f'} def getValue(self, obj): - # Cheat if this field represents p_obj's state + # Cheat if this field represents p_obj's state. if self.name == 'state': return obj.State() value = Field.getValue(self, obj) if not value: @@ -406,27 +441,46 @@ class String(Field): value = list(value) return value - def store(self, obj, value): - '''When the value is XHTML, we perform some cleanup.''' - if not self.persist: return - if (self.format == String.XHTML) and value: - # 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. - try: - value = XhtmlCleaner(keepStyles=False).clean(value) - except XhtmlCleaner.Error, e: - # Errors while parsing p_value can't prevent the user from - # storing it. - obj.log('Unparsable XHTML content in field "%s".' % self.name, - type='warning') - Field.store(self, obj, value) + def valueIsInRequest(self, request, name): + if not self.isMultilingual(): + return Field.valueIsInRequest(self, request, name) + return request.has_key('%s_%s' % (name, self.languages[0])) - def storeFromAjax(self, obj): - '''Stores the new field value from an Ajax request, or do nothing if - the action was canceled.''' - rq = obj.REQUEST - if rq.get('cancel') != 'True': self.store(obj, rq['fieldContent']) + def getRequestValue(self, request, requestName=None): + '''The request value may be multilingual.''' + name = requestName or self.name + # A unilingual field. + if not self.isMultilingual(): return request.get(name, None) + # A multilingual field. + res = {} + for language in self.languages: + res[language] = request.get('%s_%s' % (name, language), None) + return res + + def isEmptyValue(self, value, obj=None): + '''Returns True if the p_value must be considered as an empty value.''' + if not self.isMultilingual(): + return Field.isEmptyValue(self, value, obj) + # For a multilingual value, as soon as a value is not empty for a given + # language, the whole value is considered as not being empty. + if not value: return True + for v in value.itervalues(): + if not Field.isEmptyValue(self, v, obj): return + return True + + def isCompleteValue(self, value, obj=None): + '''Returns True if the p_value must be considered as complete. For a + unilingual field, being complete simply means not being empty. For a + multilingual field, being complete means that a value is present for + every language''' + if not self.isMultilingual(): + return Field.isCompleteValue(self, value, obj) + # As soon as a given language value is empty, the global value is not + # complete. + if not value: return True + for v in value.itervalues(): + if Field.isEmptyValue(self, v, obj): return + return True def getDiffValue(self, obj, value): '''Returns a version of p_value that includes the cumulative diffs @@ -452,8 +506,8 @@ class String(Field): comparator = HtmlDiff(res, value or '', iMsg, dMsg) return comparator.get() - def getFormattedValue(self, obj, value, showChanges=False): - if self.isEmptyValue(value): return '' + def getUnilingualFormattedValue(self, obj, value, showChanges=False): + if Field.isEmptyValue(self, value): return '' res = value if self.isSelect: if isinstance(self.validator, Selection): @@ -480,6 +534,17 @@ class String(Field): (res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res return res + def getFormattedValue(self, obj, value, showChanges=False): + if not self.isMultilingual(): + return self.getUnilingualFormattedValue(obj, value, showChanges) + # Return the dict of values whose individual, language-specific values + # have been formatted via m_getUnilingualFormattedValue. + if not value: return value + res = {} + for lg in self.languages: + res[lg]=self.getUnilingualFormattedValue(obj,value[lg],showChanges) + return res + emptyStringTuple = ('',) emptyValuesCatalogIgnored = (None, '') def getIndexValue(self, obj, forSearch=False): @@ -623,12 +688,23 @@ class String(Field): elif self.transform == 'capitalize': return value.capitalize() return value - def getStorableValue(self, value): + def getUnilingualStorableValue(self, value): isString = isinstance(value, basestring) + isEmpty = Field.isEmptyValue(self, value) # Apply transform if required - if isString and not self.isEmptyValue(value) and \ - (self.transform != 'none'): + 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: + # 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. + try: + value = XhtmlCleaner(keepStyles=False).clean(value) + except XhtmlCleaner.Error, e: + # Errors while parsing p_value can't prevent the user from + # storing it. + pass # Truncate the result if longer than self.maxChars if isString and self.maxChars and (len(value) > self.maxChars): value = value[:self.maxChars] @@ -638,6 +714,33 @@ class String(Field): value = [value] return value + def getStorableValue(self, value): + if not self.isMultilingual(): + return self.getUnilingualStorableValue(value) + # A multilingual value is stored as a dict whose keys are ISO 2-letters + # language codes and whose values are strings storing content in the + # language ~{s_language: s_content}~. + if not value: return + for lg in self.languages: + value[lg] = self.getUnilingualStorableValue(value[lg]) + return value + + def store(self, obj, value): + '''Stores p_value on p_obj for this field.''' + if self.isMultilingual() and value and \ + (not isinstance(value, dict) or (len(value) != len(self.languages))): + raise Exception('Multilingual field "%s" accepts a dict whose '\ + 'keys are in field.languages and whose ' \ + 'values are strings.' % self.name) + Field.store(self, obj, value) + + def storeFromAjax(self, obj): + '''Stores the new field value from an Ajax request, or do nothing if + the action was canceled.''' + rq = obj.REQUEST + if rq.get('cancel') != 'True': + self.store(obj, self.getStorableValue(rq['fieldContent'])) + def getIndexType(self): '''Index type varies depending on String parameters.''' # If String.isSelect, be it multivalued or not, we define a ListIndex: @@ -679,19 +782,19 @@ class String(Field): 'fi': 'fi_FI', 'fr': 'fr_FR', 'de': 'de_DE', 'el': 'el_GR', 'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES', 'sv': 'sv_SE'} - def getCkLanguage(self): - '''Gets the language for CK editor SCAYT. We will use - self.contentLanguage. If it is not supported by CK, we use - english.''' - lang = self.contentLanguage - if lang and (lang in self.ckLanguages): return self.ckLanguages[lang] + def getCkLanguage(self, language): + '''Gets the language for CK editor SCAYT. p_language is one of + self.languages if the field is multilingual, None else. If p_language + is not supported by CK, we use english.''' + if not language: language = self.languages[0] + if language in self.ckLanguages: return self.ckLanguages[language] return 'en_US' - def getCkParams(self, obj): + def getCkParams(self, obj, language): '''Gets the base params to set on a rich text field.''' ckAttrs = {'toolbar': 'Appy', 'format_tags': ';'.join(self.styles), - 'scayt_sLang': self.getCkLanguage()} + 'scayt_sLang': self.getCkLanguage(language)} if self.width: ckAttrs['width'] = self.width if self.spellcheck: ckAttrs['scayt_autoStartup'] = True if self.allowImageUpload: @@ -704,31 +807,28 @@ class String(Field): ck.append('%s: %s' % (k, sv)) return ', '.join(ck) - def getJsInit(self, obj): + def getJsInit(self, obj, language): '''Gets the Javascript init code for displaying a rich editor for this - field (rich field only).''' + field (rich field only). If the field is multilingual, we must init + the rich text editor for a given p_language (among self.languages). + Else, p_languages is None.''' + name = not language and self.name or ('%s_%s' % (self.name, language)) return 'CKEDITOR.replace("%s", {%s})' % \ - (self.name, self.getCkParams(obj)) + (name, self.getCkParams(obj, language)) - def getJsInlineInit(self, obj): + def getJsInlineInit(self, obj, language): '''Gets the Javascript init code for enabling inline edition of this - field (rich text only).''' + field (rich text only). If the field is multilingual, the current + p_language is given. Else, p_language is None.''' uid = obj.id + name = not language and self.name or ('%s_%s' % (self.name, language)) return "CKEDITOR.disableAutoInline = true;\n" \ "CKEDITOR.inline('%s_%s_ck', {%s, on: {blur: " \ "function( event ) { var content = event.editor.getData(); " \ "doInlineSave('%s', '%s', '%s', content)}}})" % \ - (uid, self.name, self.getCkParams(obj), uid, self.name, + (uid, name, self.getCkParams(obj, language), uid, name, obj.absolute_url()) - return "CKEDITOR.disableAutoInline = true;\n" \ - "CKEDITOR.inline('%s_%s_ck', {on: {blur: " \ - "function( event ) { var data = event.editor.getData(); " \ - "askAjaxChunk('%s_%s','POST','%s','%s:pxSave', " \ - "{'fieldContent': encodeURIComponent(data)}, " \ - "null, evalInnerScripts);}}});"% \ - (uid, self.name, uid, self.name, obj.absolute_url(), self.name) - def isSelected(self, obj, fieldName, vocabValue, dbValue): '''When displaying a selection box (only for fields with a validator being a list), must the _vocabValue appear as selected? p_fieldName diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 8f43360..9761f12 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -29,13 +29,13 @@ input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8; cursor: pointer } input[type=password] { border: 1px solid #d0d0d0; background-color: #f8f8f8; font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif } -input[type=text] { border: 1px solid #d0d0d0; background-color: #f8f8f8; +input[type=text] { border: 1px solid #d0d0d0; font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif; margin-bottom: 1px } select { border: 1px solid #d0d0d0; background-color: #f8f8f8 } textarea { width: 99%; font: 100% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif; - border: 1px solid #d0d0d0; background-color: #f8f8f8 } + border: 1px solid #d0d0d0; background-color: white } label { color: #888888; font-size: 11px; margin: 3px 0; text-transform: uppercase } legend { padding-bottom: 2px; padding-right: 3px; color: black } @@ -167,3 +167,5 @@ td.search { padding-top: 8px } .tabs { position:relative; bottom:-2px } .tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%; font-weight: bold} +.language {color: grey; font-size: 7pt; border: grey 1px solid; padding: 2px; + margin: 0 2px 0 4px} diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index 971572f..3e9ed7f 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -211,7 +211,7 @@ class AbstractWrapper(object):