Skip to content

Definición de campos adicionales para Conjuntos de Datos y Recursos CKAN

Rodrigo Parra edited this page Jun 27, 2014 · 2 revisions
  1. Introducción
  2. Requisitos
  3. Campos adicionales para Conjuntos de Datos
  4. Campos de definición de esquema para Recursos
  5. Campos adicionales dinámicos para Recursos
  6. Añadir campos adicionales al índice de búsqueda

Introducción

En este tutorial se presenta la serie de pasos necesarios para registrar metadatos adicionales relacionados a los conjuntos de datos y los recursos almacenados en CKAN. Los campos adicionales que se definen permiten a los usuarios registrar información relacionada a los datos que los formularios de CKAN no solicitan por defecto.

De modo a comprender la finalidad de esta guía, es importante conocer algunos conceptos relacionados.

Un Conjunto de Datos de CKAN es una colección de recursos de datos (como ficheros), junto con una descripción y otra información, unida a una URL. Los resultados de una búsqueda por parte del usuario son conjuntos de datos.

Un recurso puede ser cualquier archivo o enlace a un archivo que contiene datos útiles.

Requisitos

Para seguir este tutorial es necesario contar con una instalación de la última versión estable (2.2) de CKAN desde el código fuente, realizada de acuerdo a los pasos que se detallan en: http://datosparaguay.org/xwiki/bin/view/CKAN_Guide/WebHome.

Además, es necesaria una extensión de CKAN, como la que se creó en la guía para integrar CKAN con el widget de Twitter. En esta guía se utilizará el nombre ckanext-prueba para la extensión.

Campos adicionales para Conjuntos de Datos

Al crear un nuevo conjunto de datos en CKAN, se registran varios atributos relacionados al mismo como título, descripción, URL, etiquetas, los nombres y correos electrónicos del autor y el mantenedor del conjunto de datos, etc.

Puede resultar necesario almacenar campos adicionales para describir mejor el conjunto de datos que se almacena en CKAN. Por ejemplo: almacenar el periodo de validez del conjunto de datos (validez temporal), la ubicación geográfica a la que corresponden los datos (validez espacial) o la frecuencia con la cual se actualiza el conjunto de datos.

Para añadir estos campos pueden seguirse los siguientes pasos:

1. CKAN se ejecuta en un entorno virtual, de modo a aislar sus dependencias de otros programas Python que se ejecuten en el equipo. Es importante activar el entorno virtual de CKAN SIEMPRE antes de realizar alguna actividad relacionada como levantar el servidor, implementar una extensión, etc.

Para más información acerca de entornos virtuales de Python, se recomienda consultar: http://www.virtualenv.org/en/latest/virtualenv.html.

Activar el entorno virtual de CKAN, ejecutando el siguiente comando desde cualquier ubicación:

. /usr/lib/ckan/default/bin/activate

2. Es necesario ubicarse en el directorio donde se encuentran las extensiones CKAN. Si el CKAN se encuentra en el directorio /usr/lib/ckan/default/src/ckan, este directorio se encuentra en:

cd /usr/lib/ckan/default/src

3. En el directorio ckanext-prueba/ckanext/prueba crear un archivo dataset.py, con el siguiente contenido:

import ckan.plugins.toolkit as toolkit
import ckan.plugins as plugins
from logging import getLogger

log = getLogger(__name__)



class PruebaDatasetFormPlugin(plugins.SingletonPlugin,
                             toolkit.DefaultDatasetForm):
    """
    Custom dataset form plugin.
    """

    plugins.implements(plugins.IDatasetForm)
    plugins.implements(plugins.ITemplateHelpers)

    _custom_fields = ['valid_from', 'valid_until', 'valid_spatial', 'update_frequency']

    def get_helpers(self):
        return {
            'dpy_get_custom_fields': self._get_custom_fields,
        }

    def _get_custom_fields(self):
        return self._custom_text_fields

    def is_fallback(self):
        return True

    def package_types(self):
        return []

    def _modify_package_schema_for_edit(self, schema):
        for field_name in self._custom_fields:
            schema[field_name] = [toolkit.get_validator('ignore_missing'),
                    toolkit.get_converter('convert_to_extras')]

    def _modify_package_schema_for_read(self, schema):
        for field_name in self._custom_fields:
            schema[field_name] = [toolkit.get_converter('convert_from_extras'),
                toolkit.get_validator('ignore_missing')]

    def create_package_schema(self):
        schema = super(PruebaDatasetFormPlugin, self).create_package_schema()
        self._modify_package_schema_for_edit(schema)
        return schema

    def update_package_schema(self):
        schema = super(PruebaDatasetFormPlugin, self).update_package_schema()
        self._modify_package_schema_for_edit(schema)
        return schema

    def show_package_schema(self):
        schema = super(PruebaDatasetFormPlugin, self).show_package_schema()
        self._modify_package_schema_for_read(schema)
        return schema

