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 ' ,
' 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 )
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-10-08 09:22:33 +00:00
column = record . _model . _all_columns [ field_name ] . column
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-10-08 09:22:33 +00:00
[ ( ' data-oe-translate ' , 1 if column . translate else 0 ) ]
)
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 )
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 ) )
] )
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 ) :
return element . text_content ( )
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 ) :
# 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?
matches = self . pool [ column . _obj ] . name_search (
cr , uid , name = element . text_content ( ) . strip ( ) , context = context )
# FIXME: no match? More than 1 match?
assert len ( matches ) == 1
return matches [ 0 ] [ 0 ]
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 :
return False
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 False
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 )
return False
# 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 ] )
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 )
2013-12-16 09:23:43 +00:00
return ir_qweb . HTMLSafe ( html )