Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task endpoints #768

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions services/madoc-ts/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { updateCanvasDetails } from './routes/iiif/canvases/update-canvas-detail
import { updateManifestDetails } from './routes/iiif/manifests/update-manifest-details';
import { getAutomatedUsers } from './routes/manage-site/get-automated-users';
import { createProjectExport } from './routes/projects/create-project-export';
import { getCanvasDetail } from "./routes/projects/get-canvas-detail";
import { getCanvasTask } from "./routes/projects/get-canvas-task";
import { getProjectRawData } from './routes/projects/get-project-raw-data';
import { listProjectModelEntityAutocomplete } from './routes/projects/list-project-model-entity-autocomplete';
import { updateProjectAnnotationStyle } from './routes/projects/update-project-annotation-style';
Expand Down Expand Up @@ -506,6 +508,11 @@ export const router = new TypedRouter({
'/api/madoc/projects/:id/personal-notes/:resourceId',
updateProjectNote,
],
// New canvas endpoints.
'get-project-canvas-task': [TypedRouter.GET, '/api/madoc/projects/:id/canvas/:canvasId/task', getCanvasTask],
'get-project-canvas-detail': [TypedRouter.GET, '/api/madoc/projects/:id/canvas/:canvasId/detail', getCanvasDetail],


'get-project-deletion-summary': [TypedRouter.GET, '/api/madoc/projects/:id/deletion-summary', deleteProjectSummary],
'delete-project': [TypedRouter.DELETE, '/api/madoc/projects/:id', deleteProjectEndpoint],

Expand Down
31 changes: 31 additions & 0 deletions services/madoc-ts/src/routes/projects/get-canvas-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { RouteMiddleware } from '../../../types/route-middleware';

export const getCanvasDetail: RouteMiddleware = async context => {
// This will combine the typical requests made on the canvas page and project canvas page.

// Sample requests made:
// - Annotation page
// - getSiteCanvas
// - getSiteCanvasPublishedModels
// - getSiteProjectCanvasModel
// - Manifest structure (next/prev)
// - API Canvas
// - getPersonalNote
// - getSiteProjectManifestTasks
// - getSiteProjectCanvasTasks
// - model-preview

// Some require access to the Tasks API:
// - getSiteProjectManifestTasks
// - getSiteProjectCanvasTasks

// Things this route will not do, but will also be combined into a single request:
// - Get the latest capture model (this is done on the client)
// - Can the current user submit
// - Does the user have access to the project
// - Message to the current user

// This second route will be refetched when the user makes contributions.

context.response.body = { test: 'getCanvasDetail' };
};
155 changes: 155 additions & 0 deletions services/madoc-ts/src/routes/projects/get-canvas-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import invariant from 'tiny-invariant';
import { getProject } from '../../database/queries/project-queries';
import { api } from '../../gateway/api.server';
import { CrowdsourcingCanvasTask } from '../../gateway/tasks/crowdsourcing-canvas-task';
import { RouteMiddleware } from '../../types/route-middleware';
import { parseProjectId } from '../../utility/parse-project-id';
import { optionalUserWithScope } from '../../utility/user-with-scope';

export const getCanvasTask: RouteMiddleware = async context => {
const { siteId, siteUrn, userUrn } = optionalUserWithScope(context, ['site.admin']);
const parsedId = parseProjectId(context.params.id);
const project = parsedId ? await context.connection.one(getProject(parsedId, siteId)) : undefined;
const canvasId = Number(context.params.canvasId as string);
const manifestId = context.query.manifest_id as string | undefined;
const siteApi = api.asUser({ siteId });

invariant(project, 'Project not found');

// Gather all tasks and data.
const [config, allTasks, projectTask, manifestTasks] = await Promise.all([
siteApi.getProjectConfiguration(project.id, siteUrn),
siteApi.getTasks(0, {
all: true,
root_task_id: project.task_id,
subject: `urn:madoc:canvas:${canvasId}`,
detail: true,
}),
siteApi.getTask(project.task_id),
Promise.resolve(
manifestId
? siteApi
.getTasks(0, {
all: true,
root_task_id: project.task_id,
subject: `urn:madoc:manifest:${manifestId}`,
detail: true,
})
.then(t => t?.tasks) || []
: []
),
]);

const canvasTask = allTasks.tasks.find(task => task.type === 'crowdsourcing-canvas-task') as
| CrowdsourcingCanvasTask
| undefined;

const manifestTask = manifestTasks.find(task => task.type === 'crowdsourcing-manifest-task');
const userManifestTasks = manifestTasks.filter(
task => task.type === 'crowdsourcing-task' && task.assignee && task.assignee.id === userUrn
);

// Start gathering the facts.
const isProjectActive = projectTask?.status === 1;
const isProjectPreparing = projectTask?.status === 4;
const isProjectPaused = !isProjectActive && !isProjectPreparing;
const isManifestComplete = manifestTask?.status === 3;
const isCanvasComplete = canvasTask?.status === 3;

const allUserTasks = allTasks.tasks.filter(task => task.assignee && task.assignee.id === userUrn);
const currentUserTasks = allUserTasks.filter(task => task.type === 'crowdsourcing-task');
const doesCurrentUserHaveReviewTask = allUserTasks.some(
task => task.type === 'crowdsourcing-review' && task.status !== 3
);

const hasUserTaskBeenRejected = currentUserTasks.some(task => task.status === -1);
const canUserStillSubmitAfterRejection = !config.modelPageOptions?.preventContributionAfterRejection;

const uniqueContributors = new Set<string>();

for (const task of allTasks.tasks) {
if (task.type === 'crowdsourcing-task' && task.status !== -1 && task.assignee) {
uniqueContributors.add(task.assignee.id);
}
}
const totalUniqueContributors = uniqueContributors.size;
const maxContributorsReached = config.maxContributionsPerResource
? totalUniqueContributors >= config.maxContributionsPerResource
: false;

const hasUserSubmittedTask = currentUserTasks.some(task => task.status === 2);
const hasUserCompletedTask = currentUserTasks.some(task => task.status === 3);
const hasUserCompletedAndCanStillContribute =
(hasUserCompletedTask || hasUserSubmittedTask) && config.allowSubmissionsWhenCanvasComplete;

const isUserAssignedManifest = userManifestTasks.filter(task => task.status !== -1).length > 0;
const hasUserCompletedManifest = userManifestTasks.filter(task => (task.status || 0) >= 2).length > 0;
const isManifestInReview = userManifestTasks.filter(task => (task.status || 0) === 2).length > 0;

const isUserWorkingOnCanvas = currentUserTasks.length > 0;

const inProgressUserTask = currentUserTasks.find(
task => (task.type === 'crowdsourcing-task' && task.status === 1) || task.status === 0
);
const rejectedUserTask = currentUserTasks.find(task => task.type === 'crowdsourcing-task' && task.status === -1);

const rejectedMessage = !inProgressUserTask && rejectedUserTask ? rejectedUserTask.state.rejectedMessage : undefined;
const progressMessage = inProgressUserTask ? 'In progress' : undefined;

const completeMessage = hasUserCompletedTask ? 'Complete' : hasUserSubmittedTask ? 'Submitted for review' : undefined;

const canUserMakeNewContribution =
(isUserWorkingOnCanvas &&
(!hasUserCompletedTask || hasUserCompletedAndCanStillContribute) &&
(!hasUserTaskBeenRejected || canUserStillSubmitAfterRejection)) ||
!maxContributorsReached;

context.response.body = {
// This will change the most often.
canWork: canUserMakeNewContribution,

// @todo get the new field for contribution "type" from the project template.
// - Add data that would be required from the tasks (e.g. revision)
// detail: {
// type: 'default',
// revisionId: '...',
// },
messages: {
rejected: rejectedMessage,
progress: progressMessage,
complete: completeMessage,
},
manifestFacts: {
isManifestComplete,
isManifestInReview,
isUserAssignedManifest,
hasUserCompletedManifest,
},
canvasFacts: {
isUserWorkingOnCanvas,
canUserMakeNewContribution,
hasUserSubmittedTask,
hasUserCompletedTask,
hasUserCompletedAndCanStillContribute,
maxContributorsReached,
totalUniqueContributors,
hasUserTaskBeenRejected,
canUserStillSubmitAfterRejection,
doesCurrentUserHaveReviewTask,
isManifestComplete,
isCanvasComplete,
},
projectFacts: {
isProjectPaused,
isProjectPreparing,
isProjectActive,
},
userManifestTasks,
currentUserTasks,
manifestId,
manifestTask,
canvasTask,
config,
project,
};
};
52 changes: 52 additions & 0 deletions services/madoc-ts/src/types/canvas-sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { InternationalString, MetadataItem } from '@iiif/presentation-3';
import { CaptureModel } from '../frontend/shared/capture-models/types/capture-model';

export interface CanvasSidebar {
metadata?: MetadataItem[];

annotations?: Array<{
id: string;
label: InternationalString;
type: 'project' | 'capture-model' | 'external';
count?: number;
}>;

transcriptions: Array<{
id: string;
text: string;
primary?: boolean;
}>;

translations: Array<{
id: string;
text: string;
language: string;
}>;

documents: Array<{
id: string;
label: InternationalString;
project?: { id: number; slug: string; template?: string };
model?: CaptureModel['document'];
template?: {
type: 'markdown' | 'html' | 'text';
value: string;
};
}>;

personalNotes: {
enabled?: boolean;
count: number;
};

downloads: {
enabled?: boolean;
count: number;
items: Array<{
id: string;
label: InternationalString;
type: string;
url: string;
}>;
};
}