ABM Simple

Paso a paso para la creación de un ABM simple para BIMS.

Introducción

Este tutorial le guiará paso a paso en la creación de un ABM (Alta, Baja y Modificación) de una entidad en BIMS.

En este ejercicio implementaremos la configuración de un maestro de Libros, asociados a un maestro de Autores.

Modo de Desarrollo

Para comenzar a desarrollar configuremos el framework en modo de desarrollo. En este modo se evita el caché de las estructuras de las tablas para los modelos, lo que nos permitirá hacer cambios en las estructuras que los modelos aprenderán con cada ejecución, también se evita el caché de localización y nos permitirá ver todos los errores en pantalla. Por otro lado, la ejecución se realentiza bastante, principalmente en ejecuciones sobre MS Windows. Así que no nos preocupemos si la ejecución de BIMS se vuelve lenta en nuestras estaciones.

Editemos el archivo app/config/core.php y reemplacemos la linea:

Configure::write('debug', 0);

por:

Configure::write('debug', 1);

Antes de hacer commit de nuestros cambios, debemos asegurarnos siempre de retornar el valor de "debug" a su estado original: "1".

Flujo Normalizado Básico de ABM

Las pantallas para la mayoría de los elementos configurables en BIMS mantienen este flujo estándar.

Base de Datos

Recuerde que, por convención y en función de la automatización de muchos procesos, establecemos los nombres de las tablas en inglés y en plural. Así, a la tabla del maestro de Libros la llamaremos "books" y a la de autores "authors".

Como también está convenido, los nombres de los campos que harán de clave primeria serán "id", y los nombres de las claves foráneas se contruirán con el nombre de la tabla referenciada, pero en singular y con el sufijo "_id".

CREATE TABLE "authors"
(
    id bigserial NOT NULL,
    name character varying NOT NULL,
    user_id bigint NOT NULL,
    created timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified timestamp without time zone,
    PRIMARY KEY (id),
    UNIQUE (name),
    FOREIGN KEY (user_id)
        REFERENCES "users" (id) MATCH SIMPLE
        ON UPDATE CASCADE
        ON DELETE RESTRICT
);

CREATE TABLE "books"
(
    id bigserial NOT NULL,
    name character varying NOT NULL,
    release_date date,
    author_id bigint,
    user_id bigint NOT NULL,
    created timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified timestamp with time zone,
    PRIMARY KEY (id),
    FOREIGN KEY (author_id)
        REFERENCES "authors" (id) MATCH SIMPLE
        ON UPDATE CASCADE
        ON DELETE RESTRICT,
    FOREIGN KEY (user_id)
        REFERENCES "users" (id) MATCH SIMPLE
        ON UPDATE CASCADE
        ON DELETE RESTRICT
);

Solo con fines didácticos, ingresemos ya algunos datos.

INSERT INTO "authors" (
    "name",
    "user_id"
)
VALUES (
    'Augusto Roa Bastos',
    ( SELECT MIN(id) FROM users )
);

INSERT INTO "books" (
    "name",
    "release_date",
    "author_id",
    "user_id"
)
VALUES (
    'Yo, El Supremo',
    '1974-08-01',
    ( SELECT MIN(id) FROM authors ),
    ( SELECT MIN(id) FROM users )
);

INSERT INTO "books" (
    "name",
    "release_date",
    "author_id",
    "user_id"
)
VALUES (
    'Hijo de Hombre',
    '1974-05-20',
    ( SELECT MIN(id) FROM authors ),
    ( SELECT MIN(id) FROM users )
);

Creación de Modelos, Vistas y Controladores con el Bake Shell

Ejecución del Bake

CakePHP ofrece un shell para automatizar la creación de Modelos, Controladores y Vistas a partir del diseño de las tablas. Hemos adaptado muchas funciones del baking de Cake para que los elementos se ajusten a las convenciones de BIMS, no obstante, luego de su generación deberemos realizar de forma manual algunas adaptaciones mínimas. Como sea, el trabajo ahorramos mucho trabajo empleando esta herramienta.

Una vez creadas las tablas "authors" y "books" en nuestra base de datos, ejecutamos el shell "bake" del core de CakePHP.

Ejecutamos lo siguiente desde el

