diff --git a/ckanext/datagov_inventory/plugin.py b/ckanext/datagov_inventory/plugin.py index 0d0486cc..ad066bac 100644 --- a/ckanext/datagov_inventory/plugin.py +++ b/ckanext/datagov_inventory/plugin.py @@ -12,6 +12,7 @@ from ckanext.datagov_inventory import action from flask import Blueprint, redirect, session +from datetime import datetime, timezone import logging import re from urllib.parse import quote @@ -95,6 +96,7 @@ class Datagov_IauthfunctionsPlugin(plugins.SingletonPlugin): plugins.implements(plugins.IActions) plugins.implements(plugins.IConfigurer) plugins.implements(plugins.IBlueprint) + plugins.implements(plugins.IResourceController, inherit=True) def get_auth_functions(self): return {'format_autocomplete': restrict_anon_access, @@ -128,6 +130,37 @@ def update_config(self, config): def get_blueprint(self): return pusher + def after_resource_create(self, context, resource): + _touch_dataset_modified(context, resource['package_id']) + + def after_resource_update(self, context, resource): + _touch_dataset_modified(context, resource['package_id']) + + def before_resource_delete(self, context, resource, resources): + resource_id = resource.get('id') + for existing_resource in resources: + if existing_resource.get('id') == resource_id: + context['datagov_inventory_package_id'] = ( + existing_resource['package_id'] + ) + break + + def after_resource_delete(self, context, resources): + package_id = context.pop('datagov_inventory_package_id', None) + if package_id: + _touch_dataset_modified(context, package_id) + + +def _touch_dataset_modified(context, package_id): + """update dataset exported modified date (for any resource change).""" + modified = datetime.now(timezone.utc).isoformat( + timespec='milliseconds' + ).replace('+00:00', 'Z') + toolkit.get_action('package_patch')( + context, + {'id': package_id, 'modified': modified} + ) + def redirect_homepage(): if current_user.is_authenticated or g.user: diff --git a/ckanext/datagov_inventory/tests/test_resource_modified.py b/ckanext/datagov_inventory/tests/test_resource_modified.py new file mode 100644 index 00000000..9457ce2f --- /dev/null +++ b/ckanext/datagov_inventory/tests/test_resource_modified.py @@ -0,0 +1,79 @@ +from datetime import datetime + +import pytest + +from ckanext.datagov_inventory import plugin as plugin_module + + +@pytest.fixture +def plugin(): + return plugin_module.Datagov_IauthfunctionsPlugin() + + +@pytest.mark.parametrize( + 'hook_name', + ['after_resource_create', 'after_resource_update'] +) +def test_resource_save_touches_parent_dataset( + monkeypatch, plugin, hook_name): + # resource creation and updates should use the same parent-touch behavior. + touched = [] + monkeypatch.setattr( + plugin_module, + '_touch_dataset_modified', + lambda context, package_id: touched.append((context, package_id)) + ) + context = {'user': 'editor'} + + getattr(plugin, hook_name)( + context, + {'id': 'resource-id', 'package_id': 'dataset-id'} + ) + + assert touched == [(context, 'dataset-id')] + + +def test_resource_delete_touches_parent_when_no_resources_remain( + monkeypatch, plugin): + # package id must survive deletion when the remaining list is empty. + touched = [] + monkeypatch.setattr( + plugin_module, + '_touch_dataset_modified', + lambda context, package_id: touched.append((context, package_id)) + ) + context = {'user': 'editor'} + + plugin.before_resource_delete( + context, + {'id': 'resource-id'}, + [{'id': 'resource-id', 'package_id': 'dataset-id'}] + ) + plugin.after_resource_delete(context, []) + + assert touched == [(context, 'dataset-id')] + assert 'datagov_inventory_package_id' not in context + + +def test_touch_dataset_modified_uses_utc_iso_timestamp(monkeypatch): + # data.json exports this custom modified field, so keep its ISO UTC format. + calls = [] + + def get_action(name): + assert name == 'package_patch' + + def action(context, data_dict): + calls.append((context, data_dict)) + + return action + + monkeypatch.setattr(plugin_module.toolkit, 'get_action', get_action) + context = {'user': 'editor'} + + plugin_module._touch_dataset_modified(context, 'dataset-id') + + assert calls[0][0] is context + assert calls[0][1]['id'] == 'dataset-id' + modified = calls[0][1]['modified'] + assert modified.endswith('Z') + assert datetime.fromisoformat(modified.replace('Z', '+00:00')).tzinfo