Saltar al contenido principal

Exploración de cursos por categorías

Ficha técnica
  • Impacto — Tema: classes/output/core/course_renderer.php (override de course_category(), coursecat_category(), coursecat_category_content() y coursecat_coursebox()), templates coursecategory_card.mustache, coursecategory_subcat.mustache, coursecategory_courserow.mustache y extensión de course_search_hero.mustache, SCSS scss/coursecategories.scss, AMD coursecategories.js
  • Impacto — Plugin: —
  • Requisito: Diseño (Figma) · Figma: 19674-15647 (encabezado "Explora todos los cursos" con selector + buscador), 19674-15652 (card de categoría, colapsada y expandida) · Estado: Implementado

Propósito​

Sustituye el listado plano de categorías que renderiza core en /course/index.php (core_course_renderer::course_category(), árbol de <div class="category"> con html_writer puro) por el mismo lenguaje visual ya usado en la búsqueda de cursos: un encabezado ilustrado con selector de categoría y buscador, y una card por categoría que se expande in situ para mostrar sus subcategorías y cursos.

  • Encabezado compartido con /course/search.php. Misma tarjeta ilustrada ("Explora todos los cursos" + subtítulo), ahora con el selector "Todas las categorías" que esa página dejó pendiente (ver Selector de categoría descartado en esta iteración de esa ficha) — aquí sí es viable porque es navegación directa, no un filtro combinado con búsqueda de texto libre.
  • Card de categoría con portada (extraída de la descripción de la categoría), etiqueta "Categoría", nombre, contador "N Cursos" y chevron que expande/colapsa.
  • Listado expandido con borde y esquinas redondeadas, filas de curso (enlace directo) y filas de subcategoría (en negrita, expandibles a su vez, con el mismo mecanismo, cualquier nivel de profundidad).
  • Reutiliza el expansor AJAX de core (moodle-course-categoryexpander, course/category.ajax.php) sin reimplementarlo: solo cambia el markup que ese mecanismo muestra y oculta.
  • Ancho de página ampliado a 1087px (#page.drawers .main-inner, mismo patrón ya usado en "Mis cursos" y en /course/search.php): el limitedwidth de core lo deja en 1024px, por debajo de los 1038-1041px que miden el hero y la card en el Figma.

Por qué un renderer y no solo CSS​

El markup de core (coursecat_category(), coursecat_coursebox(), ambos en course/renderer.php) no tiene portada, etiqueta ni tarjeta — es una lista con indentación y un ícono de flecha de fondo. La card, el listado con borde y las filas de curso/subcategoría del Figma no tienen equivalente en el markup de core, así que se sobreescriben los métodos que construyen ese árbol.

Dónde vive el renderer nuevo​

Reutiliza el mismo renderer namespaced que ya soporta la card de búsqueda (theme_cdigital\output\core\course_renderer, que extiende la clase legacy theme_cdigital_core_course_renderer y no core_course_renderer directamente, por la misma razón ya documentada ahí). No fue necesario crear ningún renderer adicional.

Guarda por pagetype — con una trampa​

/course/index.php fija $PAGE->set_pagetype('course-index-category') (en las dos ramas, con y sin categoryid). Pero la expansión AJAX entra por course/category.ajax.php, que no llama a set_pagetype(), así que initialise_default_pagetype() (lib/pagelib.php) deriva el pagetype del script: solo quita el sufijo literal .php y cambia / por -, sin tocar los demás puntos, así que course/category.ajax.php se convierte en course-category.ajax (con un punto antes de "ajax", no un guion). Se verificó en vivo: con la guarda mal escrita (course-category-ajax, guion) el contenido cargado por AJAX volvía al markup de core (<a class="aalink"> plano) mientras la carga inicial de la página ya mostraba el diseño nuevo — el bug solo aparecía al expandir una card. Los métodos que dependen de esta guarda aceptan ambos pagetypes vía la constante CATEGORYBROWSE_PAGETYPES.

course_category() no necesita guarda: /course/index.php:68 es su único invocador en todo el código base (mismo criterio ya aplicado a search_courses()).

Datos de la card​

CampoOrigen
PortadaPrimera <img> de la descripción de la categoría (coursecat_helper::get_category_formatted_description(), parseada con DOMDocument), con fallback a pix/no-image.jpg
Nombrecore_course_category::get_formatted_name()
Contador "N Cursos"core_course_category::get_courses_count() — cursos directos, no recursivo
Selector de categoríascore_course_category::make_categories_list(), con una opción "Todas las categorías" añadida al inicio apuntando a /course/index.php sin parámetro

La portada no es un campo nuevo de la categoría. Moodle no tiene imagen dedicada de categoría; se reutiliza la descripción (Administración del sitio → Cursos → Gestionar categorías → Editar categoría → subir la ilustración dentro del editor de descripción). El texto de la descripción en sí no se muestra en la card — solo se extrae la imagen. La descripción completa (con la imagen incluida) sí se sigue mostrando, sin cambios, cuando se navega a una categoría concreta (?categoryid=N), igual que hacía core.

Por qué la card no usa un enlace ni una imagen real​

moodle-course-categoryexpander (course/yui/src/categoryexpander) delega los clics a .category .info .categoryname para expandir/contraer, y ignora cualquier clic cuyo objetivo sea un <a> o una <img> (categoryexpander.js, comprobación al inicio del handler). Por eso:

  • La portada es un <div> con background-image, no un <img>.
  • El nombre de la categoría no es un enlace — la card entera es la superficie clicable (role="button", tabindex="0"), y la navegación a una categoría concreta queda cubierta por el selector del encabezado (ver Desviaciones).

Las filas de curso, en cambio, sí son <a> normales: no forman parte de la delegación de moodle-course-categoryexpander (que solo vigila .categoryname y .coursebox .moreinfo, ninguno de los cuales usa esta fila), así que no hay conflicto.

Cards, filas y profundidad​

El árbol de categorías de core es recursivo por profundidad (coursecat_category($chelper, $coursecat, $depth)); el tema decide el markup según $depth:

  • $depth <= 1 → card completa (coursecategory_card.mustache): portada, etiqueta, nombre, contador.
  • $depth >= 2 → fila compacta (coursecategory_subcat.mustache): solo nombre en negrita y chevron, sin portada ni contador — así se ve tanto una subcategoría de primer nivel como cualquier nivel más profundo, expandible de la misma forma.

El listado expandido de una card (coursecat_category_content()) se envuelve en una caja con borde redondeado (cdigital-catcard__list) siempre que $depth >= 1 — es decir, cuando lo que va a mostrar son filas compactas, no cards. La vista de "todas las categorías" ($depth == 0, pseudo-categoría raíz) y la vista de una categoría concreta navegada directamente (?categoryid=N, también $depth == 0 para ese árbol) no se envuelven: en ambos casos lo siguiente que se muestra son cards de primer nivel, que ya aportan su propio borde — envolverlas también habría duplicado el recuadro.

Desviaciones conscientes respecto al Figma​

  1. Nombre de categoría no enlazado. Core lo emite como enlace a /course/index.php?categoryid=…; aquí el chevron es la interacción principal (expandir), y el expansor de core ignora los clics sobre <a>. La navegación directa a una categoría queda cubierta por el selector del encabezado — mismo criterio ya aplicado al chip de categoría en la card de búsqueda.
  2. Vista de una categoría concreta (?categoryid=N) sin card contenedora. El Figma solo especifica la vista de "todas las categorías". Al navegar a una categoría, sus subcategorías se muestran como cards (mismo componente, un nivel más adentro) y sus cursos directos como filas sueltas, sin la caja con borde que normalmente las envuelve (ver Cards, filas y profundidad) — no hay spec de Figma para esta variante.
  3. Enlace "Expandir todo / Contraer todo" (coursecat_tree(), solo aparece si la categoría tiene hijos): no está en el Figma. Se conserva por funcionalidad y accesibilidad, con un estilo discreto.
  4. Menú "Más acciones" (core_course\output\category_action_bar, visible solo para quien puede crear cursos o gestionar categorías): no está en el Figma, se conserva dentro del encabezado por funcionalidad.
  5. Contador "N Cursos": cuenta cursos directos de la categoría, no recursivo (mismo comportamiento que core_course_category::get_courses_count() sin opciones).

Archivos clave​

ArchivoDescripción
theme/cdigital/classes/output/core/course_renderer.phpOverride de course_category() (hero + selector, sin guarda), coursecat_category()/coursecat_category_content()/coursecat_coursebox() (con guarda CATEGORYBROWSE_PAGETYPES), y category_cover_url() (extracción de portada)
theme/cdigital/templates/coursecategory_card.mustacheMarkup de la card de categoría de primer nivel
theme/cdigital/templates/coursecategory_subcat.mustacheMarkup de la fila de subcategoría anidada
theme/cdigital/templates/coursecategory_courserow.mustacheMarkup de la fila de curso
theme/cdigital/templates/course_search_hero.mustacheAmpliado con categoryselect/additionaloptions opcionales (sin efecto en /course/search.php, que no los pasa)
theme/cdigital/scss/coursecategories.scssCard, listado con borde, filas de curso/subcategoría; también aloja ahora el bloque de hero compartido con /course/search.php (antes vivía solo en coursesearch.scss)
theme/cdigital/amd/src/coursecategories.jsSincroniza aria-expanded en la cabecera clicable a partir de la clase collapsed que ya gestiona moodle-course-categoryexpander (sin registrar listeners de clic propios, mismo patrón que mycourses_filters.js)
theme/cdigital/lang/{es,en}/theme_cdigital.phpStrings coursecategory_herotitle, coursecategory_herosubtitle, coursecategory_allcategories, coursecategory_categorytag, coursecategory_coursecount(one)

Build y despliegue​

Hay un módulo AMD nuevo (coursecategories.js): requiere grunt amd antes de desplegar. El SCSS lo compila Moodle en runtime. Para el despliegue completo (copia de código, upgrade, purga de cachés), ver Procedimiento de despliegue.