Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] openupgrade_160, openupgrade_tools: BS4 to BS5 transformation yeahhhhh #338

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 357 additions & 1 deletion openupgradelib/openupgrade_160.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@
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,
replace_html_replacement_class_rp_by_inline_shortcut as _class_rp_by_inline,
)

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


def migrate_translations_to_jsonb(env, fields_spec):
Expand Down Expand Up @@ -45,3 +58,346 @@ 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment i use here following the same order in https://getbootstrap.com/docs/5.1/migration

_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"),
# Special case where text-justify no longer exist
_class_rp_by_inline(
selector="//*[contains(@class, 'text-justify')]",
selector_mode="xpath",
class_rp_by_inline={"text-justify": ["text-align: justify"]},
),
_r(class_rm="text-justify", class_add=""),
# 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_,
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a bug in the cr.execute() call, assuming syntax is cr.execute(query, params)?
I got the below error during migration which lead me here.

2023-11-11 15:54:29,149 58099 CRITICAL test odoo.service.server: Failed to initialize database `test`.
Traceback (most recent call last):
  File "/Users/lijo/work/odoo16/odoo/service/server.py", line 1299, in preload_registries
    registry = Registry.new(dbname, update_module=update_module)
  File "<decorator-gen-16>", line 2, in new
  File "/Users/lijo/work/odoo16/odoo/tools/func.py", line 87, in locked
    return func(inst, *args, **kwargs)
  File "/Users/lijo/work/odoo16/odoo/modules/registry.py", line 90, in new
    odoo.modules.load_modules(registry, force_demo, status, update_module)
  File "/Users/lijo/work/odoo16/odoo/modules/loading.py", line 523, in load_modules
    migrations.migrate_module(package, 'end')
  File "/Users/lijo/work/oca/OpenUpgrade/openupgrade_framework/odoo_patch/odoo/modules/migration.py", line 18, in migrate_module
    MigrationManager.migrate_module._original_method(self, pkg, stage)
  File "/Users/lijo/work/odoo16/odoo/modules/migration.py", line 189, in migrate_module
    migrate(self.cr, installed_version)
  File "/Users/lijo/miniforge3/envs/odoo16/lib/python3.8/site-packages/openupgradelib/openupgrade.py", line 2277, in wrapped_function
    func(
  File "/Users/lijo/work/oca/OpenUpgrade/openupgrade_scripts/scripts/website/16.0.1.0/end-migration.py", line 81, in migrate
    convert_custom_qweb_templates_bootstrap_4to5(env)
  File "/Users/lijo/work/oca/OpenUpgrade/openupgrade_scripts/scripts/website/16.0.1.0/end-migration.py", line 26, in convert_custom_qweb_templates_bootstrap_4to5
    _convert_field_bootstrap_4to5_sql(env.cr, "ir_ui_view", "arch_db", ids=view_ids)
  File "/Users/lijo/miniforge3/envs/odoo16/lib/python3.8/site-packages/openupgradelib/openupgrade_160.py", line 397, in _convert_field_bootstrap_4to5_sql
    cr.execute(
TypeError: execute() takes from 2 to 4 positional arguments but 6 were given

Changing the execute call as below seems to fix it

cr.execute(
                "UPDATE %s SET %s = %s WHERE id = %s",
                (AsIs(table), AsIs(field), new_content, id_),
            )

Is my understanding wrong?

Loading
Loading