[gen] Added ref.render == 'titles' = a way to render linked objects as a simple list of comma-separated, non clickable titles; bugfix in inline-edit of string XHTML fields.

This commit is contained in:
Gaetan Delannay 2014-04-30 21:08:42 +02:00
parent 4d78996938
commit 14f85509e1
15 changed files with 106 additions and 64 deletions

View file

@ -40,6 +40,7 @@ class Field:
cssFiles = {} cssFiles = {}
jsFiles = {} jsFiles = {}
dLayouts = 'lrv-d-f' dLayouts = 'lrv-d-f'
hLayouts = 'lhrv-f'
wLayouts = Table('lrv-f') wLayouts = Table('lrv-f')
# Render a field. Optional vars: # Render a field. Optional vars:

View file

@ -78,7 +78,7 @@ class Pod(Field):
<td> <td>
<a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \ <a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \
q(info.template), q(fmt), q('unfreeze'))" q(info.template), q(fmt), q('unfreeze'))"
class="podName">:_('unfreezeField')</a> class="smaller">:_('unfreezeField')</a>
</td> </td>
<td align="center"><img src=":url('unfreeze')"/></td> <td align="center"><img src=":url('unfreeze')"/></td>
</tr> </tr>
@ -87,7 +87,7 @@ class Pod(Field):
<td> <td>
<a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \ <a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \
q(info.template), q(fmt), q('freeze'))" q(info.template), q(fmt), q('freeze'))"
class="podName">:_('freezeField')</a> class="smaller">:_('freezeField')</a>
</td> </td>
<td align="center"><img src=":url('freeze')"/></td> <td align="center"><img src=":url('freeze')"/></td>
</tr> </tr>
@ -96,7 +96,7 @@ class Pod(Field):
<td> <td>
<a onclick=":'uploadPod(%s,%s,%s,%s)' % (q(uid), q(name), \ <a onclick=":'uploadPod(%s,%s,%s,%s)' % (q(uid), q(name), \
q(info.template), q(fmt))" q(info.template), q(fmt))"
class="podName">:_('uploadField')</a> class="smaller">:_('uploadField')</a>
</td> </td>
<td align="center"><img src=":url('upload')"/></td> <td align="center"><img src=":url('upload')"/></td>
</tr> </tr>
@ -107,7 +107,7 @@ class Pod(Field):
template. For a single template, the field label already does template. For a single template, the field label already does
the job. --> the job. -->
<td if="len(field.template) &gt; 1" <td if="len(field.template) &gt; 1"
class="podName">:field.getTemplateName(obj, info.template)</td> class="smaller">:field.getTemplateName(obj, info.template)</td>
</tr> </tr>
</table> </table>
</td> </td>

View file