El plugin implementa la interfaz IDatasetForm y hereda de la clase DefaultDatasetForm, lo cual permite redefinir ciertos aspectos del formulario de conjuntos de datos, heredando el comportamiento por defecto del mismo.

Para más información acerca de como modificar el formulario de conjuntos de datos, se recomienda consultar: http://docs.ckan.org/en/latest/extensions/plugin-interfaces.html#ckan.plugins.interfaces.IDatasetForm

A su vez, implementar la interfaz ITemplateHelpers permite definir helpers Jinja que pueden utilizarse en los templates de CKAN. El método get_helpers define los helpers que se expondrán a los templates.

Para más información acerca de como definir helpers Jinja para los templates de CKAN, se recomienda consultar: http://ckan.readthedocs.org/en/latest/extensions/plugin-interfaces.html#ckan.plugins.interfaces.ITemplateHelpers

4. Crear el directorio templates/package/snippets

mkdir -p templates/package/snippets

5. Dentro del directorio templates/package/snippets , crear el archivo prueba_package_custom_fields.html , con el siguiente contenido:

{% import 'macros/form.html' as form %}
{% set data = data or {} %}
{% set errors = errors or {} %}

{{ form.input('valid_from', type='date', label=_('Valid from'), id='field-valid-from',
    value=data['valid_from'], error=errors['valid_from'], classes=['control-medium']) }}

{{ form.input('valid_until',  type='date', label=_('Valid until'), id='field-valid-until',
    value=data['valid_until'], error=errors['valid_until'], classes=['control-medium']) }}

{% set departments = [_('Asunción'), _('San Pedro'), _('Concepción'), _('Cordillera'), _('Guairá'), _('Caaguazú'),
                        _('Caazapá'), _('Itapúa'), _('Misiones'), _('Paraguarí'), _('Alto Paraná'), _('Central'),
                        _('Ñeembucú'), _('Amambay'), _('Canindeyú'), _('Presidente Hayes'), _('Alto Paraguay'),
                        _('Boquerón')] %}

<div class="control-group control-medium">
    <label class="control-label" for="field-valid-spatial" class="control-label">{{_('Validez Espacial')}}</label>
    <div class="controls">
        <select id="field-valid-spatial" name="valid_spatial" style="width:334px;">
     {% set value = data.get('valid_spatial', '') %}
        <option value="" disabled selected></option>
     {% for option in departments %}
            <option value="{{option}}"{% if value == option %}selected='selected'{% endif %}>{{ option }}</option>
     {% endfor %}
        </select>
    </div>
</div>

{% set frequencies = [_('Anual'), _('Mensual'), _('Semanal'), _('Diaria')] %}
<div class="control-group control-medium">
    <label class="control-label" for="field-update-frequency" class="control-label">{{_('Frecuencia de Actualización')}}</label>
    <div class="controls">
        <select id="field-update-frequency" name="update_frequency" style="width:334px;">
     {% set value = data.get('update_frequency', '') %}
        <option value="" disabled selected></option>
     {% for option in frequencies %}
            <option value="{{option}}"{% if value == option %}selected='selected'{% endif %}>{{ option }}</option>
     {% endfor %}
        </select>
    </div>
</div>

Este archivo html define los campos adicionales que se añadirán al formulario de conjuntos de datos.

6. Dentro del directorio templates/package/snippets , crear el archivo package_metadata_fields.html , con el siguiente contenido:

{% ckan_extends %}

{% block package_metadata_fields_custom %}
    {% snippet 'package/snippets/prueba_package_custom_fields.html', data=data, errors=errors %}
{% endblock %}

Este archivo redefine el contenido del snippet templates/package/snippets/package_metadata_fields, insertando los campos adicionales que se definieron previamente.

Un snippet Jinja es un template pequeño que puede incluirse en varios templates. Para más información acerca de los snippets utilizados en CKAN, se recomienda consultar http://ckan.readthedocs.org/en/latest/theming/templates.html

