[ADD] migration: offer to remove the themes before the first bump
A theme carries view copies and SCSS through every version bump, and a bump can rename what they rely on. Removing it first drops a whole family of failures, and it can be put back afterwards. Asked in step 1, before OpenUpgrade runs, and only when a theme is actually installed. Default is no: removing a theme changes how a site looks, and a migration does not decide that for its owner. theme_default is not offered — it IS the absence of a theme. --- FR --- [ADD] migration : proposer de retirer les thèmes avant le premier palier Un thème traîne des copies de vues et des SCSS à travers chaque palier de version, et un palier peut renommer ce dont ils dépendent. Le retirer d'abord enlève une famille entière de pannes, et se refait ensuite. Posée à l'étape 1, avant OpenUpgrade, et seulement s'il y a vraiment un thème installé. Par défaut non : retirer un thème change l'apparence d'un site, et ce n'est pas à une migration de trancher cela à la place de son propriétaire. theme_default n'est pas proposé — il EST l'absence de thème. Assisted-by: Claude Opus 5 (cherry picked from commit dcbee87a6b920408c315f7354a096b0ecfabcf06)
This commit is contained in:
parent
aa2f78c404
commit
3465bfdd19
3 changed files with 172 additions and 0 deletions
|
|
@ -4770,6 +4770,26 @@ TRANSLATIONS = {
|
||||||
"fr": "d'une personnalisation. Lire avant de retirer.",
|
"fr": "d'une personnalisation. Lire avant de retirer.",
|
||||||
"en": "of a customization. Read before removing.",
|
"en": "of a customization. Read before removing.",
|
||||||
},
|
},
|
||||||
|
"Installed theme(s) on": {
|
||||||
|
"fr": "Thème(s) installé(s) sur",
|
||||||
|
"en": "Installed theme(s) on",
|
||||||
|
},
|
||||||
|
"A theme carries view copies and SCSS through every": {
|
||||||
|
"fr": "Un thème traîne des copies de vues et des SCSS à travers chaque",
|
||||||
|
"en": "A theme carries view copies and SCSS through every",
|
||||||
|
},
|
||||||
|
"version bump, and a bump can rename what they rely on.": {
|
||||||
|
"fr": "palier de version, et un palier peut renommer ce dont ils dépendent.",
|
||||||
|
"en": "version bump, and a bump can rename what they rely on.",
|
||||||
|
},
|
||||||
|
"Uninstall them properly before migrating?": {
|
||||||
|
"fr": "Les désinstaller proprement avant de migrer ?",
|
||||||
|
"en": "Uninstall them properly before migrating?",
|
||||||
|
},
|
||||||
|
"Kept. Nothing was uninstalled.": {
|
||||||
|
"fr": "Conservés. Rien n'a été désinstallé.",
|
||||||
|
"en": "Kept. Nothing was uninstalled.",
|
||||||
|
},
|
||||||
"Nothing to decide yet": {
|
"Nothing to decide yet": {
|
||||||
"fr": "Rien à décider pour l'instant",
|
"fr": "Rien à décider pour l'instant",
|
||||||
"en": "Nothing to decide yet",
|
"en": "Nothing to decide yet",
|
||||||
|
|
|
||||||
|
|
@ -1545,6 +1545,11 @@ class TodoUpgrade:
|
||||||
self.dct_progression["state_1_neutralize_database"] = True
|
self.dct_progression["state_1_neutralize_database"] = True
|
||||||
self.write_config()
|
self.write_config()
|
||||||
|
|
||||||
|
if not self.dct_progression.get("state_1_theme_uninstalled"):
|
||||||
|
self.prompt_uninstall_theme(database_name)
|
||||||
|
self.dct_progression["state_1_theme_uninstalled"] = True
|
||||||
|
self.write_config()
|
||||||
|
|
||||||
config_state_1_uninstall_module = self.dct_progression.get(
|
config_state_1_uninstall_module = self.dct_progression.get(
|
||||||
"config_state_1_uninstall_module"
|
"config_state_1_uninstall_module"
|
||||||
)
|
)
|
||||||
|
|
@ -2706,6 +2711,62 @@ class TodoUpgrade:
|
||||||
prefix, sep, label = msg.partition(" - ")
|
prefix, sep, label = msg.partition(" - ")
|
||||||
print(f"🔷 {prefix}{sep}{t(label)}" if sep else f"🔷 {t(msg)}")
|
print(f"🔷 {prefix}{sep}{t(label)}" if sep else f"🔷 {t(msg)}")
|
||||||
|
|
||||||
|
def installed_theme(self, database_name):
|
||||||
|
"""Thèmes installés, hors theme_default qui EST l'absence de thème."""
|
||||||
|
status, _cmd, output = self.todo_upgrade_execute(
|
||||||
|
f'psql -X -w -d {database_name} -tAc "SELECT name FROM'
|
||||||
|
" ir_module_module WHERE name LIKE 'theme%' AND"
|
||||||
|
" state = 'installed' AND name <> 'theme_default'"
|
||||||
|
' ORDER BY name;"',
|
||||||
|
get_output=True,
|
||||||
|
wait_at_error=False,
|
||||||
|
quiet=True,
|
||||||
|
)
|
||||||
|
if status:
|
||||||
|
return []
|
||||||
|
return [line.strip() for line in (output or []) if line.strip()]
|
||||||
|
|
||||||
|
def prompt_uninstall_theme(self, database_name):
|
||||||
|
"""Proposer de retirer les thèmes AVANT de monter de version.
|
||||||
|
|
||||||
|
Un thème installé traverse la migration : ses copies de vues et ses
|
||||||
|
SCSS suivent chaque palier, et chaque palier peut renommer ce dont
|
||||||
|
ils dépendent. Le retirer d'abord enlève d'un coup une famille
|
||||||
|
entière de pannes, et se refait après.
|
||||||
|
|
||||||
|
« non » par défaut : retirer un thème change l'apparence du site, et
|
||||||
|
ce n'est pas à une migration de le décider à la place de quelqu'un.
|
||||||
|
La question n'est posée que s'il y a un thème à retirer.
|
||||||
|
"""
|
||||||
|
lst_theme = self.installed_theme(database_name)
|
||||||
|
if not lst_theme:
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
f"\n✨ {t('Installed theme(s) on')} '{database_name}' :"
|
||||||
|
f" {', '.join(lst_theme)}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" {t('A theme carries view copies and SCSS through every')}"
|
||||||
|
f" {t('version bump, and a bump can rename what they rely on.')}"
|
||||||
|
)
|
||||||
|
answer = (
|
||||||
|
self.ask_gate(
|
||||||
|
f"💬 {t('Uninstall them properly before migrating?')}"
|
||||||
|
f" (y/N, {t('(b = go back to a previous step)')}) : "
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if answer != "y":
|
||||||
|
print(f"ℹ -> {t('Kept. Nothing was uninstalled.')}")
|
||||||
|
return
|
||||||
|
for theme in lst_theme:
|
||||||
|
self.todo_upgrade_execute(
|
||||||
|
f"./script/addons/uninstall_addons_theme.sh"
|
||||||
|
f" {database_name} {theme}",
|
||||||
|
wait_at_error=False,
|
||||||
|
)
|
||||||
|
|
||||||
def show_cow_drift(self, database_name, next_version, mode="diff"):
|
def show_cow_drift(self, database_name, next_version, mode="diff"):
|
||||||
"""Montre les copies COW à risque. Ne touche à rien.
|
"""Montre les copies COW à risque. Ne touche à rien.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,97 @@ class TestTheLeftoverReport(unittest.TestCase):
|
||||||
self.assertIn("default_transaction_read_only=on", source)
|
self.assertIn("default_transaction_read_only=on", source)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheMigrationOffersIt(unittest.TestCase):
|
||||||
|
"""La question doit venir AVANT le premier palier, et par défaut non.
|
||||||
|
|
||||||
|
Un thème installé traverse la migration : ses copies de vues et ses SCSS
|
||||||
|
suivent chaque palier, et chaque palier peut renommer ce dont ils
|
||||||
|
dépendent. Le proposer tôt retire d'un coup une famille de pannes.
|
||||||
|
|
||||||
|
Par défaut non : retirer un thème change l'apparence d'un site, et ce
|
||||||
|
n'est pas à une migration de trancher cela à la place de quelqu'un.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from script.todo import todo_i18n
|
||||||
|
|
||||||
|
self.addCleanup(
|
||||||
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
||||||
|
)
|
||||||
|
todo_i18n._current_lang = "en"
|
||||||
|
|
||||||
|
def upgrade(self, lst_theme, answer):
|
||||||
|
from script.todo.todo_upgrade import TodoUpgrade
|
||||||
|
|
||||||
|
obj = TodoUpgrade.__new__(TodoUpgrade)
|
||||||
|
obj.dct_progression = {}
|
||||||
|
obj.lst_command_executed = []
|
||||||
|
obj.write_config = lambda: None
|
||||||
|
obj.installed_theme = lambda db: lst_theme
|
||||||
|
self.lst_cmd = []
|
||||||
|
obj.todo_upgrade_execute = lambda cmd, **kw: (
|
||||||
|
self.lst_cmd.append(cmd),
|
||||||
|
(False, cmd, []),
|
||||||
|
)[1]
|
||||||
|
obj.ask_gate = lambda prompt: answer
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def run_prompt(self, lst_theme, answer):
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
|
||||||
|
obj = self.upgrade(lst_theme, answer)
|
||||||
|
out = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out):
|
||||||
|
obj.prompt_uninstall_theme("db")
|
||||||
|
return self.lst_cmd, out.getvalue()
|
||||||
|
|
||||||
|
def test_the_default_answer_uninstalls_nothing(self):
|
||||||
|
# « Entrée » ne doit RIEN faire : la migration ne décide pas de
|
||||||
|
# l'apparence d'un site à la place de son propriétaire.
|
||||||
|
lst_cmd, text = self.run_prompt(["theme_technolibre"], "")
|
||||||
|
self.assertEqual(lst_cmd, [])
|
||||||
|
self.assertIn("Kept", text)
|
||||||
|
|
||||||
|
def test_no_theme_means_no_question(self):
|
||||||
|
lst_cmd, text = self.run_prompt([], "y")
|
||||||
|
self.assertEqual(lst_cmd, [])
|
||||||
|
self.assertEqual(text, "")
|
||||||
|
|
||||||
|
def test_yes_runs_the_proper_uninstaller(self):
|
||||||
|
lst_cmd, _ = self.run_prompt(["theme_technolibre"], "y")
|
||||||
|
self.assertEqual(len(lst_cmd), 1)
|
||||||
|
self.assertIn(
|
||||||
|
"uninstall_addons_theme.sh db theme_technolibre", lst_cmd[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_every_theme_is_offered_together(self):
|
||||||
|
lst_cmd, _ = self.run_prompt(["theme_a", "theme_b"], "Y")
|
||||||
|
self.assertEqual(len(lst_cmd), 2)
|
||||||
|
|
||||||
|
def test_it_is_asked_before_the_first_bump(self):
|
||||||
|
# L'ordre est le point : posée après le premier palier, la question
|
||||||
|
# arrive quand les copies ont déjà traversé un renommage.
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from script.todo.todo_upgrade import TodoUpgrade
|
||||||
|
|
||||||
|
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||||||
|
appel = source.index("prompt_uninstall_theme")
|
||||||
|
palier = source.index("4 - Upgrade version with OpenUpgrade")
|
||||||
|
self.assertLess(appel, palier)
|
||||||
|
|
||||||
|
def test_theme_default_is_not_a_theme_to_remove(self):
|
||||||
|
# theme_default EST l'absence de thème : le proposer au retrait
|
||||||
|
# ferait poser une question sans objet à chaque migration.
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from script.todo.todo_upgrade import TodoUpgrade
|
||||||
|
|
||||||
|
source = inspect.getsource(TodoUpgrade.installed_theme)
|
||||||
|
self.assertIn("name <> 'theme_default'", source)
|
||||||
|
|
||||||
|
|
||||||
class TestExitCodes(unittest.TestCase):
|
class TestExitCodes(unittest.TestCase):
|
||||||
"""0 rien, 1 des restes, 2 l'outil a échoué — comme les outils voisins."""
|
"""0 rien, 1 des restes, 2 l'outil a échoué — comme les outils voisins."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue