viernes, 23 de mayo de 2014

Alfresco cambiará su política de precios

El popular gestor documental Alfresco había mantenido desde el principio una política de precios para su versión Enterprise basada en el número de CPUs o instancias donde se desplegaba el producto, indpendientemente del número de usuarios del sistema. Este tipo de licenciamiento lo hacía una opción muy competitiva tanto para pequeñas o medianas empresas como para grandes corporaciones que quisieran evitar el licenciamiento por usuario que utilizan casi todos los gestores documentales del mercado. Ahora esta situación va a cambiar ya que Alfresco va a licenciar su versión Enterprise basándose en el número de usuarios.

La opción más asequible, conocida como Alfresco One Departamental, está pensada para un máximo de 300 usuarios y no permitirá instalaciones en alta disponibilidad. Esta versión ofrecerá la misma funcionalidad que ofrece la versión actual, incluyendo un espacio de almacenaje en el cloud de Alfresco y el gestor de procesos Activiti. Esta versión tendrá un precio de 48.000€ anuales y no incluirá módulos como el Record Management, el Transformation Server o el próximo módulo de integración con Outlook que se pagarán a parte. Haciendo números, sale que cada usuario tendrá un coste de 160€ anuales, aunque se tendrá que pagar por los 300, independientemente de si la instalación de Alfresco vaya a usar dos usuarios o doscientos.


La segunda opción será conocida como Alfresco One Enterprise y estará pensada para un máximo de 1.000 usuarios y permitirá alta disponibilidad. El precio de esta versión será de 109.000€ e incluirá algún conector adicional y soporte 24x7. A partir de ahí, los usuarios adicionales se tendrán que contratar en bloques de 500.


Esta nueva política de precios, que entrará en vigor el 3 de Junio, no sólo implica un incremento de casi el 100% en el precio de la licencia más básica, sino que elimina una de las ventajas más competitivas que tenía Alfresco, que era la posibilidad de tener un número ilimitado de usuarios manteniendo estable el coste de la plataforma. Sin duda se trata de un movimiento arriegado por parte de Alfresco que envía un mensaje claro sobre el tipo de clientes que está buscando: las grandes empresas, dejando de lado el segmento de mercado de las pequeñas y medianas empresas en las que Alfresco tenía una fuerte implantación en algunos mercados.

De momento la versión Community no se verá afectada por estos cambios y seguirá permitiendo un número ilimitado de usuarios sin coste alguno.

viernes, 3 de enero de 2014

Tratamiento de URLs largas en el protocolo Sharepoint de Alfresco

Una de las funcionalidades más apreciadas por los usuarios de Alfresco Share es la posibilidad de editar ficheros de MS Office directamente desde la aplicación sin necesidad de descargarse el fichero en local. Alfresco ofrece esta funcionalidad mediante el protocolo Sharepoint incluido en el módulo vti. A pesar de su buena funcionalidad, el módulo no está exento de problemas y uno de ellos es el tratamiento de URLs demasiado largas.

Las URLs utilizadas por Alfresco para invocar la edición online de documentos tienen la siguiente forma:

{protocol}://{host}:{vti_port}/{alfresco_context}/{site}/{path_to_file}/{filename}

Todos estos elementos tienen una longitud más o menos acotada excepto que representa toda la ruta de carpetas desde la raíz del Sitio hasta el fichero. El protocolo Sharepoint tiene un problema cuando esta ruta es demasiado larga y el resultado puede ser que el fichero no se pueda abrir para su edición online. Hasta ahora la única solución era acortar estos paths cambiando el nombre de las carpetas que lo forman, pero esto puede ser una solución que no todos los usuarios de Alfresco estén dispuestos a aceptar.

Buscando una solución a este problema se me ocurrió la posibilidad de trabajar con los indentificadores de fichero de Alfresco en lugar del path. La ventaja de usar identificadores es que su longitud es fija y no excede el límite fijado por el protocolo. Con este enfoque la URL a construir sería del siguiente estilo:

{protocol}://{host}:{vti_port}/{alfresco_context}/{site}/__ID/{content_uuid}/{filename}