7. Dentro del directorio templates/package/snippets , crear el archivo additional_info.html , con el siguiente contenido:

{% ckan_extends %}
{% block extras scoped %}
          {{ super() }}
          {% if pkg_dict.valid_from %}
          <tr>
            <th scope="row" class="dataset-label">{{_('Valid from')}}</th>
            <td class="dataset-details">{{ pkg_dict.valid_from}}</td>
          </tr>
          {% endif %}

          {% if pkg_dict.valid_until %}
          <tr>
            <th scope="row" class="dataset-label">{{_('Valid until')}}</th>
            <td class="dataset-details">{{ pkg_dict.valid_until}}</td>
          </tr>
          {% endif %}

          {% if pkg_dict.valid_spatial %}
          <tr>
            <th scope="row" class="dataset-label">{{_('Validez Espacial')}}</th>
            <td class="dataset-details">{{ pkg_dict.valid_spatial}}</td>
          </tr>
          {% endif %}

          {% if pkg_dict.update_frequency %}
          <tr>
            <th scope="row" class="dataset-label">{{_('Frecuencia de Actualización')}}</th>
            <td class="dataset-details">{{ pkg_dict.update_frequency}}</td>
          </tr>
          {% endif %}
{% endblock %}

Este archivo redefine la visualización por defecto de un conjunto de datos, añadiendo los campos adicionales definidos previamente.

8. Añadir el plugin creado a la lista de plugins de la extensión en el archivo ckanext-prueba/setup.py . Para esto es necesario agregar el plugin a los entry_points de la extensión, con lo cual la definición queda de la siguiente manera:

entry_points='''
        [ckan.plugins]
        prueba_theme=ckanext.prueba.theme:PruebaThemePlugin
        prueba_dataset = ckanext.prueba.dataset:PruebaDatasetFormPlugin
    '''

9. En el archivo development.ini de CKAN, añadir el plugin creado a la lista de plugins habilitados.

ckan.plugins = stats text_preview recline_preview prueba_theme prueba_dataset

10. Reinstalar la extensión de CKAN.

cd /usr/lib/ckan/default/src/ckanext-prueba
python setup.py develop

11. Iniciar el servidor de CKAN, utilizando paster desde cualquier directorio.

paster serve --reload /etc/ckan/default/development.ini

12. Ingresar a http://localhost:5000/ y crear un nuevo conjunto de datos. El paso final del formulario de creación de conjuntos de datos debe desplegarse de manera similar a la siguiente captura de pantalla:

dataset.png

Campos de definición de esquema para Recursos

Un conjunto de datos está compuesto por uno o más recursos, los cuales pueden registrarse al crear el conjunto de datos o posteriormente.Para cada recurso se registran atributos como nombre, descripción, URL y formato.

Puede resultar interesante contar con detalles de los datos pertenecientes al recurso, como los atributos que se definen, el tipo de dato y una descripción breve de cada uno. En este tutorial, los atributos que se añaden son un subconjunto de los definidos por la especificación JSON Table Schema.

Para añadir estos campos pueden seguirse los siguientes pasos:

1. En el directorio ckanext-prueba/ckanext/prueba crear un archivo resource.py, con el siguiente contenido:

import ckan.plugins as plugins
import json
from logging import getLogger

log = getLogger(__name__)

class PruebaResourceFormPlugin(plugins.SingletonPlugin):
    """
    Custom Resource form.
    """
    plugins.implements(plugins.IResourceController)
    plugins.implements(plugins.ITemplateHelpers)

    _schema_fields = []


    def get_helpers(self):
        return {
            'dpy_get_schema_fields': self._get_schema_fields,
        }

    def _get_schema_fields(self):
        return self._schema_fields

    def is_fallback(self):
        return True

    def package_types(self):
        return []

    def before_show(self, resource_dict):
        if 'schema' in resource_dict:
            try:
                self._schema_fields = json.loads(resource_dict['schema'])['fields']
            except ValueError:
                log.info("Entry 'schema' is not valid JSON. This should not happen.")
            except KeyError:
                log.info("Entry 'schema' does not have 'fields' attribute. This is mandatory JSON Table Schema syntax.")
        else:
            self._schema_fields = []

El plugin implementa la interfaz IResourceController, de modo a definir el método before_show. Este método permite realizar cambios al recurso antes de desplegarlo en la vista.

En este caso, esto se utilizará para exponer la lista de atributos del esquema del recurso como un helper para el template.

