Instead of doing a name_get on the edited value and trying to find out an m2o to assign back (which there's pretty much no chance of given there's no autocompletion or anything), alter the m2o record in-place by setting the provided edited value to its _rec_name. Ideally, both features could be supported via more advanced m2o edition widgets which would allow selecting an existing m2o, creating a new m2o record from scratch or (maybe) editing the existing m2o's display_name if possible, somewhat similar to what the form view provides. Without these though, the only action which makes any sense is to edit the user-visible "value" where it is found, as with more normal fields. bzr revid: xmo@openerp.com-20131218140917-4eo2o55yfcumzhov
420 lines
15 KiB
Python
420 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Website-context rendering needs to add some metadata to rendered fields,
|
|
as well as render a few fields differently.
|
|
|
|
Also, adds methods to convert values back to openerp models.
|
|
"""
|
|
|
|
import cStringIO
|
|
import datetime
|
|
import itertools
|
|
import logging
|
|
import os
|
|
import urllib2
|
|
import urlparse
|
|
import re
|
|
|
|
import werkzeug.utils
|
|
from dateutil import parser
|
|
from lxml import etree, html
|
|
from PIL import Image as I
|
|
import openerp.modules
|
|
|
|
import openerp
|
|
from openerp.osv import orm, fields
|
|
from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
|
from openerp.addons.web.http import request
|
|
from openerp.addons.base.ir import ir_qweb
|
|
|
|
REMOTE_CONNECTION_TIMEOUT = 2.5
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class QWeb(orm.AbstractModel):
|
|
""" QWeb object for rendering stuff in the website context
|
|
"""
|
|
_name = 'website.qweb'
|
|
_inherit = 'ir.qweb'
|
|
|
|
URL_ATTRS = {
|
|
'form': 'action',
|
|
'a': 'href',
|
|
'link': 'href',
|
|
'frame': 'src',
|
|
'iframe': 'src',
|
|
'script': 'src',
|
|
}
|
|
|
|
def add_template(self, into, name, node, context):
|
|
# preprocessing for multilang static urls
|
|
if request and 'url_for' in context:
|
|
router = request.httprequest.app.get_db_router(request.db).bind('')
|
|
for tag, attr in self.URL_ATTRS.items():
|
|
for e in node.getElementsByTagName(tag):
|
|
url = e.getAttribute(attr)
|
|
if url:
|
|
try:
|
|
func = router.match(url)[0]
|
|
if func.multilang:
|
|
e.setAttribute(attr, context['url_for'](url))
|
|
except Exception, e:
|
|
pass
|
|
super(QWeb, self).add_template(into, name, node, context)
|
|
|
|
def get_converter_for(self, field_type):
|
|
return self.pool.get(
|
|
'website.qweb.field.' + field_type,
|
|
self.pool['website.qweb.field'])
|
|
|
|
class Field(orm.AbstractModel):
|
|
_name = 'website.qweb.field'
|
|
_inherit = 'ir.qweb.field'
|
|
|
|
def attributes(self, cr, uid, field_name, record, options,
|
|
source_element, g_att, t_att, qweb_context, context=None):
|
|
if options is None: options = {}
|
|
column = record._model._all_columns[field_name].column
|
|
attrs = [('data-oe-translate', 1 if column.translate else 0)]
|
|
|
|
placeholder = options.get('placeholder') \
|
|
or source_element.getAttribute('placeholder') \
|
|
or getattr(column, 'placeholder', None)
|
|
if placeholder:
|
|
attrs.append(('placeholder', placeholder))
|
|
|
|
return itertools.chain(
|
|
super(Field, self).attributes(cr, uid, field_name, record, options,
|
|
source_element, g_att, t_att,
|
|
qweb_context, context=context),
|
|
attrs
|
|
)
|
|
|
|
def value_from_string(self, value):
|
|
return value
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
return self.value_from_string(element.text_content().strip())
|
|
|
|
def qweb_object(self):
|
|
return self.pool['website.qweb']
|
|
|
|
class Integer(orm.AbstractModel):
|
|
_name = 'website.qweb.field.integer'
|
|
_inherit = ['website.qweb.field']
|
|
|
|
value_from_string = int
|
|
|
|
class Float(orm.AbstractModel):
|
|
_name = 'website.qweb.field.float'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.float']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
lang = self.user_lang(cr, uid, context=context)
|
|
|
|
value = element.text_content().strip()
|
|
|
|
return float(value.replace(lang.thousands_sep, '')
|
|
.replace(lang.decimal_point, '.'))
|
|
|
|
|
|
def parse_fuzzy(in_format, value):
|
|
day_first = in_format.find('%d') < in_format.find('%m')
|
|
|
|
if '%y' in in_format:
|
|
year_first = in_format.find('%y') < in_format.find('%d')
|
|
else:
|
|
year_first = in_format.find('%Y') < in_format.find('%d')
|
|
|
|
return parser.parse(value, dayfirst=day_first, yearfirst=year_first)
|
|
|
|
class Date(orm.AbstractModel):
|
|
_name = 'website.qweb.field.date'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.date']
|
|
|
|
def attributes(self, cr, uid, field_name, record, options,
|
|
source_element, g_att, t_att, qweb_context,
|
|
context=None):
|
|
attrs = super(Date, self).attributes(
|
|
cr, uid, field_name, record, options, source_element, g_att, t_att,
|
|
qweb_context, context=None)
|
|
return itertools.chain(attrs, [('data-oe-original', record[field_name])])
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
value = element.text_content().strip()
|
|
if not value: return False
|
|
|
|
datetime.datetime.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
|
|
return value
|
|
|
|
class DateTime(orm.AbstractModel):
|
|
_name = 'website.qweb.field.datetime'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.datetime']
|
|
|
|
def attributes(self, cr, uid, field_name, record, options,
|
|
source_element, g_att, t_att, qweb_context,
|
|
context=None):
|
|
column = record._model._all_columns[field_name].column
|
|
value = record[field_name]
|
|
if isinstance(value, basestring):
|
|
value = datetime.datetime.strptime(
|
|
value, DEFAULT_SERVER_DATETIME_FORMAT)
|
|
value = column.context_timestamp(
|
|
cr, uid, timestamp=value, context=context)
|
|
|
|
attrs = super(DateTime, self).attributes(
|
|
cr, uid, field_name, record, options, source_element, g_att, t_att,
|
|
qweb_context, context=None)
|
|
return itertools.chain(attrs, [
|
|
('data-oe-original', value.strftime(openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT))
|
|
])
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
value = element.text_content().strip()
|
|
if not value: return False
|
|
|
|
datetime.datetime.strptime(value, DEFAULT_SERVER_DATETIME_FORMAT)
|
|
return value
|
|
|
|
class Text(orm.AbstractModel):
|
|
_name = 'website.qweb.field.text'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.text']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
return element.text_content()
|
|
|
|
class Selection(orm.AbstractModel):
|
|
_name = 'website.qweb.field.selection'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.selection']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
value = element.text_content().strip()
|
|
selection = column.reify(cr, uid, model, column, context=context)
|
|
for k, v in selection:
|
|
if isinstance(v, str):
|
|
v = ustr(v)
|
|
if value == v:
|
|
return k
|
|
|
|
raise ValueError(u"No value found for label %s in selection %s" % (
|
|
value, selection))
|
|
|
|
class ManyToOne(orm.AbstractModel):
|
|
_name = 'website.qweb.field.many2one'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.many2one']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
# FIXME: layering violations all the things
|
|
Model = self.pool[element.get('data-oe-model')]
|
|
M2O = self.pool[column._obj]
|
|
field = element.get('data-oe-field')
|
|
id = int(element.get('data-oe-id'))
|
|
value = element.text_content().strip()
|
|
|
|
# if anything blows up, just ignore it and bail
|
|
try:
|
|
# get parent record
|
|
[obj] = Model.read(cr, uid, [id], [field])
|
|
# get m2o record id
|
|
(m2o_id, _) = obj[field]
|
|
# assume _rec_name and write directly to it
|
|
M2O.write(cr, uid, [m2o_id], {
|
|
M2O._rec_name: value
|
|
}, context=context)
|
|
except:
|
|
logger.exception("Could not save %r to m2o field %s of model %s",
|
|
value, field, Model._name)
|
|
|
|
# not necessary, but might as well be explicit about it
|
|
return None
|
|
|
|
class HTML(orm.AbstractModel):
|
|
_name = 'website.qweb.field.html'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.html']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
content = []
|
|
if element.text: content.append(element.text)
|
|
content.extend(html.tostring(child)
|
|
for child in element.iterchildren(tag=etree.Element))
|
|
return '\n'.join(content)
|
|
|
|
|
|
class Image(orm.AbstractModel):
|
|
"""
|
|
Widget options:
|
|
|
|
``class``
|
|
set as attribute on the generated <img> tag
|
|
"""
|
|
_name = 'website.qweb.field.image'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.image']
|
|
|
|
def to_html(self, cr, uid, field_name, record, options,
|
|
source_element, t_att, g_att, qweb_context, context=None):
|
|
assert source_element.nodeName != 'img',\
|
|
"Oddly enough, the root tag of an image field can not be img. " \
|
|
"That is because the image goes into the tag, or it gets the " \
|
|
"hose again."
|
|
|
|
return super(Image, self).to_html(
|
|
cr, uid, field_name, record, options,
|
|
source_element, t_att, g_att, qweb_context, context=context)
|
|
|
|
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
|
if options is None: options = {}
|
|
classes = ['img', 'img-responsive'] + options.get('class', '').split()
|
|
|
|
return ir_qweb.HTMLSafe('<img class="%s" src="/website/image?model=%s&field=%s&id=%s"/>' % (
|
|
' '.join(itertools.imap(werkzeug.utils.escape, classes)),
|
|
record._model._name,
|
|
field_name, record.id))
|
|
|
|
local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
url = element.find('img').get('src')
|
|
|
|
url_object = urlparse.urlsplit(url)
|
|
query = dict(urlparse.parse_qsl(url_object.query))
|
|
if url_object.path == '/website/image':
|
|
item = self.pool[query['model']].browse(
|
|
cr, uid, int(query['id']), context=context)
|
|
return item[query['field']]
|
|
|
|
if self.local_url_re.match(url_object.path):
|
|
return self.load_local_url(url)
|
|
|
|
return self.load_remote_url(url)
|
|
|
|
def load_local_url(self, url):
|
|
match = self.local_url_re.match(urlparse.urlsplit(url).path)
|
|
|
|
rest = match.group('rest')
|
|
for sep in os.sep, os.altsep:
|
|
if sep and sep != '/':
|
|
rest.replace(sep, '/')
|
|
|
|
path = openerp.modules.get_module_resource(
|
|
match.group('module'), 'static', *(rest.split('/')))
|
|
|
|
if not path:
|
|
return None
|
|
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
# force complete image load to ensure it's valid image data
|
|
image = I.open(f)
|
|
image.load()
|
|
f.seek(0)
|
|
return f.read().encode('base64')
|
|
except Exception:
|
|
logger.exception("Failed to load local image %r", url)
|
|
return None
|
|
|
|
def load_remote_url(self, url):
|
|
try:
|
|
# should probably remove remote URLs entirely:
|
|
# * in fields, downloading them without blowing up the server is a
|
|
# challenge
|
|
# * in views, may trigger mixed content warnings if HTTPS CMS
|
|
# linking to HTTP images
|
|
# implement drag & drop image upload to mitigate?
|
|
|
|
req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT)
|
|
# PIL needs a seekable file-like image, urllib result is not seekable
|
|
image = I.open(cStringIO.StringIO(req.read()))
|
|
# force a complete load of the image data to validate it
|
|
image.load()
|
|
except Exception:
|
|
logger.exception("Failed to load remote image %r", url)
|
|
return None
|
|
|
|
# don't use original data in case weird stuff was smuggled in, with
|
|
# luck PIL will remove some of it?
|
|
out = cStringIO.StringIO()
|
|
image.save(out, image.format)
|
|
return out.getvalue().encode('base64')
|
|
|
|
class Monetary(orm.AbstractModel):
|
|
_name = 'website.qweb.field.monetary'
|
|
_inherit = ['website.qweb.field', 'ir.qweb.field.monetary']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
lang = self.user_lang(cr, uid, context=context)
|
|
|
|
value = element.find('span').text.strip()
|
|
|
|
return float(value.replace(lang.thousands_sep, '')
|
|
.replace(lang.decimal_point, '.'))
|
|
|
|
class Duration(orm.AbstractModel):
|
|
_name = 'website.qweb.field.duration'
|
|
_inherit = [
|
|
'ir.qweb.field.duration',
|
|
'website.qweb.field.float',
|
|
]
|
|
|
|
def attributes(self, cr, uid, field_name, record, options,
|
|
source_element, g_att, t_att, qweb_context,
|
|
context=None):
|
|
attrs = super(Duration, self).attributes(
|
|
cr, uid, field_name, record, options, source_element, g_att, t_att,
|
|
qweb_context, context=None)
|
|
return itertools.chain(attrs, [('data-oe-original', record[field_name])])
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
value = element.text_content().strip()
|
|
|
|
# non-localized value
|
|
return float(value)
|
|
|
|
|
|
class RelativeDatetime(orm.AbstractModel):
|
|
_name = 'website.qweb.field.relative'
|
|
_inherit = [
|
|
'ir.qweb.field.relative',
|
|
'website.qweb.field.datetime',
|
|
]
|
|
|
|
# get formatting from ir.qweb.field.relative but edition/save from datetime
|
|
|
|
|
|
class Contact(orm.AbstractModel):
|
|
_name = 'website.qweb.field.contact'
|
|
_inherit = ['website.qweb.field', 'website.qweb.field.many2one']
|
|
|
|
def from_html(self, cr, uid, model, column, element, context=None):
|
|
# FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
|
|
divs = element.xpath(".//div")
|
|
for div in divs:
|
|
if div != divs[0]:
|
|
div.getparent().remove(div)
|
|
return super(Contact, self).from_html(cr, uid, model, column, element, context=context)
|
|
|
|
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
|
opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
|
|
|
|
if not getattr(record, field_name):
|
|
return None
|
|
|
|
id = getattr(record, field_name).id
|
|
field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
|
|
value = werkzeug.utils.escape( field_browse.name_get()[0][1] )
|
|
|
|
IMD = self.pool["ir.model.data"]
|
|
model, id = IMD.get_object_reference(cr, uid, "website", "contact")
|
|
view = self.pool["ir.ui.view"].browse(cr, uid, id, context=context)
|
|
|
|
html = view.render({
|
|
'name': value.split("\n")[0],
|
|
'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
|
|
'phone': field_browse.phone,
|
|
'mobile': field_browse.mobile,
|
|
'fax': field_browse.fax,
|
|
'email': field_browse.email,
|
|
'fields': opf,
|
|
'options': options
|
|
}, engine='website.qweb', context=context)
|
|
|
|
return ir_qweb.HTMLSafe(html)
|