968 lines
36 KiB
Python
968 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from ecospecies_api import repository
|
|
|
|
|
|
SAMPLE_PAYLOAD = [
|
|
{
|
|
"slug": "test-shad",
|
|
"source_file": "Test Shad.txt",
|
|
"title": "Test Shad (Alosa testus)",
|
|
"common_name": "Test Shad",
|
|
"scientific_name": "Alosa testus",
|
|
"flelmr_code": "9999",
|
|
"summary": "",
|
|
"section_count": 2,
|
|
"diagnostics": [
|
|
{
|
|
"level": "warning",
|
|
"code": "missing_summary",
|
|
"message": "Summary/Abstract section is missing.",
|
|
},
|
|
{
|
|
"level": "warning",
|
|
"code": "missing_citations",
|
|
"message": "References section not found.",
|
|
},
|
|
],
|
|
"sections": [
|
|
{"heading": "HEADER", "content": "Header content"},
|
|
{"heading": "HABITAT", "content": "Habitat content"},
|
|
],
|
|
}
|
|
]
|
|
|
|
UPDATED_PAYLOAD = [
|
|
{
|
|
"slug": "test-shad",
|
|
"source_file": "Test Shad v2.txt",
|
|
"title": "Test Shad Revised (Alosa testus)",
|
|
"common_name": "Test Shad",
|
|
"scientific_name": "Alosa testus revised",
|
|
"flelmr_code": "1000",
|
|
"summary": "Imported replacement summary.",
|
|
"section_count": 2,
|
|
"diagnostics": [
|
|
{
|
|
"level": "warning",
|
|
"code": "missing_flelmr_code",
|
|
"message": "Replacement diagnostic.",
|
|
}
|
|
],
|
|
"sections": [
|
|
{"heading": "HEADER", "content": "Replacement header content"},
|
|
{"heading": "HABITAT", "content": "Replacement habitat content"},
|
|
],
|
|
}
|
|
]
|
|
|
|
DIFFERENT_PAYLOAD = [
|
|
{
|
|
"slug": "other-fish",
|
|
"source_file": "Other Fish.txt",
|
|
"title": "Other Fish (Pisces otherus)",
|
|
"common_name": "Other Fish",
|
|
"scientific_name": "Pisces otherus",
|
|
"flelmr_code": "2000",
|
|
"summary": "Other fish summary.",
|
|
"section_count": 1,
|
|
"diagnostics": [],
|
|
"sections": [
|
|
{"heading": "HEADER", "content": "Other fish header"},
|
|
],
|
|
}
|
|
]
|
|
|
|
|
|
class RepositoryWorkflowTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.tempdir = tempfile.TemporaryDirectory()
|
|
db_path = Path(self.tempdir.name) / "test.db"
|
|
self.engine = create_engine(f"sqlite:///{db_path}", future=True)
|
|
self.session_local = sessionmaker(
|
|
bind=self.engine,
|
|
autoflush=False,
|
|
autocommit=False,
|
|
future=True,
|
|
)
|
|
self.engine_patch = patch.object(repository, "create_db_engine", return_value=self.engine)
|
|
self.session_patch = patch.object(repository, "SessionLocal", self.session_local)
|
|
self.engine_patch.start()
|
|
self.session_patch.start()
|
|
repository.import_species_payload(SAMPLE_PAYLOAD)
|
|
|
|
def tearDown(self) -> None:
|
|
self.session_patch.stop()
|
|
self.engine_patch.stop()
|
|
self.engine.dispose()
|
|
self.tempdir.cleanup()
|
|
|
|
def test_import_filters_missing_summary_diagnostic_from_accepted_dataset(self) -> None:
|
|
detail = repository.get_species_by_slug("test-shad")
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertEqual(detail["section_count"], 2)
|
|
self.assertEqual([section["position"] for section in detail["sections"]], [1, 2])
|
|
self.assertEqual([item["code"] for item in detail["diagnostics"]], ["missing_citations"])
|
|
self.assertEqual(
|
|
detail["legacy_identifiers"],
|
|
[
|
|
{
|
|
"authority": "legacy-ecospecies",
|
|
"identifier": "9999",
|
|
"label": "FLELMR",
|
|
}
|
|
],
|
|
)
|
|
|
|
def test_species_detail_includes_structured_document_and_legacy_source(self) -> None:
|
|
input_dir = Path(self.tempdir.name) / "input-data" / "InputFiles"
|
|
input_dir.mkdir(parents=True, exist_ok=True)
|
|
(input_dir / "Test Shad.txt").write_text("HEADER\nLegacy header content\n", encoding="utf-8")
|
|
|
|
with patch.object(repository, "get_default_data_dir", return_value=str(input_dir)):
|
|
detail = repository.get_species_by_slug("test-shad")
|
|
|
|
self.assertIsNotNone(detail)
|
|
assert detail is not None
|
|
self.assertEqual(detail["structured_document"]["source_format"], "ecospecies-markdown-v1")
|
|
self.assertIn(
|
|
"HABITAT",
|
|
[node["title"] for node in detail["structured_document"]["ast"]["nodes"]],
|
|
)
|
|
self.assertEqual(detail["legacy_source"]["source_file"], "Test Shad.txt")
|
|
self.assertIn("Legacy header content", detail["legacy_source"]["text"])
|
|
self.assertEqual(detail["taxon_identifiers"], [])
|
|
|
|
def test_editorial_update_changes_publication_visibility_and_creates_audit(self) -> None:
|
|
result = repository.update_species_editorial(
|
|
slug="test-shad",
|
|
publication_status="draft",
|
|
summary="Editor-authored summary.",
|
|
editor_notes="Needs another review pass.",
|
|
is_archived=None,
|
|
username="bob",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["publication_status"], "draft")
|
|
self.assertEqual(result["summary"], "Editor-authored summary.")
|
|
self.assertEqual(result["last_modified_by"], "bob")
|
|
self.assertEqual(repository.get_species_by_slug("test-shad"), None)
|
|
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertEqual(editor_detail["publication_status"], "draft")
|
|
self.assertEqual(editor_detail["summary"], "Editor-authored summary.")
|
|
self.assertEqual(editor_detail["editor_notes"], "Needs another review pass.")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "editorial_update")
|
|
self.assertEqual(audit[0]["changed_by"], "bob")
|
|
self.assertIn("summary", audit[0]["details"])
|
|
self.assertIn("publication_status", audit[0]["details"])
|
|
|
|
def test_section_update_records_section_audit_metadata(self) -> None:
|
|
result = repository.update_species_section(
|
|
slug="test-shad",
|
|
section_position=2,
|
|
content="Updated habitat content.",
|
|
username="carol",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["section"]["position"], 2)
|
|
self.assertEqual(result["section"]["content"], "Updated habitat content.")
|
|
self.assertEqual(result["last_modified_by"], "carol")
|
|
self.assertEqual(sorted(result["changed_fields"].keys()), ["section_content"])
|
|
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertEqual(editor_detail["sections"][1]["content"], "Updated habitat content.")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "section_update")
|
|
self.assertEqual(audit[0]["changed_by"], "carol")
|
|
self.assertEqual(audit[0]["details"]["section_position"], 2)
|
|
self.assertEqual(audit[0]["details"]["section_heading"], "HABITAT")
|
|
self.assertEqual(
|
|
audit[0]["details"]["section_content"],
|
|
{"from": "Habitat content", "to": "Updated habitat content."},
|
|
)
|
|
|
|
def test_reimport_preserves_editorial_state_and_audit_history(self) -> None:
|
|
repository.update_species_editorial(
|
|
slug="test-shad",
|
|
publication_status="draft",
|
|
summary="Editor-authored summary.",
|
|
editor_notes="Needs another review pass.",
|
|
is_archived=None,
|
|
username="bob",
|
|
)
|
|
repository.update_species_section(
|
|
slug="test-shad",
|
|
section_position=2,
|
|
content="Updated habitat content.",
|
|
username="carol",
|
|
)
|
|
|
|
repository.import_species_payload(UPDATED_PAYLOAD)
|
|
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertEqual(editor_detail["source_file"], "Test Shad v2.txt")
|
|
self.assertEqual(editor_detail["title"], "Test Shad Revised (Alosa testus)")
|
|
self.assertEqual(editor_detail["scientific_name"], "Alosa testus revised")
|
|
self.assertEqual(editor_detail["flelmr_code"], "1000")
|
|
self.assertEqual(editor_detail["publication_status"], "draft")
|
|
self.assertEqual(editor_detail["summary"], "Editor-authored summary.")
|
|
self.assertEqual(editor_detail["editor_notes"], "Needs another review pass.")
|
|
self.assertEqual(editor_detail["sections"][0]["content"], "Replacement header content")
|
|
self.assertEqual(editor_detail["sections"][1]["content"], "Updated habitat content.")
|
|
self.assertEqual([item["code"] for item in editor_detail["diagnostics"]], ["missing_flelmr_code"])
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(len(audit), 2)
|
|
self.assertEqual([entry["action"] for entry in audit], ["section_update", "editorial_update"])
|
|
|
|
def test_reimport_preserves_persisted_taxon_identifiers(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad
|
|
common_name: Test Shad
|
|
scientific_name: Alosa testus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 9999
|
|
label: FLELMR
|
|
taxon_identifiers:
|
|
- authority: gbif
|
|
identifier: 12345
|
|
label: taxonKey
|
|
primary: true
|
|
primary_taxon_authority: gbif
|
|
---
|
|
|
|
## Summary
|
|
Taxon-reviewed summary.
|
|
""",
|
|
username="edith",
|
|
)
|
|
|
|
repository.import_species_payload(UPDATED_PAYLOAD)
|
|
|
|
detail = repository.get_editor_species_detail("test-shad")
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertEqual(detail["primary_taxon_authority"], "gbif")
|
|
self.assertEqual(
|
|
detail["primary_taxon_identifier"],
|
|
{
|
|
"authority": "gbif",
|
|
"identifier": "12345",
|
|
"label": "taxonKey",
|
|
"primary": True,
|
|
"source_url": "",
|
|
},
|
|
)
|
|
self.assertEqual(
|
|
detail["taxon_identifiers"],
|
|
[
|
|
{
|
|
"authority": "gbif",
|
|
"identifier": "12345",
|
|
"label": "taxonKey",
|
|
"primary": True,
|
|
"source_url": "",
|
|
}
|
|
],
|
|
)
|
|
|
|
def test_reimport_updates_summary_when_no_editorial_override_exists(self) -> None:
|
|
repository.import_species_payload(UPDATED_PAYLOAD)
|
|
|
|
detail = repository.get_species_by_slug("test-shad")
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertEqual(detail["summary"], "Imported replacement summary.")
|
|
self.assertEqual(detail["sections"][0]["content"], "Replacement header content")
|
|
|
|
def test_editor_can_archive_species_explicitly(self) -> None:
|
|
result = repository.update_species_editorial(
|
|
slug="test-shad",
|
|
publication_status=None,
|
|
summary=None,
|
|
editor_notes=None,
|
|
is_archived=True,
|
|
username="dana",
|
|
)
|
|
|
|
public_detail = repository.get_species_by_slug("test-shad")
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertTrue(result["is_archived"])
|
|
self.assertEqual(result["last_modified_by"], "dana")
|
|
self.assertIsNone(public_detail)
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertTrue(editor_detail["is_archived"])
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "editorial_update")
|
|
self.assertEqual(audit[0]["details"]["is_archived"], {"from": False, "to": True})
|
|
|
|
def test_editor_can_unarchive_species_explicitly(self) -> None:
|
|
repository.update_species_editorial(
|
|
slug="test-shad",
|
|
publication_status=None,
|
|
summary=None,
|
|
editor_notes=None,
|
|
is_archived=True,
|
|
username="dana",
|
|
)
|
|
|
|
result = repository.update_species_editorial(
|
|
slug="test-shad",
|
|
publication_status=None,
|
|
summary=None,
|
|
editor_notes=None,
|
|
is_archived=False,
|
|
username="erin",
|
|
)
|
|
|
|
public_detail = repository.get_species_by_slug("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertFalse(result["is_archived"])
|
|
self.assertEqual(result["last_modified_by"], "erin")
|
|
self.assertIsNotNone(public_detail)
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["details"]["is_archived"], {"from": True, "to": False})
|
|
|
|
def test_missing_species_is_archived_instead_of_deleted(self) -> None:
|
|
repository.import_species_payload(DIFFERENT_PAYLOAD)
|
|
|
|
public_detail = repository.get_species_by_slug("test-shad")
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
editor_items = repository.get_editor_species_list()
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNone(public_detail)
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertTrue(editor_detail["is_archived"])
|
|
self.assertEqual([item["slug"] for item in repository.list_species()], ["other-fish"])
|
|
self.assertEqual([item["slug"] for item in editor_items], ["other-fish", "test-shad"])
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "import_archive")
|
|
self.assertEqual(audit[0]["details"]["is_archived"], {"from": False, "to": True})
|
|
|
|
def test_archived_species_is_restored_when_it_reappears(self) -> None:
|
|
repository.import_species_payload(DIFFERENT_PAYLOAD)
|
|
repository.import_species_payload(UPDATED_PAYLOAD)
|
|
|
|
public_detail = repository.get_species_by_slug("test-shad")
|
|
editor_detail = repository.get_editor_species_detail("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(public_detail)
|
|
self.assertIsNotNone(editor_detail)
|
|
self.assertFalse(editor_detail["is_archived"])
|
|
self.assertEqual(public_detail["summary"], "Imported replacement summary.")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "import_restore")
|
|
self.assertEqual(audit[0]["details"]["is_archived"], {"from": True, "to": False})
|
|
|
|
def test_document_markdown_update_refreshes_flat_projection(self) -> None:
|
|
result = repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
species_code: 4242
|
|
---
|
|
|
|
## Summary
|
|
Markdown summary.
|
|
|
|
## Habitat
|
|
Open water.
|
|
|
|
### Type
|
|
Pelagic.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
detail = repository.get_editor_species_detail("test-shad")
|
|
document = repository.get_species_document("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertIsNotNone(detail)
|
|
self.assertIsNotNone(document)
|
|
self.assertEqual(detail["title"], "Test Shad Markdown")
|
|
self.assertEqual(detail["scientific_name"], "Alosa markdownus")
|
|
self.assertEqual(detail["flelmr_code"], "4242")
|
|
self.assertEqual(detail["summary"], "Markdown summary.")
|
|
self.assertEqual(
|
|
[section["heading"] for section in detail["sections"]],
|
|
["Habitat", "Habitat / Type"],
|
|
)
|
|
self.assertEqual(document["updated_by"], "frank")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "document_update")
|
|
|
|
def test_document_markdown_update_extracts_citations(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## Summary
|
|
Markdown summary.
|
|
|
|
## References
|
|
|
|
- Smith, J. 2024. Example paper. doi:10.1000/example-doi
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
detail = repository.get_editor_species_detail("test-shad")
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertEqual(detail["citation_count"], 2)
|
|
self.assertEqual(detail["citations"][0]["section_heading"], "References")
|
|
self.assertEqual(detail["citations"][0]["legacy_reference_number"], "")
|
|
self.assertEqual(detail["citations"][0]["doi"], "10.1000/example-doi")
|
|
self.assertTrue(detail["citations"][0]["citation_key"])
|
|
self.assertIn("@", detail["citations"][0]["draft_bibtex"])
|
|
self.assertEqual(detail["citations"][0]["review_status"], "draft")
|
|
self.assertEqual(detail["citations"][1]["legacy_reference_number"], "7")
|
|
self.assertEqual(detail["citations"][1]["doi"], "")
|
|
self.assertIn("ecospecies_reference_number = \\{7\\}", detail["citations"][1]["draft_bibtex"])
|
|
|
|
def test_editor_can_review_citations_and_reviews_survive_document_save(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
citation = citations["citations"][0]
|
|
|
|
result = repository.update_species_citation_review(
|
|
slug="test-shad",
|
|
citation_id=citation["id"],
|
|
review_status="accepted",
|
|
normalized_text="Jones, A. (2022). Fisheries review.",
|
|
doi="10.1000/review-doi",
|
|
citation_key="jones2022review",
|
|
entry_type="article",
|
|
draft_bibtex="@article{jones2022review,\n doi = {10.1000/review-doi}\n}",
|
|
username="edith",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["citation"]["review_status"], "accepted")
|
|
self.assertEqual(result["citation"]["source_type"], "editor_review")
|
|
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(citations)
|
|
self.assertEqual(citations["citation_count"], 1)
|
|
self.assertEqual(citations["citations"][0]["review_status"], "accepted")
|
|
self.assertEqual(citations["citations"][0]["doi"], "10.1000/review-doi")
|
|
self.assertEqual(citations["citations"][0]["citation_key"], "jones2022review")
|
|
self.assertEqual(citations["citations"][0]["entry_type"], "article")
|
|
self.assertIn("10.1000/review-doi", citations["citations"][0]["draft_bibtex"])
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[1]["action"], "citation_review_update")
|
|
|
|
def test_editor_can_run_citation_enrichment(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
""",
|
|
username="frank",
|
|
)
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
citation = citations["citations"][0]
|
|
|
|
with patch.object(
|
|
repository,
|
|
"enrich_citation_payload",
|
|
return_value={
|
|
"citation_key": "jones2022review",
|
|
"entry_type": "article",
|
|
"normalized_text": "Jones, A. (2022). Fisheries review. Journal of Tests. DOI:10.1000/review-doi",
|
|
"draft_bibtex": "@article{jones2022review,\n doi = {10.1000/review-doi},\n}",
|
|
"doi": "10.1000/review-doi",
|
|
"source_url": "https://doi.org/10.1000/review-doi",
|
|
"openalex_id": "W12345",
|
|
"resolver_source_label": "crossref:doi:10.1000/review-doi",
|
|
"enrichment_status": "resolved",
|
|
"enrichment_error": "",
|
|
"conflicts": [],
|
|
},
|
|
):
|
|
result = repository.update_species_citation_enrichment(
|
|
slug="test-shad",
|
|
citation_id=citation["id"],
|
|
username="edith",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["citation"]["enrichment_status"], "resolved")
|
|
self.assertEqual(result["citation"]["doi"], "10.1000/review-doi")
|
|
self.assertEqual(result["citation"]["openalex_id"], "W12345")
|
|
self.assertEqual(result["citation"]["resolver_source_label"], "crossref:doi:10.1000/review-doi")
|
|
self.assertEqual(result["citation"]["source_url"], "https://doi.org/10.1000/review-doi")
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
audit = repository.list_species_audit("test-shad")
|
|
|
|
self.assertIsNotNone(citations)
|
|
self.assertEqual(citations["citations"][0]["citation_key"], "jones2022review")
|
|
self.assertEqual(citations["citations"][0]["entry_type"], "article")
|
|
self.assertEqual(citations["citations"][0]["enrichment_status"], "resolved")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "citation_enrichment")
|
|
|
|
def test_editor_can_run_batch_citation_enrichment(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
- [8] Smith, B. 2021. Estuarine habitat paper.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
payloads = [
|
|
{
|
|
"citation_key": "jones2022review",
|
|
"entry_type": "article",
|
|
"normalized_text": "Jones, A. (2022). Fisheries review.",
|
|
"draft_bibtex": "@article{jones2022review,\n}",
|
|
"doi": "10.1000/review-doi",
|
|
"source_url": "https://doi.org/10.1000/review-doi",
|
|
"openalex_id": "W12345",
|
|
"resolver_source_label": "crossref:doi:10.1000/review-doi",
|
|
"enrichment_status": "resolved",
|
|
"enrichment_error": "",
|
|
"conflicts": [],
|
|
},
|
|
{
|
|
"citation_key": "smith2021estuarine",
|
|
"entry_type": "misc",
|
|
"normalized_text": "",
|
|
"draft_bibtex": "",
|
|
"doi": "",
|
|
"source_url": "",
|
|
"openalex_id": "",
|
|
"resolver_source_label": "",
|
|
"enrichment_status": "unresolved",
|
|
"enrichment_error": "No metadata match found from DOI, title, or authority identifiers.",
|
|
"conflicts": [],
|
|
},
|
|
]
|
|
|
|
with patch.object(repository, "enrich_citation_payload", side_effect=payloads):
|
|
result = repository.update_species_citations_enrichment_batch(
|
|
slug="test-shad",
|
|
username="edith",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["citation_count"], 2)
|
|
self.assertEqual(result["changed_count"], 2)
|
|
self.assertEqual(result["resolved_count"], 1)
|
|
self.assertEqual(result["unresolved_count"], 1)
|
|
self.assertEqual(result["error_count"], 0)
|
|
|
|
def test_editor_can_review_and_apply_citation_candidates(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Daniell, W.C. 1872. Letters referring to experiments of W.C. Daniell, M.D., in introducing shad into the Alabama River. Comm. Rept. U.S. Comm. Fish & Fish. 2: 387-390.
|
|
""",
|
|
username="frank",
|
|
)
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
citation = citations["citations"][0]
|
|
|
|
with patch.object(
|
|
repository,
|
|
"discover_citation_candidates",
|
|
return_value={
|
|
"seed": {
|
|
"fields": {
|
|
"author": "Daniell, W.C.",
|
|
"year": "1872",
|
|
"title": "Letters referring to experiments of W.C. Daniell, M.D., in introducing shad into the Alabama River",
|
|
"journal": "Comm. Rept. U.S. Comm. Fish & Fish.",
|
|
"volume": "2",
|
|
"pages": "387-390",
|
|
}
|
|
},
|
|
"candidate_count": 1,
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "crossref-search-1-daniell-good",
|
|
"source_label": "crossref:search:1:daniell-good",
|
|
"entry_type": "article",
|
|
"citation_key": "daniell1872lettersreferringexperiments",
|
|
"fields": {
|
|
"author": "Daniell, W.C.",
|
|
"year": "1872",
|
|
"title": "Letters referring to experiments of W.C. Daniell, M.D., in introducing shad into the Alabama River",
|
|
"journal": "Comm. Rept. U.S. Comm. Fish & Fish.",
|
|
"volume": "2",
|
|
"pages": "387-390",
|
|
},
|
|
}
|
|
],
|
|
},
|
|
):
|
|
candidates = repository.get_species_citation_candidates("test-shad", citation["id"])
|
|
|
|
self.assertIsNotNone(candidates)
|
|
self.assertEqual(candidates["candidate_count"], 1)
|
|
|
|
result = repository.apply_species_citation_candidate_selection(
|
|
slug="test-shad",
|
|
citation_id=citation["id"],
|
|
candidate={
|
|
"source_label": "crossref:search:1:daniell-good",
|
|
"entry_type": "article",
|
|
"fields": {
|
|
"author": "Daniell, W.C.",
|
|
"year": "1872",
|
|
"title": "Letters referring to experiments of W.C. Daniell, M.D., in introducing shad into the Alabama River",
|
|
"journal": "Comm. Rept. U.S. Comm. Fish & Fish.",
|
|
"volume": "2",
|
|
"pages": "387-390",
|
|
},
|
|
},
|
|
username="edith",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["citation"]["resolver_source_label"], "editor:selected:crossref:search:1:daniell-good")
|
|
self.assertEqual(result["citation"]["source_type"], "editor_selected_candidate")
|
|
self.assertEqual(result["citation"]["review_status"], "accepted")
|
|
audit = repository.list_species_audit("test-shad")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "citation_candidate_selection")
|
|
|
|
def test_editor_can_add_candidate_as_additional_citation_and_preserve_it(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Daniell, W.C. 1872. Letters referring to experiments of W.C. Daniell, M.D., in introducing shad into the Alabama River. Comm. Rept. U.S. Comm. Fish & Fish. 2: 387-390.
|
|
""",
|
|
username="frank",
|
|
)
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
source_citation = citations["citations"][0]
|
|
|
|
result = repository.add_species_citation_from_candidate(
|
|
slug="test-shad",
|
|
citation_id=source_citation["id"],
|
|
candidate={
|
|
"source_label": "crossref:search:1:daniell-related",
|
|
"entry_type": "article",
|
|
"fields": {
|
|
"author": "Jordan, F.",
|
|
"year": "2009",
|
|
"title": "Habitat use of age 0 Alabama shad in the Pascagoula River drainage, USA",
|
|
"journal": "Transactions of the American Fisheries Society",
|
|
"volume": "19",
|
|
"number": "1",
|
|
"pages": "107-115",
|
|
"doi": "10.1111/j.1600-0633.2009.00395.x",
|
|
"url": "https://doi.org/10.1111/j.1600-0633.2009.00395.x",
|
|
},
|
|
},
|
|
username="edith",
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["citation"]["source_type"], "editor_added_candidate")
|
|
self.assertEqual(result["citation"]["review_status"], "accepted")
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
self.assertEqual(citations["citation_count"], 2)
|
|
self.assertEqual(citations["citations"][1]["section_heading"], "References")
|
|
document = repository.get_species_document("test-shad")
|
|
self.assertIsNotNone(document)
|
|
self.assertIn("Habitat use of age 0 Alabama shad in the Pascagoula River drainage, USA", document["markdown"])
|
|
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown=document["markdown"],
|
|
username="frank",
|
|
)
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
self.assertEqual(citations["citation_count"], 2)
|
|
self.assertEqual(citations["citations"][1]["source_type"], "editor_added_candidate")
|
|
audit = repository.list_species_audit("test-shad")
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "document_update")
|
|
self.assertEqual(audit[1]["action"], "citation_candidate_addition")
|
|
|
|
def test_contributor_can_view_only_owned_citations(self) -> None:
|
|
created = repository.create_contributor_species(
|
|
"writer@example.org",
|
|
"""---
|
|
title: Contributor Draft
|
|
common_name: Contributor Fish
|
|
scientific_name: Pisces contributoris
|
|
species_code:
|
|
---
|
|
|
|
## References
|
|
|
|
- [12] Example, A. 2025. Draft reference.
|
|
""",
|
|
)
|
|
|
|
owned = repository.get_contributor_species_citations(created["slug"], "writer@example.org")
|
|
other = repository.get_contributor_species_citations(created["slug"], "other@example.org")
|
|
|
|
self.assertIsNotNone(owned)
|
|
self.assertEqual(owned["citation_count"], 1)
|
|
self.assertEqual(owned["citations"][0]["legacy_reference_number"], "12")
|
|
self.assertIsNone(other)
|
|
|
|
def test_public_bibliography_aggregates_species_citations(self) -> None:
|
|
repository.update_species_document_markdown(
|
|
slug="test-shad",
|
|
markdown="""---
|
|
title: Test Shad Markdown
|
|
common_name: Test Shad
|
|
scientific_name: Alosa markdownus
|
|
legacy_identifiers:
|
|
- authority: legacy-ecospecies
|
|
identifier: 4242
|
|
label: FLELMR
|
|
---
|
|
|
|
## References
|
|
|
|
- [7] Jones, A. 2022. Fisheries review.
|
|
""",
|
|
username="frank",
|
|
)
|
|
|
|
citations = repository.get_editor_species_citations("test-shad")
|
|
self.assertIsNotNone(citations)
|
|
citation = citations["citations"][0]
|
|
repository.update_species_citation_review(
|
|
slug="test-shad",
|
|
citation_id=citation["id"],
|
|
review_status="accepted",
|
|
normalized_text="Jones, A. (2022). Fisheries review.",
|
|
doi="10.1000/review-doi",
|
|
citation_key="jones2022review",
|
|
entry_type="article",
|
|
draft_bibtex="@article{jones2022review,\n doi = {10.1000/review-doi}\n}",
|
|
username="edith",
|
|
abstract_text="A short abstract about fisheries review.",
|
|
)
|
|
|
|
bibliography = repository.list_public_bibliography()
|
|
|
|
self.assertEqual(len(bibliography), 1)
|
|
self.assertEqual(bibliography[0]["citation_key"], "jones2022review")
|
|
self.assertEqual(bibliography[0]["abstract_text"], "A short abstract about fisheries review.")
|
|
self.assertEqual(bibliography[0]["legacy_reference_numbers"], ["7"])
|
|
self.assertEqual(bibliography[0]["species_count"], 1)
|
|
self.assertEqual(bibliography[0]["species_refs"][0]["slug"], "test-shad")
|
|
|
|
def test_register_contributor_creates_token_and_enforces_age_gate(self) -> None:
|
|
with self.assertRaisesRegex(ValueError, "at least 13 years old"):
|
|
repository.register_contributor("person@example.org", False)
|
|
|
|
result = repository.register_contributor("Person@Example.org", True)
|
|
|
|
self.assertEqual(result["username"], "person@example.org")
|
|
self.assertEqual(result["role"], "contributor")
|
|
self.assertEqual(result["minimum_age"], 13)
|
|
self.assertTrue(result["token"])
|
|
|
|
def test_contributor_can_create_and_edit_only_owned_species(self) -> None:
|
|
created = repository.create_contributor_species(
|
|
"writer@example.org",
|
|
"""---
|
|
title: Contributor Draft
|
|
common_name: Contributor Fish
|
|
scientific_name: Pisces contributoris
|
|
species_code:
|
|
---
|
|
|
|
## Summary
|
|
Draft summary.
|
|
|
|
## Habitat
|
|
Mangroves.
|
|
""",
|
|
)
|
|
|
|
detail = repository.get_contributor_species_detail(created["slug"], "writer@example.org")
|
|
public_detail = repository.get_species_by_slug(created["slug"])
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertIsNone(public_detail)
|
|
self.assertEqual(detail["publication_status"], "draft")
|
|
self.assertEqual(detail["common_name"], "Contributor Fish")
|
|
|
|
updated = repository.update_contributor_species_document_markdown(
|
|
created["slug"],
|
|
"""---
|
|
title: Contributor Draft Revised
|
|
common_name: Contributor Fish
|
|
scientific_name: Pisces contributoris
|
|
species_code:
|
|
---
|
|
|
|
## Summary
|
|
Revised summary.
|
|
|
|
## Habitat
|
|
Seagrass.
|
|
|
|
### Depth
|
|
Shallow bays.
|
|
""",
|
|
"writer@example.org",
|
|
)
|
|
|
|
self.assertIsNotNone(updated)
|
|
detail = repository.get_contributor_species_detail(created["slug"], "writer@example.org")
|
|
other_user_detail = repository.get_contributor_species_detail(created["slug"], "other@example.org")
|
|
audit = repository.list_species_audit(created["slug"])
|
|
|
|
self.assertIsNotNone(detail)
|
|
self.assertEqual(detail["summary"], "Revised summary.")
|
|
self.assertEqual(
|
|
[section["heading"] for section in detail["sections"]],
|
|
["Habitat", "Habitat / Depth"],
|
|
)
|
|
self.assertIsNone(other_user_detail)
|
|
self.assertIsNotNone(audit)
|
|
self.assertEqual(audit[0]["action"], "contributor_document_update")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|