# cd cake/console/
# ./cake -app ../../app bake

Se desplegará el siguiente menú:

Welcome to CakePHP v1.3.14 Console
---------------------------------------------------------------
App : app
Path: /Users/victor/Sites/bims2/cake/console/../../app
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 

Generación de los Modelos

Primeramente creamos los modelos. Para eso presionamos la tecla "m", tal como se indica en pantalla. Se listarán los nombres de modelos para todas las tablas de la base de datos.

Possible Models based on your current database:
1. Accentry
2. AccountsDefault
.
.
.
728. Author
729. Book
Enter a number from the list above,
type in the name of another model, or 'q' to exit  
[q] > Author

Los nombres de los modelos son los de las tablas pero en singular y capitalizados. Ejemplo:

Nombre de la Tabla

Nombre del Modelo

accentries

Accentry

accounts_defaults

AccountsDefault

authors

Author

books

Book

Seleccionamos primero "Author" ingresando el número que le corresponde en la lista (728 en este ejemplo) o directamente ingresando "Author".

El Bake nos consultará si deseamos definir los criterios de validación de los campos de la tabla. Aceptemos la propuesta con "y".

Would you like to supply validation criteria 
for the fields in your model? (y/n) 
[y] > y

El shell en base a la configuración tipo de dato y constraints sobre los campos, sugerirá las reglas de validación adecuadas, pero podremos modificarlas tanto en esta instancia como luego editando directamente el modelo. Aceptemos todas las reglas sugeridas por el Bake.

Estos criterios de validación se implementarán como reglas en la clase del modelo en el array dentro de un atributo llamado "validate", y estas reglas se validarán siempre antes de guardar un registro a través del modelo.

La siguiente fase del Bake es la definición de relaciones con otros modelos.

Would you like to define model associations
(hasMany, hasOne, belongsTo, etc.)? (y/n) 
[y] > y

Aceptaremos con "y" y seguiremos el wizard para modelar estas relaciones. El Bake reconocerá automáticamente y sugerirá las relaciones en base a la construción de la BD, siempre que hayamos respetado las convenciones en la nomenclatura de las tablas y los campos de clave primaria y foránea.

Would you like to define model associations
(hasMany, hasOne, belongsTo, etc.)? (y/n) 
[y] > y
One moment while the associations are detected.
---------------------------------------------------------------
Please confirm the following associations:
---------------------------------------------------------------
Author belongsTo User? (y/n) 
[y] > y
Author hasMany Book? (y/n) 
[y] > 
Would you like to define some additional model associations? (y/n) 
[n] > n

La tabla "authors" tiene una sola clave foránea: "user_id" -> "users.id". Esta relación es del tipo "belongsTo". Por otro lado, la tabla "books" referencia a "authors" a través de la clave foránea "books"."author_id" -> "authors"."id". Dese la perspectiva del modelo Book, esta relación es del tipo "belongsTo", mientras que desde la perspectiva de "Author" esta relación es del tipo "hasMany". Se aprovechará el modelado de estas relaciones tanto al consultar como al guardar registros. Ya llegaremos a eso.

Para finalizar, el shell presentará un resumen del modelo y esperará a nuestra confirmación para crearlo. Confirmaremos ingresando: "y".

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       Author
DB Table:   "authors"
Validation: Array
(
    [name] => Array
        (
            [notempty] => notempty
        )

    [user_id] => Array
        (
            [numeric] => numeric
        )

)

Associations:
	Author belongsTo User
	Author hasMany Book
---------------------------------------------------------------
Look okay? (y/n) 
[y] > y

Se creará el archivo app/models/author.php. Que se verá así:

<?php
	class Author extends AppModel {
		var $name = 'Author';
		var $displayField = 'name';
		var $validate = array(
			'name' => array(
				'notempty' => array(
					'rule' => array('notempty'),
					//'message' => 'Your custom message here',
					//'allowEmpty' => false,
					//'required' => false,
					//'last' => false, // Stop validation after this rule
					//'on' => 'create', // Limit validation to 'create' or 'update' operations
				),
			),
			'user_id' => array(
				'numeric' => array(
					'rule' => array('numeric'),
					//'message' => 'Your custom message here',
					//'allowEmpty' => false,
					//'required' => false,
					//'last' => false, // Stop validation after this rule
					//'on' => 'create', // Limit validation to 'create' or 'update' operations
				),
			),
		);
		//The Associations below have been created with all possible keys, those that are not needed can be removed
	
		var $belongsTo = array(
			'User' => array(
				'className' => 'User',
				'foreignKey' => 'user_id',
				'conditions' => '',
				'fields' => '',
				'order' => ''
			)
		);
	
		var $hasMany = array(
			'Book' => array(
				'className' => 'Book',
				'foreignKey' => 'author_id',
				'dependent' => false,
				'conditions' => '',
				'fields' => '',
				'order' => '',
				'limit' => '',
				'offset' => '',
				'exclusive' => '',
				'finderQuery' => '',
				'counterQuery' => ''
			)
		);
	
	}
?>

Seguidamente ejecutamos este mismo ejercicio para crear el modelo "Book" para la tabla "books".

Generación de los Controladores

Una vez creados ambos modelos: "Author" y "Book", seguimos con la creación de los controladores correspondientes, que llevarán los nombres: "AuthorsController" y "BooksController" respectivamente. Para esto, en el menú principal del shell ingresamos: "c".

---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> c
---------------------------------------------------------------

Así como con los modelos, se listarán todas las tablas de la base de datos con la nomenclatura estructurada para los controladores.

Possible Controllers based on your current database:
1. Accentries
2. AccountsDefaults
.
.
.
728. Authors
729. Books
Enter a number from the list above,
type in the name of another controller, or 'q' to exit  
[q] > 

Para la construcción de los nombres de los controladores se toma el nombre de la tabla pero capitalizado. Ejemplos:

Nombre de la Tabla

Nombre del Controlador

accentries

Accentries

accounts_defaults

AccountsDefaults

authors

Authors

books

Books

Seleccionaremos primero el controlador "Authors", ingresando el número que le corresponde en la lista (en esta caso: 728) o escribiendo directamente "Authors" y las demás opciones según el ejemplo de abajo:

Possible Controllers based on your current database:
1. Accentries
2. AccountsDefaults
.
.
.
728. Authors
729. Books
Enter a number from the list above,
type in the name of another controller, or 'q' to exit  
[q] > Authors

---------------------------------------------------------------
Baking AuthorsController
---------------------------------------------------------------
Would you like to build your controller interactively? (y/n) 
[y] > y
Would you like to use dynamic scaffolding? (y/n) 
[n] > n
Would you like to create some basic class methods 
(index(), add(), view(), edit())? (y/n) 
[n] > y
Would you like to create the basic class methods for admin routing? (y/n) 
[n] > n
Would you like this controller to use other helpers
besides HtmlHelper and FormHelper? (y/n) 
[n] > n
Would you like this controller to use any components? (y/n) 
[n] > n
Would you like to use Session flash messages? (y/n) 
[y] > y

---------------------------------------------------------------
The following controller will be created:
---------------------------------------------------------------
Controller Name:
	Authors
---------------------------------------------------------------
Look okay? (y/n) 
[y] > y

Se creará el archivo app/controllers/authors_controller.php con la definición del controlador. Debe tener la siguiente estructura:

<?php
class AuthorsController extends AppController {

	var $name = 'Authors';

	function index() {
		$this->Author->recursive = 0;
		$this->set('authors', $this->paginate());
	}

	function view($id = null) {
		if (!$id) {
			$this->setFlash('Invalid author', 'error');
			$this->redirect(array('action' => 'index'));
		}
		$data = $this->Author->read(null, $id);
		$this->set('author', $data);
		
		// Personalizaciones para BIMS de TIVA
		$this->navbar = array(
			'Authors' => array('action'=>'index')
		);
		$this->sidebar->name = 'author';
		$this->sidebar->data = $data;
	}

	function add() {
		if (!empty($this->data)) {
			$this->Author->create();
			if ($this->Author->save($this->data)) {
				$this->setFlash('The author has been saved');
				$this->redirect(array('action' => 'view', $this->Author->id));
			} else {
				$this->setFlash('The author could not be saved. Please, try again.', 'error');
			}
		}
		$users = $this->Author->User->find('list');
		$this->set(compact('users'));

		// Personalizaciones para BIMS de TIVA
		$this->navbar = array(
			'Authors' => array('action'=>'index')
		);
	}

	function edit($id = null) {
		if (!$id && empty($this->data)) {
			$this->setFlash('Invalid author');
			$this->redirect(array('action' => 'index'));
		}
		if (!empty($this->data)) {
			if ($this->Author->save($this->data)) {
				$this->setFlash('The author has been saved');
				$this->redirect(array('action' => 'view', $id));
			} else {
				$this->setFlash('The author could not be saved. Please, try again.', 'error');
			}
		}
		if (empty($this->data)) {
			$this->data = $this->Author->read(null, $id);
		}
		$users = $this->Author->User->find('list');
		$this->set(compact('users'));
	
		// Personalizaciones para BIMS de TIVA
		$this->navbar = array(
			'Authors' => array('action'=>'index'),
			$this->data['Author']['name'] => array('action'=>'index', $id)
		);
		$this->sidebar->name = 'author';
	}

	function delete($id = null) {
		if (!$id) {
			$this->setFlash('Invalid id for author', 'error');
			$this->redirect(array('action'=>'index'));
		}
		if ($this->Author->delete($id)) {
			$this->setFlash('Author deleted', 'error');
			$this->redirect(array('action'=>'index'));
		}
		$this->setFlash('Author was not deleted');
		$this->redirect(array('action' => 'index'));
	}
}

Ejecutamos el mismo proceso para el controlador "Books".

Generación de las Vistas

Una vez generados los modelos y los controladores, seguiremos con las vistas. En el menú principal del shell ingresamos: "v".

---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> v

Se listarán todas las tablas de la base de datos con la nomenclatura estructurada para los controladores.

Possible Controllers based on your current database:
1. Accentries
2. AccountsDefaults
.
.
.
728. Authors
729. Books
Enter a number from the list above,
type in the name of another controller, or 'q' to exit  
[q] > 

Seleccionamos "Authors" ingresando el número que le corresponde en la lista o la palabra "Authors" y seguimos el proceso como en el ejemplo siguiente:

Would you like bake to build your views interactively?
Warning: Choosing no will overwrite Authors views if it exist. (y/n) 
[n] > n

Creating file /Users/victor/Sites/bims2/cake/console/../../app/views/authors/index.ctp
Wrote `/Users/victor/Sites/bims2/cake/console/../../app/views/authors/index.ctp`

Creating file /Users/victor/Sites/bims2/cake/console/../../app/views/authors/view.ctp
Wrote `/Users/victor/Sites/bims2/cake/console/../../app/views/authors/view.ctp`

Creating file /Users/victor/Sites/bims2/cake/console/../../app/views/authors/add.ctp
Wrote `/Users/victor/Sites/bims2/cake/console/../../app/views/authors/add.ctp`

Creating file /Users/victor/Sites/bims2/cake/console/../../app/views/authors/edit.ctp
Wrote `/Users/victor/Sites/bims2/cake/console/../../app/views/authors/edit.ctp`
---------------------------------------------------------------

View Scaffolding Complete.

Se crearán los siguientes archivos, cada uno para uno de los métodos del controlador correspondiente:

  • app/views/authors/index.ctp, donde se listan los autores;

  • app/views/authors/view.ctp, la interna o "ficha" del autor;

  • app/views/authors/add.ctp, el formulario de alta de un autor;

  • app/views/authors/edit.ctp, el formulario de edición de un autor.

Todos los archivos de vistas llevan la extensión .ctp y su código el HTML / CSS / PHP / JS.

En este punto, ya podremos ver estas vistas en la web en las siguientes URLs:

  • http://<bims>/books (index)

  • http://<bims>/add (add)

  • http://<bims>/view/<id> (view)

  • http://<bims>/edit/<id> (edit)

Notaremos que el diseño de la GUI aún no luce como las pantallas de BIMS. Así que seguidamente haremos las adaptaciones necesarias sobre las vistas.

Adaptaciones sobre las Vistas

A continuación veremos las adaptaciones que deberán realizarse sobre las vistas para que nuestras pantallas adopten los linamientos de BIMS. Emplearemos algunos helpers para el efecto:

Helper

Descripción

Archivo

Html5Helper