En esta nueva URL he usado una partícula (__ID) que me permite identificar que se trata de una URL que contiene un identificador de contenido y he añadido el UUID del contenido que quiero editar. Una vez definida la URL adecuada es hora de construirla y para ello hay que modificar la librería JavaScript que se utiliza en Alfresco para generar estas URLs. En concreto hay que buscar el fichero documentlibrary-actions.js (o su versión minificada para ser más exactos) y buscar el siguiente trozo de código:

var onlineEditUrl = this.doclistMetadata.custom.vtiServer.host + ":" + this.doclistMetadata.custom.vtiServer.port + "/" + $combine("alfresco", loc.site.name, loc.container.name, loc.path, loc.file);

Aquí se puede ver como la URL se compone a partir del site, el path y el nombre del fichero. Hay que sustituir dicho código por el siguiente:

var onlineEditUrl = this.doclistMetadata.custom.vtiServer.host + ":" + this.doclistMetadata.custom.vtiServer.port + "/" + $combine("alfresco", loc.site.name, "__ID", record.jsNode.nodeRef.uri.substring(record.jsNode.nodeRef.uri.lastIndexOf("/")+1), loc.file);

En este nuevo código se utiliza el site, el UUID (calculado a partir del nodeRef del documento) y el nombre del fichero.

El siguiente paso consiste en tratar estas URLs. Hay varias opciones para ello pero yo he optado por modificar una clase del módulo vti de Alfresco que se utiliza para realizar operaciones con el path de los ficheros. La clase en cuestión es:

org.alfresco.module.vti.handler.alfresco.VtiPathHelper

y el código de esta clase se puede obtener del repositotio público de svn de Alfresco (por ejemplo de aquí: /alfresco/COMMUNITYTAGS/V4.0e/root/modules/sharepoint/amp/source/java/org/alfresco/module/vti/handler/alfresco). También se puede obtener del repositorio enterprise si se tiene acceso.

En esta clase hay un método que devuelve la información del fichero a partir del path que le llega por la URL. Este método se utiliza en varias operaciones del protocolo por lo que hay que tener cuidado a la hora de cambiar el código para que no se vea alterado el correcto funcionamiento del módulo. El método que hay que modificar es: resolvePathFileInfo. En el código original del método aparecen las siguientes líneas:

           List splitPath = Arrays.asList(initialURL.split("/"));
           fileInfo = fileFolderService.resolveNamePath(rootNodeRef, splitPath);

Estas líneas hay que sustituirlas por estas otras para poder tratar las nuevas URLs:

List splitPath = Arrays.asList(initialURL.split("/"));
if(splitPath.contains("__ID")){
  if(splitPath.size()==4){
    NodeRef fileNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE

                                   , splitPath.get(2));
    fileInfo = fileFolderService.getFileInfo(fileNodeRef);
  } else if (splitPath.size()==3) {
    NodeRef fileNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE

                                   , splitPath.get(2));
    NodeRef parentNodeRef = nodeService.getPrimaryParent(fileNodeRef)

                                       .getParentRef();
    fileInfo = fileFolderService.getFileInfo(parentNodeRef);                 
  }

}else{
  fileInfo = fileFolderService.resolveNamePath(rootNodeRef, splitPath);
}


El nuevo código permite tratar tanto las nuevas URLs como las antiguas, manteniendo así la compatibilidad con el módulo de Alfresco. La primera parte del condicional (cuando el path contiene cuantro elementos) sirve para localizar la información del fichero que se quiere editar en el momento que se abre el fichero por primera vez. La segunda parte del condicional (cuando el path contiene 3 elementos) se usa cuando se guardan los cambios en el documento y se necesita recuperar la carpeta que lo contiene.

Con este cambio se puede generar un nuevo fichero JAR que contenga la implementación del protocolo vti y desplegarlo en el servidor de aplicaciones.

Este problema afecta a todas las versiones del módulo vti y en Alfresco están trabajando en una solución que estará disponibles en futuras versiones del módulo.

martes, 28 de agosto de 2012

Alfresco Custom Behavior

El repositorio de Alfresco permite la automatización de acciones sobre los contenidos que alberga. Hay varias formas que automatizar estas acciones como pueden ser las reglas sobre carpetas o las tareas programadas. En este artículo exploraremos una tercera opción: los behaviors.

