Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[IMP] openupgrade_160, openupgrade_tools: BS4 to BS5 Tranformation
Browse files Browse the repository at this point in the history
ndd-odoo committed Aug 26, 2023
1 parent 5d9a7b3 commit ddcd1cd
Showing 2 changed files with 387 additions and 2 deletions.
353 changes: 352 additions & 1 deletion openupgradelib/openupgrade_160.py
Original file line number Diff line number Diff line change
@@ -5,10 +5,22 @@
the >=16.0 migration.
"""
import itertools
import logging
from itertools import product

from psycopg2.extensions import AsIs

from odoo.tools.translate import _get_translation_upgrade_queries

from .openupgrade import logged_query, table_exists
from .openupgrade import logged_query, table_exists, update_field_multilang
from .openupgrade_tools import (
convert_html_fragment,
convert_html_replacement_class_shortcut as _r,
replace_html_replacement_attr_shortcut as _attr_replace,
)

logger = logging.getLogger("OpenUpgrade")
logger.setLevel(logging.DEBUG)


def migrate_translations_to_jsonb(env, fields_spec):
@@ -45,3 +57,342 @@ def migrate_translations_to_jsonb(env, fields_spec):
if initial_translation_tables == "ir_translation":
query = query.replace("_ir_translation", "ir_translation")
logged_query(env.cr, query)


_BADGE_CONTEXTS = (
"secondary",
"primary",
"success",
"info",
"warning",
"danger",
)

_RTL_REPLACEMENT_CONTEXT = (
("left", "start"),
("right", "end"),
)

_RTL_REPLACEMENT_ELEMENT = (
"text",
"float",
"border",
"border-top",
"border-bottom",
"rounded",
)

_MARGIN_PADDING_ELEMENT_REPLACEMENT = (
("pl", "ps"),
("ml", "ms"),
("pr", "pr"),
("mr", "me"),
)

_MARGIN_PADDING_SIZE = ("0", "1", "2", "3", "4", "5", "auto")

_MARGIN_PADDING = ("sm", "lg")

# These replacements are from standard Bootstrap 4 to 5
_BS5_REPLACEMENTS = (
# Grid stuff
_r(class_rm="no-gutters", class_add="g-0"),
_r(class_rm="media", class_add="d-flex"),
_r("media-body", "flex-grow-1"),
# Content, Reboot, etc
_r("thead-light", "table-light"),
_r("thead-dark", "table-dark"),
_r(
"text-justify", "text-center"
), # actually boostrap 5 only drop without any replacements
# RTL
*(
_r("%s-%s" % (elem, t4), "%s-%s" % (elem, t5))
for (t4, t5), elem in product(
_RTL_REPLACEMENT_CONTEXT, _RTL_REPLACEMENT_ELEMENT
)
),
_r("pl", "ps"),
_r("pr", "pe"),
_r("ml", "ms"),
_r("mr", "me"),
# For stub like pl-0 -> ps-0
*(
_r("%s-%s" % (t4, size), "%s-%s" % (t5, size))
for (t4, t5), size in product(
_MARGIN_PADDING_ELEMENT_REPLACEMENT, _MARGIN_PADDING_SIZE
)
),
# For stub like ml-sm-1 -> ms-sm-1
*(
_r("%s-%s-%s" % (t4, context, size), "%s-%s-%s" % (t5, context, size))
for (t4, t5), context, size in product(
_MARGIN_PADDING_ELEMENT_REPLACEMENT, _MARGIN_PADDING, _MARGIN_PADDING_SIZE
)
),
# Forms
_r("custom-control", "form-control"),
_r("custom-checkbox", "form-check"),
_r("custom-control-input", "form-check-input"),
_r("custom-control-label", "form-check-label"),
_r("custom-switch", "form-switch"),
_r("custom-select", "form-select"),
_r("custom-select-sm", "form-select-sm"),
_r("custom-select-lg", "form-select-lg"),
_r("custom-range", "form-range"),
_r("form-control-file", "form-control"),
_r("form-control-range", "form-control"),
_r(selector="span.input-group-append", class_rm="input-group-append"),
_r(
selector="div.input-group-append",
class_rm="input-group-append",
class_add="input-group-text",
),
_r(selector="div.input-group-prepend", class_rm="input-group-prepend"),
_r(
selector="span.input-group-prepend",
class_rm="input-group-prepend",
),
_r(class_rm="form-row", class_add="row"),
_r(selector=".form-inline", class_rm="form-inline"),
# Badges
*(
_r(class_rm="badge-%s" % badge_context, class_add="bg-%s" % badge_context)
for badge_context in _BADGE_CONTEXTS
),
_r(class_rm="badge-pill", class_add="rounded-pill"),
# Close button
_r(class_rm="close", class_add="btn-close"),
# Utilities
_r("text-monospace", "font-monospace"),
_r("text-hide", "visually-hidden"),
_r("font-weight-normal", "fw-normal"),
_r("font-weight-bold", "fw-bold"),
_r("font-weight-lighter", "fw-lighter"),
_r("font-weight-bolder", "fw-bolder"),
_r("font-weight-medium", "fw-medium"),
_r("font-weight-normal", "fw-normal"),
_r("font-weight-normal", "fw-normal"),
_r("font-italic", "fst-italic"),
_r("font-normal", "fst-normal"),
_r("rounded-sm", "rounded-1"),
_r("rounded-lg", "rounded-3"),
# Helpers
_r(selector="embed-responsive-item", class_rm="embed-responsive-item"),
_r("sr-only", "visually-hidden"),
_r("sr-only-focusable", "visually-hidden-focusable"),
# JavaScript
_attr_replace(
selector="//*[@data-ride]",
selector_mode="xpath",
attr_rp={"data-ride": "data-bs-ride"},
),
_attr_replace(
selector="//*[@data-interval]",
selector_mode="xpath",
attr_rp={"data-interval": "data-bs-interval"},
),
_attr_replace(
selector="//*[@data-toggle]",
selector_mode="xpath",
attr_rp={"data-toggle": "data-bs-toggle"},
),
_attr_replace(
selector="//*[@data-dismiss]",
selector_mode="xpath",
attr_rp={"data-dismiss": "data-bs-dismiss"},
),
_attr_replace(
selector="//*[@data-trigger]",
selector_mode="xpath",
attr_rp={"data-trigger": "data-bs-trigger"},
),
_attr_replace(
selector="//*[@data-target]",
selector_mode="xpath",
attr_rp={"data-target": "data-bs-target"},
),
_attr_replace(
selector="//*[@data-spy]",
selector_mode="xpath",
attr_rp={"data-spy": "data-bs-spy"},
),
_attr_replace(
selector="//*[@data-display]",
selector_mode="xpath",
attr_rp={"data-display": "data-bs-display"},
),
_attr_replace(
selector="//*[@data-backdrop]",
selector_mode="xpath",
attr_rp={"data-backdrop": "data-bs-backdrop"},
),
_attr_replace(
selector="//*[@data-original-title]",
selector_mode="xpath",
attr_rp={"data-original-title": "data-bs-original-title"},
),
_attr_replace(
selector="//*[@data-template]",
selector_mode="xpath",
attr_rp={"data-template": "data-bs-template"},
),
_attr_replace(
selector="//*[@data-html]",
selector_mode="xpath",
attr_rp={"data-html": "data-bs-html"},
),
_attr_replace(
selector="//*[@data-slide]",
selector_mode="xpath",
attr_rp={"data-slide": "data-bs-slide"},
),
_attr_replace(
selector="//*[@data-slide-to]",
selector_mode="xpath",
attr_rp={"data-slide-to": "data-bs-slide-to"},
),
_attr_replace(
selector="//*[@data-parent]",
selector_mode="xpath",
attr_rp={"data-parent": "data-bs-parent"},
),
_attr_replace(
selector="//*[@data-focus]",
selector_mode="xpath",
attr_rp={"data-focus": "data-bs-focus"},
),
_attr_replace(
selector="//*[@data-content]",
selector_mode="xpath",
attr_rp={"data-content": "data-bs-content"},
),
_attr_replace(
selector="//*[@data-placement]",
selector_mode="xpath",
attr_rp={"data-placement": "data-bs-placement"},
),
)

# These replacements are specific for Odoo v15 to v16
_ODOO16_REPLACEMENTS = (
# Form
_r(class_rm="form-group", class_add="mb-3"),
# Helpers
_r(selector="embed-responsive-16by9", class_rm="embed-responsive"),
_r("embed-responsive-16by9", "ratio ratio-16x9"),
# Javascript
_attr_replace(
selector="//*[@data-keyboard]",
selector_mode="xpath",
attr_rp={"data-keyboard": "data-bs-keyboard"},
),
)

ALL_REPLACEMENTS = _BS5_REPLACEMENTS + _ODOO16_REPLACEMENTS


def convert_string_bootstrap_4to5(html_string, pretty_print=True):
"""Convert an HTML string from Bootstrap 4 to 5.
: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 BS4 to BS5:\n%s" % html_string)
raise


