[MIG] website: migration to new api

Migration of website module to new API.
The tricky phase is ir_http.py : we need to use `request.env`
only when the authenfication phase is done. Normally by
calling `super` of `_dispatch` method, but website module
required it to be done before. This is important since
`env`is a lazy property of `request` object.

Some hack were kept since this commit is a migration ('RequestUID'
in ir.http, ...)

Some docstrings were added.
This commit is contained in:
Ravi Gadhia
2016-08-04 12:02:14 +02:00
committed by Jérome Maes
parent 7c20b4785b
commit fd09ddb6f3
23 changed files with 761 additions and 759 deletions
+3
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import backend import backend
import main import main
+3 -8
View File
@@ -1,13 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from openerp import http # Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.http import request
from dateutil.relativedelta import relativedelta
from datetime import datetime, date, timedelta
from math import floor
import time
import operator
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT from odoo import http
from odoo.http import request
class WebsiteBackend(http.Controller): class WebsiteBackend(http.Controller):
+109 -125
View File
@@ -1,22 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime import datetime
from itertools import islice from itertools import islice
import json import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import logging import logging
import re import re
import werkzeug.utils
import urllib2 import urllib2
import werkzeug.utils
import werkzeug.wrappers import werkzeug.wrappers
import openerp import odoo
from openerp.addons.web.controllers.main import WebClient, Binary from odoo import http
from openerp import http from odoo import fields
from openerp.http import request from odoo.http import request
from openerp.osv.orm import browse_record from odoo.osv.orm import browse_record
from openerp.addons.website.models.website import slug
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import WebClient, Binary, Home
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -34,8 +36,8 @@ class QueryURL(object):
def __call__(self, path=None, path_args=None, **kw): def __call__(self, path=None, path_args=None, **kw):
path = path or self.path path = path or self.path
for k, v in self.args.items(): for key, value in self.args.items():
kw.setdefault(k, v) kw.setdefault(key, value)
path_args = set(path_args or []).union(self.path_args) path_args = set(path_args or []).union(self.path_args)
paths, fragments = [], [] paths, fragments = [], []
for key, value in kw.items(): for key, value in kw.items():
@@ -56,40 +58,40 @@ class QueryURL(object):
return path return path
class Website(openerp.addons.web.controllers.main.Home): class Website(Home):
#------------------------------------------------------
# View
#------------------------------------------------------
@http.route('/', type='http', auth="public", website=True) @http.route('/', type='http', auth="public", website=True)
def index(self, **kw): def index(self, **kw):
page = 'homepage' page = 'homepage'
try: main_menu = request.env.ref('website.main_menu', raise_if_not_found=False)
main_menu = request.env.ref('website.main_menu') if main_menu:
except Exception:
pass
else:
first_menu = main_menu.child_id and main_menu.child_id[0] first_menu = main_menu.child_id and main_menu.child_id[0]
if first_menu: if first_menu:
if first_menu.url and (not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url == '/'))): if first_menu.url and (not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url == '/'))):
return request.redirect(first_menu.url) return request.redirect(first_menu.url)
if first_menu.url and first_menu.url.startswith('/page/'): if first_menu.url and first_menu.url.startswith('/page/'):
return request.registry['ir.http'].reroute(first_menu.url) return request.env['ir.http'].reroute(first_menu.url)
return self.page(page) return self.page(page)
#------------------------------------------------------ #------------------------------------------------------
# Login - overwrite of the web login so that regular users are redirected to the backend # Login - overwrite of the web login so that regular users are redirected to the backend
# while portal users are redirected to the frontend by default # while portal users are redirected to the frontend by default
#------------------------------------------------------ #------------------------------------------------------
@http.route(website=True, auth="public") @http.route(website=True, auth="public")
def web_login(self, redirect=None, *args, **kw): def web_login(self, redirect=None, *args, **kw):
r = super(Website, self).web_login(redirect=redirect, *args, **kw) response = super(Website, self).web_login(redirect=redirect, *args, **kw)
if not redirect and request.params['login_success']: if not redirect and request.params['login_success']:
if request.registry['res.users'].has_group(request.cr, request.uid, 'base.group_user'): if request.env.user.has_group('base.group_user'):
redirect = '/web?' + request.httprequest.query_string redirect = '/web?' + request.httprequest.query_string
else: else:
redirect = '/' redirect = '/'
return http.redirect_with_hash(redirect) return http.redirect_with_hash(redirect)
return r return response
#------------------------------------------------------
# Business
#------------------------------------------------------
@http.route('/website/lang/<lang>', type='http', auth="public", website=True, multilang=False) @http.route('/website/lang/<lang>', type='http', auth="public", website=True, multilang=False)
def change_lang(self, lang, r='/', **kwargs): def change_lang(self, lang, r='/', **kwargs):
@@ -104,7 +106,7 @@ class Website(openerp.addons.web.controllers.main.Home):
def page(self, page, **opt): def page(self, page, **opt):
values = { values = {
'path': page, 'path': page,
'deletable': True, # used to add 'delete this page' in content menu 'deletable': True, # used to add 'delete this page' in content menu
} }
# /page/website.XXX --> /page/XXX # /page/website.XXX --> /page/XXX
if page.startswith('website.'): if page.startswith('website.'):
@@ -120,7 +122,7 @@ class Website(openerp.addons.web.controllers.main.Home):
values.pop('deletable') values.pop('deletable')
page = 'website.page_404' page = 'website.page_404'
else: else:
return request.registry['ir.http']._handle_exception(e, 404) return request.env['ir.http']._handle_exception(e, 404)
return request.render(page, values) return request.render(page, values)
@@ -135,35 +137,32 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route('/sitemap.xml', type='http', auth="public", website=True) @http.route('/sitemap.xml', type='http', auth="public", website=True)
def sitemap_xml_index(self): def sitemap_xml_index(self):
cr, uid, context = request.cr, openerp.SUPERUSER_ID, request.context Attachment = request.env['ir.attachment'].sudo()
ira = request.registry['ir.attachment'] View = request.env['ir.ui.view'].sudo()
iuv = request.registry['ir.ui.view'] mimetype = 'application/xml;charset=utf-8'
mimetype ='application/xml;charset=utf-8'
content = None content = None
def create_sitemap(url, content): def create_sitemap(url, content):
return ira.create(cr, uid, dict( return Attachment.create({
datas=content.encode('base64'), 'datas': content.encode('base64'),
mimetype=mimetype, 'mimetype': mimetype,
type='binary', 'type': 'binary',
name=url, 'name': url,
url=url, 'url': url,
), context=context) })
sitemap = ira.search_read(cr, uid, [('url', '=' , '/sitemap.xml'), ('type', '=', 'binary')], ('datas', 'create_date'), context=context) sitemap = Attachment.search([('url', '=', '/sitemap.xml'), ('type', '=', 'binary')], limit=1)
if sitemap: if sitemap:
# Check if stored version is still valid # Check if stored version is still valid
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT create_date = fields.Datetime.from_string(sitemap.create_date)
create_date = datetime.datetime.strptime(sitemap[0]['create_date'], server_format)
delta = datetime.datetime.now() - create_date delta = datetime.datetime.now() - create_date
if delta < SITEMAP_CACHE_TIME: if delta < SITEMAP_CACHE_TIME:
content = sitemap[0]['datas'].decode('base64') content = sitemap.datas.decode('base64')
if not content: if not content:
# Remove all sitemaps in ir.attachments as we're going to regenerated them # Remove all sitemaps in ir.attachments as we're going to regenerated them
sitemap_ids = ira.search(cr, uid, [('url', '=like' , '/sitemap%.xml'), ('type', '=', 'binary')], context=context) sitemaps = Attachment.search([('url', '=like', '/sitemap%.xml'), ('type', '=', 'binary')])
if sitemap_ids: sitemaps.unlink()
ira.unlink(cr, uid, sitemap_ids, context=context)
pages = 0 pages = 0
locs = request.website.sudo(user=request.website.user_id.id).enumerate_pages() locs = request.website.sudo(user=request.website.user_id.id).enumerate_pages()
@@ -172,23 +171,27 @@ class Website(openerp.addons.web.controllers.main.Home):
'locs': islice(locs, 0, LOC_PER_SITEMAP), 'locs': islice(locs, 0, LOC_PER_SITEMAP),
'url_root': request.httprequest.url_root[:-1], 'url_root': request.httprequest.url_root[:-1],
} }
urls = iuv.render_template(cr, uid, 'website.sitemap_locs', values, context=context) urls = View.render_template('website.sitemap_locs', values)
if urls.strip(): if urls.strip():
content = iuv.render_template(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context) content = View.render_template('website.sitemap_xml', {'content': urls})
pages += 1 pages += 1
last = create_sitemap('/sitemap-%d.xml' % pages, content) last_sitemap = create_sitemap('/sitemap-%d.xml' % pages, content)
else: else:
break break
if not pages: if not pages:
return request.not_found() return request.not_found()
elif pages == 1: elif pages == 1:
ira.write(cr, uid, last, dict(url="/sitemap.xml", name="/sitemap.xml"), context=context) last_sitemap.write({
'url': "/sitemap.xml",
'name': "/sitemap.xml"
})
else: else:
# Sitemaps must be split in several smaller files with a sitemap index # Sitemaps must be split in several smaller files with a sitemap index
content = iuv.render_template(cr, uid, 'website.sitemap_index_xml', dict( content = View.render_template('website.sitemap_index_xml', {
pages=range(1, pages + 1), 'pages': range(1, pages + 1),
url_root=request.httprequest.url_root, 'url_root': request.httprequest.url_root,
), context=context) })
create_sitemap('/sitemap.xml', content) create_sitemap('/sitemap.xml', content)
return request.make_response(content, [('Content-Type', mimetype)]) return request.make_response(content, [('Content-Type', mimetype)])
@@ -198,34 +201,34 @@ class Website(openerp.addons.web.controllers.main.Home):
try: try:
request.website.get_template('website.info').name request.website.get_template('website.info').name
except Exception, e: except Exception, e:
return request.registry['ir.http']._handle_exception(e, 404) return request.env['ir.http']._handle_exception(e, 404)
irm = request.env['ir.module.module'].sudo() Module = request.env['ir.module.module'].sudo()
apps = irm.search([('state','=','installed'),('application','=',True)]) apps = Module.search([('state', '=', 'installed'), ('application', '=', True)])
modules = irm.search([('state','=','installed'),('application','=',False)]) modules = Module.search([('state', '=', 'installed'), ('application', '=', False)])
values = { values = {
'apps': apps, 'apps': apps,
'modules': modules, 'modules': modules,
'version': openerp.service.common.exp_version() 'version': odoo.service.common.exp_version()
} }
return request.render('website.info', values) return request.render('website.info', values)
#------------------------------------------------------ #------------------------------------------------------
# Edit # Edit
#------------------------------------------------------ #------------------------------------------------------
@http.route('/website/add/<path:path>', type='http', auth="user", website=True) @http.route('/website/add/<path:path>', type='http', auth="user", website=True)
def pagenew(self, path, noredirect=False, add_menu=None, template=False): def pagenew(self, path, noredirect=False, add_menu=None, template=False):
if template: if template:
xml_id = request.registry['website'].new_page(request.cr, request.uid, path, template=template, context=request.context) xml_id = request.env['website'].new_page(path, template=template)
else: else:
xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context) xml_id = request.env['website'].new_page(path)
if add_menu: if add_menu:
request.registry['website.menu'].create( request.env['website.menu'].create({
request.cr, request.uid, { 'name': path,
'name': path, 'url': "/page/" + xml_id[8:],
'url': "/page/" + xml_id[8:], 'parent_id': request.website.menu_id.id,
'parent_id': request.website.menu_id.id, 'website_id': request.website.id,
'website_id': request.website.id, })
}, context=request.context)
# Reverse action in order to allow shortcut for /page/<website_xml_id> # Reverse action in order to allow shortcut for /page/<website_xml_id>
url = "/page/" + re.sub(r"^website\.", '', xml_id) url = "/page/" + re.sub(r"^website\.", '', xml_id)
@@ -242,7 +245,7 @@ class Website(openerp.addons.web.controllers.main.Home):
templates = request.httprequest.form.getlist('templates') templates = request.httprequest.form.getlist('templates')
modules_to_update = [] modules_to_update = []
for temp_id in templates: for temp_id in templates:
view = request.registry['ir.ui.view'].browse(request.cr, request.uid, int(temp_id), context=request.context) view = request.env['ir.ui.view'].browse(int(temp_id))
if view.page: if view.page:
continue continue
view.model_data_id.write({ view.model_data_id.write({
@@ -252,8 +255,7 @@ class Website(openerp.addons.web.controllers.main.Home):
modules_to_update.append(view.model_data_id.module) modules_to_update.append(view.model_data_id.module)
if modules_to_update: if modules_to_update:
module_obj = request.env['ir.module.module'].sudo() modules = request.env['ir.module.module'].sudo().search([('name', 'in', modules_to_update)])
modules = module_obj.search([('name', 'in', modules_to_update)])
if modules: if modules:
modules.button_immediate_upgrade() modules.button_immediate_upgrade()
return request.redirect(redirect) return request.redirect(redirect)
@@ -261,13 +263,11 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route('/website/customize_template_get', type='json', auth='user', website=True) @http.route('/website/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, key, full=False, bundles=False): def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``. By default, only """ Get inherit view's informations of the template ``key``. By default, only
returns ``customize_show`` templates (which can be active or not), if :returns ``customize_show`` templates (which can be active or not), if
``full=True`` returns inherit view's informations of the template ``key``. ``full=True`` returns inherit view's informations of the template ``key``.
``bundles=True`` returns also the asset bundles ``bundles=True`` returns also the asset bundles
""" """
return request.registry["ir.ui.view"].customize_template_get( return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
request.cr, request.uid, key, full=full, bundles=bundles,
context=request.context)
@http.route('/website/translations', type='json', auth="public", website=True) @http.route('/website/translations', type='json', auth="public", website=True)
def get_website_translations(self, lang, mods=None): def get_website_translations(self, lang, mods=None):
@@ -282,18 +282,14 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route(['/website/publish'], type='json', auth="public", website=True) @http.route(['/website/publish'], type='json', auth="public", website=True)
def publish(self, id, object): def publish(self, id, object):
_id = int(id) Model = request.env[object]
_object = request.registry[object] record = Model.browse(int(id))
obj = _object.browse(request.cr, request.uid, _id)
values = {} values = {}
if 'website_published' in _object._fields: if 'website_published' in Model._fields:
values['website_published'] = not obj.website_published values['website_published'] = not record.website_published
_object.write(request.cr, request.uid, [_id], record.write(values)
values, context=request.context) return bool(record.website_published)
obj = _object.browse(request.cr, request.uid, _id)
return bool(obj.website_published)
@http.route(['/website/seo_suggest'], type='json', auth="user", website=True) @http.route(['/website/seo_suggest'], type='json', auth="user", website=True)
def seo_suggest(self, keywords=None, lang=None): def seo_suggest(self, keywords=None, lang=None):
@@ -302,10 +298,10 @@ class Website(openerp.addons.web.controllers.main.Home):
try: try:
req = urllib2.Request("%s?%s" % (url, werkzeug.url_encode({ req = urllib2.Request("%s?%s" % (url, werkzeug.url_encode({
'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords, 'hl': language[0], 'gl': language[1]}))) 'ie': 'utf8', 'oe': 'utf8', 'output': 'toolbar', 'q': keywords, 'hl': language[0], 'gl': language[1]})))
request = urllib2.urlopen(req) response = urllib2.urlopen(req)
except (urllib2.HTTPError, urllib2.URLError): except (urllib2.HTTPError, urllib2.URLError):
return [] return []
xmlroot = ET.fromstring(request.read()) xmlroot = ET.fromstring(response.read())
return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']]) return json.dumps([sugg[0].attrib['data'] for sugg in xmlroot if len(sugg) and sugg[0].attrib['data']])
#------------------------------------------------------ #------------------------------------------------------
@@ -314,54 +310,46 @@ class Website(openerp.addons.web.controllers.main.Home):
def get_view_ids(self, xml_ids): def get_view_ids(self, xml_ids):
ids = [] ids = []
imd = request.registry['ir.model.data']
for xml_id in xml_ids: for xml_id in xml_ids:
if "." in xml_id: if "." in xml_id:
xml = xml_id.split(".") record_id = request.env.ref(xml_id).id
view_model, id = imd.get_object_reference(request.cr, request.uid, xml[0], xml[1])
else: else:
id = int(xml_id) record_id = int(xml_id)
ids.append(id) ids.append(record_id)
return ids return ids
@http.route(['/website/theme_customize_get'], type='json', auth="public", website=True) @http.route(['/website/theme_customize_get'], type='json', auth="public", website=True)
def theme_customize_get(self, xml_ids): def theme_customize_get(self, xml_ids):
view = request.registry["ir.ui.view"]
enable = [] enable = []
disable = [] disable = []
ids = self.get_view_ids(xml_ids) ids = self.get_view_ids(xml_ids)
context = dict(request.context or {}, active_test=True) for view in request.env['ir.ui.view'].with_context(active_test=True).browse(ids):
for v in view.browse(request.cr, request.uid, ids, context=context): if view.active:
if v.active: enable.append(view.xml_id)
enable.append(v.xml_id)
else: else:
disable.append(v.xml_id) disable.append(view.xml_id)
return [enable, disable] return [enable, disable]
@http.route(['/website/theme_customize'], type='json', auth="public", website=True) @http.route(['/website/theme_customize'], type='json', auth="public", website=True)
def theme_customize(self, enable, disable, get_bundle=False): def theme_customize(self, enable, disable, get_bundle=False):
""" enable or Disable lists of ``xml_id`` of the inherit templates """ enable or Disable lists of ``xml_id`` of the inherit templates """
"""
cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
view = pool["ir.ui.view"]
context = dict(request.context or {}, active_test=True)
def set_active(ids, active): def set_active(ids, active):
if ids: if ids:
view.write(cr, uid, self.get_view_ids(ids), {'active': active}, context=context) real_ids = self.get_view_ids(ids)
request.env['ir.ui.view'].with_context(active_test=True).browse(real_ids).write({'active': active})
set_active(disable, False) set_active(disable, False)
set_active(enable, True) set_active(enable, True)
if get_bundle: if get_bundle:
context = dict(request.context, active_test=True)
return request.env["ir.qweb"]._get_asset('web.assets_frontend', options=context) return request.env["ir.qweb"]._get_asset('web.assets_frontend', options=context)
return True return True
@http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True) @http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True)
def theme_customize_reload(self, href, enable, disable): def theme_customize_reload(self, href, enable, disable):
self.theme_customize(enable and enable.split(",") or [],disable and disable.split(",") or []) self.theme_customize(enable and enable.split(",") or [], disable and disable.split(",") or [])
return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true")) return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true"))
@http.route(['/website/multi_render'], type='json', auth="public", website=True) @http.route(['/website/multi_render'], type='json', auth="public", website=True)
@@ -375,22 +363,20 @@ class Website(openerp.addons.web.controllers.main.Home):
#------------------------------------------------------ #------------------------------------------------------
# Server actions # Server actions
#------------------------------------------------------ #------------------------------------------------------
@http.route([ @http.route([
'/website/action/<path_or_xml_id_or_id>', '/website/action/<path_or_xml_id_or_id>',
'/website/action/<path_or_xml_id_or_id>/<path:path>', '/website/action/<path_or_xml_id_or_id>/<path:path>',
], type='http', auth="public", website=True) ], type='http', auth="public", website=True)
def actions_server(self, path_or_xml_id_or_id, **post): def actions_server(self, path_or_xml_id_or_id, **post):
cr, uid, context = request.cr, request.uid, request.context ServerActions = request.env['ir.actions.server']
res, action_id, action = None, None, None
ServerActions = request.registry['ir.actions.server']
# find the action_id: either an xml_id, the path, or an ID # find the action_id: either an xml_id, the path, or an ID
if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id: if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
action_id = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, request.uid, path_or_xml_id_or_id, raise_if_not_found=False) action = request.env.ref(path_or_xml_id_or_id, raise_if_not_found=False)
if not action_id: if not action:
action_ids = ServerActions.search(cr, uid, [('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], context=context) action = ServerActions.search([('website_path', '=', path_or_xml_id_or_id), ('website_published', '=', True)], limit=1)
action_id = action_ids and action_ids[0] or None if not action:
if not action_id:
try: try:
action_id = int(path_or_xml_id_or_id) action_id = int(path_or_xml_id_or_id)
except ValueError: except ValueError:
@@ -398,24 +384,22 @@ class Website(openerp.addons.web.controllers.main.Home):
# check it effectively exists # check it effectively exists
if action_id: if action_id:
action_ids = ServerActions.exists(cr, uid, [action_id], context=context) action = ServerActions.browse(action_id).exists()
action_id = action_ids and action_ids[0] or None
# run it, return only if we got a Response object # run it, return only if we got a Response object
if action_id: if action:
action = ServerActions.browse(cr, uid, action_id, context=context)
if action.state == 'code' and action.website_published: if action.state == 'code' and action.website_published:
action_res = ServerActions.run(cr, uid, [action_id], context=context) action_res = action.run()
if isinstance(action_res, werkzeug.wrappers.Response): if isinstance(action_res, werkzeug.wrappers.Response):
res = action_res return action_res
if res:
return res
return request.redirect('/') return request.redirect('/')
#------------------------------------------------------ #------------------------------------------------------
# Retrocompatibility routes # Retrocompatibility routes
#------------------------------------------------------ #------------------------------------------------------
class WebsiteBinary(openerp.http.Controller): class WebsiteBinary(http.Controller):
@http.route([ @http.route([
'/website/image', '/website/image',
'/website/image/<xmlid>', '/website/image/<xmlid>',
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <odoo>
<data noupdate="1"> <data noupdate="1">
<record id="default_website" model="website"> <record id="default_website" model="website">
<field name="name">Website localhost</field> <field name="name">Website localhost</field>
@@ -368,4 +368,4 @@
<field name="url">/web/image/website.s_background_image_01</field> <field name="url">/web/image/website.s_background_image_01</field>
</record> </record>
</data> </data>
</openerp> </odoo>
+17 -19
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <odoo>
<data> <data>
<record id="default_website" model="website"> <record id="default_website" model="website">
@@ -15,28 +15,26 @@
</data> </data>
<data noupdate="1"> <data noupdate="1">
<!-- <!--
Some demo data for front-end server action generating templates Some demo data for front-end server action generating templates
--> -->
<template id="template_partner_post" name="Partner Post and Thanks (Website Server Action Demo)"> <template id="template_partner_post" name="Partner Post and Thanks (Website Server Action Demo)">
<t t-call="website.layout"> <t t-call="website.layout">
<t t-set="additional_title">Partner Detail</t> <t t-set="additional_title">Partner Detail</t>
<div id="wrap"> <div id="wrap">
<div class="oe_structure"> <div class="oe_structure">
<section> <section>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h2>Thank you for posting a message !</h2> <h2>Thank you for posting a message !</h2>
</div> </div>
</div>
</div>
</section>
</div> </div>
</div> </div>
</section> </t>
</div>
</div>
</t>
</template> </template>
<record id="action_partner_post" model="ir.actions.server"> <record id="action_partner_post" model="ir.actions.server">
@@ -207,4 +205,4 @@ response = request.render("website.template_partner_comment", values)
</record> </record>
</data> </data>
</openerp> </odoo>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import ir_actions import ir_actions
+12 -10
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import urlparse import urlparse
@@ -6,16 +7,16 @@ from odoo import api, fields, models
from odoo.http import request from odoo.http import request
class actions_server(models.Model): class ServerAction(models.Model):
""" Add website option in server actions. """ """ Add website option in server actions. """
_name = 'ir.actions.server' _name = 'ir.actions.server'
_inherit = 'ir.actions.server' _inherit = 'ir.actions.server'
xml_id = fields.Char(compute='_compute_xml_id', string="External ID", xml_id = fields.Char('External ID', compute='_compute_xml_id', help="ID of the action if defined in a XML file")
help="ID of the action if defined in a XML file") website_path = fields.Char('Website Path')
website_path = fields.Char() website_url = fields.Char('Website Url', compute='_get_website_url', help='The full URL to access the server action through the website.')
website_url = fields.Char(compute='_get_website_url', help='The full URL to access the server action through the website.') website_published = fields.Boolean('Available on the Website', copy=False,
website_published = fields.Boolean(string='Available on the Website', copy=False,
help='A code server action can be executed from the website, using a dedicated ' help='A code server action can be executed from the website, using a dedicated '
'controller. The address is <base>/website/action/<website_path>. ' 'controller. The address is <base>/website/action/<website_path>. '
'Set this field as True to allow users to run this action. If it ' 'Set this field as True to allow users to run this action. If it '
@@ -43,7 +44,7 @@ class actions_server(models.Model):
@api.model @api.model
def _get_eval_context(self, action): def _get_eval_context(self, action):
""" Override to add the request object in eval_context. """ """ Override to add the request object in eval_context. """
eval_context = super(actions_server, self)._get_eval_context(action) eval_context = super(ServerAction, self)._get_eval_context(action)
if action.state == 'code': if action.state == 'code':
eval_context['request'] = request eval_context['request'] = request
return eval_context return eval_context
@@ -51,7 +52,8 @@ class actions_server(models.Model):
@api.model @api.model
def run_action_code_multi(self, action, eval_context=None): def run_action_code_multi(self, action, eval_context=None):
""" Override to allow returning response the same way action is already """ Override to allow returning response the same way action is already
returned by the basic server action behavior. Note that response has returned by the basic server action behavior. Note that response has
priority over action, avoid using both. """ priority over action, avoid using both.
res = super(actions_server, self).run_action_code_multi(action, eval_context) """
res = super(ServerAction, self).run_action_code_multi(action, eval_context)
return eval_context.get('response', res) return eval_context.get('response', res)
+6 -7
View File
@@ -1,13 +1,12 @@
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import openerp from odoo import fields, models
from openerp.osv import osv, fields
class ir_attachment(osv.osv): class Attachment(models.Model):
_inherit = "ir.attachment" _inherit = "ir.attachment"
_columns = { # related for backward compatibility with saas-6
'website_url': fields.related("local_url", string="Attachment URL", type='char', deprecated=True), # related for backward compatibility with saas-6 website_url = fields.Char(string="Attachment URL", related='local_url', deprecated=True)
}
+79 -49
View File
@@ -1,67 +1,84 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging import logging
import os import os
import re import re
import time
import traceback import traceback
import werkzeug import werkzeug
import werkzeug.routing import werkzeug.routing
import werkzeug.utils import werkzeug.utils
import openerp import odoo
from openerp.addons.base import ir from odoo import api, models
from odoo import SUPERUSER_ID
from odoo.http import request
from odoo.tools import config
from odoo.exceptions import QWebException from odoo.exceptions import QWebException
from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE from odoo.tools.safe_eval import safe_eval
from openerp.http import request
from openerp.tools import config from odoo.addons.base import ir
from openerp.osv import orm from odoo.addons.website.models.website import slug, url_for, _UNSLUG_RE
from openerp.tools.safe_eval import safe_eval as eval
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RequestUID(object): class RequestUID(object):
def __init__(self, **kw): def __init__(self, **kw):
self.__dict__.update(kw) self.__dict__.update(kw)
class ir_http(orm.AbstractModel):
class Http(models.AbstractModel):
_inherit = 'ir.http' _inherit = 'ir.http'
rerouting_limit = 10 rerouting_limit = 10
_geoip_resolver = None _geoip_resolver = None
def _get_converters(self): def _get_converters(self):
""" Get the converters list for custom url pattern werkzeug need to
match Rule. This override adds the website ones.
"""
return dict( return dict(
super(ir_http, self)._get_converters(), super(Http, self)._get_converters(),
model=ModelConverter, model=ModelConverter,
page=PageConverter, page=PageConverter,
) )
def _auth_method_public(self): def _auth_method_public(self):
""" If no user logged, set the public user of current website, or default
public user as request uid.
After this method `request.env` can be called, since the `request.uid` is
set. The `env` lazy property of `request` will be correct.
"""
if not request.session.uid: if not request.session.uid:
website = self.pool['website'].get_current_website(request.cr, openerp.SUPERUSER_ID, context=request.context) env = api.Environment(request.cr, SUPERUSER_ID, request.context)
website = env['website'].get_current_website()
if website: if website:
request.uid = website.user_id.id request.uid = website.user_id.id
else: else:
request.uid = self.pool['ir.model.data'].xmlid_to_res_id(request.cr, openerp.SUPERUSER_ID, 'base', 'public_user') request.uid = env.ref('base.public_user').id
else: else:
request.uid = request.session.uid request.uid = request.session.uid
bots = "bot|crawl|slurp|spider|curl|wget|facebookexternalhit".split("|") bots = "bot|crawl|slurp|spider|curl|wget|facebookexternalhit".split("|")
def is_a_bot(self): def is_a_bot(self):
# We don't use regexp and ustr voluntarily # We don't use regexp and ustr voluntarily
# timeit has been done to check the optimum method # timeit has been done to check the optimum method
ua = request.httprequest.environ.get('HTTP_USER_AGENT', '').lower() user_agent = request.httprequest.environ.get('HTTP_USER_AGENT', '').lower()
try: try:
return any(bot in ua for bot in self.bots) return any(bot in user_agent for bot in self.bots)
except UnicodeDecodeError: except UnicodeDecodeError:
return any(bot in ua.encode('ascii', 'ignore') for bot in self.bots) return any(bot in user_agent.encode('ascii', 'ignore') for bot in self.bots)
def get_nearest_lang(self, lang): def get_nearest_lang(self, lang):
# Try to find a similar lang. Eg: fr_BE and fr_FR # Try to find a similar lang. Eg: fr_BE and fr_FR
short = lang.partition('_')[0] short = lang.partition('_')[0]
short_match = False short_match = False
for code, name in request.website.get_languages(): for code, dummy in request.website.get_languages():
if code == lang: if code == lang:
return lang return lang
if not short_match and code.startswith(short): if not short_match and code.startswith(short):
@@ -94,6 +111,14 @@ class ir_http(orm.AbstractModel):
return (self._name, "cache", request.uid, request.lang, request.httprequest.full_path) return (self._name, "cache", request.uid, request.lang, request.httprequest.full_path)
def _dispatch(self): def _dispatch(self):
""" Before executing the endpoint method, add website params on request, such as
- current website (record)
- multilang support (set on cookies)
- geoip dict data are added in the session
Then follow the parent dispatching.
Reminder : Do not use `request.env` before authentication phase, otherwise the env
set on request will be created with uid=None (and it is a lazy property)
"""
first_pass = not hasattr(request, 'website') first_pass = not hasattr(request, 'website')
request.website = None request.website = None
func = None func = None
@@ -116,6 +141,7 @@ class ir_http(orm.AbstractModel):
self._geoip_setup_resolver() self._geoip_setup_resolver()
self._geoip_resolve() self._geoip_resolve()
# For website routes (only), add website params on `request`
cook_lang = request.httprequest.cookies.get('website_lang') cook_lang = request.httprequest.cookies.get('website_lang')
if request.website_enabled: if request.website_enabled:
try: try:
@@ -127,7 +153,7 @@ class ir_http(orm.AbstractModel):
return self._handle_exception(e) return self._handle_exception(e)
request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code) request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code)
request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context) request.website = request.env['website'].get_current_website() # can use `request.env` since auth methods are called
request.context['website_id'] = request.website.id request.context['website_id'] = request.website.id
langs = [lg[0] for lg in request.website.get_languages()] langs = [lg[0] for lg in request.website.get_languages()]
path = request.httprequest.path.split('/') path = request.httprequest.path.split('/')
@@ -170,7 +196,7 @@ class ir_http(orm.AbstractModel):
# removed cache for auth public # removed cache for auth public
request.cache_save = False request.cache_save = False
resp = super(ir_http, self)._dispatch() resp = super(Http, self)._dispatch()
if request.website_enabled and cook_lang != request.lang and hasattr(resp, 'set_cookie'): if request.website_enabled and cook_lang != request.lang and hasattr(resp, 'set_cookie'):
resp.set_cookie('website_lang', request.lang) resp.set_cookie('website_lang', request.lang)
@@ -192,11 +218,11 @@ class ir_http(orm.AbstractModel):
return self._dispatch() return self._dispatch()
def _postprocess_args(self, arguments, rule): def _postprocess_args(self, arguments, rule):
super(ir_http, self)._postprocess_args(arguments, rule) super(Http, self)._postprocess_args(arguments, rule)
for key, val in arguments.items(): for key, val in arguments.items():
# Replace uid placeholder by the current request.uid # Replace uid placeholder by the current request.uid
if isinstance(val, orm.BaseModel) and isinstance(val._uid, RequestUID): if isinstance(val, models.BaseModel) and isinstance(val._uid, RequestUID):
arguments[key] = val.sudo(request.uid) arguments[key] = val.sudo(request.uid)
try: try:
@@ -219,10 +245,10 @@ class ir_http(orm.AbstractModel):
is_website_request = bool(getattr(request, 'website_enabled', False) and request.website) is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
if not is_website_request: if not is_website_request:
# Don't touch non website requests exception handling # Don't touch non website requests exception handling
return super(ir_http, self)._handle_exception(exception) return super(Http, self)._handle_exception(exception)
else: else:
try: try:
response = super(ir_http, self)._handle_exception(exception) response = super(Http, self)._handle_exception(exception)
if isinstance(response, Exception): if isinstance(response, Exception):
exception = response exception = response
else: else:
@@ -246,21 +272,20 @@ class ir_http(orm.AbstractModel):
else: else:
code = exception.code code = exception.code
if isinstance(exception, openerp.exceptions.AccessError): if isinstance(exception, odoo.exceptions.AccessError):
code = 403 code = 403
if isinstance(exception, QWebException): if isinstance(exception, QWebException):
values.update(qweb_exception=exception) values.update(qweb_exception=exception)
if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError): if isinstance(exception.qweb.get('cause'), odoo.exceptions.AccessError):
code = 403 code = 403
if code == 500: if code == 500:
logger.error("500 Internal Server Error:\n\n%s", values['traceback']) logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
if 'qweb_exception' in values: if 'qweb_exception' in values:
view = request.registry.get("ir.ui.view") view = request.env["ir.ui.view"]
views = view._views_get(request.cr, request.uid, exception.qweb['template'], views = view._views_get(exception.qweb['template'])
context=request.context) to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and not view.page)
to_reset = [v for v in views if v.model_data_id.noupdate is True and not v.page]
values['views'] = to_reset values['views'] = to_reset
elif code == 403: elif code == 403:
logger.warn("403 Forbidden:\n\n%s", values['traceback']) logger.warn("403 Forbidden:\n\n%s", values['traceback'])
@@ -288,10 +313,12 @@ class ir_http(orm.AbstractModel):
obj = env[model].browse(int(id)) obj = env[model].browse(int(id))
if obj and 'website_published' in obj._fields: if obj and 'website_published' in obj._fields:
if env[obj._name].sudo().search([('id', '=', obj.id), ('website_published', '=', True)]): if env[obj._name].sudo().search([('id', '=', obj.id), ('website_published', '=', True)]):
env = env(user=openerp.SUPERUSER_ID) env = env(user=SUPERUSER_ID)
return super(ir_http, self).binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env) return super(Http, self).binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env)
class ModelConverter(ir.ir_http.ModelConverter): class ModelConverter(ir.ir_http.ModelConverter):
def __init__(self, url_map, model=False, domain='[]'): def __init__(self, url_map, model=False, domain='[]'):
super(ModelConverter, self).__init__(url_map, model) super(ModelConverter, self).__init__(url_map, model)
self.domain = domain self.domain = domain
@@ -301,43 +328,46 @@ class ModelConverter(ir.ir_http.ModelConverter):
return slug(value) return slug(value)
def to_python(self, value): def to_python(self, value):
m = re.match(self.regex, value) matching = re.match(self.regex, value)
_uid = RequestUID(value=value, match=m, converter=self) _uid = RequestUID(value=value, match=matching, converter=self)
record_id = int(m.group(2)) record_id = int(matching.group(2))
env = api.Environment(request.cr, _uid, request.context)
if record_id < 0: if record_id < 0:
# limited support for negative IDs due to our slug pattern, assume abs() if not found # limited support for negative IDs due to our slug pattern, assume abs() if not found
if not request.registry[self.model].exists(request.cr, _uid, [record_id]): if not env[self.model].browse(record_id).exists():
record_id = abs(record_id) record_id = abs(record_id)
return request.registry[self.model].browse( return env[self.model].browse(record_id)
request.cr, _uid, record_id, context=request.context)
def generate(self, cr, uid, query=None, args=None, context=None): def generate(self, query=None, args=None):
obj = request.registry[self.model] Model = request.env[self.model]
domain = eval( self.domain, (args or {}).copy()) domain = safe_eval(self.domain, (args or {}).copy())
if query: if query:
domain.append((obj._rec_name, 'ilike', '%'+query+'%')) domain.append((Model._rec_name, 'ilike', '%' + query + '%'))
for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context): for record in Model.search_read(domain=domain, fields=['write_date', Model._rec_name]):
if record.get(obj._rec_name, False): if record.get(Model._rec_name, False):
yield {'loc': (record['id'], record[obj._rec_name])} yield {'loc': (record['id'], record[Model._rec_name])}
class PageConverter(werkzeug.routing.PathConverter): class PageConverter(werkzeug.routing.PathConverter):
""" Only point of this converter is to bundle pages enumeration logic """ """ Only point of this converter is to bundle pages enumeration logic """
def generate(self, cr, uid, query=None, args={}, context=None):
View = request.registry['ir.ui.view'] def generate(self, query=None, args={}):
View = request.env['ir.ui.view']
domain = [('page', '=', True)] domain = [('page', '=', True)]
query = query and query.startswith('website.') and query[8:] or query query = query and query.startswith('website.') and query[8:] or query
if query: if query:
domain += [('key', 'like', query)] domain += [('key', 'like', query)]
views = View.search_read(cr, uid, domain, fields=['key', 'priority', 'write_date'], order='name', context=context) views = View.search_read(domain, fields=['key', 'priority', 'write_date'], order='name')
for view in views: for view in views:
xid = view['key'].startswith('website.') and view['key'][8:] or view['key'] xid = view['key'].startswith('website.') and view['key'][8:] or view['key']
# the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
# when we will have an url mapping mechanism, replace this by a rule: page/homepage --> / # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
if xid=='homepage': continue if xid == 'homepage':
continue
record = {'loc': xid} record = {'loc': xid}
if view['priority'] <> 16: if view['priority'] != 16:
record['__priority'] = min(round(view['priority'] / 32.0,1), 1) record['__priority'] = min(round(view['priority'] / 32.0, 1), 1)
if view['write_date']: if view['write_date']:
record['__lastmod'] = view['write_date'][:10] record['__lastmod'] = view['write_date'][:10]
yield record yield record
+8 -5
View File
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from openerp.http import request # Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import orm
import ast import ast
from odoo import models
from odoo.http import request
class QWeb(orm.AbstractModel):
""" QWeb object for rendering stuff in the website context class QWeb(models.AbstractModel):
""" """ QWeb object for rendering stuff in the website context """
_inherit = 'ir.qweb' _inherit = 'ir.qweb'
@@ -36,6 +38,7 @@ class QWeb(orm.AbstractModel):
url_att = self.URL_ATTRS.get(el.tag) url_att = self.URL_ATTRS.get(el.tag)
cdn_att = self.CDN_TRIGGERS.get(el.tag) cdn_att = self.CDN_TRIGGERS.get(el.tag)
def process(item): def process(item):
if isinstance(item, tuple) and (item[0] in (url_att, cdn_att)): if isinstance(item, tuple) and (item[0] in (url_att, cdn_att)):
return (item[0], ast.Call( return (item[0], ast.Call(
+114 -124
View File
@@ -1,100 +1,96 @@
# -*- coding: ascii -*- # -*- coding: ascii -*-
import copy # Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging import logging
from itertools import groupby from itertools import groupby
from lxml import etree, html from lxml import etree
from openerp import SUPERUSER_ID, api, tools from odoo import api, fields, models
from openerp.addons.website.models import website from odoo import tools
from openerp.http import request
from openerp.osv import osv, fields from odoo.addons.website.models import website
from odoo.http import request
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class view(osv.osv): class View(models.Model):
_name = "ir.ui.view" _name = "ir.ui.view"
_inherit = ["ir.ui.view", "website.seo.metadata"] _inherit = ["ir.ui.view", "website.seo.metadata"]
_columns = {
'page': fields.boolean("Whether this view is a web page template (complete)"),
'customize_show': fields.boolean("Show As Optional Inherit"),
'website_id': fields.many2one('website', ondelete='cascade', string="Website"),
}
_defaults = { page = fields.Boolean("Whether this view is a web page template (complete)", default=False)
'page': False, customize_show = fields.Boolean("Show As Optional Inherit", default=False)
'customize_show': False, website_id = fields.Many2one('website', ondelete='cascade', string="Website")
}
def unlink(self, cr, uid, ids, context=None): @api.multi
res = super(view, self).unlink(cr, uid, ids, context=context) def unlink(self):
result = super(View, self).unlink()
self.clear_caches() self.clear_caches()
return res return result
@api.multi
def _sort_suitability_key(self): def _sort_suitability_key(self):
""" Key function to sort views by descending suitability
Suitability of a view is defined as follow:
* if the view and request website_id are matched
* then if the view has no set website
""" """
Key function to sort views by descending suitability self.ensure_one()
Suitability of a view is defined as follow:
* if the view and request website_id are matched
* then if the view has no set website
"""
context_website_id = self.env.context.get('website_id', 1) context_website_id = self.env.context.get('website_id', 1)
website_id = self.website_id.id or 0 website_id = self.website_id.id or 0
different_website = context_website_id != website_id different_website = context_website_id != website_id
return (different_website, website_id) return (different_website, website_id)
def filter_duplicate(self): def filter_duplicate(self):
""" """ Filter current recordset only keeping the most suitable view per distinct key """
Filter current recordset only keeping the most suitable view per distinct key filtered = self.env['ir.ui.view']
""" for dummy, group in groupby(self, key=lambda record: record.key):
filtered = self.browse([]) filtered += sorted(group, key=lambda record: record._sort_suitability_key())[0]
for _, group in groupby(self, key=lambda r:r.key):
filtered += sorted(group, key=lambda r:r._sort_suitability_key())[0]
return filtered return filtered
def _view_obj(self, cr, uid, view_id, context=None): @api.model
def _view_obj(self, view_id):
if isinstance(view_id, basestring): if isinstance(view_id, basestring):
if 'website_id' in (context or {}): if 'website_id' in self._context:
domain = [('key', '=', view_id), '|', ('website_id', '=', False), ('website_id', '=', context.get('website_id'))] domain = [('key', '=', view_id), '|', ('website_id', '=', False), ('website_id', '=', self._context.get('website_id'))]
rec_id = self.search(cr, uid, domain, order='website_id', context=context) order = 'website_id'
else: else:
rec_id = self.search(cr, uid, [('key', '=', view_id)], context=context) domain = [('key', '=', view_id)]
if rec_id: order = self._order
return self.browse(cr, uid, rec_id, context=context).filter_duplicate() views = self.search(domain, order=order)
if views:
return views.filter_duplicate()
else: else:
return self.pool['ir.model.data'].xmlid_to_object( return self.env.ref(view_id)
cr, uid, view_id, raise_if_not_found=True, context=context)
elif isinstance(view_id, (int, long)): elif isinstance(view_id, (int, long)):
return self.browse(cr, uid, view_id, context=context) return self.browse(view_id)
# assume it's already a view object (WTF?) # assume it's already a view object (WTF?)
return view_id return view_id
# Returns all views (called and inherited) related to a view # Returns all views (called and inherited) related to a view
# Used by translation mechanism, SEO and optional templates # Used by translation mechanism, SEO and optional templates
def _views_get(self, cr, uid, view_id, options=True, bundles=False, context=None, root=True):
@api.model
def _views_get(self, view_id, options=True, bundles=False, root=True):
""" For a given view ``view_id``, should return: """ For a given view ``view_id``, should return:
* the view itself
* the view itself * all views inheriting from it, enabled or not
* all views inheriting from it, enabled or not - but not the optional children of a non-enabled child
- but not the optional children of a non-enabled child * all views called from it (via t-call)
* all views called from it (via t-call) :returns recordset of ir.ui.view
""" """
try: try:
view = self._view_obj(cr, uid, view_id, context=context) view = self._view_obj(view_id)
except ValueError: except ValueError:
_logger.warning("Could not find view object with view_id '%s'" % (view_id)) _logger.warning("Could not find view object with view_id '%s'", view_id)
# Shall we log that ? Yes, you should !
return [] return []
while root and view.inherit_id: while root and view.inherit_id:
view = view.inherit_id view = view.inherit_id
result = [view] views_to_return = view
node = etree.fromstring(view.arch) node = etree.fromstring(view.arch)
xpath = "//t[@t-call]" xpath = "//t[@t-call]"
@@ -102,49 +98,44 @@ class view(osv.osv):
xpath += "| //t[@t-call-assets]" xpath += "| //t[@t-call-assets]"
for child in node.xpath(xpath): for child in node.xpath(xpath):
try: try:
called_view = self._view_obj(cr, uid, child.get('t-call', child.get('t-call-assets')), context=context) called_view = self._view_obj(child.get('t-call', child.get('t-call-assets')))
except ValueError: except ValueError:
continue continue
if called_view not in result: if called_view not in views_to_return:
result += self._views_get(cr, uid, called_view, options=options, bundles=bundles, context=context) views_to_return += self._views_get(called_view, options=options, bundles=bundles)
extensions = view.inherit_children_ids extensions = view.inherit_children_ids
if not options: if not options:
# only active children # only active children
extensions = (v for v in view.inherit_children_ids if v.active) extensions = view.inherit_children_ids.filtered(lambda view: view.active)
# Keep options in a deterministic order regardless of their applicability # Keep options in a deterministic order regardless of their applicability
for extension in sorted(extensions, key=lambda v: v.id): for extension in extensions.sorted(key=lambda v: v.id):
for r in self._views_get( # only return optional grandchildren if this child is enabled
cr, uid, extension, for ext_view in self._views_get(extension, options=extension.active, root=False):
# only return optional grandchildren if this child is enabled if ext_view not in views_to_return:
options=extension.active, views_to_return += ext_view
context=context, root=False): return views_to_return
if r not in result:
result.append(r)
return result
@tools.ormcache_context('uid', 'xml_id', keys=('website_id',)) @api.model
def get_view_id(self, cr, uid, xml_id, context=None): @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
if context and 'website_id' in context and not isinstance(xml_id, (int, long)): def get_view_id(self, xml_id):
domain = [('key', '=', xml_id), '|', ('website_id', '=', context['website_id']), ('website_id', '=', False)] if 'website_id' in self._context and not isinstance(xml_id, (int, long)):
[view_id] = self.search(cr, uid, domain, order='website_id', limit=1, context=context) or [None] domain = [('key', '=', xml_id), '|', ('website_id', '=', self._context['website_id']), ('website_id', '=', False)]
if not view_id: view = self.search(domain, order='website_id', limit=1)
_logger.warning("Could not find view object with xml_id '%s'" % (xml_id)) if not view:
raise ValueError('View %r in website %r not found' % (xml_id, context['website_id'])) _logger.warning("Could not find view object with xml_id '%s'", xml_id)
else: raise ValueError('View %r in website %r not found' % (xml_id, self._context['website_id']))
view_id = super(view, self).get_view_id(cr, uid, xml_id, context=context) return view.id
return view_id return super(View, self).get_view_id(xml_id)
@api.cr_uid_ids_context @api.multi
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None): def render(self, values=None, engine='ir.qweb'):
""" Render the template. If website is enabled on request, then extend rendering context with website values. """
new_context = dict(self._context)
if request and getattr(request, 'website_enabled', False): if request and getattr(request, 'website_enabled', False):
engine = 'ir.qweb'
if isinstance(id_or_xml_id, list): qcontext = self._prepare_qcontext()
id_or_xml_id = id_or_xml_id[0]
qcontext = self._prepare_qcontext(cr, uid, context=context)
# add some values # add some values
if values: if values:
@@ -153,78 +144,77 @@ class view(osv.osv):
# in edit mode ir.ui.view will tag nodes # in edit mode ir.ui.view will tag nodes
if not qcontext.get('translatable') and not qcontext.get('rendering_bundle'): if not qcontext.get('translatable') and not qcontext.get('rendering_bundle'):
if qcontext.get('editable'): if qcontext.get('editable'):
context = dict(context, inherit_branding=True) new_context = dict(self._context, inherit_branding=True)
elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'): elif request.env.user.has_group('base.group_website_publisher'):
context = dict(context, inherit_branding_auto=True) new_context = dict(self._context, inherit_branding_auto=True)
view_obj = request.website.get_template(id_or_xml_id)
if 'main_object' not in qcontext: if 'main_object' not in qcontext:
qcontext['main_object'] = view_obj qcontext['main_object'] = self
values = qcontext values = qcontext
return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context) return super(View, self.with_context(new_context)).render(values, engine=engine)
def _prepare_qcontext(self, cr, uid, context=None): @api.model
if not context: def _prepare_qcontext(self):
context = {} """ Returns the qcontext : rendering context with website specific value (required
to render website layout template)
company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, context=context) """
company = request.website.company_id.sudo()
editable = request.website.is_publisher() editable = request.website.is_publisher()
translatable = editable and context.get('lang') != request.website.default_lang_code translatable = editable and self._context.get('lang') != request.website.default_lang_code
editable = not translatable and editable editable = not translatable and editable
qcontext = dict( qcontext = dict(
context.copy(), self._context.copy(),
website=request.website, website=request.website,
url_for=website.url_for, url_for=website.url_for,
slug=website.slug, slug=website.slug,
res_company=company, res_company=company,
user_id=self.pool.get("res.users").browse(cr, uid, uid), user_id=self.env["res.users"].browse(self.env.user.id),
default_lang_code=request.website.default_lang_code, default_lang_code=request.website.default_lang_code,
languages=request.website.get_languages(), languages=request.website.get_languages(),
translatable=translatable, translatable=translatable,
editable=editable, editable=editable,
menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None, menu_data=self.env['ir.ui.menu'].load_menus_root() if request.website.is_user() else None,
) )
return qcontext return qcontext
def customize_template_get(self, cr, uid, key, full=False, bundles=False, context=None): @api.model
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``. By default, only """ Get inherit view's informations of the template ``key``. By default, only
returns ``customize_show`` templates (which can be active or not), if returns ``customize_show`` templates (which can be active or not), if
``full=True`` returns inherit view's informations of the template ``key``. ``full=True`` returns inherit view's informations of the template ``key``.
``bundles=True`` returns also the asset bundles ``bundles=True`` returns also the asset bundles
""" """
imd = self.pool['ir.model.data'] imd = self.env['ir.model.data']
theme_view_id = imd.xmlid_to_res_id(cr, uid, 'website.theme') view_theme_id = imd.xmlid_to_res_id('website.theme')
user = self.pool['res.users'].browse(cr, uid, context=context) user = self.env.user
user_groups = set(user.groups_id) user_groups = set(user.groups_id)
views = self._views_get( views = self.with_context(active_test=False)._views_get(key, bundles=bundles)
cr, uid, key, bundles=bundles,
context=dict(context or {}, active_test=False))
done = set() done = set()
result = [] result = []
for v in views: for view in views:
if not user_groups.issuperset(v.groups_id): if not user_groups.issuperset(view.groups_id):
continue continue
if full or (v.customize_show and v.inherit_id.id != theme_view_id): if full or (view.customize_show and view.inherit_id.id != view_theme_id):
if v.inherit_id not in done: if view.inherit_id not in done:
result.append({ result.append({
'name': v.inherit_id.name, 'name': view.inherit_id.name,
'id': v.id, 'id': view.id,
'key': v.key, 'key': view.key,
'inherit_id': v.inherit_id.id, 'inherit_id': view.inherit_id.id,
'header': True, 'header': True,
'active': False 'active': False
}) })
done.add(v.inherit_id) done.add(view.inherit_id)
result.append({ result.append({
'name': v.name, 'name': view.name,
'id': v.id, 'id': view.id,
'key': v.key, 'key': view.key,
'inherit_id': v.inherit_id.id, 'inherit_id': view.inherit_id.id,
'header': False, 'header': False,
'active': v.active, 'active': view.active,
}) })
return result return result
+11 -10
View File
@@ -1,18 +1,19 @@
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import openerp from odoo import api, models
from openerp.osv import osv, fields
class res_company(osv.osv): class Company(models.Model):
_inherit = "res.company" _inherit = "res.company"
def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None): @api.multi
partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id def google_map_img(self, zoom=8, width=298, height=298):
return partner and partner.google_map_img(zoom, width, height, context=context) or None partner = self.sudo().partner_id
return partner and partner.google_map_img(zoom, width, height) or None
def google_map_link(self, cr, uid, ids, zoom=8, context=None): @api.multi
partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id def google_map_link(self, zoom=8):
return partner and partner.google_map_link(zoom, context=context) or None partner = self.sudo().partner_id
return partner and partner.google_map_link(zoom) or None
+27 -44
View File
@@ -1,52 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv from odoo import fields, models
class WebsiteConfigSettings(models.TransientModel):
class website_config_settings(osv.osv_memory):
_name = 'website.config.settings' _name = 'website.config.settings'
_inherit = 'res.config.settings' _inherit = 'res.config.settings'
_columns = { def _default_website(self):
'website_id': fields.many2one('website', string="website", required=True), return self.env['website'].search([], limit=1)
'website_name': fields.related('website_id', 'name', type="char", string="Website Name"),
'language_ids': fields.related('website_id', 'language_ids', type='many2many', relation='res.lang', string='Languages'), website_id = fields.Many2one('website', string="website", default=_default_website, required=True)
'default_lang_id': fields.related('website_id', 'default_lang_id', type='many2one', relation='res.lang', string='Default language'), website_name = fields.Char('Website Name', related='website_id.name')
'default_lang_code': fields.related('website_id', 'default_lang_code', type="char", string="Default language code"),
'google_analytics_key': fields.related('website_id', 'google_analytics_key', type="char", string='Google Analytics Key'),
'social_twitter': fields.related('website_id', 'social_twitter', type="char", string='Twitter Account'),
'social_facebook': fields.related('website_id', 'social_facebook', type="char", string='Facebook Account'),
'social_github': fields.related('website_id', 'social_github', type="char", string='GitHub Account'),
'social_linkedin': fields.related('website_id', 'social_linkedin', type="char", string='LinkedIn Account'),
'social_youtube': fields.related('website_id', 'social_youtube', type="char", string='Youtube Account'),
'social_googleplus': fields.related('website_id', 'social_googleplus', type="char", string='Google+ Account'),
'compress_html': fields.related('website_id', 'compress_html', type="boolean", string='Compress rendered HTML for a better Google PageSpeed result'),
'cdn_activated': fields.related('website_id', 'cdn_activated', type="boolean", string='Use a Content Delivery Network (CDN)'),
'cdn_url': fields.related('website_id', 'cdn_url', type="char", string='CDN Base URL'),
'cdn_filters': fields.related('website_id', 'cdn_filters', type="text", string='CDN Filters'),
'module_website_form_editor': fields.boolean("Form builder: create and customize forms"),
'module_website_version': fields.boolean("A/B testing and versioning"),
'favicon': fields.related('website_id', 'favicon', type="binary", string="Favicon", help="This field holds the image used to display a favicon on the website."),
}
def on_change_website_id(self, cr, uid, ids, website_id, context=None): language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang')
if not website_id: default_lang_id = fields.Many2one(related='website_id.default_lang_id', relation='res.lang')
return {'value': {}} default_lang_code = fields.Char('Default language code', related='website_id.default_lang_code')
website_data = self.pool.get('website').read(cr, uid, [website_id], [], context=context)[0] google_analytics_key = fields.Char('Google Analytics Key', related='website_id.google_analytics_key')
values = {'website_name': website_data['name']}
for fname, v in website_data.items():
if fname in self._columns:
values[fname] = v[0] if v and self._columns[fname]._type == 'many2one' else v
return {'value' : values}
# FIXME in trunk for god sake. Change the fields above to fields.char instead of fields.related, social_twitter = fields.Char(related='website_id.social_twitter')
# and create the function set_website who will set the value on the website_id social_facebook = fields.Char(related='website_id.social_facebook')
# create does not forward the values to the related many2one. Write does. social_github = fields.Char(related='website_id.social_github')
def create(self, cr, uid, vals, context=None): social_linkedin = fields.Char(related='website_id.social_linkedin')
config_id = super(website_config_settings, self).create(cr, uid, vals, context=context) social_youtube = fields.Char(related='website_id.social_youtube')
self.write(cr, uid, config_id, vals, context=context) social_googleplus = fields.Char(related='website_id.social_googleplus')
return config_id compress_html = fields.Boolean('Compress rendered HTML for a better Google PageSpeed result', related='website_id.compress_html')
cdn_activated = fields.Boolean('Use a Content Delivery Network (CDN)', related='website_id.cdn_activated')
_defaults = { cdn_url = fields.Char(related='website_id.cdn_url')
'website_id': lambda self,cr,uid,c: self.pool.get('website').search(cr, uid, [], context=c)[0], cdn_filters = fields.Text(related='website_id.cdn_filters')
} module_website_form_editor = fields.Boolean("Form builde = create and customize forms")
module_website_version = fields.Boolean("A/B testing and versioning")
favicon = fields.Binary('Favicon', related='website_id.favicon')
+12 -12
View File
@@ -1,33 +1,33 @@
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug import werkzeug
import openerp from odoo import api, models
from openerp.osv import orm, osv, fields
def urlplus(url, params): def urlplus(url, params):
return werkzeug.Href(url)(params or None) return werkzeug.Href(url)(params or None)
class res_partner(osv.osv): class Partner(models.Model):
_inherit = "res.partner" _inherit = "res.partner"
def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None): @api.multi
partner = self.browse(cr, uid, ids[0], context=context) def google_map_img(self, zoom=8, width=298, height=298):
params = { params = {
'center': '%s, %s %s, %s' % (partner.street or '', partner.city or '', partner.zip or '', partner.country_id and partner.country_id.name_get()[0][1] or ''), 'center': '%s, %s %s, %s' % (self.street or '', self.city or '', self.zip or '', self.country_id and self.country_id.name_get()[0][1] or ''),
'size': "%sx%s" % (height, width), 'size': "%sx%s" % (height, width),
'zoom': zoom, 'zoom': zoom,
'sensor': 'false', 'sensor': 'false',
} }
return urlplus('//maps.googleapis.com/maps/api/staticmap' , params) return urlplus('//maps.googleapis.com/maps/api/staticmap', params)
def google_map_link(self, cr, uid, ids, zoom=10, context=None): @api.multi
partner = self.browse(cr, uid, ids[0], context=context) def google_map_link(self, zoom=10):
params = { params = {
'q': '%s, %s %s, %s' % (partner.street or '', partner.city or '', partner.zip or '', partner.country_id and partner.country_id.name_get()[0][1] or ''), 'q': '%s, %s %s, %s' % (self.street or '', self.city or '', self.zip or '', self.country_id and self.country_id.name_get()[0][1] or ''),
'z': zoom, 'z': zoom,
} }
return urlplus('https://maps.google.com/maps' , params) return urlplus('https://maps.google.com/maps', params)
+3 -1
View File
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json import json
from openerp import api, models from odoo import api, models
class WebsitePlanner(models.Model): class WebsitePlanner(models.Model):
+318 -293
View File
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import inspect import inspect
import logging import logging
import math import math
@@ -6,26 +8,34 @@ import unicodedata
import re import re
import urlparse import urlparse
import hashlib import hashlib
from sys import maxint
import werkzeug import werkzeug
from werkzeug.exceptions import NotFound
# optional python-slugify import (https://github.com/un33k/python-slugify) # optional python-slugify import (https://github.com/un33k/python-slugify)
try: try:
import slugify as slugify_lib import slugify as slugify_lib
except ImportError: except ImportError:
slugify_lib = None slugify_lib = None
import openerp from odoo import api, fields, models
from openerp.tools.translate import _ from odoo import tools
from openerp.osv import orm, osv, fields from odoo.tools import ustr
from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web from odoo.http import request
from openerp.tools.safe_eval import safe_eval from odoo.tools.translate import _
from openerp.http import request
from werkzeug.exceptions import NotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_CDN_FILTERS = [
"^/[^/]+/static/",
"^/web/(css|js)/",
"^/web/image",
"^/web/content",
# retrocompatibility
"^/website/image/",
]
def url_for(path_or_uri, lang=None): def url_for(path_or_uri, lang=None):
if isinstance(path_or_uri, unicode): if isinstance(path_or_uri, unicode):
path_or_uri = path_or_uri.encode('utf-8') path_or_uri = path_or_uri.encode('utf-8')
@@ -58,6 +68,7 @@ def url_for(path_or_uri, lang=None):
return location.decode('utf-8') return location.decode('utf-8')
def is_multilang_url(local_url, langs=None): def is_multilang_url(local_url, langs=None):
if not langs: if not langs:
langs = [lg[0] for lg in request.website.get_languages()] langs = [lg[0] for lg in request.website.get_languages()]
@@ -79,17 +90,20 @@ def is_multilang_url(local_url, langs=None):
except Exception: except Exception:
return False return False
####################################################
# Slug API
####################################################
def slugify(s, max_length=None): def slugify(s, max_length=None):
""" Transform a string to a slug that can be used in a url path. """ Transform a string to a slug that can be used in a url path.
This method will first try to do the job with python-slugify if present.
This method will first try to do the job with python-slugify if present. Otherwise it will process string by stripping leading and ending spaces,
Otherwise it will process string by stripping leading and ending spaces, converting unicode chars to ascii, lowering all chars and replacing spaces
converting unicode chars to ascii, lowering all chars and replacing spaces and underscore with hyphen "-".
and underscore with hyphen "-". :param s: str
:param max_length: int
:param s: str :rtype: str
:param max_length: int
:rtype: str
""" """
s = ustr(s) s = ustr(s)
if slugify_lib: if slugify_lib:
@@ -99,35 +113,27 @@ def slugify(s, max_length=None):
except TypeError: except TypeError:
pass pass
uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii') uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
slug = re.sub('[\W_]', ' ', uni).strip().lower() slug_str = re.sub('[\W_]', ' ', uni).strip().lower()
slug = re.sub('[-\s]+', '-', slug) slug_str = re.sub('[-\s]+', '-', slug_str)
return slug_str[:max_length]
return slug[:max_length]
def slug(value): def slug(value):
if isinstance(value, orm.browse_record): if isinstance(value, models.BaseModel):
# [(id, name)] = value.name_get() # [(id, name)] = value.name_get()
id, name = value.id, value.display_name identifier, name = value.id, value.display_name
else: else:
# assume name_search result tuple # assume name_search result tuple
id, name = value identifier, name = value
slugname = slugify(name or '').strip().strip('-') slugname = slugify(name or '').strip().strip('-')
if not slugname: if not slugname:
return str(id) return str(identifier)
return "%s-%d" % (slugname, id) return "%s-%d" % (slugname, identifier)
# NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
_UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)') _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
DEFAULT_CDN_FILTERS = [
"^/[^/]+/static/",
"^/web/(css|js)/",
"^/web/image",
"^/web/content",
# retrocompatibility
"^/website/image/",
]
def unslug(s): def unslug(s):
"""Extract slug and id from a string. """Extract slug and id from a string.
@@ -139,72 +145,70 @@ def unslug(s):
return m.group(1), int(m.group(2)) return m.group(1), int(m.group(2))
class website(osv.osv): class Website(models.Model):
def _get_menu(self, cr, uid, ids, name, arg, context=None):
res = {}
menu_obj = self.pool.get('website.menu')
for id in ids:
menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
res[id] = menu_ids and menu_ids[0] or False
return res
def _active_languages(self, cr, uid, context=None): _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
return self.pool['res.lang'].search(cr, uid, [], context=context)
def _default_language(self, cr, uid, context=None):
lang_code = self.pool['ir.values'].get_default(cr, uid, 'res.partner', 'lang')
def_langs = self.pool['res.lang'].search(cr, uid, [('code', '=', lang_code)], context=context)
return def_langs[0] if def_langs else self._active_languages(cr, uid, context=context)[0]
_name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
_description = "Website" _description = "Website"
_columns = {
'name': fields.char('Website Name'), def _active_languages(self):
'domain': fields.char('Website Domain'), return self.env['res.lang'].search([]).ids
'company_id': fields.many2one('res.company', string="Company"),
'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'), def _default_language(self):
'default_lang_id': fields.many2one('res.lang', string="Default language"), lang_code = self.env['ir.values'].get_default('res.partner', 'lang')
'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True), def_lang = self.env['res.lang'].search([('code', '=', lang_code)], limit=1)
'social_twitter': fields.char('Twitter Account'), return def_lang.id if def_lang else self._active_languages()[0]
'social_facebook': fields.char('Facebook Account'),
'social_github': fields.char('GitHub Account'), name = fields.Char('Website Name')
'social_linkedin': fields.char('LinkedIn Account'), domain = fields.Char('Website Domain')
'social_youtube': fields.char('Youtube Account'), company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.ref('base.main_company').id)
'social_googleplus': fields.char('Google+ Account'), language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
'google_analytics_key': fields.char('Google Analytics Key'), default_lang_id = fields.Many2one('res.lang', string="Default language", default=_default_language)
'user_id': fields.many2one('res.users', string='Public User'), default_lang_code = fields.Char(related='default_lang_id.code', string="Default language code", store=True)
'compress_html': fields.boolean('Compress HTML'),
'cdn_activated': fields.boolean('Activate CDN for assets'), social_twitter = fields.Char('Twitter Account')
'cdn_url': fields.char('CDN Base URL'), social_facebook = fields.Char('Facebook Account')
'cdn_filters': fields.text('CDN Filters', help="URL matching those filters will be rewritten using the CDN Base URL"), social_github = fields.Char('GitHub Account')
'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'), social_linkedin = fields.Char('LinkedIn Account')
'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu'), social_youtube = fields.Char('Youtube Account')
'favicon': fields.binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website."), social_googleplus = fields.Char('Google+ Account')
} google_analytics_key = fields.Char('Google Analytics Key')
_defaults = {
'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'), user_id = fields.Many2one('res.users', string='Public User', default=lambda self: self.env.ref('base.public_user').id)
'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'), compress_html = fields.Boolean('Compress HTML')
'compress_html': False, cdn_activated = fields.Boolean('Activate CDN for assets')
'cdn_activated': False, cdn_url = fields.Char('CDN Base URL', default='')
'cdn_url': '', cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL")
'cdn_filters': '\n'.join(DEFAULT_CDN_FILTERS), partner_id = fields.Many2one(related='user_id.partner_id', relation='res.partner', string='Public Partner')
'language_ids': _active_languages, menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu')
'default_lang_id': _default_language, favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
}
@api.multi
def _compute_menu(self):
Menu = self.env['website.menu']
for website in self:
website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', '=', website.id)], order='id', limit=1).id
# cf. Wizard hack in website_views.xml # cf. Wizard hack in website_views.xml
def noop(self, *args, **kwargs): def noop(self, *args, **kwargs):
pass pass
def write(self, cr, uid, ids, vals, context=None): @api.multi
def write(self, values):
self._get_languages.clear_cache(self) self._get_languages.clear_cache(self)
return super(website, self).write(cr, uid, ids, vals, context) return super(Website, self).write(values)
def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None): #----------------------------------------------------------
context = context or {} # Page Management
imd = self.pool.get('ir.model.data') #----------------------------------------------------------
view = self.pool.get('ir.ui.view')
template_module, template_name = template.split('.') @api.model
def new_page(self, name, template='website.default_page', ispage=True):
""" Create a new website page, and assign it a xmlid based on the given one
:param name : the name of the page
:param template : potential xml_id of the page to create
"""
template_module, dummy = template.split('.')
website_id = self._context.get('website_id')
# completely arbitrary max_length # completely arbitrary max_length
page_name = slugify(name, max_length=50) page_name = slugify(name, max_length=50)
@@ -212,77 +216,76 @@ class website(osv.osv):
# find a free xmlid # find a free xmlid
inc = 0 inc = 0
dom = [('website_id', '=', False), ('website_id', '=', context.get('website_id'))] domain_static = [('website_id', '=', False), ('website_id', '=', website_id)]
while view.search(cr, openerp.SUPERUSER_ID, [('key', '=', page_xmlid), '|'] + dom, context=dict(context or {}, active_test=False)): while self.env['ir.ui.view'].with_context(active_test=False).sudo().search([('key', '=', page_xmlid), '|'] + domain_static):
inc += 1 inc += 1
page_xmlid = "%s.%s" % (template_module, page_name + (inc and "-%s" % inc or "")) page_xmlid = "%s.%s" % (template_module, page_name + ("-%s" % inc if inc else ""))
page_name += (inc and "-%s" % inc or "") page_name += (inc and "-%s" % inc or "")
# new page # new page
_, template_id = imd.get_object_reference(cr, uid, template_module, template_name) template_record = self.env.ref(template)
website_id = context.get('website_id') key = '%s.%s' % (template_module, page_name)
key = template_module+'.'+page_name page = template_record.copy({'website_id': website_id, 'key': key})
page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context) page.with_context(lang=None).write({
page = view.browse(cr, uid, page_id, context=dict(context, lang=None))
page.write({
'arch': page.arch.replace(template, page_xmlid), 'arch': page.arch.replace(template, page_xmlid),
'name': page_name, 'name': page_name,
'page': ispage, 'page': ispage,
}) })
return page_xmlid return page_xmlid
def key_to_view_id(self, cr, uid, view_id, context=None): def key_to_view_id(self, view_id):
View = self.pool.get('ir.ui.view') return self.env['ir.ui.view'].search([
return View.search(cr, uid, [
('id', '=', view_id), ('id', '=', view_id),
"|", ('website_id', '=', context.get('website_id')), ('website_id', '=', False), '|', ('website_id', '=', self._context.get('website_id')), ('website_id', '=', False),
('page', '=', True), ('page', '=', True),
('type', '=', 'qweb') ('type', '=', 'qweb')
], context=context) ])
def delete_page(self, cr, uid, view_id, context=None): @api.model
if context is None: def delete_page(self, view_id):
context = {} """ Delete a page, given its identifier
View = self.pool.get('ir.ui.view') :param view_id : ir.ui.view identifier
view_find = self.key_to_view_id(cr, uid, view_id, context=context) """
if view_find: view = self.key_to_view_id(view_id)
View.unlink(cr, uid, view_find, context=context) if view:
view.unlink()
def rename_page(self, cr, uid, view_id, new_name, context=None):
if context is None:
context = {}
View = self.pool.get('ir.ui.view')
view_find = self.key_to_view_id(cr, uid, view_id, context=context)
if view_find:
v = View.browse(cr, uid, view_find, context=context)
@api.model
def rename_page(self, view_id, new_name):
""" Change the name of the given page
:param view_id : id of the view to rename
:param new_name : name to use
"""
view = self.key_to_view_id(view_id)
if view:
# slugify the new name and prefix by module if
# not already done by end user
new_name = slugify(new_name, max_length=50) new_name = slugify(new_name, max_length=50)
# Prefix by module if not already done by end user prefix = view.key.split('.')[0]
prefix = v.key.split('.')[0]
if not new_name.startswith(prefix): if not new_name.startswith(prefix):
new_name = "%s.%s" % (prefix, new_name) new_name = "%s.%s" % (prefix, new_name)
View.write(cr, uid, view_find, { view.write({
'key': new_name, 'key': new_name,
'arch_db': v.arch_db.replace(v.key, new_name, 1) 'arch_db': view.arch_db.replace(view.key, new_name, 1)
}) })
return new_name return new_name
return False
def page_search_dependencies(self, cr, uid, view_id=False, context=None): @api.model
dep = {} def page_search_dependencies(self, view_id=False):
""" Search dependencies just for information. It will not catch 100%
of dependencies and False positive is more than possible
Each module could add dependences in this dict
:returns a dictionnary where key is the 'categorie' of object related to the given
view, and the value is the list of text and link to the resource using given page
"""
dependencies = {}
if not view_id: if not view_id:
return dep return dependencies
# search dependencies just for information. view = self.env['ir.ui.view'].browse(view_id)
# It will not catch 100% of dependencies and False positive is more than possible website_id = self._context.get('website_id')
# Each module could add dependences in this dict
if context is None:
context = {}
View = self.pool.get('ir.ui.view')
Menu = self.pool.get('website.menu')
view = View.browse(cr, uid, view_id, context=context)
website_id = context.get('website_id')
name = view.key.replace("website.", "") name = view.key.replace("website.", "")
fullname = "website.%s" % name fullname = "website.%s" % name
@@ -292,18 +295,18 @@ class website(osv.osv):
'|', ('website_id', '=', website_id), ('website_id', '=', False), '|', ('website_id', '=', website_id), ('website_id', '=', False),
'|', ('arch_db', 'ilike', '/page/%s' % name), ('arch_db', 'ilike', '/page/%s' % fullname) '|', ('arch_db', 'ilike', '/page/%s' % name), ('arch_db', 'ilike', '/page/%s' % fullname)
] ]
pages = View.search(cr, uid, page_search_dom, context=context)
if pages: page_key = _('Page')
page_key = _('Page') pages = self.env['ir.ui.view'].search(page_search_dom)
dep[page_key] = [] for page in pages:
for page in View.browse(cr, uid, pages, context=context): dependencies.setdefault(page_key, [])
if page.page: if page.page:
dep[page_key].append({ dependencies[page_key].append({
'text': _('Page <b>%s</b> contains a link to this page') % page.key, 'text': _('Page <b>%s</b> contains a link to this page') % page.key,
'link': '/page/%s' % page.key 'link': '/page/%s' % page.key
}) })
else: else:
dep[page_key].append({ dependencies[page_key].append({
'text': _('Template <b>%s (id:%s)</b> contains a link to this page') % (page.key, page.id), 'text': _('Template <b>%s (id:%s)</b> contains a link to this page') % (page.key, page.id),
'link': '#' 'link': '#'
}) })
@@ -314,73 +317,65 @@ class website(osv.osv):
'|', ('url', 'ilike', '/page/%s' % name), ('url', 'ilike', '/page/%s' % fullname) '|', ('url', 'ilike', '/page/%s' % name), ('url', 'ilike', '/page/%s' % fullname)
] ]
menus = Menu.search(cr, uid, menu_search_dom, context=context) menu_key = _('Menu')
if menus: menus = self.env['website.menu'].search(menu_search_dom)
menu_key = _('Menu') for menu in menus:
dep[menu_key] = [] dependencies.setdefault(menu_key, []).append({
for menu in Menu.browse(cr, uid, menus, context=context):
dep[menu_key].append({
'text': _('This page is in the menu <b>%s</b>') % menu.name, 'text': _('This page is in the menu <b>%s</b>') % menu.name,
'link': False 'link': False
}) })
return dep return dependencies
def page_for_name(self, cr, uid, ids, name, module='website', context=None): @api.model
# whatever def page_exists(self, name, module='website'):
return '%s.%s' % (module, slugify(name, max_length=50))
def page_exists(self, cr, uid, ids, name, module='website', context=None):
try: try:
name = (name or "").replace("/page/website.", "").replace("/page/", "") name = (name or "").replace("/page/website.", "").replace("/page/", "")
if not name: if not name:
return False return False
return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name) return self.env.ref('%s.%s' % module, name)
except: except:
return False return False
@openerp.tools.ormcache('id') #----------------------------------------------------------
def _get_languages(self, cr, uid, id, context=None): # Languages
website = self.browse(cr, uid, id) #----------------------------------------------------------
return [(lg.code, lg.name) for lg in website.language_ids]
def get_cdn_url(self, cr, uid, uri, context=None): @api.multi
# Currently only usable in a website_enable request context def get_languages(self):
if request and request.website and not request.debug and request.website.user_id.id == request.uid: self.ensure_one()
cdn_url = request.website.cdn_url return self._get_languages()
cdn_filters = (request.website.cdn_filters or '').splitlines()
for flt in cdn_filters:
if flt and re.match(flt, uri):
return urlparse.urljoin(cdn_url, uri)
return uri
def get_languages(self, cr, uid, ids, context=None): @tools.cache('self.id')
return self._get_languages(cr, uid, ids[0]) def _get_languages(self):
return [(lg.code, lg.name) for lg in self.language_ids]
def get_alternate_languages(self, cr, uid, ids, req=None, context=None): @api.multi
def get_alternate_languages(self, req=None):
langs = [] langs = []
if req is None: if req is None:
req = request.httprequest req = request.httprequest
default = self.get_current_website(cr, uid, context=context).default_lang_code default = self.get_current_website().default_lang_code
shorts = [] shorts = []
def get_url_localized(router, lang): def get_url_localized(router, lang):
arguments = dict(request.endpoint_arguments) arguments = dict(request.endpoint_arguments)
for k, v in arguments.items(): for key, val in arguments.items():
if isinstance(v, orm.browse_record): if isinstance(val, models.BaseModel):
arguments[k] = v.with_context(lang=lang) arguments[key] = val.with_context(lang=lang)
return router.build(request.endpoint, arguments) return router.build(request.endpoint, arguments)
router = request.httprequest.app.get_db_router(request.db).bind('') router = request.httprequest.app.get_db_router(request.db).bind('')
for code, name in self.get_languages(cr, uid, ids, context=context): for code, dummy in self.get_languages():
lg_path = ('/' + code) if code != default else '' lg_path = ('/' + code) if code != default else ''
lg = code.split('_') lg_codes = code.split('_')
shorts.append(lg[0]) shorts.append(lg_codes[0])
uri = request.endpoint and get_url_localized(router, code) or request.httprequest.path uri = get_url_localized(router, code) if request.endpoint else request.httprequest.path
if req.query_string: if req.query_string:
uri += '?' + req.query_string uri += '?' + req.query_string
lang = { lang = {
'hreflang': ('-'.join(lg)).lower(), 'hreflang': ('-'.join(lg_codes)).lower(),
'short': lg[0], 'short': lg_codes[0],
'href': req.url_root[0:-1] + lg_path + uri, 'href': req.url_root[0:-1] + lg_path + uri,
} }
langs.append(lang) langs.append(lang)
@@ -389,47 +384,68 @@ class website(osv.osv):
lang['hreflang'] = lang['short'] lang['hreflang'] = lang['short']
return langs return langs
@openerp.tools.ormcache('domain_name') #----------------------------------------------------------
def _get_current_website_id(self, cr, uid, domain_name, context=None): # Utilities
ids = self.search(cr, uid, [('domain', '=', domain_name)], limit=1, context=context) #----------------------------------------------------------
return ids and ids[0] or self.search(cr, uid, [], limit=1)[0]
def get_current_website(self, cr, uid, context=None): @api.model
def get_current_website(self):
domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
website_id = self._get_current_website_id(cr, uid, domain_name) website_id = self._get_current_website_id(domain_name)
request.context['website_id'] = website_id request.context['website_id'] = website_id
return self.browse(cr, uid, website_id, context=context) return self.browse(website_id)
def is_publisher(self, cr, uid, ids, context=None): @tools.cache('domain_name')
Access = self.pool['ir.model.access'] def _get_current_website_id(self, domain_name):
is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context) """ Reminder : cached method should be return record, since they will use a closed cursor. """
return is_website_publisher website = self.search([('domain', '=', domain_name)], limit=1)
if not website:
website = self.search([], limit=1)
return website.id
def is_user(self, cr, uid, ids, context=None): @api.model
Access = self.pool['ir.model.access'] def is_publisher(self):
return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context) return self.env['ir.model.access'].check('ir.ui.view', 'write', False)
def get_template(self, cr, uid, ids, template, context=None): @api.model
View = self.pool['ir.ui.view'] def is_user(self):
return self.env['ir.model.access'].check('ir.ui.menu', 'read', False)
@api.model
def get_template(self, template):
View = self.env['ir.ui.view']
if isinstance(template, (int, long)): if isinstance(template, (int, long)):
view_id = template view_id = template
else: else:
if '.' not in template: if '.' not in template:
template = 'website.%s' % template template = 'website.%s' % template
view_id = View.get_view_id(cr, uid, template, context=context) view_id = View.get_view_id(template)
if not view_id: if not view_id:
raise NotFound raise NotFound
return View.browse(cr, uid, view_id, context=context) return View.browse(view_id)
def _render(self, cr, uid, ids, template, values=None, context=None): @api.model
def render(self, template, values=None, status_code=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3) # TODO: remove this. (just kept for backward api compatibility for saas-3)
return self.pool['ir.ui.view'].render_template(cr, uid, template, values=values, context=context) return request.render(template, values, uid=self._uid)
def render(self, cr, uid, ids, template, values=None, status_code=None, context=None): def _render(self, template, values=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3) # TODO: remove this. (just kept for backward api compatibility for saas-3)
return request.render(template, values, uid=uid) return self.env['ir.ui.view'].render_template(template, values=values)
def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None): @api.model
def pager(self, url, total, page=1, step=30, scope=5, url_args=None):
""" Generate a dict with required value to render `website.pager` template. This method compute
url, page range to display, ... in the pager.
:param url : base url of the page link
:param total : number total of item to be splitted into pages
:param page : current page
:param step : item per page
:param scope : number of page to display on pager
:param url_args : additionnal parameters to add as query params to page url
:type url_args : dict
:returns dict
"""
# Compute Pager # Compute Pager
page_count = int(math.ceil(float(total) / step)) page_count = int(math.ceil(float(total) / step))
@@ -472,17 +488,15 @@ class website(osv.osv):
'num': pmax 'num': pmax
}, },
"pages": [ "pages": [
{'url': get_url(page), 'num': page} {'url': get_url(page), 'num': page} for page in xrange(pmin, pmax+1)
for page in xrange(pmin, pmax+1)
] ]
} }
def rule_is_enumerable(self, rule): def rule_is_enumerable(self, rule):
""" Checks that it is possible to generate sensible GET queries for """ Checks that it is possible to generate sensible GET queries for
a given rule (if the endpoint matches its own requirements) a given rule (if the endpoint matches its own requirements)
:type rule: werkzeug.routing.Rule
:type rule: werkzeug.routing.Rule :rtype: bool
:rtype: bool
""" """
endpoint = rule.endpoint endpoint = rule.endpoint
methods = endpoint.routing.get('methods') or ['GET'] methods = endpoint.routing.get('methods') or ['GET']
@@ -504,21 +518,20 @@ class website(osv.osv):
args = spec.args[1:(-defaults_count or None)] args = spec.args[1:(-defaults_count or None)]
# check that all args have a converter # check that all args have a converter
return all( (arg in rule._converters) for arg in args) return all((arg in rule._converters) for arg in args)
def enumerate_pages(self, cr, uid, ids, query_string=None, context=None): @api.multi
def enumerate_pages(self, query_string=None):
""" Available pages in the website/CMS. This is mostly used for links """ Available pages in the website/CMS. This is mostly used for links
generation and can be overridden by modules setting up new HTML generation and can be overridden by modules setting up new HTML
controllers for dynamic pages (e.g. blog). controllers for dynamic pages (e.g. blog).
By default, returns template views marked as pages.
By default, returns template views marked as pages. :param str query_string: a (user-provided) string, fetches pages
matching the string
:param str query_string: a (user-provided) string, fetches pages :returns: a list of mappings with two keys: ``name`` is the displayable
matching the string name of the resource (page), ``url`` is the absolute URL
:returns: a list of mappings with two keys: ``name`` is the displayable of the same.
name of the resource (page), ``url`` is the absolute URL :rtype: list({name: str, url: str})
of the same.
:rtype: list({name: str, url: str})
""" """
router = request.httprequest.app.get_db_router(request.db) router = request.httprequest.app.get_db_router(request.db)
# Force enumeration to be performed as public user # Force enumeration to be performed as public user
@@ -533,23 +546,23 @@ class website(osv.osv):
values = [{}] values = [{}]
convitems = converters.items() convitems = converters.items()
# converters with a domain are processed after the other ones # converters with a domain are processed after the other ones
gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]') gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain != '[]')
convitems.sort(lambda x, y: cmp(gd(x), gd(y))) convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
for (i,(name, converter)) in enumerate(convitems): for (i, (name, converter)) in enumerate(convitems):
newval = [] newval = []
for val in values: for val in values:
query = i==(len(convitems)-1) and query_string query = i == len(convitems)-1 and query_string
for v in converter.generate(request.cr, uid, query=query, args=val, context=context): for value_dict in converter.generate(query=query, args=val):
newval.append( val.copy() ) newval.append(val.copy())
v[name] = v['loc'] value_dict[name] = value_dict['loc']
del v['loc'] del value_dict['loc']
newval[-1].update(v) newval[-1].update(value_dict)
values = newval values = newval
for value in values: for value in values:
domain_part, url = rule.build(value, append_unknown=False) domain_part, url = rule.build(value, append_unknown=False)
page = {'loc': url} page = {'loc': url}
for key,val in value.items(): for key, val in value.items():
if key.startswith('__'): if key.startswith('__'):
page[key[2:]] = val page[key[2:]] = val
if url in ('/sitemap.xml',): if url in ('/sitemap.xml',):
@@ -560,54 +573,63 @@ class website(osv.osv):
yield page yield page
def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None): @api.multi
def search_pages(self, needle=None, limit=None):
name = re.sub(r"^/p(a(g(e(/(w(e(b(s(i(t(e(\.)?)?)?)?)?)?)?)?)?)?)?)?", "", needle or "") name = re.sub(r"^/p(a(g(e(/(w(e(b(s(i(t(e(\.)?)?)?)?)?)?)?)?)?)?)?)?", "", needle or "")
res = [] res = []
for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context): for page in self.enumerate_pages(query_string=name):
if needle in page['loc']: if needle in page['loc']:
res.append(page) res.append(page)
if len(res) == limit: if len(res) == limit:
break break
return res return res
def image_url(self, cr, uid, record, field, size=None, context=None): @api.model
"""Returns a local url that points to the image field of a given browse record.""" def image_url(self, record, field, size=None):
""" Returns a local url that points to the image field of a given browse record. """
sudo_record = record.sudo() sudo_record = record.sudo()
sha = hashlib.sha1(getattr(sudo_record, '__last_update')).hexdigest()[0:7] sha = hashlib.sha1(getattr(sudo_record, '__last_update')).hexdigest()[0:7]
size = '' if size is None else '/%s' % size size = '' if size is None else '/%s' % size
return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha) return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha)
class website_menu(osv.osv): @api.model
def get_cdn_url(self, uri):
# Currently only usable in a website_enable request context
if request and request.website and not request.debug and request.website.user_id.id == request.uid:
cdn_url = request.website.cdn_url
cdn_filters = (request.website.cdn_filters or '').splitlines()
for flt in cdn_filters:
if flt and re.match(flt, uri):
return urlparse.urljoin(cdn_url, uri)
return uri
class Menu(models.Model):
_name = "website.menu" _name = "website.menu"
_description = "Website Menu" _description = "Website Menu"
_columns = {
'name': fields.char('Menu', required=True, translate=True),
'url': fields.char('Url'),
'new_window': fields.boolean('New Window'),
'sequence': fields.integer('Sequence'),
# TODO: support multiwebsite once done for ir.ui.views
'website_id': fields.many2one('website', 'Website'),
'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
'parent_left': fields.integer('Parent Left', select=True),
'parent_right': fields.integer('Parent Right', select=True),
}
def __defaults_sequence(self, cr, uid, context):
menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
return menu and menu[0]["sequence"] or 0
_defaults = {
'url': '',
'sequence': __defaults_sequence,
'new_window': False,
}
_parent_store = True _parent_store = True
_parent_order = 'sequence' _parent_order = 'sequence'
_order = "sequence" _order = "sequence"
def _default_sequence(self):
menu = self.search(["sequence"], limit=1, order="sequence DESC")
return menu.sequence or 0
name = fields.Char('Menu', required=True, translate=True)
url = fields.Char('Url', default='')
new_window = fields.Boolean('New Window')
sequence = fields.Integer(defualt=_default_sequence)
website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_left = fields.Integer('Parent Left', index=True)
parent_right = fields.Integer('Parent Rigth', index=True)
# would be better to take a menu_id as argument # would be better to take a menu_id as argument
def get_tree(self, cr, uid, website_id, menu_id=None, context=None): @api.model
def get_tree(self, website_id, menu_id=None):
def make_tree(node): def make_tree(node):
menu_node = dict( menu_node = dict(
id=node.id, id=node.id,
@@ -622,12 +644,13 @@ class website_menu(osv.osv):
menu_node['children'].append(make_tree(child)) menu_node['children'].append(make_tree(child))
return menu_node return menu_node
if menu_id: if menu_id:
menu = self.browse(cr, uid, menu_id, context=context) menu = self.browse(menu_id)
else: else:
menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id menu = self.env['website'].browse(website_id).menu_id
return make_tree(menu) return make_tree(menu)
def save(self, cr, uid, website_id, data, context=None): @api.model
def save(self, website_id, data):
def replace_id(old_id, new_id): def replace_id(old_id, new_id):
for menu in data['data']: for menu in data['data']:
if menu['id'] == old_id: if menu['id'] == old_id:
@@ -636,52 +659,54 @@ class website_menu(osv.osv):
menu['parent_id'] = new_id menu['parent_id'] = new_id
to_delete = data['to_delete'] to_delete = data['to_delete']
if to_delete: if to_delete:
self.unlink(cr, uid, to_delete, context=context) self.browse(to_delete).unlink()
for menu in data['data']: for menu in data['data']:
mid = menu['id'] mid = menu['id']
if isinstance(mid, basestring): if isinstance(mid, basestring):
new_id = self.create(cr, uid, {'name': menu['name']}, context=context) new_menu = self.create({'name': menu['name']})
replace_id(mid, new_id) replace_id(mid, new_menu.id)
for menu in data['data']: for menu in data['data']:
self.write(cr, uid, [menu['id']], menu, context=context) self.browse(menu['id']).write(menu)
return True return True
class website_seo_metadata(osv.AbstractModel): class SeoMetadata(models.AbstractModel):
_name = 'website.seo.metadata' _name = 'website.seo.metadata'
_description = 'SEO metadata' _description = 'SEO metadata'
_columns = { website_meta_title = fields.Char("Website meta title", translate=True)
'website_meta_title': fields.char("Website meta title", translate=True), website_meta_description = fields.Text("Website meta description", translate=True)
'website_meta_description': fields.text("Website meta description", translate=True), website_meta_keywords = fields.Char("Website meta keywords", translate=True)
'website_meta_keywords': fields.char("Website meta keywords", translate=True),
}
class website_published_mixin(osv.AbstractModel): class WebsitePublishedMixin(models.AbstractModel):
_name = "website.published.mixin" _name = "website.published.mixin"
_website_url_proxy = lambda self, *a, **kw: self._website_url(*a, **kw) website_published = fields.Boolean('Visible in Website', copy=False)
website_url = fields.Char('Website URL', compute='_website_url_proxy', help='The full URL to access the document through the website.')
_columns = { # TODO remove proxy and _website_url unused arguments
'website_published': fields.boolean('Visible in Website', copy=False), def _website_url_proxy(self):
'website_url': fields.function(_website_url_proxy, type='char', string='Website URL', result = self._website_url(False, False)
help='The full URL to access the document through the website.'), for website in self:
} website.website_url = result.get(website.id)
def _website_url(self, cr, uid, ids, field_name, arg, context=None): @api.multi
return dict.fromkeys(ids, '#') def _website_url(self, field_name, arg):
return dict.fromkeys(self.ids, '#')
def website_publish_button(self, cr, uid, ids, context=None): @api.multi
for i in self.browse(cr, uid, ids, context): def website_publish_button(self):
if self.pool['res.users'].has_group(cr, uid, 'base.group_website_publisher') and i.website_url != '#': self.ensure_one()
return self.open_website_url(cr, uid, ids, context) if self.env.user.has_group('base.group_website_publisher') and self.website_url != '#':
i.write({'website_published': not i.website_published}) return self.open_website_url()
return True return self.write({'website_published': not self.website_published})
def open_website_url(self, cr, uid, ids, context=None): def open_website_url(self):
return { return {
'type': 'ir.actions.act_url', 'type': 'ir.actions.act_url',
'url': self.browse(cr, uid, ids[0]).website_url, 'url': self.website_url,
'target': 'self', 'target': 'self',
} }
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <odoo>
<data>
<record id="website_designer_edit_qweb" model="ir.rule"> <record id="website_designer_edit_qweb" model="ir.rule">
<field name="name">website_designer: Manage Website and qWeb view</field> <field name="name">website_designer: Manage Website and qWeb view</field>
<field name="model_id" ref="base.model_ir_ui_view"/> <field name="model_id" ref="base.model_ir_ui_view"/>
@@ -31,5 +30,4 @@
<field name="perm_create" eval="True"/> <field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/> <field name="perm_unlink" eval="True"/>
</record> </record>
</data> </odoo>
</openerp>
+1 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import unittest import unittest
from openerp.addons.website.models.website import slugify, unslug from odoo.addons.website.models.website import slugify, unslug
class TestUnslug(unittest.TestCase): class TestUnslug(unittest.TestCase):
def test_unslug(self): def test_unslug(self):
+8 -9
View File
@@ -5,13 +5,13 @@ import time
import lxml.html import lxml.html
import openerp import odoo
import re import re
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class Crawler(openerp.tests.HttpCase): class Crawler(odoo.tests.HttpCase):
""" Test suite crawling an openerp CMS instance and checking that all """ Test suite crawling an Odoo CMS instance and checking that all
internal links lead to a 200 response. internal links lead to a 200 response.
If a username and a password are provided, authenticates the user before If a username and a password are provided, authenticates the user before
@@ -22,7 +22,7 @@ class Crawler(openerp.tests.HttpCase):
post_install = True post_install = True
def crawl(self, url, seen=None, msg=''): def crawl(self, url, seen=None, msg=''):
if seen == None: if seen is None:
seen = set() seen = set()
url_slug = re.sub(r"[/](([^/=?&]+-)?[0-9]+)([/]|$)", '/<slug>/', url) url_slug = re.sub(r"[/](([^/=?&]+-)?[0-9]+)([/]|$)", '/<slug>/', url)
@@ -35,7 +35,7 @@ class Crawler(openerp.tests.HttpCase):
_logger.info("%s %s", msg, url) _logger.info("%s %s", msg, url)
r = self.url_open(url) r = self.url_open(url)
code = r.getcode() code = r.getcode()
self.assertIn( code, xrange(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code)) self.assertIn(code, xrange(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code))
if r.info().gettype() == 'text/html': if r.info().gettype() == 'text/html':
doc = lxml.html.fromstring(r.read()) doc = lxml.html.fromstring(r.read())
@@ -64,7 +64,6 @@ class Crawler(openerp.tests.HttpCase):
self.crawl(href, seen, msg) self.crawl(href, seen, msg)
return seen return seen
def test_10_crawl_public(self): def test_10_crawl_public(self):
t0 = time.time() t0 = time.time()
t0_sql = self.registry.test_cr.sql_log_count t0_sql = self.registry.test_cr.sql_log_count
@@ -72,7 +71,7 @@ class Crawler(openerp.tests.HttpCase):
count = len(seen) count = len(seen)
duration = time.time() - t0 duration = time.time() - t0
sql = self.registry.test_cr.sql_log_count - t0_sql sql = self.registry.test_cr.sql_log_count - t0_sql
_logger.log(25, "public crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request, ", count, duration, sql, duration/count, float(sql)/count) _logger.log(25, "public crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request, ", count, duration, sql, duration / count, float(sql) / count)
def test_20_crawl_demo(self): def test_20_crawl_demo(self):
t0 = time.time() t0 = time.time()
@@ -82,7 +81,7 @@ class Crawler(openerp.tests.HttpCase):
count = len(seen) count = len(seen)
duration = time.time() - t0 duration = time.time() - t0
sql = self.registry.test_cr.sql_log_count - t0_sql sql = self.registry.test_cr.sql_log_count - t0_sql
_logger.log(25, "demo crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration/count, float(sql)/count) _logger.log(25, "demo crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration / count, float(sql) / count)
def test_30_crawl_admin(self): def test_30_crawl_admin(self):
t0 = time.time() t0 = time.time()
@@ -92,4 +91,4 @@ class Crawler(openerp.tests.HttpCase):
count = len(seen) count = len(seen)
duration = time.time() - t0 duration = time.time() - t0
sql = self.registry.test_cr.sql_log_count - t0_sql sql = self.registry.test_cr.sql_log_count - t0_sql
_logger.log(25, "admin crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration/count, float(sql)/count) _logger.log(25, "admin crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration / count, float(sql) / count)
+2 -2
View File
@@ -1,7 +1,7 @@
import openerp.tests import odoo.tests
class TestUi(openerp.tests.HttpCase): class TestUi(odoo.tests.HttpCase):
def test_01_public_homepage(self): def test_01_public_homepage(self):
self.phantom_js("/", "console.log('ok')", "'website.snippets.animation' in odoo.__DEBUG__.services") self.phantom_js("/", "console.log('ok')", "'website.snippets.animation' in odoo.__DEBUG__.services")
+3 -5
View File
@@ -208,18 +208,16 @@ class TestViewSaving(common.TransactionCase):
def test_save_only_embedded(self): def test_save_only_embedded(self):
Company = self.registry('res.company') Company = self.registry('res.company')
company_id = 1 company_id = 1
Company.write(self.cr, self.uid, company_id, {'name': "Foo Corporation"}) company = Company.browse(company_id)
company.write({'name': "Foo Corporation"})
node = html.tostring(h.SPAN( node = html.tostring(h.SPAN(
"Acme Corporation", "Acme Corporation",
attrs(model='res.company', id=company_id, field="name", expression='bob', type='char'))) attrs(model='res.company', id=company_id, field="name", expression='bob', type='char')))
View = self.env['ir.ui.view'] View = self.env['ir.ui.view']
View.browse(company_id).save(value=node) View.browse(company_id).save(value=node)
company = Company.browse(self.cr, self.uid, company_id)
self.assertEqual(company.name, "Acme Corporation") self.assertEqual(company.name, "Acme Corporation")
def test_field_tail(self): def test_field_tail(self):
View = self.env['ir.ui.view'] View = self.env['ir.ui.view']
replacement = ET.tostring( replacement = ET.tostring(
@@ -231,7 +229,7 @@ class TestViewSaving(common.TransactionCase):
View.browse(self.view_id).save(value=replacement, xpath='/div/div[2]/ul/li[3]') View.browse(self.view_id).save(value=replacement, xpath='/div/div[2]/ul/li[3]')
self.eq( self.eq(
ET.fromstring(View.browse(self.view_id).arch.encode('utf-8')), ET.fromstring(self.view_id.arch.encode('utf-8')),
h.DIV( h.DIV(
h.DIV( h.DIV(
h.H3("Column 1"), h.H3("Column 1"),
+3 -5
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <odoo>
<data>
<record id="view_website_config_settings" model="ir.ui.view"> <record id="view_website_config_settings" model="ir.ui.view">
<field name="name">Website settings</field> <field name="name">Website settings</field>
@@ -12,7 +11,7 @@
<button string="Cancel" type="object" name="cancel" class="oe_link"/> <button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header> </header>
<div> <div>
<field name="website_id" on_change="on_change_website_id(website_id)" invisible="1"/> <field name="website_id" invisible="1"/>
<group string="Domain"> <group string="Domain">
<field name="website_name" /> <field name="website_name" />
<label for="google_analytics_key"/> <label for="google_analytics_key"/>
@@ -159,5 +158,4 @@
sequence="100" name="Configuration" groups="base.group_system"/> sequence="100" name="Configuration" groups="base.group_system"/>
<menuitem id="menu_website_website_settings" parent="menu_website_global_configuration" name="Settings" action="action_website_configuration"/> <menuitem id="menu_website_website_settings" parent="menu_website_global_configuration" name="Settings" action="action_website_configuration"/>
</data> </odoo>
</openerp>
+17 -23
View File
@@ -1,41 +1,35 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import openerp from odoo import api, fields, models
from openerp.osv import orm, osv, fields
class base_language_install(osv.osv_memory): class BaseLanguageInstall(models.TransientModel):
_inherit = "base.language.install" _inherit = "base.language.install"
_columns = {
'website_ids': fields.many2many('website', string='Websites to translate'),
}
def default_get(self, cr, uid, fields, context=None): website_ids = fields.Many2many('website', string='Websites to translate')
if context is None:
context = {} @api.model
defaults = super(base_language_install, self).default_get(cr, uid, fields, context) def default_get(self, fields):
website_id = context.get('params', {}).get('website_id') defaults = super(BaseLanguageInstall, self).default_get(fields)
website_id = self._context.get('params', {}).get('website_id')
if website_id: if website_id:
if 'website_ids' not in defaults: if 'website_ids' not in defaults:
defaults['website_ids'] = [] defaults['website_ids'] = []
defaults['website_ids'].append(website_id) defaults['website_ids'].append(website_id)
return defaults return defaults
def lang_install(self, cr, uid, ids, context=None): @api.multi
if context is None: def lang_install(self):
context = {} action = super(BaseLanguageInstall, self).lang_install()
action = super(base_language_install, self).lang_install(cr, uid, ids, context) lang = self.env['res.lang'].search([('code', '=', self.lang)], limit=1)
language_obj = self.browse(cr, uid, ids)[0] if self.website_ids and lang:
website_ids = [website.id for website in language_obj['website_ids']] self.website_ids.write({'language_ids': [(4, lang.id)]})
lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])]) params = self._context.get('params', {})
if website_ids and lang_id:
data = {'language_ids': [(4, lang_id[0])]}
self.pool['website'].write(cr, uid, website_ids, data)
params = context.get('params', {})
if 'url_return' in params: if 'url_return' in params:
return { return {
'url': params['url_return'].replace('[lang]', language_obj['lang']), 'url': params['url_return'].replace('[lang]', self.lang),
'type': 'ir.actions.act_url', 'type': 'ir.actions.act_url',
'target': 'self' 'target': 'self'
} }