Skip to content

Commit

Permalink
Open resource links from any organization/sandbox (#5892)
Browse files Browse the repository at this point in the history
  • Loading branch information
klakhov authored May 26, 2023
1 parent 23e5998 commit ed3dbe8
Show file tree
Hide file tree
Showing 42 changed files with 738 additions and 1,860 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ without use_cache option (<https://github.com/opencv/cvat/pull/6074>)
- Support task creation with cloud storage data and without use_cache option (<https://github.com/opencv/cvat/pull/6074>)

### Changed
- Resource links are opened from any organization/sandbox if available for user (<https://github.com/opencv/cvat/pull/5892>)
- Cloud storage manifest file is optional (<https://github.com/opencv/cvat/pull/6074>)
- Updated Django to 4.2.x version (<https://github.com/opencv/cvat/pull/6122>)
- Some Nuclio functions' names were changed to follow a common convention:
Expand Down
10 changes: 8 additions & 2 deletions cvat-core/src/api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,17 @@ export default function implementAPI(cvat) {

cvat.organizations.activate.implementation = (organization) => {
checkObjectType('organization', organization, null, Organization);
config.organizationID = organization.slug;
config.organization = {
organizationID: organization.id,
organizationSlug: organization.slug,
};
};

cvat.organizations.deactivate.implementation = async () => {
config.organizationID = null;
config.organization = {
organizationID: null,
organizationSlug: null,
};
};

cvat.webhooks.get.implementation = async (filter) => {
Expand Down
6 changes: 6 additions & 0 deletions cvat-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@ function build() {
set removeUnderlyingMaskPixels(value: boolean) {
config.removeUnderlyingMaskPixels = value;
},
get onOrganizationChange(): (orgId: number) => void {
return config.onOrganizationChange;
},
set onOrganizationChange(value: (orgId: number) => void) {
config.onOrganizationChange = value;
},
},
client: {
version: `${pjson.version}`,
Expand Down
4 changes: 4 additions & 0 deletions cvat-core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ export class FieldUpdateTrigger {
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

export function isResourceURL(url: string): boolean {
return /\/([0-9]+)$/.test(url);
}
6 changes: 5 additions & 1 deletion cvat-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

const config = {
backendAPI: '/api',
organizationID: null,
organization: {
organizationID: null,
organizationSlug: null,
},
origin: '',
uploadChunkSize: 100,
removeUnderlyingMaskPixels: false,
onOrganizationChange: null,
};

export default config;
5 changes: 4 additions & 1 deletion cvat-core/src/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,10 @@ Object.defineProperties(Organization.prototype.remove, {
value: async function implementation() {
if (typeof this.id === 'number') {
await serverProxy.organizations.delete(this.id);
config.organizationID = null;
config.organization = {
organizationID: null,
organizationSlug: null,
};
}
},
},
Expand Down
19 changes: 17 additions & 2 deletions cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from 'server-response-types';
import { Storage } from './storage';
import { StorageLocation, WebhookSourceType } from './enums';
import { isEmail } from './common';
import { isEmail, isResourceURL } from './common';
import config from './config';
import DownloadWorker from './download.worker';
import { ServerError } from './exceptions';
Expand All @@ -32,7 +32,7 @@ type Params = {
};

function enableOrganization(): { org: string } {
return { org: config.organizationID || '' };
return { org: config.organization.organizationSlug || '' };
}

function configureStorage(storage: Storage, useDefaultLocation = false): Partial<Params> {
Expand Down Expand Up @@ -266,10 +266,25 @@ Axios.interceptors.request.use((reqConfig) => {
return reqConfig;
}

if (isResourceURL(reqConfig.url)) {
return reqConfig;
}

reqConfig.params = { ...organization, ...(reqConfig.params || {}) };
return reqConfig;
});

Axios.interceptors.response.use((response) => {
if (isResourceURL(response.config.url)) {
const newOrg = response.data.organization;
if (newOrg && config.organization.organizationID !== newOrg) {
config?.onOrganizationChange(newOrg);
}
}

return response;
});

let token = store.get('token');
if (token) {
Axios.defaults.headers.common.Authorization = `Token ${token}`;
Expand Down
3 changes: 2 additions & 1 deletion cvat-ui/src/components/cvat-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import EmailConfirmationPage from './email-confirmation-pages/email-confirmed';
import EmailVerificationSentPage from './email-confirmation-pages/email-verification-sent';
import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation';
import CreateModelPage from './create-model-page/create-model-page';
import OrganizationWatcher from './watchers/organization-watcher';

interface CVATAppProps {
loadFormats: () => void;
Expand Down Expand Up @@ -499,14 +500,14 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
/>
</Switch>
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<ExportDatasetModal />
<ExportBackupModal />
<ImportDatasetModal />
<ImportBackupModal />
{ loggedInModals.map((Component, idx) => (
<Component key={idx} targetProps={this.props} targetState={this.state} />
))}
<OrganizationWatcher />
{/* eslint-disable-next-line */}
<a id='downloadAnchor' target='_blank' style={{ display: 'none' }} download />
</Layout.Content>
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/src/components/task-page/task-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function TaskPageComponent(): JSX.Element {
}).catch((error: Error) => {
if (mounted.current) {
notification.error({
message: 'Could not receive the requested project from the server',
message: 'Could not receive the requested task from the server',
description: error.toString(),
});
}
Expand Down
28 changes: 28 additions & 0 deletions cvat-ui/src/components/watchers/organization-watcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import { getCore } from 'cvat-core-wrapper';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { CombinedState } from 'reducers';

const core = getCore();

function OrganizationWatcher(): JSX.Element {
const organizationList = useSelector((state: CombinedState) => state.organizations.list);

useEffect(() => {
core.config.onOrganizationChange = (newOrgId: number) => {
const newOrganization = organizationList.find((org) => org.id === newOrgId);
if (newOrganization) {
localStorage.setItem('currentOrganization', newOrganization.slug);
window.location.reload();
}
};
}, []);

return <></>;
}

export default React.memo(OrganizationWatcher);
16 changes: 10 additions & 6 deletions cvat/apps/engine/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,8 @@ def get_task_id(self):
task = self.segment.task
return task.id if task else None

def get_organization_id(self):
@property
def organization_id(self):
return self.segment.task.organization_id

def get_organization_slug(self):
Expand Down Expand Up @@ -542,7 +543,8 @@ def create(cls, **kwargs):
except IntegrityError:
raise InvalidLabel("All label names must be unique")

def get_organization_id(self):
@property
def organization_id(self):
if self.project is not None:
return self.project.organization.id
if self.task is not None:
Expand Down Expand Up @@ -720,8 +722,9 @@ class Issue(models.Model):
def get_project_id(self):
return self.job.get_project_id()

def get_organization_id(self):
return self.job.get_organization_id()
@property
def organization_id(self):
return self.job.organization_id

def get_organization_slug(self):
return self.job.get_organization_slug()
Expand All @@ -743,8 +746,9 @@ class Comment(models.Model):
def get_project_id(self):
return self.issue.get_project_id()

def get_organization_id(self):
return self.issue.get_organization_id()
@property
def organization_id(self):
return self.issue.organization_id

def get_organization_slug(self):
return self.issue.get_organization_slug()
Expand Down
24 changes: 24 additions & 0 deletions cvat/apps/engine/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import textwrap
from typing import Type
from rest_framework import serializers
from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.extensions import OpenApiSerializerExtension
from drf_spectacular.plumbing import force_instance, build_basic_type
from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -228,4 +229,27 @@ class CloudStorageReadSerializerExtension(_CloudStorageSerializerExtension):
class CloudStorageWriteSerializerExtension(_CloudStorageSerializerExtension):
target_class = 'cvat.apps.engine.serializers.CloudStorageWriteSerializer'

ORGANIZATION_OPEN_API_PARAMETERS = [
OpenApiParameter(
name='org',
type=str,
required=False,
location=OpenApiParameter.QUERY,
description="Organization unique slug",
),
OpenApiParameter(
name='org_id',
type=int,
required=False,
location=OpenApiParameter.QUERY,
description="Organization identifier",
),
OpenApiParameter(
name='X-Organization',
type=str,
required=False,
location=OpenApiParameter.HEADER
),
]

__all__ = [] # No public symbols here
3 changes: 2 additions & 1 deletion cvat/apps/engine/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ class JobReadSerializer(serializers.ModelSerializer):
assignee = BasicUserSerializer(allow_null=True, read_only=True)
dimension = serializers.CharField(max_length=2, source='segment.task.dimension', read_only=True)
data_chunk_size = serializers.ReadOnlyField(source='segment.task.data.chunk_size')
organization = serializers.ReadOnlyField(source='segment.task.organization.id', allow_null=True)
data_compressed_chunk_type = serializers.ReadOnlyField(source='segment.task.data.compressed_chunk_type')
mode = serializers.ReadOnlyField(source='segment.task.mode')
bug_tracker = serializers.CharField(max_length=2000, source='get_bug_tracker',
Expand All @@ -560,7 +561,7 @@ class Meta:
model = models.Job
fields = ('url', 'id', 'task_id', 'project_id', 'assignee',
'dimension', 'bug_tracker', 'status', 'stage', 'state', 'mode',
'start_frame', 'stop_frame', 'data_chunk_size', 'data_compressed_chunk_type',
'start_frame', 'stop_frame', 'data_chunk_size', 'organization', 'data_compressed_chunk_type',
'updated_date', 'issues', 'labels'
)
read_only_fields = fields
Expand Down
2 changes: 1 addition & 1 deletion cvat/apps/engine/view_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def list_action(serializer_class: Type[Serializer], **kwargs):
def get_cloud_storage_for_import_or_export(
storage_id: int, *, request, is_default: bool = False
) -> CloudStorageModel:
perm = CloudStoragePermission.create_scope_view(request=request, storage_id=storage_id)
perm = CloudStoragePermission.create_scope_view(None, storage_id=storage_id, request=request)
result = perm.check_access()
if not result.allow:
if is_default:
Expand Down
9 changes: 9 additions & 0 deletions cvat/apps/engine/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
CloudStorageReadSerializer, DatasetFileSerializer,
ProjectFileSerializer, TaskFileSerializer, CloudStorageContentSerializer)
from cvat.apps.engine.view_utils import get_cloud_storage_for_import_or_export
from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS

from utils.dataset_manifest import ImageManifestManager
from cvat.apps.engine.utils import (
Expand Down Expand Up @@ -204,6 +205,7 @@ def plugins(request):
create=extend_schema(
summary='Method creates a new project',
request=ProjectWriteSerializer,
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
responses={
'201': ProjectReadSerializer, # check ProjectWriteSerializer.to_representation
}),
Expand Down Expand Up @@ -484,6 +486,7 @@ def export_backup(self, request, pk=None):

@extend_schema(summary='Methods create a project from a backup',
parameters=[
*ORGANIZATION_OPEN_API_PARAMETERS,
OpenApiParameter('location', description='Where to import the backup file from',
location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False,
enum=Location.list(), default=Location.LOCAL),
Expand Down Expand Up @@ -637,6 +640,7 @@ def __call__(self, request, start, stop, db_data):
create=extend_schema(
summary='Method creates a new task in a database without any attached images and videos',
request=TaskWriteSerializer,
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
responses={
'201': TaskReadSerializer, # check TaskWriteSerializer.to_representation
}),
Expand Down Expand Up @@ -715,6 +719,7 @@ def get_queryset(self):

@extend_schema(summary='Method recreates a task from an attached task backup file',
parameters=[
*ORGANIZATION_OPEN_API_PARAMETERS,
OpenApiParameter('location', description='Where to import the backup file from',
location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False,
enum=Location.list(), default=Location.LOCAL),
Expand Down Expand Up @@ -1648,6 +1653,7 @@ def preview(self, request, pk):
create=extend_schema(
summary='Method creates an issue',
request=IssueWriteSerializer,
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
responses={
'201': IssueReadSerializer, # check IssueWriteSerializer.to_representation
}),
Expand Down Expand Up @@ -1718,6 +1724,7 @@ def perform_create(self, serializer, **kwargs):
create=extend_schema(
summary='Method creates a comment',
request=CommentWriteSerializer,
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
responses={
'201': CommentReadSerializer, # check CommentWriteSerializer.to_representation
}),
Expand Down Expand Up @@ -1784,6 +1791,7 @@ def perform_create(self, serializer, **kwargs):
description='A simple equality filter for task id'),
OpenApiParameter('project_id', type=OpenApiTypes.INT,
description='A simple equality filter for project id'),
*ORGANIZATION_OPEN_API_PARAMETERS
],
responses={
'200': LabelSerializer(many=True),
Expand Down Expand Up @@ -2032,6 +2040,7 @@ def self(self, request):
create=extend_schema(
summary='Method creates a cloud storage with a specified characteristics',
request=CloudStorageWriteSerializer,
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
responses={
'201': CloudStorageReadSerializer, # check CloudStorageWriteSerializer.to_representation
})
Expand Down
5 changes: 1 addition & 4 deletions cvat/apps/events/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ def organization_id(instance):
return instance.id

try:
oid = getattr(instance, "organization_id", None)
if oid is None:
return instance.get_organization_id()
return oid
return getattr(instance, "organization_id", None)
except Exception:
return None

Expand Down
2 changes: 1 addition & 1 deletion cvat/apps/events/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ClientEventsSerializer(serializers.Serializer):

def to_internal_value(self, data):
request = self.context.get("request")
org = request.iam_context['organization']
org = request.iam_context["organization"]
org_id = getattr(org, "id", None)
org_slug = getattr(org, "slug", None)

Expand Down
2 changes: 2 additions & 0 deletions cvat/apps/events/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from cvat.apps.iam.permissions import EventsPermission
from cvat.apps.events.serializers import ClientEventsSerializer
from cvat.apps.engine.log import vlogger
from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS
from .export import export

class EventsViewSet(viewsets.ViewSet):
Expand All @@ -21,6 +22,7 @@ class EventsViewSet(viewsets.ViewSet):
@extend_schema(summary='Method saves logs from a client on the server',
methods=['POST'],
description='Sends logs to the Clickhouse if it is connected',
parameters=ORGANIZATION_OPEN_API_PARAMETERS,
request=ClientEventsSerializer(),
responses={
'201': ClientEventsSerializer(),
Expand Down
Loading

0 comments on commit ed3dbe8

Please sign in to comment.