2. Dentro del directorio templates/package/snippets , crear el archivo prueba_resource_custom_fields.html , con el siguiente contenido:

{% resource 'ckanext-prueba/scripts/resource-form.js' %}
{% set datatypes = [_('string'), _('number'), _('integer'), _('date'), _('datetime'), _('boolean'),
    _('binary'), _('object'), _('geopoint'), _('geojson'), _('array'), _('any')] %}


<div data-module="custom-fields" data-module-field-selector=".schema-custom">


            {% if new_resource %}
                {% set count = 0 %}
            {% else %}
                {% set schema_fields = h.dpy_get_schema_fields() %}
                {% set count = schema_fields|length %}
             {% for field in schema_fields %}
                <div class="control-group schema-custom">

                <label class="control-label">{{_('Esquema')}}</label>

                <div class="controls">
                <div class="input-prepend">
                <label for="field-schema-{{loop.index0}}-attr" class="add-on">{{_('Atributo')}}</label><input id="field-schema-{{loop.index0}}-attr" type="text"
                                                                                 name="schema__{{loop.index0}}__attr"
                                                                                 value="{{field.get('name', '')}}"
                                                                                 placeholder="">
                <label for="field-schema-{{loop.index0}}-type" class="add-on">{{_('Tipo')}}</label>

                <select id="field-schema-{{loop.index0}}-type" name="schema__{{loop.index0}}__type">
                 {% set value = field.get('type', '') %}
                    <option value="" disabled selected></option>
                 {% for option in datatypes %}
                    <option value="{{option}}"{% if value == option %}selected='selected'{% endif %}>{{ option }}</option>
             {% endfor %}
                </select>
                </div>
                <div class="input-prepend" style="margin-top:10px;width: 70%;">
                    <label for="field-schema-{{loop.index0}}-description" class="add-on">{{_('Descripción')}}</label>
                    <textarea id="field-schema-{{loop.index0}}-description" class="resource_description"
                          name="schema__{{loop.index0}}__description" placeholder=""
                          style="width:100%;height:20px">{{field.get('description','')}}</textarea>
                <label class="checkbox btn btn-danger icon-remove" for="field-schema-{{loop.index0}}-remove">
                        <input type="checkbox" id="field-schema-{{loop.index0}}-remove" name="schema__{{loop.index0}}__deleted"> <span>_('Quitar')</span>
                </label>
                </div>
                    </div>

        </div>
                {% endfor %}

            {% endif %}
                    <div class="control-group control-custom">

             <label class="control-label">{{_('Esquema')}}</label>

            <div class="controls">
            <div class="input-prepend">
                <label for="field-schema-{{count}}-attr" class="add-on">{{_('Atributo')}}</label><input id="field-schema-{{count}}-attr" type="text"
                                                                                 name="schema__{{count}}__attr" value=""
                                                                                 placeholder="">
                <label for="field-schema-{{count}}-type" class="add-on">{{_('Tipo')}}</label>

                <select id="field-schema-{{count}}-type" name="schema__{{count}}__type">
                    <option value="" disabled selected></option>
                 {% for option in datatypes %}
                    <option value="{{option}}">{{ option }}</option>
                 {% endfor %}
                </select>
            </div>
            <div class="input-prepend" style="margin-top:10px;width: 70%;">
                <label for="field-schema-{{count}}-description" class="add-on">{{_('Descripción')}}</label>
                <textarea id="field-schema-{{count}}-description" class="resource_description"
                          name="schema__{{count}}__description" placeholder="" style="width:100%;height:20px"></textarea>
            </div>
            </div>
    </div>

</div>
    <input id="field-schema" type="hidden" name="schema" value="" placeholder="">

Este template define los campos adicionales relacionados al esquema del recurso. Incluir los campos adicionales dentro de un div con la clase control-custom permite que el número de atributos que pueden definirse sea dinámico, gracias a un script que forma parte de CKAN.

3. Dentro del directorio fanstatic/scripts , crear el archivo resource-form.js , con el siguiente contenido:

/**
Merge resource form extra fields into a unique
hidden 'schema' field.

Se supone que el nombre del atributo SIEMPRE
está primero.

Author: Rodrigo Parra
*/

