[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:
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
+384
-435
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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+','),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user