Los behaviors son porciones de lógica de negocio que están vinculadas a políticas del repositorio. En el repositorio hay definido un conjunto de políticas que no son más que interfaces Java que son llamadas por los servicios de Alfresco. Las políticas disponibles en Alfresco para el servicio de nodo (NodeService) son:


Interface
Inner Interface
NodeServicePolicies
BeforeCreateStorePolicy

OnCreateStorePolicy

BeforeCreateNodePolicy

OnCreateNodePolicy

BeforeMoveNodePolicy

OnMoveNodePolicy

BeforeUpdateNodePolicy

OnUpdateNodePolicy

OnUpdatePropertiesPolicy

BeforeDeleteNodePolicy

OnDeleteNodePolicy

BeforeAddAspectPolicy

OnAddAspectPolicy

BeforeRemoveAspectPolicy

OnRemoveAspectPolicy

OnRestoreNodePolicy

BeforeCreateNodeAssociationPolicy

OnCreateNodeAssociationPolicy

OnCreateChildAssociationPolicy

BeforeDeleteChildAssociationPolicy

OnDeleteChildAssociationPolicy

OnCreateAssociationPolicy

OnDeleteAssociationPolicy

BeforeSetNodeTypePolicy

OnSetNodeTypePolicy

Tambíen hay políticas para el ContentService, el CopyService y el VersionService.

Un behavior sólo es una clase Java que implementa una de estas interfaces. La ventaja de un behavior sobre una regla es que los behavior aplican a todo el reposiotio mientras que las reglas aplican sobre carpetas concretas del reposiotio. Respecto a las tareas programadas, un behavior tiene la ventaja que se aplica en tiempo real y no hay que esperar hasta la hora de ejecución de la tarea para comprobar sus efectos.

Realizaré un ejemplo práctico extendiendo el ejemplo que propuse en este artículo. La idea es que todos los contenidos que se creen cuyo mimetype sea image/tiff se almacenen físicamente en un alamcen de contenidos especial. Para ello crearé un behavior vinculado a la política onCreateNodePolicy que añada el aspecto storeSelector e informe la propiedad storeName.

La definición de la clase será:

public class StoreSelectorBehavior implements NodeServicePolicies.OnCreateNodePolicy 


A continuación definimos las propiedades que inyectaremos desde Spring:


private NodeService nodeService;
private PolicyComponent policyComponent;



Creamos un método para inicializar el behavior y registrar la clase con la política adecuada. En este caso lo haremos para todos los contenidos de tipo cm:content :



public void init() {
    // Create behavior
    this.onCreateNode = new JavaBehaviour(this,"onCreateNode",NotificationFrequency.TRANSACTION_COMMIT);
    // Bind behavior to node policy
    this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI,"onCreateNode"),QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI ,"content"),this.onCreateNode);
}



Definimos el método que se ejecutará al crearse un nodo y donde comprobamos el mimetype y asignamos el aspecto storeSelector cuando sea necesario:


    public void onCreateNode(ChildAssociationRef childAssocRef) {
        // get the child node
        NodeRef node = childAssocRef.getChildRef();

        // check if the node has the correct mimetype
        ContentData contentData = (ContentData) nodeService.getProperty(node, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "content"));
        if (contentData.getMimetype().equalsIgnoreCase("image/tiff")) {
        // ass the aspect
            Map storeSelectorProps = new HashMap(1, 1.0f);
            storeSelectorProps.put(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"storeName"), "images"); 
            nodeService.addAspect(node, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "storeSelector"), storeSelectorProps);
        }
    }



Por último hay que definir el bean para cargar el behavior desde el contexto de Spring.



    <bean class="org.alfresco.sample.StoreSelectorBehavior" id="store-selector-behavior" init-method="init">
        <property name="nodeService">
            <ref bean="nodeService">
        </ref></property>
        <property name="policyComponent">
            <ref bean="policyComponent">
        </ref></property>
    </bean>
 
 

Ahora sólo falta desplegar los cambios y reiniciar Alfresco. A partir de este momento cada vez que se cree un contenido que tenga mimetype image/tiff se almacenará en el store images.

jueves, 16 de agosto de 2012