/**
Añade un atributo al JSON de inputs de serializeObject.
@param elem El form input a procesar.
@param re Una expresión regular para determinar si el value del input corresponde al atributo.
@param o El JSON donde se añade el value del input.
@param attr Un string que identifica al atributo.

*/
function addAttribute(elem, re, o, attr){
    var found = elem.name.match(re);
    if(found && elem.value){
        var key = found[1];
        if(!o[key]) o[key]={};
        o[key][attr]= elem.value;
    }
    return o;
}

/**
@this {Form}
@return Retorna un JSON de los inputs del form que invoca la función.
El JSON que se retorna tiene el siguiente formato:
{
    0:{
    "name": "edad",
    "type": "integer",
    "description": "Cantidad de meses desde el nacimiento."
    },
    1:{
    "name": "apellido",
    "type": "string",
    "description": "Apellido de la persona."
    }
}
*/
$.fn.serializeObject = function()
{
    var o = {};
    var indexToValue = {}
    var a = this.serializeArray();
    var reAttr = /schema__(\d*)__attr/;
    var reType = /schema__(\d*)__type/;
    var reDescription = /schema__(\d*)__description/;

    $.each(a, function() {
        addAttribute(this, reAttr, o, 'name');
        addAttribute(this, reType, o, 'type');
        addAttribute(this, reDescription, o, 'description');
    });
    return o;
};

/**
@this {Form}
Retorna un JSON de los inputs extras del form que invoca la función, en formato JSON Table Schema.
*/
$.fn.jsonTableSchema = function(){
    var dict = $(this).serializeObject();
    var fields = [];
    for(var key in dict){
        var field_dict = {'name': dict[key]['name'], 'type': dict[key]['type'] , 'description': dict[key]['description']}
        fields.push(field_dict);
    }
    return {'fields': fields};
}

$(function() {
    $('.dataset-resource-form').submit(function() {
        $(".disabled :input").attr("disabled", true);
        var schema = $('.dataset-resource-form').jsonTableSchema();
        if(schema['fields'].length === 0){
             $('#field-schema').prop('disabled', true);
        }else{
             $('#field-schema').val(JSON.stringify(schema));
        }

        $("div[data-module='custom-fields'] :input").attr("disabled", true);
        return true;
    });
});

CKAN almacena todos los datos adicionales correspondientes a un recurso en un único campo de tipo JSON, por lo cual es necesario transformar la entrada del formulario antes de enviar los datos al servidor.

El código de resource-form.js transforma la lista de inputs del formulario a un objeto JSON que cumple con la especificación JSON Table Schema.

4. Dentro del directorio templates/package/snippets , crear el archivo resource_form.html , con el siguiente contenido:

{% ckan_extends %}

{% block metadata_fields %}
    {{ super() }}
    {% snippet 'package/snippets/prueba_resource_custom_fields.html', data=data, errors=errors, new_resource=True %}
{% endblock %}

Este template hereda y redefine el formulario para creación de recursos de CKAN, añadiendo los campos correspondientes al esquema.

5. Dentro del directorio templates/package/snippets , crear el archivo resource_edit_form.html , con el siguiente contenido:

{% ckan_extends %}

{% block metadata_fields %}
    {% snippet 'package/snippets/prueba_resource_custom_fields.html', data=data, errors=errors, new_resource=False %}
{% endblock %}

Este template hereda y redefine el formulario para edición de recursos de CKAN, añadiendo los campos correspondientes al esquema.

6. Dentro del directorio templates/package/snippets , crear el archivo resource_help.html , con el siguiente contenido:

<section class="module module-narrow module-shallow">
  <h2 class="module-heading"><i class="icon-info-sign"></i> {{ _('What\'s a resource?') }}</h2>
  <div class="module-content">
    <p>{{ _('Un recurso puede ser cualquier archivo o enlace a un archivo que contiene datos útiles.') }}</p>
  </div>
</section>

<section class="module module-narrow module-shallow">
  <h2 class="module-heading"><i class="icon-info-sign"></i> {{ _('Qué es el esquema de un recurso?') }}</h2>
  <div class="module-content">
    <p>{{ _('El esquema de un recurso es una descripción de los campos que contiene.') }}</p>
    <p>{{ _('Para cada campo del esquema, pueden definirse las siguientes propiedades:') }}</p>
    <p><strong>Atributo:</strong> El nombre del campo como se encuentra en el recurso.</p>
    <p><strong>Tipo:</strong> El tipo de datos de los valores del campo.</p>
    <p><strong>Descripción:</strong> Una breve descripción del campo.</p>
    <p>{{ _('Estas propiedades (y sus valores) se definen de acuerdo a la especificación') }}
        <a href="http://dataprotocols.org/json-table-schema/">{{ _('JSON Table Schema') }}</a>{{_('.')}}</p>
  </div>
