Skip to content

Commit

Permalink
backend & ui: inject data authors from literature link
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalEgn committed Jan 20, 2025
1 parent 8587e45 commit 37ddbe6
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 65 deletions.
19 changes: 19 additions & 0 deletions backend/inspirehep/records/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,24 @@ def default_handler(error):
}
)

DATA_AUTHORS = deepcopy(DATA)
DATA_AUTHORS.update(
{
"default_endpoint_prefix": False,
"search_factory_imp": (
"inspirehep.search.factories.search:search_factory_only_with_aggs"
),
"pid_type": "lit",
"list_route": "/data/authors/",
"item_route": '/data/<inspirepid(dat,record_class="inspirehep.records.api.DataRecord"):pid_value>/authors',
"record_serializers": {
"application/json": (f"{INSPIRE_SERIALIZERS}:data_authors_json_response")
},
"search_serializers": {
"application/json": "invenio_records_rest.serializers:json_v1_search"
},
}
)
INSTITUTIONS = deepcopy(RECORD)
INSTITUTIONS.update(
{
Expand Down Expand Up @@ -623,6 +641,7 @@ def default_handler(error):
"conferences": CONFERENCES,
"conferences_facets": CONFERENCES_FACETS,
"data": DATA,
"data_authors": DATA_AUTHORS,
"institutions": INSTITUTIONS,
"seminars": SEMINARS,
"seminars_facets": SEMINARS_FACETS,
Expand Down
21 changes: 21 additions & 0 deletions backend/inspirehep/records/marshmallow/data/authors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# Copyright (C) 2019 CERN.
#
# inspirehep is free software; you can redistribute it and/or modify it under
# the terms of the MIT License; see LICENSE file for more details.

from inspire_dojson.utils import strip_empty_values
from marshmallow import Schema, fields, post_dump

from inspirehep.records.marshmallow.data.utils import get_authors


class DataAuthorsSchema(Schema):
authors = fields.Method("get_authors", dump_only=True)

def get_authors(self, data):
return get_authors(data)

@post_dump
def strip_empty(self, data):
return strip_empty_values(data)
30 changes: 29 additions & 1 deletion backend/inspirehep/records/marshmallow/data/es.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,37 @@
# inspirehep is free software; you can redistribute it and/or modify it under
# the terms of the MIT License; see LICENSE file for more details.

from inspire_dojson.utils import get_recid_from_ref
from inspire_utils.record import get_value
from marshmallow import fields

from inspirehep.records.marshmallow.base import ElasticSearchBaseSchema
from inspirehep.records.marshmallow.data.base import DataRawSchema
from inspirehep.records.marshmallow.fields.list_with_limit import ListWithLimit
from inspirehep.records.marshmallow.literature.common.author import AuthorSchemaV1
from inspirehep.search.api import LiteratureSearch


class DataElasticSearchSchema(ElasticSearchBaseSchema, DataRawSchema):
pass
authors = ListWithLimit(fields.Nested(AuthorSchemaV1, dump_only=True), limit=10)

def fetch_authors_from_literature(self, data):
literature = get_value(data, "literature")
if not literature:
return None

control_number = get_recid_from_ref(get_value(literature[0], "record"))
if control_number:
literature_record = LiteratureSearch.get_records_by_pids(
[(0, control_number)], source="authors"
)
if not literature_record:
return []
return literature_record[0].to_dict().get("authors", [])

def dump(self, obj, *args, **kwargs):
if not obj.get("authors"):
authors = self.fetch_authors_from_literature(obj)
if authors:
obj["authors"] = authors
return super().dump(obj, *args, **kwargs)
19 changes: 18 additions & 1 deletion backend/inspirehep/records/marshmallow/data/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
# inspirehep is free software; you can redistribute it and/or modify it under
# the terms of the MIT License; see LICENSE file for more details.

from marshmallow import fields
from marshmallow import fields, missing

from inspirehep.records.marshmallow.common.literature_record import (
LiteratureRecordSchemaV1,
)
from inspirehep.records.marshmallow.data.base import DataPublicSchema
from inspirehep.records.marshmallow.data.utils import get_authors
from inspirehep.records.marshmallow.fields.list_with_limit import ListWithLimit
from inspirehep.records.marshmallow.literature.common.author import AuthorSchemaV1


class DataBaseSchema(DataPublicSchema):
Expand All @@ -18,6 +21,20 @@ class DataBaseSchema(DataPublicSchema):

class DataDetailSchema(DataBaseSchema):
literature = fields.Nested(LiteratureRecordSchemaV1, dump_only=True, many=True)
authors = ListWithLimit(fields.Nested(AuthorSchemaV1, dump_only=True), limit=10)
number_of_authors = fields.Method("get_number_of_authors")

def get_number_of_authors(self, data):
authors = data.get("authors")
if not authors:
authors = get_authors(data)
return self.get_len_or_missing(authors)

@staticmethod
def get_len_or_missing(maybe_none_list):
if maybe_none_list is None:
return missing
return len(maybe_none_list)


class DataListSchema(DataBaseSchema):
Expand Down
29 changes: 29 additions & 0 deletions backend/inspirehep/records/marshmallow/data/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from inspire_dojson.utils import get_recid_from_ref
from inspire_utils.record import get_value

from inspirehep.records.marshmallow.literature.common.author import AuthorSchemaV1
from inspirehep.search.api import LiteratureSearch


def get_authors(data):
schema = AuthorSchemaV1(many=True)
authors = data.get("authors", [])
if authors:
return schema.dump(authors)[0]

literature = get_value(data, "literature")
if not literature:
return schema.dump(authors)[0]

control_number = get_recid_from_ref(get_value(literature[0], "record"))
if not control_number:
return schema.dump(authors)[0]

literature_record = LiteratureSearch.get_records_by_pids(
[(0, control_number)], source="authors"
)
if not literature_record:
return schema.dump(authors)[0]

linked_authors = literature_record[0].to_dict().get("authors", [])
return schema.dump(linked_authors)[0]
1 change: 1 addition & 0 deletions backend/inspirehep/records/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
seminars_json_response,
seminars_json_response_search,
data_json_detail_response,
data_authors_json_response,
data_json_list_response,
data_json_response,
data_json_response_search,
Expand Down
1 change: 1 addition & 0 deletions backend/inspirehep/records/serializers/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
)
from .data import (
data_json_detail_response,
data_authors_json_response,
data_json_list_response,
data_json_response,
data_json_response_search,
Expand Down
6 changes: 6 additions & 0 deletions backend/inspirehep/records/serializers/json/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from inspirehep.accounts.api import is_superuser_or_cataloger_logged_in
from inspirehep.records.marshmallow.base import wrap_schema_class_with_metadata
from inspirehep.records.marshmallow.data.authors import DataAuthorsSchema
from inspirehep.records.marshmallow.data.base import (
DataAdminSchema,
DataPublicSchema,
Expand Down Expand Up @@ -47,3 +48,8 @@
data_json_list_response = search_responsify(
data_json_list, "application/vnd+inspire.record.ui+json"
)

# Data Authors
data_authors_json = JSONSerializer(wrap_schema_class_with_metadata(DataAuthorsSchema))

data_authors_json_response = record_responsify(data_authors_json, "application/json")
132 changes: 132 additions & 0 deletions backend/tests/integration/records/serializers/json/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,135 @@ def test_data_detail_citation_count(inspire_app):
response_metadata = response.json["metadata"]

assert expected_citation_count == response_metadata["citation_count"]


def test_data_detail_schema_limits_authors(inspire_app):
with inspire_app.test_client() as client:
headers = {"Accept": "application/vnd+inspire.record.ui+json"}
authors = [{"full_name": f"Author {i}"} for i in range(15)]
data_record = create_record("dat", data={"authors": authors})

response = client.get(f"/data/{data_record['control_number']}", headers=headers)

response_metadata = response.json["metadata"]
assert "authors" in response_metadata
assert len(response_metadata["authors"]) == 10
assert response_metadata["number_of_authors"] == 15


def test_data_detail_schema_fetches_for_author_count(inspire_app):
with inspire_app.test_client() as client:
headers = {"Accept": "application/vnd+inspire.record.ui+json"}

literature_authors = [{"full_name": f"Author {i}"} for i in range(15)]
lit_record = create_record("lit", data={"authors": literature_authors})

data_record = create_record(
"dat",
data={
"literature": [
{
"record": {
"$ref": f"http://localhost:5000/api/literature/{lit_record['control_number']}"
}
}
]
},
)

response = client.get(f"/data/{data_record['control_number']}", headers=headers)
response_metadata = response.json["metadata"]
assert "authors" not in response_metadata
assert response_metadata["number_of_authors"] == 15


def test_data_public_schema_does_not_limit_authors(inspire_app):
with inspire_app.test_client() as client:
authors = [{"full_name": f"Author {i}"} for i in range(15)]
data_record = create_record("dat", data={"authors": authors})

response = client.get(f"/data/{data_record['control_number']}")

response_metadata = response.json["metadata"]

assert "authors" in response_metadata
assert len(response_metadata["authors"]) == 15


def test_data_authors_schema(inspire_app):
with inspire_app.test_client() as client:
headers = {"Accept": "application/json"}
authors = [{"full_name": f"Author {i}"} for i in range(15)]
data_record = create_record("dat", data={"authors": authors})

response = client.get(
f"/data/{data_record['control_number']}/authors", headers=headers
)

response_metadata = response.json["metadata"]
assert "authors" in response_metadata
assert len(response_metadata["authors"]) == 15


def test_data_author_schema_fetches_from_linked_literature(inspire_app):
with inspire_app.test_client() as client:
headers = {"Accept": "application/json"}
literature_authors = [{"full_name": f"Author {i}"} for i in range(15)]
lit_record = create_record("lit", data={"authors": literature_authors})

data_record = create_record(
"dat",
data={
"literature": [
{
"record": {
"$ref": f"http://localhost:5000/api/literature/{lit_record['control_number']}"
}
}
]
},
)

response = client.get(
f"/data/{data_record['control_number']}/authors", headers=headers
)
response_metadata = response.json["metadata"]
assert "authors" in response_metadata
assert len(response_metadata["authors"]) == 15


def test_data_elasticsearch_schema_limits_authors(inspire_app):
with inspire_app.test_client() as client:
authors = [{"full_name": f"Author {i}"} for i in range(15)]
create_record("dat", data={"authors": authors})

response = client.get("/data/")
response_data_hit = response.json["hits"]["hits"][0]["metadata"]
assert "authors" in response_data_hit
assert len(response_data_hit["authors"]) == 10
assert "number_of_authors" not in response_data_hit


def test_data_elasticsearch_schema_fetches_from_linked_literature(inspire_app):
with inspire_app.test_client() as client:
literature_authors = [{"full_name": f"Author {i}"} for i in range(15)]
lit_record = create_record("lit", data={"authors": literature_authors})

create_record(
"dat",
data={
"literature": [
{
"record": {
"$ref": f"http://localhost:5000/api/literature/{lit_record['control_number']}"
}
}
]
},
)

response = client.get("/data/")
response_data_hit = response.json["hits"]["hits"][0]["metadata"]
assert "authors" in response_data_hit
assert len(response_data_hit["authors"]) == 10
assert "number_of_authors" not in response_data_hit
39 changes: 37 additions & 2 deletions ui/src/actions/__tests__/data.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import MockAdapter from 'axios-mock-adapter';

import { getStore } from '../../fixtures/store';
import http from '../../common/http';
import {
DATA_REQUEST,
DATA_SUCCESS,
DATA_ERROR,
DATA_AUTHORS_REQUEST,
DATA_AUTHORS_SUCCESS,
DATA_AUTHORS_ERROR,
} from '../actionTypes';
import fetchData from '../data';
import fetchData, { fetchDataAuthors } from '../data';

const mockHttp = new MockAdapter(http.httpClient);

Expand Down Expand Up @@ -50,3 +52,36 @@ describe('data - async action creators', () => {
});
});
});

describe('data authors', () => {
it('happy - creates DATA_AUTHORS_SUCCESS', async () => {
mockHttp.onGet('/data/123/authors').replyOnce(200, {});

const expectedActions = [
{ type: DATA_AUTHORS_REQUEST },
{ type: DATA_AUTHORS_SUCCESS, payload: {} },
];

const store = getStore();
await store.dispatch(fetchDataAuthors(123));
expect(store.getActions()).toEqual(expectedActions);
});

it('unhappy - creates DATA_AUTHORS_ERROR', async () => {
mockHttp.onGet('/data/123/authors').replyOnce(500);

const expectedActions = [
{ type: DATA_AUTHORS_REQUEST },
{
type: DATA_AUTHORS_ERROR,
payload: {
error: { status: 500 },
},
},
];

const store = getStore();
await store.dispatch(fetchDataAuthors(123));
expect(store.getActions()).toEqual(expectedActions);
});
});
3 changes: 3 additions & 0 deletions ui/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export const JOURNAL_ERROR = 'JOURNAL_ERROR';
export const DATA_REQUEST = 'DATA_REQUEST';
export const DATA_SUCCESS = 'DATA_SUCCESS';
export const DATA_ERROR = 'DATA_ERROR';
export const DATA_AUTHORS_REQUEST = 'DATA_AUTHORS_REQUEST';
export const DATA_AUTHORS_SUCCESS = 'DATA_AUTHORS_SUCCESS';
export const DATA_AUTHORS_ERROR = 'DATA_AUTHORS_ERROR';

export const UI_CLOSE_BANNER = 'UI_CLOSE_BANNER';
export const UI_CHANGE_GUIDE_MODAL_VISIBILITY =
Expand Down
Loading

0 comments on commit 37ddbe6

Please sign in to comment.