@ -195,24 +195,10 @@ class Ref(Field):
# PX that displays referred objects as a list. # PX that displays referred objects as a list.
pxViewList = Px(''' pxViewList = Px('''
<!-- Display a simplified widget if at most 1 referenced object. --> <!-- No object at all -->
<table if="atMostOneRef"> <div if="not objects" class="smaller">:_('no_ref')</div>
<tr valign="top">
<!-- If there is no object -->
<x if="not objects">
<td class="discreet">:_('no_ref')</td>
<td>:field.pxAdd</td>
</x>
<!-- If there is an object -->
<x if="objects">
<td for="tied in objects"
var2="includeShownInfo=True">:field.pxObjectTitle</td>
</x>
</tr>
</table>
<!-- Display a table in all other cases --> <x if="objects">
<x if="not atMostOneRef">
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px"> <div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
<span if="subLabel" class="discreet">:_(subLabel)</span> <span if="subLabel" class="discreet">:_(subLabel)</span>
(<span class="discreet">:totalNumber</span>) (<span class="discreet">:totalNumber</span>)
@ -231,10 +217,7 @@ class Ref(Field):
<!-- (Top) navigation --> <!-- (Top) navigation -->
<x>:tool.pxNavigate</x> <x>:tool.pxNavigate</x>
<!-- No object is present --> <!-- Linked objects -->
<p class="discreet" if="not objects">:_('no_ref')</p>
<!-- Show forward or backward reference(s) -->
<table if="objects" class=":not innerRef and 'list' or ''" <table if="objects" class=":not innerRef and 'list' or ''"
width=":innerRef and '100%' or field.layouts['view'].width" width=":innerRef and '100%' or field.layouts['view'].width"
var2="columns=ztool.getColumnsSpecifiers(tiedClassName, \ var2="columns=ztool.getColumnsSpecifiers(tiedClassName, \
@ -312,7 +295,6 @@ class Ref(Field):
not field.isBack and zobj.allows(field.writePermission); not field.isBack and zobj.allows(field.writePermission);
mayUnlink=False; mayUnlink=False;
showPlusIcon=False; showPlusIcon=False;
atMostOneRef=False;
navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \ navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \
(q(ajaxHookId), q(zobj.absolute_url()), \ (q(ajaxHookId), q(zobj.absolute_url()), \
q(field.name), q(innerRef)); q(field.name), q(innerRef));
@ -356,14 +338,19 @@ class Ref(Field):
</td> </td>
</tr></table>''') </tr></table>''')
# Simplified widget showing comma-separated not-clickable object titles.
pxViewTitles = Px('''<span class="smaller"
var2="titles=[o.title for o in objects]">:', '.join(titles) or \
_('no_ref')</span>''')
# PX that displays referred objects through this field. In mode link="list", # PX that displays referred objects through this field. In mode link="list",
# if, in the request, key "scope" is present and holds value "objs", the # if, in the request, key "scope" is present and holds value "objs", the
# pick list (containing possible values) will not be rendered. # pick list (containing possible values) will not be rendered.
pxView = Px(''' pxView = Px('''
<x var="innerRef=req.get('innerRef', False) == 'True'; <x var="innerRef=req.get('innerRef', False) == 'True';
ajaxHookId='%s_%s_objs' % (zobj.id, field.name); ajaxHookId='%s_%s_objs' % (zobj.id, field.name);
render=render|'list';
layoutType=layoutType|'view'; layoutType=layoutType|'view';
render=field.getRenderMode(layoutType);
linkList=field.link == 'list'; linkList=field.link == 'list';
renderAll=req.get('scope') != 'objs'; renderAll=req.get('scope') != 'objs';
inPickList=False; inPickList=False;
@ -381,7 +368,6 @@ class Ref(Field):
mayUnlink=not isBack and canWrite and \ mayUnlink=not isBack and canWrite and \
field.getAttribute(zobj, 'unlink'); field.getAttribute(zobj, 'unlink');
showPlusIcon=field.mayAdd(zobj); showPlusIcon=field.mayAdd(zobj);
atMostOneRef=(field.multiplicity[1]==1) and (len(objects)&lt;=1);
addConfirmMsg=field.addConfirm and \ addConfirmMsg=field.addConfirm and \
_('%s_addConfirm' % field.labelId) or ''; _('%s_addConfirm' % field.labelId) or '';
navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \ navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \
@ -395,10 +381,6 @@ class Ref(Field):
(layoutType != 'cell'); (layoutType != 'cell');
checkboxes=checkboxesEnabled and (totalNumber &gt; 1); checkboxes=checkboxesEnabled and (totalNumber &gt; 1);
showSubTitles=req.get('showSubTitles', 'true') == 'true'"> showSubTitles=req.get('showSubTitles', 'true') == 'true'">
<!-- 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). -->
<!-- JS tables storing checkbox statuses if checkboxes are enabled --> <!-- JS tables storing checkbox statuses if checkboxes are enabled -->
<script if="checkboxesEnabled and renderAll" <script if="checkboxesEnabled and renderAll"
type="text/javascript">:field.getCbJsInit(zobj)</script> type="text/javascript">:field.getCbJsInit(zobj)</script>
@ -410,12 +392,11 @@ class Ref(Field):
<div if="renderAll" id=":ajaxHookId">:field.pxViewList</div> <div if="renderAll" id=":ajaxHookId">:field.pxViewList</div>
<x if="not renderAll">:field.pxViewList</x> <x if="not renderAll">:field.pxViewList</x>
</x> </x>
<x if="render == 'menus'">:field.pxViewMenus</x> <x if="render in ('menus','titles')">:getattr(field, 'pxView%s' % \
render.capitalize())</x>
</x>''') </x>''')
# The "menus" render mode is only applicable in "cell", not in "view". pxCell = pxView
pxCell = Px('''<x var="render=field.render">:field.pxView</x>''')
pxEdit = Px(''' pxEdit = Px('''
<select if="field.link" <select if="field.link"
var2="objects=field.getPossibleValues(zobj); var2="objects=field.getPossibleValues(zobj);
@ -579,7 +560,9 @@ class Ref(Field):
# Note that render mode "menus" will only be applied in "cell" layouts. # Note that render mode "menus" will only be applied in "cell" layouts.
# Indeed, we need to keep the "list" rendering in the "view" layout # Indeed, we need to keep the "list" rendering in the "view" layout
# because the "menus" rendering is minimalist and does not allow to # because the "menus" rendering is minimalist and does not allow to
# perform all operations on Ref objects (add, move, delete, edit...). # perform all operations on linked objects (add, move, delete, edit...);
# - "titles" renders a list of comma-separated, not-even-clickable,
# titles.
self.render = render self.render = render
# If render is 'menus', 2 methods must be provided. # If render is 'menus', 2 methods must be provided.
# "menuIdMethod" will be called, with every linked object as single arg, # "menuIdMethod" will be called, with every linked object as single arg,
@ -1054,6 +1037,12 @@ class Ref(Field):
reverse = rq.get('reverse') == 'True' reverse = rq.get('reverse') == 'True'
obj.appy().sort(self.name, sortKey=sortKey, reverse=reverse) obj.appy().sort(self.name, sortKey=sortKey, reverse=reverse)
def getRenderMode(self, layoutType):
'''Gets the render mode, determined by self.render and some
exceptions.'''
if (layoutType == 'view') and (self.render == 'menus'): return 'list'
return self.render
def onUiRequest(self, obj, rq): def onUiRequest(self, obj, rq):
'''This method is called when an action tied to this Ref field is '''This method is called when an action tied to this Ref field is
triggered from the user interface (link, unlink, link_many, triggered from the user interface (link, unlink, link_many,

View file

@ -97,12 +97,13 @@ class String(Field):
<!-- Unformatted text --> <!-- Unformatted text -->
<x if="value and (fmt == 1)">::zobj.formatText(value, format='html')</x> <x if="value and (fmt == 1)">::zobj.formatText(value, format='html')</x>
<!-- XHTML text --> <!-- XHTML text -->
<x if="value and (fmt == 2)"> <x if="fmt == 2">
<div if="not mayAjaxEdit" class="xhtml">::value</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.UID(), name)">::value</div> id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj)"></script> <script if="mayAjaxEdit">::field.getJsInlineInit(zobj)</script>
</x> </x>
<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>''')
@ -298,7 +299,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', persist=True, transform='none',
styles=('p','h1','h2','h3','h4'), allowImageUpload=True, styles=('p','h1','h2','h3','h4'), allowImageUpload=True,
inlineEdit=False): spellcheck=False, contentLanguage=None, 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
@ -310,6 +311,10 @@ class String(Field):
self.styles = styles self.styles = styles
# When format is XHTML, do we allow the user to upload images in it ? # When format is XHTML, do we allow the user to upload images in it ?
self.allowImageUpload = allowImageUpload 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
# 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
@ -409,6 +414,12 @@ class String(Field):
type='warning') type='warning')
Field.store(self, obj, value) 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, rq['fieldContent'])
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
between successive versions.''' between successive versions.'''
@ -656,27 +667,52 @@ class String(Field):
generator).''' generator).'''
return self.getCaptchaChallenge({})['text'] return self.getCaptchaChallenge({})['text']
def getJsInit(self, obj): ckLanguages = {'en': 'en_US', 'pt': 'pt_BR', 'da': 'da_DK', 'nl': 'nl_NL',
'''Gets the Javascript init code for displaying a rich editor for this 'fi': 'fi_FI', 'fr': 'fr_FR', 'de': 'de_DE', 'el': 'el_GR',
field (rich field only).''' 'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES',
# Define the attributes that will initialize the ckeditor instance for 'sv': 'sv_SE'}
# this field. 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]
return 'en_US'
def getCkParams(self, obj):
'''Gets the base params to set on a rich text field.'''
ckAttrs = {'toolbar': 'Appy', ckAttrs = {'toolbar': 'Appy',
'format_tags': '%s' % ';'.join(self.styles)} 'format_tags': ';'.join(self.styles),
'scayt_sLang': self.getCkLanguage()}
if self.width: ckAttrs['width'] = self.width if self.width: ckAttrs['width'] = self.width
if self.spellcheck: ckAttrs['scayt_autoStartup'] = True
if self.allowImageUpload: if self.allowImageUpload:
ckAttrs['filebrowserUploadUrl'] = '%s/upload' % obj.absolute_url() ckAttrs['filebrowserUploadUrl'] = '%s/upload' % obj.absolute_url()
ck = [] ck = []
for k, v in ckAttrs.iteritems(): for k, v in ckAttrs.iteritems():
if isinstance(v, int): sv = str(v) if isinstance(v, int): sv = str(v)
if isinstance(v, bool): sv = str(v).lower()
else: sv = '"%s"' % v else: sv = '"%s"' % v
ck.append('%s: %s' % (k, sv)) ck.append('%s: %s' % (k, sv))
return 'CKEDITOR.replace("%s", {%s})' % (self.name, ', '.join(ck)) return ', '.join(ck)
def getJsInit(self, obj):
'''Gets the Javascript init code for displaying a rich editor for this
field (rich field only).'''
return 'CKEDITOR.replace("%s", {%s})' % \
(self.name, self.getCkParams(obj))
def getJsInlineInit(self, obj): def getJsInlineInit(self, obj):
'''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).'''
uid = obj.UID() uid = obj.id
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,
obj.absolute_url())
return "CKEDITOR.disableAutoInline = true;\n" \ return "CKEDITOR.disableAutoInline = true;\n" \
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \ "CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
"function( event ) { var data = event.editor.getData(); " \ "function( event ) { var data = event.editor.getData(); " \

View file

@ -12,7 +12,7 @@ class Protos:
# List of attributes that can't be given to a Type constructor # List of attributes that can't be given to a Type constructor
notInit = ('id', 'type', 'pythonType', 'slaves', 'isSelect', 'hasLabel', notInit = ('id', 'type', 'pythonType', 'slaves', 'isSelect', 'hasLabel',
'hasDescr', 'hasHelp', 'required', 'filterable', 'validable', 'hasDescr', 'hasHelp', 'required', 'filterable', 'validable',
'isBack', 'pageName', 'masterName') 'isBack', 'pageName', 'masterName', 'renderLabel')
@classmethod @classmethod
def get(self, appyType): def get(self, appyType):
'''Returns a prototype instance for p_appyType.''' '''Returns a prototype instance for p_appyType.'''

View file

@ -79,7 +79,7 @@ msgstr ""
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "" msgstr ""
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "" msgstr ""

View file

@ -79,7 +79,7 @@ msgstr ""
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "" msgstr ""
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "" msgstr ""

View file

@ -79,7 +79,7 @@ msgstr "Hier müssen Sie Elemente auswählen."
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "Sie haben zuviele Elemente ausgewählt." msgstr "Sie haben zuviele Elemente ausgewählt."
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "Kein Element" msgstr "Kein Element"

View file

@ -80,7 +80,7 @@ msgstr "You must choose more elements here."
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "Too much elements are selected here." msgstr "Too much elements are selected here."
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "No object." msgstr "No object."

View file

@ -79,7 +79,7 @@ msgstr "Debe elegir más elementos aquí."
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "Demasiados elementos son seleccionados aquí." msgstr "Demasiados elementos son seleccionados aquí."
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "Ningún elemento." msgstr "Ningún elemento."

View file

@ -80,9 +80,9 @@ msgstr "Vous devez choisir plus d'éléments ici."
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "Trop d'éléments sont sélectionnés ici." msgstr "Trop d'éléments sont sélectionnés ici."
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "Aucun élément." msgstr "-"
#. Default: "Add a new one" #. Default: "Add a new one"
msgid "add_ref" msgid "add_ref"

View file

@ -79,9 +79,9 @@ msgstr "Qui deve scegliere un maggior numero di elementi"
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "Un numero eccessivo di elementi sono scelti" msgstr "Un numero eccessivo di elementi sono scelti"
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "Nessun elemento" msgstr "-"
#. Default: "Add a new one" #. Default: "Add a new one"
msgid "add_ref" msgid "add_ref"

View file

@ -79,9 +79,9 @@ msgstr "U moet hier meerdere elementen selecteren."
msgid "max_ref_violated" msgid "max_ref_violated"
msgstr "U hebt teveel elementen geselecteerd." msgstr "U hebt teveel elementen geselecteerd."
#. Default: "No object." #. Default: "-"
msgid "no_ref" msgid "no_ref"
msgstr "Geen element." msgstr "-"
#. Default: "Add a new one" #. Default: "Add a new one"
msgid "add_ref" msgid "add_ref"

View file

@ -100,7 +100,7 @@ td.search { padding-top: 8px }
border: 1px solid grey; box-shadow: 2px 2px 2px #888888} border: 1px solid grey; box-shadow: 2px 2px 2px #888888}
.dropdown { display:none; position: absolute; border: 1px solid #cccccc; .dropdown { display:none; position: absolute; border: 1px solid #cccccc;
background-color: white; padding: 3px 4px 0; font-size: 8pt; background-color: white; padding: 3px 4px 0; font-size: 8pt;
font-weight: normal } font-weight: normal; z-index: 2 }
.dropdownMenu { cursor: pointer; padding-right: 4px; font-size: 93% } .dropdownMenu { cursor: pointer; padding-right: 4px; font-size: 93% }
.dropdown a:hover { text-decoration: underline } .dropdown a:hover { text-decoration: underline }
.list { margin-bottom: 3px } .list { margin-bottom: 3px }
@ -155,7 +155,7 @@ td.search { padding-top: 8px }
.homeTable th { padding-top: 5px; font-size: 105% } .homeTable th { padding-top: 5px; font-size: 105% }
.first { margin-top: 0px } .first { margin-top: 0px }
.error { margin: 5px } .error { margin: 5px }
.podName { font-size: 95% } .smaller { font-size: 95% }
.podTable { margin-left: 15px } .podTable { margin-left: 15px }
.cbCell { width: 10px; text-align: center} .cbCell { width: 10px; text-align: center}
.tabs { position:relative; bottom:-2px } .tabs { position:relative; bottom:-2px }

View file

@ -275,6 +275,22 @@ function askField(hookId, objectUrl, layoutType, showChanges, masterValues,
askAjaxChunk(hookId, 'GET', objectUrl, px, params, null, evalInnerScripts); askAjaxChunk(hookId, 'GET', objectUrl, px, params, null, evalInnerScripts);
} }
function doInlineSave(objectUid, name, objectUrl, content){
/* Ajax-saves p_content of field named p_name on object whose id is
p_objectUid and whose URL is p_objectUrl. Asks a confirmation before
doing it. */
var doIt = confirm('Do it?');
var params = {'action': 'storeFromAjax', 'layoutType': 'view'};
var hook = null;
if (!doIt) {
params['cancel'] = 'True';
hook = objectUid + '_' + name;
}
else { params['fieldContent'] = encodeURIComponent(content) }
askAjaxChunk(hook, 'POST', objectUrl, name + ':pxRender', params, null,
evalInnerScripts);
}
// Used by checkbox widgets for having radio-button-like behaviour. // Used by checkbox widgets for having radio-button-like behaviour.
function toggleCheckbox(visibleCheckbox, hiddenBoolean) { function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
vis = document.getElementById(visibleCheckbox); vis = document.getElementById(visibleCheckbox);