Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aaf1401
feat: update python and ckan versions in GitHub actions
ChasNelson1990 Dec 8, 2025
e7f7e07
Replace deprecated mock import with unittest.mock
A-Souhei Jan 8, 2026
6b02957
Focus test workflow on CKAN 2.11 and failing tests only
A-Souhei Jan 8, 2026
0f51c86
Fix resource_autocomplete to search across resource fields
A-Souhei Jan 8, 2026
94168f6
Fix test_valid_activity_id for CKAN 2.11 activity plugin
A-Souhei Jan 8, 2026
353837b
Focus test workflow on first failing test
A-Souhei Jan 9, 2026
bb9d0e4
Fix resource_autocomplete multi-word query matching
A-Souhei Jan 9, 2026
e2ccc37
Focus test workflow on next failing test: Private Resource 01
A-Souhei Jan 9, 2026
77b0fd1
Fix resource_autocomplete multi-token query matching with threshold
A-Souhei Jan 9, 2026
830af0f
Run all test_resource_autocomplete tests to verify fix
A-Souhei Jan 9, 2026
f43856b
Run all test_actions.py tests
A-Souhei Jan 9, 2026
287b853
Enable activity plugin and update test fixtures for CKAN 2.11
A-Souhei Jan 9, 2026
90a6ac2
Document Fix #10: Activity plugin and test fixture updates (partial)
A-Souhei Jan 9, 2026
a9a6b20
Fix TestResourceAutocomplete fixtures and NotFound exception for CKAN…
A-Souhei Jan 9, 2026
a15d5bf
Add user context to resource actions for CKAN 2.11 activity plugin
A-Souhei Jan 9, 2026
2db337b
Fix dataset fixture and add user context to fork actions for CKAN 2.11
A-Souhei Jan 9, 2026
8be0141
Fix API authentication for CKAN 2.11 - Use JWT tokens instead of API …
A-Souhei Jan 9, 2026
35d1767
Document resource_autocomplete *:* wildcard limitation
A-Souhei Apr 17, 2026
cb0f0d8
Address Copilot review comments
A-Souhei Apr 17, 2026
f326df1
Remove PROGRESS.md migration notes file
A-Souhei Apr 17, 2026
8d77eee
Fix resource_autocomplete to use targeted Solr query instead of *:* w…
A-Souhei Apr 27, 2026
7996131
Revert resource_autocomplete Solr query to *:* wildcard
A-Souhei Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
name: Tests
on: [push, pull_request]
on: [pull_request]
jobs:
test:
name: CKAN 2.11 - Focus on failing tests
runs-on: ubuntu-latest
container:
# The CKAN version tag of the Solr and Postgres containers should match
# the one of the container the tests run on.
# You can switch this base image with a custom image tailored to your project
image: openknowledge/ckan-dev:2.9
image: ckan/ckan-dev:2.11-py3.10
options: --user root
services:
solr:
image: ckan/ckan-solr-dev:2.9
image: ckan/ckan-solr:2.11-solr9
postgres:
image: ckan/ckan-postgres-dev:2.9
image: ckan/ckan-postgres-dev:2.11
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand All @@ -29,7 +28,7 @@ jobs:
CKAN_REDIS_URL: redis://redis:6379/1

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Install requirements
# Install any extra requirements your extension has here (dev requirements, other extensions etc)
run: |
Expand All @@ -43,6 +42,9 @@ jobs:
sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini

ckan -c test.ini db init
ckan -c test.ini db upgrade
- name: Run tests
run: pytest --ckan-ini=test.ini --cov=ckanext.fork --disable-warnings ckanext/fork
run: |
# Run ALL tests
pytest --ckan-ini=test.ini --disable-warnings ckanext/fork/tests/

75 changes: 60 additions & 15 deletions ckanext/fork/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,51 @@ def resource_autocomplete(context, data_dict):
datasets = _get_dataset_from_resource_uuid(context, q_lower)

if not datasets:
datasets = toolkit.get_action('package_search')(context, {
"q": q,
"rows": 10,
# CKAN's Solr schema (ckan/config/solr/schema.xml) indexes `res_name` but
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This schema hasn't changed in years... so I don't see why this change is needed. It feels really bad for performance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: is needed because Solr doesn't index resource IDs

# NOT `res_id`, so package_search cannot find datasets by resource UUID
# substring (required by ADX-879). We fetch up to 100 datasets and filter
# resource IDs in Python. Capped by rows=100: instances with more datasets
# won't match resource IDs beyond the top 100 by default sort. Lifting
# this cap would require adding res_id to Solr (e.g. via
# IPackageController.before_index) so package_search can filter directly.
search_results = toolkit.get_action('package_search')(context, {
"q": "*:*",
"rows": 100,
"include_private": True
})['results']
})
datasets = search_results['results']

