[gen] More work on string multilingual fields.

This commit is contained in:
Gaetan Delannay 2014-08-13 17:17:25 +02:00
parent 6770d23a50
commit 4131ba899e
5 changed files with 210 additions and 87 deletions

View file

@ -63,7 +63,7 @@ class Field:
value=not isSearch and \ value=not isSearch and \
field.getFormattedValue(zobj, rawValue, showChanges); field.getFormattedValue(zobj, rawValue, showChanges);
requestValue=not isSearch and zobj.getRequestFieldValue(name); requestValue=not isSearch and zobj.getRequestFieldValue(name);
inRequest=req.has_key(name); inRequest=field.valueIsInRequest(req, name);
error=req.get('%s_error' % name); error=req.get('%s_error' % name);
isMultiple=(field.multiplicity[1] == None) or \ isMultiple=(field.multiplicity[1] == None) or \
(field.multiplicity[1] > 1); (field.multiplicity[1] > 1);
@ -544,6 +544,14 @@ class Field:
return res return res
return value 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): def getRequestValue(self, request, requestName=None):
'''Gets a value for this field as carried in the request object. In the '''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 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.''' '''Returns True if the p_value must be considered as an empty value.'''
return value in self.nullValues 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): def validateValue(self, obj, value):
'''This method may be overridden by child classes and will be called at '''This method may be overridden by child classes and will be called at
the right moment by m_validate defined below for triggering 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 m_getRequestValue defined above, is valid according to this type
definition. If it is the case, None is returned. Else, a translated definition. If it is the case, None is returned. Else, a translated
error message is returned.''' error message is returned.'''
# Check that a value is given if required. # If the value is required, check that a (complete) value is present.
if self.isEmptyValue(value, obj): if not self.isCompleteValue(value, obj):
if self.required and self.isClientVisible(obj): if self.required and self.isClientVisible(obj):
# If the field is required, but not visible according to # If the field is required, but not visible according to
# master/slave relationships, we consider it not to be required. # master/slave relationships, we consider it not to be required.

View file

@ -35,6 +35,7 @@ class Boolean(Field):
cLayouts = {'view': 'lf|', 'edit': 'flrv|'} cLayouts = {'view': 'lf|', 'edit': 'flrv|'}
# Layout for radio buttons (render = "radios") # Layout for radio buttons (render = "radios")
rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'} rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'}
rlLayouts = {'edit': 'l-f', 'view': 'lf', 'search': 'l-f'}
pxView = pxCell = Px('''<x>:value</x> pxView = pxCell = Px('''<x>:value</x>
<input type="hidden" if="masterCss" <input type="hidden" if="masterCss"

View file

@ -77,6 +77,13 @@ class String(Field):
URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \ URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
'(([0-9]{1,5})?\/.*)?') '(([0-9]{1,5})?\/.*)?')
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
PASSWORD = 3
CAPTCHA = 4
pxView = Px(''' pxView = Px('''
<x var="fmt=field.format; isUrl=field.isUrl; <x var="fmt=field.format; isUrl=field.isUrl;
mayAjaxEdit=not showChanges and field.inlineEdit and \ mayAjaxEdit=not showChanges and field.inlineEdit and \
@ -101,16 +108,45 @@ class String(Field):
<div if="not mayAjaxEdit" class="xhtml">::value or '-'</div> <div if="not mayAjaxEdit" class="xhtml">::value or '-'</div>
<div if="mayAjaxEdit" class="xhtml" contenteditable="true" <div if="mayAjaxEdit" class="xhtml" contenteditable="true"
id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div> id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj)</script> <script if="mayAjaxEdit">::field.getJsInlineInit(zobj,None)</script>
</x> </x>
<span if="not value and (fmt != 2)" class="smaller">-</span> <span if="not value and (fmt != 2)" class="smaller">-</span>
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue" <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
name=":name" id=":name"/> name=":name" id=":name"/>
</x>''') </x>''')
# pxEdit part for formats String.LINE (but that are not selections),
# String.PASSWORD and String.CAPTCHA.
pxEditText = Px('''
<input var="inputId=not lg and name or '%s_%s' % (name, lg);
placeholder=field.getAttribute(obj, 'placeholder') or ''"
id=":inputId" name=":inputId" size=":field.width"
maxlength=":field.maxChars" placeholder=":placeholder"
value=":inRequest and requestValue or value"
style=":'text-transform:%s' % field.transform"
type=":(fmt == 3) and 'password' or 'text'"/>
<!-- Display a captcha if required -->
<span if="fmt == 4">:_('captcha_text', \
mapping=field.getCaptchaChallenge(req.SESSION))
</span>''')
# pxEdit part for formats String.TEXT and String.XHTML.
pxEditTextArea = Px('''
<textarea var="inputId=not lg and name or '%s_%s' % (name, lg)"
id=":inputId" name=":inputId" cols=":field.width"
class=":(fmt == 2) and ('rich_%s' % name) or ''"
style=":'text-transform:%s' % field.transform"
rows=":field.height">:inRequest and requestValue or value
</textarea>
<script if="fmt == 2"
type="text/javascript">::field.getJsInit(zobj, lg)</script>''')
# Mapping formats -> PXs defining the edit widgets.
editPx = {LINE:pxEditText, TEXT:pxEditTextArea, XHTML:pxEditTextArea,
PASSWORD:pxEditText, CAPTCHA:pxEditText}
pxEdit = Px(''' pxEdit = Px('''
<x var="fmt=field.format; <x var="fmt=field.format;
isOneLine=fmt in (0,3,4)"> multilingual=field.isMultilingual()">
<select if="field.isSelect" <select if="field.isSelect"
var2="possibleValues=field.getPossibleValues(zobj, \ var2="possibleValues=field.getPossibleValues(zobj, \
withTranslations=True, withBlankValue=True)" withTranslations=True, withBlankValue=True)"
@ -122,28 +158,22 @@ class String(Field):
selected=":field.isSelected(zobj, name, val[0], rawValue)" selected=":field.isSelected(zobj, name, val[0], rawValue)"
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option> title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
</select> </select>
<x if="isOneLine and not field.isSelect" <!-- Any other unilingual field. -->
var2="placeholder=field.getAttribute(obj, 'placeholder') or ''"> <x if="not field.isSelect and not multilingual"
<input id=":name" name=":name" size=":field.width" var2="lg=None">:field.editPx[fmt]</x>
maxlength=":field.maxChars" placeholder=":placeholder" <!-- Any other multilingual field. -->
value=":inRequest and requestValue or value" <table if="not field.isSelect and multilingual" width="100%">
style=":'text-transform:%s' % field.transform" <tr valign="top">
type=":(fmt == 3) and 'password' or 'text'"/> <x for="lg in field.languages">
<!-- Display a captcha if required --> <td style=":(fmt==2) and 'padding-top:1px' or 'padding-top:3px'"
<span if="fmt == 4">:_('captcha_text', \ width="25px">
mapping=field.getCaptchaChallenge(req.SESSION)) <span class="language help"
</span> title=":ztool.getLanguageName(lg)">:lg.upper()</span></td>
<td var="requestValue=requestValue[lg];
value=value[lg]|''">:field.editPx[fmt]</td>
</x> </x>
<x if="fmt in (1,2)"> </tr>
<textarea id=":name" name=":name" cols=":field.width" </table></x>''')
class=":(fmt == 2) and ('rich_%s' % name) or ''"
style=":'text-transform:%s' % field.transform"
rows=":field.height">:inRequest and requestValue or value
</textarea>
<script if="fmt == 2"
type="text/javascript">::field.getJsInit(zobj)</script>
</x>
</x>''')
pxCell = Px(''' pxCell = Px('''
<x var="multipleValues=value and isMultiple"> <x var="multipleValues=value and isMultiple">
@ -285,12 +315,6 @@ class String(Field):
if not alpha.match(c): return False if not alpha.match(c): return False
return True 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, def __init__(self, validator=None, multiplicity=(0,1), default=None,
format=LINE, show=True, page='main', group=None, layouts=None, format=LINE, show=True, page='main', group=None, layouts=None,
move=0, indexed=False, searchable=False, move=0, indexed=False, searchable=False,
@ -300,7 +324,7 @@ class String(Field):
label=None, sdefault='', scolspan=1, swidth=None, sheight=None, label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
persist=True, transform='none', placeholder=None, persist=True, transform='none', placeholder=None,
styles=('p','h1','h2','h3','h4'), allowImageUpload=True, 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, # According to format, the widget will be different: input field,
# textarea, inline editor... Note that there can be only one String # textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is # field of format CAPTCHA by page, because the captcha challenge is
@ -314,8 +338,11 @@ class String(Field):
self.allowImageUpload = allowImageUpload self.allowImageUpload = allowImageUpload
# When format is XHTML, do we run the CK spellchecker ? # When format is XHTML, do we run the CK spellchecker ?
self.spellcheck = spellcheck self.spellcheck = spellcheck
# What is the language of field content? # If "languages" holds more than one language, the field will be
self.contentLanguage = contentLanguage # 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)? # When format in XHTML, can the field be inline-edited (ckeditor)?
self.inlineEdit = inlineEdit self.inlineEdit = inlineEdit
# The following field has a direct impact on the text entered by the # The following field has a direct impact on the text entered by the
@ -361,6 +388,12 @@ class String(Field):
not self.isSelect not self.isSelect
self.swidth = self.swidth or self.width self.swidth = self.swidth or self.width
self.sheight = self.sheight or self.height 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): def isSelection(self):
'''Does the validator of this type definition define a list of values '''Does the validator of this type definition define a list of values
@ -376,6 +409,8 @@ class String(Field):
res = False res = False
return res return res
def isMultilingual(self): return len(self.languages) > 1
def getDefaultLayouts(self): def getDefaultLayouts(self):
'''Returns the default layouts for this type. Default layouts can vary '''Returns the default layouts for this type. Default layouts can vary
acccording to format, multiplicity or history.''' acccording to format, multiplicity or history.'''
@ -394,7 +429,7 @@ class String(Field):
return {'view': 'l-f', 'edit': 'lrv-f'} return {'view': 'l-f', 'edit': 'lrv-f'}
def getValue(self, obj): 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() if self.name == 'state': return obj.State()
value = Field.getValue(self, obj) value = Field.getValue(self, obj)
if not value: if not value:
@ -406,27 +441,46 @@ class String(Field):
value = list(value) value = list(value)
return value return value
def store(self, obj, value): def valueIsInRequest(self, request, name):
'''When the value is XHTML, we perform some cleanup.''' if not self.isMultilingual():
if not self.persist: return return Field.valueIsInRequest(self, request, name)
if (self.format == String.XHTML) and value: return request.has_key('%s_%s' % (name, self.languages[0]))
# 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 storeFromAjax(self, obj): def getRequestValue(self, request, requestName=None):
'''Stores the new field value from an Ajax request, or do nothing if '''The request value may be multilingual.'''
the action was canceled.''' name = requestName or self.name
rq = obj.REQUEST # A unilingual field.
if rq.get('cancel') != 'True': self.store(obj, rq['fieldContent']) 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): def getDiffValue(self, obj, value):
'''Returns a version of p_value that includes the cumulative diffs '''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) comparator = HtmlDiff(res, value or '', iMsg, dMsg)
return comparator.get() return comparator.get()
def getFormattedValue(self, obj, value, showChanges=False): def getUnilingualFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return '' if Field.isEmptyValue(self, value): return ''
res = value res = value
if self.isSelect: if self.isSelect:
if isinstance(self.validator, Selection): if isinstance(self.validator, Selection):
@ -480,6 +534,17 @@ class String(Field):
(res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res (res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res
return 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 = ('',) emptyStringTuple = ('',)
emptyValuesCatalogIgnored = (None, '') emptyValuesCatalogIgnored = (None, '')
def getIndexValue(self, obj, forSearch=False): def getIndexValue(self, obj, forSearch=False):
@ -623,12 +688,23 @@ class String(Field):
elif self.transform == 'capitalize': return value.capitalize() elif self.transform == 'capitalize': return value.capitalize()
return value return value
def getStorableValue(self, value): def getUnilingualStorableValue(self, value):
isString = isinstance(value, basestring) isString = isinstance(value, basestring)
isEmpty = Field.isEmptyValue(self, value)
# Apply transform if required # Apply transform if required
if isString and not self.isEmptyValue(value) and \ if isString and not isEmpty and (self.transform != 'none'):
(self.transform != 'none'):
value = self.applyTransform(value) 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 # Truncate the result if longer than self.maxChars
if isString and self.maxChars and (len(value) > self.maxChars): if isString and self.maxChars and (len(value) > self.maxChars):
value = value[:self.maxChars] value = value[:self.maxChars]
@ -638,6 +714,33 @@ class String(Field):
value = [value] value = [value]
return 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): def getIndexType(self):
'''Index type varies depending on String parameters.''' '''Index type varies depending on String parameters.'''
# If String.isSelect, be it multivalued or not, we define a ListIndex: # 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', 'fi': 'fi_FI', 'fr': 'fr_FR', 'de': 'de_DE', 'el': 'el_GR',
'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES', 'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES',
'sv': 'sv_SE'} 'sv': 'sv_SE'}
def getCkLanguage(self): def getCkLanguage(self, language):
'''Gets the language for CK editor SCAYT. We will use '''Gets the language for CK editor SCAYT. p_language is one of
self.contentLanguage. If it is not supported by CK, we use self.languages if the field is multilingual, None else. If p_language
english.''' is not supported by CK, we use english.'''
lang = self.contentLanguage if not language: language = self.languages[0]
if lang and (lang in self.ckLanguages): return self.ckLanguages[lang] if language in self.ckLanguages: return self.ckLanguages[language]
return 'en_US' return 'en_US'
def getCkParams(self, obj): def getCkParams(self, obj, language):
'''Gets the base params to set on a rich text field.''' '''Gets the base params to set on a rich text field.'''
ckAttrs = {'toolbar': 'Appy', ckAttrs = {'toolbar': 'Appy',
'format_tags': ';'.join(self.styles), 'format_tags': ';'.join(self.styles),
'scayt_sLang': self.getCkLanguage()} 'scayt_sLang': self.getCkLanguage(language)}
if self.width: ckAttrs['width'] = self.width if self.width: ckAttrs['width'] = self.width
if self.spellcheck: ckAttrs['scayt_autoStartup'] = True if self.spellcheck: ckAttrs['scayt_autoStartup'] = True
if self.allowImageUpload: if self.allowImageUpload:
@ -704,31 +807,28 @@ class String(Field):
ck.append('%s: %s' % (k, sv)) ck.append('%s: %s' % (k, sv))
return ', '.join(ck) return ', '.join(ck)
def getJsInit(self, obj): def getJsInit(self, obj, language):
'''Gets the Javascript init code for displaying a rich editor for this '''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})' % \ 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 '''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 uid = obj.id
name = not language and self.name or ('%s_%s' % (self.name, language))
return "CKEDITOR.disableAutoInline = true;\n" \ return "CKEDITOR.disableAutoInline = true;\n" \
"CKEDITOR.inline('%s_%s_ck', {%s, on: {blur: " \ "CKEDITOR.inline('%s_%s_ck', {%s, on: {blur: " \
"function( event ) { var content = event.editor.getData(); " \ "function( event ) { var content = event.editor.getData(); " \
"doInlineSave('%s', '%s', '%s', content)}}})" % \ "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()) 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): def isSelected(self, obj, fieldName, vocabValue, dbValue):
'''When displaying a selection box (only for fields with a validator '''When displaying a selection box (only for fields with a validator
being a list), must the _vocabValue appear as selected? p_fieldName being a list), must the _vocabValue appear as selected? p_fieldName

View file

@ -29,13 +29,13 @@ input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
cursor: pointer } cursor: pointer }
input[type=password] { border: 1px solid #d0d0d0; background-color: #f8f8f8; input[type=password] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif } 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; font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
margin-bottom: 1px } margin-bottom: 1px }
select { border: 1px solid #d0d0d0; background-color: #f8f8f8 } select { border: 1px solid #d0d0d0; background-color: #f8f8f8 }
textarea { width: 99%; font: 100% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif; 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; label { color: #888888; font-size: 11px; margin: 3px 0;
text-transform: uppercase } text-transform: uppercase }
legend { padding-bottom: 2px; padding-right: 3px; color: black } legend { padding-bottom: 2px; padding-right: 3px; color: black }
@ -167,3 +167,5 @@ td.search { padding-top: 8px }
.tabs { position:relative; bottom:-2px } .tabs { position:relative; bottom:-2px }
.tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%; .tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%;
font-weight: bold} font-weight: bold}
.language {color: grey; font-size: 7pt; border: grey 1px solid; padding: 2px;
margin: 0 2px 0 4px}