src/server/routes/revision.js
/*
* Copyright (C) 2015-2016 Ben Ockmore
* 2016 Sean Burke
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import * as baseFormatter from '../helpers/diffFormatters/base';
import * as entityFormatter from '../helpers/diffFormatters/entity';
import * as entityRoutes from './entity/entity';
import * as error from '../../common/helpers/error';
import * as languageSetFormatter from '../helpers/diffFormatters/languageSet';
import * as middleware from '../helpers/middleware';
import * as propHelpers from '../../client/helpers/props';
import * as publisherSetFormatter from '../helpers/diffFormatters/publisherSet';
import * as releaseEventSetFormatter from
'../helpers/diffFormatters/releaseEventSet';
import {escapeProps, generateProps} from '../helpers/props';
import Layout from '../../client/containers/layout';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import RevisionPage from '../../client/components/pages/revision';
import _ from 'lodash';
import express from 'express';
import log from 'log';
import {makePromiseFromObject} from '../../common/helpers/utils';
import target from '../templates/target';
const router = express.Router();
function formatAuthorChange(change) {
if (_.isEqual(change.path, ['beginDate'])) {
return baseFormatter.formatScalarChange(change, 'Begin Date');
}
if (_.isEqual(change.path, ['endDate'])) {
return baseFormatter.formatScalarChange(change, 'End Date');
}
if (_.isEqual(change.path, ['gender'])) {
return baseFormatter.formatGenderChange(change);
}
if (_.isEqual(change.path, ['ended'])) {
return baseFormatter.formatEndedChange(change);
}
if (_.isEqual(change.path, ['authorType']) ||
_.isEqual(change.path, ['authorType', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Author Type');
}
if (_.isEqual(change.path, ['beginArea']) ||
_.isEqual(change.path, ['beginArea', 'name'])) {
return baseFormatter.formatAreaChange(change, 'Begin Area');
}
if (_.isEqual(change.path, ['endArea']) ||
_.isEqual(change.path, ['endArea', 'name'])) {
return baseFormatter.formatAreaChange(change, 'End Area');
}
return null;
}
function formatEditionChange(change) {
if (_.isEqual(change.path, ['editionGroupBbid'])) {
return baseFormatter.formatScalarChange(change, 'EditionGroup');
}
if (publisherSetFormatter.changed(change)) {
return publisherSetFormatter.format(change);
}
if (releaseEventSetFormatter.changed(change)) {
return releaseEventSetFormatter.format(change);
}
if (languageSetFormatter.changed(change)) {
return languageSetFormatter.format(change);
}
if (_.isEqual(change.path, ['width']) ||
_.isEqual(change.path, ['height']) ||
_.isEqual(change.path, ['depth']) ||
_.isEqual(change.path, ['weight'])) {
return baseFormatter.formatScalarChange(
change, _.startCase(change.path[0])
);
}
if (_.isEqual(change.path, ['pages'])) {
return baseFormatter.formatScalarChange(change, 'Page Count');
}
if (_.isEqual(change.path, ['editionFormat']) ||
_.isEqual(change.path, ['editionFormat', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Edition Format');
}
if (_.isEqual(change.path, ['editionStatus']) ||
_.isEqual(change.path, ['editionStatus', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Edition Status');
}
return null;
}
function formatPublisherChange(change) {
if (_.isEqual(change.path, ['beginDate'])) {
return baseFormatter.formatScalarChange(change, 'Begin Date');
}
if (_.isEqual(change.path, ['endDate'])) {
return baseFormatter.formatScalarChange(change, 'End Date');
}
if (_.isEqual(change.path, ['ended'])) {
return baseFormatter.formatEndedChange(change);
}
if (_.isEqual(change.path, ['publisherType']) ||
_.isEqual(change.path, ['publisherType', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Publisher Type');
}
if (_.isEqual(change.path, ['area']) ||
_.isEqual(change.path, ['area', 'name'])) {
return baseFormatter.formatAreaChange(change);
}
return null;
}
function formatSeriesChange(change) {
if (_.isEqual(change.path, ['seriesOrderingType']) ||
_.isEqual(change.path, ['seriesOrderingType', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Series Ordering Type');
}
if (_.isEqual(change.path, ['entityType'])) {
return baseFormatter.formatTypeChange(change, 'Series Type');
}
return null;
}
function formatWorkChange(change) {
if (languageSetFormatter.changed(change)) {
return languageSetFormatter.format(change);
}
if (_.isEqual(change.path, ['workType']) ||
_.isEqual(change.path, ['workType', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Work Type');
}
return null;
}
function formatEditionGroupChange(change) {
if (_.isEqual(change.path, ['editionGroupType']) ||
_.isEqual(change.path, ['editionGroupType', 'label'])) {
return baseFormatter.formatTypeChange(change, 'Edition Group Type');
}
return [];
}
function diffRevisionsWithParents(orm, entityRevisions, entityType) {
// entityRevisions - collection of *entityType*_revisions matching id
const promises = entityRevisions.map(
async (revision) => {
const dataId = revision.get('dataId');
const revisionEntity = revision.related('entity');
const entityBBID = revisionEntity.get('bbid');
const entity = await orm.func.entity.getEntity(orm, entityType, entityBBID);
const isEntityDeleted = !entity.dataId;
try {
const parent = await revision.parent();
let isNew = false;
const isDeletion = !dataId;
if (!parent) {
isNew = Boolean(dataId);
}
return makePromiseFromObject({
changes: revision.diff(parent),
entity: revisionEntity,
entityAlias: dataId ?
revision.related('data').fetch({require: false, withRelated: ['aliasSet.defaultAlias', 'aliasSet.aliases']}) :
orm.func.entity.getEntityParentAlias(
orm, entityType, revision.get('bbid')
),
isDeletion,
isEntityDeleted,
isNew,
revision
});
}
// If calling .parent() is rejected (no parent rev), we still want to go ahead without the parent
catch {
return makePromiseFromObject({
changes: revision.diff(null),
entity: revisionEntity,
entityAlias: dataId ?
revision.related('data').fetch({require: false, withRelated: ['aliasSet.defaultAlias', 'aliasSet.aliases']}) :
orm.func.entity.getEntityParentAlias(
orm, entityType, revision.get('bbid')
),
isDeletion: !dataId,
isEntityDeleted,
isNew: Boolean(dataId),
revision
});
}
}
);
return Promise.all(promises);
}
router.param(
'id',
middleware.checkValidRevisionId
);
router.get('/:id', async (req, res, next) => {
const {
AuthorRevision, EditionRevision, EditionGroupRevision,
SeriesRevision, PublisherRevision, Revision, WorkRevision
} = req.app.locals.orm;
let revision;
async function _createRevision(EntityRevisionModel, entityType) {
/**
* EntityRevisions can have duplicate ids
* the 'merge' and 'remove' options instructs the ORM to consider that normal instead of merging
* see https://github.com/bookshelf/bookshelf/pull/1846
*/
try {
const entityRevisions = await EntityRevisionModel.forge()
.where('id', req.params.id)
.fetchAll({merge: false, remove: false, require: false, withRelated: 'entity'});
return await diffRevisionsWithParents(req.app.locals.orm, entityRevisions, entityType);
}
catch (err) {
log.error(err);
throw err;
}
}
try {
/*
* Here, we need to get the Revision, then get all <Entity>Revision
* objects with the same ID, formatting each revision individually, then
* concatenating the diffs
*/
revision = await new Revision({id: req.params.id})
.fetch({
withRelated: [
'author',
'author.titleUnlock.title',
{
'notes'(q) {
q.orderBy('note.posted_at');
}
},
'notes.author',
'notes.author.titleUnlock.title'
]
})
.catch(Revision.NotFoundError, () => {
throw new error.NotFoundError(`Revision #${req.params.id} not found`, req);
});
const authorDiffs = await _createRevision(AuthorRevision, 'Author');
const editionDiffs = await _createRevision(EditionRevision, 'Edition');
const editionGroupDiffs = await _createRevision(EditionGroupRevision, 'EditionGroup');
const publisherDiffs = await _createRevision(PublisherRevision, 'Publisher');
const seriesDiffs = await _createRevision(SeriesRevision, 'Series');
const workDiffs = await _createRevision(WorkRevision, 'Work');
const diffs = _.concat(
entityFormatter.formatEntityDiffs(
authorDiffs,
'Author',
formatAuthorChange
),
entityFormatter.formatEntityDiffs(
editionDiffs,
'Edition',
formatEditionChange
),
entityFormatter.formatEntityDiffs(
editionGroupDiffs,
'EditionGroup',
formatEditionGroupChange
),
entityFormatter.formatEntityDiffs(
publisherDiffs,
'Publisher',
formatPublisherChange
),
entityFormatter.formatEntityDiffs(
seriesDiffs,
'Series',
formatSeriesChange
),
entityFormatter.formatEntityDiffs(
workDiffs,
'Work',
formatWorkChange
)
);
const props = generateProps(req, res, {
diffs,
revision: revision.toJSON(),
title: 'RevisionPage'
});
const markup = ReactDOMServer.renderToString(
<Layout {...propHelpers.extractLayoutProps(props)}>
<RevisionPage
diffs={props.diffs}
revision={props.revision}
user={props.user}
/>
</Layout>
);
const script = '/js/revision.js';
return res.send(target({
markup,
props: escapeProps(props),
script
}));
}
catch (err) {
return next(err);
}
});
router.post('/:id/note', (req, res) => {
entityRoutes.addNoteToRevision(req, res);
});
export default router;