def convert_field_bootstrap_4to5(
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_4to5_orm(
env,
model_name,
field_name,
domain,
)
records = env[model_name].search(domain or [])
return _convert_field_bootstrap_4to5_sql(
env.cr,
records._table,
field_name,
)


def _convert_field_bootstrap_4to5_orm(env, model_name, field_name, domain=None):
"""Convert a field from Bootstrap 4 to 5, 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_4to5(old),
)


def _convert_field_bootstrap_4to5_sql(cr, table, field, ids=None):
"""Convert a field from Bootstrap 4 to 5, 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_4to5(old_content)
if old_content != new_content:
cr.execute(
"UPDATE %s SET %s = %s WHERE id = %s",
AsIs(table),
AsIs(field),
new_content,
id_,
)
36 changes: 35 additions & 1 deletion openupgradelib/openupgrade_tools.py
Original file line number Diff line number Diff line change
@@ -104,6 +104,7 @@ def convert_xml_node(
style_rm=frozenset(),
tag="",
wrap="",
attr_rp=None,
):
"""Apply conversions to an XML node.
@@ -157,9 +158,14 @@ def convert_xml_node(
:param str wrap:
XML element that will wrap the :param:`node`.
:param dict attr_rp:
Specify a dict of attribute to place from old to the new one
Ex: {"data-toggle": "data-bs-togle"} (typical case when convert BS4 to BS5 in odoo 16)
"""
# Fix params
attr_add = attr_add or {}
attr_rp = attr_rp or {}
class_add = set(class_add.split())
class_rm = set(class_rm.split())
style_add = style_add or {}
@@ -180,14 +186,18 @@ def convert_xml_node(
_call = lambda v: v(**originals) if callable(v) else v # noqa: E731
attr_add = _call(attr_add)
attr_rm = _call(attr_rm)
attr_rp = _call(attr_rp)
class_add = _call(class_add)
class_rm = _call(class_rm)
style_add = _call(style_add)
style_rm = _call(style_rm)
tag = _call(tag)
wrap = _call(wrap)
# Patch node attributes
if attr_add or attr_rm:
if attr_add or attr_rm or attr_rp:
for key, value in attr_rp.items():
if key in node.attrib:
node.attrib[value] = node.attrib.pop(key, None)
for key in attr_rm:
node.attrib.pop(key, None)
for key, value in attr_add.items():
@@ -247,3 +257,27 @@ def convert_html_replacement_class_shortcut(class_rm="", class_add="", **kwargs)
}
)
return kwargs


def replace_html_replacement_attr_shortcut(attr_rp="", **kwargs):
"""Shortcut to replace an attribute spec.
:param dict attr_rp:
EX: {'data-toggle': 'data-bs-toggle'}
Where the 'key' is the attribute will be replaced by the 'value'
:return dict:
Generated spec, to be included in a list of replacements to be
passed to :meth:`convert_xml_fragment`.
"""

# Disallow selector to be empty
assert "selector" in kwargs and kwargs["selector"] != ""
# Also to be able to get exact element that have that attribute need selector_mode xpath
assert "selector_mode" in kwargs and kwargs["selector_mode"] == "xpath"
kwargs.update(
{
"attr_rp": attr_rp,
}
)
return kwargs

0 comments on commit ddcd1cd

Please sign in to comment.