Go to file
Francesco Mangiacrapa 8855465c3c Add an Auth column to the Actions-at-a-glance tables
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>
2026-07-31 17:04:31 +02:00
ckanext Show authors on the dataset page and add an Author email field 2026-07-31 16:45:03 +02:00
plans Show authors on the dataset page and add an Author email field 2026-07-31 16:45:03 +02:00
solr Remove the stale Solr schema snapshot, rewrite additions doc as prose 2026-07-31 16:54:04 +02:00
.gitignore just created the plugin 2026-04-13 12:11:03 +02:00
API.md Add an Auth column to the Actions-at-a-glance tables 2026-07-31 17:04:31 +02:00
DEVELOPER.md Extract REST API Actions into its own API.md 2026-07-31 17:00:58 +02:00
FUNDING.md updated the docs 2026-07-30 10:58:20 +02:00
LICENSE.md updated the docs 2026-07-30 10:58:20 +02:00
MANIFEST.in just created the plugin 2026-04-13 12:11:03 +02:00
README.md Add an Auth column to the Actions-at-a-glance tables 2026-07-31 17:04:31 +02:00
pytest.ini just created the plugin 2026-04-13 12:11:03 +02:00
requirements.txt just created the plugin 2026-04-13 12:11:03 +02:00
setup.cfg just created the plugin 2026-04-13 12:11:03 +02:00
setup.py just created the plugin 2026-04-13 12:11:03 +02:00
test.ini just created the plugin 2026-04-13 12:11:03 +02:00

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_extra table - 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.name namespace with Groups and Organizations - author_create auto-generates a free slug from the title when none is supplied
  • Author ≠ Item Creator: the CKAN built-in author field 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 the member table.

Table of contents


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_view is 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 whether package/read_base.html renders the read-only package/snippets/dataset_authors.html snippet (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/author share one hook, but can still be configured separately: CKAN core's ckan/views/group.py _update_facet_titles() only calls IFacets.group_facets() when the page's group_type is literally "group" - every other type, including organization and our own author, is routed through IFacets.organization_facets() instead. The plugin's organization_facets() still receives the actual type string as an argument, so ckanext.d4science_authors.facet_pages can list organization without author (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 way register_group_plugin_rules does for the routes above. The plugin still implements activity_template() (an IGroupForm hook) and author_show/author_list work 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. via IBlueprint).


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_show is public and its schema includes email in the returned Author object. Do not treat this field as confidential until equivalent filtering is implemented in the action/helper layer.

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_titles are string + docValues="true" - un-tokenized, needed for exact-match filtering (fq=author_names:...) and faceting (facet.field=author_names); a string field 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 though package.author (the item creator) is someone else.
  • Also copied into the dedicated text_general field author_names_text, which is tokenized (so it supports partial/full-text matching, unlike author_names/author_titles) but kept separate from the text catchall
    • a future author-only search box could query just this field without the noise of matching on titles/notes/tags too.

author_names_text is currently unused. It's populated by these copyFields, 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