2.3.0: proyectos por programa, carpetas vacías y mapa

Tres cosas para la reorganización, pedidas juntas.

Proyectos por programa: la plantilla pasa de 01_Proyectos/{nombre} a
01_Proyectos/{app}/{nombre}, con {app} = el programa que abre el archivo
(R.APPS_PROYECTO, R.app_proyecto). Lo que ya viene en su propia carpeta se
queda con el nombre de esa carpeta; lo que se separa es lo suelto. Las
plantillas guardadas se migran solas si nadie las tocó. Entran extensiones
que caían en «otro»: .sesx, .c4d, .blend, .motn, .aepx, .aet, .prel, .idml,
.psdt, .afpub.

Carpetas vacías: carpetas_vacias() las encuentra desde el inventario y las
comprueba en el disco; borrar_vacias() las quita. Es lo único que la app
borra, y solo porque no hay nada dentro: cada una queda en la bitácora como
rmdir y Deshacer las vuelve a crear con su nombre y en el inventario. No
entra en «Hacer las N de una vez». 29 huecos en David Reyes, 40 en N:.

Mapa: pestaña nueva con el árbol que producen las plantillas de verdad
(mapa_estructura, GET /api/estructura). Al tocar una carpeta dice qué guarda
—derivado de ROL_A_CUBETA y ROL_QUE—, cuánto hay hoy, y deja cambiar el
destino ahí mismo. POST /api/plantillas pasa a fusionar cubeta a cubeta:
guardar una sola plantilla borraba las otras diecinueve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-16 22:34:12 -05:00
padre 2930320690
commit ba186d4a26
Se han modificado 6 ficheros con 608 adiciones y 29 borrados

Ver fichero

@@ -46,6 +46,15 @@ class Reglas(unittest.TestCase):
self.assertEqual(R.detectar_anio("2026"), 2026)
self.assertIsNone(R.detectar_anio("2026 - agosto"))
def test_app_proyecto(self):
self.assertEqual(R.app_proyecto(".prproj"), "Premiere")
self.assertEqual(R.app_proyecto(".AEP"), "After Effects")
self.assertEqual(R.app_proyecto(".drp"), "DaVinci Resolve")
self.assertEqual(R.app_proyecto(".psd"), "Photoshop")
self.assertIsNone(R.app_proyecto(".mp4"))
# la plantilla de proyectos separa por programa
self.assertIn("{app}", R.PLANTILLAS["contenido"]["PROYECTOS"])
def test_basura_y_proxies(self):
self.assertTrue(R.es_basura_archivo("Thumbs.db"))
self.assertTrue(R.es_basura_archivo(".DS_Store"))
@@ -205,6 +214,47 @@ class Servidor(unittest.TestCase):
c, res = self.post("/api/deshacer", {"lote": res["lote"]})
self.assertEqual(c, 200)
self.assertEqual(antes, self._arbol())
# 4b. el mapa dice dónde va a acabar cada cosa, con las plantillas de verdad
est = self.get("/api/estructura" + q)
proy = next(c for c in est["cubetas"] if c["clave"] == "PROYECTOS")
self.assertIn("{app}", proy["plantilla"])
self.assertTrue(proy["guarda"])
nombres = []
def bajar(n):
nombres.append(n["nombre"])
for h in n["hijos"]:
bajar(h)
bajar(est["arbol"])
self.assertIn("00_Grabacion", nombres)
self.assertIn("Premiere", nombres)
# guardar UNA plantilla no puede llevarse por delante las demás
antes_pl = len([c for c in est["cubetas"] if c["plantilla"]])
self.assertEqual(self.post("/api/plantillas", {"plantillas": {"contenido": {"MUSICA": "_Recursos/Musica/{sub}"}}})[0], 200)
self.assertEqual(len([c for c in self.get("/api/estructura" + q)["cubetas"] if c["plantilla"]]), antes_pl)
# 4c. carpetas vacías: se encuentran, se borran sin tocar un archivo y deshacer las devuelve
for rel in ("Huecos/uno", "Huecos/dos", "Sola"):
os.makedirs(os.path.join(self.raiz, rel.replace("/", os.sep)), exist_ok=True)
self.post("/api/escanear", {"raiz": self.raiz})
self.esperar_escaneo()
v = self.get("/api/vacias" + q)
rels = {c["rel"] for c in v["carpetas"]}
self.assertIn("Huecos", rels) # el tope, no cada hija por separado
self.assertIn("Sola", rels)
self.assertNotIn(os.path.join("Huecos", "uno"), rels)
arbol_antes = self._arbol()
c, res = self.post("/api/vacias/borrar", {"raiz": self.raiz, "rutas": [x["ruta"] for x in v["carpetas"]]})
self.assertEqual(c, 200)
self.assertEqual(res["errores"], [])
self.assertEqual(res["borradas"], 4) # Huecos con sus dos hijas, y Sola
self.assertFalse(os.path.isdir(os.path.join(self.raiz, "Sola")))
self.assertEqual(arbol_antes, self._arbol()) # ni un archivo tocado
c, _ = self.post("/api/deshacer", {"lote": res["lote"]})
self.assertEqual(c, 200)
self.assertTrue(os.path.isdir(os.path.join(self.raiz, "Huecos", "uno")))
self.post("/api/vacias/borrar", {"raiz": self.raiz, "rutas": [x["ruta"] for x in v["carpetas"]]})
# 5. desconectar con datos; la segunda vez ya no existe
c, d = self.post("/api/raiz/quitar", {"raiz": self.raiz, "datos": True})
self.assertEqual(c, 200)