Modificar User Dashboards

El cliente web Alfresco Share está muy orientado a usuario y por eso el punto de entrada a la aplicación es un panel de inicio donde se muestra la información de interés para el usuario. Este panel de inicio (user dashboard) está personalizado para cada usuario.

El panel de inicio de los usuarios de Alfresco Share se configura usando presets. Estos presets se definen en el fichero /WEB-INF/classes/alfresco/site-data/presets/presets.xml (o en su equivalente de la carpeta de extensión). Un preset contiene la definición del layout de la página y la lista de componentes que aparecerán en ella. En el caso del panel de inicio de los usuarios, cuando estos acceden por primera, se crea una instancia de panel de inicio siguiendo la plantilla definida en el preset y a partir de ahí los usuarios pueden modificar la apariencia y contenido de su panel de inicio como quieran.

Este enfoque es válido cuando se quiere que los usuarios tengan el control total sobre su panel de inicio pero no lo es cuando se quiere que sea el administrador el responsable de mantener la apariencia del panel de inicio común para todos los usuarios. Imaginemos la siguiente situación. El administrador define un preset y los usuarios empiezan a acceder. Se ha hecho una modificación en el cliente web para que los usuarios no tengan disponible el botón que les permite editar su panel de inicio, por lo que todos los usuarios conservan el mismo panel de inicio desde que acceden por primera vez y todos los paneles de inicio son iguales. Hasta aquí todo va según las necesidades del administrador. Ahora bien, ¿qué pasará si el administrador hace un cambio en el preset y quiere que se aplique a todos los usuarios? Pasará que el cambio en el preset sólo se aplicará a los usuarios que accedan por primera vez después de la modificación. Todos aquellos usuarios que hubieran accedido antes del cambio conservarán el panel de inicio anterior. ¿Cómo se puede solucionar esto?

En Alfresco 4.0, la información de los paneles de inicio de los usuarios se guarda en el propio repositorio, en la ruta /company_home/Sites/surf-config . Esta ruta no se puede ver desde Alfresco Share y sólo se puede alcanzar desde Alfresco Explorer (o desde cualquiera de las APIs de Alfresco). En esta ruta hay varios espacios, entre ellos dos llamados pages y components. El primero corresponde a las páginas de los paneles de inicio de los usuarios y el segundo corresponde a cada uno de los componentes que aparecen en cada página.

Para solucionar el problema planteado, bastaría con borrar el contenido de estos dos espacios y reiniciar Alfresco para que todos los usuarios tengan su panel de inicio según lo indicado en el nuevo preset.

Esta solución también nos permitiría tratar problemas más concretos, como el de eliminar el panel de inicio de un usuario concreto o quitar un componente de un panel de inicio que esté dando problemas. También se podría aplicar esta solución para aplicar cambios mediante código. El único problema que he encontrado en esta solución es la necesidad de reiniciar Alfresco para que los cambios tengan efecto.

domingo, 15 de abril de 2012

Document library custom actions

La version 4.0 del gestor documental Alfresco incluye un nuevo mecanismo para personalizar y extender las acciones de la biblioteca de documentos del cliente Share. Este mecanismo pretende ser una simplificación del modelo anterior y permite tener más control sobre como se presentan las acciones.

La configuración por defecto de las acciones se encuentra en el fichero share-documentlibrary-config.xml. Este cambio permite que las acciones se pueden personalizar o extender en el fichero share-config-custom.xml.

Para ver como funciona este mecanismo voy a realizar un ejemplo aprovechando parte de lo que presenté en este post. La idea es crear una acción que cambie el store en el que está almacenado un documento.

Web client tier 

El primer paso es definir la acción en el fichero share-config-custom.xml. La acción se debe definir dentro del elemento de configuración llamado DocLibActions.

      <actions>
        <action id="set-content-store-selector" type="javascript" label="actions.set-content-store-selector">
            <param name="function">onActionChangeStore</param> 
            <permissions>
               <permission allow="true">Write</permission>
            </permissions>
            <evaluator negate="true">evaluator.doclib.action.isLocked</evaluator>
        </action>     
      </actions>

