Expanded edit fields. Added revision history to scene and user pages.

This commit is contained in:
2024-10-06 02:45:56 +02:00
parent 8bf9e22b39
commit 8f843f321d
57 changed files with 1664 additions and 156 deletions

View File

@@ -1,11 +1,13 @@
import config from 'config';
import util from 'util'; /* eslint-disable-line no-unused-vars */
import { MerkleJson } from 'merkle-json';
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
import { utilsApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
import { fetchTagsById } from './tags.js';
import { fetchMoviesById } from './movies.js';
import { fetchEntitiesById } from './entities.js';
import { curateStash } from './stashes.js';
import { curateMedia } from './media.js';
@@ -14,6 +16,7 @@ import promiseProps from '../utils/promise-props.js';
import initLogger from './logger.js';
const logger = initLogger();
const mj = new MerkleJson();
function getWatchUrl(scene) {
if (scene.url) {
@@ -600,59 +603,169 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
};
}
async function applySceneValueDelta(sceneId, delta, trx) {
console.log('value delta', delta);
function curateRevision(revision) {
return {
id: revision.id,
sceneId: revision.scene_id,
base: revision.base,
deltas: revision.deltas,
hash: revision.hash,
comment: revision.comment,
user: revision.user_id && {
id: revision.user_id,
username: revision.username,
},
review: typeof revision.approved === 'boolean' ? {
isApproved: revision.approved,
userId: revision.reviewed_by,
username: revision.reviewer_username,
reviewedAt: revision.reviewed_at,
} : null,
appliedAt: revision.applied_at,
failed: revision.failed,
createdAt: revision.created_at,
};
}
export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
const limit = filters.limit || 50;
const page = filters.page || 1;
const revisions = await knexOwner('scenes_revisions')
.select(
'scenes_revisions.*',
'users.username as username',
'reviewers.username as reviewer_username',
)
.leftJoin('users', 'users.id', 'scenes_revisions.user_id')
.leftJoin('users as reviewers', 'reviewers.id', 'scenes_revisions.reviewed_by')
.modify((builder) => {
if (reqUser?.role !== 'admin' && !filters.userId && !filters.sceneId) {
builder.where('user_id', reqUser.id);
}
if (filters.userId) {
if (reqUser?.role !== 'admin' && filters.userId !== reqUser.id) {
throw new HttpError('You are not permitted to view revisions from other users.', 403);
}
builder.where('scenes_revisions.user_id', filters.userId);
}
if (revisionId) {
builder.where('scenes_revisions.id', revisionId);
return;
}
if (filters.sceneId) {
builder.where('scenes_revisions.scene_id', filters.sceneId);
}
console.log(filters);
if (filters.isFinalized === false) {
builder.whereNull('approved');
}
if (filters.isFinalized === true) {
builder.whereNotNull('approved');
}
})
.orderBy('created_at', 'desc')
.limit(limit)
.offset((page - 1) * limit);
const actorIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.actors, ...(revision.deltas.find((delta) => delta.key === 'actors')?.value || [])])));
const tagIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.tags, ...(revision.deltas.find((delta) => delta.key === 'tags')?.value || [])])));
const movieIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.movies, ...(revision.deltas.find((delta) => delta.key === 'movies')?.value || [])])));
const [actors, tags, movies] = await Promise.all([
fetchActorsById(actorIds),
fetchTagsById(tagIds),
fetchMoviesById(movieIds),
]);
const curatedRevisions = revisions.map((revision) => curateRevision(revision));
return {
revisions: curatedRevisions,
revision: revisionId && curatedRevisions.find((revision) => revision.id === revisionId),
actors,
tags,
movies,
};
}
const keyMap = {
productionDate: 'production_date',
};
async function applySceneValueDelta(sceneId, delta, trx) {
return knexOwner('releases')
.where('id', sceneId)
.update(delta.key, delta.value)
.update(keyMap[delta.key] || delta.key, delta.value)
.transacting(trx);
}
async function applySceneActorsDelta(sceneId, delta, trx) {
console.log('actors delta', delta);
await knexOwner('releases_actors')
.where('release_id', sceneId)
.delete()
.transacting(trx);
await knexOwner('releases_actors')
.insert(delta.value.map((actorId) => ({
release_id: sceneId,
actor_id: actorId,
})))
.transacting(trx);
if (delta.value.length > 0) {
await knexOwner('releases_actors')
.insert(delta.value.map((actorId) => ({
release_id: sceneId,
actor_id: actorId,
})))
.transacting(trx);
}
}
async function applySceneTagsDelta(sceneId, delta, trx) {
console.log('tags delta', delta);
// don't remove unidentified tags
await knexOwner('releases_tags')
.where('release_id', sceneId)
.whereNotNull('tag_id')
.delete()
.transacting(trx);
await knexOwner('releases_tags')
.insert(delta.value.map((tagId) => ({
release_id: sceneId,
tag_id: tagId,
source: 'editor',
})))
.transacting(trx);
if (delta.value.length > 0) {
await knexOwner('releases_tags')
.insert(delta.value.map((tagId) => ({
release_id: sceneId,
tag_id: tagId,
source: 'editor',
})))
.transacting(trx);
}
}
async function applySceneRevision(sceneIds) {
async function applySceneMoviesDelta(sceneId, delta, trx) {
await knexOwner('movies_scenes')
.where('scene_id', sceneId)
.delete()
.transacting(trx);
if (delta.value.length > 0) {
await knexOwner('movies_scenes')
.insert(delta.value.map((movieId) => ({
scene_id: sceneId,
movie_id: movieId,
})))
.transacting(trx);
}
}
async function applySceneRevision(revisionIds) {
const revisions = await knexOwner('scenes_revisions')
.whereIn('scene_id', sceneIds)
.whereNull('applied_at');
.whereIn('id', revisionIds)
.whereNull('applied_at'); // should not re-apply revision that was already applied
await revisions.reduce(async (chain, revision) => {
await chain;
console.log('revision', revision);
await knexOwner.transaction(async (trx) => {
await revision.deltas.map(async (delta) => {
if ([
@@ -660,10 +773,10 @@ async function applySceneRevision(sceneIds) {
'description',
'date',
'duration',
'production_date',
'production_location',
'production_city',
'production_state',
'productionDate',
'productionLocation',
'productionCity',
'productionState',
].includes(delta.key)) {
return applySceneValueDelta(revision.scene_id, delta, trx);
}
@@ -676,6 +789,10 @@ async function applySceneRevision(sceneIds) {
return applySceneTagsDelta(revision.scene_id, delta, trx);
}
if (delta.key === 'movies') {
return applySceneMoviesDelta(revision.scene_id, delta, trx);
}
return null;
});
@@ -690,20 +807,44 @@ async function applySceneRevision(sceneIds) {
}, Promise.resolve());
}
const keyMap = {
productionDate: 'production_date',
};
export async function reviewSceneRevision(revisionId, isApproved, { feedback }, reqUser) {
if (!reqUser || reqUser.role === 'user') {
throw new HttpError('You are not permitted to approve revisions', 403);
}
export async function createSceneRevision(sceneId, { edits, comment }, reqUser) {
if (typeof isApproved !== 'boolean') {
throw new HttpError('You must either approve or reject the revision', 400);
}
await knexOwner('scenes_revisions')
.where('id', revisionId)
.whereRaw('approved is not true') // don't rerun approved and applied revision, must be forked into new revision instead
.whereNull('applied_at')
.update({
approved: isApproved,
reviewed_at: knex.fn.now(),
reviewed_by: reqUser.id,
feedback,
});
if (isApproved) {
await applySceneRevision([revisionId]);
}
}
export async function createSceneRevision(sceneId, { edits, comment, apply }, reqUser) {
const [
[scene],
openRevisions,
] = await Promise.all([
fetchScenesById([sceneId], { reqUser, includeAssets: true }),
fetchScenesById([sceneId], {
reqUser,
includeAssets: true,
includePartOf: true,
}),
knexOwner('scenes_revisions')
.where('user_id', reqUser.id)
.whereNull('approved_by')
.whereNot('failed', true),
.whereNull('approved'),
]);
if (!scene) {
@@ -754,25 +895,28 @@ export async function createSceneRevision(sceneId, { edits, comment }, reqUser)
}
}
return {
key: keyMap[key] || key,
value,
};
return { key, value };
}).filter(Boolean);
if (deltas.length === 0) {
throw new HttpError('No effective changes provided', 400);
}
await knexOwner('scenes_revisions').insert({
user_id: reqUser.id,
scene_id: scene.id,
base: JSON.stringify(baseScene),
deltas: JSON.stringify(deltas),
comment,
});
const [revisionEntry] = await knexOwner('scenes_revisions')
.insert({
user_id: reqUser.id,
scene_id: scene.id,
base: JSON.stringify(baseScene),
deltas: JSON.stringify(deltas),
hash: mj.hash({
base: baseScene,
deltas,
}),
comment,
})
.returning('id');
if (['admin', 'editor'].includes(reqUser.role)) {
await applySceneRevision([scene.id]);
if (['admin', 'editor'].includes(reqUser.role) && apply) {
await reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
}
}