[FIX] base: fix and complete migration of base/ir

- add or fix decorators on methods
- fix most compute methods (wrong dependencies)
- revert changes in `eval_context` of `ir.actions`
- various code simplifications and improvements
- migrate methods that were not
- ir_qweb: modify `QWebContext` to take an environment instead of `cr`, `uid`, `context`
- ir_qweb: convert `AssetsBundle` to use new API `env`
- ir_ui_view: use decorator `multi` on method `read_combined`
- ir_ui_view: ensure that method `render` is never called with an xml_id
- ir_ui_view: factor out part of the `ormcache` key of method `_read_template`
This commit is contained in:
Raphael Collet
2016-03-21 13:46:44 +01:00
parent eb04aa1e39
commit 4ddc323139
47 changed files with 2150 additions and 2376 deletions
+9 -7
View File
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import models
from openerp import SUPERUSER_ID
from openerp.osv import osv
class IrConfigParameter(models.Model):
class ir_configparameter(osv.Model):
_inherit = 'ir.config_parameter'
def init(self, force=False):
super(IrConfigParameter, self).init(force=force)
def init(self, cr, force=False):
super(ir_configparameter, self).init(cr, force=force)
if force:
oauth_oe = self.env.ref('auth_oauth.provider_openerp')
oauth_oe.write({'client_id': self.sudo().get_param('database.uuid')})
IMD = self.pool['ir.model.data']
oauth_oe = IMD.xmlid_to_object(cr, SUPERUSER_ID, 'auth_oauth.provider_openerp')
dbuuid = self.get_param(cr, SUPERUSER_ID, 'database.uuid')
oauth_oe.write({'client_id': dbuuid})
+1 -1
View File
@@ -3,7 +3,7 @@
from openerp import models
class AutoVacuum(models.TransientModel):
class AutoVacuum(models.AbstractModel):
_inherit = 'ir.autovacuum'
def power_on(self, cr, uid, *args, **kwargs):
@@ -22,4 +22,4 @@ class Web_Editor(Web_Editor):
@http.route(['/mass_mailing/snippets'], type='json', auth="user", website=True)
def mass_mailing_snippets(self):
values = {'company_id': request.env['res.users'].browse(request.uid).company_id}
return request.registry["ir.ui.view"].render(request.cr, request.uid, 'mass_mailing.email_designer_snippets', values, context=request.context)
return request.env.ref('mass_mailing.email_designer_snippets').render(values)
+2 -3
View File
@@ -279,8 +279,7 @@ class PaymentAcquirer(osv.Model):
})
values.setdefault('return_url', False)
# because render accepts view ids but not qweb -> need to use the xml_id
return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, values, engine='ir.qweb', context=context)
return acquirer.view_template_id.render(values, engine='ir.qweb')
def _registration_render(self, cr, uid, id, partner_id, qweb_context=None, context=None):
acquirer = self.browse(cr, uid, id, context=context)
@@ -291,7 +290,7 @@ class PaymentAcquirer(osv.Model):
if hasattr(self, method_name):
method = getattr(self, method_name)
qweb_context.update(method(cr, uid, id, qweb_context, context=context))
return self.pool['ir.ui.view'].render(cr, uid, acquirer.registration_view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
return acquirer.registration_view_template_id.render(qweb_context, engine='ir.qweb')
def s2s_process(self, cr, uid, id, data, context=None):
acquirer = self.browse(cr, uid, id, context=context)
+2 -2
View File
@@ -103,7 +103,7 @@ class Report(osv.Model):
res_company=user.company_id,
website=website,
)
return view_obj.render(cr, uid, template, values, context=context)
return view_obj.render_template(cr, uid, template, values, context=context)
#--------------------------------------------------------------------------
# Main report methods
@@ -184,7 +184,7 @@ class Report(osv.Model):
# Minimal page renderer
view_obj = self.pool['ir.ui.view']
render_minimal = partial(view_obj.render, cr, uid, 'report.minimal_layout', context=context)
render_minimal = partial(view_obj.render_template, cr, uid, 'report.minimal_layout', context=context)
# The received html report must be simplified. We convert it in a xml tree
# in order to extract headers, bodies and footers.
+4 -4
View File
@@ -25,7 +25,7 @@ class ir_actions_report_xml(orm.Model):
" is printed on a separate HTML but memory and disk usage are wider.")
}
def _lookup_report(self, name):
def _lookup_report(self, cr, name):
"""
Look up a report definition.
"""
@@ -41,8 +41,8 @@ class ir_actions_report_xml(orm.Model):
if not isinstance(new_report, WebKitParser):
new_report = None
else:
self.env.cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
r = self.env.cr.dictfetchone()
cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
r = cr.dictfetchone()
if r:
if r['parser']:
parser = operator.attrgetter(r['parser'])(openerp.addons)
@@ -59,4 +59,4 @@ class ir_actions_report_xml(orm.Model):
if new_report:
return new_report
else:
return super(ir_actions_report_xml, self)._lookup_report(name)
return super(ir_actions_report_xml, self)._lookup_report(cr, name)
+1 -1
View File
@@ -18,7 +18,7 @@ class Web_Editor(http.Controller):
#------------------------------------------------------
@http.route('/web_editor/snippets', type='json', auth="user")
def snippets(self, **kwargs):
return request.registry["ir.ui.view"].render(request.cr, request.uid, 'web_editor.snippets', None, context=request.context)
return request.env.ref('web_editor.snippets').render(None)
#------------------------------------------------------
# Backend html field
-6
View File
@@ -57,9 +57,6 @@ class QWeb(orm.AbstractModel):
template_attributes['call'] = template_attributes['snippet']
return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
def get_converter_for(self, field_type):
return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
#------------------------------------------------------
# QWeb fields
@@ -102,9 +99,6 @@ class Field(orm.AbstractModel):
def from_html(self, cr, uid, model, field, element, context=None):
return self.value_from_string(element.text_content().strip())
def qweb_object(self):
return self.pool['ir.qweb']
class Integer(orm.AbstractModel):
_name = 'ir.qweb.field.integer'
+2
View File
@@ -12,6 +12,8 @@ class view(osv.osv):
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
if not values:
values = {}
if values.get('editable'):
+22 -40
View File
@@ -6,7 +6,7 @@ from lxml import etree, html
from lxml.builder import E
from openerp.tests import common
from openerp.addons.base.ir import ir_qweb
from openerp.addons.base.ir.ir_qweb import QWebContext
from openerp.addons.web_editor.models.ir_qweb import html_to_text
@@ -116,32 +116,25 @@ class TestHTMLToText(unittest.TestCase):
class TestConvertBack(common.TransactionCase):
def setUp(self):
super(TestConvertBack, self).setUp()
self.env = self.env(context={'inherit_branding': True})
def qwebcontext(self, data):
return QWebContext(self.env, data)
def field_rountrip_result(self, field, value, expected):
model = 'web_editor.converter.test'
Model = self.registry(model)
id = Model.create(
self.cr, self.uid, {
field: value
})
[record] = Model.browse(self.cr, self.uid, [id])
record = self.env[model].create({field: value})
e = etree.Element('span')
field_value = 'record.%s' % field
e.set('t-field', field_value)
rendered = self.registry('ir.qweb').render_tag_field(
e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
'record': record,
}, context={'inherit_branding': True}))
element = html.fromstring(
rendered, parser=html.HTMLParser(encoding='utf-8'))
qwebcontext = self.qwebcontext({'record': record})
rendered = self.env['ir.qweb'].render_tag_field(e, {'field': field_value}, '', qwebcontext)
element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
converter = self.registry('ir.qweb').get_converter_for(
element.get('data-oe-type'))
value_back = converter.from_html(
self.cr, self.uid, model, Model._fields[field], element)
converter = self.env['ir.qweb'].get_converter_for(element.get('data-oe-type'))
value_back = converter.from_html(model, record._fields[field], element)
if isinstance(expected, str):
expected = expected.decode('utf-8')
@@ -162,7 +155,6 @@ class TestConvertBack(common.TransactionCase):
def test_char(self):
self.field_roundtrip('char', "foo bar")
self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ")
def test_selection(self):
@@ -199,45 +191,35 @@ class TestConvertBack(common.TransactionCase):
model = 'web_editor.converter.test'
field = 'many2one'
Sub = self.registry('web_editor.converter.test.sub')
sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"})
sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"})
Model = self.registry(model)
id = Model.create(self.cr, self.uid, {field: sub_id})
[record] = Model.browse(self.cr, self.uid, [id])
subrec1 = self.env['web_editor.converter.test.sub'].create({'name': "Foo"})
subrec2 = self.env['web_editor.converter.test.sub'].create({'name': "Bar"})
record = self.env[model].create({field: subrec1.id})
e = etree.Element('span')
field_value = 'record.%s' % field
e.set('t-field', field_value)
rendered = self.registry('ir.qweb').render_tag_field(
e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
'record': record,
}, context={'inherit_branding': True}))
qwebcontext = self.qwebcontext({'record': record})
rendered = self.env['ir.qweb'].render_tag_field(e, {'field': field_value}, '', qwebcontext)
element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
# emulate edition
element.set('data-oe-many2one-id', str(sub2_id))
element.set('data-oe-many2one-id', str(subrec2.id))
element.text = "New content"
converter = self.registry('ir.qweb').get_converter_for(
element.get('data-oe-type'))
value_back = converter.from_html(
self.cr, self.uid, model, Model._fields[field], element)
converter = self.env['ir.qweb'].get_converter_for(element.get('data-oe-type'))
value_back = converter.from_html(model, record._fields[field], element)
self.assertIsNone(
value_back, "the m2o converter should return None to avoid spurious"
" or useless writes on the parent record")
self.assertEqual(
Sub.browse(self.cr, self.uid, sub_id).name,
subrec1.name,
"Foo",
"element edition can't change directly the m2o record"
)
self.assertEqual(
Model.browse(self.cr, self.uid, id).many2one.name,
record.many2one.name,
"Bar",
"element edition should have been change the m2o id"
)
+6 -5
View File
@@ -135,9 +135,9 @@ class Website(openerp.addons.web.controllers.main.Home):
'locs': islice(locs, 0, LOC_PER_SITEMAP),
'url_root': request.httprequest.url_root[:-1],
}
urls = iuv.render(cr, uid, 'website.sitemap_locs', values, context=context)
urls = iuv.render_template(cr, uid, 'website.sitemap_locs', values, context=context)
if urls.strip():
content = iuv.render(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context)
content = iuv.render_template(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context)
pages += 1
last = create_sitemap('/sitemap-%d.xml' % pages, content)
else:
@@ -148,7 +148,7 @@ class Website(openerp.addons.web.controllers.main.Home):
ira.write(cr, uid, last, dict(url="/sitemap.xml", name="/sitemap.xml"), context=context)
else:
# Sitemaps must be split in several smaller files with a sitemap index
content = iuv.render(cr, uid, 'website.sitemap_index_xml', dict(
content = iuv.render_template(cr, uid, 'website.sitemap_index_xml', dict(
pages=range(1, pages + 1),
url_root=request.httprequest.url_root,
), context=context)
@@ -317,7 +317,7 @@ class Website(openerp.addons.web.controllers.main.Home):
set_active(enable, True)
if get_bundle:
bundle = AssetsBundle('web.assets_frontend', cr=http.request.cr, uid=http.request.uid, context={}, registry=http.request.registry)
bundle = AssetsBundle('web.assets_frontend', env=request.env(context={}))
return bundle.to_html()
return True
@@ -329,9 +329,10 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route(['/website/multi_render'], type='json', auth="public", website=True)
def multi_render(self, ids_or_xml_ids, values=None):
View = request.env['ir.ui.view']
res = {}
for id_or_xml_id in ids_or_xml_ids:
res[id_or_xml_id] = request.registry["ir.ui.view"].render(request.cr, request.uid, id_or_xml_id, values=values, engine='ir.qweb', context=request.context)
res[id_or_xml_id] = View.render_template(id_or_xml_id, values)
return res
#------------------------------------------------------
+10 -13
View File
@@ -2,8 +2,8 @@
import urlparse
from odoo.http import request
from odoo import api, fields, models
from odoo.http import request
class actions_server(models.Model):
@@ -11,7 +11,7 @@ class actions_server(models.Model):
_name = 'ir.actions.server'
_inherit = 'ir.actions.server'
xml_id = fields.Char(compute=models.Model.get_xml_id, string="External ID",
xml_id = fields.Char(compute='_compute_xml_id', string="External ID",
help="ID of the action if defined in a XML file")
website_path = fields.Char()
website_url = fields.Char(compute='_get_website_url', help='The full URL to access the server action through the website.')
@@ -21,6 +21,11 @@ class actions_server(models.Model):
'Set this field as True to allow users to run this action. If it '
'is set to False the action cannot be run through the website.')
def _compute_xml_id(self):
res = self.get_external_id()
for action in self:
action.xml_id = res.get(action.id)
def _compute_website_url(self, website_path, xml_id):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
link = website_path or xml_id or (self.id and '%d' % self.id) or ''
@@ -29,19 +34,12 @@ class actions_server(models.Model):
return '%s' % urlparse.urljoin(base_url, path)
return ''
@api.depends('website_url')
@api.depends('state', 'website_published', 'website_path', 'xml_id')
def _get_website_url(self):
for action in self:
if action.state == 'code' and action.website_published:
action.website_url = self._compute_website_url(action.website_path, action.xml_id)
@api.onchange('website_path', 'xml_id')
def on_change_website_path(self):
values = {
'website_url': self._compute_website_url(self.website_path, self.xml_id)
}
return {'value': values}
@api.model
def _get_eval_context(self, action):
""" Override to add the request object in eval_context. """
@@ -50,11 +48,10 @@ class actions_server(models.Model):
eval_context['request'] = request
return eval_context
@api.model
def run_action_code_multi(self, action, eval_context=None):
""" Override to allow returning response the same way action is already
returned by the basic server action behavior. Note that response has
priority over action, avoid using both. """
res = super(actions_server, self).run_action_code_multi(action, eval_context)
if 'response' in eval_context:
return eval_context['response']
return res
return eval_context.get('response', res)
+1 -1
View File
@@ -417,7 +417,7 @@ class website(osv.osv):
def _render(self, cr, uid, ids, template, values=None, context=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3)
return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
return self.pool['ir.ui.view'].render_template(cr, uid, template, values=values, context=context)
def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3)
@@ -195,7 +195,7 @@ class MailGroup(http.Controller):
'msg_more_count': msg_count - self._replies_per_page,
'replies_per_page': self._replies_per_page,
}
return request.registry['ir.ui.view'].render(request.cr, request.uid, 'website_mail_channel.messages_short', values, engine='ir.qweb', context=request.context)
return request.env.ref('website_mail_channel.messages_short').render(values, engine='ir.qweb')
@http.route("/groups/<model('mail.channel'):group>/get_alias_info", type='json', auth='public', website=True)
def get_alias_info(self, group, **post):
+1 -1
View File
@@ -62,7 +62,7 @@
<field name="name">Choose Your Theme Todo Action</field>
<field name="model_id" ref="model_ir_module_module"/>
<field name="code">
self.update_list()
self.update_list(cr, uid, context=context)
action = {
'type': 'ir.actions.act_url',
'url': '/web?reload#action=website_theme_install.theme_install_kanban_action', # the ?reload option is there to fool the webclient into thinking it is a different location and so to force a reload
File diff suppressed because it is too large Load Diff
+188 -225
View File
@@ -7,109 +7,105 @@ import logging
import mimetypes
import os
import re
from collections import defaultdict
from openerp import SUPERUSER_ID, tools
from openerp.exceptions import AccessError
from openerp.osv import fields,osv
from openerp.tools.translate import _
from openerp.tools.misc import ustr
from openerp.tools.mimetypes import guess_mimetype
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.exceptions import AccessError
from odoo.tools import config, human_size, ustr
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
class ir_attachment(osv.osv):
class IrAttachment(models.Model):
"""Attachments are used to link binary files or url to any openerp document.
External attachment storage
---------------------------
The 'data' function field (_data_get,data_set) is implemented using
_file_read, _file_write and _file_delete which can be overridden to
implement other storage engines, such methods should check for other
location pseudo uri (example: hdfs://hadoppserver)
The computed field ``datas`` is implemented using ``_file_read``,
``_file_write`` and ``_file_delete``, which can be overridden to implement
other storage engines. Such methods should check for other location pseudo
uri (example: hdfs://hadoppserver).
The default implementation is the file:dirname location that stores files
on the local filesystem using name based on their sha1 hash
"""
_name = 'ir.attachment'
_order = 'id desc'
def _name_get_resname(self, cr, uid, ids, object, method, context):
data = {}
for attachment in self.browse(cr, uid, ids, context=context):
model_object = attachment.res_model
res_id = attachment.res_id
if model_object and res_id:
model_pool = self.pool[model_object]
res = model_pool.name_get(cr,uid,[res_id],context)
res_name = res and res[0][1] or None
if res_name:
field = self._columns.get('res_name',False)
if field and len(res_name) > field.size:
res_name = res_name[:30] + '...'
data[attachment.id] = res_name or False
else:
data[attachment.id] = False
return data
def _storage(self, cr, uid, context=None):
return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file')
@api.depends('res_model', 'res_id')
def _compute_res_name(self):
for attachment in self:
if attachment.res_model and attachment.res_id:
record = self.env[attachment.res_model].browse(attachment.res_id)
attachment.res_name = record.display_name
def _filestore(self, cr, uid, context=None):
return tools.config.filestore(cr.dbname)
@api.model
def _storage(self):
return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')
def force_storage(self, cr, uid, context=None):
@api.model
def _filestore(self):
return config.filestore(self._cr.dbname)
@api.model
def force_storage(self):
"""Force all attachments to be stored in the currently configured storage"""
if not self.pool['res.users']._is_admin(cr, uid, [uid]):
if not self.env.user._is_admin():
raise AccessError(_('Only administrators can execute this action.'))
location = self._storage(cr, uid, context)
# domain to retrieve the attachments to migrate
domain = {
'db': [('store_fname', '!=', False)],
'file': [('db_datas', '!=', False)],
}[location]
}[self._storage()]
ids = self.search(cr, uid, domain, context=context)
for attach in self.browse(cr, uid, ids, context=context):
for attach in self.search(domain):
attach.write({'datas': attach.datas})
return True
# 'data' field implementation
def _full_path(self, cr, uid, path):
# sanitize ath
@api.model
def _full_path(self, path):
# sanitize path
path = re.sub('[.]', '', path)
path = path.strip('/\\')
return os.path.join(self._filestore(cr, uid), path)
return os.path.join(self._filestore(), path)
def _get_path(self, cr, uid, bin_data, sha):
@api.model
def _get_path(self, bin_data, sha):
# retro compatibility
fname = sha[:3] + '/' + sha
full_path = self._full_path(cr, uid, fname)
full_path = self._full_path(fname)
if os.path.isfile(full_path):
return fname, full_path # keep existing path
# scatter files across 256 dirs
# we use '/' in the db (even on windows)
fname = sha[:2] + '/' + sha
full_path = self._full_path(cr, uid, fname)
full_path = self._full_path(fname)
dirname = os.path.dirname(full_path)
if not os.path.isdir(dirname):
os.makedirs(dirname)
return fname, full_path
def _file_read(self, cr, uid, fname, bin_size=False):
full_path = self._full_path(cr, uid, fname)
@api.model
def _file_read(self, fname, bin_size=False):
full_path = self._full_path(fname)
r = ''
try:
if bin_size:
r = os.path.getsize(full_path)
r = human_size(os.path.getsize(full_path))
else:
r = open(full_path,'rb').read().encode('base64')
except (IOError, OSError):
_logger.info("_read_file reading %s", full_path, exc_info=True)
return r
def _file_write(self, cr, uid, value, checksum):
@api.model
def _file_write(self, value, checksum):
bin_value = value.decode('base64')
fname, full_path = self._get_path(cr, uid, bin_value, checksum)
fname, full_path = self._get_path(bin_value, checksum)
if not os.path.exists(full_path):
try:
with open(full_path, 'wb') as fp:
@@ -118,11 +114,12 @@ class ir_attachment(osv.osv):
_logger.info("_file_write writing %s", full_path, exc_info=True)
return fname
def _file_delete(self, cr, uid, fname):
@api.model
def _file_delete(self, fname):
# using SQL to include files hidden through unlink or due to record rules
cr.execute("SELECT COUNT(*) FROM ir_attachment WHERE store_fname = %s", (fname,))
count = cr.fetchone()[0]
full_path = self._full_path(cr, uid, fname)
self._cr.execute("SELECT COUNT(*) FROM ir_attachment WHERE store_fname = %s", (fname,))
count = self._cr.fetchone()[0]
full_path = self._full_path(fname)
if not count and os.path.exists(full_path):
try:
os.unlink(full_path)
@@ -132,60 +129,39 @@ class ir_attachment(osv.osv):
# Harmless and needed for race conditions
_logger.info("_file_delete could not unlink %s", full_path, exc_info=True)
def _data_get(self, cr, uid, ids, name, arg, context=None):
if context is None:
context = {}
result = {}
bin_size = context.get('bin_size')
for attach in self.browse(cr, uid, ids, context=context):
@api.depends('store_fname', 'db_datas')
def _compute_datas(self):
bin_size = self._context.get('bin_size')
for attach in self:
if attach.store_fname:
result[attach.id] = self._file_read(cr, uid, attach.store_fname, bin_size)
attach.datas = self._file_read(attach.store_fname, bin_size)
else:
result[attach.id] = attach.db_datas
return result
attach.datas = attach.db_datas
def _data_set(self, cr, uid, id, name, value, arg, context=None):
# compute the field depending of datas, supporting the case of a empty/None datas
bin_data = value and value.decode('base64') or '' # empty string to compute its hash
checksum = self._compute_checksum(bin_data)
vals = {
'file_size': len(bin_data),
'checksum' : checksum,
}
# We dont handle setting data to null
# datas is false, but file_size and checksum are not (computed as datas is an empty string)
if not value:
# reset computed fields
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], vals, context=context)
return True
if context is None:
context = {}
# browse the attachment and get the file to delete
attach = self.browse(cr, uid, id, context=context)
fname_to_delete = attach.store_fname
location = self._storage(cr, uid, context)
# compute the index_content field
vals['index_content'] = self._index(cr, SUPERUSER_ID, bin_data, attach.datas_fname, attach.mimetype),
if location != 'db':
# create the file
fname = self._file_write(cr, uid, value, checksum)
vals.update({
'store_fname': fname,
'db_datas': False
})
else:
vals.update({
def _inverse_datas(self):
location = self._storage()
for attach in self:
# compute the fields that depend on datas
value = attach.datas
bin_data = value and value.decode('base64') or ''
vals = {
'file_size': len(bin_data),
'checksum': self._compute_checksum(bin_data),
'index_content': self._index(bin_data, attach.datas_fname, attach.mimetype),
'store_fname': False,
'db_datas': value
})
# SUPERUSER_ID as probably don't have write access, trigger during create
super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], vals, context=context)
'db_datas': value,
}
if value and location != 'db':
# save it to the filestore
vals['store_fname'] = self._file_write(value, vals['checksum'])
vals['db_datas'] = False
# After de-referencing the file in the database, check whether we need
# to garbage-collect it on the filesystem
if fname_to_delete:
self._file_delete(cr, uid, fname_to_delete)
return True
# take current location in filestore to possibly garbage-collect it
fname = attach.store_fname
# write as superuser, as user probably does not have write access
super(IrAttachment, attach.sudo()).write(vals)
if fname:
self._file_delete(fname)
def _compute_checksum(self, bin_data):
""" compute the checksum for the given datas
@@ -208,7 +184,8 @@ class ir_attachment(osv.osv):
mimetype = guess_mimetype(values['datas'].decode('base64'))
return mimetype
def _index(self, cr, uid, bin_data, datas_fname, file_type):
@api.model
def _index(self, bin_data, datas_fname, file_type):
""" compute the index content of the given filename, or binary data.
This is a python implementation of the unix command 'strings'.
:param bin_data : datas in binary form
@@ -222,104 +199,93 @@ class ir_attachment(osv.osv):
index_content = ustr("\n".join(words))
return index_content
name = fields.Char('Attachment Name', required=True)
datas_fname = fields.Char('File Name')
description = fields.Text('Description')
res_name = fields.Char('Resource Name', compute='_compute_res_name', store=True)
res_model = fields.Char('Resource Model', readonly=True, help="The database object this attachment will be attached to.")
res_field = fields.Char('Resource Field', readonly=True)
res_id = fields.Integer('Resource ID', readonly=True, help="The record id this is attached to.")
create_date = fields.Datetime('Date Created', readonly=True)
create_uid = fields.Many2one('res.users', string='Owner', readonly=True)
company_id = fields.Many2one('res.company', string='Company', change_default=True,
default=lambda self: self.env['res.company']._company_default_get('ir.attachment'))
type = fields.Selection([('url', 'URL'), ('binary', 'File')],
string='Type', required=True, default='binary', change_default=True,
help="You can either upload a file from your computer or copy/paste an internet link to your file.")
url = fields.Char('Url', size=1024)
public = fields.Boolean('Is public document')
_name = 'ir.attachment'
_columns = {
'name': fields.char('Attachment Name', required=True),
'datas_fname': fields.char('File Name'),
'description': fields.text('Description'),
'res_name': fields.function(_name_get_resname, type='char', string='Resource Name', store=True),
'res_model': fields.char('Resource Model', readonly=True, help="The database object this attachment will be attached to"),
'res_field': fields.char('Resource Field', readonly=True),
'res_id': fields.integer('Resource ID', readonly=True, help="The record id this is attached to"),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Owner', readonly=True),
'company_id': fields.many2one('res.company', 'Company', change_default=True),
'type': fields.selection( [ ('url','URL'), ('binary','File'), ],
'Type', help="You can either upload a file from your computer or copy/paste an internet link to your file", required=True, change_default=True),
'url': fields.char('Url', size=1024),
# al: We keep shitty field names for backward compatibility with document
'datas': fields.function(_data_get, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
'store_fname': fields.char('Stored Filename'),
'db_datas': fields.binary('Database Data'),
# computed fields depending on datas
'file_size': fields.integer('File Size', readonly=True),
'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
'mimetype': fields.char('Mime Type', readonly=True),
'index_content': fields.text('Indexed Content', readonly=True, _prefetch=False),
'public': fields.boolean('Is public document'),
}
# the field 'datas' is computed and may use the other fields below
datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas')
db_datas = fields.Binary('Database Data')
store_fname = fields.Char('Stored Filename')
file_size = fields.Integer('File Size', readonly=True)
checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
mimetype = fields.Char('Mime Type', readonly=True)
index_content = fields.Text('Indexed Content', readonly=True, _prefetch=False)
_defaults = {
'type': 'binary',
'file_size': 0,
'mimetype' : False,
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
}
@api.model_cr_context
def _auto_init(self):
super(IrAttachment, self)._auto_init()
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
self._cr.commit()
def _auto_init(self, cr, context=None):
super(ir_attachment, self)._auto_init(cr, context)
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
if not cr.fetchone():
cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
cr.commit()
def check(self, cr, uid, ids, mode, context=None, values=None):
@api.model
def check(self, mode, values=None):
"""Restricts the access to an ir.attachment, according to referred model
In the 'document' module, it is overriden to relax this hard rule, since
more complex ones apply there.
"""
res_ids = {}
# collect the records to check (by model)
model_ids = defaultdict(set) # {model_name: set(ids)}
require_employee = False
if ids:
if isinstance(ids, (int, long)):
ids = [ids]
cr.execute('SELECT res_model, res_id, create_uid, public FROM ir_attachment WHERE id = ANY (%s)', (ids,))
for rmod, rid, create_uid, public in cr.fetchall():
if self:
self._cr.execute('SELECT res_model, res_id, create_uid, public FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
for res_model, res_id, create_uid, public in self._cr.fetchall():
if public and mode == 'read':
continue
if not (rmod and rid):
if create_uid != uid:
if not (res_model and res_id):
if create_uid != self._uid:
require_employee = True
continue
res_ids.setdefault(rmod,set()).add(rid)
if values:
if values.get('res_model') and values.get('res_id'):
res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
model_ids[res_model].add(res_id)
if values and values.get('res_model') and values.get('res_id'):
model_ids[values['res_model']].add(values['res_id'])
ima = self.pool.get('ir.model.access')
for model, mids in res_ids.items():
# ignore attachments that are not attached to a resource anymore when checking access rights
# (resource was deleted but attachment was not)
if not self.pool.get(model):
# check access rights on the records
for res_model, res_ids in model_ids.iteritems():
# ignore attachments that are not attached to a resource anymore
# when checking access rights (resource was deleted but attachment
# was not)
if res_model not in self.env:
require_employee = True
continue
existing_ids = self.pool[model].exists(cr, uid, mids)
if len(existing_ids) != len(mids):
records = self.env[res_model].browse(res_ids).exists()
if len(records) < len(res_ids):
require_employee = True
# For related models, check if we can write to the model, as unlinking
# and creating attachments can be seen as an update to the model
if (mode in ['unlink','create']):
ima.check(cr, uid, model, 'write')
else:
ima.check(cr, uid, model, mode)
self.pool[model].check_access_rule(cr, uid, existing_ids, mode, context=context)
records.check_access_rights('write' if mode in ('create', 'unlink') else mode)
records.check_access_rule(mode)
if require_employee:
if not uid == SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_user'):
if not (self.env.user._is_admin() or self.env.user.has_group('base.group_user')):
raise AccessError(_("Sorry, you are not allowed to access this document."))
def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
# add res_field=False in domain if not present; the arg[0] trick below
# works for domain items and '&'/'|'/'!' operators too
if not any(arg[0] in ('id', 'res_field') for arg in args):
args.insert(0, ('res_field', '=', False))
ids = super(ir_attachment, self)._search(cr, uid, args, offset=offset,
limit=limit, order=order,
context=context, count=False,
access_rights_uid=access_rights_uid)
ids = super(IrAttachment, self)._search(args, offset=offset, limit=limit, order=order,
count=False, access_rights_uid=access_rights_uid)
if uid == SUPERUSER_ID:
if self._uid == SUPERUSER_ID:
# rules do not apply for the superuser
return len(ids) if count else ids
@@ -336,85 +302,82 @@ class ir_attachment(osv.osv):
# the linked document.
# Use pure SQL rather than read() as it is about 50% faster for large dbs (100k+ docs),
# and the permissions are checked in super() and below anyway.
cr.execute("""SELECT id, res_model, res_id, public FROM ir_attachment WHERE id = ANY(%s)""", (list(ids),))
targets = cr.dictfetchall()
model_attachments = {}
for target_dict in targets:
if not target_dict['res_model'] or target_dict['public']:
model_attachments = defaultdict(lambda: defaultdict(set)) # {res_model: {res_id: set(ids)}}
self._cr.execute("""SELECT id, res_model, res_id, public FROM ir_attachment WHERE id IN %s""", [tuple(ids)])
for row in self._cr.dictfetchall():
if not row['res_model'] or row['public']:
continue
# model_attachments = { 'model': { 'res_id': [id1,id2] } }
model_attachments.setdefault(target_dict['res_model'],{}).setdefault(target_dict['res_id'] or 0, set()).add(target_dict['id'])
# model_attachments = {res_model: {res_id: set(ids)}}
model_attachments[row['res_model']][row['res_id']].add(row['id'])
# To avoid multiple queries for each attachment found, checks are
# performed in batch as much as possible.
ima = self.pool.get('ir.model.access')
for model, targets in model_attachments.iteritems():
if model not in self.pool:
for res_model, targets in model_attachments.iteritems():
if res_model not in self.env:
continue
if not ima.check(cr, uid, model, 'read', False):
if not self.env[res_model].check_access_rights('read', False):
# remove all corresponding attachment ids
for attach_id in itertools.chain(*targets.values()):
ids.remove(attach_id)
continue # skip ir.rule processing, these ones are out already
ids.difference_update(itertools.chain(*targets.itervalues()))
continue
# filter ids according to what access rules permit
target_ids = targets.keys()
allowed_ids = [0] + self.pool[model].search(cr, uid, [('id', 'in', target_ids)], context=context)
disallowed_ids = set(target_ids).difference(allowed_ids)
for res_id in disallowed_ids:
for attach_id in targets[res_id]:
ids.remove(attach_id)
target_ids = list(targets)
allowed = self.env[res_model].search([('id', 'in', target_ids)])
for res_id in set(target_ids).difference(allowed.ids):
ids.difference_update(targets[res_id])
# sort result according to the original sort ordering
result = [id for id in orig_ids if id in ids]
return len(result) if count else list(result)
def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
if isinstance(ids, (int, long)):
ids = [ids]
self.check(cr, uid, ids, 'read', context=context)
return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context=context, load=load)
@api.v7
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
result = IrAttachment.read(self.browse(cr, uid, ids, context), fields, load=load)
return result if isinstance(ids, list) else (bool(result) and result[0])
def write(self, cr, uid, ids, vals, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
self.check(cr, uid, ids, 'write', context=context, values=vals)
@api.v8
def read(self, fields=None, load='_classic_read'):
self.check('read')
return super(IrAttachment, self).read(fields, load=load)
@api.multi
def write(self, vals):
self.check('write', values=vals)
# remove computed field depending of datas
for field in ['file_size', 'checksum']:
for field in ('file_size', 'checksum'):
vals.pop(field, False)
return super(ir_attachment, self).write(cr, uid, ids, vals, context)
return super(IrAttachment, self).write(vals)
def copy(self, cr, uid, id, default=None, context=None):
self.check(cr, uid, [id], 'write', context=context)
return super(ir_attachment, self).copy(cr, uid, id, default, context)
@api.multi
def copy(self, default=None):
self.check('write')
return super(IrAttachment, self).copy(default)
def unlink(self, cr, uid, ids, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
self.check(cr, uid, ids, 'unlink', context=context)
@api.multi
def unlink(self):
self.check('unlink')
# First delete in the database, *then* in the filesystem if the
# database allowed it. Helps avoid errors when concurrent transactions
# are deleting the same file, and some of the transactions are
# rolled back by PostgreSQL (due to concurrent updates detection).
to_delete = set([a.store_fname for a in self.browse(cr, uid, ids, context=context)
if a.store_fname])
res = super(ir_attachment, self).unlink(cr, uid, ids, context)
to_delete = set(attach.store_fname for attach in self if attach.store_fname)
res = super(IrAttachment, self).unlink()
for file_path in to_delete:
self._file_delete(cr, uid, file_path)
self._file_delete(file_path)
return res
def create(self, cr, uid, values, context=None):
@api.model
def create(self, values):
# remove computed field depending of datas
for field in ['file_size', 'checksum']:
for field in ('file_size', 'checksum'):
values.pop(field, False)
# if mimetype not given, compute it !
if 'mimetype' not in values:
values['mimetype'] = self._compute_mimetype(values)
self.check(cr, uid, [], mode='write', context=context, values=values)
return super(ir_attachment, self).create(cr, uid, values, context)
self.browse().check('write', values=values)
return super(IrAttachment, self).create(values)
def action_get(self, cr, uid, context=None):
return self.pool.get('ir.actions.act_window').for_xml_id(
cr, uid, 'base', 'action_attachment', context=context)
@api.model
def action_get(self):
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
+7 -5
View File
@@ -8,25 +8,27 @@ from odoo import api, models
_logger = logging.getLogger(__name__)
class AutoVacuum(models.TransientModel):
class AutoVacuum(models.AbstractModel):
""" Expose the vacuum method to the cron jobs mechanism. """
_name = 'ir.autovacuum'
@api.model
def _gc_transient_models(self):
for model in self.pool.itervalues():
for mname in self.env:
model = self.env[mname]
if model.is_transient():
model._transient_vacuum(self._cr, self._uid, force=True)
model._transient_vacuum(force=True)
@api.model
def _gc_user_logs(self):
self.env.cr.execute("""
self._cr.execute("""
DELETE FROM res_users_log log1 WHERE EXISTS (
SELECT 1 FROM res_users_log log2
WHERE log1.create_uid = log2.create_uid
AND log1.create_date < log2.create_date
)
""")
_logger.info("GC'd %d user log entries", self.env.cr.rowcount)
_logger.info("GC'd %d user log entries", self._cr.rowcount)
@api.model
def power_on(self):
+19 -22
View File
@@ -16,14 +16,13 @@ A dictionary holding some configuration parameters to be initialized when the da
_default_parameters = {
"database.secret": lambda: (str(uuid.uuid4()), ['base.group_erp_manager']),
"database.uuid": lambda: (str(uuid.uuid1()), []),
"database.create_date": lambda: (datetime.datetime.now().strftime(misc.DEFAULT_SERVER_DATETIME_FORMAT), ['base.group_user']),
"database.create_date": lambda: (fields.Datetime.now(), ['base.group_user']),
"web.base.url": lambda: ("http://localhost:%s" % config.get('xmlrpc_port'), []),
}
class IrConfigParameter(models.Model):
"""Per-database storage of configuration key-value pairs."""
_name = 'ir.config_parameter'
_rec_name = 'key'
@@ -35,6 +34,7 @@ class IrConfigParameter(models.Model):
('key_uniq', 'unique (key)', 'Key must be unique.')
]
@api.model_cr
def init(self, force=False):
"""
Initializes the parameters listed in _default_parameters.
@@ -56,17 +56,13 @@ class IrConfigParameter(models.Model):
:return: The value of the parameter, or ``default`` if it does not exist.
:rtype: string
"""
result = self._get_param(key)
if result:
return result
return default
return self._get_param(key) or default
@ormcache('uid', 'key')
def _get_param(self, cr, uid, key):
params = self.search_read(cr, uid, [('key', '=', key)], fields=['value'], limit=1)
if not params:
return None
return params[0]['value']
@api.model
@ormcache('self._uid', 'key')
def _get_param(self, key):
params = self.search_read([('key', '=', key)], fields=['value'], limit=1)
return params[0]['value'] if params else None
@api.model
def set_param(self, key, value, groups=()):
@@ -80,23 +76,24 @@ class IrConfigParameter(models.Model):
:rtype: string
"""
self._get_param.clear_cache(self)
self = self.search([('key', '=', key)])
param = self.search([('key', '=', key)])
gids = []
for group_xml in groups:
res_id = self.env['ir.model.data'].xmlid_to_res_id(group_xml)
if res_id:
gids.append((4, res_id))
group = self.env.ref(group_xml, raise_if_not_found=False)
if group:
gids.append((4, group.id))
vals = {'value': value}
if gids:
vals.update(group_ids=gids)
if self.ids:
if param:
old = param.value
if value is not False and value is not None:
self.write(vals)
param.write(vals)
else:
self.unlink()
return self.value
param.unlink()
return old
else:
vals.update(key=key)
if value is not False and value is not None:
@@ -105,10 +102,10 @@ class IrConfigParameter(models.Model):
@api.multi
def write(self, vals):
self._get_param.clear_cache(self)
self.clear_caches()
return super(IrConfigParameter, self).write(vals)
@api.multi
def unlink(self):
self._get_param.clear_cache(self)
self.clear_caches()
return super(IrConfigParameter, self).unlink()
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<record model="ir.ui.view" id="view_ir_config_search">
<field name="model">ir.config_parameter</field>
<field name="arch" type="xml">
@@ -35,4 +35,4 @@
</record>
<act_window name="System Parameters" res_model="ir.config_parameter" id="ir_config_list_action"/>
<menuitem id="ir_config_menu" name="System Parameters" parent="menu_ir_property" action="ir_config_list_action"/>
</openerp>
</odoo>
+42 -44
View File
@@ -10,8 +10,7 @@ from dateutil.relativedelta import relativedelta
import odoo
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
@@ -56,7 +55,7 @@ class ir_cron(models.Model):
('months', 'Months')], string='Interval Unit', default='months')
numbercall = fields.Integer(string='Number of Calls', default=1, help='How many times the method is called,\na negative number indicates no limit.')
doall = fields.Boolean(string='Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts.")
nextcall = fields.Datetime(string='Next Execution Date', required=True, default=lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), help="Next planned execution date for this job.")
nextcall = fields.Datetime(string='Next Execution Date', required=True, default=fields.Datetime.now, help="Next planned execution date for this job.")
model = fields.Char(string='Object', help="Model name on which the method to be called is located, e.g. 'res.partner'.")
function = fields.Char(string='Method', help="Name of the method to be called when this job is processed.")
args = fields.Text(string='Arguments', help="Arguments to be passed to the method, e.g. (uid,).")
@@ -68,18 +67,16 @@ class ir_cron(models.Model):
for this in self:
str2tuple(this.args)
except Exception:
raise Exception('Invalid arguments')
raise ValidationError(_('Invalid arguments'))
@api.multi
def method_direct_trigger(self):
for cron in self:
self._callback(cron.model, cron.function, cron.args, cron.id)
return True
def method_direct_trigger(self, cr, uid, ids, context=None):
if context is None:
context = {}
cron_obj = self.browse(cr, uid, ids, context=context)
for cron in cron_obj:
self._callback(cr, uid, cron_obj.model, cron_obj.function, cron_obj.args, cron_obj.id)
return True
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
@api.model
def _handle_callback_exception(self, model_name, method_name, args, job_id, job_exception):
""" Method called when an exception is raised by a job.
Simply logs the exception and rollback the transaction.
@@ -91,10 +88,12 @@ class ir_cron(models.Model):
:param job_exception: exception raised by the job.
"""
cr.rollback()
_logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
self._cr.rollback()
_logger.exception("Call of self.env[%r].%s(*%r) failed in Job %s",
model_name, method_name, args, job_id)
def _callback(self, cr, uid, model_name, method_name, args, job_id):
@api.model
def _callback(self, model_name, method_name, args, job_id):
""" Run the method associated to a given job
It takes care of logging and exception handling.
@@ -106,28 +105,25 @@ class ir_cron(models.Model):
"""
try:
args = str2tuple(args)
odoo.modules.registry.RegistryManager.check_registry_signaling(cr.dbname)
registry = odoo.registry(cr.dbname)
if model_name in registry:
model = registry[model_name]
odoo.modules.registry.RegistryManager.check_registry_signaling(self._cr.dbname)
if model_name in self.env:
model = self.env[model_name]
if hasattr(model, method_name):
log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1)
odoo.netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (cr.dbname, uid, '*', model_name, method_name)+tuple(args), depth=log_depth)
odoo.netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (self._cr.dbname, self._uid, '*', model_name, method_name)+tuple(args), depth=log_depth)
if _logger.isEnabledFor(logging.DEBUG):
start_time = time.time()
getattr(model, method_name)(cr, uid, *args)
getattr(model, method_name)(*args)
if _logger.isEnabledFor(logging.DEBUG):
end_time = time.time()
_logger.debug('%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
odoo.modules.registry.RegistryManager.signal_caches_change(cr.dbname)
_logger.debug('%.3fs (%s, %s)', end_time - start_time, model_name, method_name)
odoo.modules.registry.RegistryManager.signal_caches_change(self._cr.dbname)
else:
msg = "Method `%s.%s` does not exist." % (model_name, method_name)
_logger.warning(msg)
_logger.warning("Method '%s.%s' does not exist.", model_name, method_name)
else:
msg = "Model `%s` does not exist." % model_name
_logger.warning(msg)
_logger.warning("Model %r does not exist.", model_name)
except Exception, e:
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
self._handle_callback_exception(model_name, method_name, args, job_id, e)
def _process_job(self, job_cr, job, cron_cr):
""" Run a given job taking care of the repetition.
@@ -139,8 +135,13 @@ class ir_cron(models.Model):
"""
try:
with api.Environment.manage():
now = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.now())
nextcall = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
cron = api.Environment(job_cr, job['user_id'], {})[self._name]
# Use the user's timezone to compare and compute datetimes,
# otherwise unexpected results may appear. For instance, adding
# 1 month in UTC to July 1st at midnight in GMT+2 gives July 30
# instead of August 1st!
now = fields.Datetime.context_timestamp(cron, datetime.now())
nextcall = fields.Datetime.context_timestamp(cron, fields.Datetime.from_string(job['nextcall']))
numbercall = job['numbercall']
ok = False
@@ -148,7 +149,7 @@ class ir_cron(models.Model):
if numbercall > 0:
numbercall -= 1
if not ok or job['doall']:
self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
cron._callback(job['model'], job['function'], job['args'], job['id'])
if numbercall:
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
ok = True
@@ -156,8 +157,8 @@ class ir_cron(models.Model):
if not numbercall:
addsql = ', active=False'
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
(nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
self.invalidate_cache(job_cr, odoo.SUPERUSER_ID)
(fields.Datetime.to_string(nextcall.astimezone(pytz.UTC)), numbercall, job['id']))
cron.invalidate_cache()
finally:
job_cr.commit()
@@ -248,23 +249,20 @@ class ir_cron(models.Model):
if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect
del threading.current_thread().dbname
@api.multi
def _try_lock(self):
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
to make sure a following write() or unlink() will not block due
to a process currently executing those cron tasks"""
try:
self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(self.ids),), log_exceptions=False)
self._cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
[tuple(self.ids)], log_exceptions=False)
except psycopg2.OperationalError:
self.env.cr.rollback() # early rollback to allow translations to work for the user feedback
self._cr.rollback() # early rollback to allow translations to work for the user feedback
raise UserError(_("Record cannot be modified right now: "
"This cron task is currently being executed and may not be modified "
"Please try again in a few minutes"))
@api.model
def create(self, vals):
return super(ir_cron, self).create(vals)
@api.multi
def write(self, vals):
self._try_lock()
@@ -278,9 +276,9 @@ class ir_cron(models.Model):
@api.multi
def try_write(self, values):
try:
with self.env.cr.savepoint():
self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(self.ids),), log_exceptions=False)
with self._cr.savepoint():
self._cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
[tuple(self.ids)], log_exceptions=False)
except psycopg2.OperationalError:
pass
else:
+2 -2
View File
@@ -9,7 +9,7 @@ class IrExports(models.Model):
_order = 'name'
name = fields.Char(string='Export Name')
resource = fields.Char(select=True)
resource = fields.Char(index=True)
export_fields = fields.One2many('ir.exports.line', 'export_id', string='Export ID', copy=True)
@@ -18,4 +18,4 @@ class IrExportsLine(models.Model):
_order = 'id'
name = fields.Char(string='Field Name')
export_id = fields.Many2one('ir.exports', string='Export', select=True, ondelete='cascade')
export_id = fields.Many2one('ir.exports', string='Export', index=True, ondelete='cascade')
+8 -10
View File
@@ -3,21 +3,18 @@
import functools
import itertools
import time
import psycopg2
import pytz
from odoo import api, fields, models, _
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
from odoo.tools import ustr
REFERENCING_FIELDS = {None, 'id', '.id'}
def only_ref_fields(record):
return {k: v for k, v in record.iteritems()
if k in REFERENCING_FIELDS}
return {k: v for k, v in record.iteritems() if k in REFERENCING_FIELDS}
def exclude_ref_fields(record):
return {k: v for k, v in record.iteritems()
if k not in REFERENCING_FIELDS}
return {k: v for k, v in record.iteritems() if k not in REFERENCING_FIELDS}
CREATE = lambda values: (0, False, values)
UPDATE = lambda id, values: (1, id, values)
@@ -31,7 +28,8 @@ class ImportWarning(Warning):
""" Used to send warnings upwards the stack during the import process """
pass
class ConversionNotFound(ValueError): pass
class ConversionNotFound(ValueError):
pass
class IrFieldsConverter(models.AbstractModel):
@@ -201,8 +199,8 @@ class IrFieldsConverter(models.AbstractModel):
@api.model
def _str_to_date(self, model, field, value):
try:
time.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
return value, []
parsed_value = fields.Date.from_string(value)
return fields.Date.to_string(parsed_value), []
except ValueError:
raise self._format_import_error(
ValueError,
@@ -246,7 +244,7 @@ class IrFieldsConverter(models.AbstractModel):
input_tz = self._input_tz()# Apply input tz to the parsed naive datetime
dt = input_tz.localize(parsed_value, is_dst=False)
# And convert to UTC before reformatting for writing
return dt.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), []
return fields.Datetime.to_string(dt.astimezone(pytz.UTC)), []
@api.model
def _get_translations(self, types, src):
+37 -32
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import exceptions
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class IrFilters(models.Model):
@@ -10,10 +10,6 @@ class IrFilters(models.Model):
_description = 'Filters'
_order = 'model_id, name, id desc'
def _list_all_models(self):
self.env.cr.execute("SELECT model, name FROM ir_model ORDER BY name")
return self.env.cr.fetchall()
name = fields.Char(string='Filter Name', translate=True, required=True)
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', default=lambda self: self._uid,
help="The user this filter is private to. When left empty the filter is public "
@@ -29,12 +25,18 @@ class IrFilters(models.Model):
"for this model.")
active = fields.Boolean(default=True)
@api.model
def _list_all_models(self):
self._cr.execute("SELECT model, name FROM ir_model ORDER BY name")
return self._cr.fetchall()
@api.multi
def copy(self, default=None):
self.ensure_one()
default.update({'name': _('%s (copy)') % self.name})
default = dict(default or {}, name=_('%s (copy)') % self.name)
return super(IrFilters, self).copy(default)
@api.model
def _get_action_domain(self, action_id=None):
"""Return a domain component for matching filters that are visible in the
same context (menu/view) as the given action."""
@@ -59,12 +61,13 @@ class IrFilters(models.Model):
# available filters: private filters (user_id=uid) and public filters (uid=NULL),
# and filters for the action (action_id=action_id) or global (action_id=NULL)
action_domain = self._get_action_domain(action_id)
filter_ids = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])])
my_filters = filter_ids.with_context(self.env.user.context_get()).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort'])
return my_filters
filters = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])])
user_context = self.env.user.context_get()
return filters.with_context(user_context).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort'])
@api.model
def _check_global_default(self, vals, matching_filters):
""" _check_global_default(cursor, UID, dict, list(dict), dict) -> None
""" _check_global_default(dict, list(dict), dict) -> None
Checks if there is a global default for the model_id requested.
@@ -76,21 +79,22 @@ class IrFilters(models.Model):
This method should only be called if ``vals`` is trying to set
``is_default``
:raises openerp.exceptions.Warning: if there is an existing default and
:raises openerp.exceptions.UserError: if there is an existing default and
we're not updating it
"""
action_domain = self._get_action_domain(vals.get('action_id'))
existing_default = self.search(action_domain + [
domain = self._get_action_domain(vals.get('action_id'))
defaults = self.search(domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', False),
('is_default', '=', True)])
('is_default', '=', True),
])
if not existing_default:
if not defaults:
return
if matching_filters and (matching_filters[0]['id'] == existing_default.id):
if matching_filters and (matching_filters[0]['id'] == defaults.id):
return
raise exceptions.Warning(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')})
raise UserError(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')})
@api.model
def create_or_replace(self, vals):
@@ -107,14 +111,14 @@ class IrFilters(models.Model):
if vals.get('user_id'):
# Setting new default: any other default that belongs to the user
# should be turned off
action_domain = self._get_action_domain(action_id)
act_ids = self.search(action_domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', vals['user_id']),
('is_default', '=', True),
domain = self._get_action_domain(action_id)
defaults = self.search(domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', vals['user_id']),
('is_default', '=', True),
])
if act_ids:
act_ids.write({'is_default': False})
if defaults:
defaults.write({'is_default': False})
else:
self._check_global_default(vals, matching_filters)
@@ -127,18 +131,19 @@ class IrFilters(models.Model):
return self.create(vals)
_sql_constraints = [
# Partial constraint, complemented by unique index (see below)
# Still useful to keep because it provides a proper error message when a violation
# occurs, as it shares the same prefix as the unique index.
# Partial constraint, complemented by unique index (see below). Still
# useful to keep because it provides a proper error message when a
# violation occurs, as it shares the same prefix as the unique index.
('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'),
]
@api.model_cr_context
def _auto_init(self):
result = super(IrFilters, self)._auto_init()
# Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
self.env.cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
if not self.env.cr.fetchone():
self.env.cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
(lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""")
self._cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
if not self._cr.fetchone():
self._cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
(lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""")
return result
+22 -22
View File
@@ -19,8 +19,8 @@ import werkzeug.urls
import werkzeug.utils
import odoo
from odoo import models, tools
from odoo import http
from odoo import api, http, models, tools, SUPERUSER_ID
from odoo.exceptions import AccessDenied, AccessError
from odoo.http import request, STATIC_CACHE
from odoo.modules.module import get_resource_path, get_module_path
@@ -34,12 +34,11 @@ class ModelConverter(werkzeug.routing.BaseConverter):
def __init__(self, url_map, model=False):
super(ModelConverter, self).__init__(url_map)
self.model = model
self.regex = '([0-9]+)'
self.regex = r'([0-9]+)'
def to_python(self, value):
m = re.match(self.regex, value)
return request.registry[self.model].browse(
request.cr, UID_PLACEHOLDER, int(m.group(1)), context=request.context)
env = api.Environment(request.cr, UID_PLACEHOLDER, request.context)
return env[self.model].browse(int(value))
def to_url(self, value):
return value.id
@@ -51,13 +50,14 @@ class ModelsConverter(werkzeug.routing.BaseConverter):
super(ModelsConverter, self).__init__(url_map)
self.model = model
# TODO add support for slug in the form [A-Za-z0-9-] bla-bla-89 -> id 89
self.regex = '([0-9,]+)'
self.regex = r'([0-9,]+)'
def to_python(self, value):
return request.registry[self.model].browse(request.cr, UID_PLACEHOLDER, [int(i) for i in value.split(',')], context=request.context)
env = api.Environment(request.cr, UID_PLACEHOLDER, request.context)
return env[self.model].browse(map(int, value.split(',')))
def to_url(self, value):
return ",".join(i.id for i in value)
return ",".join(value.ids)
class SignedIntConverter(werkzeug.routing.NumberConverter):
@@ -85,7 +85,7 @@ class IrHttp(models.AbstractModel):
def _auth_method_public(self):
if not request.session.uid:
dummy, request.uid = self.env['ir.model.data'].sudo().get_object_reference(request.cr, 'base', 'public_user')
request.uid = request.env.ref('base.public_user').id
else:
request.uid = request.session.uid
@@ -97,22 +97,24 @@ class IrHttp(models.AbstractModel):
# what if error in security.check()
# -> res_users.check()
# -> res_users.check_credentials()
except (odoo.exceptions.AccessDenied, http.SessionExpiredException):
except (AccessDenied, http.SessionExpiredException):
# All other exceptions mean undetermined status (e.g. connection pool full),
# let them bubble up
request.session.logout(keep_db=True)
if request.uid is None:
getattr(self, "_auth_method_%s" % auth_method)()
except (odoo.exceptions.AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException):
except (AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException):
raise
except Exception:
_logger.info("Exception during request Authentication.", exc_info=True)
raise odoo.exceptions.AccessDenied()
raise AccessDenied()
return auth_method
def _serve_attachment(self):
env = api.Environment(request.cr, SUPERUSER_ID, request.context)
domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
attach = self.pool['ir.attachment'].search_read(request.cr, odoo.SUPERUSER_ID, domain, ['__last_update', 'datas', 'name', 'mimetype', 'checksum'], context=request.context)
fields = ['__last_update', 'datas', 'name', 'mimetype', 'checksum']
attach = env['ir.attachment'].search_read(domain, fields)
if attach:
wdate = attach[0]['__last_update']
datas = attach[0]['datas'] or ''
@@ -124,7 +126,7 @@ class IrHttp(models.AbstractModel):
return werkzeug.utils.redirect(name, 301)
response = werkzeug.wrappers.Response()
server_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
try:
response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
except ValueError:
@@ -156,7 +158,7 @@ class IrHttp(models.AbstractModel):
raise
try:
return request._handle_exception(exception)
except odoo.exceptions.AccessDenied:
except AccessDenied:
return werkzeug.exceptions.Forbidden()
def _dispatch(self):
@@ -191,11 +193,9 @@ class IrHttp(models.AbstractModel):
def _postprocess_args(self, arguments, rule):
""" post process arg to set uid on browse records """
for name, arg in arguments.items():
if isinstance(arg, odoo.osv.orm.browse_record) and arg._uid is UID_PLACEHOLDER:
if isinstance(arg, models.BaseModel) and arg._uid is UID_PLACEHOLDER:
arguments[name] = arg.sudo(request.uid)
try:
arg.exists()
except models.MissingError:
if not arg.exists():
return self._handle_exception(werkzeug.exceptions.NotFound())
def routing_map(self):
@@ -256,7 +256,7 @@ class IrHttp(models.AbstractModel):
# check read access
try:
last_update = obj['__last_update']
except odoo.exceptions.AccessError:
except AccessError:
return (403, [], None)
status, headers, content = None, [], None
@@ -341,5 +341,5 @@ def convert_exception_to(to_type, with_message=False):
message = str(with_message)
raise to_type, message, tb
except to_type, e:
except to_type as e:
return e
+3 -3
View File
@@ -9,9 +9,9 @@ class IrLogging(models.Model):
create_date = fields.Datetime(readonly=True)
create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal
name = fields.Char(required=True)
type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, select=True)
dbname = fields.Char(string='Database Name', select=True)
level = fields.Char(select=True)
type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, index=True)
dbname = fields.Char(string='Database Name', index=True)
level = fields.Char(index=True)
message = fields.Text(required=True)
path = fields.Char(required=True)
func = fields.Char(string='Function', required=True)
+41 -49
View File
@@ -14,13 +14,8 @@ import smtplib
import threading
from odoo import api, fields, models, tools, _
from odoo.exceptions import except_orm
from odoo.exceptions import UserError
from odoo.tools import html2text
# ustr was originally from tools.misc.
# it is moved to loglevels until we refactor tools.
from odoo.loglevels import ustr
from odoo.exceptions import except_orm, UserError
from odoo.tools import html2text, ustr
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('openerp.tests')
@@ -75,14 +70,13 @@ def encode_header(header_text):
return ""
# convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are
# encoded as ASCII in utf-8
header_text_utf8 = tools.ustr(header_text).encode('utf-8')
header_text_utf8 = ustr(header_text).encode('utf-8')
header_text_ascii = try_coerce_ascii(header_text_utf8)
# if this header contains non-ASCII characters,
# we'll need to wrap it up in a message.header.Header
# that will take care of RFC2047-encoding it as
# 7-bit string.
return header_text_ascii if header_text_ascii\
else Header(header_text_utf8, 'utf-8')
return header_text_ascii or Header(header_text_utf8, 'utf-8')
def encode_header_param(param_text):
@@ -101,16 +95,13 @@ def encode_header_param(param_text):
# For details see the encode_header() method that uses the same logic
if not param_text:
return ""
param_text_utf8 = tools.ustr(param_text).encode('utf-8')
param_text_utf8 = ustr(param_text).encode('utf-8')
param_text_ascii = try_coerce_ascii(param_text_utf8)
return param_text_ascii if param_text_ascii\
else Charset('utf8').header_encode(param_text_utf8)
return param_text_ascii or Charset('utf8').header_encode(param_text_utf8)
# TODO master, remove me, no longer used internaly
name_with_email_pattern = re.compile(r'("[^<@>]+")\s*<([^ ,<@]+@[^> ,]+)>')
address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
def extract_rfc2822_addresses(text):
"""Returns a list of valid RFC2822 addresses
that can be found in ``source``, ignoring
@@ -118,7 +109,7 @@ def extract_rfc2822_addresses(text):
"""
if not text:
return []
candidates = address_pattern.findall(tools.ustr(text).encode('utf-8'))
candidates = address_pattern.findall(ustr(text).encode('utf-8'))
return filter(try_coerce_ascii, candidates)
@@ -135,7 +126,7 @@ def encode_rfc2822_address_header(header_text):
name = str(Header(name, 'utf-8'))
return formataddr((name, email))
addresses = getaddresses([tools.ustr(header_text).encode('utf-8')])
addresses = getaddresses([ustr(header_text).encode('utf-8')])
return COMMASPACE.join(map(encode_addr, addresses))
@@ -176,17 +167,18 @@ class IrMailServer(models.Model):
@api.multi
def name_get(self):
return [(a.id, "(%s)" % (a.name)) for a in self]
return [(server.id, "(%s)" % server.name) for server in self]
@api.multi
def test_smtp_connection(self):
for smtp_server in self:
for server in self:
smtp = False
try:
smtp = self.connect(smtp_server.smtp_host, smtp_server.smtp_port, user=smtp_server.smtp_user,
password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
smtp_debug=smtp_server.smtp_debug)
except Exception, e:
raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % tools.ustr(e))
smtp = self.connect(server.smtp_host, server.smtp_port, user=server.smtp_user,
password=server.smtp_pass, encryption=server.smtp_encryption,
smtp_debug=server.smtp_debug)
except Exception as e:
raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % ustr(e))
finally:
try:
if smtp:
@@ -231,8 +223,8 @@ class IrMailServer(models.Model):
# The user/password must be converted to bytestrings in order to be usable for
# certain hashing schemes, like HMAC.
# See also bug #597143 and python issue #5285
user = tools.ustr(user).encode('utf-8')
password = tools.ustr(password).encode('utf-8')
user = ustr(user).encode('utf-8')
password = ustr(password).encode('utf-8')
connection.login(user, password)
return connection
@@ -275,11 +267,10 @@ class IrMailServer(models.Model):
# Note: we must force all strings to to 8-bit utf-8 when crafting message,
# or use encode_header() for headers, which does it automatically.
headers = headers or {} # need valid dict later
if not email_cc: email_cc = []
if not email_bcc: email_bcc = []
if not body: body = u''
headers = headers or {} # need valid dict later
email_cc = email_cc or []
email_bcc = email_bcc or []
body = body or u''
email_body_utf8 = ustr(body).encode('utf-8')
email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8')
@@ -343,6 +334,7 @@ class IrMailServer(models.Model):
msg.attach(part)
return msg
@api.model
def _get_default_bounce_address(self):
'''Compute the default bounce address.
@@ -362,6 +354,7 @@ class IrMailServer(models.Model):
if postmaster and domain:
return '%s@%s' % (postmaster, domain)
@api.model
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False):
"""Sends an email directly (no queuing).
@@ -392,11 +385,7 @@ class IrMailServer(models.Model):
# Use the default bounce address **only if** no Return-Path was
# provided by caller. Caller may be using Variable Envelope Return
# Path (VERP) to detect no-longer valid email addresses.
smtp_from = message['Return-Path']
if not smtp_from:
smtp_from = self._get_default_bounce_address()
if not smtp_from:
smtp_from = message['From']
smtp_from = message['Return-Path'] or self._get_default_bounce_address() or message['From']
assert smtp_from, "The Return-Path or From header is required for any outbound email"
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
@@ -416,7 +405,7 @@ class IrMailServer(models.Model):
if x_forge_to:
# `To:` header forged, e.g. for posting on mail.channels, to avoid confusion
del message['X-Forge-To']
del message['To'] # avoid multiple To: headers!
del message['To'] # avoid multiple To: headers!
message['To'] = x_forge_to
# Do not actually send emails in testing mode!
@@ -445,7 +434,7 @@ class IrMailServer(models.Model):
smtp_user = smtp_user or tools.config.get('smtp_user')
smtp_password = smtp_password or tools.config.get('smtp_password')
if smtp_encryption is None and tools.config.get('smtp_ssl'):
smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
if not smtp_server:
raise UserError(_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, or provide the SMTP parameters explicitly."))
@@ -468,20 +457,23 @@ class IrMailServer(models.Model):
finally:
if smtp is not None:
smtp.quit()
except Exception, e:
msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server),
e.__class__.__name__,
tools.ustr(e))
except Exception as e:
params = (ustr(smtp_server), e.__class__.__name__, ustr(e))
msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % params
_logger.info(msg)
raise MailDeliveryException(_("Mail Delivery Failed"), msg)
return message_id
def on_change_encryption(self, cr, uid, ids, smtp_encryption):
if smtp_encryption == 'ssl':
result = {'value': {'smtp_port': 465}}
@api.onchange('smtp_encryption')
def _onchange_encryption(self):
result = {}
if self.smtp_encryption == 'ssl':
self.smtp_port = 465
if not 'SMTP_SSL' in smtplib.__all__:
result['warning'] = {'title': _('Warning'),
'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead')}
result['warning'] = {
'title': _('Warning'),
'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead'),
}
else:
result = {'value': {'smtp_port': 25}}
self.smtp_port = 25
return result
@@ -16,7 +16,7 @@
<field name="smtp_debug" groups="base.group_no_one"/>
</group>
<group string="Security and Authentication" colspan="4">
<field name="smtp_encryption" on_change="on_change_encryption(smtp_encryption)"/>
<field name="smtp_encryption"/>
<field name="smtp_user"/>
<field name="smtp_pass" password="True"/>
<button name="test_smtp_connection" type="object" string="Test Connection" icon="gtk-network"/>
File diff suppressed because it is too large Load Diff
-1
View File
@@ -20,7 +20,6 @@ class IrNeedactionMixin(models.AbstractModel):
This class also offers several global services:
- ``_needaction_count``: returns the number of actions uid has to perform
"""
_name = 'ir.needaction_mixin'
_needaction = True
+172 -220
View File
@@ -1,8 +1,4 @@
# -*- coding: utf-8 -*-
import collections
import copy
import cStringIO
import datetime
import hashlib
import itertools
import json
@@ -13,6 +9,10 @@ import re
import sys
import textwrap
import uuid
from collections import defaultdict, Mapping, Sized
from copy import deepcopy
from cStringIO import StringIO
from datetime import datetime
from subprocess import Popen, PIPE
from urlparse import urlparse
@@ -23,16 +23,11 @@ import werkzeug
from lxml import etree, html
from PIL import Image
import odoo.http
import odoo.tools
import odoo.tools.lru
from odoo import api, models, _
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import QWebException
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools import html_escape as escape
from odoo.tools.func import lazy_property
from odoo.tools.misc import find_in_path
from odoo.tools import html_escape as escape, find_in_path, lazy_property, posix_to_ldml
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
@@ -54,17 +49,17 @@ def raise_qweb_exception(etype=None, **kw):
try:
raise etype, original, tb
except etype, e:
for k, v in kw.items():
e.qweb[k] = v
# Will use `raise foo from bar` in python 3 and rename cause to __cause__
e.qweb['cause'] = original
e.qweb.update(kw, cause=original)
raise
def _build_attribute(name, value):
value = escape(value)
if isinstance(name, unicode): name = name.encode('utf-8')
if isinstance(value, unicode): value = value.encode('utf-8')
if isinstance(name, unicode):
name = name.encode('utf-8')
if isinstance(value, unicode):
value = value.encode('utf-8')
return ' %s="%s"' % (name, value)
@@ -84,35 +79,35 @@ class FileSystemLoader(object):
for node in self.doc:
if node.get('t-name') == name:
root = etree.Element('templates')
root.append(copy.deepcopy(node))
root.append(deepcopy(node))
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
class QWebContext(dict):
def __init__(self, cr, uid, data, loader=None, context=None):
self.cr = cr
self.uid = uid
def __init__(self, env, data, loader=None):
self.env = env
self.loader = loader
self.context = context
dic = dict(data)
super(QWebContext, self).__init__(dic)
super(QWebContext, self).__init__(data)
self['defined'] = lambda key: key in self
# deprecated, use 'env' instead
cr = property(lambda self: self.env.cr)
uid = property(lambda self: self.env.uid)
context = property(lambda self: self.env.context)
def safe_eval(self, expr):
locals_dict = collections.defaultdict(lambda: None)
locals_dict = defaultdict(lambda: None)
locals_dict.update(self)
locals_dict.pop('cr', None)
locals_dict.pop('loader', None)
return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
def copy(self):
""" Clones the current context, conserving all data and metadata
""" Clone the current context, conserving all data and metadata
(loader, template cache, ...)
"""
return QWebContext(self.cr, self.uid, dict.copy(self),
loader=self.loader,
context=self.context)
return QWebContext(self.env, dict(self), loader=self.loader)
def __copy__(self):
return self.copy()
@@ -137,20 +132,13 @@ class QWeb(models.AbstractModel):
u'area', u'base', u'br', u'col', u'embed', u'hr', u'img', u'input',
u'keygen', u'link', u'menuitem', u'meta', u'param', u'source',
u'track', u'wbr'])
_format_regex = re.compile(
'(?:'
# ruby-style pattern
'#\{(.+?)\}'
')|(?:'
# jinja-style pattern
'\{\{(.+?)\}\}'
')')
# regex for ruby-style pattern or jinja-style pattern
_format_regex = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
def __init__(self, pool, cr):
super(QWeb, self).__init__(pool, cr)
self._render_tag = self.prefixed_methods('render_tag_')
self._render_att = self.prefixed_methods('render_att_')
type(self)._render_tag = self.prefixed_methods('render_tag_')
type(self)._render_att = self.prefixed_methods('render_att_')
def prefixed_methods(self, prefix):
""" Extracts all methods prefixed by ``prefix``, and returns a mapping
@@ -229,9 +217,9 @@ class QWeb(models.AbstractModel):
def eval_bool(self, expr, qwebcontext):
return int(bool(self.eval(expr, qwebcontext)))
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
""" render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
@api.model
def render(self, template, qwebcontext=None, loader=None):
""" render(template, qwebcontext=None, loader=None)
Renders the template specified by the provided template name
@@ -244,17 +232,16 @@ class QWeb(models.AbstractModel):
qwebcontext = {}
if not isinstance(qwebcontext, QWebContext):
qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
qwebcontext = QWebContext(self.env, qwebcontext, loader=loader)
qwebcontext['__template__'] = id_or_xml_id
stack = qwebcontext.get('__stack__', [])
qwebcontext['__template__'] = template
stack = qwebcontext.setdefault('__stack__', [])
if stack:
qwebcontext['__caller__'] = stack[-1]
stack.append(id_or_xml_id)
qwebcontext['__stack__'] = stack
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
stack.append(template)
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
element = self.get_template(id_or_xml_id, qwebcontext)
element = self.get_template(template, qwebcontext)
element.attrib.pop("name", False)
return self.render_node(element, qwebcontext, generated_attributes=qwebcontext.pop('generated_attributes', ''))
@@ -264,7 +251,7 @@ class QWeb(models.AbstractModel):
debugger = element.get('t-debug')
if debugger is not None:
if odoo.tools.config['dev_mode']:
if tools.config['dev_mode']:
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
else:
_logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
@@ -272,9 +259,8 @@ class QWeb(models.AbstractModel):
for (attribute_name, attribute_value) in element.attrib.iteritems():
attribute_name = unicode(attribute_name)
if attribute_name == "groups":
cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
can_see = (qwebcontext.cr and qwebcontext.uid and
qwebcontext.env['ir.qweb'].user_has_groups(attribute_value))
if not can_see:
return element.tail and self.render_tail(element.tail, element, qwebcontext) or ''
@@ -286,7 +272,8 @@ class QWeb(models.AbstractModel):
attrs = self._render_att[attribute](
self, element, attribute_name, attribute_value, qwebcontext)
for att, val in attrs:
if not val: continue
if not val:
continue
generated_attributes += self.render_attribute(element, att, val, qwebcontext)
break
else:
@@ -322,7 +309,7 @@ class QWeb(models.AbstractModel):
for current_node in element.iterchildren(tag=etree.Element):
try:
g_inner.append(self.render_node(current_node, qwebcontext,
generated_attributes= name == "t" and generated_attributes or ''))
generated_attributes=(name == "t" and generated_attributes or '')))
except QWebException:
raise
except Exception:
@@ -366,7 +353,7 @@ class QWeb(models.AbstractModel):
return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
result = self.eval_object(attribute_value, qwebcontext)
if isinstance(result, collections.Mapping):
if isinstance(result, Mapping):
return result.iteritems()
# assume tuple
return [result]
@@ -383,7 +370,7 @@ class QWeb(models.AbstractModel):
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def _iterate(self, iterable):
if isinstance(iterable, collections.Mapping):
if isinstance(iterable, Mapping):
return iterable.iteritems()
return itertools.izip(*itertools.tee(iterable))
@@ -401,7 +388,7 @@ class QWeb(models.AbstractModel):
copy_qwebcontext = qwebcontext.copy()
size = None
if isinstance(enum, collections.Sized):
if isinstance(enum, Sized):
size = len(enum)
copy_qwebcontext["%s_size" % varname] = size
@@ -444,15 +431,13 @@ class QWeb(models.AbstractModel):
d = qwebcontext.copy()
if 'lang' in template_attributes:
init_lang = d.context.get('lang', 'en_US')
# add 'lang' in the context of d
lang = template_attributes['lang']
d.context['lang'] = self.eval(lang, d) or lang
if not self.pool['res.lang'].search(d.cr, d.uid, [('code', '=', lang)], count=True, context=d.context):
d.env = d.env(context=dict(d.context, lang=self.eval(lang, d) or lang))
if not d.env['res.lang'].search_count([('code', '=', lang)]):
_logger.info("'%s' is not a valid language code, is an empty field or is not installed, falling back to en_US", lang)
d[0] = self.render_element(element, template_attributes, generated_attributes, d)
cr = d.get('request') and d['request'].cr or None
uid = d.get('request') and d['request'].uid or None
template = self.eval_format(template_attributes["call"], d)
try:
@@ -461,11 +446,7 @@ class QWeb(models.AbstractModel):
pass
d['generated_attributes'] = generated_attributes
res = self.render(cr, uid, template, d)
# we need to reset the lang after the rendering
if 'lang' in template_attributes:
d.context['lang'] = init_lang
res = d.env['ir.qweb'].render(template, d)
return res
@@ -478,8 +459,7 @@ class QWeb(models.AbstractModel):
template = qwebcontext.get('__template__')
raise QWebException("t-call-assets cannot contain children nodes", template=template)
xmlid = template_attributes['call-assets']
cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
bundle = AssetsBundle(xmlid, env=qwebcontext.env)
css = self.get_attr_bool(template_attributes.get('css'), default=True)
js = self.get_attr_bool(template_attributes.get('js'), default=True)
async = self.get_attr_bool(template_attributes.get('async'), default=False)
@@ -505,36 +485,41 @@ class QWeb(models.AbstractModel):
record, field_name = template_attributes["field"].rsplit('.', 1)
record = self.eval_object(record, qwebcontext)
field = record._fields[field_name]
foptions = self.eval_format(template_attributes.get('field-options') or '{}', qwebcontext)
options = json.loads(foptions)
field_type = get_field_type(field, options)
field_type = get_field_type(record._fields[field_name], options)
converter = self.get_converter_for(field_type)
return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
return converter.to_html(field_name, record, options, element,
template_attributes, generated_attributes, qwebcontext)
def get_converter_for(self, field_type):
""" returns a :class:`~openerp.models.Model` used to render a
``t-field``.
""" Return a model instance to render a ``t-field``. The result is of
the same kind (old/new api) as ``self``
By default, tries to get the model named
:samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
:param str field_type: type or widget of field to render
"""
return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
registry = getattr(self, 'env', self.pool)
try:
return registry['ir.qweb.field.' + field_type]
except KeyError:
return registry['ir.qweb.field']
def get_widget_for(self, widget):
""" returns a :class:`~openerp.models.Model` used to render a
``t-esc``
""" Return a model instance to render a ``t-esc``. The result is of the
same kind (old/new api) as ``self``
:param str widget: name of the widget to use, or ``None``
"""
widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
registry = getattr(self, 'env', self.pool)
try:
return registry['ir.qweb.widget.' + (widget or '')]
except KeyError:
return registry['ir.qweb.widget']
def get_attr_bool(self, attr, default=False):
if attr:
@@ -563,9 +548,8 @@ class FieldConverter(models.AbstractModel):
_name = 'ir.qweb.field'
@api.model
def attributes(self, field_name, record, options,
source_element, g_att, t_att, qweb_context):
""" attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
def attributes(self, field_name, record, options, source_element, g_att, t_att, qweb_context):
""" attributes(field_name, record, options, source_element, g_att, t_att, qweb_context)
Generates the metadata attributes (prefixed by ``data-oe-`` for the
root node of the field conversion. Attribute values are escaped by the
@@ -600,16 +584,15 @@ class FieldConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
""" value_to_html(cr, uid, value, field, options=None, context=None)
""" value_to_html(value, field, options=None)
Converts a single value to its HTML version/output
"""
if not value: return ''
return value
return value or ''
@api.model
def record_to_html(self, field_name, record, options=None):
""" record_to_html(cr, uid, field_name, record, options=None, context=None)
""" record_to_html(field_name, record, options=None)
Converts the specified field of the browse_record ``record`` to HTML
"""
@@ -617,9 +600,8 @@ class FieldConverter(models.AbstractModel):
return self.value_to_html(record[field_name], field, options=options)
@api.model
def to_html(self, field_name, record, options,
source_element, t_att, g_att, qweb_context):
""" to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
""" to_html(field_name, record, options, source_element, t_att, g_att, qweb_context)
Converts a ``t-field`` to its HTML output. A ``t-field`` may be
extended by a ``t-field-options``, which is a JSON-serialized mapping
@@ -639,10 +621,10 @@ class FieldConverter(models.AbstractModel):
field_name, record._name, exc_info=True)
content = None
inherit_branding = self.env.context and self.env.context.get('inherit_branding')
if not inherit_branding and self.env.context and self.env.context.get('inherit_branding_auto'):
inherit_branding = self.env['ir.model.access'].check(record._name, 'write', False)
translate = self.env.context and self.env.context.get('edit_translations') and self.env.context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
inherit_branding = self._context.get('inherit_branding')
if not inherit_branding and self._context.get('inherit_branding_auto'):
inherit_branding = record.check_access_rights('write', False)
translate = self._context.get('edit_translations') and self._context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
if inherit_branding or translate:
# add branding attributes
@@ -653,36 +635,29 @@ class FieldConverter(models.AbstractModel):
source_element, g_att, t_att, qweb_context)
)
return self.render_element(source_element, t_att, g_att,
qweb_context, content)
return self.render_element(source_element, t_att, g_att, qweb_context, content)
def qweb_object(self):
return self.pool['ir.qweb']
def render_element(self, cr, uid, source_element, t_att, g_att,
qweb_context, content):
""" render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
@api.model
def render_element(self, source_element, t_att, g_att, qweb_context, content):
""" render_element(source_element, t_att, g_att, qweb_context, content)
Final rendering hook, by default just calls ir.qweb's ``render_element``
"""
return self.qweb_object().render_element(
return self.env['ir.qweb'].render_element(
source_element, t_att, g_att, qweb_context, content or '')
@api.model
def user_lang(self):
""" user_lang(cr, uid, context)
""" user_lang()
Fetches the res.lang object corresponding to the language code stored
Fetches the res.lang record corresponding to the language code stored
in the user's context. Fallbacks to en_US if no lang is present in the
context *or the language code is not valid*.
:returns: res.lang browse_record
"""
lang_code = self.env.context.get('lang') or 'en_US'
Lang = self.env['res.lang']
return Lang.browse(Lang._lang_get(lang_code))
lang_code = self._context.get('lang') or 'en_US'
return self.env['res.lang']._lang_get(lang_code)
class IntegerConverter(models.AbstractModel):
@@ -691,9 +666,7 @@ class IntegerConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
lang_code = self.env.context.get('lang') or 'en_US'
return self.pool['res.lang'].format(self.env.cr, self.env.uid, [lang_code], '%d', value, grouping=True)
return self.user_lang().format('%d', value, grouping=True)
class FloatConverter(models.AbstractModel):
@@ -709,10 +682,8 @@ class FloatConverter(models.AbstractModel):
def value_to_html(self, value, field, options=None):
precision = self.precision(field, options=options)
fmt = '%f' if precision is None else '%.{precision}f'
lang_code = self.env.context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted = lang.format(self.env.cr, self.env.uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
lang = self.user_lang()
formatted = lang.format(fmt.format(precision=precision), value, grouping=True)
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
@@ -729,23 +700,21 @@ class DateConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
if not value or len(value) < 10: return ''
if not value or len(value) < 10:
return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value[:10], odoo.tools.DEFAULT_SERVER_DATE_FORMAT)
value = fields.Datetime.from_string(value[:10])
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = lang.date_format
pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
pattern = posix_to_ldml(strftime_pattern, locale=locale)
return babel.dates.format_date(
value, format=pattern,
locale=locale)
return babel.dates.format_date(value, format=pattern, locale=locale)
class DateTimeConverter(models.AbstractModel):
@@ -754,21 +723,21 @@ class DateTimeConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
if not value: return ''
if not value:
return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value, odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT)
value = fields.Datetime.from_string(value)
value = odoo.osv.fields.datetime.context_timestamp(self.env.cr, self.env.uid, timestamp=value, context=self.env.context)
value = fields.Datetime.context_timestamp(self, value)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
pattern = posix_to_ldml(strftime_pattern, locale=locale)
if options and options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
@@ -785,9 +754,7 @@ class TextConverter(models.AbstractModel):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
if not value: return ''
return nl2br(value, options=options)
return nl2br(value, options=options) if value else ''
class SelectionConverter(models.AbstractModel):
@@ -797,7 +764,8 @@ class SelectionConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options=None):
value = record[field_name]
if not value: return ''
if not value:
return ''
field = record._fields[field_name]
selection = dict(field.get_description(record.env)['selection'])
return self.value_to_html(selection[value], field, options=options)
@@ -809,10 +777,8 @@ class ManyToOneConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options=None):
[read] = record.read([field_name])
if not read[field_name]: return ''
_, value = read[field_name]
return nl2br(value, options=options)
value = record[field_name].sudo().display_name
return nl2br(value, options=options) if value else ''
class HTMLConverter(models.AbstractModel):
@@ -839,11 +805,11 @@ class ImageConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
try:
image = Image.open(cStringIO.StringIO(value.decode('base64')))
image = Image.open(StringIO(value.decode('base64')))
image.verify()
except IOError:
raise ValueError("Non-image binary fields can not be converted to HTML")
except: # image.verify() throws "suitable exceptions", I have no idea what they are
except: # image.verify() throws "suitable exceptions", I have no idea what they are
raise ValueError("Invalid image content")
return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
@@ -867,8 +833,7 @@ class MonetaryConverter(models.AbstractModel):
_inherit = 'ir.qweb.field'
@api.model
def to_html(self, field_name, record, options,
source_element, t_att, g_att, qweb_context):
def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
options['_qweb_context'] = qweb_context
return super(MonetaryConverter, self).to_html(
field_name, record, options,
@@ -876,7 +841,6 @@ class MonetaryConverter(models.AbstractModel):
@api.model
def record_to_html(self, field_name, record, options):
Currency = self.pool['res.currency']
cur_field = record._fields[field_name]
display_currency = False
#currency should be specified by monetary field
@@ -897,12 +861,11 @@ class MonetaryConverter(models.AbstractModel):
if options.get('from_currency'):
from_currency = self.display_currency(options['from_currency'], options)
from_amount = Currency.compute(self.env.cr, self.env.uid, from_currency.id, display_currency.id, from_amount)
from_amount = from_currency.compute(from_amount, display_currency)
lang_code = self.env.context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted_amount = lang.format(self.env.cr, self.env.uid, [lang_code],
fmt, Currency.round(self.env.cr, self.env.uid, display_currency, from_amount), grouping=True, monetary=True)
lang = self.user_lang()
formatted_amount = lang.format(fmt, display_currency.round(from_amount),
grouping=True, monetary=True)
pre = post = u''
if display_currency.position == 'before':
@@ -911,14 +874,15 @@ class MonetaryConverter(models.AbstractModel):
post = u'\N{NO-BREAK SPACE}{symbol}'
return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
formatted_amount,
pre=pre, post=post,
formatted_amount, pre=pre, post=post,
).format(
symbol=display_currency.symbol,
))
@api.model
def display_currency(self, currency, options):
return self.qweb_object().eval_object(currency, options['_qweb_context'])
return self.env['ir.qweb'].eval_object(currency, options['_qweb_context'])
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
@@ -961,7 +925,8 @@ class DurationConverter(models.AbstractModel):
r = value * factor
for unit, secs_per_unit in TIMEDELTA_UNITS:
v, r = divmod(r, secs_per_unit)
if not v: continue
if not v:
continue
section = babel.dates.format_timedelta(
v*secs_per_unit, threshold=1, locale=locale)
if section:
@@ -975,14 +940,13 @@ class RelativeDatetimeConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, field, options=None):
parse_format = odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(self.user_lang().code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(value, parse_format)
value = fields.Datetime.from_string(value)
# value should be a naive datetime in UTC. So is fields.Datetime.now()
reference = datetime.datetime.strptime(field.now(), parse_format)
reference = fields.Datetime.from_string(field.now())
return babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)
@@ -1018,7 +982,8 @@ class Contact(models.AbstractModel):
'options': options
}
html = self.env['ir.ui.view']._model.render(self.env.cr, self.env.uid, "base.contact", val, engine='ir.qweb', context=self.env.context).decode('utf8')
view = self.env.ref('base.contact')
html = view.render(val, engine='ir.qweb').decode('utf8')
return HTMLSafe(html)
@@ -1034,13 +999,12 @@ class QwebView(models.AbstractModel):
view = getattr(record, field_name)
if view._model._name != "ir.ui.view":
if view._name != "ir.ui.view":
_logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
return None
ctx = (self.env.context or {}).copy()
ctx['object'] = record
html = view._model.render(self.env.cr, self.env.uid, ctx, engine='ir.qweb', context=self.env.ctx).decode('utf8')
view = view.with_context(object=record)
html = view.render(view._context, engine='ir.qweb').decode('utf8')
return HTMLSafe(html)
@@ -1060,14 +1024,13 @@ class QwebWidgetMonetary(models.AbstractModel):
_inherit = 'ir.qweb.widget'
def _format(self, inner, options, qwebcontext):
inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
inner = qwebcontext.env['ir.qweb'].eval(inner, qwebcontext)
display = qwebcontext.env['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
precision = int(round(math.log10(display.rounding)))
fmt = "%.{0}f".format(-precision if precision < 0 else 0)
lang_code = qwebcontext.context.get('lang') or 'en_US'
formatted_amount = self.pool['res.lang'].format(
qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
)
lang = qwebcontext.env['res.lang']._lang_get(lang_code)
formatted_amount = lang.format(fmt, inner, grouping=True, monetary=True)
pre = post = u''
if display.position == 'before':
pre = u'{symbol}\N{NO-BREAK SPACE}'
@@ -1116,7 +1079,8 @@ def nl2br(string, options=None):
:param dict options:
:rtype: HTMLSafe
"""
if options is None: options = {}
if options is None:
options = {}
if options.get('html-escape', True):
string = escape(string)
@@ -1141,12 +1105,9 @@ class AssetsBundle(object):
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
def __init__(self, xmlid, cr=None, uid=None, context=None, registry=None, max_css_rules=MAX_CSS_RULES):
def __init__(self, xmlid, env=None, max_css_rules=MAX_CSS_RULES):
self.xmlid = xmlid
self.cr = request.cr if cr is None else cr
self.uid = request.uid if uid is None else uid
self.context = request.context if context is None else context
self.registry = request.registry if registry is None else registry
self.env = request.env if env is None else env
self.max_css_rules = max_css_rules
self.javascripts = []
self.stylesheets = []
@@ -1154,11 +1115,11 @@ class AssetsBundle(object):
self.remains = []
self._checksum = None
context = self.context.copy()
context['inherit_branding'] = False
context['inherit_branding_auto'] = False
context['rendering_bundle'] = True
self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
env = self.env(context=dict(self.env.context,
inherit_branding=False,
inherit_branding_auto=False,
rendering_bundle=True))
self.html = env.ref(xmlid).render()
self.parse()
def parse(self):
@@ -1219,13 +1180,13 @@ class AssetsBundle(object):
response.append(jscript.to_html())
else:
if qwebcontext is None:
qwebcontext = QWebContext(self.cr, self.uid, {})
qwebcontext = QWebContext(self.env(context={}), {})
if css and self.stylesheets:
css_attachments = self.css()
if not self.css_errors:
for attachment in css_attachments:
el = etree.fromstring('<link href="%s" rel="stylesheet"/>' % attachment.url)
response.append(self.registry['ir.qweb'].render_node(el, qwebcontext))
response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
else:
msg = '\n'.join(self.css_errors)
self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
@@ -1233,7 +1194,7 @@ class AssetsBundle(object):
response.append(style.to_html())
if js and self.javascripts:
el = etree.fromstring('<script %s type="text/javascript" src="%s"></script>' % (async and 'async="async"' or '', self.js().url))
response.append(self.registry['ir.qweb'].render_node(el, qwebcontext))
response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
response.extend(self.remains)
return sep + sep.join(response)
@@ -1272,22 +1233,20 @@ class AssetsBundle(object):
of an ir.attachment unlink (because we cannot rollback a removal on the filestore), thus we
must exclude the current bundle.
"""
ira = self.registry['ir.attachment']
ira = self.env['ir.attachment']
domain = [
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.xmlid, type)), # The wilcards are id, version and pagination number (if any)
'!', ('url', '=like', '/web/content/%-{}/%'.format(self.version))
]
attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, context=self.context)
return ira.unlink(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
return ira.sudo().search(domain).unlink()
def get_attachments(self, type, inc=None):
ira = self.registry['ir.attachment']
ira = self.env['ir.attachment']
domain = [('url', '=like', '/web/content/%%-%s/%s%s.%s' % (self.version, self.xmlid, ('%%' if inc is None else '.%s' % inc), type))]
attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, order='name asc', context=self.context)
return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
return ira.sudo().search(domain, order='name asc')
def save_attachment(self, type, content, inc=None):
ira = self.registry['ir.attachment']
ira = self.env['ir.attachment']
fname = '%s%s.%s' % (self.xmlid, ('' if inc is None else '.%s' % inc), type)
values = {
@@ -1299,21 +1258,21 @@ class AssetsBundle(object):
'public': True,
'datas': content.encode('utf8').encode('base64'),
}
attachment_id = ira.create(self.cr, odoo.SUPERUSER_ID, values, context=self.context)
attachment = ira.sudo().create(values)
url = '/web/content/%s-%s/%s' % (attachment_id, self.version, fname)
url = '/web/content/%s-%s/%s' % (attachment.id, self.version, fname)
values = {
'name': url,
'url': url,
}
ira.write(self.cr, odoo.SUPERUSER_ID, attachment_id, values, context=self.context)
attachment.write(values)
if self.context.get('commit_assetsbundle') is True:
self.cr.commit()
if self.env.context.get('commit_assetsbundle') is True:
self.env.cr.commit()
self.clean_attachments(type)
return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_id, context=self.context)
return attachment
def js(self):
attachments = self.get_attachments('js')
@@ -1371,29 +1330,27 @@ class AssetsBundle(object):
""" % message
def is_css_preprocessed(self):
uid = openerp.SUPERUSER_ID
preprocessed = True
for atype in (SassStylesheetAsset, LessStylesheetAsset):
outdated = False
assets = dict((asset.html_url % asset.url, asset) for asset in self.stylesheets if isinstance(asset, atype))
if assets:
assets_domain = [('url', 'in', assets.keys())]
ira_ids = self.registry['ir.attachment'].search(self.cr, uid, assets_domain)
ira_records = self.registry['ir.attachment'].browse(self.cr, uid, ira_ids)
for ira_record in ira_records:
asset = assets[ira_record.url]
if asset.last_modified > Datetime.from_string(ira_record['__last_update']):
attachments = self.env['ir.attachment'].sudo().search(assets_domain)
for attachment in attachments:
asset = assets[attachment.url]
if asset.last_modified > fields.Datetime.from_string(attachment['__last_update']):
outdated = True
break
if asset._content is None:
asset._content = ira_record.datas and ira_record.datas.decode('base64').decode('utf8')
asset._content = attachment.datas and attachment.datas.decode('base64').decode('utf8')
if any(asset._content is None for asset in assets.itervalues()):
outdated = True
if outdated:
if ira_ids:
self.registry['ir.attachment'].unlink(self.cr, uid, ira_ids)
if attachments:
attachments.unlink()
preprocessed = False
return preprocessed
@@ -1422,11 +1379,10 @@ class AssetsBundle(object):
if debug:
try:
ira = self.registry['ir.attachment']
fname = os.path.basename(asset.url)
url = asset.html_url % asset.url
with self.cr.savepoint():
ira.create(self.cr, openerp.SUPERUSER_ID, dict(
with self.env.cr.savepoint():
self.env['ir.attachment'].sudo().create(dict(
datas=asset.content.encode('utf8').encode('base64'),
mimetype='text/css',
type='binary',
@@ -1435,10 +1391,10 @@ class AssetsBundle(object):
datas_fname=fname,
res_model=False,
res_id=False,
), context=self.context)
))
if self.context.get('commit_assetsbundle') is True:
self.cr.commit()
if self.env.context.get('commit_assetsbundle') is True:
self.env.cr.commit()
except psycopg2.Error:
pass
@@ -1497,10 +1453,7 @@ class WebAsset(object):
self.bundle = bundle
self.inline = inline
self.url = url
self.cr = bundle.cr
self.uid = bundle.uid
self.registry = bundle.registry
self.context = bundle.context
self.env = bundle.env
self._content = None
self._filename = None
self._ir_attach = None
@@ -1519,8 +1472,7 @@ class WebAsset(object):
# Test url against ir.attachments
fields = ['__last_update', 'datas', 'mimetype']
domain = [('type', '=', 'binary'), ('url', '=', self.url)]
ira = self.registry['ir.attachment']
attach = ira.search_read(self.cr, odoo.SUPERUSER_ID, domain, fields, context=self.context)
attach = self.env['ir.attachment'].sudo().search_read(domain, fields)
self._ir_attach = attach[0]
except Exception:
raise AssetNotFound("Could not find %s" % self.name)
@@ -1533,17 +1485,17 @@ class WebAsset(object):
try:
self.stat()
if self._filename:
return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
return datetime.fromtimestamp(os.path.getmtime(self._filename))
elif self._ir_attach:
server_format = odoo.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
last_update = self._ir_attach['__last_update']
try:
return datetime.datetime.strptime(last_update, server_format + '.%f')
return datetime.strptime(last_update, server_format + '.%f')
except ValueError:
return datetime.datetime.strptime(last_update, server_format)
return datetime.strptime(last_update, server_format)
except Exception:
pass
return datetime.datetime(1970, 1, 1)
return datetime(1970, 1, 1)
@property
def content(self):
+71 -63
View File
@@ -1,45 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from collections import defaultdict
import odoo
from odoo import api, fields, models, _
from odoo import SUPERUSER_ID
from odoo import tools
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.misc import unquote as unquote
class IrRule(models.Model):
_name = 'ir.rule'
_order = 'name, model_id DESC'
_order = 'model_id DESC'
_MODES = ['read', 'write', 'create', 'unlink']
def _get_value(self, cr, uid, ids, field_name, arg, context=None):
res = {}
for rule in self.browse(cr, uid, ids, context):
if not rule.groups:
res[rule.id] = True
else:
res[rule.id] = False
return res
_columns = {'global': odoo.osv.fields.function(_get_value, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone")}
name = fields.Char(index=True)
active = fields.Boolean(default=True, help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module).")
model_id = fields.Many2one('ir.model', string='Object', index=True, required=True, ondelete="cascade")
groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id')
domain_force = fields.Text(string='Domain')
domain = fields.Binary(compute='_domain_force_get', string='Domain')
domain = fields.Binary(compute='_force_domain', string='Domain')
perm_read = fields.Boolean(string='Apply for Read', default=True)
perm_write = fields.Boolean(string='Apply for Write', default=True)
perm_create = fields.Boolean(string='Apply for Create', default=True)
perm_unlink = fields.Boolean(string='Apply for Delete', default=True)
_sql_constraints = [
('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right !'),
('no_access_rights',
'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)',
'Rule must have at least one checked access right !'),
]
def _eval_context_for_combinations(self):
@@ -47,8 +36,8 @@ class IrRule(models.Model):
ir.rule domains, when the goal is to obtain python lists
that are easier to parse and combine, but not to
actually execute them."""
return {'user': unquote('user'),
'time': unquote('time')}
return {'user': tools.unquote('user'),
'time': tools.unquote('time')}
@api.model
def _eval_context(self):
@@ -56,8 +45,8 @@ class IrRule(models.Model):
ir.rule domains."""
return {'user': self.env.user, 'time': time}
@api.depends('domain')
def _domain_force_get(self):
@api.depends('domain_force')
def _force_domain(self):
eval_context = self._eval_context()
for rule in self:
if rule.domain_force:
@@ -65,56 +54,66 @@ class IrRule(models.Model):
else:
rule.domain = []
@api.depends('groups')
def _compute_global(self):
for rule in self:
rule['global'] = not rule.groups
@api.constrains('model_id')
def _check_model_obj(self):
if any(self.pool[rule.model_id.model].is_transient() for rule in self):
raise UserWarning(_('Rules can not be applied on Transient models.'))
def _check_model_transience(self):
if any(self.env[rule.model_id.model].is_transient() for rule in self):
raise ValidationError(_('Rules can not be applied on Transient models.'))
@api.constrains('model_id')
def _check_model_name(self):
# Don't allow rules on rules records (this model).
if any(rule.model_id.model == self._name for rule in self):
raise UserWarning(_('Rules can not be applied on the Record Rules model.'))
raise ValidationError(_('Rules can not be applied on the Record Rules model.'))
@tools.ormcache('uid', 'model_name', 'mode')
def _compute_domain(self, cr, uid, model_name, mode="read"):
@api.model
@tools.ormcache('self._uid', 'model_name', 'mode')
def _compute_domain(self, model_name, mode="read"):
if mode not in self._MODES:
raise ValueError('Invalid mode: %r' % (mode,))
if uid == SUPERUSER_ID:
if self._uid == SUPERUSER_ID:
return None
cr.execute("""SELECT r.id
FROM ir_rule r
JOIN ir_model m ON (r.model_id = m.id)
WHERE m.model = %s
AND r.active is True
AND r.perm_""" + mode + """
AND (r.id IN (SELECT rule_group_id FROM rule_group_rel g_rel
JOIN res_groups_users_rel u_rel ON (g_rel.group_id = u_rel.gid)
WHERE u_rel.uid = %s) OR r.global)""", (model_name, uid))
rule_ids = [x[0] for x in cr.fetchall()]
if rule_ids:
# browse user as super-admin root to avoid access errors!
user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid)
global_domains = [] # list of domains
group_domains = {} # map: group -> list of domains
for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
# read 'domain' as UID to have the correct eval context for the rule.
rule_domain = self.read(cr, uid, [rule.id], ['domain'])[0]['domain']
dom = expression.normalize_domain(rule_domain)
for group in rule.groups:
if group in user.groups_id:
group_domains.setdefault(group, []).append(dom)
if not rule.groups:
global_domains.append(dom)
# combine global domains and group domains
if group_domains:
group_domain = expression.OR(map(expression.OR, group_domains.values()))
else:
group_domain = []
domain = expression.AND(global_domains + [group_domain])
return domain
return []
query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id)
WHERE m.model=%s AND r.active AND r.perm_{mode}
AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg
JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid)
WHERE gu.uid=%s)
OR r.global)
""".format(mode=mode)
self._cr.execute(query, (model_name, self._uid))
rule_ids = [row[0] for row in self._cr.fetchall()]
if not rule_ids:
return []
# read 'domain' as self._uid to have the correct eval context for the rules.
rules = self.browse(rule_ids)
rule_domain = {vals['id']: vals['domain'] for vals in rules.read(['domain'])}
# browse user and rules as SUPERUSER_ID to avoid access errors!
user = self.env.user
global_domains = [] # list of domains
group_domains = defaultdict(list) # {group: list of domains}
for rule in rules.sudo():
dom = expression.normalize_domain(rule_domain[rule.id])
for group in rule.groups:
if group in user.groups_id:
group_domains[group].append(dom)
if not rule.groups:
global_domains.append(dom)
# combine global domains and group domains
if group_domains:
group_domain = expression.OR(map(expression.OR, group_domains.values()))
else:
group_domain = []
domain = expression.AND(global_domains + [group_domain])
return domain
@api.model
def clear_cache(self):
@@ -131,7 +130,7 @@ class IrRule(models.Model):
# references another object the user can't see).
query = self.env[model_name].sudo()._where_calc(dom, active_test=False)
return query.where_clause, query.where_clause_params, query.tables
return [], [], ['"' + self.env[model_name]._table + '"']
return [], [], ['"%s"' % self.env[model_name]._table]
@api.multi
def unlink(self):
@@ -150,3 +149,12 @@ class IrRule(models.Model):
res = super(IrRule, self).write(vals)
self.clear_caches()
return res
#
# Hack for field 'global': this field cannot be defined like others, because
# 'global' is a Python keyword. Therefore, we add it to the class by assignment.
# Note that the attribute '_module' is normally added by the class' metaclass.
#
setattr(IrRule, 'global',
fields.Boolean(compute='_compute_global', store=True, _module=IrRule._module,
help="If no group is specified the rule is global and applied to everyone"))
+60 -72
View File
@@ -11,38 +11,26 @@ _logger = logging.getLogger(__name__)
def _create_sequence(cr, seq_name, number_increment, number_next):
""" Create a PostreSQL sequence.
There is no access rights check.
"""
""" Create a PostreSQL sequence. """
if number_increment == 0:
raise UserError(_('Step must not be zero.'))
sql = "CREATE SEQUENCE %s INCREMENT BY %%s START WITH %%s" % seq_name
cr.execute(sql, (number_increment, number_next))
def _drop_sequence(cr, seq_names):
""" Drop the PostreSQL sequence if it exists.
There is no access rights check.
"""
names = []
for n in seq_names:
names.append(n)
names = ','.join(names)
def _drop_sequences(cr, seq_names):
""" Drop the PostreSQL sequences if they exist. """
names = ','.join(seq_names)
# RESTRICT is the default; it prevents dropping the sequence if an
# object depends on it.
cr.execute("DROP SEQUENCE IF EXISTS %s RESTRICT " % names)
def _alter_sequence(cr, seq_name, number_increment=None, number_next=None):
""" Alter a PostreSQL sequence.
There is no access rights check.
"""
""" Alter a PostreSQL sequence. """
if number_increment == 0:
raise UserError(_("Step must not be zero."))
cr.execute("SELECT relname FROM pg_class WHERE relkind = %s AND relname=%s", ('S', seq_name))
cr.execute("SELECT relname FROM pg_class WHERE relkind=%s AND relname=%s", ('S', seq_name))
if not cr.fetchone():
# sequence is not created yet, we're inside create() so ignore it, will be set later
return
@@ -61,8 +49,8 @@ def _select_nextval(cr, seq_name):
def _update_nogap(self, number_increment):
number_next = self.number_next
self.env.cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id))
self.env.cr.execute("UPDATE %s SET number_next=number_next+%s WHERE id=%s " % (self._table, number_increment, self.id))
self._cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id))
self._cr.execute("UPDATE %s SET number_next=number_next+%s WHERE id=%s " % (self._table, number_increment, self.id))
self.invalidate_cache(['number_next'], [self.id])
return number_next
@@ -81,26 +69,26 @@ class IrSequence(models.Model):
def _get_number_next_actual(self):
'''Return number from ir_sequence row when no_gap implementation,
and number from postgres sequence when standard implementation.'''
for element in self:
if element.implementation != 'standard':
element.number_next_actual = element.number_next
for seq in self:
if seq.implementation != 'standard':
seq.number_next_actual = seq.number_next
else:
# get number from postgres sequence. Cannot use currval, because that might give an error when
# not having used nextval before.
query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % element.id
self.env.cr.execute(query)
(last_value, increment_by, is_called) = self.env.cr.fetchone()
query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % seq.id
self._cr.execute(query)
(last_value, increment_by, is_called) = self._cr.fetchone()
if is_called:
element.number_next_actual = last_value + increment_by
seq.number_next_actual = last_value + increment_by
else:
element.number_next_actual = last_value
seq.number_next_actual = last_value
def _set_number_next_actual(self):
for record in self:
record.write({'number_next': record.number_next_actual or 0})
for seq in self:
seq.write({'number_next': seq.number_next_actual or 0})
name = fields.Char(required=True)
code = fields.Char(strring='Sequence Code')
code = fields.Char(string='Sequence Code')
implementation = fields.Selection([('standard', 'Standard'), ('no_gap', 'No gap')],
string='Implementation', required=True, default='standard',
help="Two sequence object implementations are offered: Standard "
@@ -124,18 +112,20 @@ class IrSequence(models.Model):
use_date_range = fields.Boolean(string='Use subsequences per date_range')
date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', string='Subsequences')
def init(self, cr):
@api.model_cr
def init(self):
return # Don't do the following index yet.
# CONSTRAINT/UNIQUE INDEX on (code, company_id)
# /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
# only support field names in constraint definitions, and we need a function here:
# we need to special-case company_id to treat all NULL company_id as equal, otherwise
# we would allow duplicate (code, NULL) ir_sequences.
self.env.cr.execute("""
self._cr.execute("""
SELECT indexname FROM pg_indexes WHERE indexname =
'ir_sequence_unique_code_company_id_idx'""")
if not self.env.cr.fetchone():
self.env.cr.execute("""
if not self._cr.fetchone():
self._cr.execute("""
CREATE UNIQUE INDEX ir_sequence_unique_code_company_id_idx
ON ir_sequence (code, (COALESCE(company_id,-1)))""")
@@ -145,12 +135,12 @@ class IrSequence(models.Model):
"""
seq = super(IrSequence, self).create(values)
if values.get('implementation', 'standard') == 'standard':
_create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1))
_create_sequence(self._cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1))
return seq
@api.multi
def unlink(self):
_drop_sequence(self.env.cr, ["ir_sequence_%03d" % x.id for x in self])
_drop_sequences(self._cr, ["ir_sequence_%03d" % x.id for x in self])
return super(IrSequence, self).unlink()
@api.multi
@@ -165,52 +155,50 @@ class IrSequence(models.Model):
# Implementation has NOT changed.
# Only change sequence if really requested.
if values.get('number_next'):
_alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_next=n)
_alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_next=n)
if seq.number_increment != i:
_alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_increment=i)
_alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_increment=i)
seq.date_range_ids._alter_sequence(number_increment=i)
else:
_drop_sequence(self.env.cr, ["ir_sequence_%03d" % seq.id])
_drop_sequences(self._cr, ["ir_sequence_%03d" % seq.id])
for sub_seq in seq.date_range_ids:
_drop_sequence(self.env.cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)])
_drop_sequences(self._cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)])
else:
if new_implementation in ('no_gap', None):
pass
else:
_create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, i, n)
_create_sequence(self._cr, "ir_sequence_%03d" % seq.id, i, n)
for sub_seq in seq.date_range_ids:
_create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n)
_create_sequence(self._cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n)
return super(IrSequence, self).write(values)
def _next_do(self):
if self.implementation == 'standard':
number_next = _select_nextval(self.env.cr, 'ir_sequence_%03d' % self.id)
number_next = _select_nextval(self._cr, 'ir_sequence_%03d' % self.id)
else:
number_next = _update_nogap(self, self.number_increment)
return self.get_next_char(number_next)
def get_next_char(self, number_next):
def _interpolate(s, d):
if s:
return s % d
return ''
return (s % d) if s else ''
def _interpolation_dict():
now = range_date = effective_date = datetime.now(pytz.timezone(self.env.context.get('tz') or 'UTC'))
if self.env.context.get('ir_sequence_date'):
effective_date = datetime.strptime(self.env.context.get('ir_sequence_date'), '%Y-%m-%d')
if self.env.context.get('ir_sequence_date_range'):
range_date = datetime.strptime(self.env.context.get('ir_sequence_date_range'), '%Y-%m-%d')
now = range_date = effective_date = datetime.now(pytz.timezone(self._context.get('tz') or 'UTC'))
if self._context.get('ir_sequence_date'):
effective_date = datetime.strptime(self._context.get('ir_sequence_date'), '%Y-%m-%d')
if self._context.get('ir_sequence_date_range'):
range_date = datetime.strptime(self._context.get('ir_sequence_date_range'), '%Y-%m-%d')
sequences = {
'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W',
'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S'
}
res = {}
for key, sequence in sequences.iteritems():
res[key] = effective_date.strftime(sequence)
res['range_' + key] = range_date.strftime(sequence)
res['current_' + key] = now.strftime(sequence)
for key, format in sequences.iteritems():
res[key] = effective_date.strftime(format)
res['range_' + key] = range_date.strftime(format)
res['current_' + key] = now.strftime(format)
return res
@@ -247,8 +235,8 @@ class IrSequence(models.Model):
return self._next_do()
# date mode
dt = fields.Date.today()
if self.env.context.get('ir_sequence_date'):
dt = self.env.context.get('ir_sequence_date')
if self._context.get('ir_sequence_date'):
dt = self._context.get('ir_sequence_date')
seq_date = self.env['ir.sequence.date_range'].search([('sequence_id', '=', self.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1)
if not seq_date:
seq_date = self._create_date_range_seq(dt)
@@ -278,7 +266,7 @@ class IrSequence(models.Model):
seq_ids = self.search(['&', ('code', '=', sequence_code), ('company_id', 'in', company_ids)])
if not seq_ids:
return False
force_company = self.env.context.get('force_company')
force_company = self._context.get('force_company')
if not force_company:
force_company = self.env.user.company_id.id
preferred_sequences = [s for s in seq_ids if s.company_id and s.company_id.id == force_company]
@@ -317,22 +305,22 @@ class IrSequenceDateRange(models.Model):
def _get_number_next_actual(self):
'''Return number from ir_sequence row when no_gap implementation,
and number from postgres sequence when standard implementation.'''
for element in self:
if element.sequence_id.implementation != 'standard':
element.number_next_actual = element.number_next
for seq in self:
if seq.sequence_id.implementation != 'standard':
seq.number_next_actual = seq.number_next
else:
# get number from postgres sequence. Cannot use currval, because that might give an error when
# not having used nextval before.
self.env.cr.execute("SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (element.sequence_id.id, element.id))
(last_value, increment_by, is_called) = self.env.cr.fetchone()
self._cr.execute("SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id))
(last_value, increment_by, is_called) = self._cr.fetchone()
if is_called:
element.number_next_actual = last_value + increment_by
seq.number_next_actual = last_value + increment_by
else:
element.number_next_actual = last_value
seq.number_next_actual = last_value
def _set_number_next_actual(self):
for record in self:
record.write({'number_next': record.number_next_actual or 0})
for seq in self:
seq.write({'number_next': seq.number_next_actual or 0})
date_from = fields.Date(string='From', required=True)
date_to = fields.Date(string='To', required=True)
@@ -345,7 +333,7 @@ class IrSequenceDateRange(models.Model):
def _next(self):
if self.sequence_id.implementation == 'standard':
number_next = _select_nextval(self.env.cr, 'ir_sequence_%03d_%03d' % (self.sequence_id.id, self.id))
number_next = _select_nextval(self._cr, 'ir_sequence_%03d_%03d' % (self.sequence_id.id, self.id))
else:
number_next = _update_nogap(self, self.sequence_id.number_increment)
return self.sequence_id.get_next_char(number_next)
@@ -353,7 +341,7 @@ class IrSequenceDateRange(models.Model):
@api.multi
def _alter_sequence(self, number_increment=None, number_next=None):
for seq in self:
_alter_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id), number_increment=number_increment, number_next=number_next)
_alter_sequence(self._cr, "ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id), number_increment=number_increment, number_next=number_next)
@api.model
def create(self, values):
@@ -362,12 +350,12 @@ class IrSequenceDateRange(models.Model):
seq = super(IrSequenceDateRange, self).create(values)
main_seq = seq.sequence_id
if main_seq.implementation == 'standard':
_create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1))
_create_sequence(self._cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1))
return seq
@api.multi
def unlink(self):
_drop_sequence(self.env.cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self])
_drop_sequences(self._cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self])
return super(IrSequenceDateRange, self).unlink()
@api.multi
+194 -191
View File
@@ -5,9 +5,9 @@ import logging
from collections import defaultdict
from difflib import get_close_matches
import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.modules import get_module_path, get_module_resource
_logger = logging.getLogger(__name__)
@@ -24,38 +24,35 @@ TRANSLATION_TYPE = [
]
class IrTranslationImportCursor(object):
"""Temporary cursor for optimizing mass insert into ir.translation
class IrTranslationImport(object):
""" Temporary cursor for optimizing mass insert into model 'ir.translation'.
Open it (attached to a sql cursor), feed it with translation data and
finish() it in order to insert multiple translations in a batch.
"""
_table_name = 'tmp_ir_translation_import'
_table = 'tmp_ir_translation_import'
def __init__(self, cr, uid, parent, context):
""" Initializer
Store some values, and also create a temporary SQL table to accept
def __init__(self, model):
""" Store some values, and also create a temporary SQL table to accept
the data.
@param parent an instance of ir.translation ORM model
:param model: the model to insert the data into (as a recordset)
"""
self._cr = cr
self._uid = uid
self._context = context
self._overwrite = context.get('overwrite', False)
self._cr = model._cr
self._model_table = model._table
self._overwrite = model._context.get('overwrite', False)
self._debug = False
self._parent_table = parent._table
# Note that Postgres will NOT inherit the constraints or indexes
# of ir_translation, so this copy will be much faster.
cr.execute('''CREATE TEMP TABLE %s(
imd_model VARCHAR(64),
imd_name VARCHAR(128)
) INHERITS (%s) ''' % (self._table_name, self._parent_table))
query = """ CREATE TEMP TABLE %s (
imd_model VARCHAR(64),
imd_name VARCHAR(128)
) INHERITS (%s) """ % (self._table, self._model_table)
self._cr.execute(query)
def push(self, trans_dict):
"""Feed a translation, as a dictionary, into the cursor
"""
""" Feed a translation, as a dictionary, into the cursor """
if not trans_dict['value']:
return
@@ -92,38 +89,37 @@ class IrTranslationImportCursor(object):
params['name'] = 'ir.ui.view,arch_db'
params['imd_model'] = "ir.ui.view"
self._cr.execute("""INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
%%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s)""" % self._table_name,
params)
query = """ INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
%%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s) """ % self._table
self._cr.execute(query, params)
def finish(self):
""" Transfer the data from the temp table to ir.translation
"""
""" Transfer the data from the temp table to ir.translation """
cr = self._cr
if self._debug:
cr.execute("SELECT count(*) FROM %s" % self._table_name)
c = cr.fetchone()[0]
_logger.debug("ir.translation.cursor: We have %d entries to process", c)
cr.execute("SELECT count(*) FROM %s" % self._table)
count = cr.fetchone()[0]
_logger.debug("ir.translation.cursor: We have %d entries to process", count)
# Step 1: resolve ir.model.data references to res_ids
cr.execute("""UPDATE %s AS ti
SET res_id = imd.res_id
FROM ir_model_data AS imd
WHERE ti.res_id IS NULL
AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
AND ti.module = imd.module AND ti.imd_name = imd.name
AND ti.imd_model = imd.model; """ % self._table_name)
cr.execute(""" UPDATE %s AS ti
SET res_id = imd.res_id
FROM ir_model_data AS imd
WHERE ti.res_id IS NULL
AND ti.module IS NOT NULL AND ti.imd_name IS NOT NULL
AND ti.module = imd.module AND ti.imd_name = imd.name
AND ti.imd_model = imd.model; """ % self._table)
if self._debug:
cr.execute("SELECT module, imd_name, imd_model FROM %s " \
"WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
cr.execute(""" SELECT module, imd_name, imd_model FROM %s
WHERE res_id IS NULL AND module IS NOT NULL """ % self._table)
for row in cr.fetchall():
_logger.info("ir.translation.cursor: missing res_id for %s.%s <%s> ", *row)
# Records w/o res_id must _not_ be inserted into our db, because they are
# referencing non-existent data.
cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % self._table_name)
cr.execute("DELETE FROM %s WHERE res_id IS NULL AND module IS NOT NULL" % self._table)
find_expr = """
irt.lang = ti.lang
@@ -140,31 +136,31 @@ class IrTranslationImportCursor(object):
# Step 2: update existing (matching) translations
if self._overwrite:
cr.execute("""UPDATE ONLY %s AS irt
SET value = ti.value,
src = ti.src,
state = 'translated'
FROM %s AS ti
WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
""" % (self._parent_table, self._table_name, find_expr))
cr.execute(""" UPDATE ONLY %s AS irt
SET value = ti.value,
src = ti.src,
state = 'translated'
FROM %s AS ti
WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
""" % (self._model_table, self._table, find_expr))
# Step 3: insert new translations
cr.execute("""INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
SELECT name, lang, res_id, src, type, value, module, state, comments
FROM %s AS ti
WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
""" % (self._parent_table, self._table_name, self._parent_table, find_expr))
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
SELECT name, lang, res_id, src, type, value, module, state, comments
FROM %s AS ti
WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
""" % (self._model_table, self._table, self._model_table, find_expr))
if self._debug:
cr.execute('SELECT COUNT(*) FROM ONLY %s' % self._parent_table)
c1 = cr.fetchone()[0]
cr.execute('SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s' % \
(self._parent_table, self._table_name, find_expr))
c = cr.fetchone()[0]
_logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", c1, c)
cr.execute("SELECT COUNT(*) FROM ONLY %s" % self._model_table)
total = cr.fetchone()[0]
cr.execute("SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s" % \
(self._model_table, self._table, find_expr))
count = cr.fetchone()[0]
_logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", total, count)
# Step 4: cleanup
cr.execute("DROP TABLE %s" % self._table_name)
cr.execute("DROP TABLE %s" % self._table)
return True
@@ -172,18 +168,13 @@ class IrTranslation(models.Model):
_name = "ir.translation"
_log_access = False
@api.model
def _get_language(self):
lang_model = self.env['res.lang']
lang = lang_model.search([('translatable', '=', True)])
return [(data.code, data.name) for data in lang]
name = fields.Char(string='Translated field', required=True)
res_id = fields.Integer(string='Record ID', index=True)
lang = fields.Selection(selection='_get_language', string='Language')
lang = fields.Selection(selection='_get_languages', string='Language')
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
source = fields.Text(compute='_get_src', inverse='_set_src', search='_search_src', string='Source term')
source = fields.Text(string='Source term', compute='_compute_source',
inverse='_inverse_source', search='_search_source')
value = fields.Text(string='Translation Value')
module = fields.Char(index=True, help="Module this term belongs to")
@@ -197,60 +188,65 @@ class IrTranslation(models.Model):
# cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
comments = fields.Text(string='Translation comments', index=True)
_sql_constraints = [('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
'Language code of translation item must be among known languages')]
_sql_constraints = [
('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
'Language code of translation item must be among known languages'),
]
@api.depends('source')
def _get_src(self):
''' Get source name for the translation. If object type is model then
return the value store in db. Otherwise return value store in src field
@api.model
def _get_languages(self):
langs = self.env['res.lang'].search([('translatable', '=', True)])
return [(lang.code, lang.name) for lang in langs]
@api.depends('type', 'name', 'res_id')
def _compute_source(self):
''' Get source name for the translation. If object type is model, return
the value stored in db. Otherwise, return value store in src field.
'''
for record in self:
record.source = record.src
if record.type != 'model':
continue
model_name, field_name = record.name.split(',')
if model_name not in self.env:
continue
model = self.env[model_name]
field = model._fields.get(field_name)
if field is None:
continue
if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
result = model.browse(record.res_id).with_context(lang=None).read([field_name])
record.source = result[0][field_name] if result else False
def _inverse_source(self):
''' When changing source term of a translation, change its value in db
for the associated object, and the src field.
'''
for record in self:
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.env[model_name]
if model is None:
continue
field = model._fields.get(field_name)
if field is None:
continue
field = model._fields[field_name]
if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
context_no_lang = dict(self.env.context, lang=None)
result = model.with_context(context_no_lang).browse(record.res_id).read([field_name])
record.source = result[0][field_name] if result else False
def _set_src(self):
''' When changing source term of a translation, change its value in db for
the associated object, and the src field
'''
if self.type == 'model':
model_name, field_name = self.name.split(',')
model = self.env[model_name]
field = model._fields[field_name]
if not callable(field.translate):
# Make a context without language information, because we want
# to write on the value stored in db and not on the one
# associated with the current language. Also not removing lang
# from context trigger an error when lang is different.
context_wo_lang = self.env.context.copy()
context_wo_lang.pop('lang', None)
model.with_context(context_wo_lang).browse(self.res_id).write({field_name: self.source})
# Make a context without language information, because we want
# to write on the value stored in db and not on the one
# associated with the current language. Also not removing lang
# from context trigger an error when lang is different.
model.browse(record.res_id).with_context(lang=None).write({field_name: record.source})
return self.write({'src': self.source})
@api.model
def _search_src(self, operator, operand):
def _search_source(self, operator, value):
''' the source term is stored on 'src' field '''
res = []
for field, operator, value in operand:
res.append(('src', operator, value))
return res
return [('src', operator, value)]
@api.model_cr_context
def _auto_init(self):
super(IrTranslation, self)._auto_init()
cr = self._cr
self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
indexes = [row[0] for row in self.env.cr.fetchall()]
cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
indexes = [row[0] for row in cr.fetchall()]
# Removed because there is a size limit on btree indexed values (problem with column src):
# cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)')
@@ -259,17 +255,17 @@ class IrTranslation(models.Model):
# Removed because hash indexes are not compatible with postgres streaming replication:
# cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation USING hash (src)')
if set(indexes) & set(['ir_translation_ltns', 'ir_translation_lts', 'ir_translation_src_hash_idx']):
self.env.cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
self.env.cr.commit()
cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
cr.commit()
# Add separate md5 index on src (no size limit on values, and good performance).
if 'ir_translation_src_md5' not in indexes:
self.env.cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
self.env.cr.commit()
cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
cr.commit()
if 'ir_translation_ltn' not in indexes:
self.env.cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
self.env.cr.commit()
cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
cr.commit()
@api.model
def _check_selection_field_value(self, field, value):
@@ -278,54 +274,64 @@ class IrTranslation(models.Model):
return super(IrTranslation, self)._check_selection_field_value(field, value)
@api.model
def _get_ids(self, name, tt, lang, in_ids):
if in_ids:
self.env.cr.execute('select res_id,value '
'from ir_translation '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s',
(lang, tt, name, tuple(in_ids)))
for res_id, value in self.env.cr.fetchall():
self.res_id = value
def _get_ids(self, name, tt, lang, ids):
""" Return the translations of records.
:param name: a string defined as "<model_name>,<field_name>"
:param tt: the type of translation (should always be "model")
:param lang: the language code
:param ids: the ids of the given records
"""
translations = dict.fromkeys(ids, False)
if ids:
self._cr.execute("""SELECT res_id, value FROM ir_translation
WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s""",
(lang, tt, name, tuple(ids)))
for res_id, value in self._cr.fetchall():
translations[res_id] = value
return translations
@api.model
def _set_ids(self, name, tt, lang, rec_ids, value, src=None):
def _set_ids(self, name, tt, lang, ids, value, src=None):
""" Update the translations of records.
:param name: a string defined as "<model_name>,<field_name>"
:param tt: the type of translation (should always be "model")
:param lang: the language code
:param ids: the ids of the given records
:param value: the value of the translation
:param src: the source of the translation
"""
self.clear_caches()
self.env.cr.execute('update ir_translation '
'set value=%s '
' , src=%s '
' , state=%s '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s '
'returning res_id',
(value, src, 'translated', lang, tt, name, tuple(rec_ids),))
existing_ids = [x[0] for x in self.env.cr.fetchall()]
# update existing translations
self._cr.execute("""UPDATE ir_translation
SET value=%s, src=%s, state=%s
WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s
RETURNING res_id""",
(value, src, 'translated', lang, tt, name, tuple(ids)))
existing_ids = [row[0] for row in self._cr.fetchall()]
for id in list(set(rec_ids) - set(existing_ids)):
self.create({'lang': lang,
'type': tt,
'name': name,
'res_id': id,
'value': value,
'src': src,
'state': 'translated'}).id
return len(rec_ids)
# create missing translations
for res_id in set(ids) - set(existing_ids):
self.create({
'lang': lang,
'type': tt,
'name': name,
'res_id': res_id,
'value': value,
'src': src,
'state': 'translated',
})
return len(ids)
@api.model
def _get_source_query(self, name, types, lang, source, res_id):
if source:
# Note: the extra test on md5(src) is a hint for postgres to use the
# index ir_translation_src_md5
query = """SELECT value
FROM ir_translation
WHERE lang=%s
AND type in %s
AND src=%s AND md5(src)=md5(%s)"""
query = """SELECT value FROM ir_translation
WHERE lang=%s AND type in %s AND src=%s AND md5(src)=md5(%s)"""
source = tools.ustr(source)
params = (lang or '', types, source, source)
if res_id:
@@ -335,12 +341,8 @@ class IrTranslation(models.Model):
query += " AND name=%s"
params += (tools.ustr(name),)
else:
query = """SELECT value
FROM ir_translation
WHERE lang=%s
AND type in %s
AND name=%s"""
query = """ SELECT value FROM ir_translation
WHERE lang=%s AND type in %s AND name=%s """
params = (lang or '', types, tools.ustr(name))
return (query, params)
@@ -349,8 +351,8 @@ class IrTranslation(models.Model):
def __get_source(self, name, types, lang, source, res_id):
# res_id is a tuple or None, otherwise ormcache cannot cache it!
query, params = self._get_source_query(name, types, lang, source, res_id)
self.env.cr.execute(query, params)
res = self.env.cr.fetchone()
self._cr.execute(query, params)
res = self._cr.fetchone()
trad = res and res[0] or u''
if source and not trad:
return tools.ustr(source)
@@ -358,10 +360,9 @@ class IrTranslation(models.Model):
@api.model
def _get_source(self, name, types, lang, source=None, res_id=None):
"""
Returns the translation for the given combination of name, type, language
and source. All values passed to this method should be unicode (not byte strings),
especially ``source``.
""" Return the translation for the given combination of ``name``,
``type``, ``language`` and ``source``. All values passed to this method
should be unicode (not byte strings), especially ``source``.
:param name: identification of the term to translate, such as field name (optional if source is passed)
:param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
@@ -372,8 +373,8 @@ class IrTranslation(models.Model):
:return: the request translation, or an empty unicode string if no translation was
found and `source` was not passed
"""
# FIXME: should assert that `source` is unicode and fix all callers to always pass unicode
# so we can remove the string encoding/decoding.
# FIXME: should assert that `source` is unicode and fix all callers to
# always pass unicode so we can remove the string encoding/decoding.
if not lang:
return tools.ustr(source or '')
if isinstance(types, basestring):
@@ -659,24 +660,25 @@ class IrTranslation(models.Model):
}
return action
def _get_import_cursor(self, cr, uid, context=None):
""" Return a cursor-like object for fast inserting translations
"""
return IrTranslationImportCursor(cr, uid, self, context=context)
@api.model
def _get_import_cursor(self):
""" Return a cursor-like object for fast inserting translations """
return IrTranslationImport(self)
def load_module_terms(self, cr, modules, langs, context=None):
context_template = dict(context or {}) # local copy
@api.model_cr_context
def load_module_terms(self, modules, langs):
""" Load PO files of the given modules for the given languages. """
# make sure the given languages are active
lang_obj = self.pool['res.lang']
res_lang = self.env['res.lang'].sudo()
for lang in langs:
lang_obj.load_lang(cr, odoo.SUPERUSER_ID, lang)
res_lang.load_lang(lang)
# load i18n files
for module_name in modules:
modpath = odoo.modules.get_module_path(module_name)
modpath = get_module_path(module_name)
if not modpath:
continue
for lang in langs:
context = dict(context_template)
context = dict(self._context)
lang_code = tools.get_iso_codes(lang)
base_lang_code = None
if '_' in lang_code:
@@ -684,31 +686,31 @@ class IrTranslation(models.Model):
# Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po)
if base_lang_code:
base_trans_file = odoo.modules.get_module_resource(module_name, 'i18n', base_lang_code + '.po')
base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po')
if base_trans_file:
_logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
# i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
base_trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
if base_trans_extra_file:
_logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language
trans_file = odoo.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file:
_logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
elif lang_code != 'en_US':
_logger.info('module %s: no translation for language %s', module_name, lang_code)
trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
if trans_extra_file:
_logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
return True
@api.model
@@ -721,18 +723,19 @@ class IrTranslation(models.Model):
:return: action definition to open the list of available translations
"""
fields = self.env['ir.model.fields'].search([('model', '=', model_name)])
view = self.env.ref("base.view_translation_tree", False)
view = self.env.ref("base.view_translation_tree", False) or self.env['ir.ui.view']
return {
'name': _("Technical Translation"),
'name': _("Technical Translations"),
'view_mode': 'tree',
'views': [(view and view.id or False, "list")],
'views': [(view.id, "list")],
'res_model': 'ir.translation',
'type': 'ir.actions.act_window',
'domain': ['|',
'&',('type', '=', 'model'),
'&',('res_id', 'in', fields.ids),
('name', 'like', 'ir.model.fields,'),
'&',('type', '=', 'selection'),
('name', 'like', model_name+','),
],
'domain': [
'|',
'&', ('type', '=', 'model'),
'&', ('res_id', 'in', fields.ids),
('name', 'like', 'ir.model.fields,'),
'&', ('type', '=', 'selection'),
('name', 'like', model_name+','),
],
}
+97 -128
View File
@@ -5,13 +5,14 @@ import base64
import operator
import re
import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.modules import get_module_resource
from odoo.tools.safe_eval import safe_eval as eval
MENU_ITEM_SEPARATOR = "/"
NUMBER_PARENS = re.compile(r"\(([0-9]+)\)")
class IrUiMenu(models.Model):
@@ -33,7 +34,7 @@ class IrUiMenu(models.Model):
'menu_id', 'gid', string='Groups',
help="If you have groups, the visibility of this menu will be based on these groups. "\
"If this field is empty, Odoo will compute visibility based on the related object's read access.")
complete_name = fields.Char(compute='_get_full_name', string='Full Path')
complete_name = fields.Char(compute='_compute_complete_name', string='Full Path')
web_icon = fields.Char(string='Web Icon File')
action = fields.Reference(selection=[('ir.actions.report.xml', 'ir.actions.report.xml'),
('ir.actions.act_window', 'ir.actions.act_window'),
@@ -43,6 +44,20 @@ class IrUiMenu(models.Model):
web_icon_data = fields.Binary(string='Web Icon Image', compute="_compute_web_icon", store=True, attachment=True)
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for menu in self:
menu.complete_name = menu._get_full_name()
def _get_full_name(self, level=6):
""" Return the full name of ``self`` (up to a certain level). """
if level <= 0:
return '...'
if self.parent_id:
return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name
else:
return self.name
@api.depends('web_icon')
def _compute_web_icon(self):
for menu in self:
@@ -52,29 +67,17 @@ class IrUiMenu(models.Model):
if not path:
return False
path_info = path.split(',')
icon_path = odoo.modules.get_module_resource(path_info[0], path_info[1])
icon_path = get_module_resource(path_info[0], path_info[1])
icon_image = False
if icon_path:
try:
icon_file = tools.file_open(icon_path, 'rb')
with tools.file_open(icon_path, 'rb') as icon_file:
icon_image = base64.encodestring(icon_file.read())
finally:
icon_file.close()
return icon_image
@api.depends('complete_name')
def _get_full_name(self):
for element in self:
element.complete_name = self._get_one_full_name(element)
def _get_one_full_name(self, elmt, level=6):
if level <= 0:
return '...'
if elmt.parent_id:
parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
else:
parent_path = ''
return parent_path + elmt.name
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('Error! You cannot create recursive menus.'))
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
@@ -84,7 +87,9 @@ class IrUiMenu(models.Model):
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search([])
groups = self.env.user.groups_id if debug else self.env.user.groups_id - self.env.ref('base.group_no_one')
groups = self.env.user.groups_id
if not debug:
groups = groups - self.env.ref('base.group_no_one')
# first discard all menus with groups the user does not have
menus = menus.filtered(
lambda menu: not menu.groups_id or menu.groups_id & groups)
@@ -124,39 +129,22 @@ class IrUiMenu(models.Model):
visible_ids = self._visible_menu_ids(request.debug if request else False)
return self.filtered(lambda menu: menu.id in visible_ids)
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None:
context = {}
ids = super(IrUiMenu, self).search(cr, uid, args, offset=0,
limit=None, order=order, context=context, count=False)
if not ids:
if count:
return 0
return []
# menu filtering is done only on main menu tree, not other menu lists
if context.get('ir.ui.menu.full_list'):
result = ids
else:
result = self._filter_visible_menus(cr, uid, ids, context=context)
if offset:
result = result[long(offset):]
if limit:
result = result[:long(limit)]
if count:
return len(result)
return result
@api.model
def search(self, args, offset=0, limit=None, order=None, count=False):
menus = super(IrUiMenu, self).search(args, offset=0, limit=None, order=order, count=False)
if menus:
# menu filtering is done only on main menu tree, not other menu lists
if not self._context.get('ir.ui.menu.full_list'):
menus = menus._filter_visible_menus()
if offset:
menus = menus[long(offset):]
if limit:
menus = menus[:long(limit)]
return len(menus) if count else menus
@api.multi
def name_get(self):
res = []
for element in self:
res.append((element.id, self._get_one_full_name(element)))
return res
return [(menu.id, menu._get_full_name()) for menu in self]
@api.model
def create(self, values):
@@ -174,81 +162,66 @@ class IrUiMenu(models.Model):
# cascade-delete submenus blindly. We also can't use ondelete=set null because
# that is not supported when _parent_store is used (would silently corrupt it).
# TODO: ideally we should move them under a generic "Orphans" menu somewhere?
local_context = dict(self.env.context or {})
local_context['ir.ui.menu.full_list'] = True
direct_children = self.with_context(local_context).search([('parent_id', 'in', self.ids)])
if direct_children:
direct_children.write({'parent_id': False})
extra = {'ir.ui.menu.full_list': True}
direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)])
direct_children.write({'parent_id': False})
result = super(IrUiMenu, self).unlink()
self.clear_caches()
return result
return super(IrUiMenu, self).unlink()
@api.multi
def copy(self, default=None):
res = super(IrUiMenu, self).copy(default=default)
rex = re.compile('\([0-9]+\)')
concat = rex.findall(res.name)
if concat:
next_num = int(concat[0])+1
res.name = rex.sub(('(%d)' % next_num), res.name)
record = super(IrUiMenu, self).copy(default=default)
match = NUMBER_PARENS.search(record.name)
if match:
next_num = int(match.group(1)) + 1
record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name)
else:
res.name += '(1)'
res.write({'name': res.name})
return res
record.name = record.name + '(1)'
return record
@api.multi
def get_needaction_data(self):
""" Return for each menu entry of ids :
- if it uses the needaction mechanism (needaction_enabled)
""" Return for each menu entry in ``self``:
- whether it uses the needaction mechanism (needaction_enabled)
- the needaction counter of the related action, taking into account
the action domain
"""
res = {}
menu_ids = set()
for menu in self:
menu_ids.add(menu.id)
ctx = None
ctx = {}
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.context:
try:
with tools.ignore(Exception):
# use magical UnquoteEvalContext to ignore undefined client-side variables such as `active_id`
ctxt = dict(self.env.context or {})
eval_ctx = tools.UnquoteEvalContext(**ctxt)
ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or None
except Exception:
# if the eval still fails for some reason, we'll simply skip this menu
pass
menu_ref = ctx and ctx.get('needaction_menu_ref')
if menu_ref:
if not isinstance(menu_ref, list):
menu_ref = [menu_ref]
model_data_obj = self.env['ir.model.data']
for menu_data in menu_ref:
try:
model, model_id = model_data_obj.get_object_reference(menu_data.split('.')[0], menu_data.split('.')[1])
if (model == 'ir.ui.menu'):
menu_ids.add(model_id)
except Exception:
pass
eval_ctx = tools.UnquoteEvalContext(self._context)
ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or {}
menu_refs = ctx.get('needaction_menu_ref')
if menu_refs:
if not isinstance(menu_refs, list):
menu_refs = [menu_refs]
for menu_ref in menu_refs:
record = self.env.ref(menu_ref, False)
if record and record._name == 'ir.ui.menu':
menu_ids.add(record.id)
menu_ids = list(menu_ids)
for menu in self:
res = {}
for menu in self.browse(menu_ids):
res[menu.id] = {
'needaction_enabled': False,
'needaction_counter': False,
}
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
if menu.action.res_model in self.pool:
obj = self.env[menu.action.res_model]
if obj._needaction:
if menu.action.res_model in self.env:
model = self.env[menu.action.res_model]
if model._needaction:
if menu.action.type == 'ir.actions.act_window':
eval_context = self.env['ir.actions.act_window']._get_eval_context()
dom = menu.action.domain and eval(menu.action.domain, eval_context) or []
dom = eval(menu.action.domain or '[]', eval_context)
else:
dom = eval(menu.action.params_store or '{}', {'uid': self.env.uid}).get('domain')
res[menu.id]['needaction_enabled'] = obj._needaction
res[menu.id]['needaction_counter'] = obj._needaction_count(dom)
dom = eval(menu.action.params_store or '{}', {'uid': self._uid}).get('domain')
res[menu.id]['needaction_enabled'] = model._needaction
res[menu.id]['needaction_counter'] = model._needaction_count(dom)
return res
@api.model
@@ -261,66 +234,62 @@ class IrUiMenu(models.Model):
"""
return self.search([('parent_id', '=', False)])
@api.cr_uid_context
@tools.ormcache_context('uid', keys=('lang',))
def load_menus_root(self, cr, uid, context=None):
@api.model
@tools.ormcache_context('self._uid', keys=('lang',))
def load_menus_root(self):
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
menu_root_ids = self.get_user_roots(cr, uid, context=context)
menu_roots = self.read(cr, uid, menu_root_ids, fields, context=context) if menu_root_ids else []
menu_roots = self.get_user_roots()
menu_roots_data = menu_roots.read(fields) if menu_roots else []
return {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': menu_roots,
'all_menu_ids': menu_root_ids,
'children': menu_roots_data,
'all_menu_ids': menu_roots.ids,
}
@api.cr_uid_context
@tools.ormcache_context('uid', 'debug', keys=('lang',))
def load_menus(self, cr, uid, debug, context=None):
@api.model
@tools.ormcache_context('self._uid', 'debug', keys=('lang',))
def load_menus(self, debug):
""" Loads all menu items (all applications and their sub-menus).
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
menu_root_ids = self.get_user_roots(cr, uid, context=context)
menu_roots = self.read(cr, uid, menu_root_ids, fields, context=context) if menu_root_ids else []
menu_roots = self.get_user_roots()
menu_roots_data = menu_roots.read(fields) if menu_roots else []
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': menu_roots,
'all_menu_ids': menu_root_ids,
'children': menu_roots_data,
'all_menu_ids': menu_roots.ids,
}
if not menu_roots:
if not menu_roots_data:
return menu_root
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
menu_ids = self.search(cr, uid, [('id', 'child_of', menu_root_ids)], 0, False, False, context=context)
menu_items = self.read(cr, uid, menu_ids, fields, context=context)
# adds roots at the end of the sequence, so that they will overwrite
menus = self.search([('id', 'child_of', menu_roots.ids)])
menu_items = menus.read(fields)
# add roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots)
menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
menu_items.extend(menu_roots_data)
menu_root['all_menu_ids'] = menus.ids # includes menu_roots!
# make a tree using parent_id
menu_items_map = dict(
(menu_item["id"], menu_item) for menu_item in menu_items)
menu_items_map = {menu_item["id"]: menu_item for menu_item in menu_items}
for menu_item in menu_items:
if menu_item['parent_id']:
parent = menu_item['parent_id'][0]
else:
parent = False
parent = menu_item['parent_id'] and menu_item['parent_id'][0]
if parent in menu_items_map:
menu_items_map[parent].setdefault(
'children', []).append(menu_item)
# sort by sequence a tree using parent_id
for menu_item in menu_items:
menu_item.setdefault('children', []).sort(
key=operator.itemgetter('sequence'))
menu_item.setdefault('children', []).sort(key=operator.itemgetter('sequence'))
return menu_root
+292 -291
View File
@@ -14,20 +14,19 @@ from operator import itemgetter
import json
import werkzeug
from lxml import etree
from lxml.builder import E
import odoo
from odoo import api, fields, models, tools, _
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.modules.module import get_resource_from_path, get_resource_path
from odoo.osv import orm, osv
from odoo.tools import config, graph, SKIPPED_ELEMENT_TYPES
from odoo.tools import misc
from odoo.osv import orm
from odoo.tools import config, graph, ConstantMapping, SKIPPED_ELEMENT_TYPES
from odoo.tools.convert import _fix_multiple_roots
from odoo.tools.parse_version import parse_version
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.view_validation import valid_view
from odoo.tools.translate import encode, TRANSLATED_ATTRS, xml_translate
from odoo.tools.translate import encode, xml_translate, TRANSLATED_ATTRS
_logger = logging.getLogger(__name__)
@@ -65,34 +64,33 @@ class ViewCustom(models.Model):
user_id = fields.Many2one('res.users', string='User', index=True, required=True, ondelete='cascade')
arch = fields.Text(string='View Architecture', required=True)
@api.depends('user_id')
@api.multi
def name_get(self):
return [(rec.id, rec.user_id.name) for rec in self]
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
if name:
self = self.search([('user_id', operator, name)] + args, limit=limit)
return self.name_get()
recs = self.search([('user_id', operator, name)] + (args or []), limit=limit)
return recs.name_get()
return super(ViewCustom, self).name_search(name, args=args, operator=operator, limit=limit)
@api.model_cr_context
def _auto_init(self):
super(ViewCustom, self)._auto_init()
self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
if not self.env.cr.fetchone():
self.env.cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_ui_view_custom_user_id_ref_id'")
if not self._cr.fetchone():
self._cr.execute("CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)")
def _hasclass(context, *cls):
""" Checks if the context node has all the classes passed as arguments
"""
node_classes = set(context.context_node.attrib.get('class', '').split())
return node_classes.issuperset(cls)
def get_view_arch_from_file(filename, xmlid):
doc = etree.parse(filename)
node = None
for n in doc.xpath('//*[@id="%s"]' % (xmlid)):
@@ -105,7 +103,6 @@ def get_view_arch_from_file(filename, xmlid):
if n.tag in ('template', 'record'):
node = n
break
if node is not None:
if node.tag == 'record':
field = node.find('field[@name="arch"]')
@@ -121,7 +118,7 @@ def get_view_arch_from_file(filename, xmlid):
node.tag = 'data'
node.attrib.pop('id', None)
return etree.tostring(node)
_logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'" % (filename, xmlid))
_logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid)
return None
xpath_utils = etree.FunctionNamespace(None)
@@ -137,63 +134,8 @@ class View(models.Model):
# Holds the RNG schema
_relaxng_validator = None
@api.depends('model_data_id')
def _get_model_data(self):
result = dict.fromkeys(self.ids, False)
IMD = self.env['ir.model.data']
data_ids = IMD.search_read([('res_id', 'in', self.ids), ('model', '=', 'ir.ui.view')], ['res_id'])
result.update(map(itemgetter('res_id', 'id'), data_ids))
return result
def _resolve_external_ids(self, cr, uid, view, arch_fs):
def replacer(m):
xmlid = m.group('xmlid')
if '.' not in xmlid:
mod = view.get_external_id(cr, uid).get(view.id).split('.')[0]
xmlid = '%s.%s' % (mod, xmlid)
return m.group('prefix') + str(self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xmlid))
return re.sub('(?P<prefix>[^%])%\((?P<xmlid>.*?)\)[ds]', replacer, arch_fs)
@api.depends('arch')
def _arch_get(self):
for view in self:
arch_fs = None
if config['dev_mode'] and view.arch_fs and view.xml_id:
# It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
fullpath = get_resource_path(*view.arch_fs.split('/'))
arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
# replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
arch_fs = arch_fs and self._resolve_external_ids(view, arch_fs)
view.arch = arch_fs or view.arch_db
def _arch_set(self):
if self.arch:
for view in self:
data = dict(arch_db=self.arch)
key = 'install_mode_data'
if self.env.context and key in self.env.context:
imd = self.env.context[key]
if self._model._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
# we store the relative path to the resource instead of the absolute path, if found
# (it will be missing e.g. when importing data-only modules using base_import_module)
path_info = get_resource_from_path(imd['xml_file'])
if path_info:
data['arch_fs'] = '/'.join(path_info[0:2])
self.write(data)
return True
@api.depends('arch_base')
def _arch_base_get(self):
""" Return the field 'arch' without translation. """
return self.with_context(lang=None)._arch_get()
def _arch_base_set(self):
""" Assign the field 'arch' without translation. """
return self.with_context(lang=None)._arch_set()
name = fields.Char(string='View Name', required=True)
model = fields.Char(string='Object', index=True)
model = fields.Char(index=True)
key = fields.Char()
priority = fields.Integer(string='Sequence', default=16, required=True)
type = fields.Selection([('tree', 'Tree'),
@@ -207,18 +149,16 @@ class View(models.Model):
('sales_team_dashboard', 'Sales Team Dashboard'),
('search', 'Search'),
('qweb', 'QWeb')], string='View Type')
arch = fields.Text(compute='_arch_get', inverse='_arch_set', string='View Architecture', nodrop=True)
arch_base = fields.Text(compute='_arch_base_get', inverse='_arch_base_set', string='View Architecture')
arch = fields.Text(compute='_compute_arch', inverse='_inverse_arch', string='View Architecture', nodrop=True)
arch_base = fields.Text(compute='_compute_arch_base', inverse='_inverse_arch_base', string='View Architecture')
arch_db = fields.Text(string='Arch Blob', translate=xml_translate, oldname='arch')
arch_fs = fields.Char(string='Arch Filename')
inherit_id = fields.Many2one('ir.ui.view', string='Inherited View', ondelete='restrict', index=True)
inherit_children_ids = fields.One2many('ir.ui.view', 'inherit_id', string='Views which inherit from this one')
field_parent = fields.Char(string='Child Field')
model_data_id = fields.Many2one('ir.model.data', compute='_get_model_data', string="Model Data", store=True)
# xml_id = fields.Char(compute=model.Model.get_xml_id, string="External ID",
# help="ID of the view defined in xml file")
_columns = {'xml_id': odoo.osv.fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
help="ID of the view defined in xml file")}
model_data_id = fields.Many2one('ir.model.data', compute='_compute_model_data_id', string="Model Data", store=True)
xml_id = fields.Char(string="External ID", compute='_compute_xml_id',
help="ID of the view defined in xml file")
groups_id = fields.Many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only.")
model_ids = fields.One2many('ir.model.data', 'res_id', domain=[('model', '=', 'ir.ui.view')], auto_join=True)
@@ -243,19 +183,75 @@ actual arch.
* if False, the view currently does not extend its parent but can be enabled
""")
@api.depends('arch_db', 'arch_fs')
def _compute_arch(self):
def resolve_external_ids(arch_fs, view_xml_id):
def replacer(m):
xmlid = m.group('xmlid')
if '.' not in xmlid:
xmlid = '%s.%s' % (view_xml_id.split('.')[0], xmlid)
return m.group('prefix') + str(self.env['ir.model.data'].xmlid_to_res_id(xmlid))
return re.sub(r'(?P<prefix>[^%])%\((?P<xmlid>.*?)\)[ds]', replacer, arch_fs)
for view in self:
arch_fs = None
if config['dev_mode'] and view.arch_fs and view.xml_id:
# It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
fullpath = get_resource_path(*view.arch_fs.split('/'))
arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
# replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id)
view.arch = arch_fs or view.arch_db
def _inverse_arch(self):
for view in self:
data = dict(arch_db=view.arch)
if 'install_mode_data' in self._context:
imd = self._context['install_mode_data']
if self._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
# we store the relative path to the resource instead of the absolute path, if found
# (it will be missing e.g. when importing data-only modules using base_import_module)
path_info = get_resource_from_path(imd['xml_file'])
if path_info:
data['arch_fs'] = '/'.join(path_info[0:2])
view.write(data)
@api.depends('arch')
def _compute_arch_base(self):
# 'arch_base' is the same as 'arch' without translation
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
view.arch_base = view_wo_lang.arch
def _inverse_arch_base(self):
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
view_wo_lang.arch = view.arch_base
def _compute_model_data_id(self):
# get the last ir_model_data record corresponding to self
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
view = self.browse(data['res_id'])
view.model_data_id = data['id']
def _compute_xml_id(self):
xml_ids = collections.defaultdict(list)
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
for data in self.env['ir.model.data'].search_read(domain, ['module', 'name', 'res_id']):
xml_ids[data['res_id']].append("%s.%s" % (data['module'], data['name']))
for view in self:
view.xml_id = xml_ids.get(view.id, [''])[0]
def _relaxng(self):
if not self._relaxng_validator:
frng = tools.file_open(os.path.join('base', 'rng', 'view.rng'))
try:
relaxng_doc = etree.parse(frng)
self._relaxng_validator = etree.RelaxNG(relaxng_doc)
except Exception:
_logger.exception('Failed to load RelaxNG XML schema for views validation')
finally:
frng.close()
with tools.file_open(os.path.join('base', 'rng', 'view.rng')) as frng:
try:
relaxng_doc = etree.parse(frng)
self._relaxng_validator = etree.RelaxNG(relaxng_doc)
except Exception:
_logger.exception('Failed to load RelaxNG XML schema for views validation')
return self._relaxng_validator
def _valid_inheritance(self, view, arch):
def _valid_inheritance(self, arch):
""" Check whether view inheritance is based on translated attribute. """
for node in arch.xpath('//*[@position]'):
# inheritance may not use a translated attribute as selector
@@ -263,23 +259,22 @@ actual arch.
match = TRANSLATED_ATTRS_RE.search(node.get('expr', ''))
if match:
message = "View inheritance may not use attribute %r as a selector." % match.group(1)
self.raise_view_error(view._cr, view._uid, message, view.id)
self.raise_view_error(message, self.id)
else:
for attr in TRANSLATED_ATTRS:
if node.get(attr):
message = "View inheritance may not use attribute %r as a selector." % attr
self.raise_view_error(view._cr, view._uid, message, view.id)
self.raise_view_error(message, self.id)
return True
@api.constrains('arch', 'arch_base')
def _check_xml(self):
# Sanity checks: the view should not break anything upon rendering!
# Any exception raised below will cause a transaction rollback.
for view in self:
view_arch = etree.fromstring(encode(view.arch))
self._valid_inheritance(view, view_arch)
view_def = self.read_combined(view.id, ['arch'])
view._valid_inheritance(view_arch)
view_def = view.read_combined(['arch'])
view_arch_utf8 = view_def['arch']
if view.type != 'qweb':
view_doc = etree.fromstring(view_arch_utf8)
@@ -308,16 +303,16 @@ actual arch.
" extend an other view"),
]
@api.model_cr_context
def _auto_init(self):
super(View, self)._auto_init()
self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
if not self.env.cr.fetchone():
self.env.cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
def _compute_defaults(self, values):
if 'inherit_id' in values:
values.setdefault(
'mode', 'extension' if values['inherit_id'] else 'primary')
values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
return values
@api.model
@@ -338,7 +333,7 @@ actual arch.
def write(self, vals):
# If view is modified we remove the arch_fs information thus activating the arch_db
# version. An `init` of the view will restore the arch_fs for the --dev mode
if 'arch' in vals and 'install_mode_data' not in self.env.context:
if 'arch' in vals and 'install_mode_data' not in self._context:
vals['arch_fs'] = False
# drop the corresponding view customizations (used for dashboards for example), otherwise
@@ -348,8 +343,7 @@ actual arch.
custom_view.unlink()
self.clear_caches()
res = super(View, self).write(self._compute_defaults(vals))
return res
return super(View, self).write(self._compute_defaults(vals))
@api.multi
def toggle(self):
@@ -369,10 +363,8 @@ actual arch.
:return: id of the default view of False if none found
:rtype: int
"""
rec = self.search([('model', '=', model), ('type', '=', view_type), ('mode', '=', 'primary')], limit=1)
if not rec:
return False
return rec.id
domain = [('model', '=', model), ('type', '=', view_type), ('mode', '=', 'primary')]
return self.search(domain, limit=1).id
#------------------------------------------------------
# Inheritance mecanism
@@ -391,8 +383,7 @@ actual arch.
:rtype: list of tuples
:return: [(view_arch,view_id), ...]
"""
user_groups = frozenset(self.env.user.groups_id or ())
user_groups = self.env.user.groups_id
conditions = [
['inherit_id', '=', view_id],
@@ -400,36 +391,37 @@ actual arch.
['mode', '=', 'extension'],
['active', '=', True],
]
if self.pool._init and not self.env.context.get('load_all_views'):
if self.pool._init and not self._context.get('load_all_views'):
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
['model_ids.module', 'in', tuple(self.pool._init_modules)],
['id', 'in', self.env.context.get('check_view_ids') or (0,)],
('model_ids.module', 'in', tuple(self.pool._init_modules)),
('id', 'in', self._context.get('check_view_ids') or (0,)),
])
view_ids = self.search(conditions)
views = self.search(conditions)
return [(view.arch, view.id)
for view in view_ids
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
for view in views.sudo()
if not view.groups_id or (view.groups_id & user_groups)]
@api.model
def raise_view_error(self, message, view_id):
view = self.browse(view_id)
not_avail = _('n/a')
message = ("%(msg)s\n\n" +
_("Error context:\nView `%(view_name)s`") +
"\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
"model: %(model)s, parent_id: %(parent)s]") % \
{
'view_name': view.name or not_avail,
'viewid': view_id or not_avail,
'xmlid': view.xml_id or not_avail,
'model': view.model or not_avail,
'parent': view.inherit_id.id or not_avail,
'msg': message,
}
message = (
"%(msg)s\n\n" +
_("Error context:\nView `%(view_name)s`") +
"\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
"model: %(model)s, parent_id: %(parent)s]"
) % {
'view_name': view.name or not_avail,
'viewid': view_id or not_avail,
'xmlid': view.xml_id or not_avail,
'model': view.model or not_avail,
'parent': view.inherit_id.id or not_avail,
'msg': message,
}
_logger.info(message)
raise AttributeError(message)
@@ -479,7 +471,6 @@ actual arch.
node.set('data-oe-xpath', xpath)
node.set('data-oe-model', 'ir.ui.view')
node.set('data-oe-field', 'arch')
return specs_tree
@api.model
@@ -576,14 +567,14 @@ actual arch.
sql_inherit = self.get_inheriting_views_arch(source_id, model)
for (specs, view_id) in sql_inherit:
specs_tree = etree.fromstring(specs.encode('utf-8'))
if self.env.context.get('inherit_branding'):
if self._context.get('inherit_branding'):
self.inherit_branding(specs_tree, view_id, root_id)
source = self.apply_inheritance_specs(source, specs_tree, view_id)
source = self.apply_view_inheritance(source, view_id, model, root_id=root_id)
return source
@api.model
def read_combined(self, view_id, fields=None):
@api.multi
def read_combined(self, fields=None):
"""
Utility function to get a view combined with its inherited views.
@@ -593,33 +584,35 @@ actual arch.
.. note:: ``arch`` is always added to the fields list even if not
requested (similar to ``id``)
"""
ctx = self.env.context.copy()
# introduce check_view_ids in context
if 'check_view_ids' not in self._context:
self = self.with_context(check_view_ids=[])
check_view_ids = self._context['check_view_ids']
# if view_id is not a root view, climb back to the top.
base = v = self.browse(view_id)
check_view_ids = ctx.setdefault('check_view_ids', [])
while v.mode != 'primary':
root = self
while root.mode != 'primary':
# Add inherited views to the list of loading forced views
# Otherwise, inherited views could not find elements created in their direct parents if that parent is defined in the same module
check_view_ids.append(v.id)
v = v.inherit_id
root = v
check_view_ids.append(root.id)
root = root.inherit_id
# arch and model fields are always returned
if fields:
fields = list({'arch', 'model'}.union(fields))
# read the view arch
[view] = root.read(fields=fields)
view_arch = etree.fromstring(view['arch'].encode('utf-8'))
if not v.inherit_id:
[view_data] = root.read(fields=fields)
view_arch = etree.fromstring(view_data['arch'].encode('utf-8'))
if not root.inherit_id:
arch_tree = view_arch
else:
parent_view = self.read_combined(v.inherit_id.id, fields=fields)
parent_view = root.inherit_id.read_combined(fields=fields)
arch_tree = etree.fromstring(parent_view['arch'])
arch_tree = self.apply_inheritance_specs(arch_tree, view_arch, parent_view['id'])
if self.env.context.get('inherit_branding'):
if self._context.get('inherit_branding'):
arch_tree.attrib.update({
'data-oe-model': 'ir.ui.view',
'data-oe-id': str(root.id),
@@ -627,9 +620,9 @@ actual arch.
})
# and apply inheritance
arch = self.apply_view_inheritance(arch_tree, root.id, base.model)
arch = self.apply_view_inheritance(arch_tree, root.id, self.model)
return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
return dict(view_data, arch=etree.tostring(arch, encoding='utf-8'))
#------------------------------------------------------
# Postprocessing: translation, groups and modifiers
@@ -654,14 +647,9 @@ actual arch.
children = True
modifiers = {}
Model = self.env[model]
if Model is None:
if model not in self.env:
self.raise_view_error(_('Model not found: %(model)s') % dict(model=model), view_id)
def encode(s):
if isinstance(s, unicode):
return s.encode('utf8')
return s
Model = self.env[model]
def check_group(node):
"""Apply group restrictions, may be set at view level or model level::
@@ -686,26 +674,19 @@ actual arch.
node.set('invisible', '1')
modifiers['invisible'] = True
if 'attrs' in node.attrib:
del(node.attrib['attrs']) # avoid making field visible later
del(node.attrib['groups'])
del node.attrib['attrs'] # avoid making field visible later
del node.attrib['groups']
return True
if node.tag in ('field', 'node', 'arrow'):
if node.get('object'):
attrs = {}
views = {}
xml = "<form>"
for f in node:
if f.tag == 'field':
xml += etree.tostring(f, encoding="utf-8")
xml += "</form>"
new_xml = etree.fromstring(encode(xml))
ctx = self.env.context.copy()
ctx['base_model_name'] = model
xarch, xfields = self.with_context(ctx).postprocess_and_fields(node.get('object'), new_xml, view_id)
xml_form = E.form(*(f for f in node if f.tag == 'field'))
xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(node.get('object'), xml_form, view_id)
views['form'] = {
'arch': xarch,
'fields': xfields
'fields': xfields,
}
attrs = {'views': views}
fields = xfields
@@ -718,18 +699,16 @@ actual arch.
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
ctx = self.env.context.copy()
ctx['base_model_name'] = model
xarch, xfields = self.with_context(ctx).postprocess_and_fields(field.comodel_name, f, view_id)
xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(field.comodel_name, f, view_id)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields
'fields': xfields,
}
attrs = {'views': views}
Relation = self.pool.get(field.comodel_name)
if Relation and field.type in ('many2one', 'many2many'):
node.set('can_create', 'true' if Relation.check_access_rights(self._cr, self._uid, 'create', raise_exception=False) else 'false')
node.set('can_write', 'true' if Relation.check_access_rights(self._cr, self._uid, 'write', raise_exception=False) else 'false')
if field.comodel_name in self.env and field.type in ('many2one', 'many2many'):
Comodel = self.env[field.comodel_name]
node.set('can_create', 'true' if Comodel.check_access_rights('create', raise_exception=False) else 'false')
node.set('can_write', 'true' if Comodel.check_access_rights('write', raise_exception=False) else 'false')
fields[node.get('name')] = attrs
field = model_fields.get(node.get('name'))
@@ -753,7 +732,7 @@ actual arch.
# The view architeture overrides the python model.
# Get the attrs before they are (possibly) deleted by check_group below
orm.transfer_node_to_modifiers(node, modifiers, self.env.context, in_tree_view)
orm.transfer_node_to_modifiers(node, modifiers, self._context, in_tree_view)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter', 'separator')):
@@ -792,27 +771,28 @@ actual arch.
return arch
@api.model
def _disable_workflow_buttons(self, model, node):
""" Set the buttons in node to readonly if the user can't activate them. """
if model is None or self.env.user.id == 1:
if model is None or self.env.user.id == SUPERUSER_ID:
# admin user can always activate workflow buttons
return node
# TODO handle the case of more than one workflow for a model or multiple
# transitions with different groups and same signal
user_group_ids = set(self.env.user.groups_id.ids)
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
for button in buttons:
user_groups = self.env.user.groups_id
self.env.cr.execute("""SELECT DISTINCT t.group_id
FROM wkf
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
INNER JOIN wkf_transition t ON (t.act_to = a.id)
WHERE wkf.osv = %s
AND t.signal = %s
AND t.group_id is NOT NULL
""", (model, button.get('name')))
group_ids = [x[0] for x in self.env.cr.fetchall() if x[0]]
can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
query = """SELECT DISTINCT t.group_id
FROM wkf
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
INNER JOIN wkf_transition t ON (t.act_to = a.id)
WHERE wkf.osv = %s
AND t.signal = %s
AND t.group_id is NOT NULL"""
self._cr.execute(query, (model, button.get('name')))
group_ids = set(row[0] for row in self._cr.fetchall() if row[0])
can_click = not group_ids or bool(user_group_ids & group_ids)
button.set('readonly', str(int(not can_click)))
return node
@@ -829,18 +809,18 @@ actual arch.
"""
fields = {}
Model = self.env[model]
if Model is None:
if model not in self.env:
self.raise_view_error(_('Model not found: %(model)s') % dict(model=model), view_id)
Model = self.env[model]
if node.tag == 'diagram':
if node.getchildren()[0].tag == 'node':
node_model = self.env[node.getchildren()[0].get('object')]
node_fields = node_model.fields_get(None)
fields.update(node_fields)
if not node.get("create") and \
not node_model.check_access_rights('create', raise_exception=False) or \
not self.env.context.get("create", True):
if (not node.get("create") and
not node_model.check_access_rights('create', raise_exception=False) or
not self._context.get("create", True)):
node.set("create", 'false')
if node.getchildren()[1].tag == 'arrow':
arrow_fields = self.env[node.getchildren()[1].get('object')].fields_get(None)
@@ -853,20 +833,20 @@ actual arch.
node = self._disable_workflow_buttons(model, node)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if not node.get(action) and \
not Model.check_access_rights(operation, raise_exception=False) or \
not self.env.context.get(action, True):
if (not node.get(action) and
not Model.check_access_rights(operation, raise_exception=False) or
not self._context.get(action, True)):
node.set(action, 'false')
if node.tag in ('kanban',):
group_by_name = node.get('default_group_by')
if group_by_name in Model._fields:
group_by_field = Model._fields[group_by_name]
if group_by_field.type == 'many2one':
group_by_model = Model.pool[group_by_field.comodel_name]
group_by_model = Model.env[group_by_field.comodel_name]
for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
if not node.get(action) and \
not group_by_model.check_access_rights(operation, raise_exception=False) or \
not self.env.context.get(action, True):
if (not node.get(action) and
not group_by_model.check_access_rights(operation, raise_exception=False) or
not self._context.get(action, True)):
node.set(action, 'false')
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
@@ -877,8 +857,7 @@ actual arch.
if field in fields:
fields[field].update(fields_def[field])
else:
message = _("Field `%(field_name)s` does not exist") % \
dict(field_name=field)
message = _("Field `%(field_name)s` does not exist") % dict(field_name=field)
self.raise_view_error(message, view_id)
return arch, fields
@@ -886,30 +865,40 @@ actual arch.
# QWeb template views
#------------------------------------------------------
def _read_template_keys(self):
""" Return the list of context keys to use for caching ``_read_template``. """
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(not config['dev_mode'],
tools.ormcache_context('uid', 'view_id',
keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations')))
def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
@api.model
@tools.conditional(
not config['dev_mode'],
tools.ormcache('self._uid', 'view_id',
'tuple(map(self._context.get, self._read_template_keys()))'),
)
def _read_template(self, view_id):
arch = self.browse(view_id).read_combined(['arch'])['arch']
arch_tree = etree.fromstring(arch)
self.distribute_branding(arch_tree)
root = etree.Element('templates')
root.append(arch_tree)
root = E.templates(arch_tree)
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
def read_template(self, cr, uid, xml_id, context=None):
if isinstance(xml_id, (int, long)):
view_id = xml_id
else:
if '.' not in xml_id:
raise ValueError('Invalid template id: %r' % (xml_id,))
view_id = self.get_view_id(cr, uid, xml_id, context=context)
return self._read_template(cr, uid, view_id, context=context)
@api.model
def read_template(self, xml_id):
return self._read_template(self.get_view_id(xml_id))
def get_view_id(self, cr, uid, xml_id, context=None):
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
@api.model
def get_view_id(self, template):
""" Return the view ID corresponding to ``template``, which may be a
view ID or an XML ID. Note that this method may be overridden for other
kinds of template values.
"""
if isinstance(template, (int, long)):
return template
if '.' not in template:
raise ValueError('Invalid template id: %r' % template)
return self.env['ir.model.data'].xmlid_to_res_id(template, raise_if_not_found=True)
def clear_cache(self):
""" Deprecated, use `clear_caches` instead. """
@@ -929,12 +918,13 @@ actual arch.
return distributed_branding
def distribute_branding(self, e, branding=None, parent_xpath='',
index_map=misc.ConstantMapping(1)):
index_map=ConstantMapping(1)):
if e.get('t-ignore') or e.tag == 'head':
# remove any view branding possibly injected by inheritance
attrs = set(MOVABLE_BRANDING)
for descendant in e.iterdescendants(tag=etree.Element):
if not attrs.intersection(descendant.attrib): continue
if not attrs.intersection(descendant.attrib):
continue
self._pop_view_branding(descendant)
# TODO: find a better name and check if we have a string to boolean helper
return
@@ -945,7 +935,8 @@ actual arch.
if branding and not (e.get('data-oe-model') or e.get('t-field')):
e.attrib.update(branding)
e.set('data-oe-xpath', node_path)
if not e.get('data-oe-model'): return
if not e.get('data-oe-model'):
return
if {'t-esc', 't-raw'}.intersection(e.attrib):
# nodes which fully generate their content and have no reason to
@@ -986,27 +977,28 @@ actual arch.
for attr in node.attrib
)
def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
@api.multi
def translate_qweb(self, arch, lang):
# Deprecated: templates are translated once read from database
return arch
@odoo.tools.ormcache('self.env.uid', 'self.id')
@api.multi
@tools.ormcache('self._uid', 'self.id')
def get_view_xmlid(self):
xmlid = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', id)])
return '%s.%s' % (xmlid.module, xmlid.name)
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', self.id)]
xmlid = self.env['ir.model.data'].search_read(domain, ['module', 'name'])[0]
return '%s.%s' % (xmlid['module'], xmlid['name'])
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
@api.model
def render_template(self, template, values=None, engine='ir.qweb'):
return self.browse(self.get_view_id(template)).render(values, engine)
if not context:
context = {}
@api.multi
def render(self, values=None, engine='ir.qweb'):
assert isinstance(self.id, (int, long))
if values is None:
values = dict()
qcontext = dict(
env=api.Environment(cr, uid, context),
env=self.env,
keep_query=keep_query,
request=request, # might be unbound if we're not in an httprequest context
debug=request.debug if request else False,
@@ -1016,22 +1008,19 @@ actual arch.
datetime=datetime,
relativedelta=relativedelta,
)
qcontext.update(values)
qcontext.update(values or {})
# TODO: This helper can be used by any template that wants to embedd the backend.
# It is currently necessary because the ir.ui.view bundle inheritance does not
# match the module dependency graph.
def get_modules_order():
if request:
from openerp.addons.web.controllers.main import module_boot
from odoo.addons.web.controllers.main import module_boot
return json.dumps(module_boot())
return '[]'
qcontext['get_modules_order'] = get_modules_order
def loader(name):
return self.read_template(cr, uid, name, context=context)
return self.pool[engine].render(cr, uid, id_or_xml_id, qcontext, loader=loader, context=context)
return self.env[engine].render(self.id, qcontext, loader=self.read_template)
#------------------------------------------------------
# Misc
@@ -1043,7 +1032,12 @@ actual arch.
return self.env['ir.translation'].translate_fields('ir.ui.view', self.id, 'arch_db')
@api.model
def graph_get(self, model, node_obj, conn_obj, src_node, des_node, label, scale):
def graph_get(self, id, model, node_obj, conn_obj, src_node, des_node, label, scale):
def rec_name(rec):
return (rec.name if 'name' in rec else
rec.x_name if 'x_name' in rec else
None)
nodes = []
nodes_name = []
transitions = []
@@ -1053,56 +1047,57 @@ actual arch.
no_ancester = []
blank_nodes = []
_Model_Obj = self.env[model]
_Node_Obj = self.env[node_obj]
Model = self.env[model]
Node = self.env[node_obj]
for model_key, model_value in _Model_Obj._columns.items():
if model_value._type == 'one2many':
if model_value._obj == node_obj:
_Node_Field = model_key
_Model_Field = model_value._fields_id
for node_key, node_value in _Node_Obj._columns.items():
if node_value._type == 'one2many':
if node_value._obj == conn_obj:
# _Source_Field = "Incoming Arrows" (connected via des_node)
if node_value._fields_id == des_node:
_Source_Field = node_key
# _Destination_Field = "Outgoing Arrows" (connected via src_node)
if node_value._fields_id == src_node:
_Destination_Field = node_key
for model_key, model_value in Model._fields.iteritems():
if model_value.type == 'one2many':
if model_value.comodel_name == node_obj:
_Node_Field = model_key
_Model_Field = model_value.inverse_name
for node_key, node_value in Node._fields.iteritems():
if node_value.type == 'one2many':
if node_value.comodel_name == conn_obj:
# _Source_Field = "Incoming Arrows" (connected via des_node)
if node_value.inverse_name == des_node:
_Source_Field = node_key
# _Destination_Field = "Outgoing Arrows" (connected via src_node)
if node_value.inverse_name == src_node:
_Destination_Field = node_key
datas = _Model_Obj.read()
for a in _Node_Obj.read(datas[_Node_Field]):
if a[_Source_Field] or a[_Destination_Field]:
nodes_name.append((a['id'], a['name'] if 'name' in a else a.get('x_name')))
nodes.append(a['id'])
record = Model.browse(id)
for line in record[_Node_Field]:
if line[_Source_Field] or line[_Destination_Field]:
nodes_name.append((line.id, rec_name(line)))
nodes.append(line.id)
else:
blank_nodes.append({'id': a['id'], 'name': a['name'] if 'name' in a else a.get('x_name')})
blank_nodes.append({'id': line.id, 'name': rec_name(line)})
if a.has_key('flow_start') and a['flow_start']:
start.append(a['id'])
else:
if not a[_Source_Field]:
no_ancester.append(a['id'])
for t in self.env[conn_obj].read(a[_Destination_Field]):
transitions.append((a['id'], t[des_node][0]))
tres[str(t['id'])] = (a['id'], t[des_node][0])
if 'flow_start' in line and line.flow_start:
start.append(line.id)
elif not line[_Source_Field]:
no_ancester.append(line.id)
for t in line[_Destination_Field]:
transitions.append((line.id, t[des_node].id))
tres[str(t['id'])] = (line.id, t[des_node].id)
label_string = ""
if label:
for lbl in eval(label):
if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl]) == 'False':
if tools.ustr(lbl) in t and tools.ustr(t[lbl]) == 'False':
label_string += ' '
else:
label_string = label_string + " " + tools.ustr(t[lbl])
labels[str(t['id'])] = (a['id'], label_string)
labels[str(t['id'])] = (line.id, label_string)
g = graph(nodes, transitions, no_ancester)
g.process(start)
g.scale(*scale)
result = g.result_get()
results = {}
for node in nodes_name:
results[str(node[0])] = result[node[0]]
results[str(node[0])]['name'] = node[1]
for node_id, node_name in nodes_name:
results[str(node_id)] = result[node_id]
results[str(node_id)]['name'] = node_name
return {'nodes': results,
'transitions': tres,
'label': labels,
@@ -1114,16 +1109,16 @@ actual arch.
"""Validate architecture of custom views (= without xml id) for a given model.
This method is called at the end of registry update.
"""
self.env.cr.execute("""SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE md.module IS NULL
AND v.model = %s
AND v.active = true
GROUP BY coalesce(v.inherit_id, v.id)
""", (model,))
query = """SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE md.module IS NULL
AND v.model = %s
AND v.active = true
GROUP BY coalesce(v.inherit_id, v.id)"""
self._cr.execute(query, [model])
rec = self.browse(map(itemgetter(0), self.env.cr.fetchall()))
rec = self.browse(map(itemgetter(0), self._cr.fetchall()))
return rec.with_context({'load_all_views': True})._check_xml()
@api.model
@@ -1135,19 +1130,25 @@ actual arch.
if self.pool._init:
# only validate the views that are still existing...
xmlid_filter = "AND md.name IN %s"
names = tuple(name for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items() if xmod == module and model == self._name)
names = tuple(
name
for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items()
if xmod == module and model == self._name
)
if not names:
# no views for this module, nothing to validate
return
params += (names,)
self.env.cr.execute("""SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE md.module = %s
{0}
GROUP BY coalesce(v.inherit_id, v.id)
""".format(xmlid_filter), params)
for vid, in self.env.cr.fetchall():
if not self._check_xml():
query = """SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE md.module = %s {0}
GROUP BY coalesce(v.inherit_id, v.id)""".format(xmlid_filter)
self._cr.execute(query, params)
for vid, in self._cr.fetchall():
try:
self.browse(vid)._check_xml()
except Exception:
self.raise_view_error("Can't validate view", vid)
+59 -65
View File
@@ -11,12 +11,12 @@ EXCLUDED_FIELDS = set((
#: Possible slots to bind an action to with :meth:`~.set_action`
ACTION_SLOTS = [
"client_action_multi", # sidebar wizard action
"client_print_multi", # sidebar report printing button
"client_action_relate", # sidebar related link
"tree_but_open", # double-click on item in tree view
"tree_but_action", # deprecated: same as tree_but_open
]
"client_action_multi", # sidebar wizard action
"client_print_multi", # sidebar report printing button
"client_action_relate", # sidebar related link
"tree_but_open", # double-click on item in tree view
"tree_but_action", # deprecated: same as tree_but_open
]
class IrValues(models.Model):
@@ -68,24 +68,24 @@ class IrValues(models.Model):
users, and set by their UI clients calling :meth:`~.set_default`.
These default values are then automatically used by the
ORM every time a new record is about to be created, i.e. when
:meth:`~odoo.osv.osv.osv.default_get`
or :meth:`~odoo.osv.osv.osv.create` are called.
:meth:`~odoo.models.Model.default_get`
or :meth:`~odoo.models.Model.create` are called.
.. rubric:: Usage: action bindings
Business applications will usually bind their actions during
installation, and Odoo UI clients will apply them as defined,
based on the list of actions included in the result of
:meth:`~odoo.osv.osv.osv.fields_view_get`,
:meth:`~odoo.models.Model.fields_view_get`,
or directly returned by explicit calls to :meth:`~.get_actions`.
"""
_name = 'ir.values'
name = fields.Char(required=True)
model = fields.Char(string='Model Name', select=True, required=True,
model = fields.Char(string='Model Name', index=True, required=True,
help="Model to which this entry applies")
# TODO: model_id and action_id should be read-write function fields
# TODO: model_id and action_id should be read-write function fields
model_id = fields.Many2one('ir.model', string='Model (change only)',
help="Model to which this entry applies - "
"helper field for setting a model, will "
@@ -96,8 +96,8 @@ class IrValues(models.Model):
"automatically set the correct reference")
value = fields.Text(help="Default value (pickled) or reference to an action")
value_unpickle = fields.Text(compute='_value_unpickle', inverse='_value_pickle',
string='Default value or action reference')
value_unpickle = fields.Text(string='Default value or action reference',
compute='_value_unpickle', inverse='_value_pickle')
key = fields.Selection([('action', 'Action'), ('default', 'Default')],
string='Type', index=True, required=True, default='action',
help="- Action: an action attached to one slot of the given model\n"
@@ -117,45 +117,41 @@ class IrValues(models.Model):
company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', index=True,
help="If set, action binding only applies for this company")
@api.depends('value_unpickle')
@api.depends('key', 'value')
def _value_unpickle(self):
for record in self:
value = record.value
if record.key == 'default' and value:
# default values are pickled on the fly
try:
with tools.ignore(Exception):
value = str(pickle.loads(value))
except Exception:
pass
record.value_unpickle = value
def _value_pickle(self):
ctx = self.env.context.copy()
if self.CONCURRENCY_CHECK_FIELD in ctx:
del ctx[self.CONCURRENCY_CHECK_FIELD]
if self.key == 'default':
# default values are pickled on the fly
value = pickle.dumps(self.value)
self.write({value: value})
context = dict(self._context)
context.pop(self.CONCURRENCY_CHECK_FIELD, None)
for record in self.with_context(context):
value = record.value_unpickle
if record.key == 'default':
value = pickle.dumps(value)
record.value = value
@api.onchange('model_id')
def onchange_object_id(self):
if not self.model_id:
return {}
return {'value': {'model': self.env['ir.model'].browse(self.model_id).model}}
if self.model_id:
self.model = self.model_id.model
@api.onchange('action_id')
def onchange_action_id(self):
if not self.action_id:
return {}
act = self.env['ir.actions.actions'].browse(self.action_id)
return {'value': {'value_unpickle': act.type + ',' + str(act.id)}}
if self.action_id:
self.value_unpickle = self.action_id
@api.model_cr_context
def _auto_init(self):
super(IrValues, self)._auto_init()
self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_values_key_model_key2_res_id_user_id_idx\'')
if not self.env.cr.fetchone():
self.env.cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_values_key_model_key2_res_id_user_id_idx'")
if not self._cr.fetchone():
self._cr.execute("CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)")
@api.model
def create(self, vals):
@@ -173,6 +169,7 @@ class IrValues(models.Model):
return super(IrValues, self).unlink()
@api.model
@api.returns('self', lambda value: value.id)
def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False):
"""Defines a default value for the given model and field_name. Any previous
default for the same scope (model, field_name, value, for_all_users, company_id, condition)
@@ -205,7 +202,7 @@ class IrValues(models.Model):
(Currently, the condition is trimmed to 200 characters,
so values that share the same first 200 characters always
match)
:return: id of the newly created ir.values entry
:return: the newly created ir.values entry
"""
if isinstance(value, unicode):
value = value.encode('utf8')
@@ -280,34 +277,33 @@ class IrValues(models.Model):
"""
# use a direct SQL query for performance reasons,
# this is called very often
query = """SELECT v.id, v.name, v.value FROM ir_values v
LEFT JOIN res_users u ON (v.user_id = u.id)
WHERE v.key = %%s AND v.model = %%s
AND (v.user_id = %%s OR v.user_id IS NULL)
AND (v.company_id IS NULL OR
v.company_id =
(SELECT company_id from res_users where id = %%s)
)
%s
ORDER BY v.user_id, u.company_id"""
query = """ SELECT v.id, v.name, v.value FROM ir_values v
LEFT JOIN res_users u ON (v.user_id = u.id)
WHERE v.key = %%s AND v.model = %%s
AND (v.user_id = %%s OR v.user_id IS NULL)
AND (v.company_id IS NULL OR
v.company_id = (SELECT company_id FROM res_users WHERE id = %%s)
)
%s
ORDER BY v.user_id, u.company_id"""
params = ('default', model, self._uid, self._uid)
if condition:
query %= 'AND v.key2 = %s'
query = query % 'AND v.key2 = %s'
params += (condition[:200],)
else:
query %= 'AND v.key2 is NULL'
self.env.cr.execute(query, params)
query = query % 'AND v.key2 IS NULL'
self._cr.execute(query, params)
# keep only the highest priority default for each field
defaults = {}
for row in self.env.cr.dictfetchall():
defaults.setdefault(row['name'],
(row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
for row in self._cr.dictfetchall():
value = pickle.loads(row['value'].encode('utf-8'))
defaults.setdefault(row['name'], (row['id'], row['name'], value))
return defaults.values()
# use ormcache: this is called a lot by BaseModel.default_get()!
@api.model
@tools.ormcache('self.env.uid', 'model', 'condition')
@tools.ormcache('self._uid', 'model', 'condition')
def get_defaults_dict(self, model, condition=False):
""" Returns a dictionary mapping field names with their corresponding
default value. This method simply improves the returned value of
@@ -316,6 +312,7 @@ class IrValues(models.Model):
return dict((f, v) for i, f, v in self.get_defaults(model, condition))
@api.model
@api.returns('self', lambda value: value.id)
def set_action(self, name, action_slot, model, action, res_id=False):
"""Binds an the given action to the given model's action slot - for later
retrieval via :meth:`~.get_actions`. Any existing binding of the same action
@@ -332,7 +329,7 @@ class IrValues(models.Model):
:param string action: action reference, in the form ``'model,id'``
:param int res_id: optional record id - will bind the action only to a
specific record of the model, not all records.
:return: id of the newly created ir.values entry
:return: the newly created ir.values entry
"""
assert isinstance(action, basestring) and ',' in action, \
'Action definition must be an action reference, e.g. "ir.actions.act_window,42"'
@@ -356,10 +353,10 @@ class IrValues(models.Model):
'res_id': res_id,
'name': name,
'value': action,
}).id
})
@api.model
@tools.ormcache_context('self.env.uid', 'action_slot', 'model', 'res_id', keys=('lang',))
@tools.ormcache_context('self._uid', 'action_slot', 'model', 'res_id', keys=('lang',))
def get_actions(self, action_slot, model, res_id=False):
"""Retrieves the list of actions bound to the given model's action slot.
See the class description for more details about the various action
@@ -376,23 +373,20 @@ class IrValues(models.Model):
where ``id`` is the ID of the default entry, ``name`` is the
action label, and ``action_def`` is a dict containing the
action definition as obtained by calling
:meth:`~openerp.osv.osv.osv.read` on the action record.
:meth:`~odoo.models.Model.read` on the action record.
"""
assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot
# use a direct SQL query for performance reasons,
# this is called very often
query = """SELECT v.id, v.name, v.value FROM ir_values v
WHERE v.key = %s AND v.key2 = %s
AND v.model = %s
AND (v.res_id = %s
OR v.res_id IS NULL
OR v.res_id = 0)
ORDER BY v.id"""
self.env.cr.execute(query, ('action', action_slot, model, res_id or None))
query = """ SELECT v.id, v.name, v.value FROM ir_values v
WHERE v.key = %s AND v.key2 = %s AND v.model = %s
AND (v.res_id = %s OR v.res_id IS NULL OR v.res_id = 0)
ORDER BY v.id """
self._cr.execute(query, ('action', action_slot, model, res_id or None))
# map values to their corresponding action record
actions = []
for id, name, value in self.env.cr.fetchall():
for id, name, value in self._cr.fetchall():
if not value:
continue # skip if undefined
action_model, action_id = value.split(',')
@@ -7,7 +7,7 @@
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_ir_module_module" />
<field name="state">code</field>
<field name="code">self._model.button_immediate_install(self._cr, self._uid, self.env.context.get('active_ids', []), context=self.env.context)</field>
<field name="code">self.button_immediate_install(cr, uid, context.get('active_ids', []), context=context)</field>
</record>
<record model="ir.values" id="action_module_immediate_install">
+1
View File
@@ -179,6 +179,7 @@ class Currency(models.Model):
@api.v8
def compute(self, from_amount, to_currency, round=True):
""" Convert `from_amount` from currency `self` to `to_currency`. """
self, to_currency = self or to_currency, to_currency or self
assert self, "compute from unknown currency"
assert to_currency, "compute to unknown currency"
# apply conversion rate
+18
View File
@@ -462,3 +462,21 @@ class TestAPI(common.TransactionCase):
ps.mapped('parent_id.name'),
[p.name for p in parents]
)
@mute_logger('openerp.models')
def test_80_sorted(self):
""" Check sorted on recordsets. """
ps = self.env['res.partner'].search([])
# sort by model order
qs = ps[:len(ps) / 2] + ps[len(ps) / 2:]
self.assertEqual(qs.sorted().ids, ps.ids)
# sort by name, with a function or a field name
by_name_ids = [p.id for p in sorted(ps, key=lambda p: p.name)]
self.assertEqual(ps.sorted(lambda p: p.name).ids, by_name_ids)
self.assertEqual(ps.sorted('name').ids, by_name_ids)
# sort by inverse name, with a field name
by_name_ids.reverse()
self.assertEqual(ps.sorted('name', reverse=True).ids, by_name_ids)
+1 -1
View File
@@ -9,7 +9,7 @@ class TestOrmcache(common.TransactionCase):
XMLID = 'base.group_no_one'
# retrieve the cache, its key and stat counter
cache, key, counter = get_cache_key_counter(IMD.xmlid_lookup, self.cr, self.uid, XMLID)
cache, key, counter = get_cache_key_counter(IMD.xmlid_lookup, XMLID)
hit = counter.hit
miss = counter.miss
+7 -8
View File
@@ -7,20 +7,19 @@ import re
import collections
from lxml import etree
from openerp.addons.base.ir.ir_qweb import QWebContext, FileSystemLoader
import openerp.modules
import openerp.modules
from openerp.addons.base.ir.ir_qweb import QWebContext, FileSystemLoader
from openerp.tests import common
from openerp.addons.base.ir import ir_qweb
class TestQWebTField(common.TransactionCase):
def setUp(self):
super(TestQWebTField, self).setUp()
self.engine = self.registry('ir.qweb')
self.env = self.env(context={'inherit_branding': True})
self.engine = self.env['ir.qweb']
def context(self, values):
return ir_qweb.QWebContext(
self.cr, self.uid, values, context={'inherit_branding': True})
return QWebContext(self.env, values)
def test_trivial(self):
field = etree.Element('span', {'t-field': u'company.name'})
@@ -114,7 +113,7 @@ class TestQWeb(common.TransactionCase):
def run_test_file(self, path):
doc = etree.parse(path).getroot()
loader = FileSystemLoader(path)
context = QWebContext(self.cr, self.uid, {}, loader=loader)
context = QWebContext(self.env, {}, loader=loader)
qweb = self.env['ir.qweb']
for template in loader:
if not template or template.startswith('_'):
@@ -128,7 +127,7 @@ class TestQWeb(common.TransactionCase):
ctx.update(params)
result = doc.find('result[@id="{}"]'.format(template)).text
self.assertEqual(
qweb._model.render(self.cr, self.uid, template, qwebcontext=ctx).strip(),
qweb.render(template, qwebcontext=ctx).strip(),
(result or u'').strip().encode('utf-8'),
template
)
@@ -12,4 +12,4 @@ class TestAssetsBundleController(Controller):
bundle_id = request.env.ref('test_assetsbundle.bundle1')
extra_view_ids = registry['ir.ui.view'].search(cr, uid, [('inherit_id', '=', bundle_id.id)])
context = dict(context, check_view_ids=extra_view_ids)
return request.registry['ir.ui.view'].render(cr, uid, 'test_assetsbundle.template1', context=context)
return request.registry['ir.ui.view'].render_template(cr, uid, 'test_assetsbundle.template1', context=context)
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api
from openerp.tests import HttpCase
from openerp.tests.common import TransactionCase
from openerp.addons.base.ir.ir_qweb import AssetsBundle
from openerp.modules.module import get_resource_path
from mock import patch
from collections import Counter
from os import utime
import time
@@ -29,7 +30,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle creates an ir.attachment record when its `js` method is called
for the first time.
"""
self.bundle = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
self.bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
# there shouldn't be any attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('js')), 0)
@@ -46,7 +47,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that the bundle creates only one
ir.attachment record when rendered multiple times.
"""
bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -55,7 +56,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira0 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('js')[0])
date0 = ira0.create_date
bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle1.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -70,7 +71,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_03_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -78,7 +79,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'js', 'test_jsfile1.js')
utime(path, None) # touch
bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle1.js()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -92,7 +93,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
bundle0 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
bundle0.js()
html0 = bundle0.html
version0 = bundle0.version
@@ -114,7 +115,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
bundle1 = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={'check_view_ids': [newid]}, registry=self.registry)
bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}))
bundle1.js()
html1 = bundle1.html
version1 = bundle1.version
@@ -128,7 +129,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_05_debug(self):
""" Checks that a bundle rendered in debug mode outputs non-minified assets.
"""
debug_bundle = AssetsBundle(self.jsbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry)
debug_bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/js/test_jsfile1.js', content)
@@ -142,21 +143,21 @@ class TestJavascriptAssetsBundle(TransactionCase):
"""
# note: changing the max_css_rules of a bundle does not invalidate its attachments
# self.cssbundle_xlmid contains 3 rules
self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
self.assertEquals(len(self.bundle.get_attachments('css')), 3)
def test_07_paginated_css_generation2(self):
# self.cssbundle_xlmid contains 3 rules
self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=2)
self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=2)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 2)
self.assertEquals(len(self.bundle.get_attachments('css')), 2)
def test_08_paginated_css_generation3(self):
# self.cssbundle_xlmid contains 3 rules
self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=3)
self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=3)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
self.assertEquals(len(self.bundle.get_attachments('css')), 1)
@@ -165,7 +166,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that a bundle creates only enough
ir.attachment records when rendered multiple times.
"""
bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -178,7 +179,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira2 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('css')[2])
date2 = ira2.create_date
bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -199,7 +200,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_10_paginated_css_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -207,7 +208,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'css', 'test_cssfile1.css')
utime(path, None) # touch
bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle1.css()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -222,7 +223,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
bundle0 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
bundle0.css()
html0 = bundle0.html
version0 = bundle0.version
@@ -244,7 +245,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
bundle1 = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={'check_view_ids': [newid]}, registry=self.registry, max_css_rules=1)
bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}), max_css_rules=1)
bundle1.css()
html1 = bundle1.html
version1 = bundle1.version
@@ -258,7 +259,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_12_paginated_css_debug(self):
""" Check that a bundle in debug mode outputs non-minified assets.
"""
debug_bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
debug_bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1.css', content)
@@ -268,7 +269,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_13_paginated_css_order(self):
# self.cssbundle_xlmid contains 3 rules
self.bundle = AssetsBundle(self.cssbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry, max_css_rules=1)
self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
stylesheets = self.bundle.css()
self.assertTrue(stylesheets[0].url.endswith('.0.css'))
@@ -317,17 +318,30 @@ class TestAssetsBundleWithIRAMock(TransactionCase):
def setUp(self):
super(TestAssetsBundleWithIRAMock, self).setUp()
self.lessbundle_xmlid = 'test_assetsbundle.bundle3'
self.patcher1 = patch('openerp.addons.base.ir.ir_attachment.ir_attachment.create', wraps=self.registry['ir.attachment'].create)
self.patcher2 = patch('openerp.addons.base.ir.ir_attachment.ir_attachment.unlink', wraps=self.registry['ir.attachment'].unlink)
self.mock_ira_create = self.patcher1.start()
self.mock_ira_unlink = self.patcher2.start()
self.counter = counter = Counter()
# patch methods 'create' and 'unlink' of model 'ir.attachment'
@api.model
def create(self, vals):
counter.update(['create'])
return create.origin(self, vals)
@api.multi
def unlink(self):
counter.update(['unlink'])
return unlink.origin(self)
self.env['ir.attachment']._patch_method('create', create)
self.addCleanup(self.env['ir.attachment']._revert_method, 'create')
self.env['ir.attachment']._patch_method('unlink', unlink)
self.addCleanup(self.env['ir.attachment']._revert_method, 'unlink')
def _bundle(self, should_create, should_unlink):
self.mock_ira_create.reset_mock()
self.mock_ira_unlink.reset_mock()
AssetsBundle(self.lessbundle_xmlid, cr=self.cr, uid=self.uid, context={}, registry=self.registry).to_html(debug='assets')
self.assertEquals(self.mock_ira_create.call_count, int(should_create))
self.assertEquals(self.mock_ira_unlink.call_count, int(should_unlink))
self.counter.clear()
AssetsBundle(self.lessbundle_xmlid, env=self.env).to_html(debug='assets')
self.assertEquals(self.counter['create'], int(should_create))
self.assertEquals(self.counter['unlink'], int(should_unlink))
def test_01_debug_mode_assets(self):
""" Checks that the ir.attachments records created for compiled less assets in debug mode
@@ -354,8 +368,3 @@ class TestAssetsBundleWithIRAMock(TransactionCase):
# Compile a fourth time, without changes
self._bundle(False, False)
def tearDown(self):
self.patcher2.stop()
self.patcher1.stop()
super(TestAssetsBundleWithIRAMock, self).tearDown()
@@ -7,7 +7,7 @@ from lxml import etree
from openerp.tests import common
from openerp.tools import html_escape as e
from openerp.addons.base.ir import ir_qweb
from openerp.addons.base.ir.ir_qweb import QWebContext
directory = os.path.dirname(__file__)
@@ -113,7 +113,7 @@ class TestCurrencyExport(TestExport):
self.cr, self.uid, 'value', obj, options,
etree.Element('span'),
{'field': 'obj.value', 'field-options': json.dumps(options)},
'', ir_qweb.QWebContext(self.cr, self.uid, {'obj': obj, 'c2': dest, }),
'', QWebContext(self.env, {'obj': obj, 'c2': dest, }),
context=context,
)
return converted
+2 -3
View File
@@ -1429,9 +1429,8 @@ class Response(werkzeug.wrappers.Response):
view_obj = request.registry["ir.ui.view"]
uid = self.uid or request.uid or openerp.SUPERUSER_ID
self.qcontext['request'] = request
return view_obj.render(
request.cr, uid, self.template, self.qcontext,
context=request.context)
return view_obj.render_template(request.cr, uid, self.template,
self.qcontext, context=request.context)
def flatten(self):
""" Forces the rendering of the response's template, sets the result
+6 -5
View File
@@ -32,7 +32,7 @@ import re
import time
from collections import defaultdict, MutableMapping
from inspect import getmembers, currentframe
from operator import itemgetter
from operator import attrgetter, itemgetter
import babel.dates
import dateutil.relativedelta
@@ -5642,16 +5642,17 @@ class BaseModel(object):
""" Return the recordset ``self`` ordered by ``key``.
:param key: either a function of one argument that returns a
comparison key for each record, or ``None``, in which case
records are ordered according the default model's order
comparison key for each record, or a field name, or ``None``, in
which case records are ordered according the default model's order
:param reverse: if ``True``, return the result in reverse order
"""
if key is None:
recs = self.search([('id', 'in', self.ids)])
return self.browse(reversed(recs._ids)) if reverse else recs
else:
return self.browse(map(itemgetter('id'), sorted(self, key=key, reverse=reverse)))
if isinstance(key, basestring):
key = itemgetter(key)
return self.browse(map(attrgetter('id'), sorted(self, key=key, reverse=reverse)))
@api.multi
def update(self, values):