ckanext-citations 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. ckanext_citations-0.1.8/PKG-INFO +111 -0
  2. ckanext_citations-0.1.8/README.md +96 -0
  3. ckanext_citations-0.1.8/ckanext/__init__.py +8 -0
  4. ckanext_citations-0.1.8/ckanext/citations/__init__.py +0 -0
  5. ckanext_citations-0.1.8/ckanext/citations/cli.py +89 -0
  6. ckanext_citations-0.1.8/ckanext/citations/i18n/ckanext-citations.pot +116 -0
  7. ckanext_citations-0.1.8/ckanext/citations/i18n/fr/LC_MESSAGES/ckanext-citations.mo +0 -0
  8. ckanext_citations-0.1.8/ckanext/citations/i18n/fr/LC_MESSAGES/ckanext-citations.po +130 -0
  9. ckanext_citations-0.1.8/ckanext/citations/lib/__init__.py +0 -0
  10. ckanext_citations-0.1.8/ckanext/citations/lib/datacite_events_client.py +88 -0
  11. ckanext_citations-0.1.8/ckanext/citations/lib/helpers.py +74 -0
  12. ckanext_citations-0.1.8/ckanext/citations/lib/openalex_client.py +154 -0
  13. ckanext_citations-0.1.8/ckanext/citations/lib/package_extras.py +91 -0
  14. ckanext_citations-0.1.8/ckanext/citations/lib/refresh.py +255 -0
  15. ckanext_citations-0.1.8/ckanext/citations/lib/scoring.py +37 -0
  16. ckanext_citations-0.1.8/ckanext/citations/migration/citations/alembic.ini +41 -0
  17. ckanext_citations-0.1.8/ckanext/citations/migration/citations/env.py +50 -0
  18. ckanext_citations-0.1.8/ckanext/citations/migration/citations/versions/8f3b2c1a9d4e_initialisation.py +130 -0
  19. ckanext_citations-0.1.8/ckanext/citations/model/__init__.py +21 -0
  20. ckanext_citations-0.1.8/ckanext/citations/model/author_sindex.py +30 -0
  21. ckanext_citations-0.1.8/ckanext/citations/model/citation_stats.py +52 -0
  22. ckanext_citations-0.1.8/ckanext/citations/model/citing_researcher.py +51 -0
  23. ckanext_citations-0.1.8/ckanext/citations/model/citing_work.py +55 -0
  24. ckanext_citations-0.1.8/ckanext/citations/model/score_history.py +31 -0
  25. ckanext_citations-0.1.8/ckanext/citations/plugin.py +41 -0
  26. ckanext_citations-0.1.8/ckanext/citations/templates/package/read.html +8 -0
  27. ckanext_citations-0.1.8/ckanext/citations/templates/package/snippets/citations_panel.html +158 -0
  28. ckanext_citations-0.1.8/ckanext/citations/tests/__init__.py +0 -0
  29. ckanext_citations-0.1.8/ckanext/citations/tests/conftest.py +43 -0
  30. ckanext_citations-0.1.8/ckanext/citations/tests/test.ini +44 -0
  31. ckanext_citations-0.1.8/ckanext/citations/tests/test_db.py +130 -0
  32. ckanext_citations-0.1.8/ckanext/citations/tests/test_package_extras.py +74 -0
  33. ckanext_citations-0.1.8/ckanext/citations/tests/test_scoring.py +66 -0
  34. ckanext_citations-0.1.8/ckanext_citations.egg-info/PKG-INFO +111 -0
  35. ckanext_citations-0.1.8/ckanext_citations.egg-info/SOURCES.txt +42 -0
  36. ckanext_citations-0.1.8/ckanext_citations.egg-info/dependency_links.txt +1 -0
  37. ckanext_citations-0.1.8/ckanext_citations.egg-info/entry_points.txt +2 -0
  38. ckanext_citations-0.1.8/ckanext_citations.egg-info/not-zip-safe +1 -0
  39. ckanext_citations-0.1.8/ckanext_citations.egg-info/requires.txt +6 -0
  40. ckanext_citations-0.1.8/ckanext_citations.egg-info/top_level.txt +2 -0
  41. ckanext_citations-0.1.8/pyproject.toml +49 -0
  42. ckanext_citations-0.1.8/setup.cfg +26 -0
  43. ckanext_citations-0.1.8/setup.py +12 -0
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.2
2
+ Name: ckanext-citations
3
+ Version: 0.1.8
4
+ Summary: Citation tracking (cited-by via OpenAlex/DataCite Event Data), dataset disruption index and per-researcher S-index, displayed on the dataset page.
5
+ Author-email: ICS <groupe-info-ics@igbmc.fr>
6
+ License: AGPL-3.0-or-later
7
+ Keywords: CKAN,data,citations,bibliometrics
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest==7.4.4; extra == "test"
13
+ Requires-Dist: pytest-cov==4.1.0; extra == "test"
14
+ Requires-Dist: responses; extra == "test"
15
+
16
+ ![CKAN](https://img.shields.io/badge/CKAN-2.12.0-orange)
17
+ ![License](https://img.shields.io/badge/license-AGPL--3.0-blue)
18
+
19
+ # ckanext-citations
20
+
21
+ Citation tracking (cited-by, via OpenAlex with a DataCite Event Data
22
+ fallback), a per-dataset disruption index, and a per-researcher S-index
23
+ ("how many unique researchers has your data enabled" - see
24
+ [s-index.science](https://s-index.science/)), displayed on the dataset
25
+ page.
26
+
27
+ ## v1 scope
28
+
29
+ This first iteration deliberately does **not** implement the 17
30
+ FAIRsFAIR/F-UJI metrics, a merged "FAIR score", or the metadata-enrichment
31
+ assistant described in the original design note. It covers only the
32
+ citation/impact-tracking half:
33
+
34
+ - Citation count (cited-by) per dataset.
35
+ - [Disruption index](https://doi.org/10.1038/s41586-019-0941-9) (Wu, Wang &
36
+ Evans, 2019): `DI = (N_F - N_B) / (N_F + N_B + N_R)`.
37
+ - S-index per ORCID: count of distinct researchers (deduplicated by ORCID,
38
+ falling back to normalised name) who authored a work citing any of that
39
+ researcher's FAIR3R datasets.
40
+ - Display on the dataset page (citation count, disruption index, S-index
41
+ with a co-author breakdown, a citing-works table).
42
+
43
+ ## Why dedicated tables, not `package_extra`
44
+
45
+ Citations and scores are refreshed by a periodic batch job, not by the user
46
+ editing the dataset. Writing them through `package_update`/`package_extra`
47
+ would create a `package_revision` row and trigger a full Solr reindex for
48
+ every dataset on every refresh run - pure overhead for data nobody searches
49
+ on. Instead this plugin owns five Postgres tables of its own
50
+ (`fair_citation_stats`, `fair_citing_works`, `fair_citing_researchers`,
51
+ `fair_author_sindex`, `fair_score_history`), created via the same
52
+ Alembic-based migration mechanism as `ckanext-doi`
53
+ (`ckan db upgrade -p citations`).
54
+
55
+ `fair_citing_researchers` exists only to make the S-index's "unique
56
+ researchers" dedup correct across an author's several datasets: it's one
57
+ row per (FAIR3R author ORCID, citing-researcher identity) pair, and
58
+ `fair_author_sindex.s_index_current` is the cached `COUNT(DISTINCT ...)`
59
+ over it. Both the citation count and the S-index are also tracked as
60
+ high-water marks (`*_max`) so a researcher's score never visibly regresses
61
+ just because an external API temporarily deduplicates differently between
62
+ two refresh runs.
63
+
64
+ ## The disruption index needs a reference list the dataset usually doesn't have
65
+
66
+ The Wu/Wang/Evans formula needs to know what the focal work's own
67
+ references are, to tell "citing works that also build on the same sources"
68
+ (`N_B`) apart from "citing works that only cite the focal work" (`N_F`).
69
+ OpenAlex rarely has a populated reference list for a dataset-type Work,
70
+ which would make `N_B`/`N_R` collapse to 0 and the index trivially tend to
71
+ `+1.0` for almost every dataset with any citations at all.
72
+
73
+ To get a meaningful signal, the reference set is enriched with the
74
+ dataset's own DataCite `relatedIdentifiers` whose `relationType` is
75
+ `References` or `Cites` (already supported by the FDF schema via
76
+ `ckanext-doi`) - each such DOI is resolved to an OpenAlex work id and added
77
+ to the focal work's reference set before classifying citing works.
78
+
79
+ ## Config
80
+
81
+ ```ini
82
+ # Contact email sent as OpenAlex's "polite pool" mailto param (better rate
83
+ # limits, not auth). Optional but recommended.
84
+ ckanext.citations.contact_email = your-team@example.org
85
+
86
+ # Pause between outbound API calls during a refresh, in seconds.
87
+ ckanext.citations.request_pause_seconds = 0.1
88
+ ```
89
+
90
+ ## Running a refresh
91
+
92
+ ```bash
93
+ ckan citations refresh --min-age-days 7
94
+ ```
95
+
96
+ Idempotent and safe to run repeatedly: only datasets whose
97
+ `fair_citation_stats.last_checked` is older than `--min-age-days` (or
98
+ `NULL`, i.e. never checked) are touched. Intended to run from a weekly cron
99
+ job, same pattern as `fair3r update-schema`.
100
+
101
+ ## Tests
102
+
103
+ Two `test.ini` files, same convention as the other FAIR3R extensions:
104
+
105
+ - `test.ini` at the repo root: Docker DEV (`/srv/app/src/ckan/test-core.ini`).
106
+ - `ckanext/citations/tests/test.ini`: shipped with the package, used by
107
+ `deploy/ckanext_test.py` on integration/validation (`/usr/lib/ckan/default/...`).
108
+
109
+ ```shell
110
+ docker exec -u ckan -it ckan-app pytest --ckan-ini=/plugins/ckanext-citations/test.ini /plugins/ckanext-citations/ckanext/citations/tests
111
+ ```
@@ -0,0 +1,96 @@
1
+ ![CKAN](https://img.shields.io/badge/CKAN-2.12.0-orange)
2
+ ![License](https://img.shields.io/badge/license-AGPL--3.0-blue)
3
+
4
+ # ckanext-citations
5
+
6
+ Citation tracking (cited-by, via OpenAlex with a DataCite Event Data
7
+ fallback), a per-dataset disruption index, and a per-researcher S-index
8
+ ("how many unique researchers has your data enabled" - see
9
+ [s-index.science](https://s-index.science/)), displayed on the dataset
10
+ page.
11
+
12
+ ## v1 scope
13
+
14
+ This first iteration deliberately does **not** implement the 17
15
+ FAIRsFAIR/F-UJI metrics, a merged "FAIR score", or the metadata-enrichment
16
+ assistant described in the original design note. It covers only the
17
+ citation/impact-tracking half:
18
+
19
+ - Citation count (cited-by) per dataset.
20
+ - [Disruption index](https://doi.org/10.1038/s41586-019-0941-9) (Wu, Wang &
21
+ Evans, 2019): `DI = (N_F - N_B) / (N_F + N_B + N_R)`.
22
+ - S-index per ORCID: count of distinct researchers (deduplicated by ORCID,
23
+ falling back to normalised name) who authored a work citing any of that
24
+ researcher's FAIR3R datasets.
25
+ - Display on the dataset page (citation count, disruption index, S-index
26
+ with a co-author breakdown, a citing-works table).
27
+
28
+ ## Why dedicated tables, not `package_extra`
29
+
30
+ Citations and scores are refreshed by a periodic batch job, not by the user
31
+ editing the dataset. Writing them through `package_update`/`package_extra`
32
+ would create a `package_revision` row and trigger a full Solr reindex for
33
+ every dataset on every refresh run - pure overhead for data nobody searches
34
+ on. Instead this plugin owns five Postgres tables of its own
35
+ (`fair_citation_stats`, `fair_citing_works`, `fair_citing_researchers`,
36
+ `fair_author_sindex`, `fair_score_history`), created via the same
37
+ Alembic-based migration mechanism as `ckanext-doi`
38
+ (`ckan db upgrade -p citations`).
39
+
40
+ `fair_citing_researchers` exists only to make the S-index's "unique
41
+ researchers" dedup correct across an author's several datasets: it's one
42
+ row per (FAIR3R author ORCID, citing-researcher identity) pair, and
43
+ `fair_author_sindex.s_index_current` is the cached `COUNT(DISTINCT ...)`
44
+ over it. Both the citation count and the S-index are also tracked as
45
+ high-water marks (`*_max`) so a researcher's score never visibly regresses
46
+ just because an external API temporarily deduplicates differently between
47
+ two refresh runs.
48
+
49
+ ## The disruption index needs a reference list the dataset usually doesn't have
50
+
51
+ The Wu/Wang/Evans formula needs to know what the focal work's own
52
+ references are, to tell "citing works that also build on the same sources"
53
+ (`N_B`) apart from "citing works that only cite the focal work" (`N_F`).
54
+ OpenAlex rarely has a populated reference list for a dataset-type Work,
55
+ which would make `N_B`/`N_R` collapse to 0 and the index trivially tend to
56
+ `+1.0` for almost every dataset with any citations at all.
57
+
58
+ To get a meaningful signal, the reference set is enriched with the
59
+ dataset's own DataCite `relatedIdentifiers` whose `relationType` is
60
+ `References` or `Cites` (already supported by the FDF schema via
61
+ `ckanext-doi`) - each such DOI is resolved to an OpenAlex work id and added
62
+ to the focal work's reference set before classifying citing works.
63
+
64
+ ## Config
65
+
66
+ ```ini
67
+ # Contact email sent as OpenAlex's "polite pool" mailto param (better rate
68
+ # limits, not auth). Optional but recommended.
69
+ ckanext.citations.contact_email = your-team@example.org
70
+
71
+ # Pause between outbound API calls during a refresh, in seconds.
72
+ ckanext.citations.request_pause_seconds = 0.1
73
+ ```
74
+
75
+ ## Running a refresh
76
+
77
+ ```bash
78
+ ckan citations refresh --min-age-days 7
79
+ ```
80
+
81
+ Idempotent and safe to run repeatedly: only datasets whose
82
+ `fair_citation_stats.last_checked` is older than `--min-age-days` (or
83
+ `NULL`, i.e. never checked) are touched. Intended to run from a weekly cron
84
+ job, same pattern as `fair3r update-schema`.
85
+
86
+ ## Tests
87
+
88
+ Two `test.ini` files, same convention as the other FAIR3R extensions:
89
+
90
+ - `test.ini` at the repo root: Docker DEV (`/srv/app/src/ckan/test-core.ini`).
91
+ - `ckanext/citations/tests/test.ini`: shipped with the package, used by
92
+ `deploy/ckanext_test.py` on integration/validation (`/usr/lib/ckan/default/...`).
93
+
94
+ ```shell
95
+ docker exec -u ckan -it ckan-app pytest --ckan-ini=/plugins/ckanext-citations/test.ini /plugins/ckanext-citations/ckanext/citations/tests
96
+ ```
@@ -0,0 +1,8 @@
1
+ try:
2
+ import pkg_resources
3
+
4
+ pkg_resources.declare_namespace(__name__)
5
+ except ImportError:
6
+ import pkgutil
7
+
8
+ __path__ = pkgutil.extend_path(__path__, __name__)
File without changes
@@ -0,0 +1,89 @@
1
+ import logging
2
+ import time
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import click
6
+ from ckan.model import Package, Session
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ DEFAULT_MIN_AGE_DAYS = 7
11
+ DEFAULT_PAUSE_SECONDS = 0.1
12
+
13
+
14
+ def get_commands():
15
+ return [citations]
16
+
17
+
18
+ @click.group()
19
+ def citations():
20
+ """Citation/impact tracking commands."""
21
+
22
+
23
+ @citations.command(name='refresh')
24
+ @click.option(
25
+ '--min-age-days',
26
+ default=DEFAULT_MIN_AGE_DAYS,
27
+ show_default=True,
28
+ help='Only refresh datasets whose citation stats were last checked more '
29
+ 'than this many days ago (or never checked at all).',
30
+ )
31
+ @click.option(
32
+ '--limit',
33
+ default=None,
34
+ type=int,
35
+ help='Process at most this many datasets this run.',
36
+ )
37
+ @click.option(
38
+ '--pause-seconds',
39
+ default=DEFAULT_PAUSE_SECONDS,
40
+ show_default=True,
41
+ help='Pause between datasets, on top of the per-API-call pause already '
42
+ 'applied inside each dataset refresh.',
43
+ )
44
+ def refresh(min_age_days, limit, pause_seconds):
45
+ """Batched, idempotent citation refresh across every published-DOI
46
+ dataset due for a check. Safe to run on a cron - only datasets whose
47
+ last check is older than --min-age-days (or never checked) are touched.
48
+ """
49
+ from ckanext.citations.lib.refresh import refresh_dataset
50
+
51
+ package_ids = _packages_due_for_refresh(min_age_days)
52
+ if limit:
53
+ package_ids = package_ids[:limit]
54
+
55
+ click.echo(f'{len(package_ids)} dataset(s) due for a citation refresh.')
56
+
57
+ for i, package_id in enumerate(package_ids, start=1):
58
+ try:
59
+ refresh_dataset(package_id)
60
+ click.echo(f'[{i}/{len(package_ids)}] refreshed {package_id}')
61
+ except Exception:
62
+ log.exception('Citation refresh failed for dataset %s', package_id)
63
+ click.secho(f'[{i}/{len(package_ids)}] FAILED {package_id}', fg='red')
64
+ Session.rollback()
65
+ time.sleep(pause_seconds)
66
+
67
+
68
+ def _packages_due_for_refresh(min_age_days):
69
+ """Queries ckan.model directly (not the package_search action) so this
70
+ maintenance job sees every dataset - public or private - and doesn't
71
+ depend on Solr being in sync."""
72
+ from ckanext.citations.model import CitationStats
73
+
74
+ cutoff = datetime.now(timezone.utc) - timedelta(days=min_age_days)
75
+
76
+ already_checked_recently = {
77
+ row.package_id
78
+ for row in Session.query(CitationStats.package_id).filter(
79
+ CitationStats.last_checked.isnot(None), CitationStats.last_checked > cutoff
80
+ )
81
+ }
82
+
83
+ all_active_ids = [
84
+ row.id
85
+ for row in Session.query(Package.id).filter(
86
+ Package.state == 'active', Package.type == 'dataset'
87
+ )
88
+ ]
89
+ return [pid for pid in all_active_ids if pid not in already_checked_recently]
@@ -0,0 +1,116 @@
1
+ # Translations template for ckanext-citations.
2
+ # Copyright (C) 2026 ORGANIZATION
3
+ # This file is distributed under the same license as the ckanext-citations
4
+ # project.
5
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
6
+ #
7
+ #, fuzzy
8
+ msgid ""
9
+ msgstr ""
10
+ "Project-Id-Version: ckanext-citations 0.1.6\n"
11
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
12
+ "POT-Creation-Date: 2026-10-05 15:53+0000\n"
13
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
15
+ "Language-Team: LANGUAGE <LL@li.org>\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=utf-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Generated-By: Babel 2.18.0\n"
20
+
21
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:20
22
+ msgid "FAIR & Impact"
23
+ msgstr ""
24
+
25
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:29
26
+ msgid "Completeness"
27
+ msgstr ""
28
+
29
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:30
30
+ msgid ""
31
+ "How complete the FAIR3R metadata of this dataset is: the weighted share of "
32
+ "the fields that apply (required fields count double), CKAN basics included. "
33
+ "Conditional fields that are hidden are ignored."
34
+ msgstr ""
35
+
36
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:38
37
+ msgid "Missing fields"
38
+ msgstr ""
39
+
40
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:50
41
+ msgid "Citations"
42
+ msgstr ""
43
+
44
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:55
45
+ msgid "last checked"
46
+ msgstr ""
47
+
48
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:62
49
+ msgid "Disruption index"
50
+ msgstr ""
51
+
52
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:63
53
+ msgid ""
54
+ "Wu, Wang & Evans (2019). Between -1 and +1: close to +1 means the works "
55
+ "citing this dataset tend to replace it; close to -1 means they build on it "
56
+ "together with its own references. Uses the dataset References/Cites related "
57
+ "identifiers when present."
58
+ msgstr ""
59
+
60
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:69
61
+ msgid "not enough data yet"
62
+ msgstr ""
63
+
64
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:76
65
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:87
66
+ msgid "S-index"
67
+ msgstr ""
68
+
69
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:77
70
+ msgid ""
71
+ "Number of distinct researchers (identified by ORCID) who have cited at least "
72
+ "one of this author FAIR3R datasets. Shown for the highest co-author with an "
73
+ "ORCID."
74
+ msgstr ""
75
+
76
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:82
77
+ msgid "highest among co-authors"
78
+ msgstr ""
79
+
80
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:84
81
+ msgid "All co-authors"
82
+ msgstr ""
83
+
84
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:99
85
+ msgid "Not checked yet: citations are tracked once the dataset has a published DOI."
86
+ msgstr ""
87
+
88
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:103
89
+ msgid "Citing works"
90
+ msgstr ""
91
+
92
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:107
93
+ msgid "Title"
94
+ msgstr ""
95
+
96
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:108
97
+ msgid "Authors"
98
+ msgstr ""
99
+
100
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:109
101
+ msgid "Year"
102
+ msgstr ""
103
+
104
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:110
105
+ msgid "Venue"
106
+ msgstr ""
107
+
108
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:111
109
+ msgid "DOI"
110
+ msgstr ""
111
+
112
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:134
113
+ #, python-format
114
+ msgid "Show all %(count)s citing works"
115
+ msgstr ""
116
+
@@ -0,0 +1,130 @@
1
+ # French translations for ckanext-citations.
2
+ # Copyright (C) 2026 ORGANIZATION
3
+ # This file is distributed under the same license as the ckanext-citations
4
+ # project.
5
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
6
+ #
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: ckanext-citations 0.1.6\n"
10
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11
+ "POT-Creation-Date: 2026-10-05 15:53+0000\n"
12
+ "PO-Revision-Date: 2026-10-05 15:53+0000\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: fr <LL@li.org>\n"
15
+ "Language: fr\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=utf-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20
+ "Generated-By: Babel 2.18.0\n"
21
+
22
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:20
23
+ msgid "FAIR & Impact"
24
+ msgstr "FAIR & Impact"
25
+
26
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:29
27
+ msgid "Completeness"
28
+ msgstr "Complétude"
29
+
30
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:30
31
+ msgid ""
32
+ "How complete the FAIR3R metadata of this dataset is: the weighted share of "
33
+ "the fields that apply (required fields count double), CKAN basics included. "
34
+ "Conditional fields that are hidden are ignored."
35
+ msgstr ""
36
+ "Niveau de complétude des métadonnées FAIR3R de ce dataset : part pondérée "
37
+ "des champs applicables (les champs obligatoires comptent double), en "
38
+ "incluant les champs de base de CKAN. Les champs conditionnels masqués sont "
39
+ "ignorés."
40
+
41
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:38
42
+ msgid "Missing fields"
43
+ msgstr "Champs manquants"
44
+
45
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:50
46
+ msgid "Citations"
47
+ msgstr "Citations"
48
+
49
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:55
50
+ msgid "last checked"
51
+ msgstr "dernière vérification"
52
+
53
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:62
54
+ msgid "Disruption index"
55
+ msgstr "Indice de disruption"
56
+
57
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:63
58
+ msgid ""
59
+ "Wu, Wang & Evans (2019). Between -1 and +1: close to +1 means the works "
60
+ "citing this dataset tend to replace it; close to -1 means they build on it "
61
+ "together with its own references. Uses the dataset References/Cites related "
62
+ "identifiers when present."
63
+ msgstr ""
64
+ "Wu, Wang & Evans (2019). Entre -1 et +1 : proche de +1, les œuvres citant ce"
65
+ " dataset tendent à le remplacer ; proche de -1, elles s'appuient aussi sur "
66
+ "ses références. Utilise les identifiants liés References/Cites du dataset "
67
+ "quand ils existent."
68
+
69
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:69
70
+ msgid "not enough data yet"
71
+ msgstr "pas assez de données pour l'instant"
72
+
73
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:76
74
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:87
75
+ msgid "S-index"
76
+ msgstr "S-index"
77
+
78
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:77
79
+ msgid ""
80
+ "Number of distinct researchers (identified by ORCID) who have cited at least"
81
+ " one of this author FAIR3R datasets. Shown for the highest co-author with an"
82
+ " ORCID."
83
+ msgstr ""
84
+ "Nombre de chercheurs distincts (identifiés par ORCID) ayant cité au moins un"
85
+ " des datasets FAIR3R de cet auteur. Affiché pour le co-auteur ayant le plus "
86
+ "haut score, parmi ceux qui ont un ORCID."
87
+
88
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:82
89
+ msgid "highest among co-authors"
90
+ msgstr "le plus élevé parmi les co-auteurs"
91
+
92
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:84
93
+ msgid "All co-authors"
94
+ msgstr "Tous les co-auteurs"
95
+
96
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:99
97
+ msgid ""
98
+ "Not checked yet: citations are tracked once the dataset has a published DOI."
99
+ msgstr ""
100
+ "Pas encore vérifié : les citations sont suivies une fois le dataset publié "
101
+ "avec un DOI."
102
+
103
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:103
104
+ msgid "Citing works"
105
+ msgstr "Œuvres citantes"
106
+
107
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:107
108
+ msgid "Title"
109
+ msgstr "Titre"
110
+
111
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:108
112
+ msgid "Authors"
113
+ msgstr "Auteurs"
114
+
115
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:109
116
+ msgid "Year"
117
+ msgstr "Année"
118
+
119
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:110
120
+ msgid "Venue"
121
+ msgstr "Revue"
122
+
123
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:111
124
+ msgid "DOI"
125
+ msgstr "DOI"
126
+
127
+ #: ckanext/citations/templates/package/snippets/citations_panel.html:134
128
+ #, python-format
129
+ msgid "Show all %(count)s citing works"
130
+ msgstr "Afficher les %(count)s œuvres citantes"
@@ -0,0 +1,88 @@
1
+ """Fallback citation source: DataCite's Event Data API.
2
+
3
+ Used only to top up the citation count when OpenAlex hasn't indexed the
4
+ dataset's DOI at all. Event Data exposes bare subject/object DOI pairs, not
5
+ author/ORCID/reference-list metadata, so works discovered this way cannot
6
+ feed the disruption index or the S-index - they only contribute to
7
+ citation_count_current.
8
+ """
9
+
10
+ import logging
11
+ import time
12
+
13
+ import requests
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+ BASE_URL = 'https://api.datacite.org/events'
18
+ REQUEST_TIMEOUT = 20
19
+ MAX_RETRIES = 4
20
+ INITIAL_BACKOFF_SECONDS = 1.0
21
+
22
+
23
+ class DataciteEventsError(Exception):
24
+ pass
25
+
26
+
27
+ def _get_with_backoff(params):
28
+ backoff = INITIAL_BACKOFF_SECONDS
29
+ last_error = None
30
+ for attempt in range(1, MAX_RETRIES + 1):
31
+ try:
32
+ resp = requests.get(BASE_URL, params=params, timeout=REQUEST_TIMEOUT)
33
+ except requests.RequestException as e:
34
+ last_error = f'DataCite Event Data request failed: {e}'
35
+ log.warning(
36
+ '%s (attempt %s/%s, retrying in %.1fs)',
37
+ last_error,
38
+ attempt,
39
+ MAX_RETRIES,
40
+ backoff,
41
+ )
42
+ time.sleep(backoff)
43
+ backoff *= 2.0
44
+ continue
45
+
46
+ if resp.status_code == 200:
47
+ return resp.json()
48
+ if resp.status_code == 429 or resp.status_code >= 500:
49
+ last_error = (
50
+ f'DataCite Event Data returned {resp.status_code}: {resp.text[:200]}'
51
+ )
52
+ log.warning(
53
+ '%s (attempt %s/%s, retrying in %.1fs)',
54
+ last_error,
55
+ attempt,
56
+ MAX_RETRIES,
57
+ backoff,
58
+ )
59
+ time.sleep(backoff)
60
+ backoff *= 2.0
61
+ continue
62
+ raise DataciteEventsError(
63
+ f'DataCite Event Data returned {resp.status_code}: {resp.text[:200]}'
64
+ )
65
+
66
+ raise DataciteEventsError(
67
+ f'DataCite Event Data request failed after {MAX_RETRIES} attempts: {last_error}'
68
+ )
69
+
70
+
71
+ def get_citing_dois(doi):
72
+ """Return the list of citing DOIs for `doi` via the is-cited-by relation.
73
+ Returns [] if DataCite has no events for this DOI (not an error - most
74
+ datasets simply won't have any yet)."""
75
+ params = {
76
+ 'doi': doi,
77
+ 'relation-type-id': 'is-cited-by',
78
+ 'page[size]': 200,
79
+ }
80
+ data = _get_with_backoff(params)
81
+ citing_dois = []
82
+ for event in data.get('data', []):
83
+ attrs = event.get('attributes', {})
84
+ subj_id = attrs.get('subjId') or ''
85
+ # subjId is the citing work's DOI as a URL (https://doi.org/10.xxx/yyy).
86
+ if 'doi.org/' in subj_id:
87
+ citing_dois.append(subj_id.split('doi.org/', 1)[-1])
88
+ return citing_dois