Method alone didn't tell readers whether a call needs an API key - GET happens to line up with the three public actions here, but that's incidental, not a rule. The Auth column states it directly; full per-action nuance (e.g. author_create/author_delete depending on ckan.auth.user_*_groups) stays in Authentication and permissions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| ckanext | ||
| plans | ||
| solr | ||
| .gitignore | ||
| API.md | ||
| DEVELOPER.md | ||
| FUNDING.md | ||
| LICENSE.md | ||
| MANIFEST.in | ||
| README.md | ||
| pytest.ini | ||
| requirements.txt | ||
| setup.cfg | ||
| setup.py | ||
| test.ini | ||
README.md
ckanext-d4science_authors
Ever tried to answer "who actually wrote this dataset?" in CKAN? CKAN does
have an author field on every dataset, but it's really meant for the
person who uploaded it - not the researchers, scientists, or contributors
who actually produced the work. There's no page for them, no way to click
through to "everything by this person," no ORCID, no affiliation.
This extension fixes that by turning Author into a first-class CKAN entity - with its own page, its own URL, its own search facet - built directly on the same machinery CKAN already uses for Groups. If you already know how Groups work in CKAN, you already know most of how this works too.
Concretely, that gets you:
- Full CRUD (create, read, edit, delete) via web UI and REST API
- Member management – associate CKAN users with an author as admin, editor, or member
- Faceted search – filter datasets by author via SOLR
- Activity streams and follower support
- Author-specific extra fields: ORCID, affiliation, website, email plus arbitrary custom extras
(stored in CKAN's own
group_extratable - no extra table to keep in sync) - Custom SOLR fields (
author_ids,author_names,author_titles) indexed on every package document - Collision-safe name generation: an Author name shares CKAN's global
group.namenamespace with Groups and Organizations -author_createauto-generates a free slug from the title when none is supplied - Author ≠ Item Creator: the CKAN built-in
authorfield on a dataset represents the item creator (the person who registered the dataset). Scientific authors are managed as independent Author entities and linked to datasets through themembertable.
Table of contents
- Requirements
- Installation
- Configuration
- Data Model
- URLs
- REST API Actions — full reference in API.md
- Author-specific Extra Fields
- Template Helpers
- SOLR Fields
- Theme Integration
- Development
- License
- Funding
- Authors
Requirements
Nothing exotic - if your CKAN instance already meets these, you're set:
| Dependency | Version |
|---|---|
| CKAN | ≥ 2.10 |
| Python | ≥ 3.8 |
| PostgreSQL | ≥ 12 |
| SOLR | ≥ 8 (managed-schema) |
Installation
Five short steps: install the code, tell CKAN to load it, teach SOLR about the new fields, reindex, and you're done - there's no database migration to run, since Authors live entirely in tables CKAN already has.
1. Clone and install
git clone https://github.com/d4science/ckanext-d4science_authors
cd ckanext-d4science_authors
pip install -e .
2. Enable the plugin
Add d4science_authors to the ckan.plugins setting in your CKAN .ini file:
ckan.plugins = ... d4science_authors
3. Apply SOLR schema additions
The plugin indexes author data on every package document. The following new fields must be present in your SOLR schema before running a full reindex.
Option A – Docker (recommended)
solr/Dockerfile only adds the author fields - it doesn't know or care
whether the base image also has spatial/DCAT fields, so it works layered on
top of any existing CKAN Solr image (plain CKAN, or CKAN + spatial):
docker build \
--build-arg BASE_IMAGE=<your-ckan-solr-image>:latest \
-t d4science/ckan-solr-authors:latest \
-f solr/Dockerfile \
solr/
Option B – Managed Schema API (SOLR 7+)
for FIELD in author_ids author_names author_titles; do
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d "{\"add-field\":{\"name\":\"$FIELD\",\"type\":\"string\",\"multiValued\":true,\"indexed\":true,\"stored\":true}}"
done
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d '{"add-field":{"name":"author_names_text","type":"text_general","multiValued":true,"indexed":true,"stored":false}}'
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d '{"add-copy-field":{"source":"author_names","dest":"text"}}'
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d '{"add-copy-field":{"source":"author_titles","dest":"text"}}'
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d '{"add-copy-field":{"source":"author_names","dest":"author_names_text"}}'
curl -X POST http://localhost:8983/solr/ckan/schema \
-H 'Content-Type: application/json' \
-d '{"add-copy-field":{"source":"author_titles","dest":"author_names_text"}}'
curl "http://localhost:8983/solr/admin/collections?action=RELOAD&name=ckan"
Option C – schema.xml
Refer to solr/managed-schema-additions.xml
for the complete field and copyField definitions to paste into your schema.
4. Reindex datasets
After the SOLR schema changes, run a full reindex so existing packages get the new author fields:
ckan -c /etc/ckan/default/production.ini search-index rebuild
5. Database
No new tables. Author-specific fields (orcid, affiliation, website,
email)
live in CKAN's own group_extra table via convert_to_extras/
convert_from_extras (see logic/schema.py) - the same mechanism CKAN
uses for any Group's custom extras. Dataset ↔ Author associations are
stored in CKAN's existing member table (table_name='package', group
type='author').
Configuration
The plugin activates fully through ckan.plugins - no mandatory configuration
keys. Two optional settings control where the Authors search facet and
dataset-page list appear, and where the facet sits among the other facets:
# Which pages show the Authors facet, plus (via 'dataset_view') the
# read-only Authors list on a single dataset's own page. Space-separated
# list, any of: dataset, group, organization, author, dataset_view.
# Unset (default): enable every supported page type.
ckanext.d4science_authors.facet_pages = organization author dataset_view
# 1-based position among that page's facets. Existing facets shift down
# rather than being replaced. Unset, non-numeric, or a number greater than
# the facet count (default): append at the end (today's behaviour).
ckanext.d4science_authors.facet_position = 2
dataset_viewis not a SOLR facet. A single dataset's page has no result list to filter, so there's nothing to facet on. Instead this value gates whetherpackage/read_base.htmlrenders the read-onlypackage/snippets/dataset_authors.htmlsnippet (the Author entities linked to that specific dataset) - reusing the same config knob as the other page types, everywhere-by-default when unset.
Why
organization/authorshare one hook, but can still be configured separately: CKAN core'sckan/views/group.py_update_facet_titles()only callsIFacets.group_facets()when the page'sgroup_typeis literally"group"- every other type, includingorganizationand our ownauthor, is routed throughIFacets.organization_facets()instead. The plugin'sorganization_facets()still receives the actual type string as an argument, sockanext.d4science_authors.facet_pagescan listorganizationwithoutauthor(or vice versa) and it's still honoured correctly - it's only the method name CKAN core picked that's shared, not the type it applies to.
Data Model
None of this required a new database table - Authors quietly reuse tables CKAN already has for Groups. Here's how the pieces fit together.
Author vs Item Creator
The CKAN built-in author field on a dataset is treated as the item creator
(the person or organisation who registered the dataset in CKAN). Scientific
Authors — the people who created the work — are modelled as independent
entities and linked to datasets through the member table.
CKAN core tables (reused, no plugin-specific tables)
| Table | Role |
|---|---|
group (type=author) |
Main author entity (name, title, description, image) |
group_extra |
orcid, affiliation, website, email (via convert_to_extras/convert_from_extras, see logic/schema.py) + arbitrary custom extras |
member |
Dataset ↔ Author associations (table_name='package', type='author') |
Name collisions across group types
group.name has a single unique constraint shared by all group types
(group, organization, author) - an Author can collide with an existing
Group or Organization name, not just with another Author. author_create
handles this automatically: if name isn't supplied, one is generated from
title via ckanext.d4science_authors.naming.generate_unique_author_name,
which appends a numeric suffix (-2, -3, ...) until it finds a name not
already used by any Group row. If you pass name explicitly and it collides,
you'll get CKAN's normal "Group name already exists" validation error - pick
a different one, same as creating a Group or Organization today.
URLs
Since an Author is really a Group under the hood, it gets the exact same set
of pages a Group would - just served under /author/ instead of /group/.
All routes are registered automatically by CKAN's IGroupForm machinery when
group_types() returns ['author'] (see register_group_plugin_rules in
CKAN core's ckan/views/group.py). Web UI auth checks always run against the
core group_* action names directly (e.g. group_update), not the
author_* wrappers in REST API Actions - the two share
the same underlying auth logic (ckanext/d4science_authors/auth.py just
delegates to it), so permissions are consistent either way. See the note
under REST API Actions for exactly what "Auth required"
depends on for new/edit/delete.
| URL | Method | Auth required | Description |
|---|---|---|---|
/author/ |
GET | No | List all authors |
/author/new |
GET, POST | Yes – gated by ckan.auth.user_create_groups |
Create a new author |
/author/<id> |
GET | No | Author page – associated datasets |
/author/about/<id> |
GET | No | Author biography, ORCID, affiliation, website, and email for sysadmins |
/author/edit/<id> |
GET, POST | Yes – logged-in user with editor/admin capacity on this author, or sysadmin |
Edit author |
/author/members/<id> |
GET | No | View members (read-only; managing roles happens via member_new/member_delete) |
/author/member_new/<id> |
GET, POST | Yes – logged-in user with admin capacity on this author, or sysadmin |
Add/edit a member's role |
/author/delete/<id> |
GET, POST | Yes – gated by ckan.auth.user_delete_groups |
Delete author |
/author/followers/<id> |
GET | No | Followers list |
/author/activity/<id>is not available. CKAN core hardcodes the activity-stream route to the literal/group/activity/<id>and/organization/activity/<id>paths (ckanext/activity/views.py) instead of generating one per custom group type the wayregister_group_plugin_rulesdoes for the routes above. The plugin still implementsactivity_template()(anIGroupFormhook) andauthor_show/author_listwork fine from the API, but there is currently no page to render an Author's activity stream through the web UI. If this is needed, the plugin would have to register its own/author/activity/<id>route (e.g. viaIBlueprint).
REST API Actions
The extension exposes Author management through CKAN's standard Action API.
Each custom action wraps the corresponding core group_* action and forces
type=author, so clients never need to pass the entity type themselves.
Full endpoint reference, request/response examples, and authentication
rules: API.md.
| Action | Method | Auth | Purpose |
|---|---|---|---|
author_list |
GET | No | List active authors |
author_show |
GET | No | Retrieve one author and its metadata |
author_package_list |
GET | No | List the public datasets linked to an author |
author_create |
POST | Yes | Create an author |
author_update |
POST | Yes | Replace an author's current metadata |
author_delete |
POST | Yes | Soft-delete an author |
author_member_create |
POST | Yes | Add a user or change their role |
author_member_delete |
POST | Yes | Remove a user from an author |
group_purge |
POST | Yes | Permanently delete an author (core CKAN action) |
Linking authors to a dataset (author_ids on package_create/
package_update) is also documented there:
Linking authors to a dataset.
Author-specific Extra Fields
Beyond the fields every CKAN Group already has (title, description, image),
an Author can carry a few extras that make sense for a person, not an
organisation. These fields are available on create/update (both via web UI
and API) and are returned by author_show:
| Field | Type | Description |
|---|---|---|
orcid |
string | ORCID identifier (bare ID or full URL) |
affiliation |
string | Institutional affiliation |
website |
string | Personal or institutional website URL |
email |
string | Valid email address; the Author about page renders it only for sysadmins |
extras |
list of {key, value} |
Arbitrary custom properties (same as CKAN dataset extras) |
Email visibility: the sysadmin check currently applies only to the
/author/about/<id>template. It is not an API privacy boundary:author_showis public and its schema includes
The d4s_authors_format_orcid_url template helper converts bare IDs to full
https://orcid.org/... URLs automatically.
Custom extras can be set via API:
curl -X POST .../api/3/action/author_update \
-H "Authorization: <API_KEY>" \
-d '{"id": "jane-smith", "extras": [{"key": "department", "value": "Data Science"}]}'
Or via the web UI through the "Add field" widget at the bottom of the Author create/edit form.
Template Helpers
Building a theme and need Author info inside a template? These Jinja helpers do the lookups for you, no need to call the Action API by hand:
| Helper | Arguments | Returns |
|---|---|---|
d4s_authors_get_author_url(name) |
author slug | Canonical URL for the author read page |
d4s_authors_get_packages_count(id) |
author name or UUID | Count of public datasets |
d4s_authors_format_orcid_url(orcid) |
bare ID, partial, or full URL | Full https://orcid.org/… URL |
d4s_authors_get_author_extra(author_id) |
author UUID | Dict with orcid, affiliation, website, email |
d4s_authors_get_dataset_authors(package_id) |
package UUID or name | List of linked Author dicts, including standard extras |
d4s_authors_author_list() |
— | List of all active Author entities (for select widgets) |
d4s_authors_dataset_view_enabled() |
— | Whether the linked-Author list is enabled on a dataset page |
SOLR Fields
To make "filter datasets by author" possible, the plugin teaches SOLR a handful of new fields at index time:
| Field | Type | Multi-valued | Purpose |
|---|---|---|---|
author_ids |
string |
yes | UUID(s) of linked Author entities |
author_names |
string |
yes | Slug names – used for faceting and filter queries |
author_titles |
string |
yes | Display names – shown in facet labels |
author_names_text |
text_general |
yes | Analysed copy for keyword search |
copyField copies a field's value into another field at index time, so the
same value can be indexed under two different analysis chains at once
(you can't apply two analyzers to the same field):
author_names/author_titlesarestring+docValues="true"- un-tokenized, needed for exact-match filtering (fq=author_names:...) and faceting (facet.field=author_names); astringfield can't do partial matches ("jane" wouldn't match "jane-smith").- Copied into
text(CKAN's general catchall, also fed by title/notes/tags) so author names/titles are found through the standard CKAN search box - searching "Jane Smith" surfaces her datasets even thoughpackage.author(the item creator) is someone else. - Also copied into the dedicated
text_generalfieldauthor_names_text, which is tokenized (so it supports partial/full-text matching, unlikeauthor_names/author_titles) but kept separate from thetextcatchall- a future author-only search box could query just this field without the noise of matching on titles/notes/tags too.
author_names_textis currently unused. It's populated by thesecopyFields, but no action, helper, or template in this plugin queries it
- there's no author-only search feature built yet. It's kept in the schema as groundwork for one, not because anything reads it today.
These fields are populated by IPackageController.before_dataset_index(),
which reads from the member table — not from the built-in package.author
field (which is the item creator).
The author_names facet is registered automatically via IFacets.dataset_facets:
facets_dict['author_names'] = _('Authors')
Solr Docker images
| File | Description |
|---|---|
solr/Dockerfile |
Patch-only: adds author fields on top of an existing image (any base - plain CKAN or CKAN + spatial) |
solr/managed-schema-additions.xml |
Documented field definitions for manual application |
solr/managed-schema-addtions.md |
Same field definitions, in prose/Markdown form for a schema maintainer who isn't applying the XML directly |
Theme Integration
The plugin automatically extends the dataset read page and also ships a form snippet that a CKAN theme can include when editing datasets.
Dataset read page — show linked authors
templates/package/read_base.html adds the linked-Author block to the
secondary content automatically when dataset_view is enabled in
ckanext.d4science_authors.facet_pages. The block displays each Author's
name, profile link, ORCID badge, and affiliation; it does not render email.
The underlying snippet remains available for themes that need it in another location:
{% snippet 'package/snippets/dataset_authors.html', pkg=pkg %}
Dataset create/edit form — author selector
{# e.g. inside templates/package/snippets/package_basic_fields.html #}
{% snippet 'package/snippets/dataset_authors_form.html', data=data %}
Renders a multi-select <select> listing all active Author entities. On
submit the selected UUIDs are posted as a JSON array in the hidden field
author_ids, which the plugin's before_dataset_create /
before_dataset_update hooks process transparently.
Development
Contributing or just poking around the code? Here's what you need to know. For the architecture, internal data flows, extension points, and legacy migration runbook, see the Developer guide.
Running tests
# Unit tests (no CKAN stack required)
cd /path/to/ckanext-d4science_authors
pip install -e .
pip install -r requirements.txt
pytest ckanext/d4science_authors/tests/
Project layout
ckanext-d4science_authors/
├── setup.py / setup.cfg ← packaging & entry point
├── MANIFEST.in ← include non-Python files
├── requirements.txt
├── pytest.ini
├── solr/
│ ├── Dockerfile ← patch: adds author fields on top of an existing image
│ ├── managed-schema-additions.xml ← documented field definitions
│ └── managed-schema-addtions.md ← same, in prose/Markdown form
└── ckanext/
├── __init__.py ← namespace package
└── d4science_authors/
├── plugin.py ← IGroupForm, IFacets, IActions, IAuthFunctions, IPackageController, IClick
├── naming.py ← collision-safe Author slug generation
├── cli.py ← `ckan d4s-authors migrate-legacy-authors` one-off migration
├── actions.py ← author_* logic actions
├── auth.py ← authorization functions
├── helpers.py ← template helpers
├── logic/
│ └── schema.py ← form_to_db / db_to_form schemas (incl. custom extras)
├── assets/
│ ├── webassets.yml
│ └── css/d4science_authors.css
├── templates/
│ ├── author/ ← index, read, edit, new, about, members…
│ │ └── snippets/ ← info, author_list, author_item, author_form…
│ └── package/
│ ├── read_base.html ← automatic dataset-page integration
│ └── snippets/
│ ├── dataset_authors.html ← read-only author list for dataset page
│ └── dataset_authors_form.html ← author selector for dataset form
└── tests/
└── test_plugin.py
License
This project is licensed under the terms specified in the LICENSE.md file.
Funding
See FUNDING.md
Authors
- Francesco Mangiacrapa (ORCID) Istituto di Scienza e Tecnologie dell'Informazione 'A. Faedo', Consiglio Nazionale delle Ricerche, Pisa, Italy
- AI-assisted development:
- Claude