2013-10-08 09:22:33 +00:00
# -*- coding: utf-8 -*-
2013-10-08 09:26:12 +00:00
"""
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 .
"""
2013-10-10 10:30:05 +00:00
import cStringIO
2013-10-23 14:57:29 +00:00
import datetime
2013-10-08 09:22:33 +00:00
import itertools
2013-10-10 10:30:05 +00:00
import logging
2013-11-27 10:46:38 +00:00
import os
2013-10-10 10:30:05 +00:00
import urllib2
2013-10-24 10:34:01 +00:00
import urlparse
2013-11-27 10:46:38 +00:00
import re
2013-10-08 09:22:33 +00:00
2013-10-08 12:04:56 +00:00
import werkzeug . utils
2013-10-23 14:57:29 +00:00
from dateutil import parser
2013-10-09 13:31:12 +00:00
from lxml import etree , html
2013-10-10 10:30:05 +00:00
from PIL import Image as I
2013-11-27 10:46:38 +00:00
import openerp . modules
2013-10-08 12:04:56 +00:00
2013-12-06 13:30:03 +00:00
import openerp
2013-10-08 09:22:33 +00:00
from openerp . osv import orm , fields
2013-10-23 14:57:29 +00:00
from openerp . tools import ustr , DEFAULT_SERVER_DATE_FORMAT , DEFAULT_SERVER_DATETIME_FORMAT
2013-11-12 14:44:42 +00:00
from openerp . addons . web . http import request
2013-12-16 09:23:43 +00:00
from openerp . addons . base . ir import ir_qweb
2013-10-08 09:22:33 +00:00
2013-10-10 12:26:52 +00:00
REMOTE_CONNECTION_TIMEOUT = 2.5
2013-10-10 10:30:05 +00:00
logger = logging . getLogger ( __name__ )
2013-10-08 09:22:33 +00:00
class QWeb ( orm . AbstractModel ) :
""" QWeb object for rendering stuff in the website context
"""
_name = ' website.qweb '
_inherit = ' ir.qweb '
2013-11-12 14:44:42 +00:00
URL_ATTRS = {
' form ' : ' action ' ,
' a ' : ' href ' ,
}
2014-01-28 20:14:17 +00:00
def add_template ( self , qcontext , name , node ) :
2013-11-12 14:44:42 +00:00
# preprocessing for multilang static urls
2014-01-28 20:14:17 +00:00
if request . website :
2013-11-12 14:44:42 +00:00
for tag , attr in self . URL_ATTRS . items ( ) :
for e in node . getElementsByTagName ( tag ) :
url = e . getAttribute ( attr )
if url :
2014-01-28 20:14:17 +00:00
e . setAttribute ( attr , qcontext . get ( ' url_for ' ) ( url ) )
super ( QWeb , self ) . add_template ( qcontext , name , node )
def render_att_att ( self , element , attribute_name , attribute_value , qwebcontext ) :
att , val = super ( QWeb , self ) . render_att_att ( element , attribute_name , attribute_value , qwebcontext )
if request . website and att == self . URL_ATTRS . get ( element . nodeName ) and isinstance ( val , basestring ) :
val = qwebcontext . get ( ' url_for ' ) ( val )
return att , val
2013-11-12 14:44:42 +00:00
2013-10-08 09:22:33 +00:00
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 ,
2013-10-21 08:28:40 +00:00
source_element , g_att , t_att , qweb_context , context = None ) :
2013-12-18 11:49:01 +00:00
if options is None : options = { }
2013-10-08 09:22:33 +00:00
column = record . _model . _all_columns [ field_name ] . column
2013-12-18 11:49:01 +00:00
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 ) )
2013-10-08 09:22:33 +00:00
return itertools . chain (
super ( Field , self ) . attributes ( cr , uid , field_name , record , options ,
source_element , g_att , t_att ,
2013-10-21 08:28:40 +00:00
qweb_context , context = context ) ,
2013-12-18 11:49:01 +00:00
attrs
2013-10-08 09:22:33 +00:00
)
2013-10-09 13:31:12 +00:00
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 ( ) )
2013-10-11 13:11:47 +00:00
def qweb_object ( self ) :
return self . pool [ ' website.qweb ' ]
2013-10-09 13:31:12 +00:00
class Integer ( orm . AbstractModel ) :
_name = ' website.qweb.field.integer '
_inherit = [ ' website.qweb.field ' ]
value_from_string = int
2013-10-08 09:22:33 +00:00
class Float ( orm . AbstractModel ) :
_name = ' website.qweb.field.float '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.float ' ]
2013-10-22 08:54:50 +00:00
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 , ' . ' ) )
2013-10-09 13:31:12 +00:00
2013-10-23 14:57:29 +00:00
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 )
2013-10-21 15:26:51 +00:00
class Date ( orm . AbstractModel ) :
_name = ' website.qweb.field.date '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.date ' ]
2013-12-05 11:39:08 +00:00
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 ] ) ] )
2013-10-23 14:57:29 +00:00
2013-12-05 11:39:08 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
2013-10-23 14:57:29 +00:00
value = element . text_content ( ) . strip ( )
2013-12-05 11:39:08 +00:00
if not value : return False
2013-10-23 14:57:29 +00:00
2013-12-05 11:39:08 +00:00
datetime . datetime . strptime ( value , DEFAULT_SERVER_DATE_FORMAT )
return value
2013-10-22 07:33:26 +00:00
2013-10-21 15:26:51 +00:00
class DateTime ( orm . AbstractModel ) :
_name = ' website.qweb.field.datetime '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.datetime ' ]
2013-12-05 11:39:08 +00:00
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 )
2013-12-22 23:00:21 +00:00
if value :
value = column . context_timestamp (
cr , uid , timestamp = value , context = context )
value = value . strftime ( openerp . tools . DEFAULT_SERVER_DATETIME_FORMAT )
2013-12-05 11:39:08 +00:00
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 , [
2013-12-22 23:00:21 +00:00
( ' data-oe-original ' , value )
2013-12-05 11:39:08 +00:00
] )
2013-10-23 14:57:29 +00:00
2013-12-05 11:39:08 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
2013-10-23 14:57:29 +00:00
value = element . text_content ( ) . strip ( )
2013-12-05 11:39:08 +00:00
if not value : return False
2013-10-23 14:57:29 +00:00
2013-12-05 11:39:08 +00:00
datetime . datetime . strptime ( value , DEFAULT_SERVER_DATETIME_FORMAT )
return value
2013-10-22 07:33:26 +00:00
2013-10-08 09:22:33 +00:00
class Text ( orm . AbstractModel ) :
_name = ' website.qweb.field.text '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.text ' ]
2013-10-09 13:31:12 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
2014-01-10 15:20:24 +00:00
return html_to_text ( element )
2013-10-09 13:31:12 +00:00
2013-10-08 09:22:33 +00:00
class Selection ( orm . AbstractModel ) :
_name = ' website.qweb.field.selection '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.selection ' ]
2013-10-09 13:31:12 +00:00
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 ) )
2013-10-08 09:22:33 +00:00
class ManyToOne ( orm . AbstractModel ) :
_name = ' website.qweb.field.many2one '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.many2one ' ]
2013-10-09 13:31:12 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
2013-12-18 14:09:17 +00:00
# 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 ' ) )
2014-01-10 15:20:24 +00:00
# FIXME: weird things are going to happen for char-type _rec_name
value = html_to_text ( element )
2013-12-18 14:09:17 +00:00
# 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
2013-10-09 13:31:12 +00:00
2013-10-08 09:22:33 +00:00
class HTML ( orm . AbstractModel ) :
_name = ' website.qweb.field.html '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.html ' ]
2013-10-09 13:31:12 +00:00
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 )
2013-10-08 09:22:33 +00:00
class Image ( orm . AbstractModel ) :
2013-10-08 12:04:56 +00:00
"""
Widget options :
` ` class ` `
set as attribute on the generated < img > tag
"""
2013-10-08 09:22:33 +00:00
_name = ' website.qweb.field.image '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.image ' ]
2013-10-08 12:04:56 +00:00
def to_html ( self , cr , uid , field_name , record , options ,
2013-10-21 08:28:40 +00:00
source_element , t_att , g_att , qweb_context , context = None ) :
2013-10-08 12:04:56 +00:00
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 ,
2013-10-21 08:28:40 +00:00
source_element , t_att , g_att , qweb_context , context = context )
2013-10-08 12:04:56 +00:00
2013-10-21 08:28:40 +00:00
def record_to_html ( self , cr , uid , field_name , record , column , options = None , context = None ) :
2013-11-22 10:41:48 +00:00
if options is None : options = { }
classes = [ ' img ' , ' img-responsive ' ] + options . get ( ' class ' , ' ' ) . split ( )
2013-10-08 12:04:56 +00:00
2013-12-16 09:23:43 +00:00
return ir_qweb . HTMLSafe ( ' <img class= " %s " src= " /website/image?model= %s &field= %s &id= %s " /> ' % (
2013-11-22 10:41:48 +00:00
' ' . join ( itertools . imap ( werkzeug . utils . escape , classes ) ) ,
record . _model . _name ,
2013-12-16 09:23:43 +00:00
field_name , record . id ) )
2013-10-08 12:04:56 +00:00
2013-11-27 10:46:38 +00:00
local_url_re = re . compile ( r ' ^/(?P<module>[^]]+)/static/(?P<rest>.+)$ ' )
2013-10-10 10:30:05 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
url = element . find ( ' img ' ) . get ( ' src ' )
2013-10-24 10:34:01 +00:00
url_object = urlparse . urlsplit ( url )
2013-11-21 12:12:35 +00:00
query = dict ( urlparse . parse_qsl ( url_object . query ) )
2013-12-02 11:17:20 +00:00
if url_object . path == ' /website/image ' :
item = self . pool [ query [ ' model ' ] ] . browse (
2013-10-24 10:34:01 +00:00
cr , uid , int ( query [ ' id ' ] ) , context = context )
2013-12-02 11:17:20 +00:00
return item [ query [ ' field ' ] ]
2013-10-10 10:30:05 +00:00
2013-11-27 10:46:38 +00:00
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 :
2013-12-18 14:09:17 +00:00
return None
2013-11-27 10:46:38 +00:00
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 )
2013-12-18 14:09:17 +00:00
return None
2013-11-27 10:46:38 +00:00
def load_remote_url ( self , url ) :
2013-10-10 10:30:05 +00:00
try :
2013-10-10 12:26:52 +00:00
# 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
2013-10-10 10:30:05 +00:00
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 )
2013-12-18 14:09:17 +00:00
return None
2013-10-10 10:30:05 +00:00
# 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 ' )
2013-10-08 12:20:39 +00:00
class Monetary ( orm . AbstractModel ) :
_name = ' website.qweb.field.monetary '
_inherit = [ ' website.qweb.field ' , ' ir.qweb.field.monetary ' ]
2013-10-11 13:11:47 +00:00
2013-10-11 14:25:28 +00:00
def from_html ( self , cr , uid , model , column , element , context = None ) :
2013-10-22 09:08:43 +00:00
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 , ' . ' ) )
2013-12-05 11:39:08 +00:00
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
2013-12-06 13:30:03 +00:00
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 ] )
2014-01-21 12:39:12 +00:00
val = {
2013-12-06 13:30:03 +00:00
' 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 ,
2014-01-27 13:58:28 +00:00
' city ' : field_browse . city ,
' country_id ' : field_browse . country_id and field_browse . country_id . name_get ( ) [ 0 ] [ 1 ] ,
2013-12-06 13:30:03 +00:00
' email ' : field_browse . email ,
' fields ' : opf ,
' options ' : options
2014-01-21 12:39:12 +00:00
}
2014-01-24 13:23:04 +00:00
html = self . pool [ " ir.ui.view " ] . render ( cr , uid , " website.contact " , val , engine = ' website.qweb ' , context = context ) . decode ( ' utf8 ' )
2013-12-06 13:30:03 +00:00
2013-12-16 09:23:43 +00:00
return ir_qweb . HTMLSafe ( html )
2014-01-10 15:20:24 +00:00
def html_to_text ( element ) :
""" Converts HTML content with HTML-specified line breaks (br, p, div, ...)
in roughly equivalent textual content .
Used to replace and fixup the roundtripping of text and m2o : when using
libxml 2.8 .0 ( but not 2.9 .1 ) and parsing HTML with lxml . html . fromstring
whitespace text nodes ( text nodes composed * solely * of whitespace ) are
stripped out with no recourse , and fundamentally relying on newlines
being in the text ( e . g . inserted during user edition ) is probably poor form
anyway .
- > this utility function collapses whitespace sequences and replaces
nodes by roughly corresponding linebreaks
* p are pre - and post - fixed by 2 newlines
* br are replaced by a single newline
* block - level elements not already mentioned are pre - and post - fixed by
a single newline
ought be somewhat similar ( but much less high - tech ) to aaronsw ' s html2text.
the latter produces full - blown markdown , our text - > html converter only
replaces newlines by < br > elements at this point so we ' re reverting that,
and a few more newline - ish elements in case the user tried to add
newlines / paragraphs into the text field
: param element : lxml . html content
: returns : corresponding pure - text output
"""
# output is a list of str | int. Integers are padding requests (in minimum
# number of newlines). When multiple padding requests, fold them into the
# biggest one
output = [ ]
_wrap ( element , output )
# remove any leading or tailing whitespace, replace sequences of
# (whitespace)\n(whitespace) by a single newline, where (whitespace) is a
# non-newline whitespace in this case
return re . sub (
r ' [ \ t \ r \ f]* \ n[ \ t \ r \ f]* ' ,
' \n ' ,
' ' . join ( _realize_padding ( output ) ) . strip ( ) )
_PADDED_BLOCK = set ( ' p h1 h2 h3 h4 h5 h6 ' . split ( ) )
# https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements minus p
_MISC_BLOCK = set ( (
' address article aside audio blockquote canvas dd dl div figcaption figure '
' footer form header hgroup hr ol output pre section tfoot ul video '
) . split ( ) )
def _collapse_whitespace ( text ) :
""" Collapses sequences of whitespace characters in ``text`` to a single
space
"""
return re . sub ( ' \ s+ ' , ' ' , text )
def _realize_padding ( it ) :
""" Fold and convert padding requests: integers in the output sequence are
requests for at least n newlines of padding . Runs thereof can be collapsed
into the largest requests and converted to newlines .
"""
padding = None
for item in it :
if isinstance ( item , int ) :
padding = max ( padding , item )
continue
if padding :
yield ' \n ' * padding
padding = None
yield item
# leftover padding irrelevant as the output will be stripped
def _wrap ( element , output , wrapper = u ' ' ) :
""" Recursively extracts text from ``element`` (via _element_to_text), and
wraps it all in ` ` wrapper ` ` . Extracted text is added to ` ` output ` `
: type wrapper : basestring | int
"""
output . append ( wrapper )
if element . text :
output . append ( _collapse_whitespace ( element . text ) )
for child in element :
_element_to_text ( child , output )
output . append ( wrapper )
def _element_to_text ( e , output ) :
if e . tag == ' br ' :
output . append ( u ' \n ' )
elif e . tag in _PADDED_BLOCK :
_wrap ( e , output , 2 )
elif e . tag in _MISC_BLOCK :
_wrap ( e , output , 1 )
else :
# inline
_wrap ( e , output )
if e . tail :
output . append ( _collapse_whitespace ( e . tail ) )