openupgradelib/openupgrade_120.py
# -*- coding: utf-8 -*- # pylint: disable=C8202
# Copyright 2019 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
"""Tools specific for migrating Odoo 11.0 to 12.0.
Important changes come from the update from Bootstrap 3 to 4.
To understand these changes, these sources have been consulted, among others
specific to some rules that can be consulted in inline comments:
* http://upgrade-bootstrap.bootply.com/
* https://bit.ly/2xmUHmo
* https://getbootstrap.com/docs/4.3/migration/#stable-changes
* https://github.com/odoo/odoo/wiki/Tips-and-tricks:-BS3-to-BS4
Don't expect perfection. But patches are welcome.
"""
import logging
from itertools import product
from psycopg2.extensions import AsIs
from .openupgrade import update_field_multilang
from .openupgrade_tools import (
convert_html_fragment,
convert_html_replacement_class_shortcut as _r,
)
logger = logging.getLogger("OpenUpgrade")
logger.setLevel(logging.DEBUG)
_COLS = range(1, 13)
_CONTEXTS = (
# (BS3 context, BS4 context)
("default", "secondary"),
("primary", "primary"),
("active", "active"),
("disabled", "disabled"),
("success", "success"),
("info", "info"),
("warning", "warning"),
("danger", "danger"),
)
_TIERS = (
# (BS3 tier, BS4 tier)
("-lg", "-xl"),
("-md", "-lg"),
("-sm", "-md"),
("-xs", ""),
)
_BS3_VISIBLES = ("block", "inline", "inline-block")
# These replacements are from standard Bootstrap 3 to 4
_BS4_REPLACEMENTS = (
# Convert columns and modifiers among tiers
*(
_r("col%s-%s" % (t3, col), "col%s-%s" % (t4, col))
for (t3, t4), col in product(_TIERS, _COLS)
),
*(
_r("col%s-offset-%s" % (t3, col), "offset%s-%s" % (t4, col))
for (t3, t4), col in product(_TIERS, _COLS)
),
*(
_r("col%s-pull-%s" % (t3, col), "order-first")
for (t3, t4), col in product(_TIERS, _COLS)
),
*(
_r("col%s-push-%s" % (t3, col), "order-last")
for (t3, t4), col in product(_TIERS, _COLS)
),
# Typography
_r(selector="blockquote", class_add="blockquote"),
_r(selector="blockquote > small", class_add="blockquote-footer"),
_r("blockquote-reverse", "blockquote text-right"),
_r(selector=".list-inline > li", class_add="list-linline-item"),
# .page-header dropped. See https://stackoverflow.com/a/49708022/1468388
_r("page-header", "pb-2 mt-4 mb-2 border-bottom"),
# <dl> & co. See https://stackoverflow.com/a/56020841/1468388
_r("dl-horizontal", "row"),
_r(selector=".dl-horizontal > dt", class_add="col-sm-3 text-sm-right"),
_r(selector=".dl-horizontal > dd", class_add="col-sm-9"),
# Images
_r("img-circle", "rounded-circle"),
_r("img-responsive", "img-fluid d-block"),
_r("img-rounded", "rounded"),
# Tables
_r("table-condensed", "table-sm"),
*(
_r("%s" % c3, "table-%s" % c4, selector=".table .%s" % c3)
for (c3, c4) in _CONTEXTS
),
# Forms
_r("control-label", "col-form-label"),
_r("form-group-lg", "form-control-lg"),
_r("form-group-sm", "form-control-sm"),
_r("input-lg", "form-control-lg"),
_r("input-sm", "form-control-sm"),
_r("help-block", "form-text"),
_r(
selector="div.checkbox, div.radio",
class_rm="checkbox radio",
class_add="form-check",
),
_r(selector="div.form-check label", class_add="form-check-label"),
_r(selector="div.form-check input", class_add="form-check-input"),
_r(
selector="label.checkbox-inline",
class_rm="checkbox-inline",
class_add="form-check-label form-check-inline",
),
_r(
selector=".checkbox-inline input",
class_rm="radio checkbox",
class_add="form-check-input",
),
_r(selector=".form-horizontal .form-group", class_add="row"),
_r(
selector=".form-horizontal .form-group .control-label",
class_rm="control-label",
class_add="col-form-label text-right",
),
_r(selector=".form-horizontal", class_rm="form-horizontal"),
_r("form-control-static", "form-control-plaintext"),
# Input groups
_r(
selector=".form-control + .input-group-addon",
class_rm="input-group-addon",
class_add="input-group-text",
wrap="<span class='input-group-append'/>",
),
_r(
selector=".form-control + .input-group-btn",
class_rm="input-group-btn",
class_add="input-group-append",
),
_r(
"input-group-addon",
"input-group-text",
wrap="<span class='input-group-prepend'/>",
),
_r("input-group-btn", "input-group-prepend"),
# Buttons
_r("btn-default", "btn-secondary"),
_r("btn-xs", "btn-sm"),
_r("btn-group-xs", "btn-group-sm"),
_r(
"btn-group-justified",
"w-100",
wrap='<div class="btn-group d-flex" role="group"/>',
),
_r(selector=".btn-group + .btn-group", class_add="ml-1"),
# Dropdowns
_r("divider", "dropdown-divider", selector=".dropdown-menu > .divider"),
_r(selector=".dropdown-menu > li > a", class_add="dropdown-item"),
# List groups
_r("list-group-item", "list-group-item-action", selector="a.list-group-item"),
# Navs
_r(selector=".nav > li", class_add="nav-item"),
_r(selector=".nav > li > a", class_add="nav-link"),
_r("nav-stacked", "flex-column"),
# Navbar
_r(selector="navbar", class_add="navbar-expand-sm"),
_r("navbar-default", "navbar-light"),
_r("navbar-toggle", "navbar-toggler"),
_r("navbar-form", "form-inline"),
_r("navbar-fixed-top", "fixed-top"),
_r("navbar-btn", "nav-item"),
_r("navbar-right", "ml-auto"),
# Pagination
_r(selector=".pagination > li", class_add="page-item"),
_r(selector=".pagination > li > a", class_add="page-link"),
# Breadcrumbs
_r(selector=".breadcrumb > li", class_add="breadcrumb-item"),
# Labels and badges
_r("label", "badge"),
_r("badge-default", "badge-secondary"),
*(_r("label-%s" % c3, "badge-%s" % c4) for (c3, c4) in _CONTEXTS),
# Convert panels, thumbnails and wells to cards
_r("panel", "card"),
_r("panel-body", "card-body"),
_r("panel-default"),
_r("panel-group"),
_r("panel-footer", "card-footer"),
_r("panel-heading", "card-header"),
_r("panel-title", "card-title"),
*(_r("panel-%s" % c3, "bg-%s" % c4) for (c3, c4) in _CONTEXTS),
_r("well", "card card-body"),
_r("thumbnail", "card card-body"),
# Progress
*(_r("progress-bar-%s" % c3, "bg-%s" % c4) for (c3, c4) in _CONTEXTS),
_r("active", "progress-bar-animated", selector=".progress-bar.active"),
# Carousel
_r("carousel-control left", "carousel-control-prev"),
_r("carousel-control right", "carousel-control-next"),
_r(selector=".item>.img", class_add="d-block"),
_r("item", "carousel-item", selector=".carousel .item"),
_r("left", "carousel-item-left", selector=".carousel .left"),
_r("next", "carousel-item-next", selector=".carousel .next"),
_r("prev", "carousel-item-prev", selector=".carousel .prev"),
_r("right", "carousel-item-right", selector=".carousel .right"),
# Utilities
_r("center-block", "d-block mx-auto"),
_r("hidden", "d-none"),
_r("hidden-xs", "d-none d-md-block"),
_r(
selector=".hidden-sm",
class_rm="hidden-sm d-md-block",
class_add="d-md-none d-lg-block",
),
_r(
selector=".hidden-md",
class_rm="hidden-md d-lg-block",
class_add="d-lg-none d-xl-block",
),
_r(selector=".hidden-lg", class_rm="hidden-lg d-xl-block", class_add="d-xl-none"),
_r("hidden-lg", "d-xl-none"),
*(_r("hidden%s" % t3, "d-none%s" % t4) for (t3, t4) in _TIERS),
_r("hidden-print", "d-print-none"),
*(
_r("visible%s-%s" % (t3, vis), "d-")
for (t3, t4), vis in product(_TIERS, _BS3_VISIBLES)
),
*(_r("visible-print-%s" % vis, "d-print-%s" % vis) for vis in _BS3_VISIBLES),
_r("pull-left", "float-left"),
_r("pull-right", "float-right"),
)
# These replacements are specific for Odoo v11 to v12
_ODOO12_REPLACEMENTS = (
# Grays renamed; handpicked closest gray equivalent matches
*(
_r("bg-gray%s" % v11, "bg-%d00" % v12)
for v11, v12 in (
("-darker", 9),
("-dark", 8),
("", 7),
("-light", 6),
("-lighter", 1),
)
),
# Odoo v12 editor adds/removes <b> tags, not <strong> tags; keep UX
_r(selector="strong", tag="b"),
# 25% opacity black background had white text in v11, but black in v12
_r(selector=".bg-black-25", class_add="text-white"),
# Image floating snippet disappears
_r(selector=".o_image_floating.o_margin_s.float-left", class_add="mr8"),
_r(selector=".o_image_floating.o_margin_s.float-right", class_add="ml8"),
_r(
selector=".o_image_floating.o_margin_m.float-left",
style_add={"margin-right": "12px"},
),
_r(
selector=".o_image_floating.o_margin_m.float-right",
style_add={"margin-left": "12px"},
),
_r(selector=".o_image_floating.o_margin_l.float-left", class_add="mr16"),
_r(selector=".o_image_floating.o_margin_l.float-right", class_add="ml16"),
_r(selector=".o_image_floating.o_margin_xl.float-left", class_add="mr32"),
_r(selector=".o_image_floating.o_margin_xl.float-right", class_add="ml32"),
_r(selector=".o_image_floating.o_margin_s", class_add="mb4"),
_r(selector=".o_image_floating.o_margin_m", class_add="mb8"),
_r(selector=".o_image_floating.o_margin_l", style_add={"margin-bottom": "12px"}),
_r(selector=".o_image_floating.o_margin_xl", class_add="mb24"),
_r(selector=".o_image_floating .o_footer", class_rm="o_footer"),
_r(class_rm="s_image_floating"),
_r(
"o_image_floating o_margin_s o_margin_m o_margin_l o_margin_xl",
"col-5 p-0",
selector=".o_image_floating",
),
# Big message (v11) or Banner (v12) snippet
_r(selector=".jumbotron h1, .jumbotron .h1", style_add={"font-size": "63px"}),
_r(selector=".jumbotron p", class_add="lead"),
# Big picture snippet
_r(selector=".s_big_picture h2", class_add="mt24"),
# Slider (v11) or Carousel (v12) snippet
_r(
selector=".carousel",
class_rm="s_banner oe_custom_bg",
class_add="s_carousel s_carousel_default",
style_rm={"height"},
style_add=lambda styles, **kw: {"min-height": styles.get("height", "400px")},
),
_r(
selector=".carousel-control-prev .fa-chevron-left",
class_rm="fa fa-chevron-left",
class_add="carousel-control-prev-icon",
tag="span",
),
_r(
selector=".carousel-control-next .fa-chevron-right",
class_rm="fa fa-chevron-right",
class_add="carousel-control-next-icon",
tag="span",
),
# Text snippet loses its built-in headers
_r(selector=".s_text_block h2", class_add="mt24"),
# Cover snippet
_r(
selector=".s_text_block_image_fw .container > .row > div",
style_add={"padding": "30px"},
),
# Image gallery snippet
_r(
selector=".s_image_gallery .o_indicators_left",
class_rm="fa fa-chevron-left",
class_add="text-center pt-2",
style_rm={"overflow", "padding", "border"},
),
_r(
selector=".s_image_gallery .o_indicators_left > br",
wrap='<i class="fa fa-chevron-left"/>',
),
_r(
selector=".s_image_gallery .o_indicators_right",
class_rm="fa fa-chevron-right",
class_add="text-center pt-2",
style_rm={"overflow", "padding", "border"},
),
_r(
selector=".s_image_gallery .o_indicators_right > br",
class_add="fa fa-chevron-right",
tag="i",
),
# Comparisons snippet
_r(
selector=".s_comparisons > .container > .row > div",
class_add="s_col_no_bgcolor",
attr_add={"data-name": "Box"},
),
_r(selector=".s_comparisons .card .list-group", class_add="list-group-flush"),
# Company team snippet
_r(selector=".s_company_team h1", class_add="mt24"),
# Call to action snippet
_r(selector=".s_button .lead:first-child", class_rm="lead", tag="h3"),
# Parallax sliders
_r(selector=".s_parallax", style_add={"min-height": "200px"}),
_r(
selector=".s_parallax_slider .blockquote",
style_add={"border-left": "5px solid #eeeeee"},
),
# Accordion snippet
_r(selector=".s_faq_collapse .card-header h4", class_add="mb0"),
_r(selector=".s_faq_collapse .panel", class_add="mt6"),
# Well snippet
_r(selector=".s_well.card", class_add="bg-100"),
# Panel snippet
_r("s_panel", "s_card"),
_r(selector=".s_card.bg-secondary", class_rm="bg-secondary", class_add="bg-white"),
)
ALL_REPLACEMENTS = _BS4_REPLACEMENTS + _ODOO12_REPLACEMENTS
def convert_string_bootstrap_3to4(html_string, pretty_print=True):
"""Convert an HTML string from Bootstrap 3 to 4.
:param str html_string:
Raw HTML fragment to convert.
:param bool pretty_print:
Indicate if you wish to return the HTML pretty formatted.
:return str:
Raw HTML fragment converted.
"""
if not html_string:
return html_string
try:
return convert_html_fragment(
html_string,
ALL_REPLACEMENTS,
pretty_print,
)
except Exception:
logger.error("Error converting string BS3 to BS4:\n%s" % html_string)
raise
def convert_field_bootstrap_3to4(
env, model_name, field_name, domain=None, method="orm"
):
"""This converts all the values for the given model and field, being
able to restrict to a domain of affected records.
:param env: Odoo environment.
:param model_name: Name of the model that contains the field.
:param field_name: Name of the field that contains the BS3 HTML content.
:param domain: Optional domain for filtering records in the model
:param method: 'orm' (default) for using ORM; 'sql' for avoiding problems
with extra triggers in ORM.
"""
assert method in {"orm", "sql"}
if method == "orm":
return _convert_field_bootstrap_3to4_orm(
env,
model_name,
field_name,
domain,
)
records = env[model_name].search(domain or [])
return _convert_field_bootstrap_3to4_sql(
env.cr,
records._table,
field_name,
)
def _convert_field_bootstrap_3to4_orm(env, model_name, field_name, domain=None):
"""Convert a field from Bootstrap 3 to 4, using Odoo ORM.
:param odoo.api.Environment env: Environment to use.
:param str model_name: Model to update.
:param str field_name: Field to convert in that model.
:param domain list: Domain to restrict conversion.
"""
domain = domain or [(field_name, "!=", False), (field_name, "!=", "<p><br></p>")]
records = env[model_name].search(domain)
update_field_multilang(
records,
field_name,
lambda old, *a, **k: convert_string_bootstrap_3to4(old),
)
def _convert_field_bootstrap_3to4_sql(cr, table, field, ids=None):
"""Convert a field from Bootstrap 3 to 4, using raw SQL queries.
TODO Support multilang fields.
:param odoo.sql_db.Cursor cr:
Database cursor.
:param str table:
Table name.
:param str field:
Field name, which should contain HTML content.
:param list ids:
List of IDs, to restrict operation to them.
"""
sql = "SELECT id, %s FROM %s " % (field, table)
params = ()
if ids:
sql += "WHERE id IN %s"
params = (ids,)
cr.execute(sql, params)
for id_, old_content in cr.fetchall():
new_content = convert_string_bootstrap_3to4(old_content)
if old_content != new_content:
cr.execute(
"UPDATE %s SET %s = %s WHERE id = %s",
AsIs(table),
AsIs(field),
new_content,
id_,
)