De esta forma hemos definido una acción llamada set-content-store-selector, de tipo javascript, que llamará a la función de javascript onActionChangeStore, que sólo estará disponible para los usuarios con permiso de escritura sobre el contenido y que aparecerá cuando el documento no esté bloqueado por otro usuario.

El icono para la acción se ha de llamar set-content-store-selector-16.png y ha deir ubicado en la carpeta \components\documentlibrary\actions.

A continuación hay que indicar en qué grupo de acciones se ha de mostrar la nueva acción. Con el nuevo mecanismo, las acciones se han agrupado según el lugar donde se han de mostrar y no según el estado del nodo como sucedía en las versiones previas de Alfresco. Esta definición va en el mismo bloque de configuración que la definición de las acciones. En el ejemplo, queremos que aparezca en el conjunto de acciones del detalle de un documento y en las acciones de navegación de los documentos:

      <actionGroups>
         <actionGroup id="document-browse">

            <action index="340" id="set-content-store-selector" />
         </actionGroup>
         <actionGroup id="document-details">
            <action index="360" id="set-content-store-selector" />
         </actionGroup>       
      </actionGroups>

Con esta configuración ya se puede comprobar como quedará la acción en el menú:



El siguiente paso es escribir la función javascript a la que se invocará desde el botón. Hay varias opciones para ello ya que en el fichero donde se definen las opciones por defecto existen algunos métodos genéricos que se pueden reutilizar. El fichero por defecto es documentlibrary-actions.js. En el ejemplo vamos a escribir una función nueva:

      onActionChangeStore: function dlA_onActionChangeStore(record)
      {
         var displayName = record.displayName;

         this.modules.actions.genericAction(
         {
            success:
            {
               event:
               {
                  name: "metadataRefresh"
               },
               message: this.msg("message.set-content-store-selector.success", displayName)
            },
            failure:
            {
               message: this.msg("message.set-content-store-selector.failure", displayName)
            },
            webscript:
            {
               method: Alfresco.util.Ajax.POST,
               name: "set-content-store-selector/node/{nodeRef}",
               params:
               {
                  nodeRef: record.jsNode.nodeRef.uri
               }
            }
         });
      },

Esta función hace una llamada AJAX por método POST a un webscript de Alfresco pasándole como parámetro la referencia al nodo sobre el que se está ejecutando la acción. Este webscript ha de ser  del package slingshot/doclib/action. Como resultado de la llamada, mostrará un mensaje de éxito o de fracaso.

Server tier

En la parte del servidor hemos de crear el web script invocado desde el método javascript. La construcción de este webscript es estándar pero aprovecharé el mecanismo de ejecución de acciones que proporciona Alfresco para simplificar la tarea.

En primer lugar definiremos el webscript:

<webscript>
  <shortname>set-content-store-selector</shortname>
  <description>Document List Action - Changes the store</description>
  <url>/slingshot/doclib/action/set-content-store-selector/node/{store_type}/{store_id}/{id}</url>
  <format default="json">argument</format>
  <authentication>user</authentication>
  <transaction>required</transaction>
  <lifecycle>internal</lifecycle>
</webscript>

Lo único a destacar es que el formato de respuesta por defecto será JSON y que el nivel de autenticación será user puesto que la acción implica un cambio en los metadatos del objeto afectado.

Para el controlador del webscript utilizaré la librería de accciones action.lib.js, que proporciona métodos para recoger los parámetros y formatear la respuesta:

<import resource="classpath:/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action/action.lib.js">
/**
 * Entrypoint required by action.lib.js
 */
function runAction(p_params)
{
   var results;

   try
   {
      var aspectAdded = p_params.destNode.addAspect("cm:storeSelector");
      p_params.destNode.properties["cm:storeName"] = "storeA";
      p_params.destNode.save();
      if (!aspectAdded)
      {
         status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, "Could not change store " + url.extension);
         return;
      }

      var resultId = p_params.destNode.name,
         resultNodeRef = p_params.destNode.nodeRef.toString();

      // Construct the result object
      results = [
      {
         id: resultId,
         nodeRef: resultNodeRef,
         action: "changeStore",
         success: true
      }];
   }
   catch(e)
   {
      e.code = status.STATUS_INTERNAL_SERVER_ERROR;
      e.message = e.toString();     
      throw e;
   }

   return results;
}

