//////////////////////////////////////////////////////////////////////////////// Copyright 2016 Newport Software Technologies, UNICEN This file is part of XGAP. XGAP is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. XGAP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with XGAP. If not, see . //////////////////////////////////////////////////////////////////////////////// :encoding: utf-8 = Ejemplo: Aplicación ``Localidades'' = :lang: es :imagesdir: ../img/ :icons: :iconsdir: ../img/icons :ascii-ids: :toc: :toclevels: 3 == Introducción == La aplicación ``Localidades'' se provee como un ejemplo simple, cuyo objetivo es demostrar la forma es que se usan algunas de las características principales de XGAP, además de presentar una posible estructura para una aplicación XGAP. Su funcionalidad principal permite registrar divisiones geopolíticas: localidades, provincias, países. Además permite asociar múltiples fotos a las localidades. El modelo de datos está representado en el siguiente diagrama: .Modelo de datos de la aplicación ``Localidades'' [plantuml] ---- class Continente class "País" as Pais class Provincia class Localidad class "Foto Localidad" as FotoLocalidad Continente "0" -- "0..*" Pais Pais "1" -- "0..*" Provincia Provincia "1" -- "0..*" Localidad Localidad "1" -- "0..*" FotoLocalidad hide members hide circle ---- Cada una de las entidades ``País'', ``Provincia'', ``Localidad'' y ``Foto Localidad'' cuentan con su correspondiente listado y formulario para consultar y operar con sus instancias, en tanto que la entidad ``Continente'' tiene sus valores predefinidos en la base de datos y no se asocia a páginas de la aplicación. NOTE: El modelo de datos está diseñado para servir como base para demostrar características de XGAP, no para ofrecer una aplicación que tenga una utilidad real. == Estructura de directorios del proyecto == // TODO: la estructura de directorios predefinida se debería documentar en una sección general, no en el ejemplo. Parte de la estructura de directorios de una aplicación está predefinida por XGAP; específicamente: * Un directorio raíz para los fuentes de la aplicación. XGAP espera que este directorio se encuentre dentro del directorio `APPS_DIR` y tenga el mismo nombre que la aplicación, para que el generador lo detecte, pero también es posible ubicarlo en cualquier otro lugar, con cualquier otro nombre, y crear un enlace en `APPS_DIR`. * Subdirectorios `extras`, `extras/menu` y `resultados` dentro de este directorio raíz. Fuera de esta estructura predefinida, cada proyecto se puede organizar como resulte más conveniente. Para el caso de esta aplicación, se optó por una estructura autocontenida: tanto la raíz de la aplicación como otros directorios adicionales se encuentran dentro de un único directorio que corresponde al proyecto entero. Este directorio se puede ubicar en cualquier parte del sistema de archivos, independientemente de dónde se encuentre la distribución de XGAP. Para poder generar la aplicación, se debe colocar un enlace a la raíz de la aplicación dentro de `APPS_DIR`, con nombre `localidades`. .Árbol de directorios de la aplicación ``Localidades'' [shaape] ---- Raíz del proyecto | +-bd | | | +-cambios | +-desarrollo<------+ | | | | +-extras | | | | | | | +-clases | | | | | | | +-img | | | | | | | +-menu | | | | | | | +-templates | | | | | +-resultados | | | +-xsd | | APPS_DIR | | | +-localidades------+ '(enlace simbólico)' options: - ".*": {fill : [no-shadow]} - "_line_": {fill : [solid]} - "localidades": {fill : [dotted]} ---- La raíz para XGAP es el subdirectorio `desarrollo`, mientras que los demás subdirectorios del mismo nivel contienen archivos que no son usados directamente por XGAP. El subdirectorio `desarrollo/extras` contiene subdirectorios adicionales con recursos que va a usar en tiempo de ejecución la aplicación generada. `bd`:: Scripts SQL para crear la base de datos inicial completa. `bd/cambios`:: Scripts SQL incrementales, para actualizar la base de datos desde una versión anterior. `desarrollo`:: Raíz para XGAP, conteniendo los fuentes XML. `desarrollo/extras`:: Requerido por XGAP. Fuentes adicionales y otros recursos que se copian directamente a la aplicación generada. `desarrollo/extras/clases`:: Clases PHP. `desarrollo/extras/img`:: Imágenes. `desarrollo/extras/menu`:: Definiciones de menúes. `desarrollo/extras/templates`:: Plantillas para usar en reportes. `desarrollo/resultados`:: Requerido por XGAP. `xsd`:: Contiene los esquemas XML (`*.xsd`) del motor en uso. Este directorio no forma parte de la distribución de la aplicación (no se guarda en el repositorio de código) pero está referenciado por el atributo `xsi:noNamespaceSchemaLocation` del elemento raíz de los fuentes XML. + Una forma práctica de completar este directorio es crear enlaces simbólicos hacia los archivos `*.xsd` del directorio `plantillas` del motor. .Definición de enlaces simbólicos para la aplicación ``Localidades'', en Linux ==== En Linux se pueden usar los comandos dados a continuación para crear los enlaces simbólicos necesarios para poder trabajar con la aplicación descargada. Para este ejemplo, se asume que: * La aplicación se encuentra en `/home/user/Projects/xgap/examples/localidades`. * `APPS_DIR` está definido como `/home/user/Projects/xgap/apps`. * El motor usado se encuentra en `/home/user/Projects/xgap/dist/motores/ultimo`. * `{APPS_DIR}/localidades` no existe previamente. [source,sh] ---- ln -s /home/user/Projects/xgap/examples/localidades/desarrollo \ /home/user/Projects/xgap/apps/localidades for file in /home/user/Projects/xgap/dist/motores/ultimo/plantillas/*.xsd; do ln -s "$file" /home/user/Projects/xgap/examples/localidades/xsd/ done ---- ==== [[esquema_bd]] == Esquema de la base de datos == El esquema de base de datos que usa la aplicación (creado por los scripts que se encuentran en el subdirectorio `bd`) está representado en la figura footnote:[Diagrama generado con https://sualeh.github.io/SchemaCrawler/[SchemaCrawler]] siguiente. .Diagrama del esquema de base de datos de la aplicación ``Localidades'' ifdef::basebackend-html[] image::examples/localidades/db_diagram.svg[width="1000",link="{imagesdir}examples/localidades/db_diagram.svg"] endif::basebackend-html[] ifndef::basebackend-html[] image::examples/localidades/db_diagram.svg[scaledwidth="100%"] endif::basebackend-html[] [[esquema_bd-modelo_propio]]Además de las tablas y vistas predefinidas por XGAP, el esquema contiene las correspondientes al modelo de datos y algunas auxiliares: * Modelo ** Tabla `public.continente` ** Tabla `public.pais` ** Tabla `public.provincia` ** Tabla `public.localidad` ** Tabla `public.fotolocalidad` ** Vista `public.vprovincia` ** Vista `public.vlocalidad` ** Vista `public.vfotolocalidad` * Auxiliares ** Tabla `sistema.traduccion` ** Tabla `sistema.traduccionboolean` ** Tabla `sistema.propiedad` == Componentes == A continuación se listan los principales fuentes XML, y las tablas y vistas más relevantes que usa cada uno de ellos. .Relación entre los fuentes XML y las tablas/vistas en la base de datos de la aplicación ``Localidades'' ifdef::basebackend-html[] [options="header,autowidth",cols="2*<.>. + -- [source,php] ---- Html::cerrarTag('div'); // ... $menu_params = array(PARAMETRO_HISTORIAL => 'skip'); $param_recarga_menu = Request::obtener(RequestXgap::PARAMETRO_RECARGA_MENU, null); if (param2boolean($param_recarga_menu)) { $menu_params[RequestXgap::PARAMETRO_RECARGA_MENU] = $param_recarga_menu; } $menu = crearDireccionPagina('ppal_menu.php', $menu_params, false); // <2> $gfxpath = XGAP_CONF_PREFIJO_WEB . '/recursos/imagenes/menu/'; Html::abrirJavascript(); echo << "100%", 23, "" ); aMenuBar.setGfxPath("$gfxpath"); aMenuBar.loadXML("$menu"); aMenuBar.showBar(); } MENU; Html::cerrarJavascript(); unset($param_recarga_menu, $menu_params, $menu, $gfxpath); ---- <1> Emite el elemento vacío dentro del cual se va a construir el menú. <2> El archivo `ppal_menu.php` produce la estructura del menú. Ver sección <>. <3> Se crea el menú dentro del elemento creado para ello. -- * Una barra de sesión, con la ruta de navegación (_breadcrumb_), el nombre del usuario logueado y el link para cerrar la sesión. + [source,php] ---- ? Contexto::obtener('nombre_usuario') : Contexto::obtener('usuario_sistema', null); // <1> if (!empty($usuario)) { Html::elemento( 'span', Formateo::prepararSalidaString($usuario, null, null, true, true), null, 'sesion-usuario', null ); if (Contexto::existe('rol')) { // <1> Html::elemento( 'span', ' (' . Formateo::prepararSalidaString( Contexto::obtener('rol'), null, null, true, true) . ')', null, 'sesion-rol', null ); } echo '  –  '; } ---- <1> Las variables `'nombre_usuario'`, `'usuario_sistema'` y `'rol'` se almacenan en la sesión después del login y selección de rol. -- * El nombre y versión de la aplicación + -- [source,php] ---- null, 'app-titulo', null); Html::elemento('span', ' v. ' . XGAP_CONF_VERSION_APLICACION, // <1> null, 'app-version', null); ---- <1> `XGAP_APP_TITULO` y `XGAP_CONF_VERSION_APLICACION` son dos constantes predefinidas por XGAP. -- [[menu_ppal]] === Menú de la aplicación === El menú de la aplicación está definido en el archivo `desarrollo/extras/menu/ppal_menu.xml`. Este archivo se procesa durante la generación de la aplicación, produciendo el archivo `ppal_menu.php`, que se carga en `norte.php` (ver sección <>) para construir el menú cuando se cargan las páginas. .Elementos del menú principal de la aplicación ``Localidades'' [shaape] ---- +---------+-----------+ | Sistema | geografía | +-+-------+-----------+--+ | Seguridad ->+---------------------------------------+ -+------------------ | | Usuarios | ^ | | Roles funcionales | | sólo | | Roles por Usuario | | rol ADMINISTRADOR | | ------------------------------------- | | | | Establecer permisos de página por rol | | | | Permisos por página | | | +---------------------------------------+ v | -------------------- | -+----------------- | Cambio de Rol | | Cambio de Contraseña | | -------------------- | |'Acerca de...' | | -------------------- | | Salir | +----------------------+ +---------+-----------+ | sistema | Geografía | +---------+-+---------+---+ | Países | | Provincias | | Localidades | +-------------+ options: - ".*": {fill : [[0.9, 0.9, 0.9]]} - "_line_": {fill : [[0, 0, 0], no-shadow]} - "geografía|sistema": {fill : [[0.6, 0.6, 0.6]]} ---- [source,xml] ---- <1> <2> <2> ---- <1> El valor `"rol"` en el atributo `cache` (o la ausencia del atributo, dado que `"rol"` es su valor por defecto) hace que la estructura que define el menú (la salida emitida por `ppal_menu.php`) se guarde en la sesión del usuario, asociada al rol actual. Si el rol cambia, la estructura se reconstruye y se vuelve a guardar. Si se usara `cache="no"`, la estructura del menú se reconstruiría en cada solicitud a `ppal_menu.php`. Si se usara `cache="usuario"`, la estructura se guardaría en el cache asociada al usuario, no al rol, y se seguiría retornando la misma aún ante un cambio de rol por parte del usuario; como en este menú se usa el atributo `roles` en algunos de los elementos (explicado a continuación), este comportamiento sería incorrecto, porque evitaría que se determinen los elementos a mostrar para el nuevo rol seleccionado. <2> El atributo `roles`, disponible en los elementos `carpeta`, `item` y `separador`, permite indicar una lista de roles para los cuales se debe mostrar el elemento. En este caso, la carpeta ``Sistema'' -> ``Seguridad'' y el separador adyacente sólo se muestran para el rol ``ADMINISTRADOR''. Los elementos que no tienen el atributo se muestran para todos los roles. // TODO: la definición y carga de menúes se debería documentar en una sección general, no en el ejemplo. En esta aplicación se utiliza un único menú para todos los usuarios y roles, pero también es posible definir más de un menú y cargar uno u otro de acuerdo a algún criterio relevante para la aplicación. Por ejemplo, se podría definir un menú diferente para cada rol y cargar el que corresponda al rol activo. El ejemplo siguiente muestra una posible forma de implementar este caso. .Uso de menúes diferentes de acuerdo al rol del usuario ==== * Especificar un archivo de definición separado para cada rol. + [shaape] ---- Raíz del proyecto | +-desarrollo | +-extras | +-menu | +-ppal_menu.xml +-rol_ADMINISTRACION_menu.xml +-rol_CONSULTA_menu.xml +-'...' options: - ".*": {fill : [no-shadow]} - "_line_": {fill : [solid]} ---- * En el código donde se carga el menú, seleccionar el archivo php que corresponda al rol activo. + En <>: + -- [source,php] ---- if (!file_exists($archivo_menu)) { $archivo_menu = 'ppal_menu.php'; // <2> } $menu_params = array(PARAMETRO_HISTORIAL => 'skip'); $param_recarga_menu = Request::obtener(RequestXgap::PARAMETRO_RECARGA_MENU, null); if (param2boolean($param_recarga_menu)) { $menu_params[RequestXgap::PARAMETRO_RECARGA_MENU] = $param_recarga_menu; } $menu = crearDireccionPagina($archivo_menu, $menu_params, false); // <3> // ... ---- <1> Se construye el nombre del archivo en base al rol del usuario. <2> Si el archivo no existe (el rol actual no tiene un menú específico) se usa un menú genérico. <3> Se construye la dirección de la página a cargar, usando el archivo seleccionado. -- ==== === Páginas y documentos === En esta sección se describen las páginas y documentos más relevantes de la aplicación, destacando algunas de las características de XGAP que utilizan, el código relacionado y cómo se refleja en la interfaz a usuario, si corresponde. .Agrupación por tipo de las páginas y documentos de la aplicación ``Localidades'' [options="header,autowidth",cols="2*>, <>, <>, <>, <>, <>, index_contenido.xml, login_contenido.xml, <>, seleccionarrol_contenido.xml |Formulario |<>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> |Listado |<>, <>, <>, <>, <>, <>, <>, <>, <> |Master |<> |Reporte ODT |<> |=== .Guía de características destacadas de XGAP usadas en la aplicación ``Localidades'' [options="header,autowidth",cols="2*>), fotolocalidad_formulario.xml (<>), fotolocalidad_listado.xml (<>), paginaapp_listado.xml (<>, <>, <>, <>, <>), pais_formulario.xml (<>), permiso_pagina_export_listado.xml (<>, <>), rol_compu_usu_formulario.xml (<>), usuario_cambioclave_formulario.xml (<>, <>), usuario_cambioclaveadmin_formulario.xml (<>, <>), usuario_formulario.xml (<>, <>), usuario_sinclave_formulario.xml (<>, <>) |Comando ``Volver'' con destino variable |<>, <> |Comando ``Volver'' condicional |<>, error_formulario_contenido.xml |Contenido de página personalizado |<>, <>, error_contenido.xml, error_formulario_contenido.xml, fotolocalidad_contenido.xml, <>, login_contenido.xml |Contenido extra en la cabecera HTML |<>, index_admin_contenido.xml, login_contenido.xml, fotolocalidad_contenido.xml |Especificación de variedades de página a generar |<>, <>, <>, <>, <>, <>, <> |Formato personalizado en columna de listado |<> |Formulario con campo de tipo `ComboBD` o `RadioBD` |<>, <>, <> |Formulario con campo de tipo `SeleccionableMultiple` |<>, <> |Formulario con operación personalizada |paginaapp_formulario.xml (<>), rol_compu_usu_formulario.xml (<>) |Listado con acciones que operan sobre filas seleccionadas |<> |Listado con buscador personalizado |<>, <>, <> |Listado con columnas condicionales |<> |Listado con columnas de imágenes |<>, <> |Listado con columnas de acciones |<> |Mensajes de página |<> |Página con acciones generales |<>, <> |Uso de XInclude |<>, <>, <> |=== [[acercade_contenido_xml]] ==== acercade_contenido.xml ==== Página que muestra información acerca de la aplicación. .Captura de pantalla de acercade_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/acercade_contenido.png[] [[feature-head-extra-css]]Contenido extra en la cabecera HTML:: Se agregan reglas CSS adicionales que definen estilos específicos para esta página. + [source,xml] ---- .... .... ]]> ---- + [TIP] ==== Una desventaja de incluir el código CSS en el fuente XML es que los estilos especificados permanecen fijos por más que se cambie el skin en uso. En lugar de hacerlo de esta manera, se puede cargar CSS adicional incluyendo uno o más archivos externos, a través del elemento `configuracion/inclusiones/archivo`. Si la ruta incluye la constante `XGAP_CONF_SKIN_DIR`, el archivo cargado va a depender del skin en uso. Por ejemplo: [source,xml] ---- XGAP_CONF_SKIN_DIR . 'extras.css' ... ---- ==== [[acercade_contenido_xml-contenido]]Contenido de la página:: Hace uso de varias constantes predefinidas por XGAP, que contienen información de la aplicación, el motor y el entorno. + [source,php] ---- return !defined('XGAP_PAGINA_SIMPLE') || !XGAP_PAGINA_SIMPLE; ---- [[error_contenido_xml-destino_volver]]Comando ``Volver'' con destino variable:: El destino del comando ``Volver'' en esta página varía de acuerdo al estado del historial. Se usa `volver/codigo-destino` para seleccionar la página de retorno correcta. + [source,xml] ---- vacio() && strpos( $objeto_historial->actual()->uri(), basename($_SERVER['REQUEST_URI']) ) !== false) { $objeto_historial->remover(); } if (Request::existe('origen') && !$objeto_historial->vacio() && strpos( $objeto_historial->actual()->uri(), Request::obtener('origen') ) !== false) { $objeto_historial->remover(); } $retorno = !$objeto_historial->vacio() ? $objeto_historial->actual()->uri() : Configuracion::paginaInicio(); $retorno = 'location="' . htmlspecialchars($retorno) . '"'; return $retorno; ]]> ---- [[error_formulario_contenido_xml]] ==== error_formulario_contenido.xml ==== .Captura de pantalla de error_formulario_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/error_formulario_contenido.png[] Comando ``Volver'' condicional:: Ver <>. [[error_formulario_contenido_xml-destino_volver]]Comando ``Volver'' con destino variable:: El destino del comando ``Volver'' en esta página varía de acuerdo al estado del historial. Se usa `volver/codigo-destino` para seleccionar la página de retorno correcta. + En `error_formulario_contenido.xml`: + [source,xml] ---- ---- + En `error_formulario_contenido-anterior.inc.php`: + [source,php] ---- vacio() && strpos($objeto_historial->actual()->uri(), $cur) !== false) $objeto_historial->remover(); if (!$objeto_historial->vacio() && strpos($objeto_historial->actual()->uri(), $cur) !== false) $objeto_historial->remover(); $retorno = !$objeto_historial->vacio() ? $objeto_historial->primero(array($cur))->uri() : Configuracion::paginaInicio(); //... ---- [[fotolocalidad_contenido_xml]] ==== fotolocalidad_contenido.xml ==== .Captura de pantalla de fotolocalidad_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/fotolocalidad_contenido.jpg[] Esta página está construida principalmente con código PHP. El código en `fotolocalidad_contenido-anterior.inc.php` valida los parámetros de request y obtiene los datos necesarios desde la base de datos. El código en `fotolocalidad_contenido.inc.php` produce el HTML del cuerpo de la página. [[fotolocalidad_formulario_xml]] ==== fotolocalidad_formulario.xml ==== Formulario de alta/baja/modificación de foto de localidad. .Captura de pantalla de fotolocalidad_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/fotolocalidad_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... nlocalidad <1> varchivo Archivo Archivo de imagen <2> vubicacion Ubicación Lugar o dirección donde se tomó la foto. <3> tdescripcion Descripción ---- <1> `fotolocalidad.varchivo character varying(2048) NOT NULL`. El nombre y destino del archivo subido se construye en <>. <2> `fotolocalidad.vubicacion character varying(100) NOT NULL`. <3> `fotolocalidad.tdescripcion text`. -- + image::examples/localidades/screenshots/fotolocalidad_formulario-campos.png[] [[fotolocalidad_formulario_xml-antes_procesar_uploads]]Código extra PHP en `antes_procesar_uploads`:: Modifica la ruta y nombre del archivo subido: + -- * Los archivos se guardan separados por localidad. La ruta tiene la forma `{XGAP_CONF_UPLOAD_DIR}/localidad/fotos/{nlocalidad}/`. * El nombre de cada archivo se prefija con la fecha y hora de alta, y el identificador del usuario que la realiza. -- + [source,php] ---- cambiarDirDestino( ruta_archivo(array('localidad', 'fotos', $params['registro']['nlocalidad'])), true ); $upload->cambiarNombreArchivo( date('YmdHis') . '-' . Contexto::obtener('ident_usuario') .'-' . $upload->nombreArchivo() ); } ---- [[fotolocalidad_listado_xml]] ==== fotolocalidad_listado.xml ==== Listado de fotos de localidades. .Captura de pantalla de fotolocalidad_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/fotolocalidad_listado.png[] [[fotolocalidad_listado_xml-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> Seleccione las fotos a imprimir fotosimp <2> Foto varchivo fotolocalidad_contenido.php <3> Ubicación Lugar o dirección donde se tomó la foto vubicacion <4> Descripción tdescripcion ---- <1> Columna de checkboxes, usada para seleccionar las filas a incluir en la acción <>. <2> `fotolocalidad.varchivo character varying(2048)`. Columna de imágenes, que muestra miniaturas de las fotos y permite abrir <>. <3> `fotolocalidad.vubicacion character varying(100)`. <4> `fotolocalidad.tdescripcion text`. -- + image::examples/localidades/screenshots/fotolocalidad_listado-columnas.png[] [[fotolocalidad_listado_xml-variedades]]Especificación de variedades a generar:: Sólo se genera el listado normal, dado que las demás variedades no se usan. + [source,xml] ---- ---- [[fotolocalidad_listado_xml-accion_imprimir_seleccionadas]]Acción sobre filas seleccionadas:: El listado provee la acción ``Imprimir seleccionadas'', que genera el reporte ODT `fotolocalidad_reporte_odt` con las filas seleccionadas por el usuario a través de los checkboxes en la columna `fotosimp`. + -- [source,xml] ---- Imprimir seleccionadas fotolocalidad_reporte_odt.php <1> fotosimp <2> fotosimp <3> ---- <1> La acción realiza un request a `fotolocalidad_reporte_odt.php`. <2> Se pasa como parámetro la columna de tipo `checkbox` `fotosimp`. El resultado es que el request lleva un parámetro con nombre `fotosimp` que tiene como valor la lista de filas seleccionadas, dada como una secuencia de los valores correspondientes de `nfotolocalidad` (`/listado/clave/campo`) separados por comas. <3> Después de ejecutar la acción se reinicia el estado de selección los checkboxes. -- [[fotolocalidad_listado_xml-despues_inicializacion]]Código extra PHP en `despues_inicializacion`:: Agrega el nombre de la ciudad al título de la página. + [source,php] ---- obtenerPrimero($sql); if (!empty($localidad)) { $params['titulo'] .= ' – ' . preparar_salida($localidad); } } ---- [[fotolocalidad_reporte_odt_xml]] ==== fotolocalidad_reporte_odt.xml ==== Reporte ODT invocado desde la acción <> en `fotolocalidad_listado_xml`. .Primera página de un reporte generado por fotolocalidad_reporte_odt.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/fotolocalidad_reporte_odt.png[] [source,xml] ---- <3> es 1 P0D <4> array('default' => 0, 'min_range' => 1)) ); } $var_nlocalidad = sanitize_db_serial($var_nlocalidad); <5> $html_entity_decode_flags = ENT_QUOTES; <6> if (defined('ENT_XHTML')) { // PHP >= 5.4.0 $html_entity_decode_flags |= constant('ENT_XHTML'); } ]]> <7> ',', array_filter( array_map( 'sanitize_db_serial', explode( ',', $var_fotosimp_nfotolocalidad ) ) ) ); if (!empty2($var_fotosimp_nfotolocalidad, false, true)) { <9> ]]> <10> <11> $var_archivo = realpath( ruta_archivo(array(Upload::dirBase(), $var_archivo), DIR_SEP, false) ); <13> if (!$var_archivo) { $var_archivo = ''; } ]]> <14> <![CDATA[{$var_ubicacion}]]> <![CDATA[{$var_descripcion}]]> <![CDATA[{$var_archivo}]]> ]]> $odt_xml_node_fotos = ''; } ]]> <16> <![CDATA[{$var_nombrecompletolocalidad}]]> <![CDATA[{$var_descripcionlocalidad}]]> {$odt_xml_node_fotos} ]]> ---- <1> La plantilla del reporte es el archivo `templates/fotolocalidad.odt`. <2> Se incluyen los templates xslt predefinidos, para usarlos en `fotolocalidad.odt`, como se muestra en la <>. <3> Se personalizan los metadatos que va a contener el ODT generado. <4> Se definen variables a partir de los parámetros de request, para usar más adelante. <5> Saneado de una variable con valor proveniente del request. La otra variable definida hasta ese punto se procesa más adelante. <6> Más adelante será necesario usar `html_entity_decode()`, dentro de un iterador. Aquí se define el valor de su segundo parámetro. <7> Consulta para obtener los datos necesarios acerca de la localidad solicitada, cuya clave está dada por la variable `nlocalidad` que se definió a partir del request. Con el resultado de esta consulta se definen las variables `nombrecompletolocalidad` y `descripcionlocalidad`. <8> Saneado de una variable con valor proveniente del request. En este caso se asegura que el valor se pueda utilizar en una cláusula SQL `IN` sin riesgo de inyección de código. <9> Sólo se debe hacer la consulta para obtener los datos de las fotos si la especificación de sus claves no es vacía. <10> Iterador para construir los datos de las fotos solicitadas. <11> Consulta para obtener los datos de las fotos solicitadas, cuyas claves se recibieron en el request y se sanearon en ➑. Para cada fila se asignan valores a las variables `archivo`, `ubicacion` y `descripcion`, que se usan a continuación. <12> La descripción de la foto tiene formato HTML con entidades codificadas. Aquí se eliminan los elementos HTML y se decodifican las entidades, para que se pueda incluir sin problemas en el ODT. <13> Se construye la ruta completa al archivo de imagen. <14> Para cada fila retornada por la consulta, se construye un fragmento de XML con los valores de esa fila. El fragmento XML final para el iterador resulta de la concatenación de los fragmentos de cada iteración. <15> `else` del `if` en ➒: si no hay claves de fotos a obtener, el fragmento de XML correspondiente queda vacío. <16> El elemento `xml` principal define el XML final que se procesa con la plantilla dada en ➊. Usa las variables definidas en ➐ y el fragmento de XML creado por el iterador ➓ (o el fragmento vacío en ⓯). El XML generado por el código descripto, que produce el reporte mostrado al comienzo de esta sección, es el siguiente: [source,xml] ---- ---- [[fotolocalidad_odt]]La plantilla `templates/fotolocalidad.odt` contiene los elementos que se describen a continuación, los cuales construyen el ODT final a partir del XML generado. .Elementos en la plantilla fotolocalidad.odt, en la aplicación ``Localidades'' image::examples/localidades/screenshots/fotolocalidad_odt.png[] [green]#➀# y [green]#➁# son campos placeholder de tipo text (Insert -> Fields -> More Fields; Pestaña Functions, Type=Placeholder, Format=Text). [green]#➂#, [green]#➃# y [green]#➄# son scripts con tipo `ODF-XSLT` (Insert -> Script; Script type=`ODF-XSLT`). ifdef::basebackend-html[] [horizontal] endif::basebackend-html[] [green]#➀#:: Campo placeholder con valores Placeholder=`LOCALIDAD` y Reference=`/localidad/nombre`. [green]#➁#:: Campo placeholder con valores Placeholder=`UBICACION` y Reference=`ubicacion`. La referencia es relativa porque el campo se encuentra dentro del <>, con lo cual el elemento de contexto en este punto es `/localidad/foto`. [green]#➂#:: Script para incluir la descripción de la localidad: + -- [source,xml] ---- {@before ancestor::text:p[1] <1> } {@after ancestor::text:p[1] <2> } {@replace . <3> } ---- <1> Se evita emitir el párrafo que corresponde a la descripción de la localidad cuando ésta es vacía. <2> Cierre del `xslt:if` anterior. <3> Se reemplaza el nodo actual con la descripción de la localidad. -- [green]#➃#:: Script para incluir la descripción de la foto: + -- [source,xml] ---- {@replace . <1> (sin descripción) <2> } ---- <1> Se reemplaza el nodo actual con la descripción de la foto. <2> Si la foto no tiene descripción, se incluye un texto por defecto. -- [green]#➄#:: Script para recorrer las fotos incluidas y referenciar el archivo de imagen de cada una: + -- [source,xml] ---- {@before //table:table[@table:name="TablaFoto"] <1> } {@after //table:table[@table:name="TablaFoto"] <2> } {@child //draw:frame[@draw:name="foto"]/draw:image <4> <3> <4> } ---- <1> [[fotolocalidad_odt__foreach_foto]]Ciclo para procesar cada foto incluida, que encierra la tabla con nombre `TablaFoto`. El resultado del ciclo es que esta tabla se repite para cada una de las fotos incluidas, en tanto que sus contenidos se reemplazan con los valores propios de la foto por medio de los placeholders y scripts que quedan dentro del ciclo. <2> Cierre del `xslt:for-each` anterior. <3> Si no hay un archivo, se mantiene la imagen original del template. <4> Se agrega un atributo `xlink:href` que apunta al archivo de la foto, como hijo del elemento `draw:image` correspondiente a la imagen insertada en la plantilla ODT con nombre `foto`. -- [green]#➅#:: El contenido del pie de página no depende de los valores recibidos en el XML de entrada; sólo se incluyeron campos comunes, no procesados por la transformación XSLT. Notar que en [green]#➂# y [green]#➃# se hace un llamado al template XSLT `eol-to-line-break`, para preservar en el documento generado los saltos de línea que pudiera haber en el texto origen (`/localidad/descripcion` y `/localidad/foto/descripcion`, respectivamente); si no se hiciera así, los saltos de línea serían ignorados. Dicho template está disponible porque en `fotolocalidad_reporte_odt.xml` se indica que se deben incluir los templates predefinidos por XGAP (`/pagina/configuracion/template/xslt-adicional/@incluir-templates-predefinidos="true"`). [[guardarindicepaginas_contenido_xml]] ==== guardarindicepaginas_contenido.xml ==== Actualiza el índice de páginas en la base de datos (tabla `seguridad.pagina`). La implementación de la funcionalidad de la página se encuentra en el archivo `extras/guardarindicepaginas_contenido-anterior.inc.php`; el código que genera la salida HTML está en `extras/guardarindicepaginas_contenido.inc.php`. .Captura de pantalla de guardarindicepaginas_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/guardarindicepaginas_contenido.png[] [[guardarindicepaginas_contenido_xml-mensajes_pagina]]Uso de mensajes de página:: XGAP provee un mecanismo para registrar mensajes que se deben mostrar al usuario en un request particular a una página. Esta página lo utiliza para informar cualquier problema que pudiera producirse durante la ejecución de su funcionalidad principal. + En `guardarindicepaginas_contenido-anterior.inc.php`: + [source,php] ---- mensajes()->agregar(ObjetoMensaje::nuevo( 'No se pudieron limpiar las páginas sin permisos.', Mensaje::TIPO_ALERTA) ); } //... if ($n_errores > 0) { Pagina::instancia()->mensajes()->agregar(ObjetoMensaje::nuevo( "Hubo errores en la actualización de $n_errores páginas.", Mensaje::TIPO_ALERTA) ); } //... } else { Pagina::instancia()->mensajes()->agregar(ObjetoMensaje::nuevo( 'No se encuentra el índice de páginas.', Mensaje::TIPO_ERROR) ); } ---- + En `guardarindicepaginas_contenido.inc.php`: + [source,php] ---- mensajes()->isEmpty()) { Mensaje::mostrarLista( Pagina::instancia()->mensajes(), true, null, 'cont-msjs-pag' ); } //... ---- + El último fragmento de código se encarga de mostrar la lista de mensajes que fueron agregados en el request actual. Este código ya se encuentra incluido en los otros tipos de página, pero en las de tipo Contenido se debe agregar manualmente en el lugar deseado. [[guardarindicepaginas_contenido_xml-contenido]]Presentación de resultados:: El único contenido que se emite en el cuerpo de esta página es un reporte de resultados. + [source,php] ---- 0 || $actualizadas > 0 || $eliminadas > 0) { Mensaje::info( Html::elemento( 'p', Html::elemento( 'strong', 'Actualización terminada.', null, null, null, true ), null, 'first', null, true ) . Html::lista( array( $eliminadas == 0 ? 'No se eliminaron páginas.' : "Se eliminaron $eliminadas páginas.", $agregadas == 0 ? 'No se agregaron páginas.' : "Se agregaron $agregadas páginas.", $actualizadas == 0 ? 'No se actualizaron páginas.' : "Se actualizaron $actualizadas páginas." ), false, null, 'last', null, null, true ), null, 'icon' ); } else { Html::elemento( 'p', 'El índice ya estaba actualizado. No se realizaron cambios.' ); } ---- [[index_admin_contenido_xml]] ==== index_admin_contenido.xml ==== Página de inicio para el rol ADMINISTRADOR. Presenta un resumen de la cantidad de elementos que hay en las entidades principales del modelo de datos. .Captura de pantalla de index_admin_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/index_admin_contenido.png[] [TIP] ==== El código PHP de los elementos `/pagina/contenido-anterior` y `/pagina/contenido` se colocó en archivos `.php` separados, para simplificar su edición: si este código se edita como texto genérico dentro de un bloque XML CDATA, no se pueden aprovechar las facilidades específicas para el lenguaje PHP que ofrecen muchos editores, como resaltado de sintaxis y errores o autocompletado. Esta técnica se utiliza también en otros fuentes XML. [source,xml] ---- ]]> ---- Los archivos `index_admin_contenido-anterior.inc.php` y `index_admin_contenido-contenido.inc.php` se encuentran dentro del subdirectorio `extras`, para que se copien automáticamente al directorio de salida durante la generación de la aplicación. ==== Contenido extra en la cabecera HTML:: Ver <>. [[index_admin_contenido_xml-contenido]]Contenido de la página:: En `index_admin_contenido-anterior.inc.php`: + [source,php] ---- obtenerPrimero( 'SELECT COUNT(npais) FROM pais' ); $n_provincias = $objeto_conexion->obtenerPrimero( 'SELECT COUNT(nprovincia) FROM provincia' ); $n_localidades = $objeto_conexion->obtenerPrimero( 'SELECT COUNT(nlocalidad) FROM localidad' ); $n_fotos = $objeto_conexion->obtenerPrimero( 'SELECT COUNT(nfotolocalidad) FROM fotolocalidad' ); ---- + En `index_admin_contenido-contenido.inc.php`: + [source,php] ---- $v) { $resumen[$n] = Html::elemento ( 'em', preparar_salida("{$v[0]}:", array(), null, true, true), null, null, null, true ) . ' ' . $v[1]; } $info .= Html::abrirDiv('resumen', null, null, true) . Html::elemento('h2', 'Resumen', null, null, null, true) . Html::lista($resumen, false, null, null, null, null, true) . Html::cerrarDiv(true); } print $info; //... ---- [[localidad_formulario_xml]] ==== localidad_formulario.xml ==== Formulario de alta/baja/modificación de localidad. .Captura de pantalla de localidad_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/localidad_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... <1> Provincia nprovincia provincia vprovincia vnombrecompleto nprovincia <2> Nombre vnombre <3> Prefijo telefónico cpreftelef <4> Descripción tdescripcion ---- <1> `localidad.nprovincia integer NOT NULL`. El Seleccionable abre `provincia_listado_seleccion.php`. <2> `localidad.vnombre character varying(100) NOT NULL`. <3> `localidad.cpreftelef character(5)`. <4> `localidad.tdescripcion text`. -- + image::examples/localidades/screenshots/localidad_formulario-campos.png[] [[localidad_listado_xml]] ==== localidad_listado.xml ==== Listado de Localidades. .Captura de pantalla de localidad_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/localidad_listado.png[] Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> País vnombrepais <2> Provincia vnombreprovincia <3> Nombre vnombre <4> Pref. tel. Prefijo telefónico cpreftelef <5> Descripción tdescripcion ---- <1> `vlocalidad.vnombrepais character varying(100)`. <2> `vlocalidad.vnombreprovincia character varying(50)`. <3> `vlocalidad.vnombre character varying(100)`. La columna provee acceso al master de Localidad, dado que incluye `@llevaAForm="true"` y `/listado/entidad/@master="si"`. <4> `vlocalidad.cpreftelef character(5)`. <5> `vlocalidad.tdescripcion text`. Se limita la longitud máxima del texto que se puede presentar en esta columna, haciendo uso del elemento `columna/recortar`. -- + image::examples/localidades/screenshots/localidad_listado-columnas.png[] [[localidad_listado_xml-variedades]]Especificación de variedades a generar:: Sólo se genera el listado normal y la exportación en PDF, ODS y CSV. + [source,xml] ---- ---- [[localidad_listado_xml-buscador]]Buscador personalizado:: El buscador del listado se personaliza para ofrecer como criterios de búsqueda los nombres de país, provincia y localidad. + -- [source,xml] ---- <1> <2> <3> vnombrepais pvnombrepais <3> vnombreprovincia pvnombreprovincia <3> vnombre pvnombre ---- <1> Los campos del buscador se definen en el elemento `personalizado-simple`. Este buscador, en particular, contiene tres campos de texto. <2> La consulta incluye el elemento `condiciones_de_parametros`, para filtrar los resultados de acuerdo a los valores que haya en los campos del buscador. Se define una condición para cada uno de los tres campos y se combinan las condiciones con AND para que el filtro considere simultáneamente todos los criterios que ingrese el usuario. <3> Las tres condiciones comparan una columna de la consulta (`col`) con el valor de una variable (`param`), definida en este caso por el campo correspondiente. El contenido del elemento `param` hace referencia al nombre del campo. -- [[localidad_master_xml]] ==== localidad_master.xml ==== Detalle de una localidad, con acceso a entidades dependientes. .Captura de pantalla de localidad_master.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/localidad_master.png[] Definición de campos:: Los campos que se muestran en el master están definidos como sigue: + [source,xml] ---- Provincia vnombreprovincia Nombre vnombre Prefijo telefónico cpreftelef Descripción tdescripcion ---- Acceso a entidades dependientes:: En este modelo, la entidad `localidad` tiene como dependiente a la entidad `fotolocalidad`. En el master se usa el elemento `details` para proveer acceso a las Fotos de la Localidad que se está consultando. + [source,xml] ----
Fotos fotolocalidad
---- + La especificación usada para `detail`, sin incluir el atributo `detail/@listado="true"`, produce como resultado un botón en la parte inferior de la página, que lleva al listado de la entidad indicada -- `fotolocalidad_listado.php` en este caso. El mismo resultado se obtiene incluyendo `detail/@tradicional="true"`. + image::examples/localidades/screenshots/localidad_master-boton_detail.png[] + También es posible presentar dentro del mismo master el listado de la entidad dependiente, agregando a la especificación el atributo `detail/@listado="true"` y opcionalmente usando el atributo `detail/@tipo-listado` para indicar el tipo de listado a usar. [[paginaapp_formulario_xml]] ==== paginaapp_formulario.xml ==== Formulario para establecer los roles que tienen permiso de acceso a una página de la aplicación. Debe recibir como parámetro el identificador de la página y opera sólo sobre las asociaciones entre ella y los roles, sin hacer modificaciones a los datos de la página en sí. .Captura de pantalla de paginaapp_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/paginaapp_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- naplicacion npagina naplicacion <1> Página npagina <2> Descripción tdescripcion <3> Roles con permiso id_rolfs rolf id_rolf desc_rolf permiso_pagina id_rolf naplicacion = '$naplicacion' AND npagina = '$npagina' ---- <1> `seguridad.pagina.npagina character(100) NOT NULL`. Se muestra como texto plano por ser de tipo "SoloLectura". <2> `seguridad.pagina.tdescripcion text`. Se muestra como texto plano por ser de tipo "SoloLectura". <3> Ver <>. -- + image::examples/localidades/screenshots/paginaapp_formulario-campos.png[] [[paginaapp_formulario_xml-seleccionable_multiple]]Campo SeleccionableMultiple:: Permite seleccionar los roles. + -- [source,xml] ---- Roles con permiso id_rolfs <1> rolf <2> id_rolf <3> desc_rolf <3> <3> permiso_pagina id_rolf naplicacion = '$naplicacion' AND npagina = '$npagina' ---- <1> El campo `id_rolfs` no existe en la base de datos; sus valores se obtienen como resultado de la ejecución de `campo/consulta-sm`. <2> La acción "Seleccionar" del campo abre `rolf_listado_seleccion_m.php`. <3> La consulta SQL definida por `campo/consulta-sm` indica los valores que tiene el campo cuando se carga el formulario. Los valores y claves de los items iniciales se obtienen a partir de una consulta SQL que combina `campo/consulta-sm`, `campo/tabla` o `campo/entidad`, `campo/descripcion` y `campo/seleccionar`: `SELECT id_rolf, desc_rolf FROM rolf WHERE id_rolf IN (SELECT DISTINCT id_rolf FROM permiso_pagina WHERE naplicacion = '$naplicacion' AND npagina = '$npagina')` (en general: `SELECT {campo/seleccionar}, {campo/descripcion} FROM {campo/tabla|campo/entidad} WHERE {campo/seleccionar} IN (SELECT DISTINCT {campo/consulta-sm/valor} FROM {campo/consulta-sm/tabla} WHERE {campo/consulta-sm/where})`). -- + El campo se genera como un elemento HTML `select` con el atributo `multiple` establecido y nombre `"{campo/dato}[]"`, es decir: + [source,html] ---- ---- + XGAP no provee un procesamiento predeterminado para los valores seleccionados en el campo, sino que es responsabilidad de la aplicación utilizarlos de acuerdo a la funcionalidad que se desee proveer. Cuando se envía el formulario y la página recibe la solicitud POST resultante, los valores de `id_rolf` correspondientes a los roles seleccionados quedan disponibles en un arreglo que se puede acceder en diversas ubicaciones de código personalizado, como por ejemplo en la variable `$registro['id_rolfs']` que está disponible en `/formulario/custom_update` (ver <>). // TODO: la explicación genérica de funcionamiento de un SeleccionableMultiple debería estar en una sección general de documentación de campos, no en el ejemplo. [[paginaapp_formulario_xml-custom_update]]Código de actualización personalizado (`/formulario/custom_update`):: Como se mencionó al comienzo de esta sección, la funcionalidad del formulario no consiste en actualizar un único registro de la base de datos, sino las asociaciones entre la página indicada y los roles seleccionados. Esto significa que se debe redefinir el comportamiento predeterminado de la operación de actualización; por otro lado, no es necesario alterar la operación de agregado, porque este formulario no se usa con ese fin. + Para realizar la actualización se hace uso del método `iSeguridad::actualizarPermisosPorPagina($aplicacion, $pagina, $roles_usuario, $operaciones)`, provisto por el objeto `$objeto_seguridad`. El parámetro `$aplicacion` corresponde al valor del campo `naplicacion`, el parámetro `$pagina` al campo `npagina` y el parámetro `$roles_usuario` al array construído con las claves de los roles seleccionados en el campo `id_rolfs`; el parámetro `$operaciones` no se utiliza en este caso. + [source,xml] ---- actualizarPermisosPorPagina( $registro['naplicacion'], $registro['npagina'], $registro['id_rolfs'], '' ); ]]> ---- [[paginaapp_listado_xml]] ==== paginaapp_listado.xml ==== Listado de páginas que componen la aplicación y permisos de acceso por página. .Captura de pantalla de paginaapp_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/paginaapp_listado.png[] [[paginaapp_listado_xml-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- seguridad.vpaginarolfs <1> Página npagina <2> Descripción tdescripcion <3> Tipo ctipo <4> Roles Roles con permiso de acceso a la página desc_rolfs ---- <1> `seguridad.vpaginarolfs.npagina character(100)`. <2> `seguridad.vpaginarolfs.tdescripcion text`. <3> `seguridad.vpaginarolfs.ctipo`. Ver <>. <4> `seguridad.vpaginarolfs.desc_rolfs text`. El valor de esta columna se construye en la vista `seguridad.vpaginarolfs`, como la concatenación de los nombres de los roles que tienen permiso de acceso a la página. Su definición es: + [source,sql] ---- SELECT ..., array_to_string( ARRAY( SELECT btrim(r.desc_rolf) AS desc_rolf FROM seguridad.permiso_pagina pp JOIN seguridad.rolf r USING (id_rolf) WHERE p.naplicacion = pp.naplicacion AND p.npagina = pp.npagina ORDER BY r.desc_rolf ), ', ') AS desc_rolfs FROM seguridad.pagina p; ---- -- + image::examples/localidades/screenshots/paginaapp_listado-columnas.png[] [[paginaapp_listado_xml-columna_formato]]Columna con formato personalizado:: En la columna `ctipo`, que corresponde al identificador interno de tipo de página, se usa `columna/formato/codigo` para mostrar la descripción del tipo en lugar de dicho identificador. El código de formato utiliza el array `$GLOBALS['tipos_pag']`, construído en <>, que mapea cada identificador con su descripción. + [source,php] ---- ---- [[paginaapp_listado_xml-buscador]]Buscador personalizado:: El buscador del listado se personaliza para permitir la búsqueda por Página, Descripción, Tipo y/o Roles. Los controles de búsqueda se implementan como campos de texto, excepto el Tipo, que se presenta como una lista desplegable. + -- [source,xml] ---- <1> <1> <2> tipos_pag <1> <1> npagina pnpagina <1> tdescripcion ptdescripcion <3> ctipo pctipo <1> desc_rolfs pdesc_rolfs ---- <1> Los valores de los campos `pnpagina`, `ptdescripcion` y `pdesc_rolfs` se aplican como filtro a las columnas `npagina`, `tdescripcion` y `desc_rolfs`, respectivamente, utilizando el operador SQL `LIKE`. <2> Los valores de la lista desplegable están dados por el array `$tipos_pag`, construído en <>. <3> El campo `pctipo` sólo permite la selección de un tipo por vez, por lo cual el filtro consiste en aplicar el operador SQL `=` entre el valor seleccionado y la columna `ctipo`. -- + La estructura general de la definición de un buscador personalizado se encuentra más detallada en la documentación de <>. [[paginaapp_listado_xml-acciones]]Acciones:: El listado provee dos acciones: + -- * [[paginaapp_listado_xml-accion_updind]]``Actualizar índice de páginas'' causa que se cargue la página <>, donde está implementada la actualización del índice de páginas en la base de datos. + [source,xml] ---- Actualizar índice de páginas guardarindicepaginas_contenido.php ---- * [[paginaapp_listado_xml-accion_exportar_permisos_csv]]``Exportar permisos a CSV'' permite exportar a formato CSV los permisos definidos para las páginas, haciendo una solicitud GET a la variedad CSV de <>. La solicitud incluye el parámetro `RequestXgap::PARAMETRO_DISPOSICION_CONTENIDO_IMPRIMIBLES` con valor `attachment`, lo que causa que el navegador ofrezca guardar o abrir el contenido de la respuesta, sin salir de la página actual. + [source,xml] ---- Exportar permisos a CSV Emite un archivo CVS que contiene todos los permisos definidos permiso_pagina_export_listado_csv.php ---- -- [[paginaapp_listado_xml-xgap_cargado]]Código extra PHP en `xgap_cargado`:: Incluye el archivo `clases/PropiedadesSistema.inc.php`, el cual define la clase `PropiedadesSistema`, que se utiliza en otras secciones de código. + [source,php] ---- . $params['conexion']->qstr(Request::obtener('aplicacion', XGAP_CONF_APLICACION)) . "'"; $GLOBALS['cte_seguridad'] = iSeguridad::PAGINA_USO_CON; // <1> Seguridad::obtenerInformacionSeguridad($opciones_pagina, $descripciones_pagina, $sufijos_pagina); $tipos_pag = array('' => 'Todas las Páginas'); asort($descripciones_pagina, SORT_STRING); foreach ($descripciones_pagina as $pag => $desc) { $tipos_pag[$pag] = $desc; } $GLOBALS['tipos_pag'] = $tipos_pag; // <2> ---- <1> Las variables `$aplicacion_q` y `$cte_seguridad` se usan en `consulta/condiciones_de_parametros` para que sólo se muestren las páginas que pertenecen a la aplicación indicada como parámetro `aplicacion` en la solicitud y que tienen seguridad habilitada (`iSeguridad::PAGINA_USO_CON`), respectivamente. + [source,xml] ---- naplicacion $aplicacion_q nseguridad $cte_seguridad ---- + TIP: En este caso se decidió utilizar `consulta/condiciones_de_parametros` para incluir en la consulta las dos condiciones mencionadas, pero también se podrían haber definido explícitamente en `consulta/where`. <2> Ya se mencionó la variable `$tipos_pag`, que se usa para definir los items en la lista desplegable del campo `pctipo` en el <> y para proveer la descripción del tipo en el <> de la columna `ctipo`. -- [[paginaapp_listado_xml-despues_inicializacion]]Código extra PHP en `despues_inicializacion`:: Determina si es necesario actualizar el índice de páginas y registra este hecho en la variable booleana `$index_update_needed`. + [source,php] ---- $time_last_index_update; ---- [[paginaapp_listado_xml-antes_tabla_datos]]Código extra PHP en `antes_tabla_datos`:: Si es necesario actualizar el índice de páginas, según lo indicado por la variable `$index_update_needed` definida en <>, agrega un mensaje de alerta para informar del hecho al usuario. + [source,php] ---- mensajes()->agregar( ObjetoMensaje::nuevo( 'Es necesario actualizar el índice de páginas.', Mensaje::TIPO_ALERTA ) ); } ---- ==== [[paginaapp_listado_xml-fin_body]]Código extra JAVASCRIPT en `fin_body`:: Si es necesario actualizar el índice de páginas, según lo indicado por la variable `$index_update_needed` definida en <>, invoca automáticamente la acción ``<>'' cuando se carga la página, previa confirmación por parte del usuario. + [source,javascript] ---- jQuery(document).ready(function() { var update_now = confirm( "El índice de páginas no está actualizado. ¿Desea actualizarlo ahora?" ); if (update_now) { jQuery('#b_accion_updind').click(); } }); ---- + image::examples/localidades/screenshots/paginaapp_listado-pregunta_upind.png[] [[pais_formulario_xml]] ==== pais_formulario.xml ==== Formulario de alta/baja/modificación de país. .Captura de pantalla de pais_formulario.php, presentado para modificación, en la aplicación ``Localidades'' image::examples/localidades/screenshots/pais_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... npais <1> Continente ncontinente continente ncontinente vnombre vnombre <2> Nombre vnombre <3> Código ccod2 <4> Prefijo telefónico cpreftelef unsigned <5> vbandera Bandera Archivo de imagen para la bandera pais <6> vhimno Himno Archivo de audio para el himno pais ---- <1> [[pais_formulario_xml-campo_ncontinente]]Valor: `pais.ncontinente integer`. Items en la lista desplegable: `continente.ncontinente integer` para los valores; `continente.vnombre character varying(50)` para las etiquetas. + La definición usa `campo/opcion-combobd-seleccion-vacia/@texto` para establecer explícitamente el texto del item en la lista que indica que no se selecciona un valor. + image::examples/localidades/screenshots/pais_formulario-campo_ncontinente.png[] <2> `pais.vnombre character varying(100) NOT NULL`. El ancho del campo de texto se limita a 60 caracteres, en vez de los 100 que tomaría por defecto de acuerdo a la definición de la columna en la base de datos, para que no ocupe demasiado espacio horizontal. <3> `pais.ccod2 character(2)`. <4> `pais.cpreftelef character(3)`. <5> `pais.vbandera character varying(2048)`. <6> `pais.vhimno character varying(2048)` La ruta del archivo subido, tanto para este campo como para el anterior, se modifica en <>. -- + image::examples/localidades/screenshots/pais_formulario-campos.png[] [[pais_formulario_xml-antes_procesar_uploads]]Código extra PHP en `antes_procesar_uploads`:: Modifica la ruta de los archivos subidos, concatenándole la clave primaria del país, para que los archivos de cada país queden almacenados en un directorio separado. + [source,php] ---- siguienteId('pais_npais_seq'); $params['registro']['npais'] = $npais; } else { $npais = $params['registro']['npais']; } foreach ($params['campos'] as $dato => $info) { if ($info['conservar'] === false && isset($info['upload'])) { $nuevo_dir_destino = $info['destino'] != '' ? ruta_archivo(array($info['destino'], $npais), DIR_SEP, false) : "pais-$npais"; $info['upload']->cambiarDirDestino($nuevo_dir_destino, true); } } ---- [[pais_listado_xml]] ==== pais_listado.xml ==== Listado de países. .Captura de pantalla de pais_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/pais_listado.png[] [[pais_listado_xml-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> Nombre vnombre <2> Código ccod2 <3> Pref. Prefijo telefónico cpreftelef <4> vhimno Himno <5> vbandera Bandera ---- <1> `pais.vnombre character varying(100)`. <2> `pais.ccod2 character(2)`. <3> `pais.cpreftelef character(3)`. <4> `pais.vhimno character varying(2048)`. <5> `pais.vbandera character varying(2048)`. Columna de imágenes, que muestra miniaturas de las banderas, con una altura de 32px. -- + image::examples/localidades/screenshots/pais_listado-columnas.png[] [[pais_listado_xml-variedades]]Especificación de variedades a generar:: Se incluye la generación de CSV y se excluye la de XLS. + [source,xml] ---- ---- [[permiso_pagina_export_listado_xml]] ==== permiso_pagina_export_listado.xml ==== Listado usado para exportar a formato CSV los permisos de páginas de la aplicación. La generación no incluye ninguna variante HTML, dado que la única función de este listado es proveer exportación de datos. [[permiso_pagina_export_listado_xml-conscols]]Definición de consulta y columnas:: La consulta obtiene todas las filas de la tabla `seguridad.permiso_pagina` que corresponden a la aplicación actual, mientras que las columnas del listado están definidas como un mapeo directo a las columnas que se quieren exportar de dicha tabla. + -- [source,xml] ---- seguridad.permiso_pagina naplicacion = '{$GLOBALS['naplicacion']}' <1> npagina, id_rolf naplicacion naplicacion npagina npagina id_rolf id_rolf ---- <1> La variable `$GLOBALS['naplicacion']` se define en <>. -- [[permiso_pagina_export_listado_xml-variedades]]Especificación de variedades a generar:: La salida de este listado está limitada a la variedad CSV. + [source,xml] ---- ---- [[permiso_pagina_export_listado_xml-antes_consulta]]Código extra PHP en `antes_consulta`:: Define la variable global `$naplicacion`, que se usa para filtrar la <>. + [source,php] ---- ... <1> País npais pais npais vnombre <2> Nombre vnombre <3> Código ccodigo ---- <1> `provincia.npais integer NOT NULL`. <2> `provincia.vnombre character varying(50) NOT NULL`. <3> `provincia.ccodigo character(2)`. -- + image::examples/localidades/screenshots/provincia_formulario-campos.png[] [[provincia_listado_xml]] ==== provincia_listado.xml ==== Listado de provincias. .Captura de pantalla de provincia_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/provincia_listado.png[] [[provincia_listado_xml-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> País vnombrepais <2> Nombre vnombre <3> Código ccodigo ---- <1> `vprovincia.character varying(100)`. <2> `vprovincia.character varying(50)`. <3> `vprovincia.ccodigo character(2)`. -- + image::examples/localidades/screenshots/provincia_listado-columnas.png[] [[provincia_listado_xml-variedades]]Especificación de variedades a generar:: Se incluye la generación de CSV y se excluye la de XLS. + [source,xml] ---- ---- [[provincia_listado_xml-buscador]]Buscador personalizado:: El buscador del listado se personaliza para permitir la búsqueda en cada una de las columnas por separado. + [source,xml] ---- vnombrepais pvnombrepais vnombre pvnombre ccodigo pccodigo ---- + Los valores de los campos `pvnombrepais` y `pvnombre` se aplican como filtro a las columnas `vnombrepais` y `vnombre`, respectivamente, utilizando el operador SQL `LIKE`, en tanto que el valor del campo `pccodigo` se compara con la columna `ccodigo` mediante el operador SQL `=`. + La estructura general de la definición de un buscador personalizado se encuentra más detallada en la documentación de <>. [[rol_compu_usu_formulario_xml]] ==== rol_compu_usu_formulario.xml ==== Formulario de modificación de roles funcionales asignados a un usuario. .Captura de pantalla de rol_compu_usu_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/rol_compu_usu_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... id_rolh id_rolv <1> id_usuario Usuario usuario id_usuario nombre <2> id_rolfs Roles rolf id_rolf desc_rolf Seleccionar rol_compu_usu id_rolf id_usuario = $id_usuario ---- <1> Campo `SoloLectura` que muestra el valor de `seguridad.usuario.nombre` correspondiente a `seguridad.usuario.id_usuario = seguridad.vrol_compu_usu.id_usuario`, por el uso de `campo/consulta`. <2> [[rol_compu_usu_formulario_xml-seleccionable_multiple]]Campo `SeleccionableMultiple` que permite elegir los roles del usuario. Los valores seleccionados se procesan en las <> del formulario. + La documentación del `SeleccionableMultiple` en <> contiene una explicación más detallada sobre el funcionamiento de este tipo de campo. -- + image::examples/localidades/screenshots/rol_compu_usu_formulario-campos.png[] [[rol_compu_usu_formulario_xml-inicio_pagina]]Código extra PHP en `inicio_pagina`:: Define una función que se usa en las <>, para evitar la duplicación de código. + [source,php] ---- AutoExecute( 'seguridad.vrol_compu_usu', $registro, 'INSERT' ); ]]> AutoExecute( 'seguridad.vrol_compu_usu', $registro, UPDATE', $xclave ); ]]> ---- + El proceso se completa en la base de datos, a través de dos reglas asociadas a la vista `seguridad.vrol_compu_usu`, las cuales se encargan de registrar las asociaciones de acuerdo a los valores del campo `roles` del registro. + [source,sql] ---- CREATE FUNCTION seguridad.fins_rol_compu_usu( pid_usuario bigint, pid_rolh integer, pid_rolv integer, proles character) RETURNS void AS $BODY$ DECLARE i integer; rol_f bpchar; rol_f_int integer; BEGIN i := 1; rol_f := split_part(proles, '|', i); WHILE ((NOT rol_f IS NULL) AND (rol_f <> '')) LOOP rol_f_int = CAST(rol_f AS INTEGER); INSERT INTO rol_compu_usu(id_rolh, id_rolv, id_rolf, id_usuario) VALUES (pid_rolh, pid_rolv, rol_f_int, pid_usuario); i := i + 1; rol_f := split_part(proles, '|', i); END LOOP; END; $BODY$ LANGUAGE 'plpgsql' VOLATILE; CREATE RULE ri_vrol_compu_usu AS ON INSERT TO seguridad.vrol_compu_usu DO INSTEAD SELECT seguridad.fins_rol_compu_usu( new.id_usuario::bigint, new.id_rolh, new.id_rolv, new.roles::bpchar ) AS fins_rol_compu_usu; CREATE RULE ru_vrol_compu_usu AS ON UPDATE TO seguridad.vrol_compu_usu DO INSTEAD ( DELETE FROM seguridad.rol_compu_usu WHERE rol_compu_usu.id_usuario = old.id_usuario; SELECT seguridad.fins_rol_compu_usu( new.id_usuario::bigint, new.id_rolh, new.id_rolv, new.roles::bpchar ) AS fins_rol_compu_usu; ); ---- [[rol_compu_usu_listado_xml]] ==== rol_compu_usu_listado.xml ==== Listado de asignaciones de roles funcionales a usuarios. .Captura de pantalla de rol_compu_usu_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/rol_compu_usu_listado.png[] [[rol_compu_usu_listado-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> Usuario nombre <2> Roles desc_rolfs ---- <1> `seguridad.vrol_compu_usu.nombre character(10)`. <2> `seguridad.vrol_compu_usu.desc_rolfs text`. Esta columna muestra la lista de todos los roles asignados al usuario. Su definición en la vista es la siguiente: + [source,sql] ---- SELECT -- ... array_to_string( ARRAY(SELECT btrim(rc.desc_rol_comp) AS desc_rol_comp_trim FROM seguridad.rol_compu_usu rcu1 JOIN seguridad.rol_compuesto rc ON rcu1.id_rolv = rc.id_rolv AND rcu1.id_rolh = rc.id_rolh AND rcu1.id_rolf = rc.id_rolf WHERE rcu1.id_usuario = u.id_usuario ORDER BY rc.desc_rol_comp), ', ' ) AS desc_rolfs -- ... ---- -- + image::examples/localidades/screenshots/rol_compu_usu_listado-columnas.png[] [[rolf_formulario_xml]] ==== rolf_formulario.xml ==== Formulario de alta y modificación de roles funcionales. .Captura de pantalla de rolf_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/rolf_formulario.png[] [[rolf_formulario_xml-campos]]Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... <1> Descripción del rol desc_rolf <2> Página de inicio pagina_inicio ---- <1> `seguridad.rolf.desc_rolf character varying(30) NOT NULL`. <2> `seguridad.rolf.pagina_inicio character varying(1024)`. -- [[rolf_listado_xml]] ==== rolf_listado.xml ==== Listado de roles funcionales. .Captura de pantalla de rolf_listado.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/rolf_listado.png[] [[rolf_listado_xml-columnas]]Definición de columnas:: Las columnas del listado están definidas como sigue: + -- [source,xml] ---- <1> Descripción desc_rolf <2> Página de inicio pagina_inicio ---- <1> `seguridad.rolf.desc_rolf character varying(30)`. <2> `seguridad.rolf.pagina_inicio character varying(1024)`. -- [[rolf_listado_xml-variedades]]Especificación de variedades a generar:: En este listado se debe incluir la generación de la variedad `seleccion_m`, que se utiliza en <> y <>. + [source,xml] ---- ---- [[seguridad_contenido_xml]] ==== seguridad_contenido.xml ==== Página para establecer los permisos de página por rol funcional. .Captura de pantalla de seguridad_contenido.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/seguridad_contenido.png[] [[seguridad_contenido_xml-comando_b_aplicar]]Comandos:: El comando ``Establecer Permisos'' realiza la actualización de los permisos seleccionados por el usuario. + -- [source,xml] ---- <1> <2> <3> ---- <1> El código del comando realiza el envío del formulario `form_seguridad`, causando una solicitud POST a la misma página, que se procesa en el código incluido en `pagina/contenido-anterior`. <2> El código en el elemento `comando/condicion` hace que el comando no se muestre si hubo algún error durante el procesamiento de la solicitud. El valor de la variable `$errores` se establece en el código incluido en `pagina/contenido-anterior`. <3> El código en el elemento `comando/preparar` deshabilita el comando si no hay un rol seleccionado. El valor de la variable `$hay_rol` se establece en el código incluido en `pagina/contenido-anterior`. -- [[usuario_cambioclave_formulario_xml]] ==== usuario_cambioclave_formulario.xml ==== Formulario de cambio de contraseña del usuario actual. .Captura de pantalla de usuario_cambioclave_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/usuario_cambioclave_formulario.png[] Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... id_usuario ident_usuario <1> Nombre Corto nombre <2> Nombre Completo nombre_ext <3> Contraseña contrasenia <4> Contraseña (nuevamente) contrasenia2 ---- <1> `seguridad.usuario.nombre character(10)`. <2> `seguridad.usuario.nombre_ext character varying(30)` <3> `seguridad.usuario.contrasenia character varying(40)` <4> No corresponde a una columna en la base de datos. Sólo se utiliza para <> ingresada. -- + image::examples/localidades/screenshots/usuario_cambioclave_formulario-campos.png[] [[usuario_cambioclave_formulario_xml-despues_inicializacion]]Código extra PHP en `despues_inicializacion`:: Asegura que el formulario no se pueda presentar en modo de agregado. + [source,php] ---- >. [[usuario_cambioclave_formulario_xml-xinclude]]Uso de XInclude:: Este formulario incluye desde `usuario_formulario.xml` los elementos que ambos tienen en común, mediante https://www.w3.org/TR/xinclude/[XInclude], para minimizar la repetición de código. + [source,xml] ---- ---- [[usuario_cambioclaveadmin_formulario_xml]] ==== usuario_cambioclaveadmin_formulario.xml ==== Formulario de cambio de contraseña de un usuario. Este formulario es muy similar a <>. La única diferencia sustancial entre ambos es que `usuario_cambioclave_formulario.xml` define un campo de tipo `Sesion` para establecer el valor del identificador de usuario (y por lo tanto siempre corresponde al usuario actual), en tanto que `usuario_cambioclaveadmin_formulario.xml` recibe este dato a través de un parámetro de la solicitud. [[usuario_cambioclaveadmin_formulario_xml-xinclude]]Uso de XInclude:: Para aprovechar la similitud mencionada, la mayor parte del código de `usuario_cambioclaveadmin_formulario.xml` se obtiene de `usuario_formulario.xml` y `usuario_cambioclave_formulario.xml` por medio de elementos https://www.w3.org/TR/xinclude/[XInclude]: + [source,xml] ---- ---- [[usuario_cambioclaveadmin_formulario_xml-despues_inicializacion]]Código extra PHP en `despues_inicializacion`:: Definido en <>. [[usuario_cambioclaveadmin_formulario_xml-fin_body]]Código extra JavaScript en `fin_body`:: Definido en <>. [[usuario_formulario_xml]] ==== usuario_formulario.xml ==== Formulario de alta y modificación de usuario. .Captura de pantalla de usuario_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/usuario_formulario.png[] [[usuario_formulario_xml-campos]]Definición de campos:: Los campos del formulario están definidos como sigue: + -- [source,xml] ---- ... <1> Nombre corto nombre Nombre para el ingreso al sistema. Sólo debe usar letras, números o guiones bajos. alphanum <2> Nombre completo nombre_ext Apellido y nombre completos <3> Contraseña contrasenia <4> Contraseña (nuevamente) contrasenia2 <5> Habilitado habilitado sistema.traduccionboolean bvalor vvalortraducido vnombre = 'SINO' ---- <1> `seguridad.usuario.nombre character(10)`. <2> `seguridad.usuario.nombre_ext character varying(30)` <3> `seguridad.usuario.contrasenia character varying(40)` <4> No corresponde a una columna en la base de datos. Sólo se utiliza para <> ingresada. <5> [[usuario_formulario_xml-campo_habilitado]] Valor: `seguridad.usuario.habilitado boolean NOT NULL`. Opciones: `sistema.traduccionboolean.bvalor boolean` para los valores; `sistema.traduccionboolean.vvalortraducido character varying(50)` para las etiquetas. -- + image::examples/localidades/screenshots/usuario_formulario-campos.png[] [[usuario_formulario_xml-xgap_cargado]]Código extra PHP en `xgap_cargado`:: Establece un valor por defecto para el usuario, cuando el formulario se presenta en modo de modificación. Ver comentario en el código. + [source,php] ---- <1> Nombre corto nombre usuario_sinclave_formulario.php <2> Nombre completo nombre_ext <3> Hab? ¿Habilitado? vhabilitado <4> puedeVerPagina('usuario_cambioclaveadmin_formulario.php'); ]]> usuario_cambioclaveadmin_formulario.php Cambiar contraseña ---- <1> `seguridad.usuario.nombre character(10)`. La definición de la columna incluye el elemento `columna/link` para especificar un formulario distinto que el predeterminado; en este caso, el link lleva al <>, dado que ésta <>. <2> `seguridad.usuario.nombre_ext character varying(30)`. <3> [[usuario_listado_xml-borrado_logico]]`seguridad.usuario.habilitado boolean`. La condición aplicada a esta columna hace que sólo sea visible cuando se están mostrando los usuarios deshabilitados, de acuerdo al estado del checkbox que se genera por la definición de borrado lógico incluída en el listado: + [source,xml] ---- habilitado false ---- + image::examples/localidades/screenshots/usuario_listado-detalle_solo_habilitados_si.png[Captura del listado cuando muestra sólo los usuarios habilitados] + image::examples/localidades/screenshots/usuario_listado-detalle_solo_habilitados_no.png[Captura del listado cuando muestra tanto los usuarios habilitados como los deshabilitados] + La condición de la columna utiliza la variable booleana global `$ocultar_borrados`, definida automáticamente por XGAP, que indica el estado de presentación de items con borrado lógico: + [source,php] ---- [[usuario_listado_xml-col_acciones]]Se incluye una columna de acciones, con una única acción definida que lleva al <> correspondiente. + La condición de la columna asegura que no sea visible si el usuario no tiene permiso para acceder al destino de la acción. + [source,php] ---- puedeVerPagina('usuario_cambioclaveadmin_formulario.php'); ---- -- + image::examples/localidades/screenshots/usuario_listado-columnas.png[] [[usuario_sinclave_formulario_xml]] ==== usuario_sinclave_formulario.xml ==== Formulario de modificación de usuario, sin cambio de contraseña. .Captura de pantalla de usuario_sinclave_formulario.php en la aplicación ``Localidades'' image::examples/localidades/screenshots/usuario_sinclave_formulario.png[] Es similar al <>, con dos diferencias: * No incluye los dos campos destinados al ingreso de contraseña. * Se impide su uso para altas (debido al punto anterior). [[usuario_sinclave_formulario_xml-xinclude]]Uso de XInclude:: Para aprovechar la similitud mencionada, la mayor parte del código de `usuario_sinclave_formulario.xml` se obtiene de `usuario_formulario.xml` , además de un elemento de código extra de `usuario_cambioclave_formulario.xml`, por medio de elementos https://www.w3.org/TR/xinclude/[XInclude]: + [source,xml] ---- ---- [[usuario_sinclave_formulario_xml-xgap_cargado]]Código extra PHP en `xgap_cargado`:: Definido en <>. [[usuario_sinclave_formulario_xml-despues_inicializacion]]Código extra PHP en `despues_inicializacion`:: Definido en <>. == Cambios al modelo básico de aplicación predefinido por XGAP == Los scripts SQL y páginas iniciales provistos por XGAP definen un modelo básico de aplicación. En esta sección se detallan los cambios que tiene la aplicación ``Localidades'' respecto a ese modelo. Cambios en funcionalidad:: * Posibilidad de definir una página de inicio diferente para cada rol funcional. * Los usuarios se pueden deshabilitar. Cambios en el esquema de base de datos:: * <> correspondientes al modelo propio de la aplicación, junto con otras tablas auxiliares. * Se agrega columna `pagina_inicio` a la tabla `seguridad.rolf`. * Se agrega columna `habilitado` a la tabla `seguridad.usuario`. Cambios en las páginas predefinidas:: `login_contenido.xml`::: * Se impide el ingreso de usuarios deshabilitados. `login_contenido.xml` y `seleccionarrol_contenido.xml`::: * Se tiene en cuenta la página de inicio que está definida para el rol funcional del usuario, para establecer el destino de la redirección final que realizan ambas páginas. * Se almacena la página de inicio en la sesión. <>::: * <> correspondiente a la columna `seguridad.rolf.pagina_inicio`. <>::: * <> para mostrar el valor de `seguridad.rolf.pagina_inicio`. <> y <>::: * <> correspondiente a la columna `seguridad.usuario.habilitado`. <>::: * Se agrega <>, de acuerdo al valor de `seguridad.usuario.habilitado`, y <> para mostrar el estado de habilitación del usuario.