# Split query into tokens for dataset-level matching (allows partial matches like "01")
query_tokens = q_lower.split()

for dataset in datasets:

if not dataset['resources']:
continue

resources = []
has_matching_resource = False

for resource in dataset['resources']:
last_modified = toolkit.h.time_ago_from_timestamp(resource['last_modified'])
match = q_lower in resource['name'].lower() or q_lower == resource['id']
# For resources: match based on number of matching tokens
# - Single token queries: require exact match (full string)
# - Multi-token queries: require at least 2 tokens to match
resource_lower = resource['name'].lower()
resource_id_lower = resource['id'].lower()

if len(query_tokens) == 1:
# Single token: use full string matching
match = q_lower in resource_lower or q_lower in resource_id_lower
else:
# Multi-token: require at least 2 tokens to match
match = sum(
1 for token in query_tokens
if token in resource_lower or token in resource_id_lower
) >= 2

if match:
has_matching_resource = True
resources.append({
'id': resource['id'],
'name': resource['name'],
Expand All @@ -89,15 +118,31 @@ def resource_autocomplete(context, data_dict):
})

organization_title = dataset.get('organization', {}).get('title', "")
match = q_lower in dataset['name'].lower() or q_lower in dataset['title'].lower()
pkg_list.append({
'id': dataset['id'],
'name': dataset['name'],
'title': dataset['title'],
'owner_org': organization_title,
'match': match,
'resources': resources
})
# For datasets: match any token from query (allows "01" in "Resource 01" to match "test-dataset-01")
dataset_match = any(
token in dataset['name'].lower() or token in dataset['title'].lower()
for token in query_tokens
)

# Only include dataset if it matches at dataset level OR has matching resources
if dataset_match or has_matching_resource:
pkg_list.append({
'id': dataset['id'],
'name': dataset['name'],
'title': dataset['title'],
'owner_org': organization_title,
'match': dataset_match,
'resources': resources
})

# Sort results: datasets with name/title matches first, then resource-only matches
# Within each group, maintain the order from package_search
for i, item in enumerate(pkg_list):
item['_original_index'] = i
pkg_list.sort(key=lambda x: (not x['match'], x['_original_index']))
# Remove the temporary index
for item in pkg_list:
del item['_original_index']

return pkg_list

Expand All @@ -118,7 +163,7 @@ def _get_dataset_from_resource_uuid(context, uuid):
{"id": resource['package_id']}
)
return [package]
except toolkit.NotFound:
except logic.NotFound:
return []


Expand Down
28 changes: 27 additions & 1 deletion ckanext/fork/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import pytest
from ckan import model
from ckan.tests import factories
from ckan.tests.helpers import call_action


@pytest.fixture
def forked_data():
"""
Creates test data with fork metadata (requires activity plugin).

Note: Tests using this fixture must use:
@pytest.mark.usefixtures('clean_db_with_migrations', 'with_plugins')
"""
giftless_metadata = {
"sha256": "dummysha",
"size": 999,
"lfs_prefix": "test/resource",
"url_type": "upload"
}
user = factories.User()
organization = factories.Organization()
forked_dataset = factories.Dataset(owner_org=organization['id'])
forked_resource = factories.Resource(
package_id=forked_dataset['id'],
**giftless_metadata
)
call_action('package_patch', id=forked_dataset['id'], notes='An activity')
# Trigger activity creation (factories don't create activities)
call_action('package_patch', context={'user': user['name']}, id=forked_dataset['id'], notes='An activity')
forked_activity_id = call_action(
'package_activity_list',
id=forked_dataset['id']
Expand All @@ -27,3 +36,20 @@ def forked_data():
'resource': forked_resource,
'activity_id': forked_activity_id
}


@pytest.fixture
def clean_db_with_migrations(clean_db):
"""
Extends the standard clean_db fixture to add activity plugin schema changes.

In CKAN 2.11, the activity plugin adds a permission_labels column (text[])
to the activity table. The clean_db fixture rebuilds the database without
plugin-specific migrations, so we manually add the column here.
"""
# Add the permission_labels column required by CKAN 2.11 activity plugin
model.Session.execute("""
ALTER TABLE activity
ADD COLUMN IF NOT EXISTS permission_labels text[];
""")
model.Session.commit()
Loading