Creamos este helper con el objeto de agilizar la construcción de las interfaces de usuario y más importante: normalizarlas. Se emplea principalmente para la creación de layouts.

app/views/elements/html5.php

MtHelper

Este helper contiene métodos para el formateo de datos y la generación de algunos componentes de la interfaz de usuario.

app/views/elements/mt.php

Índice (Index)

Con ayuda del Bake Shell se generó este archivo para el índice de libros: app/views/books/index.ctp.

<div class="books index">
	<h1><?php __('Books');?></h1>
	<table cellpadding="0" cellspacing="0">
	<tr>
			<th class="pk"><?php echo $this->Paginator->sort('id');?></th>
			<th><?php echo $this->Paginator->sort('name');?></th>
			<th><?php echo $this->Paginator->sort('release_date');?></th>
			<th><?php echo $this->Paginator->sort('author_id');?></th>
			<th><?php echo $this->Paginator->sort('user_id');?></th>
			<th><?php echo $this->Paginator->sort('created');?></th>
			<th><?php echo $this->Paginator->sort('modified');?></th>
	</tr>
	<?php
	$i = 0;
	foreach ($books as $book):
		$class = null;
		if ($i++ % 2 == 0) {
			$class = ' class="altrow"';
		}
	?>
	<tr<?php echo $class;?>>
		<td class="pk"><?php echo $book['Book']['id']; ?>&nbsp;</td>
		<td><?php echo $html->link($book['Book']['name'], array('action'=>'view', $book['Book']['id'])); ?>&nbsp;</td>
		<td><?php echo $book['Book']['release_date']; ?>&nbsp;</td>
		<td>
			<?php echo $mt->fblink($book['Author']['name'], array('controller' => 'authors', 'action' => 'view', $book['Author']['id'])); ?>
		</td>
		<td>
			<?php echo $mt->fblink($book['User']['name'], array('controller' => 'users', 'action' => 'view', $book['User']['id'])); ?>
		</td>
		<td><?php echo $book['Book']['created']; ?>&nbsp;</td>
		<td><?php echo $book['Book']['modified']; ?>&nbsp;</td>
	</tr>
<?php endforeach; ?>
	</table>
	<p>
	<?php
	echo $this->Paginator->counter(array(
	'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%', true)
	));
	?>	</p>

	<div class="paging">
		<?php echo $this->Paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled'));?>
	 | 	<?php echo $this->Paginator->numbers();?>
 |
		<?php echo $this->Paginator->next(__('next', true) . ' >>', array(), null, array('class' => 'disabled'));?>
	</div>
</div>

Reemplacemos la estructura original por esta:

<?php
	$html5->paginator = $paginator;
	$html5->listBefore(array(
		'title' => __('Books', true),
		'options' => array(
			'export' => array(
				'mode' => 'excel'
			),
			'print' => array(
				'auto' => 1
			),
			'filter' => 'books/_filter_index'
		)
	));
?>
<table class="table table-bordered table-striped table-hover table-custom-border ">
	<thead>
		<tr>
			<th class="pk"><?php echo $this->Paginator->sort('id');?></th>
			<th><?php echo $this->Paginator->sort('name');?></th>
			<th><?php echo $this->Paginator->sort('release_date');?></th>
			<th><?php echo $this->Paginator->sort('author_id');?></th>
		</tr>
	</thead>
	<tbody>
	<?php
		foreach ($books as $book):
	?>
		<tr>
			<td class="pk">
				<?php echo $book['Book']['id']; ?>
			</td>
			<td>
				<?php echo $html->link($book['Book']['name'], array('action'=>'view', $book['Book']['id'])); ?>
			</td>
			<td>
				<?php echo $book['Book']['release_date']; ?>
			</td>
			<td>
				<?php echo $mt->fblink($book['Author']['name'], array('controller' => 'authors', 'action' => 'view', $book['Author']['id'])); ?>
			</td>
		</tr>
	<?php
		endforeach;
	?>
</table>
<?php
	$html5->listAfter();
?>

La lista de Libros se verá ahora así:

Seguidamente creamos el menú del sidebar. De forma predeterminada, los sidebars de las vistas de índice (index) y adición (add) se buscarán en app/views/elements/sidebars/<nombre-de-tabla>.ctp. Así que crearemos el archivo app/views/elements/sidebars/books.ctp con este contenido:

<?php
	echo $mt->sidebutton("Add", array('controller'=>'books', 'action'=>'add'), $sidebar->selected);
?>
<ul class="nav nav-stacked">
<?php
	echo $mt->sidemenu("Listar Libros", array('controller'=>'books', 'action'=>'index'), $sidebar->selected);
?>
</ul>

Y lucirá asi:

Adición (add) y Edición (edit)

Con ayuda del Bake Shell se generó este archivo para el índice de libros: app/views/books/add.ctp y app/views/books/edit.ctp.

Editaremos el contenido de app/views/books/add.ctp para que simplemente cargue el contenido de app/views/books/edit.ctp, de manera a normalizar un código para ambos usos y preocuparnos solo por mantener un archivo.

Reemplazaremos el contenido de app/views/books/edit.ctp por el que sigue:

<?php
	$html5->tmpForm(array(
		'model' => 'Book',
		'title' => empty($this->data['Book']['id']) ? 'Add Book' : 'Edit Book',
		'displayFilters' => false,
		'sections' => array(
			array(
				'title' => 'Main Information',
				'description' => __d('desc', 'Main Information', true),
				'fields' => array(
					'name',
					'release_date' => array(
						'label' => 'Release Date',
						'type' => 'date'
					),
					'author_id'
				)
			),
		),
		'submit' => __('Save', true)
	));
?>

La pantalla deberá verse así:

Vista (view)

Con ayuda del Bake Shell se generó este archivo para la vista de libros: app/views/books/view.ctp.

Reemplazaremos su contenido actual por este, que emplea el helper Html5:

<?php
	$html5->tmpView(array(
		'head' => array(
			'title' => $book['Book']['name']
		),
		'spotlight' => array(
			'title' => $book['Book']['name'],
			'fields' => array(
				'Author'						=> $book['Author']['name'],
				'Release Date'					=> $book['Book']['release_date'],
			),
		),
		'body' => array(
			'sections' => array(
				'Main Information' => array(
					'Name'						=> $book['Author']['name'],
					'Release Date'				=> $book['Book']['release_date']
				),
				'Audit Information' => array(
					'User' => $mt->fblink($book['User']['name'], array('controller'=>'users', 'action'=>'view', $book['Book']['user_id'])),
					'Created' => $book['Book']['created'],
					'Modified' => $book['Book']['modified']
				)
			)
		)
	));
?>

Y también creamos el archivo de para el menú del sidebar: app/views/elements/sidebar/book.ctp con este contenido:

<ul class="nav nav-stacked">
<?php
	echo $mt->sidemenu("View", array('controller'=>'books', 'action'=>'view', $sidebar->data['Book']['id']), $sidebar->selected);
	echo $mt->sidemenu("Edit", array('controller'=>'books', 'action'=>'edit', $sidebar->data['Book']['id']), $sidebar->selected);
	echo $mt->sidemenu("Delete this element", array('controller'=>'books', 'action'=>'delete', $sidebar->data['Book']['id']), $sidebar->selected, null, array('confirm'=>__('Do you really want to delete this element?', true)));
?>
</ul>

La pantalla se verá así:

Localización

El texto estático nuevo que hemos incorporado a la GUI esta expresado en inglés, que es la lengua básica para la interfaz de usuario de BIMS. Así que resta configurar las traducciones al español de estos términos y mensajes en el archivo: app/locale/esp/LC_MESSAGES/default.po. Ejemplo:

msgid "Books"
msgstr "Libros"

msgid "Release Date"
msgstr "Fecha de Lanzamiento"

msgid "Authors"
msgstr "Autores"

Los cambios que realicemos en este archivo se verán reflejados en la interfaz de usuario de manera inmediata si el framework se encuentra en modo de desarrollo. Si el framework se encuentra en modo de producción, para que los cambios tengan efecto se deberá eliminar el caché, asi:

rm -f app/tmp/cache/persistent/*

¡Voilá!

¡Felicitaciones! Ya tenemos nuestro primer ABM funcional en BIMS.

En otros posts encontraremos mayor información sobre los helpers utilizados aquí.

Last updated