Skip to content

Commit

Permalink
Markdown documentation for tasks/projects (#6191)
Browse files Browse the repository at this point in the history
<!-- Raise an issue to propose your change
(https://github.com/opencv/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution
guide](https://opencv.github.io/cvat/docs/contributing/). -->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<img width="1130" alt="image"
src="https://github.com/opencv/cvat/assets/40690378/3c1b767d-2aa2-474b-af1e-1fc02d8c359d">
<img width="1133" alt="image"
src="https://github.com/opencv/cvat/assets/40690378/fdf6115a-e88d-4da6-83fb-c9fddd871e49">
<img width="1920" alt="image"
src="https://github.com/opencv/cvat/assets/40690378/f6e97db5-84cb-4c1e-9b2a-82eca633a79c">

Resolved #287
Resolved #3680
Resolved #5580

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have added a description of my changes into the
[CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md)
file
- [ ] I have updated the documentation accordingly
- [ ] I have added tests to cover my changes
- [x] I have linked related issues (see [GitHub docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [x] I have increased versions of npm packages if it is necessary

([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),

[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning),

[cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning)
and

[cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.
  • Loading branch information
bsekachev authored Jun 23, 2023
1 parent ec149df commit 307db39
Show file tree
Hide file tree
Showing 63 changed files with 2,862 additions and 225 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## \[Unreleased]
### Added
- TDB
- Now CVAT supports project/task markdown description with additional assets
(png, jpeg, gif, webp images and pdf files) (<https://github.com/opencv/cvat/pull/6191>)

### Changed
- TDB
Expand Down
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "9.1.3",
"version": "9.2.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
"scripts": {
Expand Down
11 changes: 11 additions & 0 deletions cvat-core/src/api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import Project from './project';
import CloudStorage from './cloud-storage';
import Organization from './organization';
import Webhook from './webhook';
import { ArgumentError } from './exceptions';
import { SerializedAsset } from './server-response-types';

export default function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
Expand Down Expand Up @@ -133,6 +135,15 @@ export default function implementAPI(cvat) {
return result;
};

cvat.assets.create.implementation = async (file: File, guideId: number): Promise<SerializedAsset> => {
if (!(file instanceof File)) {
throw new ArgumentError('Assets expect a file');
}

const result = await serverProxy.assets.create(file, guideId);
return result;
};

cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
Expand Down
13 changes: 8 additions & 5 deletions cvat-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
//
// SPDX-License-Identifier: MIT

/**
* External API which should be used by for development
* @module API
*/

import PluginRegistry from './plugins';
import loggerStorage from './logger-storage';
import { EventLogger } from './log';
Expand All @@ -25,6 +20,7 @@ import { FrameData } from './frames';
import CloudStorage from './cloud-storage';
import Organization from './organization';
import Webhook from './webhook';
import AnnotationGuide from './guide';

import * as enums from './enums';

Expand Down Expand Up @@ -147,6 +143,12 @@ function build() {
return result;
},
},
assets: {
async create(file: File, guideId: number) {
const result = await PluginRegistry.apiWrapper(cvat.assets.create, file, guideId);
return result;
},
},
jobs: {
async get(filter = {}) {
const result = await PluginRegistry.apiWrapper(cvat.jobs.get, filter);
Expand Down Expand Up @@ -287,6 +289,7 @@ function build() {
CloudStorage,
Organization,
Webhook,
AnnotationGuide,
},
};

Expand Down
92 changes: 92 additions & 0 deletions cvat-core/src/guide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import { SerializedGuide } from './server-response-types';
import { ArgumentError, DataError } from './exceptions';
import PluginRegistry from './plugins';
import serverProxy from './server-proxy';

class AnnotationGuide {
#id: AnnotationGuide['id'];
#taskId: AnnotationGuide['taskId'];
#projectId: AnnotationGuide['projectId'];
#createdDate?: AnnotationGuide['createdDate'];
#updatedDate?: AnnotationGuide['updatedDate'];
#markdown: AnnotationGuide['markdown'];

constructor(initialData: Partial<SerializedGuide>) {
this.#id = initialData.id;
this.#taskId = initialData.task_id || null;
this.#projectId = initialData.project_id || null;
this.#createdDate = initialData.created_date;
this.#updatedDate = initialData.updated_date;
this.#markdown = initialData.markdown || '';
}

public get id(): number | undefined {
return this.#id;
}

public get taskId(): number | null {
return this.#taskId;
}

public get projectId(): number | null {
return this.#projectId;
}

public get createdDate(): string | undefined {
return this.#createdDate;
}

public get updatedDate(): string | undefined {
return this.#updatedDate;
}

public get markdown(): string {
return this.#markdown;
}

public set markdown(value: string) {
if (typeof value !== 'string') {
throw new ArgumentError(`Markdown value must be a string, ${typeof value} received`);
}
this.#markdown = value;
}

async save(): Promise<AnnotationGuide> {
const result = await PluginRegistry.apiWrapper.call(this, AnnotationGuide.prototype.save);
return result;
}
}

Object.defineProperties(AnnotationGuide.prototype.save, {
implementation: {
writable: false,
enumerable: false,
value: async function implementation(this: AnnotationGuide) {
if (Number.isInteger(this.id)) {
const result = await serverProxy.guides.update(this.id, { markdown: this.markdown });
return new AnnotationGuide(result);
}

if (this.projectId === null && this.taskId === null) {
throw new DataError('One of projectId or taskId must be specified for a guide');
}

if (this.projectId !== null && this.taskId !== null) {
throw new DataError('Both projectId and taskId must not be presented for a guide');
}

const result = await serverProxy.guides.create({
task_id: this.taskId,
project_id: this.projectId,
markdown: this.markdown,
});
return new AnnotationGuide(result);
},
},
});

export default AnnotationGuide;
10 changes: 10 additions & 0 deletions cvat-core/src/project-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Project from './project';
import { exportDataset, importDataset } from './annotations';
import { SerializedLabel } from './server-response-types';
import { Label } from './labels';
import AnnotationGuide from './guide';

export default function implementProject(projectClass) {
projectClass.prototype.save.implementation = async function () {
Expand Down Expand Up @@ -125,5 +126,14 @@ export default function implementProject(projectClass) {
return result;
};

projectClass.prototype.guide.implementation = async function guide() {
if (this.guideId === null) {
return null;
}

const result = await serverProxy.guides.get(this.guideId);
return new AnnotationGuide(result);
};

return projectClass;
}
11 changes: 11 additions & 0 deletions cvat-core/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { ArgumentError } from './exceptions';
import { Label } from './labels';
import User from './user';
import { FieldUpdateTrigger } from './common';
import AnnotationGuide from './guide';

export default class Project {
public readonly id: number;
public name: string;
public assignee: User;
public bugTracker: string;
public readonly status: ProjectStatus;
public readonly guideId: number | null;
public readonly organization: string | null;
public readonly owner: User;
public readonly createdDate: string;
Expand All @@ -39,6 +41,7 @@ export default class Project {
name: undefined,
status: undefined,
assignee: undefined,
guide_id: undefined,
organization: undefined,
owner: undefined,
bug_tracker: undefined,
Expand Down Expand Up @@ -98,6 +101,9 @@ export default class Project {
owner: {
get: () => data.owner,
},
guideId: {
get: () => data.guide_id,
},
organization: {
get: () => data.organization,
},
Expand Down Expand Up @@ -230,6 +236,11 @@ export default class Project {
const result = await PluginRegistry.apiWrapper.call(this, Project.restore, storage, file);
return result;
}

async guide(): Promise<AnnotationGuide | null> {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.guide);
return result;
}
}

Object.defineProperties(
Expand Down
63 changes: 62 additions & 1 deletion cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
SerializedLabel, SerializedAnnotationFormats, ProjectsFilter,
SerializedProject, SerializedTask, TasksFilter, SerializedUser,
SerializedAbout, SerializedRemoteFile, SerializedUserAgreement,
SerializedRegister, JobsFilter, SerializedJob,
SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset,
} from 'server-response-types';
import { Storage } from './storage';
import { StorageLocation, WebhookSourceType } from './enums';
Expand Down Expand Up @@ -2167,6 +2167,57 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise<string[]>
}
}

async function getGuide(id: number): Promise<SerializedGuide> {
const { backendAPI } = config;

try {
const response = await Axios.get(`${backendAPI}/guides/${id}`);
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}

async function createGuide(data: Partial<SerializedGuide>): Promise<SerializedGuide> {
const { backendAPI } = config;

try {
const response = await Axios.post(`${backendAPI}/guides`, data);
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}

async function updateGuide(id: number, data: Partial<SerializedGuide>): Promise<SerializedGuide> {
const { backendAPI } = config;

try {
const response = await Axios.patch(`${backendAPI}/guides/${id}`, data);
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}

async function createAsset(file: File, guideId: number): Promise<SerializedAsset> {
const { backendAPI } = config;
const form = new FormData();
form.append('file', file);
form.append('guide_id', guideId);

try {
const response = await Axios.post(`${backendAPI}/assets`, form, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}

export default Object.freeze({
server: Object.freeze({
setAuthData,
Expand Down Expand Up @@ -2310,4 +2361,14 @@ export default Object.freeze({
ping: pingWebhook,
events: receiveWebhookEvents,
}),

guides: Object.freeze({
get: getGuide,
create: createGuide,
update: updateGuide,
}),

assets: Object.freeze({
create: createAsset,
}),
});
25 changes: 23 additions & 2 deletions cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface SerializedProject {
dimension: DimensionType;
name: string;
organization: number | null;
guide_id: number | null;
owner: SerializedUser;
source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null };
target_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null };
Expand Down Expand Up @@ -94,6 +95,7 @@ export interface SerializedTask {
overlap: number | null;
owner: SerializedUser;
project_id: number | null;
guide_id: number | null;
segment_size: number;
size: number;
source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null };
Expand All @@ -115,10 +117,11 @@ export interface SerializedJob {
labels: { count: number; url: string };
mode: TaskMode;
project_id: number | null;
guide_id: number | null;
stage: JobStage;
state: JobState;
startFrame: number;
stopFrame: number;
start_frame: number;
stop_frame: number;
task_id: number;
updated_date: string;
url: string;
Expand Down Expand Up @@ -174,3 +177,21 @@ export interface SerializedRegister {
last_name: string;
username: string;
}

export interface SerializedGuide {
id?: number;
task_id: number | null;
project_id: number | null;
owner: SerializedUser;
created_date: string;
updated_date: string;
markdown: string;
}

export interface SerializedAsset {
uuid?: string;
guide?: number;
filename: string;
created_date: string;
owner: SerializedUser;
}
Loading

0 comments on commit 307db39

Please sign in to comment.