</section>

Este template permite desplegar una breve descripción de JSON Table Schema al usuario, de modo a ayudarle a completar el formulario de recursos.

7. Añadir las siguientes líneas al archivo ckanext-prueba/ckanext/prueba/fanstatic/styles/prueba.css. Si el archivo no existe, crearlo.

.schema-custom.disabled select,
.schema-custom.disabled textarea {
  color: #aaaaaa;
  text-decoration: line-through;
  text-shadow: none;
  -webkit-box-shadow: none;
  -moz-box-shadow: none;
  box-shadow: none;
  background-color: #f3f3f3;
}

.schema-custom {
  font-size: 0;
}
.schema-custom label {
  margin-bottom: 0;
}
.schema-custom input {
  -webkit-border-radius: 0;
  -moz-border-radius: 0;
  border-radius: 0;
  width: 140px;
}
.schema-custom input:last-of-type {
  -webkit-border-radius: 0 3px 3px 0;
  -moz-border-radius: 0 3px 3px 0;
  border-radius: 0 3px 3px 0;
}
.schema-custom .checkbox {
  display: inline-block;
  margin-left: 5px;
}
.schema-custom .checkbox input {
  width: auto;
}
.schema-custom.disabled label,
.schema-custom.disabled input {
  color: #aaaaaa;
  text-decoration: line-through;
  text-shadow: none;
}
.schema-custom.disabled input {
  -webkit-box-shadow: none;
  -moz-box-shadow: none;
  box-shadow: none;
  background-color: #f3f3f3;
}
.schema-custom.disabled .checkbox {
  color: #444444;
  text-decoration: none;
}
.schema-custom .checkbox.btn {
  -webkit-border-radius: 15px;
  -moz-border-radius: 15px;
  border-radius: 15px;
  position: relative;
  top: 0;
  left: 5px;
  height: 1px;
  width: 9px;
  padding: 3px 8px;
  line-height: 18px;
}
.schema-custom .checkbox.btn span {
  display: none;
  width: 30px;
}
.schema-custom .checkbox.btn:before {
  position: relative;
  top: 1px;
  left: -1px;
  color: #fff;
}
.schema-custom .checkbox.btn input {
  display: none;
}
.schema-custom.disabled .checkbox.btn {
  color: #ffffff;
  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
  background-color: #cc5845;
  background-image: -moz-linear-gradient(top, #d36452, #c14531);
  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d36452), to(#c14531));
  background-image: -webkit-linear-gradient(top, #d36452, #c14531);
  background-image: -o-linear-gradient(top, #d36452, #c14531);
  background-image: linear-gradient(to bottom, #d36452, #c14531);
  background-repeat: repeat-x;
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd36452', endColorstr='#ffc14531', GradientType=0);
  border-color: #c14531 #c14531 #842f22;
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
  *background-color: #c14531;
  /* Darken IE7 buttons by default so they stand out more given they won't have borders */
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.schema-custom.disabled .checkbox.btn:hover,
.schema-custom.disabled .checkbox.btn:focus,
.schema-custom.disabled .checkbox.btn:active,
.schema-custom.disabled .checkbox.btn.active,
.schema-custom.disabled .checkbox.btn.disabled,
.schema-custom.disabled .checkbox.btn[disabled] {
  color: #ffffff;
  background-color: #c14531;
  *background-color: #ad3e2c;
}
.schema-custom.disabled .checkbox.btn:active,
.schema-custom.disabled .checkbox.btn.active {
  background-color: #983627 \9;
}
.schema-custom.disabled .checkbox.btn .caret {
  border-top-color: #ffffff;
  border-bottom-color: #ffffff;
}

Estas reglas CSS añaden el estilo necesario al botón de eliminar el metadato que aparecerá en el formulario de edición de un recurso.

8. Añadir el plugin creado a la lista de plugins de la extensión en el archivo ckanext-prueba/setup.py. Para esto es necesario agregar el plugin a los entry_points de la extensión, con lo cual la definición queda de la siguiente manera:

entry_points='''
        [ckan.plugins]
        prueba_theme=ckanext.prueba.theme:PruebaThemePlugin
        prueba_dataset = ckanext.prueba.dataset:PruebaDatasetFormPlugin
        prueba_resource = ckanext.prueba.resource:PruebaResourceFormPlugin
    '''

9. En el archivo development.ini de CKAN, añadir el plugin creado a la lista de plugins habilitados.

ckan.plugins = stats text_preview recline_preview prueba_theme prueba_dataset prueba_resource

10. Reinstalar la extensión de CKAN.

cd /usr/lib/ckan/default/src/ckanext-prueba
python setup.py develop

11. Parar el servidor CKAN (presionando CTRL + C) y reiniciarlo utilizando paster desde cualquier directorio.

paster serve --reload /etc/ckan/default/development.ini

12. Ingresar a http://localhost:5000/ y crear un nuevo conjunto de datos.
El segundo paso del formulario de creación de conjuntos de datos debe desplegarse de manera similar a la siguiente captura de pantalla:

resource3.png

Campos adicionales dinámicos para Recursos

Puede resultar útil permitir al usuario definir sus propios atributos relacionados a un recurso, como pares clave-valor. Para añadir estos campos pueden seguirse los siguientes pasos:

1. Editar el archivo resource.py. Añadir un helper para datos dinámicos:

 _schema_fields = []
 _dynamic_fields = []


    def get_helpers(self):
        return {
            'dpy_get_schema_fields': self._get_schema_fields,
            'dpy_get_dynamic_fields': self._get_dynamic_fields
        }

    def _get_schema_fields(self):
        return self._schema_fields

    def _get_dynamic_fields(self):
        return self._dynamic_fields

Además, añadir al final del método before_show la lógica para desplegar los atributos dinámicos:

if 'dynamic' in resource_dict:
    try:
        self._dynamic_fields = json.loads(resource_dict['dynamic'])
    except ValueError:
        log.info("Entry 'schema' is not valid JSON. This should not happen.")
    except KeyError:
        log.info("Entry 'schema' does not have 'fields' attribute. This is mandatory JSON Table Schema syntax.")
else:
    self._dynamic_fields = []
return resource_dict

2. Editar el archivo prueba_resource_custom_fields.html . Añadir al final la definición de los atributos adicionales:

{% if new_resource %}
        {% set extras = [] %}
    {% else %}
        {% set extras = h.dpy_get_dynamic_fields() %}
    {% endif %}

    {% block package_metadata_fields_custom %}
    {% block custom_fields %}
      {% snippet 'snippets/custom_form_fields.html', extras=extras, errors=errors, limit=3 %}
    {% endblock %}
  {% endblock %}

  <input id="field-extras" type="hidden" name="dynamic" value="" placeholder="">

3. Editar el archivo resource-form.js . Añadir el método para extraer los campos adicionales y convertirlos a un objeto JSON:

$.fn.getExtras = function()
{
    var o = {};
    var indexToValue = {}
    var a = this.serializeArray();
    var reKey = /extras__(\d*)__key/;
    var reValue = /extras__(\d*)__value/;

    $.each(a, function() {
        addAttribute(this, reKey, o, 'key');
        addAttribute(this, reValue, o, 'value');
    });

    var result = [];
    for(var key in o){
        result.push({
                key: o[key]['key'],
                value:o[key]['value']
        });
    }
    return result;
};

Modificar la definición de la función principal, agregando la lógica correspondiente a los campos dinámicos adicionales. Con esto, la función principal queda definida de la siguiente manera:

$(function() {
    $('.dataset-resource-form').submit(function() {
        $(".disabled :input").attr("disabled", true);
        var schema = $('.dataset-resource-form').jsonTableSchema();
        if(schema['fields'].length === 0){
             $('#field-schema').prop('disabled', true);
        }else{
             $('#field-schema').val(JSON.stringify(schema));
        }

        var extras = $('.dataset-resource-form').getExtras();
        if(extras.length === 0){
             $('#field-extras').prop('disabled', true);
        }else{
             $('#field-extras').val(JSON.stringify(extras));
        }

        $("div[data-module='custom-fields'] :input").attr("disabled", true);
        return true;
    });
});

4. Parar el servidor CKAN (presionando CTRL + C) y reiniciarlo utilizando paster desde cualquier directorio.

paster serve --reload /etc/ckan/default/development.ini

5. Ingresar a http://localhost:5000/ y crear un nuevo conjunto de datos. El segundo paso del formulario de creación de conjuntos de datos debe desplegarse de manera similar a la siguiente captura de pantalla:

resource4.png

Añadir campos adicionales al índice de búsqueda

Por defecto, la búsqueda de conjuntos de datos de CKAN no considera los campos adicionales relacionados a los recursos que forman parte del conjunto de datos. Para implementar esta funcionalidad, es necesario que CKAN incluya en su índice de búsqueda estos atributos.

Esto puede realizarse mediante los siguientes pasos:

1. Editar el plugin del archivo ckanext-prueba/ckanext/prueba/dataset.py . El plugin debe implementar la interfaz IPackageController y heredar del PackageController por defecto.

La definición inicial de la clase queda de la siguiente manera:

import ckan.controllers.package as package
import ckan.plugins.toolkit as toolkit
import ckan.plugins as plugins
import json

class PruebaDatasetFormPlugin(plugins.SingletonPlugin,
                             toolkit.DefaultDatasetForm, package.PackageController):
    """
    Custom dataset form plugin.
    """

    plugins.implements(plugins.IDatasetForm)
    plugins.implements(plugins.ITemplateHelpers)
    plugins.implements(plugins.IPackageController)

2. Añadir al final de la clase PruebaDatasetFormPlugin la definición del método before_index de la interfaz IPackageController.

    def read(self, entity):
        return entity

    def create(self, entity):
        return entity


    def edit(self, entity):
        return entity


    def authz_add_role(self, object_role):
        return object_role

    def authz_remove_role(self, object_role):
        return entity

    def delete(self, entity):
        return entity

    def after_create(self, context, pkg_dict):
        return pkg_dict

    def after_update(self, context, pkg_dict):
        return pkg_dict

    def after_delete(self, context, pkg_dict):
        return pkg_dict

    def after_show(self, context, pkg_dict):
        return pkg_dict

    def before_search(self, search_params):
        return search_params

    def after_search(self, search_results, search_params):
        return search_results

    def before_index(self, pkg_dict):
        data_dict = json.loads(pkg_dict['data_dict'])
        extras_name = set()
        extras_description = set()
        extras_attributes = set()
        extras_values = set()

        for resource in data_dict['resources']:
            if 'schema' in resource:
                try:
                    schema = json.loads(resource['schema'])
                    for field in schema['fields']:
                        extras_name.add(field['name'])
                        extras_description.add(field['description'])
                except ValueError:
                    log.info("Entry 'schema' is not valid JSON. This should not happen.")
                except KeyError:
                    log.info("JSON Table Schema syntax problem.")

            if 'dynamic' in resource:
                try:
                    for attr in json.loads(resource['dynamic']):
                        extras_attributes.add(attr['key'])
                        extras_values.add(attr['value'])
                except ValueError:
                    log.info("Entry 'dynamic' is not valid JSON. This should not happen.")
                except KeyError:
                    log.info("Syntax problem. 'key' and 'value' are mandatory for each attribute.")

        pkg_dict['extras_name'] = ' '.join(extras_name)
        pkg_dict['extras_description'] = ' '.join(extras_description)
        pkg_dict['extras_attributes'] = ' '.join(extras_attributes)
        pkg_dict['extras_values'] = ' '.join(extras_values)

        return pkg_dict

    def before_view(self, pkg_dict):
        return  pkg_dict

CKAN crea índices dinámicos de búsqueda para todos los atributos de un recurso cuyo nombre comience con 'extras'. El método before_index selecciona los atributos del esquema y los atributos dinámicos del recurso (que se definieron anteriormente) y los añade para su indexación.

La interfaz IPackageController permite personalizar la lógica de CKAN ante varios eventos como la creación, modificación, eliminación y búsqueda de conjuntos de datos. Para más información, consultar http://ckan.readthedocs.org/en/latest/extensions/plugin-interfaces.html#ckan.plugins.interfaces.IPackageController.

Para implementar la búsqueda de conjuntos de datos, CKAN utiliza el servidor de búsqueda Apache Solr. La regla de indexar todos los atributos cuyo nombre comience con 'extras' está definido en el archivo schema.xml de Solr. Para más información sobre Apache Solr, se recomienda consultar: http://lucene.apache.org/solr/documentation.html.

3. Parar el servidor CKAN (presionando CTRL + C) y reiniciarlo utilizando paster desde cualquier directorio.

paster serve --reload /etc/ckan/default/development.ini

4. Ingresar a http://localhost:5000/ y realizar una búsqueda por un atributo adicional de un recurso, de modo a verificar la funcionalidad.