main();

Por último tenemos que crear la plantilla de visualización para el formato JSON. En este caso también utilizaremos la librería de plantillas action.lib.ftl. La plantilla sería la siguiente:

<#import "action.lib.ftl" as actionLib />
<@actionLib.resultsJSON results=results />

Una vez grabados los ficheros, hay que copiarlos a la carpeta de extenxión de Alfresco, por ejemplo en /alfresco/extension/templates/webscripts/org/alfresco/components/documentlibrary/actions. Ac ontinuación refrescaremos la lista de webscripts de Alfresco y la acción ya estará disponible para probar.

Para más detalles, se puede consultar la documentación de Alfresco.

domingo, 8 de abril de 2012

IV Jornada de museos: gestión documental y archivo

El próximo día 18 de Abril se celebrará en el Museu Marítim de Barcelona la IV Jornada de museos, gestión documental y archivo: tecnologías y documentos.

Esta jornada está pensada para presentar proyectos e iniciativas de aplicación de tecnologías de gestión documental en el ámbito de los museos. Es una buena ocasión para comprobar qué se está haciendo en este sector y para compartir experiencias sobre este tema.

En esta ocasión IN2 tendrá un papel destacado al aportar dos participantes a las actividades de la jornada:
  • Por un lado, Carles Giner, gerente de IN2, participará en una mesa redonda sobre cloud computing y gestión documental de archivo.
  • Por otro, Toni de la Fuente, de Alfresco, y yo, realizaremos la presentación "Organización de contenidos de un museo con el gestor documental Alfresco", donde mostraremos como se han aplicado las últimas novedades de Alfresco 4.0 a la gestión de contenidos de varios museos.
Se puede encontrar más informacion en la web del museo.


jueves, 5 de abril de 2012

Alfresco Certified Administrator

Alfresco inició el año pasado un nuevo proceso basado en certificaciones para reconocer el nivel de los profesionales que trabajan con sus productos.

Actualmente hay disponibles dos certificaciones:

  • Alfresco Certified Administrator (ACA) : orientado a administradores de sistemas basados en Alfresco
  • Alfresco Certifies Engineer (ACE) : orientado a desarrolladores sobre Alfresco.

Dada mi experiencia profesional, la certificación que me interesaba era la primera, puesto que llevo desde el 2007 trabajando instalando y configurando Alfresco en sus múltiples variantes.

El procedimiento para obtener la certificación es aprobando un examen online. Este examen cubre un amplio temario repartido de la siguiente forma:

Competency%age of Exam
Architecture 9%
Managing the Installation 27%
Installation and Configuration
Upgrades
Monitoring health
Preventative maintenance
Installing Applications
Repository Management31%
User and group management
Security
Backup and recovery
Workflow management
Content rules
Auditing
Server management23%
Managing Storage and Content
Transformers and Metadata Extraction
Load balancing
Performance tuning
High availability
Troubleshooting5%
Provide support to developers1%
Establish test environments3%
Support end users1%

No hay mucho material espécífico para preparar este examen aunque la gente de Alfresco ha publicado algunos artículos donde hacen referencia a material de preparación. Se puede encontrar esta información aquí y aquí.

El formato del examen es un test online de 80 preguntas a resolver en 60 minutos. Hay varios tipos de preguntas: escoger la respuesta correcta (single choice), escoger las respuestas correctas (multiple choice) y escoger la respuesta más acertada (best match). El nivel de las preguntas va desde las que preguntas cosas generales (“qué características tiene la versión Enterprise que no tiene la Community ”) hasta las muy específicas (“qué valor hay que darle a la propiedad lucene.maxAtomicTransformationTime para que se fuerce la indexación de todos los contenidos”). Para superar el examen hay que acertar el 75% de las preguntas.

En mi caso opté por prepararme el examen por mi cuenta y usé como material de estudio la documentación publicada en http://docs.alfresco.com . Lo bueno de esta documentacion es que sigue bastante el guión del examen y tiene el nivel suficiente para superar el examen con éxito. Prueba de ello es que tras una lectura de este material me pude presentar al examen y aprobarlo con un 82% de aciertos. Así que ya puedo decir que soy: