Commit 2293e724 authored by Magnus Westergaard's avatar Magnus Westergaard
Browse files

DEICH-5630: Add related content based on work series.

parent 0073c2ba
......@@ -24,6 +24,7 @@ const DynamicList = ({
favourites,
onFavourite,
size,
cardOptions,
children
}) => {
const numberOfItems = size || DEFAULT_SIZES[type];
......@@ -66,6 +67,7 @@ const DynamicList = ({
favourites={favourites}
onFavourite={onFavourite}
isHorizontal
cardOptions={cardOptions}
hideFlagsList={["recommended", "liked"]}
/>
)}
......@@ -88,6 +90,7 @@ DynamicList.defaultProps = {
items: [],
favourites: [],
size: undefined,
cardOptions: {},
children: []
};
......@@ -101,6 +104,7 @@ DynamicList.propTypes = {
favourites: PropTypes.array,
onFavourite: PropTypes.func.isRequired,
size: PropTypes.number,
cardOptions: PropTypes.object,
children: PropTypes.node
};
......
......@@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import DynamicListFromAgent from "../DynamicListFromAgent";
import DynamicListFromQuery from "../DynamicListFromQuery";
import WorkSeriesPublicationList from "../WorkSeriesPublicationList";
import { translations } from "../../constants/translations";
import { FILM, GAME } from "../../constants/mediaTypes";
import {
......@@ -35,7 +36,7 @@ const findFallbackContributor = pub => {
const relatedContentForContributor = (contributor, mediaType) => {
if (contributor) {
return (
<Block top={8}>
<Block top={8} key={contributor.agent.id}>
<DynamicListFromAgent
agent={contributor.agent}
agentRole={contributor.mainEntry ? "mainEntry" : contributor.role}
......@@ -66,14 +67,23 @@ export default function PublicationRelatedContent({ publication }) {
)
);
const { workSeries = [] } = work;
const workSeriesContent = workSeries.map(ws => (
<Block top={8} key={ws.id}>
<WorkSeriesPublicationList workSeries={ws} mediaType={mediaType} />
</Block>
));
return (
<>
{workSeriesContent}
{mainEntryOrFallbackContent}
{additionalRolesContent}
<Block top={8}>
<DynamicListFromQuery
query={`*?mediatype=${mediaTypeQuery}&sort=issuesTotal:desc&excludeUnavailable=excludeUnavailable`}
title="Andre har også lånt"
inheritRouterQuery={false}
randomizeHits
/>
</Block>
......
import React from "react";
import PropTypes from "prop-types";
// in some cases, the partNumber is a year and we want to style
// it differently, so we define a threshold for what we consider
// to be a year (as opposed to a regular sequential part number)
const YEAR_THRESHOLD = 1500;
const NumberInSeries = ({ wsPartNumber }) => {
if (!wsPartNumber && !Number.isInteger(wsPartNumber)) {
return null;
}
let displayText = wsPartNumber;
const asNumber = Number(wsPartNumber);
if (Number.isInteger(asNumber)) {
displayText =
asNumber < YEAR_THRESHOLD ? `Nr. ${asNumber} i serien` : asNumber;
}
return (
<div className="work-card__data-item work-card__data-item--small">
{displayText}
</div>
);
};
NumberInSeries.propTypes = {
wsPartNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired
};
export default NumberInSeries;
import NumberInSeries from "./NumberInSeries";
export default NumberInSeries;
import React from "react";
import PropTypes from "prop-types";
import autoBind from "auto-bind";
import { connect } from "react-redux";
import { withRouter } from "next/router";
import Link from "next/link";
import DynamicList from "../DynamicList";
import NumberInSeries from "../WorkCard/NumberInSeries";
import { fullTitle } from "../../utilities/title";
import { pageLinkFromUri } from "../../utilities/pageLink";
import { translations } from "../../constants/translations";
import { toggleFavourites } from "../../store/application";
import { Block, Flex, Icon } from "@digibib/deichman-ui";
function seriesNumber(workSeriesUri) {
return ({ data }) => {
const wsInfo = data.workSeriesInfo.find(ws => ws.uri === workSeriesUri);
return <NumberInSeries wsPartNumber={wsInfo?.number} />;
};
}
class WorkSeriesPublicationList extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true
};
autoBind(this);
}
componentDidMount() {
this.search();
}
handleFavourite(recordId) {
const data = {
recordId
};
this.props.toggleFavourites(data);
}
async search() {
const { workSeries, mediaType, size } = this.props;
const mediaTypeQuery = mediaType
? `?filter=mediaType_${translations[mediaType]}`
: "";
this.setState({ isLoading: true });
const res = await fetch(
`/api/resources/workseries/${
workSeries.id
}/publications${mediaTypeQuery}&size=${size}&excludeSatellites=excludeSatellites`
);
const results = await res.json();
this.setState({ isLoading: false, results });
}
render() {
const { favourites, workSeries } = this.props;
const workSeriesLabel = fullTitle(workSeries);
const pLink = pageLinkFromUri(workSeries.uri, workSeriesLabel);
const { isLoading, results = {} } = this.state;
const { orderedHits = [], totalHits = 0 } = results;
const noHits = !isLoading && orderedHits.length === 0;
const displayLink = !isLoading && totalHits > orderedHits.length;
const cardOptions = {
additionalInfo: [seriesNumber(workSeries.uri)]
};
return (
<>
<DynamicList
type="standard"
title={`Mer fra ${workSeriesLabel}`}
isLoading={isLoading}
items={orderedHits}
noHits={noHits}
totalCount={totalHits}
favourites={favourites}
onFavourite={this.handleFavourite}
cardOptions={cardOptions}
/>
{displayLink && (
<Flex align="center">
<Icon type="arrow-large-right" size="20" />
<Block left={2}>
<Link href={pLink.href} as={pLink.as}>
<a>Se alt fra {workSeriesLabel}</a>
</Link>
</Block>
</Flex>
)}
</>
);
}
}
WorkSeriesPublicationList.defaultProps = {
mediaType: "",
favourites: [],
size: 6
};
WorkSeriesPublicationList.propTypes = {
mediaType: PropTypes.string,
workSeries: PropTypes.object.isRequired,
favourites: PropTypes.array,
toggleFavourites: PropTypes.func.isRequired,
size: PropTypes.number
};
function mapDispatchToProps(dispatch) {
return {
toggleFavourites: data => dispatch(toggleFavourites(data))
};
}
function mapStateToProps(state) {
const { biblioNumbers } = state.favourites;
return {
favourites: biblioNumbers
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(WorkSeriesPublicationList));
import WorkSeriesPublicationList from "./WorkSeriesPublicationList";
export default WorkSeriesPublicationList;
......@@ -8,6 +8,7 @@ import { toggleFavourites, unfreezeBody } from "../../store/application";
import { hideSearchOverlay } from "../../store/search";
import { extractIdFromSlug } from "../../utilities/slug";
import { workSeriesWorksSort } from "../../server/utils/sortWorkSeriesWorks";
import Head from "../../components/Head";
import FullScreen from "../../components/FullScreen";
......@@ -21,8 +22,7 @@ import "./styles.css";
import { uppercaseFirstLetter, pluraliseString } from "../../utilities/string";
import Pagination from "../../components/Grid/Pagination";
const MAX_PARTS = 1500;
import NumberInSeries from "../../components/WorkCard/NumberInSeries";
class SeriesPage extends React.Component {
constructor() {
......@@ -243,36 +243,6 @@ function mapDispatchToProps(dispatch) {
};
}
function worksSort(a, b) {
// partNumberAsInteger might be 0 (which is valid)
const aHasIntegerPartNumber = Number.isInteger(a.partNumberAsInteger);
const bHasIntegerPartNumber = Number.isInteger(b.partNumberAsInteger);
if (aHasIntegerPartNumber && !bHasIntegerPartNumber) {
return -1;
}
if (!aHasIntegerPartNumber && bHasIntegerPartNumber) {
return 1;
}
if (aHasIntegerPartNumber && bHasIntegerPartNumber) {
const diff = a.partNumberAsInteger - b.partNumberAsInteger;
if (diff !== 0) {
return diff;
}
}
if (
Number.isInteger(a.publicationYear) ||
Number.isInteger(b.publicationYear)
) {
const diff =
(a.publicationYear || Number.MAX_SAFE_INTEGER) -
(b.publicationYear || Number.MAX_SAFE_INTEGER);
if (diff !== 0) {
return diff;
}
}
return `${a.mainTitle}`.localeCompare(`${b.mainTitle}`);
}
function publicationsFromWorks(
works,
publications,
......@@ -280,7 +250,7 @@ function publicationsFromWorks(
otherPublications
) {
return works
.sort(worksSort)
.sort(workSeriesWorksSort)
.map((work, index) => {
let otherPub = { work: {} };
if (!publications[work.uri] && otherPublications[work.uri]) {
......@@ -320,22 +290,10 @@ function publicationsFromWorks(
}
function DisplaySeriesNumber({ data }) {
const numericPart = data.partNumberAsInteger;
if (numericPart) {
return (
<div className="work-card__data-item work-card__data-item--small">
{numericPart < MAX_PARTS ? `Nr. ${numericPart} i serien` : numericPart}
</div>
);
}
if (data.partNumber) {
return (
<div className="work-card__data-item work-card__data-item--small">
{data.partNumber}
</div>
);
}
return <></>;
const number = Number.isInteger(data.partNumberAsInteger)
? data.partNumberAsInteger
: data.partNumber;
return <NumberInSeries wsPartNumber={number} />;
}
function DisplayNotAvailableInLanguage(requestedLanguage, { data }) {
......
......@@ -7,6 +7,7 @@ const {
} = require("../utils/resourceHelpers");
const { translations } = require("../../constants/translations");
const { timeout } = require("../utils/apiUtils");
const { workSeriesWorksSort } = require("../utils/sortWorkSeriesWorks");
const logger = require("../../logger")(__filename);
const CALL_ID_HEADER = "Deichman-CallID";
......@@ -388,6 +389,57 @@ routes.get("/workseries/:workSeriesId", async (req, res) => {
}
});
// Get workSeries publications (in order)
routes.get("/workseries/:workSeriesId/publications", async (req, res) => {
const { filter, excludeSatellites, size } = req.query;
let query = filter ? `?filter=${encodeURIComponent(filter)}` : "";
if (excludeSatellites === "excludeSatellites") {
query += "&excludeSatellites=excludeSatellites";
}
const deichmanCallId = req.headers[CALL_ID_HEADER];
logger.info(
`Fetching publications for workseries ${req.params.workSeriesId}`,
{
workSeries_id: req.params.workSeriesId,
call_id: deichmanCallId
}
);
try {
const results = await fetch(
`${INTERNAL_URL_EULER}/api/augmented/workSeries/${
req.params.workSeriesId
}`
);
const { works } = await results.json();
const workUris = works.map(work => work.uri);
const sibylRes = await timeout(
TIMEOUT_MS,
fetch(`${INTERNAL_URL_SIBYL}/search/publicationByWorkUris${query}`, {
method: "POST",
body: JSON.stringify({ ids: workUris })
})
);
const { hits, totalHits } = await sibylRes.json();
works.sort(workSeriesWorksSort);
hits.sort(
(pubA, pubB) =>
works.findIndex(w => pubA.work.uri === w.uri) -
works.findIndex(w => pubB.work.uri === w.uri)
);
res.status(200).send({ orderedHits: hits.slice(0, size), totalHits });
} catch (error) {
logger.warn(`ERROR calling get workseries publications ${error.message}`, {
...error,
call_id: deichmanCallId
});
res.sendStatus(500);
}
});
// Get serial by ID
routes.get("/serial/:serialId", async (req, res) => {
const deichmanCallId = req.headers[CALL_ID_HEADER];
......
/*
Sort works in a workSeries according to
1. partNumber if it's an integer (the one from the WorkSeriesPart relation, not the on which is part of the work's title)
2. publication year
3. main title
*/
function workSeriesWorksSort(a, b) {
// partNumberAsInteger might be 0 (which is valid)
const aHasIntegerPartNumber = Number.isInteger(a.partNumberAsInteger);
const bHasIntegerPartNumber = Number.isInteger(b.partNumberAsInteger);
if (aHasIntegerPartNumber && !bHasIntegerPartNumber) {
return -1;
}
if (!aHasIntegerPartNumber && bHasIntegerPartNumber) {
return 1;
}
if (aHasIntegerPartNumber && bHasIntegerPartNumber) {
const diff = a.partNumberAsInteger - b.partNumberAsInteger;
if (diff !== 0) {
return diff;
}
}
if (
Number.isInteger(a.publicationYear) ||
Number.isInteger(b.publicationYear)
) {
const diff =
(a.publicationYear || Number.MAX_SAFE_INTEGER) -
(b.publicationYear || Number.MAX_SAFE_INTEGER);
if (diff !== 0) {
return diff;
}
}
return `${a.mainTitle}`.localeCompare(`${b.mainTitle}`);
}
module.exports = { workSeriesWorksSort };
......@@ -134,7 +134,8 @@ func (p *publicationSearchClient) SearchByWorkUris(uris []string, params url.Val
return SearchResults{}
}
parsedResults := transformElasticResponse(result, size, excludeAdditionalMediaTypes, false, DebugFlags{})
excludeSatellites := params.Get("excludeSatellites") == "excludeSatellites"
parsedResults := transformElasticResponse(result, size, excludeAdditionalMediaTypes, excludeSatellites, DebugFlags{})
return parsedResults
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment