mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 14:31:59 +02:00
vanilla 17.0
This commit is contained in:
parent
d72e748793
commit
a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions
|
|
@ -11,7 +11,9 @@ from . import pivot
|
|||
from . import profiling
|
||||
from . import report
|
||||
from . import session
|
||||
from . import vcard
|
||||
from . import view
|
||||
from . import webclient
|
||||
from . import webmanifest
|
||||
|
||||
from . import main # deprecated
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from odoo import _
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo.http import Controller, request, route
|
||||
from .utils import clean_action
|
||||
|
||||
|
|
@ -21,8 +23,8 @@ class Action(Controller):
|
|||
action = request.env.ref(action_id)
|
||||
assert action._name.startswith('ir.actions.')
|
||||
action_id = action.id
|
||||
except Exception:
|
||||
action_id = 0 # force failed read
|
||||
except Exception as exc:
|
||||
raise MissingError(_("The action %r does not exist.", action_id)) from exc
|
||||
|
||||
base_action = Actions.browse([action_id]).sudo().read(['type'])
|
||||
if base_action:
|
||||
|
|
@ -37,7 +39,9 @@ class Action(Controller):
|
|||
return value
|
||||
|
||||
@route('/web/action/run', type='json', auth="user")
|
||||
def run(self, action_id):
|
||||
def run(self, action_id, context=None):
|
||||
if context:
|
||||
request.update_context(**context)
|
||||
action = request.env['ir.actions.server'].browse([action_id])
|
||||
result = action.run()
|
||||
return clean_action(result, env=action.env) if result else False
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@ except ImportError:
|
|||
|
||||
import odoo
|
||||
import odoo.modules.registry
|
||||
from odoo import http, _
|
||||
from odoo import SUPERUSER_ID, _, http
|
||||
from odoo.addons.base.models.assetsbundle import ANY_UNIQUE
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.http import request, Response
|
||||
from odoo.modules import get_resource_path
|
||||
from odoo.tools import file_open, file_path, replace_exceptions, str2bool
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.tools.image import image_guess_size_from_field_name
|
||||
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -85,33 +84,59 @@ class Binary(http.Controller):
|
|||
|
||||
return stream.get_response(**send_file_kwargs)
|
||||
|
||||
@http.route(['/web/assets/debug/<string:filename>',
|
||||
'/web/assets/debug/<path:extra>/<string:filename>',
|
||||
'/web/assets/<int:id>/<string:filename>',
|
||||
'/web/assets/<int:id>-<string:unique>/<string:filename>',
|
||||
'/web/assets/<int:id>-<string:unique>/<path:extra>/<string:filename>'], type='http', auth="public")
|
||||
# pylint: disable=redefined-builtin,invalid-name
|
||||
def content_assets(self, id=None, filename=None, unique=False, extra=None, nocache=False):
|
||||
if not id:
|
||||
domain = [('url', '!=', False), ('res_model', '=', 'ir.ui.view'),
|
||||
('res_id', '=', 0), ('create_uid', '=', odoo.SUPERUSER_ID)]
|
||||
if extra:
|
||||
domain += [('url', '=like', f'/web/assets/%/{extra}/{filename}')]
|
||||
else:
|
||||
domain += [
|
||||
('url', '=like', f'/web/assets/%/{filename}'),
|
||||
('url', 'not like', f'/web/assets/%/%/{filename}')
|
||||
]
|
||||
attachments = request.env['ir.attachment'].sudo().search_read(domain, fields=['id'], limit=1)
|
||||
if not attachments:
|
||||
raise request.not_found()
|
||||
id = attachments[0]['id']
|
||||
with replace_exceptions(UserError, by=request.not_found()):
|
||||
record = request.env['ir.binary']._find_record(res_id=int(id))
|
||||
stream = request.env['ir.binary']._get_stream_from(record, 'raw', filename)
|
||||
|
||||
send_file_kwargs = {'as_attachment': False, 'content_security_policy': None}
|
||||
if unique:
|
||||
@http.route([
|
||||
'/web/assets/<string:unique>/<string:filename>'], type='http', auth="public")
|
||||
def content_assets(self, filename=None, unique=ANY_UNIQUE, nocache=False, assets_params=None):
|
||||
assets_params = assets_params or {}
|
||||
assert isinstance(assets_params, dict)
|
||||
debug_assets = unique == 'debug'
|
||||
if unique in ('any', '%'):
|
||||
unique = ANY_UNIQUE
|
||||
attachment = None
|
||||
if unique != 'debug':
|
||||
url = request.env['ir.asset']._get_asset_bundle_url(filename, unique, assets_params)
|
||||
assert not '%' in url
|
||||
domain = [
|
||||
('public', '=', True),
|
||||
('url', '!=', False),
|
||||
('url', '=like', url),
|
||||
('res_model', '=', 'ir.ui.view'),
|
||||
('res_id', '=', 0),
|
||||
('create_uid', '=', SUPERUSER_ID),
|
||||
]
|
||||
attachment = request.env['ir.attachment'].sudo().search(domain, limit=1)
|
||||
if not attachment:
|
||||
# try to generate one
|
||||
try:
|
||||
if filename.endswith('.map'):
|
||||
_logger.error(".map should have been generated through debug assets, (version %s most likely outdated)", unique)
|
||||
raise request.not_found()
|
||||
bundle_name, rtl, asset_type = request.env['ir.asset']._parse_bundle_name(filename, debug_assets)
|
||||
css = asset_type == 'css'
|
||||
js = asset_type == 'js'
|
||||
bundle = request.env['ir.qweb']._get_asset_bundle(
|
||||
bundle_name,
|
||||
css=css,
|
||||
js=js,
|
||||
debug_assets=debug_assets,
|
||||
rtl=rtl,
|
||||
assets_params=assets_params,
|
||||
)
|
||||
# check if the version matches. If not, redirect to the last version
|
||||
if not debug_assets and unique != ANY_UNIQUE and unique != bundle.get_version(asset_type):
|
||||
return request.redirect(bundle.get_link(asset_type))
|
||||
if css and bundle.stylesheets:
|
||||
attachment = bundle.css()
|
||||
elif js and bundle.javascripts:
|
||||
attachment = bundle.js()
|
||||
except ValueError as e:
|
||||
_logger.warning("Parsing asset bundle %s has failed: %s", filename, e)
|
||||
raise request.not_found() from e
|
||||
if not attachment:
|
||||
raise request.not_found()
|
||||
stream = request.env['ir.binary']._get_stream_from(attachment, 'raw', filename)
|
||||
send_file_kwargs = {'as_attachment': False}
|
||||
if unique and unique != 'debug':
|
||||
send_file_kwargs['immutable'] = True
|
||||
send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG
|
||||
if nocache:
|
||||
|
|
@ -190,7 +215,7 @@ class Binary(http.Controller):
|
|||
try:
|
||||
attachment = Model.create({
|
||||
'name': filename,
|
||||
'datas': base64.encodebytes(ufile.read()),
|
||||
'raw': ufile.read(),
|
||||
'res_model': model,
|
||||
'res_id': int(id)
|
||||
})
|
||||
|
|
@ -203,7 +228,7 @@ class Binary(http.Controller):
|
|||
else:
|
||||
args.append({
|
||||
'filename': clean(filename),
|
||||
'mimetype': ufile.content_type,
|
||||
'mimetype': attachment.mimetype,
|
||||
'id': attachment.id,
|
||||
'size': attachment.file_size
|
||||
})
|
||||
|
|
@ -217,12 +242,11 @@ class Binary(http.Controller):
|
|||
def company_logo(self, dbname=None, **kw):
|
||||
imgname = 'logo'
|
||||
imgext = '.png'
|
||||
placeholder = functools.partial(get_resource_path, 'web', 'static', 'img')
|
||||
dbname = request.db
|
||||
uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID
|
||||
|
||||
if not dbname:
|
||||
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
|
||||
response = http.Stream.from_path(file_path('web/static/img/logo.png')).get_response()
|
||||
else:
|
||||
try:
|
||||
# create an empty registry
|
||||
|
|
@ -258,9 +282,9 @@ class Binary(http.Controller):
|
|||
response_class=Response,
|
||||
)
|
||||
else:
|
||||
response = http.Stream.from_path(placeholder('nologo.png')).get_response()
|
||||
response = http.Stream.from_path(file_path('web/static/img/nologo.png')).get_response()
|
||||
except Exception:
|
||||
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
|
||||
response = http.Stream.from_path(file_path(f'web/static/img/{imgname}{imgext}')).get_response()
|
||||
|
||||
return response
|
||||
|
||||
|
|
@ -276,7 +300,7 @@ class Binary(http.Controller):
|
|||
"""
|
||||
supported_exts = ('.ttf', '.otf', '.woff', '.woff2')
|
||||
fonts = []
|
||||
fonts_directory = file_path(os.path.join('web', 'static', 'fonts', 'sign'))
|
||||
fonts_directory = file_path('web/static/fonts/sign')
|
||||
if fontname:
|
||||
font_path = os.path.join(fonts_directory, fontname)
|
||||
with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file:
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class Database(http.Controller):
|
|||
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
|
||||
try:
|
||||
if not re.match(DBNAME_PATTERN, name):
|
||||
raise Exception(_('Invalid database name. Only alphanumerical characters, underscore, hyphen and dot are allowed.'))
|
||||
raise Exception(_('Houston, we have a database naming issue! Make sure you only use letters, numbers, underscores, hyphens, or dots in the database name, and you\'ll be golden.'))
|
||||
# country code could be = "False" which is actually True in python
|
||||
country_code = post.get('country_code') or False
|
||||
dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code, post['phone']])
|
||||
|
|
@ -94,7 +94,7 @@ class Database(http.Controller):
|
|||
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
|
||||
try:
|
||||
if not re.match(DBNAME_PATTERN, new_name):
|
||||
raise Exception(_('Invalid database name. Only alphanumerical characters, underscore, hyphen and dot are allowed.'))
|
||||
raise Exception(_('Houston, we have a database naming issue! Make sure you only use letters, numbers, underscores, hyphens, or dots in the database name, and you\'ll be golden.'))
|
||||
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name, neutralize_database])
|
||||
if request.db == name:
|
||||
request.env.cr.close() # duplicating a database leads to an unusable cursor
|
||||
|
|
@ -140,7 +140,7 @@ class Database(http.Controller):
|
|||
error = "Database backup error: %s" % (str(e) or repr(e))
|
||||
return self._render_template(error=error)
|
||||
|
||||
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
|
||||
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False, max_content_length=None)
|
||||
def restore(self, master_pwd, backup_file, name, copy=False, neutralize_database=False):
|
||||
insecure = odoo.tools.config.verify_admin_password('admin')
|
||||
if insecure and master_pwd:
|
||||
|
|
|
|||
|
|
@ -15,29 +15,11 @@ _logger = logging.getLogger(__name__)
|
|||
|
||||
class DataSet(http.Controller):
|
||||
|
||||
@http.route('/web/dataset/search_read', type='json', auth="user")
|
||||
def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None):
|
||||
return request.env[model].web_search_read(domain, fields, offset=offset, limit=limit, order=sort)
|
||||
|
||||
@http.route('/web/dataset/load', type='json', auth="user")
|
||||
def load(self, model, id, fields):
|
||||
warnings.warn("the route /web/dataset/load is deprecated and will be removed in Odoo 17. Use /web/dataset/call_kw with method 'read' and a list containing the id as args instead", DeprecationWarning)
|
||||
value = {}
|
||||
r = request.env[model].browse([id]).read()
|
||||
if r:
|
||||
value = r[0]
|
||||
return {'value': value}
|
||||
|
||||
def _call_kw(self, model, method, args, kwargs):
|
||||
Model = request.env[model]
|
||||
get_public_method(Model, method) # Don't use the result, call_kw will redo the getattr
|
||||
return call_kw(Model, method, args, kwargs)
|
||||
|
||||
@http.route('/web/dataset/call', type='json', auth="user")
|
||||
def call(self, model, method, args, domain_id=None, context_id=None):
|
||||
warnings.warn("the route /web/dataset/call is deprecated and will be removed in Odoo 17. Use /web/dataset/call_kw with empty kwargs instead", DeprecationWarning)
|
||||
return self._call_kw(model, method, args, {})
|
||||
|
||||
@http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
|
||||
def call_kw(self, model, method, args, kwargs, path=None):
|
||||
return self._call_kw(model, method, args, kwargs)
|
||||
|
|
@ -50,7 +32,7 @@ class DataSet(http.Controller):
|
|||
return False
|
||||
|
||||
@http.route('/web/dataset/resequence', type='json', auth="user")
|
||||
def resequence(self, model, ids, field='sequence', offset=0):
|
||||
def resequence(self, model, ids, field='sequence', offset=0, context=None):
|
||||
""" Re-sequences a number of records in the model, by their ids
|
||||
|
||||
The re-sequencing starts at the first model of ``ids``, the sequence
|
||||
|
|
@ -64,6 +46,8 @@ class DataSet(http.Controller):
|
|||
starting the resequencing from an arbitrary number,
|
||||
defaults to ``0``
|
||||
"""
|
||||
if context:
|
||||
request.update_context(**context)
|
||||
m = request.env[model]
|
||||
if not m.fields_get([field]):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ class ExportXlsxWriter:
|
|||
cell_value = pycompat.to_text(cell_value)
|
||||
except UnicodeDecodeError:
|
||||
raise UserError(_("Binary fields can not be exported to Excel unless their content is base64-encoded. That does not seem to be the case for %s.", self.field_names)[column])
|
||||
elif isinstance(cell_value, (list, tuple)):
|
||||
elif isinstance(cell_value, (list, tuple, dict)):
|
||||
cell_value = pycompat.to_text(cell_value)
|
||||
|
||||
if isinstance(cell_value, str):
|
||||
|
|
@ -335,10 +335,7 @@ class Export(http.Controller):
|
|||
if field.get('type') in ('properties', 'properties_definition'):
|
||||
continue
|
||||
if field.get('readonly'):
|
||||
# If none of the field's states unsets readonly, skip the field
|
||||
if all(dict(attrs).get('readonly', True)
|
||||
for attrs in field.get('states', {}).values()):
|
||||
continue
|
||||
continue
|
||||
if not field.get('exportable', True):
|
||||
continue
|
||||
|
||||
|
|
@ -488,7 +485,7 @@ class ExportFormat(object):
|
|||
if not import_compat and groupby:
|
||||
groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby]
|
||||
domain = [('id', 'in', ids)] if ids else domain
|
||||
groups_data = Model.with_context(active_test=False).read_group(domain, [x if x != '.id' else 'id' for x in field_names], groupby, lazy=False)
|
||||
groups_data = Model.with_context(active_test=False).read_group(domain, ['__count'], groupby, lazy=False)
|
||||
|
||||
# read_group(lazy=False) returns a dict only for final groups (with actual data),
|
||||
# not for intermediary groups. The full group tree must be re-constructed.
|
||||
|
|
@ -523,7 +520,7 @@ class ExportFormat(object):
|
|||
class CSVExport(ExportFormat, http.Controller):
|
||||
|
||||
@http.route('/web/export/csv', type='http', auth="user")
|
||||
def index(self, data):
|
||||
def web_export_csv(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
|
|
@ -567,7 +564,7 @@ class CSVExport(ExportFormat, http.Controller):
|
|||
class ExcelExport(ExportFormat, http.Controller):
|
||||
|
||||
@http.route('/web/export/xlsx', type='http', auth="user")
|
||||
def index(self, data):
|
||||
def web_export_xlsx(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Home(http.Controller):
|
|||
# Ensure we have both a database and a user
|
||||
ensure_db()
|
||||
if not request.session.uid:
|
||||
return request.redirect('/web/login', 303)
|
||||
return request.redirect_query('/web/login', query=request.params, code=303)
|
||||
if kw.get('redirect'):
|
||||
return request.redirect(kw.get('redirect'), 303)
|
||||
if not security.check_session(request.session, request.env):
|
||||
|
|
@ -63,12 +63,16 @@ class Home(http.Controller):
|
|||
return request.redirect('/web/login?error=access')
|
||||
|
||||
@http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET'])
|
||||
def web_load_menus(self, unique):
|
||||
def web_load_menus(self, unique, lang=None):
|
||||
"""
|
||||
Loads the menus for the webclient
|
||||
:param unique: this parameters is not used, but mandatory: it is used by the HTTP stack to make a unique request
|
||||
:param lang: language in which the menus should be loaded (only works if language is installed)
|
||||
:return: the menus (including the images in Base64)
|
||||
"""
|
||||
if lang:
|
||||
request.update_context(lang=lang)
|
||||
|
||||
menus = request.env["ir.ui.menu"].load_web_menus(request.session.debug)
|
||||
body = json.dumps(menus, default=ustr)
|
||||
response = request.make_response(body, [
|
||||
|
|
@ -143,7 +147,7 @@ class Home(http.Controller):
|
|||
if request.env.user._is_system():
|
||||
uid = request.session.uid = odoo.SUPERUSER_ID
|
||||
# invalidate session token cache as we've changed the uid
|
||||
request.env['res.users'].clear_caches()
|
||||
request.env.registry.clear_cache()
|
||||
request.session.session_token = security.compute_session_token(request.session, request.env)
|
||||
|
||||
return request.redirect(self._login_redirect(uid))
|
||||
|
|
@ -165,11 +169,16 @@ class Home(http.Controller):
|
|||
('Cache-Control', 'no-store')]
|
||||
return request.make_response(data, headers, status=status)
|
||||
|
||||
@http.route(['/robots.txt'], type='http', auth="none")
|
||||
def robots(self, **kwargs):
|
||||
allowed_routes = self._get_allowed_robots_routes()
|
||||
robots_content = ["User-agent: *", "Disallow: /"]
|
||||
robots_content.extend(f"Allow: {route}" for route in allowed_routes)
|
||||
|
||||
return request.make_response("\n".join(robots_content), [('Content-Type', 'text/plain')])
|
||||
|
||||
def _get_allowed_robots_routes(self):
|
||||
"""Override this method to return a list of allowed routes.
|
||||
By default this controller does not serve robots.txt so all routes
|
||||
are implicitly open but we want any module to be able to append
|
||||
to this list, in case the website module is installed.
|
||||
|
||||
:return: A list of URL paths that should be allowed by robots.txt
|
||||
Examples: ['/social_instagram/', '/sitemap.xml', '/web/']
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class ReportController(http.Controller):
|
|||
else:
|
||||
return
|
||||
except Exception as e:
|
||||
_logger.exception("Error while generating report %s", reportname)
|
||||
_logger.warning("Error while generating report %s", reportname, exc_info=True)
|
||||
se = http.serialize_exception(e)
|
||||
error = {
|
||||
'code': 200,
|
||||
|
|
|
|||
48
odoo-bringout-oca-ocb-web/web/controllers/vcard.py
Normal file
48
odoo-bringout-oca-ocb-web/web/controllers/vcard.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
import odoo.http as http
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request, content_disposition
|
||||
|
||||
|
||||
class Partner(http.Controller):
|
||||
|
||||
@http.route(['/web_enterprise/partner/<model("res.partner"):partner>/vcard',
|
||||
'/web/partner/vcard'], type='http', auth="user")
|
||||
def download_vcard(self, partner_ids=None, partner=None, **kwargs):
|
||||
if importlib.util.find_spec('vobject') is None:
|
||||
raise UserError('vobject library is not installed')
|
||||
|
||||
if partner_ids:
|
||||
partner_ids = list(filter(None, (int(pid) for pid in partner_ids.split(',') if pid.isdigit())))
|
||||
partners = request.env['res.partner'].browse(partner_ids)
|
||||
if len(partners) > 1:
|
||||
with io.BytesIO() as buffer:
|
||||
with zipfile.ZipFile(buffer, 'w') as zipf:
|
||||
for partner in partners:
|
||||
filename = f"{partner.name or partner.email}.vcf"
|
||||
content = partner._get_vcard_file()
|
||||
zipf.writestr(filename, content)
|
||||
|
||||
return request.make_response(buffer.getvalue(), [
|
||||
('Content-Type', 'application/zip'),
|
||||
('Content-Length', len(content)),
|
||||
('Content-Disposition', content_disposition('Contacts.zip'))
|
||||
])
|
||||
|
||||
if partner or partners:
|
||||
partner = partner or partners
|
||||
content = partner._get_vcard_file()
|
||||
return request.make_response(content, [
|
||||
('Content-Type', 'text/vcard'),
|
||||
('Content-Length', len(content)),
|
||||
('Content-Disposition', content_disposition(f"{partner.name or partner.email}.vcf")),
|
||||
])
|
||||
|
||||
return request.not_found()
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import Controller, route, request
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class View(Controller):
|
||||
|
|
@ -14,6 +16,8 @@ class View(Controller):
|
|||
:param str arch: the edited arch of the custom view
|
||||
:returns: dict with acknowledged operation (result set to True)
|
||||
"""
|
||||
custom_view = request.env['ir.ui.view.custom'].browse(custom_id)
|
||||
custom_view = request.env['ir.ui.view.custom'].sudo().browse(custom_id)
|
||||
if not custom_view.user_id == request.env.user:
|
||||
raise AccessError(_("Custom view %s does not belong to user %s", custom_id, self.env.user.login))
|
||||
custom_view.write({'arch': arch})
|
||||
return {'result': True}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import werkzeug.wsgi
|
|||
import odoo
|
||||
import odoo.modules.registry
|
||||
from odoo import http
|
||||
from odoo.modules import get_manifest, get_resource_path
|
||||
from odoo.modules import get_manifest
|
||||
from odoo.http import request
|
||||
from odoo.tools import lazy
|
||||
from odoo.tools.misc import file_open
|
||||
from odoo.tools.misc import file_open, file_path
|
||||
from .utils import _local_web_translations
|
||||
|
||||
|
||||
|
|
@ -29,32 +29,11 @@ def CONTENT_MAXAGE():
|
|||
return http.STATIC_CACHE_LONG
|
||||
|
||||
|
||||
MOMENTJS_LANG_CODES_MAP = {
|
||||
"sr_RS": "sr_cyrl",
|
||||
"sr@latin": "sr"
|
||||
}
|
||||
|
||||
|
||||
class WebClient(http.Controller):
|
||||
|
||||
# FIXME: to be removed in master, deprecated since momentjs removal in commit 4327c062d820
|
||||
@http.route('/web/webclient/locale/<string:lang>', type='http', auth="none")
|
||||
def load_locale(self, lang):
|
||||
lang = MOMENTJS_LANG_CODES_MAP.get(lang, lang)
|
||||
magic_file_finding = [lang.replace("_", '-').lower(), lang.split('_')[0]]
|
||||
for code in magic_file_finding:
|
||||
try:
|
||||
return http.Response(
|
||||
werkzeug.wsgi.wrap_file(
|
||||
request.httprequest.environ,
|
||||
file_open(f'web/static/lib/moment/locale/{code}.js', 'rb')
|
||||
),
|
||||
content_type='application/javascript; charset=utf-8',
|
||||
headers=[('Cache-Control', f'max-age={http.STATIC_CACHE}')],
|
||||
direct_passthrough=True,
|
||||
)
|
||||
except IOError:
|
||||
_logger.debug("No moment locale for code %s", code)
|
||||
|
||||
return request.make_response("", headers=[
|
||||
('Content-Type', 'application/javascript'),
|
||||
('Cache-Control', f'max-age={http.STATIC_CACHE}'),
|
||||
|
|
@ -80,7 +59,7 @@ class WebClient(http.Controller):
|
|||
for addon_name in mods:
|
||||
manifest = get_manifest(addon_name)
|
||||
if manifest and manifest['bootstrap']:
|
||||
f_name = get_resource_path(addon_name, 'i18n', f'{lang}.po')
|
||||
f_name = file_path(f'{addon_name}/i18n/{lang}.po')
|
||||
if not f_name:
|
||||
continue
|
||||
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
|
||||
|
|
@ -103,10 +82,13 @@ class WebClient(http.Controller):
|
|||
elif mods is None:
|
||||
mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or [])
|
||||
|
||||
if lang and lang not in {code for code, _ in request.env['res.lang'].sudo().get_installed()}:
|
||||
lang = None
|
||||
|
||||
translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang)
|
||||
|
||||
body = json.dumps({
|
||||
'lang': lang_params and lang_params["code"],
|
||||
'lang': lang,
|
||||
'lang_parameters': lang_params,
|
||||
'modules': translations_per_module,
|
||||
'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1,
|
||||
|
|
@ -131,10 +113,6 @@ class WebClient(http.Controller):
|
|||
def test_mobile_suite(self, mod=None, **kwargs):
|
||||
return request.render('web.qunit_mobile_suite')
|
||||
|
||||
@http.route('/web/benchmarks', type='http', auth="none")
|
||||
def benchmarks(self, mod=None, **kwargs):
|
||||
return request.render('web.benchmark_suite')
|
||||
|
||||
@http.route('/web/bundle/<string:bundle_name>', auth="public", methods=["GET"])
|
||||
def bundle(self, bundle_name, **bundle_params):
|
||||
"""
|
||||
|
|
@ -148,7 +126,6 @@ class WebClient(http.Controller):
|
|||
data = [{
|
||||
"type": tag,
|
||||
"src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'),
|
||||
"content": content,
|
||||
} for tag, attrs, content in files]
|
||||
} for tag, attrs in files]
|
||||
|
||||
return request.make_json_response(data)
|
||||
|
|
|
|||
97
odoo-bringout-oca-ocb-web/web/controllers/webmanifest.py
Normal file
97
odoo-bringout-oca-ocb-web/web/controllers/webmanifest.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.tools import ustr, file_open
|
||||
|
||||
|
||||
class WebManifest(http.Controller):
|
||||
|
||||
def _get_shortcuts(self):
|
||||
module_names = ['mail', 'crm', 'project', 'project_todo']
|
||||
try:
|
||||
module_ids = request.env['ir.module.module'].search([('state', '=', 'installed'), ('name', 'in', module_names)]) \
|
||||
.sorted(key=lambda r: module_names.index(r["name"]))
|
||||
except AccessError:
|
||||
return []
|
||||
menu_roots = request.env['ir.ui.menu'].get_user_roots()
|
||||
datas = request.env['ir.model.data'].sudo().search([('model', '=', 'ir.ui.menu'),
|
||||
('res_id', 'in', menu_roots.ids),
|
||||
('module', 'in', module_names)])
|
||||
shortcuts = []
|
||||
for module in module_ids:
|
||||
data = datas.filtered(lambda res: res.module == module.name)
|
||||
if data:
|
||||
shortcuts.append({
|
||||
'name': module.display_name,
|
||||
'url': '/web#menu_id=%s' % data.mapped('res_id')[0],
|
||||
'description': module.summary,
|
||||
'icons': [{
|
||||
'sizes': '100x100',
|
||||
'src': module.icon,
|
||||
'type': mimetypes.guess_type(module.icon)[0] or 'image/png'
|
||||
}]
|
||||
})
|
||||
return shortcuts
|
||||
|
||||
@http.route('/web/manifest.webmanifest', type='http', auth='public', methods=['GET'])
|
||||
def webmanifest(self):
|
||||
""" Returns a WebManifest describing the metadata associated with a web application.
|
||||
Using this metadata, user agents can provide developers with means to create user
|
||||
experiences that are more comparable to that of a native application.
|
||||
"""
|
||||
web_app_name = request.env['ir.config_parameter'].sudo().get_param('web.web_app_name', 'Odoo')
|
||||
manifest = {
|
||||
'name': web_app_name,
|
||||
'scope': '/web',
|
||||
'start_url': '/web',
|
||||
'display': 'standalone',
|
||||
'background_color': '#714B67',
|
||||
'theme_color': '#714B67',
|
||||
'prefer_related_applications': False,
|
||||
}
|
||||
icon_sizes = ['192x192', '512x512']
|
||||
manifest['icons'] = [{
|
||||
'src': '/web/static/img/odoo-icon-%s.png' % size,
|
||||
'sizes': size,
|
||||
'type': 'image/png',
|
||||
} for size in icon_sizes]
|
||||
manifest['shortcuts'] = self._get_shortcuts()
|
||||
body = json.dumps(manifest, default=ustr)
|
||||
response = request.make_response(body, [
|
||||
('Content-Type', 'application/manifest+json'),
|
||||
])
|
||||
return response
|
||||
|
||||
@http.route('/web/service-worker.js', type='http', auth='public', methods=['GET'])
|
||||
def service_worker(self):
|
||||
response = request.make_response(
|
||||
self._get_service_worker_content(),
|
||||
[
|
||||
('Content-Type', 'text/javascript'),
|
||||
('Service-Worker-Allowed', '/web'),
|
||||
]
|
||||
)
|
||||
return response
|
||||
|
||||
def _get_service_worker_content(self):
|
||||
""" Returns a ServiceWorker javascript file scoped for the backend (aka. '/web')
|
||||
"""
|
||||
with file_open('web/static/src/service_worker.js') as f:
|
||||
body = f.read()
|
||||
return body
|
||||
|
||||
def _icon_path(self):
|
||||
return 'web/static/img/odoo-icon-192x192.png'
|
||||
|
||||
@http.route('/web/offline', type='http', auth='public', methods=['GET'])
|
||||
def offline(self):
|
||||
""" Returns the offline page delivered by the service worker """
|
||||
return request.render('web.webclient_offline', {
|
||||
'odoo_icon': base64.b64encode(file_open(self._icon_path(), 'rb').read())
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue