[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous le type de VM — juste avant les sections qu'elle commande — et le suivi la quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe. Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux : sans installation mais AVEC un bureau, le magasin d'applications et les outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser. --- EN --- "Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain it. It becomes "Install software in the VM", moves under the VM type — right before the sections it commands — and the dashboard leaves it, since that watches the VM arrive even when nothing is installed. What it makes ineffective now greys out, title included. Three states, not two: with no install but WITH a desktop, the application store and the "before" tools still act; only those living in the repository go dark. Greying the lot would have lied as much as greying nothing. Assisted-by: Claude Opus 5
This commit is contained in:
parent
009ee6e2f6
commit
677b0e6529
6 changed files with 425 additions and 43 deletions
|
|
@ -395,6 +395,9 @@ CSS_BASE = """
|
|||
}
|
||||
#totals { height: auto; color: $text-muted; padding: 0 1; }
|
||||
.grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; }
|
||||
/* Une section que le choix d'installation rend inerte : le titre pâlit avec
|
||||
ses champs, pour qu'elle se lise inactive d'un coup d'œil. */
|
||||
.grouptitle.off { color: $text-muted; text-style: none; }
|
||||
.freeval { display: none; width: 9; }
|
||||
/* « width: auto » sur la CARTE, pas seulement sur la rangée. Un conteneur
|
||||
Textual vaut « width: 1fr » par défaut : la carte se bornait donc au panneau,
|
||||
|
|
|
|||
|
|
@ -277,9 +277,19 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
value=True,
|
||||
id="f_sshcfg",
|
||||
)
|
||||
yield Static("ERPLibre", classes="grouptitle")
|
||||
# La case commande TOUTE installation — ERPLibre, Odoo,
|
||||
# mais aussi l'hyperviseur Proxmox VE d'une VM imbriquée.
|
||||
# Nommée « ERPLibre », elle laissait croire qu'un système
|
||||
# Proxmox s'installerait quand même.
|
||||
yield Static(
|
||||
t("Installation"),
|
||||
id="t_install",
|
||||
classes="grouptitle",
|
||||
)
|
||||
yield Checkbox(
|
||||
t("Install ERPLibre"), value=True, id="f_install"
|
||||
t("Install software in the VM"),
|
||||
value=True,
|
||||
id="f_install",
|
||||
)
|
||||
yield Select(
|
||||
[(lbl, i) for i, (lbl, _c) in enumerate(profiles)],
|
||||
|
|
@ -293,12 +303,19 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
allow_blank=False,
|
||||
id="f_branch",
|
||||
)
|
||||
# Hors de la section « Installation » : le suivi regarde la
|
||||
# VM ARRIVER, même quand rien ne s'installe. Rangé dedans,
|
||||
# il se serait grisé avec elle.
|
||||
yield Static(
|
||||
t("Monitoring and parallelism"),
|
||||
classes="grouptitle",
|
||||
)
|
||||
yield Checkbox(
|
||||
t("Follow the installation (dashboard)"),
|
||||
value=True,
|
||||
id="f_monitor",
|
||||
)
|
||||
yield Static(t("Parallelism"), classes="grouptitle")
|
||||
yield Static(f" {t('Parallelism')}")
|
||||
yield Select(
|
||||
[(str(n), n) for n in (1, 2, 3, 4)],
|
||||
value=1,
|
||||
|
|
@ -316,6 +333,7 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
|
||||
def on_mount(self) -> None:
|
||||
self._reload_catalog()
|
||||
self._sync_install_deps()
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# Le plan
|
||||
|
|
@ -539,9 +557,26 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
self._clear_overrides(tuple(RES_FIELDS))
|
||||
self._refresh_after(remonter=True)
|
||||
|
||||
def _sync_install_deps(self) -> None:
|
||||
"""Grise ce que la case rend sans effet : la branche et le profil.
|
||||
|
||||
Le suivi n'en fait pas partie — il regarde la VM arriver même
|
||||
quand rien ne s'installe."""
|
||||
installe = self.query_one("#f_install", Checkbox).value
|
||||
for cible in ("#f_profile_install", "#f_branch"):
|
||||
try:
|
||||
self.query_one(cible).disabled = not installe
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.query_one("#t_install").set_class(not installe, "off")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_checkbox_changed(self, event) -> None:
|
||||
if event.checkbox.id == "f_install":
|
||||
# Le disque d'ERPLibre entre — ou sort — du total.
|
||||
self._sync_install_deps()
|
||||
self._refresh_after()
|
||||
|
||||
def on_input_changed(self, event) -> None:
|
||||
|
|
|
|||
|
|
@ -866,11 +866,11 @@ class QemuDeployMixin:
|
|||
br_txt = used_br.pop() if len(used_br) == 1 else varies
|
||||
lb_txt = used_lb.pop() if len(used_lb) == 1 else varies
|
||||
print(
|
||||
f" {t('ERPLibre install:')} {t('branch')} {br_txt}, "
|
||||
f" {t('Install:')} {t('branch')} {br_txt}, "
|
||||
f"{t('profile')} {lb_txt}, {env}"
|
||||
)
|
||||
else:
|
||||
print(f" {t('ERPLibre install:')} {t('no')}")
|
||||
print(f" {t('Install:')} {t('no')}")
|
||||
flavour = spec.get("desktop")
|
||||
if flavour:
|
||||
label = (self._QEMU_DESKTOP.get(flavour) or {}).get(
|
||||
|
|
@ -1288,6 +1288,13 @@ class QemuDeployMixin:
|
|||
"vm_tool_desktops": {
|
||||
k: v["desktops"] for k, v in self._QEMU_VM_TOOLS.items()
|
||||
},
|
||||
# « after » = l'outil vit DANS le dépôt ERPLibre (compilation
|
||||
# mobile, AVD, script Forgejo) : sans installation, il n'existe
|
||||
# pas, et la commande distante le saute en le nommant.
|
||||
"vm_tool_phases": {
|
||||
k: v.get("phase", "before")
|
||||
for k, v in self._QEMU_VM_TOOLS.items()
|
||||
},
|
||||
"vm_tool_needs_desktop": {
|
||||
k: v["needs_desktop"] for k, v in self._QEMU_VM_TOOLS.items()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
# que les afficher et rendre les cases cochées.
|
||||
vm_tools = list(ctx.get("vm_tools") or [])
|
||||
tool_disk = dict(ctx.get("vm_tool_disk") or {})
|
||||
# « after » = l'outil vit DANS le dépôt ERPLibre : sans installation, il
|
||||
# n'a rien où s'installer, bureau ou pas.
|
||||
tool_phases = dict(ctx.get("vm_tool_phases") or {})
|
||||
tool_arches = dict(ctx.get("vm_tool_arches") or {})
|
||||
tool_desktops = dict(ctx.get("vm_tool_desktops") or {})
|
||||
tool_needs_desktop = dict(ctx.get("vm_tool_needs_desktop") or {})
|
||||
|
|
@ -311,31 +314,19 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
f"{t('Graphical (server + desktop):')} {label}",
|
||||
value=defaults.get("desktop", "") == key,
|
||||
)
|
||||
if app_stores:
|
||||
yield Static(
|
||||
t("Application store:"), classes="grouptitle"
|
||||
)
|
||||
with RadioSet(id="f_store"):
|
||||
for i, (_k, label) in enumerate(app_stores):
|
||||
yield RadioButton(label, value=i == 0)
|
||||
yield Static("", id="storewarn")
|
||||
if vm_tools:
|
||||
# Une case par outil, et non une liste déroulante : ils
|
||||
# sont indépendants, et chacun se prend ou se laisse.
|
||||
yield Static(
|
||||
t("Development tools:"), classes="grouptitle"
|
||||
)
|
||||
for key, label, hint in vm_tools:
|
||||
gb = tool_disk.get(key, 0)
|
||||
yield Checkbox(
|
||||
f"{label} +{gb} Go — {hint}",
|
||||
value=key in (defaults.get("tools") or ()),
|
||||
id=f"f_tool_{key}",
|
||||
)
|
||||
yield Static("", id="toolwarn")
|
||||
yield Static("ERPLibre", classes="grouptitle")
|
||||
# La case commande TOUTE installation — ERPLibre, Odoo, mais
|
||||
# aussi l'hyperviseur Proxmox VE. Nommée « ERPLibre », elle
|
||||
# laissait croire qu'un système Proxmox s'installerait
|
||||
# quand même : rapporté, une VM Proxmox est restée une
|
||||
# Debian nue. Placée SOUS le type de VM, juste avant les
|
||||
# sections qu'elle commande.
|
||||
yield Static(
|
||||
t("Installation"),
|
||||
id="t_install",
|
||||
classes="grouptitle",
|
||||
)
|
||||
yield Checkbox(
|
||||
t("Install ERPLibre"),
|
||||
t("Install software in the VM"),
|
||||
value=defaults.get("install", True),
|
||||
id="f_install",
|
||||
)
|
||||
|
|
@ -358,11 +349,32 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
value=defaults.get("prod", False),
|
||||
id="f_prod",
|
||||
)
|
||||
yield Checkbox(
|
||||
t("Monitoring dashboard"),
|
||||
value=defaults.get("monitor", True),
|
||||
id="f_monitor",
|
||||
)
|
||||
if app_stores:
|
||||
yield Static(
|
||||
t("Application store:"),
|
||||
id="t_store",
|
||||
classes="grouptitle",
|
||||
)
|
||||
with RadioSet(id="f_store"):
|
||||
for i, (_k, label) in enumerate(app_stores):
|
||||
yield RadioButton(label, value=i == 0)
|
||||
yield Static("", id="storewarn")
|
||||
if vm_tools:
|
||||
# Une case par outil, et non une liste déroulante : ils
|
||||
# sont indépendants, et chacun se prend ou se laisse.
|
||||
yield Static(
|
||||
t("Development tools:"),
|
||||
id="t_tools",
|
||||
classes="grouptitle",
|
||||
)
|
||||
for key, label, hint in vm_tools:
|
||||
gb = tool_disk.get(key, 0)
|
||||
yield Checkbox(
|
||||
f"{label} +{gb} Go — {hint}",
|
||||
value=key in (defaults.get("tools") or ()),
|
||||
id=f"f_tool_{key}",
|
||||
)
|
||||
yield Static("", id="toolwarn")
|
||||
yield Static(t("Timezone"), classes="grouptitle")
|
||||
# Une liste plutôt qu'une saisie : un nom IANA mal
|
||||
# orthographié n'est pas refusé par cloud-init, il est
|
||||
|
|
@ -397,7 +409,9 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
# Grisé quand AUCUNE des VM retenues n'est sur une
|
||||
# architecture que mise sert.
|
||||
yield Static(
|
||||
t("Python interpreter:"), classes="grouptitle"
|
||||
t("Python interpreter:"),
|
||||
id="t_python",
|
||||
classes="grouptitle",
|
||||
)
|
||||
with RadioSet(id="f_python"):
|
||||
yield RadioButton(
|
||||
|
|
@ -405,7 +419,25 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
)
|
||||
yield RadioButton(t("pyenv (compiles from source)"))
|
||||
yield Static("", id="miswarn")
|
||||
yield Static(t("Parallelism"), classes="grouptitle")
|
||||
# Hors de la section « Installation » : le suivi
|
||||
# regarde la VM ARRIVER, même quand rien ne s'installe.
|
||||
# Rangé dans cette section, il se serait grisé avec elle —
|
||||
# et décocher ERPLibre avait déjà fait disparaître le
|
||||
# tableau de bord une fois.
|
||||
yield Static(
|
||||
t("Monitoring and parallelism"),
|
||||
id="t_deploy",
|
||||
classes="grouptitle",
|
||||
)
|
||||
yield Checkbox(
|
||||
t("Monitoring dashboard"),
|
||||
value=defaults.get("monitor", True),
|
||||
id="f_monitor",
|
||||
)
|
||||
# Le parallélisme reste dans « Déploiement » : c'est le
|
||||
# nombre de VM menées de front, pas une option
|
||||
# d'installation.
|
||||
yield Static(f" {t('Parallelism')}")
|
||||
# Cochée, la case donne une exécution PAR installation :
|
||||
# le plafond du nombre de CPU ne s'applique plus. Décochée,
|
||||
# le nombre reprend la main, et son défaut suit l'hôte —
|
||||
|
|
@ -433,6 +465,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
def on_mount(self) -> None:
|
||||
self.title = t("Deploy ERPLibre VM(s)!")
|
||||
self._reload_catalog(first_load=True)
|
||||
self._sync_install_deps()
|
||||
|
||||
# -- catalogue et recalcul ------------------------------------- #
|
||||
def _entries(self):
|
||||
|
|
@ -629,10 +662,16 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
évite de le découvrir dans le journal d'installation."""
|
||||
if not vm_tools:
|
||||
return
|
||||
installe, quelque_chose = self._install_state()
|
||||
for key, _label, _hint in vm_tools:
|
||||
usable = any(self._tools_for_vm(vm, (key,)) for vm in self.vms)
|
||||
self.query_one(f"#f_tool_{key}", Checkbox).disabled = (
|
||||
not usable
|
||||
offert = (
|
||||
installe
|
||||
if tool_phases.get(key) == "after"
|
||||
else quelque_chose
|
||||
)
|
||||
self.query_one(f"#f_tool_{key}", Checkbox).disabled = not (
|
||||
usable and offert
|
||||
)
|
||||
picked = self._vm_tools()
|
||||
skipped = sorted(
|
||||
|
|
@ -654,7 +693,10 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
"""Grise le choix quand aucune VM retenue n'est servie par mise,
|
||||
et nomme les architectures qui retomberont sur pyenv."""
|
||||
usable = self._mise_usable()
|
||||
self.query_one("#f_python", RadioSet).disabled = not usable
|
||||
installe, _quelque_chose = self._install_state()
|
||||
self.query_one("#f_python", RadioSet).disabled = not (
|
||||
usable and installe
|
||||
)
|
||||
skipped = sorted(
|
||||
{
|
||||
vm["arch"]
|
||||
|
|
@ -707,13 +749,25 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
if not app_stores:
|
||||
return
|
||||
needed = self._app_store_needed()
|
||||
self.query_one("#f_store", RadioSet).disabled = not needed
|
||||
_installe, quelque_chose = self._install_state()
|
||||
self.query_one("#f_store", RadioSet).disabled = not (
|
||||
needed and quelque_chose
|
||||
)
|
||||
self.query_one("#storewarn", Static).update(
|
||||
""
|
||||
if needed
|
||||
else f" {t('No graphical VM on a snap-based distro.')}"
|
||||
)
|
||||
|
||||
def _install_state(self):
|
||||
"""(une installation ?, quelque chose à installer ?).
|
||||
|
||||
Deux réponses et non une : sans installation mais avec un bureau,
|
||||
il se pose encore des paquets — le magasin d'applications et les
|
||||
outils de la phase « avant » gardent un effet."""
|
||||
installe = self.query_one("#f_install", Checkbox).value
|
||||
return installe, bool(installe or self._default_desktop())
|
||||
|
||||
def _mise_usable(self):
|
||||
return any(vm["arch"] in mise_arches for vm in self.vms)
|
||||
|
||||
|
|
@ -893,6 +947,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
self.call_after_refresh(self._after_mount_rows)
|
||||
|
||||
def _after_mount_rows(self) -> None:
|
||||
self._sync_install_deps()
|
||||
self._sync_free_inputs()
|
||||
self._syncing = False
|
||||
|
||||
|
|
@ -941,6 +996,52 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
pass
|
||||
self._sync_free_inputs()
|
||||
|
||||
def _sync_install_deps(self) -> None:
|
||||
"""Grise ce que le choix d'installation rend SANS EFFET.
|
||||
|
||||
Trois états et non deux, parce que la commande distante en a
|
||||
trois : rien du tout, un bureau seul, ou une installation
|
||||
complète. Sans installation MAIS avec un bureau, le magasin
|
||||
d'applications et les outils de la phase « avant » servent encore
|
||||
— les griser mentirait autant que de laisser actif ce qui ne fait
|
||||
rien. La branche, le profil et l'interpréteur Python, eux, ne
|
||||
servent qu'à l'installation.
|
||||
|
||||
Le type de VM et le suivi ne sont jamais grisés : le premier est
|
||||
l'autre moitié de la décision, le second regarde la VM arriver
|
||||
même quand rien ne s'installe."""
|
||||
installe, quelque_chose = self._install_state()
|
||||
for cible, actif in (
|
||||
("#f_branch", installe),
|
||||
("#f_profile_install", installe),
|
||||
("#f_prod", quelque_chose),
|
||||
):
|
||||
try:
|
||||
self.query_one(cible).disabled = not actif
|
||||
except Exception:
|
||||
pass
|
||||
# Le magasin, les outils et l'interpréteur Python ont leur PROPRE
|
||||
# raison de se griser (architecture, bureau, distribution) : ils
|
||||
# composent les deux dans « _render_* », qui a le dernier mot.
|
||||
# Le titre suit ses champs : une section entière se lit inactive
|
||||
# d'un coup d'œil, au lieu de se déduire de trois widgets ternes.
|
||||
for cible, actif in (
|
||||
("#t_store", quelque_chose),
|
||||
("#t_tools", quelque_chose),
|
||||
("#t_python", installe),
|
||||
):
|
||||
try:
|
||||
self.query_one(cible).set_class(not actif, "off")
|
||||
except Exception:
|
||||
pass
|
||||
# Les rangées portent les mêmes choix, par VM.
|
||||
for i in range(len(self.rows)):
|
||||
for cible in (f"#v{i}_branch", f"#v{i}_prof"):
|
||||
try:
|
||||
self.query_one(cible).disabled = not installe
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _render_plan(self):
|
||||
# Le JEU de VM a-t-il changé ? Si oui on remonte les widgets, sinon
|
||||
# on se contente des titres : remonter à chaque frappe volerait le
|
||||
|
|
@ -1007,6 +1108,9 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
self._reload_catalog()
|
||||
elif event.radio_set.id == "f_type":
|
||||
self._clear_overrides(("desktop",))
|
||||
# Le type est l'autre moitié de la décision : un bureau seul
|
||||
# garde le magasin d'applications et les outils utiles.
|
||||
self._sync_install_deps()
|
||||
# Recalcul : le disque annonce inclut le bureau, et la
|
||||
# colonne Statut affiche le type de VM.
|
||||
self._recompute()
|
||||
|
|
@ -1205,6 +1309,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
|
||||
def on_checkbox_changed(self, event) -> None:
|
||||
if event.checkbox.id == "f_install":
|
||||
self._sync_install_deps()
|
||||
self._recompute() # le disque annoncé inclut le +5 G ERPLibre
|
||||
elif event.checkbox.id == "f_par_all":
|
||||
self.query_one("#f_par", Select).disabled = event.value
|
||||
|
|
|
|||
|
|
@ -4118,9 +4118,23 @@ TRANSLATIONS = {
|
|||
"fr": "Existantes, laissées intactes :",
|
||||
"en": "Existing, left untouched:",
|
||||
},
|
||||
"ERPLibre install:": {
|
||||
"fr": "Installation ERPLibre :",
|
||||
"en": "ERPLibre install:",
|
||||
# Renommée : la case commande TOUTE installation — ERPLibre, Odoo, mais
|
||||
# aussi l'hyperviseur Proxmox VE, qui n'installe ni l'un ni l'autre.
|
||||
"Install:": {
|
||||
"fr": "Installation :",
|
||||
"en": "Install:",
|
||||
},
|
||||
"Install software in the VM": {
|
||||
"fr": "Installer un logiciel dans la VM",
|
||||
"en": "Install software in the VM",
|
||||
},
|
||||
"Installation": {
|
||||
"fr": "Installation",
|
||||
"en": "Installation",
|
||||
},
|
||||
"Monitoring and parallelism": {
|
||||
"fr": "Suivi et parallélisme",
|
||||
"en": "Monitoring and parallelism",
|
||||
},
|
||||
"profile": {
|
||||
"fr": "profil",
|
||||
|
|
|
|||
218
test/test_qemu_deploy_form_install.py
Normal file
218
test/test_qemu_deploy_form_install.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""La case d'installation : son nom, sa place, et ce qu'elle grise.
|
||||
|
||||
Elle s'appelait « Installer ERPLibre » alors qu'elle commande TOUTE
|
||||
installation — l'hyperviseur Proxmox VE compris. Rapporté après coup : une VM
|
||||
Proxmox déployée avec la case décochée est restée une Debian nue, et rien ne
|
||||
disait que la case l'expliquait.
|
||||
|
||||
Elle est donc renommée, placée juste sous le type de VM (les sections qu'elle
|
||||
commande viennent après), et ce qu'elle rend sans effet se grise. Trois états
|
||||
et non deux : sans installation mais avec un bureau, il se pose encore des
|
||||
paquets — griser le magasin d'applications mentirait autant que de laisser
|
||||
actif ce qui ne fait rien.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO # noqa: E402
|
||||
|
||||
FORM = (
|
||||
Path(__file__).resolve().parent.parent / "script/todo/qemu_deploy_form.py"
|
||||
)
|
||||
|
||||
try:
|
||||
import textual # noqa: F401
|
||||
|
||||
TEXTUAL = True
|
||||
except Exception: # pragma: no cover - dépend de l'environnement
|
||||
TEXTUAL = False
|
||||
|
||||
|
||||
class TestSaPlace(unittest.TestCase):
|
||||
"""Lu dans la source : l'ordre du panneau est une décision, pas un hasard."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.src = FORM.read_text(encoding="utf-8")
|
||||
|
||||
def _rang(self, motif):
|
||||
m = re.search(re.escape(motif), self.src)
|
||||
self.assertIsNotNone(m, motif)
|
||||
return m.start()
|
||||
|
||||
def test_the_install_section_sits_under_the_vm_type(self):
|
||||
self.assertLess(
|
||||
self._rang('with RadioSet(id="f_type")'),
|
||||
self._rang('id="t_install"'),
|
||||
)
|
||||
|
||||
def test_and_before_the_sections_it_commands(self):
|
||||
# Magasin d'applications, outils : ils dépendent d'elle, donc ils
|
||||
# viennent après.
|
||||
for apres in ('id="t_store"', 'id="t_tools"'):
|
||||
self.assertLess(self._rang('id="t_install"'), self._rang(apres))
|
||||
|
||||
def test_the_checkbox_no_longer_claims_to_be_about_erplibre(self):
|
||||
self.assertIn('t("Install software in the VM")', self.src)
|
||||
self.assertNotIn('t("Install ERPLibre")', self.src)
|
||||
|
||||
def test_the_monitor_left_the_install_section(self):
|
||||
# Rangé dedans, il se serait grisé avec elle — et décocher ERPLibre
|
||||
# avait déjà fait disparaître le tableau de bord une fois.
|
||||
self.assertLess(
|
||||
self._rang('id="t_deploy"'), self._rang('id="f_monitor"')
|
||||
)
|
||||
|
||||
|
||||
def contexte():
|
||||
todo = TODO.__new__(TODO)
|
||||
mod = todo._qemu_import_module()
|
||||
todo._qemu_list_domains = lambda: []
|
||||
todo._qemu_branch_list = lambda: ["develop", "master"]
|
||||
return todo._qemu_form_context(mod)
|
||||
|
||||
|
||||
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
||||
class TestCeQuElleGrise(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.ctx = contexte()
|
||||
|
||||
def _etats(self):
|
||||
"""Relève l'état des champs dans les trois situations."""
|
||||
from script.todo.qemu_deploy_form import run_deploy_form
|
||||
|
||||
vu = {}
|
||||
|
||||
async def scenario():
|
||||
from textual.widgets import Checkbox, SelectionList
|
||||
|
||||
app = run_deploy_form(self.ctx, run_app=False)
|
||||
async with app.run_test(size=(200, 60)) as pilote:
|
||||
await pilote.pause()
|
||||
liste = app.query_one(SelectionList)
|
||||
liste.select(liste.get_option_at_index(0).value)
|
||||
await pilote.pause()
|
||||
await pilote.pause()
|
||||
|
||||
def relever():
|
||||
etat = {}
|
||||
for cible in (
|
||||
"f_branch",
|
||||
"f_profile_install",
|
||||
"f_python",
|
||||
"f_prod",
|
||||
"f_store",
|
||||
"f_type",
|
||||
"f_monitor",
|
||||
"v0_branch",
|
||||
"v0_prof",
|
||||
):
|
||||
try:
|
||||
etat[cible] = app.query_one(f"#{cible}").disabled
|
||||
except Exception:
|
||||
etat[cible] = None
|
||||
etat["outils"] = {
|
||||
w.id[7:]: w.disabled
|
||||
for w in app.query(Checkbox)
|
||||
if str(w.id or "").startswith("f_tool_")
|
||||
}
|
||||
etat["titres"] = {
|
||||
w.id
|
||||
for w in app.query("#fields Static")
|
||||
if "off" in w.classes
|
||||
}
|
||||
return etat
|
||||
|
||||
vu["installe"] = relever()
|
||||
app.query_one("#f_install", Checkbox).value = False
|
||||
await pilote.pause()
|
||||
await pilote.pause()
|
||||
vu["rien"] = relever()
|
||||
list(app.query("#f_type RadioButton"))[1].value = True
|
||||
await pilote.pause()
|
||||
await pilote.pause()
|
||||
vu["bureau_seul"] = relever()
|
||||
# Retour au SERVEUR avant de recocher : le magasin
|
||||
# d'applications a sa propre raison de se griser (aucune VM
|
||||
# graphique sur une distribution à snap), et comparer deux
|
||||
# états de type différent ne dirait rien de la case.
|
||||
list(app.query("#f_type RadioButton"))[0].value = True
|
||||
app.query_one("#f_install", Checkbox).value = True
|
||||
await pilote.pause()
|
||||
await pilote.pause()
|
||||
vu["recoche"] = relever()
|
||||
|
||||
asyncio.run(scenario())
|
||||
return vu
|
||||
|
||||
@classmethod
|
||||
def etats(cls):
|
||||
if not hasattr(cls, "_vu"):
|
||||
cls._vu = cls()._etats()
|
||||
return cls._vu
|
||||
|
||||
def test_the_branch_and_the_profile_only_serve_an_install(self):
|
||||
vu = self.etats()
|
||||
for champ in ("f_branch", "f_profile_install", "v0_branch", "v0_prof"):
|
||||
self.assertFalse(vu["installe"][champ], champ)
|
||||
self.assertTrue(vu["rien"][champ], champ)
|
||||
# Même avec un bureau : ils ne servent QU'à ERPLibre.
|
||||
self.assertTrue(vu["bureau_seul"][champ], champ)
|
||||
|
||||
def test_a_desktop_only_install_keeps_what_it_really_uses(self):
|
||||
# Le magasin d'applications et « production » servent encore : la
|
||||
# commande distante pose des paquets et coupe les mises à jour.
|
||||
vu = self.etats()
|
||||
self.assertTrue(vu["rien"]["f_store"])
|
||||
self.assertFalse(vu["bureau_seul"]["f_store"])
|
||||
self.assertTrue(vu["rien"]["f_prod"])
|
||||
self.assertFalse(vu["bureau_seul"]["f_prod"])
|
||||
|
||||
def test_the_tools_that_live_in_the_repository_need_the_install(self):
|
||||
# « after » = dans le dépôt ERPLibre. Sans installation, ils n'ont
|
||||
# rien où s'installer, bureau ou pas — la commande distante les saute.
|
||||
vu = self.etats()
|
||||
phases = self.ctx["vm_tool_phases"]
|
||||
apres = [k for k, v in phases.items() if v == "after"]
|
||||
avant = [k for k, v in phases.items() if v != "after"]
|
||||
self.assertTrue(apres and avant, phases)
|
||||
for k in apres:
|
||||
self.assertTrue(vu["bureau_seul"]["outils"][k], k)
|
||||
for k in avant:
|
||||
self.assertFalse(vu["bureau_seul"]["outils"][k], k)
|
||||
for k in phases:
|
||||
self.assertTrue(vu["rien"]["outils"][k], k)
|
||||
|
||||
def test_the_vm_type_and_the_dashboard_are_never_greyed(self):
|
||||
# Le type est l'AUTRE moitié de la décision ; le suivi regarde la VM
|
||||
# arriver même quand rien ne s'installe.
|
||||
vu = self.etats()
|
||||
for cas in ("installe", "rien", "bureau_seul"):
|
||||
self.assertFalse(vu[cas]["f_type"], cas)
|
||||
self.assertFalse(vu[cas]["f_monitor"], cas)
|
||||
|
||||
def test_a_greyed_section_reads_as_inactive(self):
|
||||
vu = self.etats()
|
||||
self.assertEqual(vu["installe"]["titres"], set())
|
||||
self.assertIn("t_store", vu["rien"]["titres"])
|
||||
self.assertIn("t_tools", vu["rien"]["titres"])
|
||||
|
||||
def test_ticking_it_back_restores_everything(self):
|
||||
vu = self.etats()
|
||||
for champ, valeur in vu["recoche"].items():
|
||||
if champ in ("outils", "titres"):
|
||||
continue
|
||||
self.assertEqual(valeur, vu["installe"][champ], champ)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Reference in a new issue