author Denis Laxalde <denis.laxalde@logilab.fr>
Thu, 18 Dec 2014 11:03:00 +0100
changeset 44 359eb926e733
parent 40 d7f97a999d12
child 53 d5eae92f3d29
permissions -rw-r--r--
[hooks] Trigger CKAN resource creation upon addition of the link to dataset relation Related to #4753964.

# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
# This program 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 Lesser General Public License for more
# details.
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

"""cubicweb-ckanpublish specific hooks and operations"""

from requests.exceptions import RequestException

from cubicweb import ValidationError, role
from cubicweb.predicates import adaptable, score_entity, PartialPredicateMixIn
from cubicweb.server import hook

from cubes.ckanpublish.utils import (ckan_post, CKANPostError,

def _ckan_action(config, eid, action, **kwargs):
    """Run `ckan_post` and eventually raise ValidationError."""
        return ckan_post(config, action, **kwargs)
    except (CKANPostError, RequestException) as exc:
        raise ValidationError(eid, {'ckan_dataset_id': unicode(exc)})

def create_dataset(config, eid, data):
    """Create a CKAN dataset and set `ckan_dataset_id` attribute or
    respective entity. Return the dataset id.
    res = _ckan_action(config, eid, 'package_create', data=data)
    return res['id']

def update_dataset(config, eid, datasetid, udata):
    """Update an existing CKAN dataset"""
    data = _ckan_action(config, eid, 'package_show', data={'id': datasetid})
    _ckan_action(config, eid, 'package_update', data=data)

def delete_dataset(config, eid, datasetid):
    """Delete a CKAN dataset"""
    _ckan_action(config, eid, 'package_delete', data={'id': datasetid})

def create_dataset_resource(config, eid, datasetid, metadata, data):
    """Add a resource to an existing CKAN dataset"""
    metadata['package_id'] = datasetid
    res = _ckan_action(config, eid, 'resource_create', data=metadata,
                       files=[('upload', data)])
    return res['id']

def update_dataset_resource(config, eid, resourceid, metadata, data):
    """Update an existing CKAN resource."""
    metadata['id'] = resourceid
    _ckan_action(config, eid, 'resource_update', data=metadata,
                 files=[('upload', data)])

def delete_dataset_resource(config, eid, resourceid):
    """Delete a CKAN resource"""
    _ckan_action(config, eid, 'resource_delete', data={'id': resourceid})

class DeleteCKANDataSetHook(hook.Hook):
    """Delete CKAN dataset upon deletion of the corresponding entity"""
    __regid__ = 'ckanpublish.delete-ckan-dataset'
    __select__ = (hook.Hook.__select__ & ckan_instance_configured &
                  adaptable('ICKANDataset') &
                  score_entity(lambda x: x.ckan_dataset_id))
    events = ('before_delete_entity', )

    def __call__(self):

class AddOrUpdateCKANDataSetHook(hook.Hook):
    """Add or update a CKAN dataset upon addition or update of an entity"""
    __regid__ = 'ckanpublish.add-update-ckan-dataset'
    __select__ = (hook.Hook.__select__ & ckan_instance_configured &
    events = ('after_add_entity', 'after_update_entity', )

    def __call__(self):

class CKANDatasetOp(hook.DataOperationMixIn, hook.Operation):
    """Operation to create, update or delete a CKAN dataset"""

    def precommit_event(self):
        for eid in self.get_data():
            entity = self.cnx.entity_from_eid(eid)
            datasetid = entity.ckan_dataset_id
            config = self.cnx.vreg.config
            if self.cnx.deleted_in_transaction(eid):
                delete_dataset(config, eid, datasetid)
                self.info('deleted CKAN dataset %s', datasetid)
                cpublish = entity.cw_adapt_to('ICKANDataset')
                data = cpublish.ckan_data()
                if datasetid is not None:
                    update_dataset(config, eid, datasetid, data)
                    self.info('updated %s fields in CKAN dataset %s',
                              data.keys(), datasetid)
                    datasetid = create_dataset(config, eid, data)
                        'SET X ckan_dataset_id %(dsid)s WHERE X eid %(eid)s',
                        {'eid': eid, 'dsid': datasetid})
                    self.info('created CKAN dataset %s', datasetid)

class DeleteCKANResourceHook(hook.Hook):
    """Delete CKAN resource upon deletion of the corresponding entity"""
    __regid__ = 'ckanpublish.delete-ckan-resource'
    __select__ = (hook.Hook.__select__ & ckan_instance_configured &
                  adaptable('ICKANResource') &
                  score_entity(lambda x: x.ckan_resource_id))
    events = ('before_delete_entity', )

    def __call__(self):

class partial_match_rtype(PartialPredicateMixIn, hook.match_rtype):
    """Same as :class:~`cubicweb.server.hook.match_rtype`, but will look for
    attributes `rtype`, `role`, `frometypes` and `toetypes` on the selected
    class to get information which is otherwise expected by the initializer.
    def __init__(self, *expected, **more):
        super(partial_match_rtype, self).__init__()

    def complete(self, cls):
        self.expected = (cls.rtype, )
        self.role = role(cls)
        self.frometypes = getattr(cls, 'frometypes', None)
        self.toetypes = getattr(cls, 'toetypes', None)

class LinkResourceToDatasetHook(hook.Hook):
    """Create a CKAN dataset upon link of a resource-like entity to a
    dataset-like entity.

    Actual implementations should at least fill the `rtype` attribute.
    __regid__ = 'ckanpublish.link-ckan-resource-to-ckan-dataset'
    __select__ = (hook.Hook.__select__ & ckan_instance_configured &
    __abstract__ = True
    events = ('after_add_relation', )
    rtype = None  # Use to fill the `expected` argument of match_rtype.
    role  = 'object'
    frometypes = None
    toetypes = None

    def __call__(self):
        eid = {'subject': self.eidfrom, 'object': self.eidto}[self.role]

class UpdateCKANResourceHook(hook.Hook):
    """Update a CKAN resource upon update of an resource-like entity"""
    __regid__ = 'ckanpublish.update-ckan-resource'
    __select__ = (hook.Hook.__select__ & ckan_instance_configured &
    events = ('after_update_entity', )

    def __call__(self):

class CKANResourceOp(hook.DataOperationMixIn, hook.Operation):
    """Operation to create, update or delete a CKAN resource"""

    def precommit_event(self):
        for eid in self.get_data():
            entity = self.cnx.entity_from_eid(eid)
            resourceid = entity.ckan_resource_id
            iresource = entity.cw_adapt_to('ICKANResource')
            config = self.cnx.vreg.config
            if self.cnx.deleted_in_transaction(eid) and resourceid is not None:
                delete_dataset_resource(config, eid, resourceid)
                self.info('deleted resource %s', resourceid)
                metadata = iresource.ckan_metadata()
                data = iresource.read()
                if resourceid is None:
                    dataset = iresource.dataset
                    assert dataset, 'no dataset for resource #%d' % eid
                    if not dataset.ckan_dataset_id:
                        self.error('skipping resource #%d as its dataset %#d is '
                                   'not in the CKAN instance', eid, dataset.eid)
                    resourceid = create_dataset_resource(
                        config, eid, dataset.ckan_dataset_id, metadata, data)
                        'SET X ckan_resource_id %(rid)s WHERE X eid %(eid)s',
                        {'eid': eid, 'rid': resourceid})
                    self.info('added resource %s', resourceid)
                        config, eid, resourceid, metadata, data)
                    self.info('updated resource %s', resourceid)