From a902db32be8ca7173d6e8aa59734b177a1ad210c Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 12:13:51 +0300 Subject: [PATCH 01/64] draft invitation --- .../invitation/invitation_message.html | 21 +++++++++++++++++++ .../invitation/invitation_message.txt | 15 +++++++++++++ .../invitation/invitation_subject.txt | 4 ++++ 3 files changed, 40 insertions(+) create mode 100644 cvat/apps/organizations/templates/invitation/invitation_message.html create mode 100644 cvat/apps/organizations/templates/invitation/invitation_message.txt create mode 100644 cvat/apps/organizations/templates/invitation/invitation_subject.txt diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html new file mode 100644 index 00000000000..2ce5f6060dd --- /dev/null +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -0,0 +1,21 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %} +

+ You're receiving this email because you requested a password reset for your user account at {{ site_name }}. +

+{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{% block reset_link %} +

+{{ protocol }}://{{ domain }}/auth/password/reset/confirm +

+{% endblock %} + +{% trans "Thanks for using our site!" %} + +

+{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +

+ +{% endautoescape %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.txt b/cvat/apps/organizations/templates/invitation/invitation_message.txt new file mode 100644 index 00000000000..bbe4b3af4e1 --- /dev/null +++ b/cvat/apps/organizations/templates/invitation/invitation_message.txt @@ -0,0 +1,15 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %} +You're receiving this email because you requested a password reset for your user account at {{ site_name }}. +{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}/auth/password/reset/confirm +{% endblock %} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_subject.txt b/cvat/apps/organizations/templates/invitation/invitation_subject.txt new file mode 100644 index 00000000000..6840c40b75e --- /dev/null +++ b/cvat/apps/organizations/templates/invitation/invitation_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Password Reset E-mail{% endblocktrans %} +{% endautoescape %} From 6c8e5fbd5406a76516028cd02303e641d1ef5364 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 12:14:21 +0300 Subject: [PATCH 02/64] sendmail on invite --- cvat/apps/organizations/models.py | 20 +++++++++++++++++--- cvat/apps/organizations/serializers.py | 20 +++++++++++++------- cvat/apps/organizations/views.py | 3 ++- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index d4b9f3a27e4..06823c0a139 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -5,6 +5,9 @@ from distutils.util import strtobool from django.conf import settings +from allauth.account.adapter import get_adapter +from django.contrib.sites.shortcuts import get_current_site + from django.db import models from django.contrib.auth import get_user_model from django.utils import timezone @@ -60,10 +63,21 @@ class Invitation(models.Model): def organization_id(self): return self.membership.organization_id - def send(self): - if not strtobool(settings.ORG_INVITATION_CONFIRM): - self.accept(self.created_date) + def send(self, request): + # if not strtobool(settings.ORG_INVITATION_CONFIRM): + # self.accept(self.created_date) + target_email = self.membership.user.email + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + context = { + 'email': target_email, + 'domain': domain, + 'site_name': site_name, + 'protocol': 'http', + } + get_adapter(request).send_mail('invitation/invitation', target_email, context) # TODO: use email backend to send invitations as well def accept(self, date=None): diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 906f894e023..be68755b561 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -6,6 +6,8 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers +from django.contrib.auth.models import User +from django.utils.crypto import get_random_string from .models import Invitation, Membership, Organization from cvat.apps.engine.serializers import BasicUserSerializer @@ -81,12 +83,15 @@ def create(self, validated_data): email__iexact=membership_data['user']['email']) del membership_data['user'] except ObjectDoesNotExist: - raise serializers.ValidationError(f'You cannot invite an user ' - f'with {membership_data["user"]["email"]} email. It is not ' - f'a valid email in the system.') - + user_email = membership_data['user']['email'] + username = user_email.split("@")[0] + user = User.objects.create_user(username=username, password=get_random_string(length=32), + email=user_email) + user.set_unusable_password() + user.save() + print('Created user', user) membership, created = Membership.objects.get_or_create( - defaults=membership_data, + # defaults=membership_data, user=user, organization=organization) if not created: raise serializers.ValidationError('The user is a member of ' @@ -99,9 +104,10 @@ def create(self, validated_data): def update(self, instance, validated_data): return super().update(instance, {}) - def save(self, **kwargs): + def save(self, request, **kwargs): + ## TODO move/remove request to/from kwarg invitation = super().save(**kwargs) - invitation.send() + invitation.send(request) return invitation diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index a8576273b9c..33cd6a83f72 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -205,7 +205,8 @@ def perform_create(self, serializer): serializer.save( owner=self.request.user, key=get_random_string(length=64), - organization=self.request.iam_context['organization'] + organization=self.request.iam_context['organization'], + request=self.request ) def perform_update(self, serializer): From 55c47e7a9615d6fb0329435a08a75d5e8c1b3fa8 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 12:20:43 +0300 Subject: [PATCH 03/64] updated message --- cvat/apps/organizations/models.py | 5 +++-- .../templates/invitation/invitation_message.html | 6 +++--- .../templates/invitation/invitation_message.txt | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 06823c0a139..d04aa6a5caf 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -72,13 +72,14 @@ def send(self, request): domain = current_site.domain context = { 'email': target_email, + 'invitation_key': self.key, 'domain': domain, 'site_name': site_name, - 'protocol': 'http', + 'protocol': 'http', ## TODO add https } get_adapter(request).send_mail('invitation/invitation', target_email, context) - # TODO: use email backend to send invitations as well + # TODO: auto accept for existing users def accept(self, date=None): if not self.membership.is_active: diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index 2ce5f6060dd..35126656a5c 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -1,14 +1,14 @@ {% load i18n %}{% autoescape off %} {% blocktrans %}

- You're receiving this email because you requested a password reset for your user account at {{ site_name }}. + You're receiving this email because you've been invited to organization in CVAT at {{ site_name }}.

{% endblocktrans %} -{% trans "Please go to the following page and choose a new password:" %} +{% trans "Please go to the following page and finish creating your account to accept the invitation:" %} {% block reset_link %}

-{{ protocol }}://{{ domain }}/auth/password/reset/confirm +{{ protocol }}://{{ domain }}/auth/register?email={{ email }}&invitation_key={{ invitation_key }}

{% endblock %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.txt b/cvat/apps/organizations/templates/invitation/invitation_message.txt index bbe4b3af4e1..b326868ab48 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.txt +++ b/cvat/apps/organizations/templates/invitation/invitation_message.txt @@ -1,15 +1,15 @@ {% load i18n %}{% autoescape off %} {% blocktrans %} -You're receiving this email because you requested a password reset for your user account at {{ site_name }}. + You're receiving this email because you've been invited to organization in CVAT at {{ site_name }}. {% endblocktrans %} -{% trans "Please go to the following page and choose a new password:" %} +{% trans "Please go to the following page and finish creating your account to accept the invitation:" %} {% block reset_link %} -{{ protocol }}://{{ domain }}/auth/password/reset/confirm +{{ protocol }}://{{ domain }}/auth/register?email={{ email }}&invitation_key={{ invitation_key }} {% endblock %} {% trans "Thanks for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} -{% endautoescape %} +{% endautoescape %} \ No newline at end of file From 705f9f1afa561606a8009fa232f7c96e0ac3b361 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 12:44:57 +0300 Subject: [PATCH 04/64] added basic inv serializer, fixed membership creation --- cvat/apps/organizations/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index be68755b561..9a95bc78aa1 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -58,6 +58,11 @@ class Meta: fields = ['key', 'created_date', 'owner', 'role', 'user', 'organization'] read_only_fields = fields +class BasicInvitationSerializer(serializers.ModelSerializer): + class Meta: + model = Invitation + fields = ['key', 'created_date'] + read_only_fields = fields class InvitationWriteSerializer(serializers.ModelSerializer): role = serializers.ChoiceField(Membership.role.field.choices, @@ -89,9 +94,9 @@ def create(self, validated_data): email=user_email) user.set_unusable_password() user.save() - print('Created user', user) + del membership_data['user'] membership, created = Membership.objects.get_or_create( - # defaults=membership_data, + defaults=membership_data, user=user, organization=organization) if not created: raise serializers.ValidationError('The user is a member of ' @@ -113,6 +118,7 @@ def save(self, request, **kwargs): class MembershipReadSerializer(serializers.ModelSerializer): user = BasicUserSerializer() + invitation = BasicInvitationSerializer() class Meta: model = Membership fields = ['id', 'user', 'organization', 'is_active', 'joined_date', 'role', From 5b519dbb8a0241c2bde97cefdf1ee6c4ada75bc2 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 12:50:37 +0300 Subject: [PATCH 05/64] removed lots of requests for invitations --- cvat-core/src/organization.ts | 11 ----------- cvat/apps/organizations/serializers.py | 3 ++- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index f9219a127d3..921c0a4e268 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -236,18 +236,7 @@ Object.defineProperties(Organization.prototype.members, { const result = await serverProxy.organizations.members(orgSlug, page, pageSize); await Promise.all( result.results.map((membership) => { - const { invitation } = membership; membership.user = new User(membership.user); - if (invitation) { - return serverProxy.organizations - .invitation(invitation) - .then((invitationData) => { - membership.invitation = invitationData; - }) - .catch(() => { - membership.invitation = null; - }); - } return Promise.resolve(); }), diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 9a95bc78aa1..28ecb85c37c 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -59,9 +59,10 @@ class Meta: read_only_fields = fields class BasicInvitationSerializer(serializers.ModelSerializer): + owner = BasicUserSerializer(allow_null=True) class Meta: model = Invitation - fields = ['key', 'created_date'] + fields = ['key', 'created_date', 'owner'] read_only_fields = fields class InvitationWriteSerializer(serializers.ModelSerializer): From 5a3bb433bf97e54dad0799a05e8a68d50830e7f9 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 13:38:49 +0300 Subject: [PATCH 06/64] update invitaiton link --- .../organizations/templates/invitation/invitation_message.html | 2 +- .../organizations/templates/invitation/invitation_message.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index 35126656a5c..768011a8286 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -8,7 +8,7 @@ {% trans "Please go to the following page and finish creating your account to accept the invitation:" %} {% block reset_link %}

-{{ protocol }}://{{ domain }}/auth/register?email={{ email }}&invitation_key={{ invitation_key }} +{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&invitation_key={{ invitation_key }}

{% endblock %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.txt b/cvat/apps/organizations/templates/invitation/invitation_message.txt index b326868ab48..59220a1cca0 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.txt +++ b/cvat/apps/organizations/templates/invitation/invitation_message.txt @@ -5,7 +5,7 @@ {% trans "Please go to the following page and finish creating your account to accept the invitation:" %} {% block reset_link %} -{{ protocol }}://{{ domain }}/auth/register?email={{ email }}&invitation_key={{ invitation_key }} +{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&key={{ invitation_key }} {% endblock %} {% trans "Thanks for using our site!" %} From ebd98b999b2d5d4c791f91eaa8f5712266548f2b Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 13:40:36 +0300 Subject: [PATCH 07/64] added register from invitation page --- .../accept-invitation-page.tsx | 37 +++++++++++++++++++ .../accept-invitation-page/styles.scss | 3 ++ cvat-ui/src/components/cvat-app.tsx | 3 ++ 3 files changed, 43 insertions(+) create mode 100644 cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx create mode 100644 cvat-ui/src/components/accept-invitation-page/styles.scss diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx new file mode 100644 index 00000000000..239e8a2c17c --- /dev/null +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -0,0 +1,37 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import RegisterPageComponent from 'components/register-page/register-page'; +import { CombinedState } from 'reducers'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router'; + +interface InvitationParams { + email: string; + key: string; +} + +function AcceptInvitationPage(): JSX.Element { + const userAgreements = useSelector((state: CombinedState) => state.userAgreements.list); + const userAgreementsFetching = useSelector((state: CombinedState) => state.userAgreements.fetching); + const authFetching = useSelector((state: CombinedState) => state.auth.fetching); + + const invitationParams = useParams(); + console.log(invitationParams); + const onRegister = () => { + console.log('register'); + }; + + return ( + + ); +} + +export default React.memo(AcceptInvitationPage); diff --git a/cvat-ui/src/components/accept-invitation-page/styles.scss b/cvat-ui/src/components/accept-invitation-page/styles.scss new file mode 100644 index 00000000000..2347fbdcaf7 --- /dev/null +++ b/cvat-ui/src/components/accept-invitation-page/styles.scss @@ -0,0 +1,3 @@ +.test { + color: red; +} diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 81d39b4ac68..ed747281b7c 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -58,6 +58,8 @@ import UpdateWebhookPage from 'components/setup-webhook-pages/update-webhook-pag import GuidePage from 'components/md-guide/guide-page'; +import AcceptInvitationPage from 'components/accept-invitation-page/accept-invitation-page'; + import AnnotationPageContainer from 'containers/annotation-page/annotation-page'; import { getCore } from 'cvat-core-wrapper'; import { ErrorState, NotificationsState, PluginsState } from 'reducers'; @@ -514,6 +516,7 @@ class CVATApplication extends React.PureComponent + From 464b814966ae4edc731100bc42445c1a90b213a0 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 16:35:18 +0300 Subject: [PATCH 08/64] added accept invitation form --- cvat-core/src/api-implementation.ts | 22 +++++++++++ cvat-core/src/api.ts | 13 +++++++ cvat-core/src/server-proxy.ts | 29 +++++++++++++++ cvat-ui/src/actions/auth-actions.ts | 35 ++++++++++++++++++ .../accept-invitation-page.tsx | 37 ++++++++++++++----- cvat-ui/src/components/cvat-app.tsx | 2 +- .../register-page/register-form.tsx | 26 ++++++++++--- .../register-page/register-page.tsx | 8 +++- .../signing-common/cvat-signing-input.tsx | 9 +++-- .../src/components/signing-common/styles.scss | 17 +++++---- 10 files changed, 169 insertions(+), 29 deletions(-) diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 56e8c1f6970..117ec0178b5 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -106,6 +106,28 @@ export default function implementAPI(cvat) { await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token); }; + cvat.server.acceptInvitation.implementation = async ( + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ) => { + const orgSlug = await serverProxy.server.acceptInvitation( + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ); + + return orgSlug; + }; + cvat.server.authorized.implementation = async () => { const result = await serverProxy.server.authorized(); return result; diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index d53b0fc8884..f1b63c0a4dd 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -96,6 +96,19 @@ function build() { ); return result; }, + async acceptInvitation(username, firstName, lastName, email, password, userConfirmations, key) { + const result = await PluginRegistry.apiWrapper( + cvat.server.acceptInvitation, + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ); + return result; + }, async authorized() { const result = await PluginRegistry.apiWrapper(cvat.server.authorized); return result; diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 978b2c4bf61..e0457814ce2 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -454,6 +454,34 @@ async function resetPassword(newPassword1: string, newPassword2: string, uid: st } } +async function acceptInvitation( + username: string, + firstName: string, + lastName: string, + email: string, + password: string, + confirmations: Record, + key: string, +): Promise { + let response = null; + try { + response = await Axios.post(`${config.backendAPI}/invitations/accept`, { + username, + first_name: firstName, + last_name: lastName, + email, + password1: password, + password2: password, + confirmations, + key, + }); + } catch (errorData) { + throw generateError(errorData); + } + + return response.data; +} + async function getSelf(): Promise { const { backendAPI } = config; @@ -2365,6 +2393,7 @@ export default Object.freeze({ request: serverRequest, userAgreements, installedApps, + acceptInvitation, }), projects: Object.freeze({ diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 1ce02605f33..5b6b5c158d9 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -36,6 +36,9 @@ export enum AuthActionTypes { LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', + ACCEPT_INVITATION = 'ACCEPT_INVITATION', + ACCEPT_INVITATION_SUCCESS = 'ACCEPT_INVITATION_SUCCESS', + ACCEPT_INVITATION_FAILED = 'ACCEPT_INVITATION_FAILED', } export const authActions = { @@ -73,6 +76,10 @@ export const authActions = { }) ), loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), + acceptInvitation: () => createAction(AuthActionTypes.ACCEPT_INVITATION), + // TODO: successs must return organization + acceptInvitationSuccess: (user: any) => createAction(AuthActionTypes.ACCEPT_INVITATION_SUCCESS, { user }), + acceptInvitationFailed: (error: any) => createAction(AuthActionTypes.ACCEPT_INVITATION_FAILED, { error }), }; export type AuthActions = ActionUnion; @@ -200,3 +207,31 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { dispatch(authActions.loadServerAuthActionsFailed(error)); } }; + +export const acceptInvitationAsync = ( + username: string, + firstName: string, + lastName: string, + email: string, + password: string, + confirmations: UserConfirmation[], + key: string, +): ThunkAction => async (dispatch) => { + dispatch(authActions.acceptInvitation()); + + try { + const orgSlug = await cvat.server.acceptInvitation( + username, + firstName, + lastName, + email, + password, + confirmations, + key, + ); + + dispatch(authActions.acceptInvitationSuccess(orgSlug)); + } catch (error) { + dispatch(authActions.acceptInvitationSuccess(error)); + } +}; diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index 239e8a2c17c..d8b83295843 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -3,33 +3,50 @@ // SPDX-License-Identifier: MIT import './styles.scss'; -import React from 'react'; +import React, { useCallback } from 'react'; import RegisterPageComponent from 'components/register-page/register-page'; import { CombinedState } from 'reducers'; -import { useSelector } from 'react-redux'; -import { useParams } from 'react-router'; +import { useSelector, useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; +import { acceptInvitationAsync } from 'actions/auth-actions'; +import { RegisterData } from 'components/register-page/register-form'; interface InvitationParams { - email: string; - key: string; + email: string | null; + key: string | null; } function AcceptInvitationPage(): JSX.Element { const userAgreements = useSelector((state: CombinedState) => state.userAgreements.list); const userAgreementsFetching = useSelector((state: CombinedState) => state.userAgreements.fetching); const authFetching = useSelector((state: CombinedState) => state.auth.fetching); - - const invitationParams = useParams(); - console.log(invitationParams); - const onRegister = () => { - console.log('register'); + const history = useHistory(); + const dispatch = useDispatch(); + const queryParams = new URLSearchParams(history.location.search); + // TODO: add check for inv params + const invitationParams: InvitationParams = { + email: queryParams.get('email'), + key: queryParams.get('key'), }; + const onRegister: (args: RegisterData) => void = useCallback((args) => { + dispatch(acceptInvitationAsync( + args.username, + args.firstName, + args.lastName, + args.email, + args.password, + args.confirmations, + invitationParams.key, + )); + }, [dispatch]); return ( ); } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index ed747281b7c..569642d897d 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -515,8 +515,8 @@ class CVATApplication extends React.PureComponent - + diff --git a/cvat-ui/src/components/register-page/register-form.tsx b/cvat-ui/src/components/register-page/register-form.tsx index 70ee19231a2..7e76606c84e 100644 --- a/cvat-ui/src/components/register-page/register-form.tsx +++ b/cvat-ui/src/components/register-page/register-form.tsx @@ -34,6 +34,8 @@ export interface RegisterData { interface Props { fetching: boolean; userAgreements: UserAgreement[]; + predifinedEmail?: string; + disableNavigation?: boolean; onSubmit(registerData: RegisterData): void; } @@ -99,16 +101,25 @@ const validateAgreement: ((userAgreements: UserAgreement[]) => RuleRender) = ( }); function RegisterFormComponent(props: Props): JSX.Element { - const { fetching, onSubmit, userAgreements } = props; + const { + fetching, onSubmit, userAgreements, predifinedEmail, disableNavigation, + } = props; const [form] = Form.useForm(); + if (predifinedEmail) { + form.setFieldsValue({ email: predifinedEmail }); + } const [usernameEdited, setUsernameEdited] = useState(false); return (
- - - - - + { + !disableNavigation && ( + + + + + + ) + }
) => { @@ -121,6 +132,7 @@ function RegisterFormComponent(props: Props): JSX.Element { onSubmit({ ...(Object.fromEntries(rest) as any as RegisterData), + ...(predifinedEmail ? { email: predifinedEmail } : {}), confirmations, }); }} @@ -186,6 +198,8 @@ function RegisterFormComponent(props: Props): JSX.Element { id='email' autoComplete='email' placeholder='Email' + value={predifinedEmail} + disabled={!!predifinedEmail} onReset={() => form.setFieldsValue({ email: '', username: '' })} onChange={(event) => { const { value } = event.target; diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 32f9eb78350..88cb84c0038 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -23,10 +23,14 @@ interface RegisterPageComponentProps { password: string, confirmations: UserConfirmation[], ) => void; + predifinedEmail?: string; + disableNavigation?: boolean; } function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps): JSX.Element { - const { fetching, userAgreements, onRegister } = props; + const { + fetching, userAgreements, onRegister, predifinedEmail, disableNavigation, + } = props; return ( @@ -36,6 +40,8 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen { onRegister( registerData.username, diff --git a/cvat-ui/src/components/signing-common/cvat-signing-input.tsx b/cvat-ui/src/components/signing-common/cvat-signing-input.tsx index fe4b89159ea..2187d083dd2 100644 --- a/cvat-ui/src/components/signing-common/cvat-signing-input.tsx +++ b/cvat-ui/src/components/signing-common/cvat-signing-input.tsx @@ -13,6 +13,7 @@ interface SocialAccountLinkProps { placeholder: string; value?: string; type?: CVATInputType; + disabled?: boolean; onReset?: () => void; onChange?: (event: React.ChangeEvent) => void; } @@ -24,9 +25,9 @@ export enum CVATInputType { function CVATSigningInput(props: SocialAccountLinkProps): JSX.Element { const { - id, autoComplete, type, onReset, placeholder, value, onChange, + id, autoComplete, type, onReset, placeholder, value, onChange, disabled, } = props; - const [valueNonEmpty, setValueNonEmpty] = useState(false); + const [valueNonEmpty, setValueNonEmpty] = useState(!!disabled); useEffect((): void => { setValueNonEmpty(!!value); }, [value]); @@ -43,6 +44,7 @@ function CVATSigningInput(props: SocialAccountLinkProps): JSX.Element { /> ); } + return ( {placeholder}} id={id} - suffix={valueNonEmpty && ( + disabled={disabled} + suffix={valueNonEmpty && !disabled && ( { diff --git a/cvat-ui/src/components/signing-common/styles.scss b/cvat-ui/src/components/signing-common/styles.scss index 31302db6cdc..3e97789d60d 100644 --- a/cvat-ui/src/components/signing-common/styles.scss +++ b/cvat-ui/src/components/signing-common/styles.scss @@ -2,21 +2,18 @@ // // SPDX-License-Identifier: MIT -@import '../../styles.scss'; +@import '../../styles'; $heading-font: 'Sora', sans-serif; $signing-font: 'Roboto Flex', sans-serif; - $heading-color: white; $error-color: #ff4d4f; $action-button-color-1: black; $action-button-color-2: gray; $action-button-color-3: #d4d4d4; $placeholder-color: #c5bfbf; - $base-transition: all 0.8s ease; $input-transition: all 0.3s ease; - $social-google-background: #4286f5; .cvat-signing-layout { @@ -40,7 +37,7 @@ $social-google-background: #4286f5; .cvat-signing-header { background: transparent; position: fixed; - padding: $grid-unit-size*2 0 0 0; + padding: $grid-unit-size * 2 0 0 0; width: 100%; .cvat-logo-icon { @@ -52,7 +49,7 @@ $social-google-background: #4286f5; position: absolute; width: 100%; height: 100%; - min-width: $grid-unit-size*128; + min-width: $grid-unit-size * 128; } .cvat-credentials-link { @@ -125,6 +122,10 @@ $social-google-background: #4286f5; } } + .ant-input-affix-wrapper-disabled { + background: transparent; + } + .cvat-signing-input-not-empty { @extend .cvat-input-floating-label; } @@ -145,7 +146,7 @@ $social-google-background: #4286f5; } .cvat-credentials-navigation { - margin-bottom: $grid-unit-size*4; + margin-bottom: $grid-unit-size * 4; } .cvat-credentials-action-button { @@ -336,6 +337,6 @@ $social-google-background: #4286f5; .cvat-register-form { @extend .cvat-signing-form; - height: $grid-unit-size*76; + height: $grid-unit-size * 76; } } From 499c1874c9c170ea4dbd79a24cfc9ab4667411e8 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 26 Sep 2023 17:07:20 +0300 Subject: [PATCH 09/64] added mock server accept endpoint --- cvat-core/src/server-proxy.ts | 4 ++-- .../accept-invitation-page/accept-invitation-page.tsx | 4 ++-- cvat/apps/organizations/views.py | 10 +++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index e0457814ce2..f880f97a59f 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -464,8 +464,9 @@ async function acceptInvitation( key: string, ): Promise { let response = null; + try { - response = await Axios.post(`${config.backendAPI}/invitations/accept`, { + response = await Axios.post(`${config.backendAPI}/invitations/${key}/accept`, { username, first_name: firstName, last_name: lastName, @@ -473,7 +474,6 @@ async function acceptInvitation( password1: password, password2: password, confirmations, - key, }); } catch (errorData) { throw generateError(errorData); diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index d8b83295843..47bd1f50487 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -9,7 +9,6 @@ import { CombinedState } from 'reducers'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { acceptInvitationAsync } from 'actions/auth-actions'; -import { RegisterData } from 'components/register-page/register-form'; interface InvitationParams { email: string | null; @@ -28,7 +27,8 @@ function AcceptInvitationPage(): JSX.Element { email: queryParams.get('email'), key: queryParams.get('key'), }; - const onRegister: (args: RegisterData) => void = useCallback((args) => { + const onRegister: (args) => void = useCallback((args) => { + console.log(args); dispatch(acceptInvitationAsync( args.username, args.firstName, diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 33cd6a83f72..9f1c4edcc15 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -6,10 +6,12 @@ from rest_framework import mixins, viewsets from rest_framework.permissions import SAFE_METHODS from django.utils.crypto import get_random_string +from rest_framework.permissions import AllowAny from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view from cvat.apps.engine.mixins import PartialUpdateModelMixin - +from rest_framework.decorators import action +from rest_framework.response import Response from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS @@ -214,3 +216,9 @@ def perform_update(self, serializer): serializer.instance.accept() else: super().perform_update(serializer) + + @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[]) + def accept(self, request, pk): + print(request, pk) + ## TODO implement accept invitation + return Response("testOrgSlug") From 768b9a80645b236b57151a83a32015dd33182d35 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 09:54:34 +0300 Subject: [PATCH 10/64] added sent date field --- .../migrations/0002_invitation_sent_date.py | 17 +++++++++++++++++ cvat/apps/organizations/models.py | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 cvat/apps/organizations/migrations/0002_invitation_sent_date.py diff --git a/cvat/apps/organizations/migrations/0002_invitation_sent_date.py b/cvat/apps/organizations/migrations/0002_invitation_sent_date.py new file mode 100644 index 00000000000..1749156bfa6 --- /dev/null +++ b/cvat/apps/organizations/migrations/0002_invitation_sent_date.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-09-27 06:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("organizations", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="invitation", + name="sent_date", + field=models.DateTimeField(null=True), + ), + ] diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index d04aa6a5caf..829785aa138 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -56,6 +56,7 @@ class Meta: class Invitation(models.Model): key = models.CharField(max_length=64, primary_key=True) created_date = models.DateTimeField(auto_now_add=True) + sent_date = models.DateTimeField(null=True) owner = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL) membership = models.OneToOneField(Membership, on_delete=models.CASCADE) @@ -64,7 +65,8 @@ def organization_id(self): return self.membership.organization_id def send(self, request): - # if not strtobool(settings.ORG_INVITATION_CONFIRM): + # TODO: auto accept for existing users + # if not strtobool(settings.ORG_INVITATION_CONFIRM): # self.accept(self.created_date) target_email = self.membership.user.email current_site = get_current_site(request) @@ -79,7 +81,9 @@ def send(self, request): } get_adapter(request).send_mail('invitation/invitation', target_email, context) - # TODO: auto accept for existing users + + self.sent_date = timezone.now() + self.save() def accept(self, date=None): if not self.membership.is_active: From a94e247d5e9d4367419b9554db509087b6fdec8f Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 10:03:24 +0300 Subject: [PATCH 11/64] updated UI register data type --- cvat-ui/src/actions/auth-actions.ts | 34 ++++++++++++------- .../accept-invitation-page.tsx | 11 ++---- .../register-page/register-page.tsx | 18 ++-------- .../register-page/register-page.tsx | 12 +++---- 4 files changed, 31 insertions(+), 44 deletions(-) diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 5b6b5c158d9..5e167030b9d 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; -import { UserConfirmation } from 'components/register-page/register-form'; +import { RegisterData } from 'components/register-page/register-form'; import { getCore } from 'cvat-core-wrapper'; import isReachable from 'utils/url-checker'; @@ -85,15 +85,19 @@ export const authActions = { export type AuthActions = ActionUnion; export const registerAsync = ( - username: string, - firstName: string, - lastName: string, - email: string, - password: string, - confirmations: UserConfirmation[], + registerData: RegisterData, ): ThunkAction => async (dispatch) => { dispatch(authActions.register()); + const { + username, + firstName, + lastName, + email, + password, + confirmations, + } = registerData; + try { const user = await cvat.server.register( username, @@ -209,16 +213,20 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { }; export const acceptInvitationAsync = ( - username: string, - firstName: string, - lastName: string, - email: string, - password: string, - confirmations: UserConfirmation[], + registerData: RegisterData, key: string, ): ThunkAction => async (dispatch) => { dispatch(authActions.acceptInvitation()); + const { + username, + firstName, + lastName, + email, + password, + confirmations, + } = registerData; + try { const orgSlug = await cvat.server.acceptInvitation( username, diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index 47bd1f50487..50766f58a05 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -9,6 +9,7 @@ import { CombinedState } from 'reducers'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { acceptInvitationAsync } from 'actions/auth-actions'; +import { RegisterData } from 'components/register-page/register-form'; interface InvitationParams { email: string | null; @@ -27,15 +28,9 @@ function AcceptInvitationPage(): JSX.Element { email: queryParams.get('email'), key: queryParams.get('key'), }; - const onRegister: (args) => void = useCallback((args) => { - console.log(args); + const onRegister = useCallback((registerData: RegisterData) => { dispatch(acceptInvitationAsync( - args.username, - args.firstName, - args.lastName, - args.email, - args.password, - args.confirmations, + registerData, invitationParams.key, )); }, [dispatch]); diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 88cb84c0038..5b8872a8f57 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -10,18 +10,13 @@ import { Row, Col } from 'antd/lib/grid'; import { UserAgreement } from 'reducers'; import SigningLayout, { formSizes } from 'components/signing-common/signing-layout'; -import RegisterForm, { RegisterData, UserConfirmation } from './register-form'; +import RegisterForm, { RegisterData } from './register-form'; interface RegisterPageComponentProps { fetching: boolean; userAgreements: UserAgreement[]; onRegister: ( - username: string, - firstName: string, - lastName: string, - email: string, - password: string, - confirmations: UserConfirmation[], + registerData: RegisterData, ) => void; predifinedEmail?: string; disableNavigation?: boolean; @@ -43,14 +38,7 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen predifinedEmail={predifinedEmail} disableNavigation={disableNavigation} onSubmit={(registerData: RegisterData): void => { - onRegister( - registerData.username, - registerData.firstName, - registerData.lastName, - registerData.email, - registerData.password, - registerData.confirmations, - ); + onRegister(registerData); }} /> diff --git a/cvat-ui/src/containers/register-page/register-page.tsx b/cvat-ui/src/containers/register-page/register-page.tsx index 02d647c3431..f74562a3b47 100644 --- a/cvat-ui/src/containers/register-page/register-page.tsx +++ b/cvat-ui/src/containers/register-page/register-page.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,7 +7,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { registerAsync } from 'actions/auth-actions'; import RegisterPageComponent from 'components/register-page/register-page'; -import { UserConfirmation } from 'components/register-page/register-form'; +import { RegisterData } from 'components/register-page/register-form'; import { CombinedState, UserAgreement } from 'reducers'; interface StateToProps { @@ -16,12 +17,7 @@ interface StateToProps { interface DispatchToProps { onRegister: ( - username: string, - firstName: string, - lastName: string, - email: string, - password: string, - userAgreement: UserConfirmation[], + registerData: RegisterData, ) => void; } @@ -34,7 +30,7 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { - onRegister: (...args): void => dispatch(registerAsync(...args)), + onRegister: (args: RegisterData): void => dispatch(registerAsync(args)), }; } From f3aa0e4b94480379b93955bc074690032745e43f Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 10:40:30 +0300 Subject: [PATCH 12/64] added user setup on accept --- cvat/apps/organizations/serializers.py | 40 +++++++++++++++++++++++++- cvat/apps/organizations/views.py | 10 +++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 28ecb85c37c..cd0b3a8e472 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -4,8 +4,9 @@ # SPDX-License-Identifier: MIT from django.contrib.auth import get_user_model -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError from rest_framework import serializers +from dj_rest_auth.registration.serializers import RegisterSerializer from django.contrib.auth.models import User from django.utils.crypto import get_random_string from .models import Invitation, Membership, Organization @@ -140,3 +141,40 @@ class Meta: model = Membership fields = ['id', 'user', 'organization', 'is_active', 'joined_date', 'role'] read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] + +class AcceptInvitationSerializer(RegisterSerializer): + def validate_username(self, username): + return username + + def validate_email(self, email): + return email + + def get_cleaned_data(self): + return { + 'username': self.validated_data.get('username', ''), + 'password1': self.validated_data.get('password1', ''), + 'email': self.validated_data.get('email', ''), + 'firstname': self.validated_data.get('firstname', ''), + 'lastname': self.validated_data.get('lastname', ''), + } + + def save(self, request, pk): + self.cleaned_data = self.get_cleaned_data() + user = User.objects.get(email=self.cleaned_data['email']) + invitation = Invitation.objects.get(key=pk) + if "password1" in self.cleaned_data: + try: + user.is_active = True + user.first_name = self.cleaned_data['firstname'] + user.last_name = self.cleaned_data['lastname'] + user.username = self.cleaned_data['username'] + user.set_password(self.cleaned_data['password1']) + user.save() + + invitation.accept() + return invitation.membership.organization.slug + except ValidationError as exc: + raise serializers.ValidationError( + detail=serializers.as_serializer_error(exc) + ) + return user diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 9f1c4edcc15..82f44d265d7 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -20,7 +20,8 @@ from .serializers import ( InvitationReadSerializer, InvitationWriteSerializer, MembershipReadSerializer, MembershipWriteSerializer, - OrganizationReadSerializer, OrganizationWriteSerializer) + OrganizationReadSerializer, OrganizationWriteSerializer, + AcceptInvitationSerializer) @extend_schema(tags=['organizations']) @extend_schema_view( @@ -217,8 +218,13 @@ def perform_update(self, serializer): else: super().perform_update(serializer) - @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[]) + @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[], serializer_class=AcceptInvitationSerializer) def accept(self, request, pk): print(request, pk) + print(request.data) + serializer = AcceptInvitationSerializer(data=request.data) + if serializer.is_valid(raise_exception=True): + org_slug = serializer.save(request, pk) + return Response(org_slug) ## TODO implement accept invitation return Response("testOrgSlug") From 4cbf36a94cf9fa497e8fb4bf197b01ee14955c34 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 10:45:10 +0300 Subject: [PATCH 13/64] updated invitation messages --- .../templates/invitation/invitation_message.html | 6 +++--- .../templates/invitation/invitation_message.txt | 4 ++-- .../templates/invitation/invitation_subject.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index 768011a8286..adf140478d1 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -1,14 +1,14 @@ {% load i18n %}{% autoescape off %} {% blocktrans %}

- You're receiving this email because you've been invited to organization in CVAT at {{ site_name }}. + You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT at {{ site_name }}.

{% endblocktrans %} {% trans "Please go to the following page and finish creating your account to accept the invitation:" %} -{% block reset_link %} +{% block setup_link %}

-{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&invitation_key={{ invitation_key }} +{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&key={{ invitation_key }}

{% endblock %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.txt b/cvat/apps/organizations/templates/invitation/invitation_message.txt index 59220a1cca0..163e7c3ea4b 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.txt +++ b/cvat/apps/organizations/templates/invitation/invitation_message.txt @@ -1,10 +1,10 @@ {% load i18n %}{% autoescape off %} {% blocktrans %} - You're receiving this email because you've been invited to organization in CVAT at {{ site_name }}. + You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT at {{ site_name }}. {% endblocktrans %} {% trans "Please go to the following page and finish creating your account to accept the invitation:" %} -{% block reset_link %} +{% block setup_link %} {{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&key={{ invitation_key }} {% endblock %} diff --git a/cvat/apps/organizations/templates/invitation/invitation_subject.txt b/cvat/apps/organizations/templates/invitation/invitation_subject.txt index 6840c40b75e..02f95a95901 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_subject.txt +++ b/cvat/apps/organizations/templates/invitation/invitation_subject.txt @@ -1,4 +1,4 @@ {% load i18n %} {% autoescape off %} -{% blocktrans %}Password Reset E-mail{% endblocktrans %} +{% blocktrans %}You're invited to join {{ organization_name }} organization on CVAT!{% endblocktrans %} {% endautoescape %} From 73477465c80cebd1afee5c064b994043c47e8e97 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 11:44:43 +0300 Subject: [PATCH 14/64] refactore accept endpoint --- cvat-core/src/server-proxy.ts | 1 - cvat/apps/organizations/models.py | 14 ++++++++++++++ cvat/apps/organizations/serializers.py | 9 ++------- cvat/apps/organizations/views.py | 23 ++++++++++++++--------- cvat/settings/base.py | 1 + 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index f880f97a59f..a32509bbe90 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -470,7 +470,6 @@ async function acceptInvitation( username, first_name: firstName, last_name: lastName, - email, password1: password, password2: password, confirmations, diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 829785aa138..0d859fa2996 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -3,10 +3,12 @@ # # SPDX-License-Identifier: MIT +from datetime import timedelta from distutils.util import strtobool from django.conf import settings from allauth.account.adapter import get_adapter from django.contrib.sites.shortcuts import get_current_site +from django.conf import settings from django.db import models from django.contrib.auth import get_user_model @@ -64,6 +66,17 @@ class Invitation(models.Model): def organization_id(self): return self.membership.organization_id + @property + def expired(self): + expiration_date = self.sent_date + timedelta( + days=settings.ORG_INVITATION_EXPIRY, + ) + return expiration_date <= timezone.now() + + @property + def organization_slug(self): + return self.membership.organization.slug + def send(self, request): # TODO: auto accept for existing users # if not strtobool(settings.ORG_INVITATION_CONFIRM): @@ -77,6 +90,7 @@ def send(self, request): 'invitation_key': self.key, 'domain': domain, 'site_name': site_name, + 'organization_name': self.membership.organization.slug, 'protocol': 'http', ## TODO add https } diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index cd0b3a8e472..e2289697ca6 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -153,15 +153,13 @@ def get_cleaned_data(self): return { 'username': self.validated_data.get('username', ''), 'password1': self.validated_data.get('password1', ''), - 'email': self.validated_data.get('email', ''), 'firstname': self.validated_data.get('firstname', ''), 'lastname': self.validated_data.get('lastname', ''), } - def save(self, request, pk): + def save(self, request, invitation): self.cleaned_data = self.get_cleaned_data() - user = User.objects.get(email=self.cleaned_data['email']) - invitation = Invitation.objects.get(key=pk) + user = invitation.membership.user if "password1" in self.cleaned_data: try: user.is_active = True @@ -170,9 +168,6 @@ def save(self, request, pk): user.username = self.cleaned_data['username'] user.set_password(self.cleaned_data['password1']) user.save() - - invitation.accept() - return invitation.membership.organization.slug except ValidationError as exc: raise serializers.ValidationError( detail=serializers.as_serializer_error(exc) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 82f44d265d7..43d3a6e9228 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: MIT -from rest_framework import mixins, viewsets +from rest_framework import mixins, viewsets, status from rest_framework.permissions import SAFE_METHODS from django.utils.crypto import get_random_string from rest_framework.permissions import AllowAny @@ -220,11 +220,16 @@ def perform_update(self, serializer): @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[], serializer_class=AcceptInvitationSerializer) def accept(self, request, pk): - print(request, pk) - print(request.data) - serializer = AcceptInvitationSerializer(data=request.data) - if serializer.is_valid(raise_exception=True): - org_slug = serializer.save(request, pk) - return Response(org_slug) - ## TODO implement accept invitation - return Response("testOrgSlug") + try: + invitation = Invitation.objects.get(key=pk) + if invitation.expired: + return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is expired. Please contact organization owner to renew it.") + if invitation.membership.is_active: + return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is already accepted.") + serializer = AcceptInvitationSerializer(data=request.data) + if serializer.is_valid(raise_exception=True): + serializer.save(request, invitation) + invitation.accept() + return Response(status=status.HTTP_200_OK, data=invitation.membership.organization.slug) + except Invitation.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist. Please contact organization owner.") diff --git a/cvat/settings/base.py b/cvat/settings/base.py index be4927d91d4..5cadff0a17e 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -307,6 +307,7 @@ def GET_IAM_DEFAULT_ROLES(user) -> list: # ORG settings ORG_INVITATION_CONFIRM = 'No' +ORG_INVITATION_EXPIRY = 14 AUTHENTICATION_BACKENDS = [ From 4b63507bd80e748c8ce753a08c9aeedc7a338f85 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 12:00:45 +0300 Subject: [PATCH 15/64] returned auto accept for existing users --- cvat/apps/organizations/models.py | 3 --- cvat/apps/organizations/serializers.py | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 0d859fa2996..e9b753c2226 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -78,9 +78,6 @@ def organization_slug(self): return self.membership.organization.slug def send(self, request): - # TODO: auto accept for existing users - # if not strtobool(settings.ORG_INVITATION_CONFIRM): - # self.accept(self.created_date) target_email = self.membership.user.email current_site = get_current_site(request) site_name = current_site.name diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index e2289697ca6..e47a06e1da4 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -93,7 +93,7 @@ def create(self, validated_data): user_email = membership_data['user']['email'] username = user_email.split("@")[0] user = User.objects.create_user(username=username, password=get_random_string(length=32), - email=user_email) + email=user_email, is_active=False) user.set_unusable_password() user.save() del membership_data['user'] @@ -112,9 +112,12 @@ def update(self, instance, validated_data): return super().update(instance, {}) def save(self, request, **kwargs): - ## TODO move/remove request to/from kwarg invitation = super().save(**kwargs) - invitation.send(request) + if invitation.membership.user.is_active: + # For existing users we auto-accept all invitations + invitation.accept() + else: + invitation.send(request) return invitation From 1d97f80031c19adb594551a546957c3999cf1572 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 12:17:48 +0300 Subject: [PATCH 16/64] added resend endpoint --- cvat/apps/organizations/throttle.py | 8 ++++++++ cvat/apps/organizations/views.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 cvat/apps/organizations/throttle.py diff --git a/cvat/apps/organizations/throttle.py b/cvat/apps/organizations/throttle.py new file mode 100644 index 00000000000..438538b61d4 --- /dev/null +++ b/cvat/apps/organizations/throttle.py @@ -0,0 +1,8 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from rest_framework.throttling import UserRateThrottle + +class ResendOrganizationInvitationThrottle(UserRateThrottle): + rate = '5/hour' diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 43d3a6e9228..ed0afb9eb84 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -15,6 +15,7 @@ from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS +from cvat.apps.organizations.throttle import ResendOrganizationInvitationThrottle from .models import Invitation, Membership, Organization from .serializers import ( @@ -233,3 +234,14 @@ def accept(self, request, pk): return Response(status=status.HTTP_200_OK, data=invitation.membership.organization.slug) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist. Please contact organization owner.") + + @action(detail=True, methods=['POST'], url_path='resend', throttle_classes=[ResendOrganizationInvitationThrottle]) + def resend(self, request, pk): + try: + invitation = Invitation.objects.get(key=pk) + if invitation.membership.is_active: + return Response(status=status.HTTP_400_BAD_REQUEST, data="This invitation is already accepted.") + invitation.send(request) + return Response(status=status.HTTP_200_OK, data="Invitation has been sent.") + except Invitation.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") From 2ba490f9f1fa8174db1ff4375fefb203811b5550 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 27 Sep 2023 12:25:41 +0300 Subject: [PATCH 17/64] updated invitation settings --- cvat/apps/organizations/serializers.py | 4 +++- cvat/settings/base.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index e47a06e1da4..d2b01fb0558 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -5,10 +5,12 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.conf import settings from rest_framework import serializers from dj_rest_auth.registration.serializers import RegisterSerializer from django.contrib.auth.models import User from django.utils.crypto import get_random_string +from distutils.util import strtobool from .models import Invitation, Membership, Organization from cvat.apps.engine.serializers import BasicUserSerializer @@ -113,7 +115,7 @@ def update(self, instance, validated_data): def save(self, request, **kwargs): invitation = super().save(**kwargs) - if invitation.membership.user.is_active: + if not strtobool(settings.ORG_INVITATION_CONFIRM) and invitation.membership.user.is_active: # For existing users we auto-accept all invitations invitation.accept() else: diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 5cadff0a17e..9b2b18738a5 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -307,7 +307,7 @@ def GET_IAM_DEFAULT_ROLES(user) -> list: # ORG settings ORG_INVITATION_CONFIRM = 'No' -ORG_INVITATION_EXPIRY = 14 +ORG_INVITATION_EXPIRY = 7 # Expiration time in days AUTHENTICATION_BACKENDS = [ From f65494aef029c32c19f93011839a93229544dccf Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 28 Sep 2023 10:44:18 +0300 Subject: [PATCH 18/64] activate organization after invite on login --- cvat-ui/src/actions/auth-actions.ts | 2 + .../accept-invitation-page.tsx | 3 ++ cvat-ui/src/components/cvat-app.tsx | 6 ++- .../watchers/organization-watcher.tsx | 40 +++++++++++++++---- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 5e167030b9d..ad1069a3c2c 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -215,6 +215,7 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { export const acceptInvitationAsync = ( registerData: RegisterData, key: string, + onSuccess?: (orgSlug: string) => void, ): ThunkAction => async (dispatch) => { dispatch(authActions.acceptInvitation()); @@ -238,6 +239,7 @@ export const acceptInvitationAsync = ( key, ); + if (onSuccess) onSuccess(orgSlug); dispatch(authActions.acceptInvitationSuccess(orgSlug)); } catch (error) { dispatch(authActions.acceptInvitationSuccess(error)); diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index 50766f58a05..f84f1d67f5d 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -32,6 +32,9 @@ function AcceptInvitationPage(): JSX.Element { dispatch(acceptInvitationAsync( registerData, invitationParams.key, + (orgSlug: string) => { + history.replace(`/auth/login?next=/tasks&activateOrganization=${orgSlug}`); + }, )); }, [dispatch]); diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 569642d897d..f340e0b9cca 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -492,7 +492,11 @@ class CVATApplication extends React.PureComponent diff --git a/cvat-ui/src/components/watchers/organization-watcher.tsx b/cvat-ui/src/components/watchers/organization-watcher.tsx index 63dc733acdd..9ae0d6c85b9 100644 --- a/cvat-ui/src/components/watchers/organization-watcher.tsx +++ b/cvat-ui/src/components/watchers/organization-watcher.tsx @@ -2,26 +2,52 @@ // // SPDX-License-Identifier: MIT -import { getCore } from 'cvat-core-wrapper'; +import { Organization, getCore } from 'cvat-core-wrapper'; import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; import { CombinedState } from 'reducers'; const core = getCore(); function OrganizationWatcher(): JSX.Element { const organizationList = useSelector((state: CombinedState) => state.organizations.list); + const history = useHistory(); + const queryParams = new URLSearchParams(history.location.search); - useEffect(() => { - core.config.onOrganizationChange = (newOrgId: number | null) => { - if (newOrgId === null) { - localStorage.removeItem('currentOrganization'); - } else { - const newOrganization = organizationList.find((org) => org.id === newOrgId); + const changeOrganization = (newOrg: number | string | null, location?: string): void => { + let newOrganization: Organization | null = null; + if (newOrg) { + if (Number.isInteger(newOrg)) { + newOrganization = organizationList.find((org) => org.id === newOrg); + } else if (typeof newOrg === 'string') { + newOrganization = organizationList.find((org) => org.slug === newOrg); + } + + if (newOrganization) { localStorage.setItem('currentOrganization', newOrganization.slug); } + } else { + localStorage.removeItem('currentOrganization'); + } + + const shouldReload = (!newOrg || newOrganization); + if (shouldReload && location) { + window.location.pathname = location; + window.location.search = ''; + } else if (shouldReload) { window.location.reload(); + } + }; + + useEffect(() => { + core.config.onOrganizationChange = (newOrgId: number | null) => { + changeOrganization(newOrgId); }; + if (queryParams.get('activateOrganization')) { + const orgSlug = queryParams.get('activateOrganization'); + changeOrganization(orgSlug, '/tasks'); + } }, []); return <>; From f3b1f417c6181de063e6f9599fc177eabe5e8bb0 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 28 Sep 2023 12:02:04 +0300 Subject: [PATCH 19/64] typed ui membership and invitation --- cvat-core/src/organization.ts | 84 +++++++++++++++---- cvat-core/src/user.ts | 2 +- .../organization-page/member-item.tsx | 4 +- .../organization-page/members-list.tsx | 5 +- .../organization-page/organization-page.tsx | 5 +- cvat-ui/src/cvat-core-wrapper.ts | 4 +- 6 files changed, 82 insertions(+), 22 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 921c0a4e268..da44d7132d2 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -10,17 +10,77 @@ import { MembershipRole } from './enums'; import { ArgumentError, DataError } from './exceptions'; import PluginRegistry from './plugins'; import serverProxy from './server-proxy'; -import User from './user'; +import User, { RawUserData } from './user'; -interface Membership { - user: User; +interface SerializedInvitationData { + created_date: string; + owner: RawUserData; +} + +interface SerializedMembershipData { + id: number; + user: RawUserData; is_active: boolean; joined_date: string; role: MembershipRole; - invitation: { - created_date: string; - owner: User; - } | null; + invitation: SerializedInvitationData | null; +} + +export class Invitation { + #createdDate: string; + #owner: User; + + constructor(initialData: SerializedInvitationData) { + this.#createdDate = initialData.created_date; + this.#owner = new User(initialData.owner); + } + + get owner(): User { + return this.#owner; + } + + get createdDate(): string { + return this.#createdDate; + } +} + +export class Membership { + #id: number; + #user: User; + #isActive: boolean; + #joinedDate: string; + #role: MembershipRole; + #invitation: Invitation | null; + + constructor(initialData: SerializedMembershipData) { + this.#id = initialData.id; + this.#user = new User(initialData.user); + this.#isActive = initialData.is_active; + this.#joinedDate = initialData.joined_date; + this.#role = initialData.role; + this.#invitation = initialData.invitation ? new Invitation(initialData.invitation) : null; + } + + get id(): number { + return this.#id; + } + + get user(): User { + return this.#user; + } + + get isActive(): boolean { + return this.#isActive; + } + get joinedDate(): string { + return this.#joinedDate; + } + get role(): MembershipRole { + return this.#role; + } + get invitation(): Invitation { + return this.#invitation; + } } export default class Organization { @@ -234,13 +294,9 @@ Object.defineProperties(Organization.prototype.members, { checkObjectType('pageSize', pageSize, 'number'); const result = await serverProxy.organizations.members(orgSlug, page, pageSize); - await Promise.all( - result.results.map((membership) => { - membership.user = new User(membership.user); - - return Promise.resolve(); - }), - ); + result.results = result.results.map((rawMembership: SerializedMembershipData) => new Membership( + rawMembership, + )); result.results.count = result.count; return result.results; diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts index 4dea7e18aa9..fc6da2cb999 100644 --- a/cvat-core/src/user.ts +++ b/cvat-core/src/user.ts @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: MIT -interface RawUserData { +export interface RawUserData { id: number; username: string; email: string; diff --git a/cvat-ui/src/components/organization-page/member-item.tsx b/cvat-ui/src/components/organization-page/member-item.tsx index 66acf94fcfe..0880ffcd51c 100644 --- a/cvat-ui/src/components/organization-page/member-item.tsx +++ b/cvat-ui/src/components/organization-page/member-item.tsx @@ -23,7 +23,7 @@ function MemberItem(props: Props): JSX.Element { membershipInstance, onRemoveMembership, onUpdateMembershipRole, } = props; const { - user, joined_date: joinedDate, role, invitation, + user, joinedDate, role, invitation, } = membershipInstance; const { username, firstName, lastName } = user; const { username: selfUserName } = useSelector((state: CombinedState) => state.auth.user); @@ -42,7 +42,7 @@ function MemberItem(props: Props): JSX.Element { {`Invited ${moment(invitation.created_date).fromNow()} ${invitation.owner ? `by ${invitation.owner.username}` : ''}`} ) : null} - {joinedDate ? {`Joined ${moment(joinedDate).fromNow()}`} : null} + {joinedDate ? {`Joined ${moment(joinedDate).fromNow()}`} : Invitation pending} - {(role === 'owner' || selfUserName === username) ? null : ( - { - Modal.confirm({ - className: 'cvat-modal-organization-member-remove', - title: `You are removing "${username}" from this organization`, - content: 'The person will not have access to the organization data anymore. Continue?', - okText: 'Yes, remove', - okButtonProps: { - danger: true, - }, - onOk: () => { - onRemoveMembership(); - }, - }); - }} - /> - )} + {leftBlock} ); diff --git a/cvat-ui/src/components/organization-page/members-list.tsx b/cvat-ui/src/components/organization-page/members-list.tsx index 8da9fd55832..b24a9ff8338 100644 --- a/cvat-ui/src/components/organization-page/members-list.tsx +++ b/cvat-ui/src/components/organization-page/members-list.tsx @@ -8,7 +8,10 @@ import Spin from 'antd/lib/spin'; import { useDispatch, useSelector } from 'react-redux'; import { CombinedState } from 'reducers'; -import { removeOrganizationMemberAsync, updateOrganizationMemberAsync } from 'actions/organization-actions'; +import { + deleteOrganizationInvitationAsync, removeOrganizationMemberAsync, + resendOrganizationInvitationAsync, updateOrganizationMemberAsync, +} from 'actions/organization-actions'; import { Membership } from 'cvat-core-wrapper'; import MemberItem from './member-item'; @@ -57,6 +60,18 @@ function MembersList(props: Props): JSX.Element { }), ); }} + onResendInvitation={(key: string) => { + dispatch( + resendOrganizationInvitationAsync(organizationInstance, key), + ); + }} + onDeleteInvitation={(key: string) => { + dispatch( + deleteOrganizationInvitationAsync(organizationInstance, key, () => { + fetchMembers(); + }), + ); + }} /> ), )} From b932980ae123bbe0c9a40aa0c0341bb921ed2a5f Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 28 Sep 2023 14:07:01 +0300 Subject: [PATCH 23/64] delete membeship with invitation --- cvat-ui/src/components/organization-page/members-list.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cvat-ui/src/components/organization-page/members-list.tsx b/cvat-ui/src/components/organization-page/members-list.tsx index b24a9ff8338..e470f3e0dbd 100644 --- a/cvat-ui/src/components/organization-page/members-list.tsx +++ b/cvat-ui/src/components/organization-page/members-list.tsx @@ -67,10 +67,13 @@ function MembersList(props: Props): JSX.Element { }} onDeleteInvitation={(key: string) => { dispatch( - deleteOrganizationInvitationAsync(organizationInstance, key, () => { + removeOrganizationMemberAsync(organizationInstance, member, () => { fetchMembers(); }), ); + dispatch( + deleteOrganizationInvitationAsync(organizationInstance, key), + ); }} /> ), From 945fcff0d8113c5b19df3d6e2573ce5e610c207a Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 29 Sep 2023 12:18:09 +0300 Subject: [PATCH 24/64] added ui error handling --- cvat-ui/src/reducers/index.ts | 3 ++ cvat-ui/src/reducers/notifications-reducer.ts | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index ea91619f8aa..f8e850b3b07 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -426,6 +426,7 @@ export interface NotificationsState { requestPasswordReset: null | ErrorState; resetPassword: null | ErrorState; loadAuthActions: null | ErrorState; + acceptingInvitation: null | ErrorState; }; projects: { fetching: null | ErrorState; @@ -536,6 +537,8 @@ export interface NotificationsState { inviting: null | ErrorState; updatingMembership: null | ErrorState; removingMembership: null | ErrorState; + resendingInvitation: null | ErrorState; + deletingInvitation: null | ErrorState; }; webhooks: { fetching: null | ErrorState; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 71e3ed368bc..112a339940b 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -38,6 +38,7 @@ const defaultState: NotificationsState = { requestPasswordReset: null, resetPassword: null, loadAuthActions: null, + acceptingInvitation: null, }, projects: { fetching: null, @@ -148,6 +149,8 @@ const defaultState: NotificationsState = { inviting: null, updatingMembership: null, removingMembership: null, + resendingInvitation: null, + deletingInvitation: null, }, webhooks: { fetching: null, @@ -380,6 +383,22 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AuthActionTypes.ACCEPT_INVITATION_FAILED: { + return { + ...state, + errors: { + ...state.errors, + auth: { + ...state.errors.auth, + acceptingInvitation: { + message: 'Could not accept invitation', + reason: action.payload.error, + shouldLog: !(action.payload.error instanceof ServerError), + }, + }, + }, + }; + } case ExportActionTypes.EXPORT_DATASET_FAILED: { const { instance, instanceType } = action.payload; return { @@ -1604,6 +1623,40 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case OrganizationActionsTypes.RESEND_ORGANIZATION_INVITATION_FAILED: { + return { + ...state, + errors: { + ...state.errors, + organizations: { + ...state.errors.organizations, + resendingInvitation: { + message: 'Could not resend invitation', + reason: action.payload.error, + shouldLog: !(action.payload.error instanceof ServerError), + className: 'cvat-notification-notice-resend-organization-invintation-failed', + }, + }, + }, + }; + } + case OrganizationActionsTypes.DELETE_ORGANIZATION_INVITATION: { + return { + ...state, + errors: { + ...state.errors, + organizations: { + ...state.errors.organizations, + deletingInvitation: { + message: 'Could not delete invitation', + reason: action.payload.error, + shouldLog: !(action.payload.error instanceof ServerError), + className: 'cvat-notification-notice-delete-organization-invintation-failed', + }, + }, + }, + }; + } case JobsActionTypes.GET_JOBS_FAILED: { return { ...state, From 1f9761bdf82c36e8b574deb34050e770e495fc35 Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 29 Sep 2023 14:33:19 +0300 Subject: [PATCH 25/64] added info notification --- cvat-ui/src/reducers/index.ts | 3 +++ cvat-ui/src/reducers/notifications-reducer.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index f8e850b3b07..fbfa24a9491 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -580,6 +580,9 @@ export interface NotificationsState { annotation: string; backup: string; }; + organizations: { + resendingInvitation: string; + } }; } diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 112a339940b..f5318ea942b 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -192,6 +192,9 @@ const defaultState: NotificationsState = { annotation: '', backup: '', }, + organizations: { + resendingInvitation: '', + }, }, }; @@ -1657,6 +1660,18 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case OrganizationActionsTypes.RESEND_ORGANIZATION_INVITATION_SUCCESS: { + return { + ...state, + messages: { + ...state.messages, + organizations: { + ...state.messages.organizations, + resendingInvitation: 'Invintation was sent suces', + }, + }, + }; + } case JobsActionTypes.GET_JOBS_FAILED: { return { ...state, From 9e576b320e202eaf20a81891af21a78a4d09cf93 Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 29 Sep 2023 14:35:16 +0300 Subject: [PATCH 26/64] removed comment --- cvat-ui/src/actions/auth-actions.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index ad1069a3c2c..77c176b850e 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -5,7 +5,7 @@ import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { RegisterData } from 'components/register-page/register-form'; -import { getCore } from 'cvat-core-wrapper'; +import { getCore, User } from 'cvat-core-wrapper'; import isReachable from 'utils/url-checker'; const cvat = getCore(); @@ -77,8 +77,7 @@ export const authActions = { ), loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), acceptInvitation: () => createAction(AuthActionTypes.ACCEPT_INVITATION), - // TODO: successs must return organization - acceptInvitationSuccess: (user: any) => createAction(AuthActionTypes.ACCEPT_INVITATION_SUCCESS, { user }), + acceptInvitationSuccess: (user: User) => createAction(AuthActionTypes.ACCEPT_INVITATION_SUCCESS, { user }), acceptInvitationFailed: (error: any) => createAction(AuthActionTypes.ACCEPT_INVITATION_FAILED, { error }), }; @@ -242,6 +241,6 @@ export const acceptInvitationAsync = ( if (onSuccess) onSuccess(orgSlug); dispatch(authActions.acceptInvitationSuccess(orgSlug)); } catch (error) { - dispatch(authActions.acceptInvitationSuccess(error)); + dispatch(authActions.acceptInvitationFailed(error)); } }; From d7ed98051db0c0c33dd00c2823ec24f2ead7ecb6 Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 29 Sep 2023 14:41:25 +0300 Subject: [PATCH 27/64] added checks --- .../accept-invitation-page/accept-invitation-page.tsx | 9 ++++----- cvat-ui/src/components/cvat-app.tsx | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index f84f1d67f5d..46bc156a3fc 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -12,8 +12,8 @@ import { acceptInvitationAsync } from 'actions/auth-actions'; import { RegisterData } from 'components/register-page/register-form'; interface InvitationParams { - email: string | null; - key: string | null; + email: string; + key: string; } function AcceptInvitationPage(): JSX.Element { @@ -23,10 +23,9 @@ function AcceptInvitationPage(): JSX.Element { const history = useHistory(); const dispatch = useDispatch(); const queryParams = new URLSearchParams(history.location.search); - // TODO: add check for inv params const invitationParams: InvitationParams = { - email: queryParams.get('email'), - key: queryParams.get('key'), + email: queryParams.get('email') || '', + key: queryParams.get('key') || '', }; const onRegister = useCallback((registerData: RegisterData) => { dispatch(acceptInvitationAsync( diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index f340e0b9cca..1bc15a658f1 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -428,6 +428,8 @@ class CVATApplication extends React.PureComponent shouldBeRendered(this.props, this.state)) .map(({ component: Component }) => Component); + const queryParams = new URLSearchParams(location.search); + if (readyForRender) { if (user && user.isVerified) { return ( @@ -493,9 +495,9 @@ class CVATApplication extends React.PureComponent From a57fed7b50e6914f7cd9e08dd855b23d8a96b0f2 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 12:46:42 +0300 Subject: [PATCH 28/64] removed excessive file --- .../accept-invitation-page/accept-invitation-page.tsx | 1 - cvat-ui/src/components/accept-invitation-page/styles.scss | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 cvat-ui/src/components/accept-invitation-page/styles.scss diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index 46bc156a3fc..e6ce473ab1e 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: MIT -import './styles.scss'; import React, { useCallback } from 'react'; import RegisterPageComponent from 'components/register-page/register-page'; import { CombinedState } from 'reducers'; diff --git a/cvat-ui/src/components/accept-invitation-page/styles.scss b/cvat-ui/src/components/accept-invitation-page/styles.scss deleted file mode 100644 index 2347fbdcaf7..00000000000 --- a/cvat-ui/src/components/accept-invitation-page/styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.test { - color: red; -} From 3d5e38ac99535a6249fcd38c8a1101b4fc937bd9 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 14:35:55 +0300 Subject: [PATCH 29/64] updated schema --- cvat/schema.yml | 106 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/cvat/schema.yml b/cvat/schema.yml index 2dc187c5ff6..b02bcd3b0f1 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.8.0 + version: 2.8.0.dev20231002094801 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: @@ -1531,6 +1531,64 @@ paths: responses: '204': description: The invitation has been deleted + /api/invitations/{key}/accept: + post: + operationId: invitations_create_accept + parameters: + - in: path + name: key + schema: + type: string + description: A unique value identifying this invitation. + required: true + tags: + - invitations + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InvitationWriteRequest' + required: true + security: + - {} + responses: + '200': + content: + application/vnd.cvat+json: + schema: + $ref: '#/components/schemas/InvitationWrite' + description: '' + /api/invitations/{key}/resend: + post: + operationId: invitations_create_resend + parameters: + - in: path + name: key + schema: + type: string + description: A unique value identifying this invitation. + required: true + tags: + - invitations + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InvitationWriteRequest' + required: true + security: + - sessionAuth: [] + csrfAuth: [] + tokenAuth: [] + - signatureAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/vnd.cvat+json: + schema: + $ref: '#/components/schemas/InvitationWrite' + description: '' /api/issues: get: operationId: issues_list @@ -6362,6 +6420,22 @@ components: oneOf: - $ref: '#/components/schemas/ProjectFileRequest' nullable: true + BasicInvitation: + type: object + properties: + key: + type: string + readOnly: true + created_date: + type: string + format: date-time + readOnly: true + owner: + allOf: + - $ref: '#/components/schemas/BasicUser' + nullable: true + required: + - owner BasicUser: type: object properties: @@ -7305,6 +7379,31 @@ components: - owner - role - user + InvitationWrite: + type: object + properties: + key: + type: string + readOnly: true + created_date: + type: string + format: date-time + readOnly: true + owner: + type: integer + readOnly: true + nullable: true + role: + $ref: '#/components/schemas/RoleEnum' + organization: + type: integer + readOnly: true + email: + type: string + format: email + required: + - email + - role InvitationWriteRequest: type: object properties: @@ -7963,10 +8062,9 @@ components: - $ref: '#/components/schemas/RoleEnum' readOnly: true invitation: - type: string - readOnly: true - nullable: true + $ref: '#/components/schemas/BasicInvitation' required: + - invitation - user MetaUser: anyOf: From 4e14ed2c1f70c31db1b6874c86bbc7403153c32b Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 14:50:36 +0300 Subject: [PATCH 30/64] fixed https link --- cvat/apps/organizations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index e9b753c2226..67d338f9be8 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -88,7 +88,7 @@ def send(self, request): 'domain': domain, 'site_name': site_name, 'organization_name': self.membership.organization.slug, - 'protocol': 'http', ## TODO add https + 'protocol': 'https' if request.is_secure() else 'http', } get_adapter(request).send_mail('invitation/invitation', target_email, context) From 8b6ac0b7b8b097b8f29114e39920900cdd362972 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:06:58 +0300 Subject: [PATCH 31/64] updated email template --- cvat/apps/organizations/models.py | 1 + .../invitation/invitation_message.html | 303 ++++++++++++++++-- .../invitation/invitation_message.txt | 15 - 3 files changed, 283 insertions(+), 36 deletions(-) delete mode 100644 cvat/apps/organizations/templates/invitation/invitation_message.txt diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 67d338f9be8..f96e26a842a 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -87,6 +87,7 @@ def send(self, request): 'invitation_key': self.key, 'domain': domain, 'site_name': site_name, + 'invitation_owner': self.owner.get_username(), 'organization_name': self.membership.organization.slug, 'protocol': 'https' if request.is_secure() else 'http', } diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index adf140478d1..4f2d590ab21 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -1,21 +1,282 @@ -{% load i18n %}{% autoescape off %} -{% blocktrans %} -

- You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT at {{ site_name }}. -

-{% endblocktrans %} - -{% trans "Please go to the following page and finish creating your account to accept the invitation:" %} -{% block setup_link %} -

-{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&key={{ invitation_key }} -

-{% endblock %} - -{% trans "Thanks for using our site!" %} - -

-{% blocktrans %}The {{ site_name }} team{% endblocktrans %} -

- -{% endautoescape %} + +{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %} +{% load static %} + + + + + + + Email Confirmation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + Logo + +
+ +
+ + + + + +
+

+ You've been invited to organization +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ {% blocktrans %} +

+ You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT by {{ invitation_owner }} at {{ site_name }}. +

+

+ To join organization and start annotating, simply tap the button below and complete registration. +

+ {% endblocktrans %} +
+ + + + +
+ + + + +
+ {% blocktrans %} + + Confirm + + {% endblocktrans %} +
+
+
+
+ {% blocktrans %} +

+ {{ domain }} +

+ {% endblocktrans %} {% endautoescape %} +
+ +
+ + + + + + + + + +
+

If you didn't request this, please ignore this email.

+
+ +
+ + + + \ No newline at end of file diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.txt b/cvat/apps/organizations/templates/invitation/invitation_message.txt deleted file mode 100644 index 163e7c3ea4b..00000000000 --- a/cvat/apps/organizations/templates/invitation/invitation_message.txt +++ /dev/null @@ -1,15 +0,0 @@ -{% load i18n %}{% autoescape off %} -{% blocktrans %} - You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT at {{ site_name }}. -{% endblocktrans %} - -{% trans "Please go to the following page and finish creating your account to accept the invitation:" %} -{% block setup_link %} -{{ protocol }}://{{ domain }}/auth/register/invitation?email={{ email }}&key={{ invitation_key }} -{% endblock %} - -{% trans "Thanks for using our site!" %} - -{% blocktrans %}The {{ site_name }} team{% endblocktrans %} - -{% endautoescape %} \ No newline at end of file From dc3745376155619f4edd0f95e12bd8048517397e Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:17:08 +0300 Subject: [PATCH 32/64] added comma --- cvat/apps/organizations/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index ed0afb9eb84..4f6f9e2b8d4 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -210,7 +210,7 @@ def perform_create(self, serializer): owner=self.request.user, key=get_random_string(length=64), organization=self.request.iam_context['organization'], - request=self.request + request=self.request, ) def perform_update(self, serializer): From d46c9f69208ddddaa4965db7cb56f555cbab148f Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:18:02 +0300 Subject: [PATCH 33/64] updated setting name --- cvat/apps/organizations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index f96e26a842a..f89d78fc074 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -69,7 +69,7 @@ def organization_id(self): @property def expired(self): expiration_date = self.sent_date + timedelta( - days=settings.ORG_INVITATION_EXPIRY, + days=settings.ORG_INVITATION_EXPIRY_DAYS, ) return expiration_date <= timezone.now() From b8046412c99deee1b134affcad2bd2a4825cefd0 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:19:48 +0300 Subject: [PATCH 34/64] updated email subject --- .../organizations/templates/invitation/invitation_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_subject.txt b/cvat/apps/organizations/templates/invitation/invitation_subject.txt index 02f95a95901..4fedaaf7bed 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_subject.txt +++ b/cvat/apps/organizations/templates/invitation/invitation_subject.txt @@ -1,4 +1,4 @@ {% load i18n %} {% autoescape off %} -{% blocktrans %}You're invited to join {{ organization_name }} organization on CVAT!{% endblocktrans %} +{% blocktrans %}You're invited to join {{ organization_name }} organization in CVAT!{% endblocktrans %} {% endautoescape %} From 2e4a396a988afbb09d4113be6981ff10506c1b5b Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:21:51 +0300 Subject: [PATCH 35/64] removed valiidate username --- cvat/apps/organizations/serializers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index d2b01fb0558..9dbbd6958fb 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -148,9 +148,6 @@ class Meta: read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] class AcceptInvitationSerializer(RegisterSerializer): - def validate_username(self, username): - return username - def validate_email(self, email): return email From 89c3c62d9559ea258e67196ec7a6df8dadc70c00 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:30:25 +0300 Subject: [PATCH 36/64] added success info message --- cvat-ui/src/reducers/index.ts | 1 + cvat-ui/src/reducers/notifications-reducer.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 35fb4937f2f..d893512c943 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -565,6 +565,7 @@ export interface NotificationsState { registerDone: string; requestPasswordResetDone: string; resetPasswordDone: string; + acceptInvitationDone: string; }; projects: { restoringDone: string; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index f5318ea942b..ae6242a2350 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -178,6 +178,7 @@ const defaultState: NotificationsState = { registerDone: '', requestPasswordResetDone: '', resetPasswordDone: '', + acceptInvitationDone: '', }, projects: { restoringDone: '', @@ -386,6 +387,19 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AuthActionTypes.ACCEPT_INVITATION_SUCCESS: { + return { + ...state, + ...state, + messages: { + ...state.messages, + auth: { + ...state.messages.auth, + acceptInvitationDone: 'Invitation accepted successfully. You can Sign in now.', + }, + }, + }; + } case AuthActionTypes.ACCEPT_INVITATION_FAILED: { return { ...state, From d8f88f2617cbe0e204b39556bb63ccb486cb38fe Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:30:46 +0300 Subject: [PATCH 37/64] updated setting --- cvat/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 6d492c1b736..b7c62077f06 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -254,7 +254,7 @@ def GET_IAM_DEFAULT_ROLES(user) -> list: # ORG settings ORG_INVITATION_CONFIRM = 'No' -ORG_INVITATION_EXPIRY = 7 # Expiration time in days +ORG_INVITATION_EXPIRY_DAYS = 7 AUTHENTICATION_BACKENDS = [ From a41d969b1ec0008beedee5feb3b770dc63826d66 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:33:26 +0300 Subject: [PATCH 38/64] removed excessive check --- cvat/apps/organizations/serializers.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 9dbbd6958fb..0bb3c060beb 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -162,16 +162,10 @@ def get_cleaned_data(self): def save(self, request, invitation): self.cleaned_data = self.get_cleaned_data() user = invitation.membership.user - if "password1" in self.cleaned_data: - try: - user.is_active = True - user.first_name = self.cleaned_data['firstname'] - user.last_name = self.cleaned_data['lastname'] - user.username = self.cleaned_data['username'] - user.set_password(self.cleaned_data['password1']) - user.save() - except ValidationError as exc: - raise serializers.ValidationError( - detail=serializers.as_serializer_error(exc) - ) + user.is_active = True + user.first_name = self.cleaned_data['firstname'] + user.last_name = self.cleaned_data['lastname'] + user.username = self.cleaned_data['username'] + user.set_password(self.cleaned_data['password1']) + user.save() return user From c03f11438af0796a840e3605c569f3481cbf9111 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:35:06 +0300 Subject: [PATCH 39/64] changed action button --- .../organizations/templates/invitation/invitation_message.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index 4f2d590ab21..4f4dc7726c0 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -211,7 +211,7 @@

- Confirm + Setup your account {% endblocktrans %} From be249c60ae406ea16e6e4ccf1b87069bc8310f74 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:38:54 +0300 Subject: [PATCH 40/64] added transaction --- cvat/apps/organizations/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 4f6f9e2b8d4..873db340f31 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -16,6 +16,7 @@ InvitationPermission, MembershipPermission, OrganizationPermission) from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS from cvat.apps.organizations.throttle import ResendOrganizationInvitationThrottle +from django.db import transaction from .models import Invitation, Membership, Organization from .serializers import ( @@ -219,6 +220,7 @@ def perform_update(self, serializer): else: super().perform_update(serializer) + @transaction.atomic @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[], serializer_class=AcceptInvitationSerializer) def accept(self, request, pk): try: From 1fa48346d02c0345abeb2c4b28b93371a0848609 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 15:42:15 +0300 Subject: [PATCH 41/64] updated imports --- cvat/apps/organizations/models.py | 2 -- cvat/apps/organizations/serializers.py | 9 +++++---- cvat/apps/organizations/views.py | 13 ++++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index f89d78fc074..bc0a5651bce 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -4,11 +4,9 @@ # SPDX-License-Identifier: MIT from datetime import timedelta -from distutils.util import strtobool from django.conf import settings from allauth.account.adapter import get_adapter from django.contrib.sites.shortcuts import get_current_site -from django.conf import settings from django.db import models from django.contrib.auth import get_user_model diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 0bb3c060beb..96b1064e77d 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -4,15 +4,16 @@ # SPDX-License-Identifier: MIT from django.contrib.auth import get_user_model -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist from django.conf import settings -from rest_framework import serializers -from dj_rest_auth.registration.serializers import RegisterSerializer from django.contrib.auth.models import User from django.utils.crypto import get_random_string + +from rest_framework import serializers +from dj_rest_auth.registration.serializers import RegisterSerializer from distutils.util import strtobool -from .models import Invitation, Membership, Organization from cvat.apps.engine.serializers import BasicUserSerializer +from .models import Invitation, Membership, Organization class OrganizationReadSerializer(serializers.ModelSerializer): owner = BasicUserSerializer(allow_null=True) diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 873db340f31..529af42e225 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -3,20 +3,23 @@ # # SPDX-License-Identifier: MIT +from django.utils.crypto import get_random_string +from django.db import transaction + from rest_framework import mixins, viewsets, status from rest_framework.permissions import SAFE_METHODS -from django.utils.crypto import get_random_string from rest_framework.permissions import AllowAny - -from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view -from cvat.apps.engine.mixins import PartialUpdateModelMixin from rest_framework.decorators import action from rest_framework.response import Response + +from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view + from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS from cvat.apps.organizations.throttle import ResendOrganizationInvitationThrottle -from django.db import transaction +from cvat.apps.engine.mixins import PartialUpdateModelMixin + from .models import Invitation, Membership, Organization from .serializers import ( From 8dec73f119b1415867ae1f081f3dc19678e44c79 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 2 Oct 2023 16:06:45 +0300 Subject: [PATCH 42/64] updated schema --- cvat/schema.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/schema.yml b/cvat/schema.yml index b02bcd3b0f1..4bcbe4bf1ad 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.8.0.dev20231002094801 + version: 2.8.0 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: From 6f82feaabed75266c050247a714695925703eeb5 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 10:26:20 +0300 Subject: [PATCH 43/64] throw explicit error if email backend is not configured --- cvat/apps/organizations/models.py | 4 ++++ cvat/apps/organizations/views.py | 13 +++++++++++++ cvat/settings/base.py | 5 +++++ 3 files changed, 22 insertions(+) diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index bc0a5651bce..8d84b56d5c5 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -10,6 +10,7 @@ from django.db import models from django.contrib.auth import get_user_model +from django.core.exceptions import ImproperlyConfigured from django.utils import timezone class Organization(models.Model): @@ -76,6 +77,9 @@ def organization_slug(self): return self.membership.organization.slug def send(self, request): + if settings.EMAIL_BACKEND is None: + raise ImproperlyConfigured("Email backend is not configured") + target_email = self.membership.user.email current_site = get_current_site(request) site_name = current_site.name diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 529af42e225..f2b87228458 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -5,6 +5,7 @@ from django.utils.crypto import get_random_string from django.db import transaction +from django.core.exceptions import ImproperlyConfigured from rest_framework import mixins, viewsets, status from rest_framework.permissions import SAFE_METHODS @@ -209,6 +210,16 @@ def get_queryset(self): permission = InvitationPermission.create_scope_list(self.request) return permission.filter(queryset) + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + try: + self.perform_create(serializer) + except ImproperlyConfigured: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data="Email backend is not configured.") + + return Response(serializer.data, status=status.HTTP_201_CREATED) + def perform_create(self, serializer): serializer.save( owner=self.request.user, @@ -250,3 +261,5 @@ def resend(self, request, pk): return Response(status=status.HTTP_200_OK, data="Invitation has been sent.") except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") + except ImproperlyConfigured: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data="Email backend is not configured.") diff --git a/cvat/settings/base.py b/cvat/settings/base.py index b7c62077f06..4736aac842b 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -700,3 +700,8 @@ class CVAT_QUEUES(Enum): SMOKESCREEN_ENABLED = True EXTRA_RULES_PATHS = [] + +# By default, email backend is django.core.mail.backends.smtp.EmailBackend +# But it won't work without additional configuration, so we set it to None +# to check configuration and throw ImproperlyConfigured if thats a case +EMAIL_BACKEND = None From c0e05f02c4d0f74e4655eea479271436b01cf900 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 10:35:27 +0300 Subject: [PATCH 44/64] removed rawuser, keep only serialized user --- cvat-core/src/organization.ts | 8 ++++---- cvat-core/src/server-response-types.ts | 1 + cvat-core/src/user.ts | 21 ++++----------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 19e302b4552..5bfba4059e9 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -3,24 +3,24 @@ // // SPDX-License-Identifier: MIT -import { SerializedOrganization, SerializedOrganizationContact } from './server-response-types'; +import { SerializedOrganization, SerializedOrganizationContact, SerializedUser } from './server-response-types'; import { checkObjectType, isEnum } from './common'; import config from './config'; import { MembershipRole } from './enums'; import { ArgumentError, DataError } from './exceptions'; import PluginRegistry from './plugins'; import serverProxy from './server-proxy'; -import User, { RawUserData } from './user'; +import User from './user'; interface SerializedInvitationData { created_date: string; key: string; - owner: RawUserData; + owner: SerializedUser; } interface SerializedMembershipData { id: number; - user: RawUserData; + user: SerializedUser; is_active: boolean; joined_date: string; role: MembershipRole; diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 6fca125b11d..24407f16f1b 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -50,6 +50,7 @@ export interface SerializedUser { is_active?: boolean; last_login?: string; date_joined?: string; + email_verification_required: boolean; } export interface SerializedProject { diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts index fc6da2cb999..1b0eb5ecfec 100644 --- a/cvat-core/src/user.ts +++ b/cvat-core/src/user.ts @@ -3,20 +3,7 @@ // // SPDX-License-Identifier: MIT -export interface RawUserData { - id: number; - username: string; - email: string; - first_name: string; - last_name: string; - groups: string[]; - last_login: string; - date_joined: string; - is_staff: boolean; - is_superuser: boolean; - is_active: boolean; - email_verification_required: boolean; -} +import { SerializedUser } from './server-response-types'; export default class User { public readonly id: number; @@ -24,7 +11,7 @@ export default class User { public readonly email: string; public readonly firstName: string; public readonly lastName: string; - public readonly groups: string[]; + public readonly groups: ('user' | 'business' | 'admin')[]; public readonly lastLogin: string; public readonly dateJoined: string; public readonly isStaff: boolean; @@ -32,7 +19,7 @@ export default class User { public readonly isActive: boolean; public readonly isVerified: boolean; - constructor(initialData: RawUserData) { + constructor(initialData: SerializedUser) { const data = { id: null, username: null, @@ -97,7 +84,7 @@ export default class User { ); } - serialize(): RawUserData { + serialize(): Partial { return { id: this.id, username: this.username, From 89aef3e9e7eb9d733b40d294699374024417409f Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 10:36:42 +0300 Subject: [PATCH 45/64] fixed memberships results --- cvat-core/src/organization.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 5bfba4059e9..821c050bbac 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -319,12 +319,12 @@ Object.defineProperties(Organization.prototype.members, { checkObjectType('pageSize', pageSize, 'number'); const result = await serverProxy.organizations.members(orgSlug, page, pageSize); - result.results = result.results.map((rawMembership: SerializedMembershipData) => new Membership( + const memeberships = result.results.map((rawMembership: SerializedMembershipData) => new Membership( rawMembership, )); - result.results.count = result.count; - return result.results; + memeberships.count = result.count; + return memeberships; }, }, }); From 19dce0dbf993ff97a222d41a15a34f04e6b95272 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 10:52:46 +0300 Subject: [PATCH 46/64] moved accept invitation to oranization namespace --- cvat-core/src/api-implementation.ts | 44 ++++++++++++++--------------- cvat-core/src/api.ts | 26 ++++++++--------- cvat-core/src/server-proxy.ts | 4 +-- cvat-ui/src/actions/auth-actions.ts | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 117ec0178b5..791513c5d65 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -106,28 +106,6 @@ export default function implementAPI(cvat) { await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token); }; - cvat.server.acceptInvitation.implementation = async ( - username, - firstName, - lastName, - email, - password, - userConfirmations, - key, - ) => { - const orgSlug = await serverProxy.server.acceptInvitation( - username, - firstName, - lastName, - email, - password, - userConfirmations, - key, - ); - - return orgSlug; - }; - cvat.server.authorized.implementation = async () => { const result = await serverProxy.server.authorized(); return result; @@ -363,6 +341,28 @@ export default function implementAPI(cvat) { }; }; + cvat.organizations.acceptInvitation.implementation = async ( + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ) => { + const orgSlug = await serverProxy.organizations.acceptInvitation( + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ); + + return orgSlug; + }; + cvat.webhooks.get.implementation = async (filter) => { checkFilter(filter, { page: isInteger, diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index f1b63c0a4dd..b6c079f8689 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -96,19 +96,6 @@ function build() { ); return result; }, - async acceptInvitation(username, firstName, lastName, email, password, userConfirmations, key) { - const result = await PluginRegistry.apiWrapper( - cvat.server.acceptInvitation, - username, - firstName, - lastName, - email, - password, - userConfirmations, - key, - ); - return result; - }, async authorized() { const result = await PluginRegistry.apiWrapper(cvat.server.authorized); return result; @@ -283,6 +270,19 @@ function build() { const result = await PluginRegistry.apiWrapper(cvat.organizations.deactivate); return result; }, + async acceptInvitation(username, firstName, lastName, email, password, userConfirmations, key) { + const result = await PluginRegistry.apiWrapper( + cvat.organizations.acceptInvitation, + username, + firstName, + lastName, + email, + password, + userConfirmations, + key, + ); + return result; + }, }, webhooks: { async get(filter: any) { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index c406d365c6f..a01db083608 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -454,7 +454,7 @@ async function resetPassword(newPassword1: string, newPassword2: string, uid: st } } -async function acceptInvitation( +async function acceptOrganizationInvitation( username: string, firstName: string, lastName: string, @@ -2410,7 +2410,6 @@ export default Object.freeze({ request: serverRequest, userAgreements, installedApps, - acceptInvitation, }), projects: Object.freeze({ @@ -2531,6 +2530,7 @@ export default Object.freeze({ deleteInvitation: deleteOrganizationInvitation, updateMembership: updateOrganizationMembership, deleteMembership: deleteOrganizationMembership, + acceptInvitation: acceptOrganizationInvitation, }), webhooks: Object.freeze({ diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 77c176b850e..8777b125828 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -228,7 +228,7 @@ export const acceptInvitationAsync = ( } = registerData; try { - const orgSlug = await cvat.server.acceptInvitation( + const orgSlug = await cvat.organizations.acceptInvitation( username, firstName, lastName, From 9b56f05d3f49b89992fc207003633fe214ff515f Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 11:41:36 +0300 Subject: [PATCH 47/64] updated email template --- .../templates/invitation/invitation_message.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index 4f4dc7726c0..b8df727101d 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -92,7 +92,8 @@ } a { - color: #1a82e2; + color: #ffffff; + text-decoration: none; } img { @@ -183,7 +184,7 @@

{% blocktrans %}

- You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT by {{ invitation_owner }} at {{ site_name }}. + You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT by {{ invitation_owner }} at {{ site_name }}.

To join organization and start annotating, simply tap the button below and complete registration. From 11114ce39c05a5e03e4e190015f9f68cca522dfb Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 11:53:41 +0300 Subject: [PATCH 48/64] removed delete invitation --- cvat-core/src/organization.ts | 22 -------------- cvat-core/src/server-proxy.ts | 10 ------- cvat-ui/src/actions/organization-actions.ts | 29 +------------------ .../organization-page/members-list.tsx | 8 ++--- cvat-ui/src/reducers/notifications-reducer.ts | 17 ----------- 5 files changed, 4 insertions(+), 82 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 821c050bbac..26c1c1bd4e7 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -269,15 +269,6 @@ export default class Organization { ); return result; } - - public async deleteInvitation(key: string): Promise { - const result = await PluginRegistry.apiWrapper.call( - this, - Organization.prototype.deleteInvitation, - key, - ); - return result; - } } Object.defineProperties(Organization.prototype.save, { @@ -430,16 +421,3 @@ Object.defineProperties(Organization.prototype.resendInvitation, { }, }, }); - -Object.defineProperties(Organization.prototype.deleteInvitation, { - implementation: { - writable: false, - enumerable: false, - value: async function implementation(key: string) { - checkObjectType('key', key, 'string'); - if (typeof this.id === 'number') { - await serverProxy.organizations.deleteInvitation(key); - } - }, - }, -}); diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index a01db083608..35deeda8711 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -2095,15 +2095,6 @@ async function resendOrganizationInvitation(key) { } } -async function deleteOrganizationInvitation(key) { - const { backendAPI } = config; - try { - await Axios.delete(`${backendAPI}/invitations/${key}`); - } catch (errorData) { - throw generateError(errorData); - } -} - async function updateOrganizationMembership(membershipId, data) { const { backendAPI } = config; let response = null; @@ -2527,7 +2518,6 @@ export default Object.freeze({ delete: deleteOrganization, invite: inviteOrganizationMembers, resendInvitation: resendOrganizationInvitation, - deleteInvitation: deleteOrganizationInvitation, updateMembership: updateOrganizationMembership, deleteMembership: deleteOrganizationMembership, acceptInvitation: acceptOrganizationInvitation, diff --git a/cvat-ui/src/actions/organization-actions.ts b/cvat-ui/src/actions/organization-actions.ts index 02acaa7005e..71ea5ba2bb6 100644 --- a/cvat-ui/src/actions/organization-actions.ts +++ b/cvat-ui/src/actions/organization-actions.ts @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -40,9 +41,6 @@ export enum OrganizationActionsTypes { RESEND_ORGANIZATION_INVITATION = 'RESEND_ORGANIZATION_INVITATION', RESEND_ORGANIZATION_INVITATION_SUCCESS = 'RESEND_ORGANIZATION_INVITATION_SUCCESS', RESEND_ORGANIZATION_INVITATION_FAILED = 'RESEND_ORGANIZATION_INVITATION_FAILED', - DELETE_ORGANIZATION_INVITATION = 'DELETE_ORGANIZATION_INVITATION', - DELETE_ORGANIZATION_INVITATION_SUCCESS = 'DELETE_ORGANIZATION_INVITATION_SUCCESS', - DELETE_ORGANIZATION_INVITATION_FAILED = 'DELETE_ORGANIZATION_INVITATION_FAILED', } const organizationActions = { @@ -110,13 +108,6 @@ const organizationActions = { resendOrganizationInvitationFailed: (error: any) => createAction( OrganizationActionsTypes.RESEND_ORGANIZATION_INVITATION_FAILED, { error }, ), - deleteOrganizationInvitation: () => createAction(OrganizationActionsTypes.DELETE_ORGANIZATION_INVITATION), - deleteOrganizationInvitationSuccess: () => createAction( - OrganizationActionsTypes.DELETE_ORGANIZATION_INVITATION_SUCCESS, - ), - deleteOrganizationInvitationFailed: (error: any) => createAction( - OrganizationActionsTypes.DELETE_ORGANIZATION_INVITATION_FAILED, { error }, - ), }; export function getOrganizationsAsync(): ThunkAction { @@ -302,22 +293,4 @@ export function resendOrganizationInvitationAsync( }; } -export function deleteOrganizationInvitationAsync( - organization: any, - invitationKey: string, - onFinish?: () => void, -): ThunkAction { - return async function (dispatch) { - dispatch(organizationActions.deleteOrganizationInvitation()); - - try { - await organization.deleteInvitation(invitationKey); - dispatch(organizationActions.deleteOrganizationInvitationSuccess()); - if (onFinish) onFinish(); - } catch (error) { - dispatch(organizationActions.deleteOrganizationInvitationFailed(error)); - } - }; -} - export type OrganizationActions = ActionUnion; diff --git a/cvat-ui/src/components/organization-page/members-list.tsx b/cvat-ui/src/components/organization-page/members-list.tsx index e470f3e0dbd..e995bb29f41 100644 --- a/cvat-ui/src/components/organization-page/members-list.tsx +++ b/cvat-ui/src/components/organization-page/members-list.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,7 +10,7 @@ import Spin from 'antd/lib/spin'; import { useDispatch, useSelector } from 'react-redux'; import { CombinedState } from 'reducers'; import { - deleteOrganizationInvitationAsync, removeOrganizationMemberAsync, + removeOrganizationMemberAsync, resendOrganizationInvitationAsync, updateOrganizationMemberAsync, } from 'actions/organization-actions'; import { Membership } from 'cvat-core-wrapper'; @@ -65,15 +66,12 @@ function MembersList(props: Props): JSX.Element { resendOrganizationInvitationAsync(organizationInstance, key), ); }} - onDeleteInvitation={(key: string) => { + onDeleteInvitation={() => { dispatch( removeOrganizationMemberAsync(organizationInstance, member, () => { fetchMembers(); }), ); - dispatch( - deleteOrganizationInvitationAsync(organizationInstance, key), - ); }} /> ), diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index ae6242a2350..f4621338ac0 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1657,23 +1657,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case OrganizationActionsTypes.DELETE_ORGANIZATION_INVITATION: { - return { - ...state, - errors: { - ...state.errors, - organizations: { - ...state.errors.organizations, - deletingInvitation: { - message: 'Could not delete invitation', - reason: action.payload.error, - shouldLog: !(action.payload.error instanceof ServerError), - className: 'cvat-notification-notice-delete-organization-invintation-failed', - }, - }, - }, - }; - } case OrganizationActionsTypes.RESEND_ORGANIZATION_INVITATION_SUCCESS: { return { ...state, From 74a9a7389563301c86ba84d33042f6ebe811cc8d Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 11:54:45 +0300 Subject: [PATCH 49/64] fixed typo --- .../accept-invitation-page.tsx | 2 +- .../src/components/register-page/register-form.tsx | 14 +++++++------- .../src/components/register-page/register-page.tsx | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index e6ce473ab1e..e0d7e10bd59 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -41,7 +41,7 @@ function AcceptInvitationPage(): JSX.Element { onRegister={onRegister} userAgreements={userAgreements} fetching={userAgreementsFetching || authFetching} - predifinedEmail={invitationParams.email} + predefinedEmail={invitationParams.email} disableNavigation /> ); diff --git a/cvat-ui/src/components/register-page/register-form.tsx b/cvat-ui/src/components/register-page/register-form.tsx index 7e76606c84e..9401d714742 100644 --- a/cvat-ui/src/components/register-page/register-form.tsx +++ b/cvat-ui/src/components/register-page/register-form.tsx @@ -34,7 +34,7 @@ export interface RegisterData { interface Props { fetching: boolean; userAgreements: UserAgreement[]; - predifinedEmail?: string; + predefinedEmail?: string; disableNavigation?: boolean; onSubmit(registerData: RegisterData): void; } @@ -102,11 +102,11 @@ const validateAgreement: ((userAgreements: UserAgreement[]) => RuleRender) = ( function RegisterFormComponent(props: Props): JSX.Element { const { - fetching, onSubmit, userAgreements, predifinedEmail, disableNavigation, + fetching, onSubmit, userAgreements, predefinedEmail, disableNavigation, } = props; const [form] = Form.useForm(); - if (predifinedEmail) { - form.setFieldsValue({ email: predifinedEmail }); + if (predefinedEmail) { + form.setFieldsValue({ email: predefinedEmail }); } const [usernameEdited, setUsernameEdited] = useState(false); return ( @@ -132,7 +132,7 @@ function RegisterFormComponent(props: Props): JSX.Element { onSubmit({ ...(Object.fromEntries(rest) as any as RegisterData), - ...(predifinedEmail ? { email: predifinedEmail } : {}), + ...(predefinedEmail ? { email: predefinedEmail } : {}), confirmations, }); }} @@ -198,8 +198,8 @@ function RegisterFormComponent(props: Props): JSX.Element { id='email' autoComplete='email' placeholder='Email' - value={predifinedEmail} - disabled={!!predifinedEmail} + value={predefinedEmail} + disabled={!!predefinedEmail} onReset={() => form.setFieldsValue({ email: '', username: '' })} onChange={(event) => { const { value } = event.target; diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 5b8872a8f57..8086aedf826 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -18,13 +18,13 @@ interface RegisterPageComponentProps { onRegister: ( registerData: RegisterData, ) => void; - predifinedEmail?: string; + predefinedEmail?: string; disableNavigation?: boolean; } function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps): JSX.Element { const { - fetching, userAgreements, onRegister, predifinedEmail, disableNavigation, + fetching, userAgreements, onRegister, predefinedEmail, disableNavigation, } = props; return ( @@ -35,7 +35,7 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen { onRegister(registerData); From 92d0e70218a349a2d7910771bd5e4bef741654c5 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 12:14:16 +0300 Subject: [PATCH 50/64] renamed disable navigation --- .../accept-invitation-page/accept-invitation-page.tsx | 2 +- cvat-ui/src/components/register-page/register-form.tsx | 6 +++--- cvat-ui/src/components/register-page/register-page.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index e0d7e10bd59..84e46fe1d0e 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -42,7 +42,7 @@ function AcceptInvitationPage(): JSX.Element { userAgreements={userAgreements} fetching={userAgreementsFetching || authFetching} predefinedEmail={invitationParams.email} - disableNavigation + hideLoginLink /> ); } diff --git a/cvat-ui/src/components/register-page/register-form.tsx b/cvat-ui/src/components/register-page/register-form.tsx index 9401d714742..16ee9058057 100644 --- a/cvat-ui/src/components/register-page/register-form.tsx +++ b/cvat-ui/src/components/register-page/register-form.tsx @@ -35,7 +35,7 @@ interface Props { fetching: boolean; userAgreements: UserAgreement[]; predefinedEmail?: string; - disableNavigation?: boolean; + hideLoginLink?: boolean; onSubmit(registerData: RegisterData): void; } @@ -102,7 +102,7 @@ const validateAgreement: ((userAgreements: UserAgreement[]) => RuleRender) = ( function RegisterFormComponent(props: Props): JSX.Element { const { - fetching, onSubmit, userAgreements, predefinedEmail, disableNavigation, + fetching, onSubmit, userAgreements, predefinedEmail, hideLoginLink, } = props; const [form] = Form.useForm(); if (predefinedEmail) { @@ -112,7 +112,7 @@ function RegisterFormComponent(props: Props): JSX.Element { return (

{ - !disableNavigation && ( + !hideLoginLink && ( diff --git a/cvat-ui/src/components/register-page/register-page.tsx b/cvat-ui/src/components/register-page/register-page.tsx index 8086aedf826..3fd58cab5d2 100644 --- a/cvat-ui/src/components/register-page/register-page.tsx +++ b/cvat-ui/src/components/register-page/register-page.tsx @@ -19,12 +19,12 @@ interface RegisterPageComponentProps { registerData: RegisterData, ) => void; predefinedEmail?: string; - disableNavigation?: boolean; + hideLoginLink?: boolean; } function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps): JSX.Element { const { - fetching, userAgreements, onRegister, predefinedEmail, disableNavigation, + fetching, userAgreements, onRegister, predefinedEmail, hideLoginLink, } = props; return ( @@ -36,7 +36,7 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen fetching={fetching} userAgreements={userAgreements} predefinedEmail={predefinedEmail} - disableNavigation={disableNavigation} + hideLoginLink={hideLoginLink} onSubmit={(registerData: RegisterData): void => { onRegister(registerData); }} From e0bedafa21a3cc135d5f2578527552d8d4fc7b19 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 12:21:10 +0300 Subject: [PATCH 51/64] removed accept organization param --- .../accept-invitation-page/accept-invitation-page.tsx | 3 ++- cvat-ui/src/components/cvat-app.tsx | 2 -- cvat-ui/src/components/watchers/organization-watcher.tsx | 7 ------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx index 84e46fe1d0e..bd0c534c08b 100644 --- a/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx +++ b/cvat-ui/src/components/accept-invitation-page/accept-invitation-page.tsx @@ -31,7 +31,8 @@ function AcceptInvitationPage(): JSX.Element { registerData, invitationParams.key, (orgSlug: string) => { - history.replace(`/auth/login?next=/tasks&activateOrganization=${orgSlug}`); + localStorage.setItem('currentOrganization', orgSlug); + history.replace('/auth/login?next=/tasks'); }, )); }, [dispatch]); diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 1bc15a658f1..f0dc2893bce 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -496,8 +496,6 @@ class CVATApplication extends React.PureComponent diff --git a/cvat-ui/src/components/watchers/organization-watcher.tsx b/cvat-ui/src/components/watchers/organization-watcher.tsx index 9ae0d6c85b9..6939b4098b7 100644 --- a/cvat-ui/src/components/watchers/organization-watcher.tsx +++ b/cvat-ui/src/components/watchers/organization-watcher.tsx @@ -5,15 +5,12 @@ import { Organization, getCore } from 'cvat-core-wrapper'; import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; import { CombinedState } from 'reducers'; const core = getCore(); function OrganizationWatcher(): JSX.Element { const organizationList = useSelector((state: CombinedState) => state.organizations.list); - const history = useHistory(); - const queryParams = new URLSearchParams(history.location.search); const changeOrganization = (newOrg: number | string | null, location?: string): void => { let newOrganization: Organization | null = null; @@ -44,10 +41,6 @@ function OrganizationWatcher(): JSX.Element { core.config.onOrganizationChange = (newOrgId: number | null) => { changeOrganization(newOrgId); }; - if (queryParams.get('activateOrganization')) { - const orgSlug = queryParams.get('activateOrganization'); - changeOrganization(orgSlug, '/tasks'); - } }, []); return <>; From 93a818b27e716ffc8767c2041d0337fece355e02 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 15:13:05 +0300 Subject: [PATCH 52/64] fixed tag in template --- .../organizations/templates/invitation/invitation_message.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/organizations/templates/invitation/invitation_message.html b/cvat/apps/organizations/templates/invitation/invitation_message.html index b8df727101d..b511462958d 100644 --- a/cvat/apps/organizations/templates/invitation/invitation_message.html +++ b/cvat/apps/organizations/templates/invitation/invitation_message.html @@ -184,7 +184,7 @@

{% blocktrans %}

- You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT by {{ invitation_owner }} at {{ site_name }}. + You're receiving this email because you've been invited to join {{ organization_name }} organization in CVAT by {{ invitation_owner }} at {{ site_name }}.

To join organization and start annotating, simply tap the button below and complete registration. From 74ddfdca827ba8e6dcc9b3b9c28fdef987b6af76 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 5 Oct 2023 15:39:11 +0300 Subject: [PATCH 53/64] fixed owner null --- cvat-core/src/organization.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 26c1c1bd4e7..f063262d41a 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -29,16 +29,16 @@ interface SerializedMembershipData { export class Invitation { #createdDate: string; - #owner: User; + #owner: User | null; #key: string; constructor(initialData: SerializedInvitationData) { this.#createdDate = initialData.created_date; - this.#owner = new User(initialData.owner); + this.#owner = initialData.owner ? new User(initialData.owner) : null; this.#key = initialData.key; } - get owner(): User { + get owner(): User | null { return this.#owner; } @@ -310,12 +310,12 @@ Object.defineProperties(Organization.prototype.members, { checkObjectType('pageSize', pageSize, 'number'); const result = await serverProxy.organizations.members(orgSlug, page, pageSize); - const memeberships = result.results.map((rawMembership: SerializedMembershipData) => new Membership( + const memberships = result.results.map((rawMembership: SerializedMembershipData) => new Membership( rawMembership, )); - memeberships.count = result.count; - return memeberships; + memberships.count = result.count; + return memberships; }, }, }); From cb6bbafc474834fb4c3e8c2cf413234a7857833a Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 6 Oct 2023 11:54:12 +0300 Subject: [PATCH 54/64] updated schema --- cvat/apps/organizations/serializers.py | 3 + cvat/apps/organizations/views.py | 19 +++++- cvat/schema.yml | 84 +++++++++++++------------- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 96b1064e77d..e82739cb799 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -149,6 +149,9 @@ class Meta: read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] class AcceptInvitationSerializer(RegisterSerializer): + first_name = serializers.CharField(required=False) + last_name = serializers.CharField(required=False) + def validate_email(self, email): return email diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index f2b87228458..075c941c6e5 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -14,6 +14,7 @@ from rest_framework.response import Response from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view +from drf_spectacular.types import OpenApiTypes from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) @@ -178,7 +179,23 @@ def get_queryset(self): summary='Method deletes an invitation', responses={ '204': OpenApiResponse(description='The invitation has been deleted'), - }) + }), + accept=extend_schema( + operation_id='invitations_accept', + summary='Method registers user and accepts invitation to organization', + request=AcceptInvitationSerializer, + responses={ + '200': OpenApiResponse(response=OpenApiTypes.STR, description='Organization slug'), + '400': OpenApiResponse(description='The invitation is expired or already accepted'), + }), + resend=extend_schema( + operation_id='invitations_resend', + summary='Method resends the invitation', + request=None, + responses={ + '200': OpenApiResponse(description='Invitation has been sent'), + '400': OpenApiResponse(description='The invitation is already accepted'), + }), ) class InvitationViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin, diff --git a/cvat/schema.yml b/cvat/schema.yml index 4bcbe4bf1ad..59a69700853 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.8.0 + version: 2.8.0.dev20231005123911 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: @@ -1533,7 +1533,8 @@ paths: description: The invitation has been deleted /api/invitations/{key}/accept: post: - operationId: invitations_create_accept + operationId: invitations_accept + summary: Method registers user and accepts invitation to organization parameters: - in: path name: key @@ -1547,7 +1548,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/InvitationWriteRequest' + $ref: '#/components/schemas/AcceptInvitationRequest' required: true security: - {} @@ -1556,11 +1557,14 @@ paths: content: application/vnd.cvat+json: schema: - $ref: '#/components/schemas/InvitationWrite' - description: '' + type: string + description: Organization slug + '400': + description: The invitation is expired or already accepted /api/invitations/{key}/resend: post: - operationId: invitations_create_resend + operationId: invitations_resend + summary: Method resends the invitation parameters: - in: path name: key @@ -1570,12 +1574,6 @@ paths: required: true tags: - invitations - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/InvitationWriteRequest' - required: true security: - sessionAuth: [] csrfAuth: [] @@ -1584,11 +1582,9 @@ paths: - basicAuth: [] responses: '200': - content: - application/vnd.cvat+json: - schema: - $ref: '#/components/schemas/InvitationWrite' - description: '' + description: Invitation has been sent + '400': + description: The invitation is already accepted /api/issues: get: operationId: issues_list @@ -6158,6 +6154,35 @@ components: - description - name - version + AcceptInvitationRequest: + type: object + properties: + username: + type: string + minLength: 5 + maxLength: 150 + email: + type: string + format: email + minLength: 1 + password1: + type: string + writeOnly: true + minLength: 1 + password2: + type: string + writeOnly: true + minLength: 1 + first_name: + type: string + minLength: 1 + last_name: + type: string + minLength: 1 + required: + - password1 + - password2 + - username AnalyticsReport: type: object properties: @@ -7379,31 +7404,6 @@ components: - owner - role - user - InvitationWrite: - type: object - properties: - key: - type: string - readOnly: true - created_date: - type: string - format: date-time - readOnly: true - owner: - type: integer - readOnly: true - nullable: true - role: - $ref: '#/components/schemas/RoleEnum' - organization: - type: integer - readOnly: true - email: - type: string - format: email - required: - - email - - role InvitationWriteRequest: type: object properties: From aec27634e6907d3d9d9521e5bc30de504f6e5a0d Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 6 Oct 2023 12:26:43 +0300 Subject: [PATCH 55/64] updated tests db --- tests/python/shared/assets/memberships.json | 156 ++++++++++++++++++-- 1 file changed, 143 insertions(+), 13 deletions(-) diff --git a/tests/python/shared/assets/memberships.json b/tests/python/shared/assets/memberships.json index 9ae6bcc8d95..038a86dcf0e 100644 --- a/tests/python/shared/assets/memberships.json +++ b/tests/python/shared/assets/memberships.json @@ -5,7 +5,17 @@ "results": [ { "id": 15, - "invitation": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX", + "invitation": { + "created_date": "2023-09-15T07:53:52.116000Z", + "key": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX", + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + } + }, "is_active": true, "joined_date": "2023-09-15T07:53:52.116000Z", "organization": 1, @@ -20,7 +30,17 @@ }, { "id": 14, - "invitation": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR", + "invitation": { + "created_date": "2023-09-15T07:53:52.115000Z", + "key": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR", + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + } + }, "is_active": true, "joined_date": "2023-09-15T07:53:52.115000Z", "organization": 1, @@ -35,7 +55,17 @@ }, { "id": 13, - "invitation": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "invitation": { + "created_date": "2022-09-28T13:11:37.839000Z", + "key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", + "owner": { + "first_name": "Admin", + "id": 1, + "last_name": "First", + "url": "http://localhost:8080/api/users/1", + "username": "admin1" + } + }, "is_active": true, "joined_date": "2022-09-28T13:11:37.839000Z", "organization": 1, @@ -50,7 +80,17 @@ }, { "id": 12, - "invitation": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", + "invitation": { + "created_date": "2022-02-24T21:29:21.978000Z", + "key": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", + "owner": { + "first_name": "Admin", + "id": 1, + "last_name": "First", + "url": "http://localhost:8080/api/users/1", + "username": "admin1" + } + }, "is_active": true, "joined_date": "2022-02-24T21:29:21.978000Z", "organization": 2, @@ -65,7 +105,17 @@ }, { "id": 11, - "invitation": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv", + "invitation": { + "created_date": "2022-01-19T13:54:42.015000Z", + "key": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2022-01-19T13:54:42.015000Z", "organization": 2, @@ -80,7 +130,17 @@ }, { "id": 10, - "invitation": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1", + "invitation": { + "created_date": "2022-01-19T13:54:42.005000Z", + "key": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2022-01-19T13:54:42.005000Z", "organization": 2, @@ -95,7 +155,17 @@ }, { "id": 9, - "invitation": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1", + "invitation": { + "created_date": "2021-12-14T19:55:13.745000Z", + "key": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2021-12-14T19:55:13.745000Z", "organization": 2, @@ -110,7 +180,17 @@ }, { "id": 8, - "invitation": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS", + "invitation": { + "created_date": "2021-12-14T19:54:56.431000Z", + "key": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2021-12-14T19:54:56.431000Z", "organization": 2, @@ -125,7 +205,17 @@ }, { "id": 7, - "invitation": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH", + "invitation": { + "created_date": "2021-12-14T19:54:46.172000Z", + "key": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2021-12-14T19:54:46.172000Z", "organization": 2, @@ -140,7 +230,17 @@ }, { "id": 6, - "invitation": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu", + "invitation": { + "created_date": "2021-12-14T19:54:33.591000Z", + "key": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu", + "owner": { + "first_name": "Business", + "id": 10, + "last_name": "First", + "url": "http://localhost:8080/api/users/10", + "username": "business1" + } + }, "is_active": true, "joined_date": "2021-12-14T19:54:33.591000Z", "organization": 2, @@ -170,7 +270,17 @@ }, { "id": 4, - "invitation": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn", + "invitation": { + "created_date": "2021-12-14T18:48:46.579000Z", + "key": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn", + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + } + }, "is_active": true, "joined_date": "2021-12-14T18:48:46.579000Z", "organization": 1, @@ -185,7 +295,17 @@ }, { "id": 3, - "invitation": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej", + "invitation": { + "created_date": "2021-12-14T18:47:49.322000Z", + "key": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej", + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + } + }, "is_active": true, "joined_date": "2021-12-14T18:47:49.322000Z", "organization": 1, @@ -200,7 +320,17 @@ }, { "id": 2, - "invitation": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr", + "invitation": { + "created_date": "2021-12-14T18:47:39.935000Z", + "key": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr", + "owner": { + "first_name": "User", + "id": 2, + "last_name": "First", + "url": "http://localhost:8080/api/users/2", + "username": "user1" + } + }, "is_active": true, "joined_date": "2021-12-14T18:47:39.935000Z", "organization": 1, From 0910c8d5f24af0727bb10d28af2208b70335b6a1 Mon Sep 17 00:00:00 2001 From: klakhov Date: Fri, 6 Oct 2023 12:30:50 +0300 Subject: [PATCH 56/64] fixed schema version --- cvat/schema.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/schema.yml b/cvat/schema.yml index 59a69700853..4eab51956f2 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.8.0.dev20231005123911 + version: 2.8.0 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: From faf4ace9ac1e469daaf51cf0197eedea7bb0c95b Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 9 Oct 2023 09:27:59 +0300 Subject: [PATCH 57/64] updated package versions & changelog --- changelog.d/20231009_092646_invite_users.md | 4 ++++ cvat-core/package.json | 2 +- cvat-ui/package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/20231009_092646_invite_users.md diff --git a/changelog.d/20231009_092646_invite_users.md b/changelog.d/20231009_092646_invite_users.md new file mode 100644 index 00000000000..b328dc66f9e --- /dev/null +++ b/changelog.d/20231009_092646_invite_users.md @@ -0,0 +1,4 @@ +### Added + +- Invite users to organization by email + () \ No newline at end of file diff --git a/cvat-core/package.json b/cvat-core/package.json index 7449b052113..264e6d0d23f 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "11.1.0", + "version": "11.2.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 4fbfa49cd72..f8d60f75383 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.57.2", + "version": "1.58.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { From 47343de4d2c698f4a9af186f3397d745f71b6b08 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 9 Oct 2023 09:36:53 +0300 Subject: [PATCH 58/64] added newline --- changelog.d/20231009_092646_invite_users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20231009_092646_invite_users.md b/changelog.d/20231009_092646_invite_users.md index b328dc66f9e..53a2d7ebf2b 100644 --- a/changelog.d/20231009_092646_invite_users.md +++ b/changelog.d/20231009_092646_invite_users.md @@ -1,4 +1,4 @@ ### Added - Invite users to organization by email - () \ No newline at end of file + () From cd86e3a7bf0b675f240c08be6c2125db74f5d13f Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 9 Oct 2023 15:41:54 +0300 Subject: [PATCH 59/64] updated accept invitation serializer --- cvat/apps/organizations/serializers.py | 25 ++++++++----------------- cvat/apps/organizations/views.py | 1 + cvat/schema.yml | 4 ---- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index e82739cb799..24ca6f24383 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -10,9 +10,9 @@ from django.utils.crypto import get_random_string from rest_framework import serializers -from dj_rest_auth.registration.serializers import RegisterSerializer from distutils.util import strtobool from cvat.apps.engine.serializers import BasicUserSerializer +from cvat.apps.iam.serializers import RegisterSerializerEx from .models import Invitation, Membership, Organization class OrganizationReadSerializer(serializers.ModelSerializer): @@ -148,27 +148,18 @@ class Meta: fields = ['id', 'user', 'organization', 'is_active', 'joined_date', 'role'] read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] -class AcceptInvitationSerializer(RegisterSerializer): - first_name = serializers.CharField(required=False) - last_name = serializers.CharField(required=False) - - def validate_email(self, email): - return email - - def get_cleaned_data(self): - return { - 'username': self.validated_data.get('username', ''), - 'password1': self.validated_data.get('password1', ''), - 'firstname': self.validated_data.get('firstname', ''), - 'lastname': self.validated_data.get('lastname', ''), - } +class AcceptInvitationSerializer(RegisterSerializerEx): + def get_fields(self): + fields = super().get_fields() + fields.pop('email', default=None) + return fields def save(self, request, invitation): self.cleaned_data = self.get_cleaned_data() user = invitation.membership.user user.is_active = True - user.first_name = self.cleaned_data['firstname'] - user.last_name = self.cleaned_data['lastname'] + user.first_name = self.cleaned_data['first_name'] + user.last_name = self.cleaned_data['last_name'] user.username = self.cleaned_data['username'] user.set_password(self.cleaned_data['password1']) user.save() diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 075c941c6e5..f519a317e07 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -261,6 +261,7 @@ def accept(self, request, pk): if invitation.membership.is_active: return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is already accepted.") serializer = AcceptInvitationSerializer(data=request.data) + # serializer = RegisterSerializerEx(data=request.data) if serializer.is_valid(raise_exception=True): serializer.save(request, invitation) invitation.accept() diff --git a/cvat/schema.yml b/cvat/schema.yml index 4eab51956f2..a489d246842 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -6161,10 +6161,6 @@ components: type: string minLength: 5 maxLength: 150 - email: - type: string - format: email - minLength: 1 password1: type: string writeOnly: true From 2acb93268a137fa53543acc4839ab1ea859912dd Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 9 Oct 2023 15:46:16 +0300 Subject: [PATCH 60/64] updated status code & fixed typo --- cvat-ui/src/reducers/notifications-reducer.ts | 2 +- cvat/apps/organizations/views.py | 4 ++-- cvat/schema.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index f4621338ac0..40a2e5405b6 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1664,7 +1664,7 @@ export default function (state = defaultState, action: AnyAction): Notifications ...state.messages, organizations: { ...state.messages.organizations, - resendingInvitation: 'Invintation was sent suces', + resendingInvitation: 'Invintation was sent sucessfully', }, }, }; diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index f519a317e07..499ac4c2f6e 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -193,7 +193,7 @@ def get_queryset(self): summary='Method resends the invitation', request=None, responses={ - '200': OpenApiResponse(description='Invitation has been sent'), + '204': OpenApiResponse(description='Invitation has been sent'), '400': OpenApiResponse(description='The invitation is already accepted'), }), ) @@ -276,7 +276,7 @@ def resend(self, request, pk): if invitation.membership.is_active: return Response(status=status.HTTP_400_BAD_REQUEST, data="This invitation is already accepted.") invitation.send(request) - return Response(status=status.HTTP_200_OK, data="Invitation has been sent.") + return Response(status=status.HTTP_204_NO_CONTENT) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist.") except ImproperlyConfigured: diff --git a/cvat/schema.yml b/cvat/schema.yml index a489d246842..4b8e776e535 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1581,7 +1581,7 @@ paths: - signatureAuth: [] - basicAuth: [] responses: - '200': + '204': description: Invitation has been sent '400': description: The invitation is already accepted From c30588f75006b49fb8b770f557ba811aa2093a9f Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 10 Oct 2023 10:15:07 +0300 Subject: [PATCH 61/64] reverted invitation field --- cvat-core/src/organization.ts | 17 ++++++++++++++--- cvat/apps/organizations/serializers.py | 9 +-------- cvat/schema.yml | 21 +++------------------ 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index f063262d41a..ba05f9bcfbd 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -310,9 +310,20 @@ Object.defineProperties(Organization.prototype.members, { checkObjectType('pageSize', pageSize, 'number'); const result = await serverProxy.organizations.members(orgSlug, page, pageSize); - const memberships = result.results.map((rawMembership: SerializedMembershipData) => new Membership( - rawMembership, - )); + const memberships = await Promise.all(result.results.map(async (rawMembership) => { + const { invitation } = rawMembership; + let rawInvitation = null; + if (invitation) { + try { + rawInvitation = await serverProxy.organizations.invitation(invitation); + // eslint-disable-next-line no-empty + } catch (e) {} + } + return new Membership({ + ...rawMembership, + invitation: rawInvitation, + }); + })); memberships.count = result.count; return memberships; diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index 24ca6f24383..a047f2924c5 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -62,13 +62,6 @@ class Meta: fields = ['key', 'created_date', 'owner', 'role', 'user', 'organization'] read_only_fields = fields -class BasicInvitationSerializer(serializers.ModelSerializer): - owner = BasicUserSerializer(allow_null=True) - class Meta: - model = Invitation - fields = ['key', 'created_date', 'owner'] - read_only_fields = fields - class InvitationWriteSerializer(serializers.ModelSerializer): role = serializers.ChoiceField(Membership.role.field.choices, source='membership.role') @@ -126,7 +119,7 @@ def save(self, request, **kwargs): class MembershipReadSerializer(serializers.ModelSerializer): user = BasicUserSerializer() - invitation = BasicInvitationSerializer() + class Meta: model = Membership fields = ['id', 'user', 'organization', 'is_active', 'joined_date', 'role', diff --git a/cvat/schema.yml b/cvat/schema.yml index 4b8e776e535..4583f2b838d 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -6441,22 +6441,6 @@ components: oneOf: - $ref: '#/components/schemas/ProjectFileRequest' nullable: true - BasicInvitation: - type: object - properties: - key: - type: string - readOnly: true - created_date: - type: string - format: date-time - readOnly: true - owner: - allOf: - - $ref: '#/components/schemas/BasicUser' - nullable: true - required: - - owner BasicUser: type: object properties: @@ -8058,9 +8042,10 @@ components: - $ref: '#/components/schemas/RoleEnum' readOnly: true invitation: - $ref: '#/components/schemas/BasicInvitation' + type: string + readOnly: true + nullable: true required: - - invitation - user MetaUser: anyOf: From 0627576f55ddc5c97814ee0b2f69b043646e7e72 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 10 Oct 2023 10:32:21 +0300 Subject: [PATCH 62/64] return object instead of string --- cvat/apps/organizations/serializers.py | 5 ++++- cvat/apps/organizations/views.py | 15 +++++++-------- cvat/schema.yml | 15 +++++++++++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py index a047f2924c5..25b37e0a84c 100644 --- a/cvat/apps/organizations/serializers.py +++ b/cvat/apps/organizations/serializers.py @@ -141,7 +141,7 @@ class Meta: fields = ['id', 'user', 'organization', 'is_active', 'joined_date', 'role'] read_only_fields = ['user', 'organization', 'is_active', 'joined_date'] -class AcceptInvitationSerializer(RegisterSerializerEx): +class AcceptInvitationWriteSerializer(RegisterSerializerEx): def get_fields(self): fields = super().get_fields() fields.pop('email', default=None) @@ -157,3 +157,6 @@ def save(self, request, invitation): user.set_password(self.cleaned_data['password1']) user.save() return user + +class AcceptInvitationReadSerializer(serializers.Serializer): + organization_slug = serializers.CharField() diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 499ac4c2f6e..a5060f65321 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -14,7 +14,6 @@ from rest_framework.response import Response from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view -from drf_spectacular.types import OpenApiTypes from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) @@ -28,7 +27,7 @@ InvitationReadSerializer, InvitationWriteSerializer, MembershipReadSerializer, MembershipWriteSerializer, OrganizationReadSerializer, OrganizationWriteSerializer, - AcceptInvitationSerializer) + AcceptInvitationReadSerializer, AcceptInvitationWriteSerializer) @extend_schema(tags=['organizations']) @extend_schema_view( @@ -183,9 +182,9 @@ def get_queryset(self): accept=extend_schema( operation_id='invitations_accept', summary='Method registers user and accepts invitation to organization', - request=AcceptInvitationSerializer, + request=AcceptInvitationWriteSerializer, responses={ - '200': OpenApiResponse(response=OpenApiTypes.STR, description='Organization slug'), + '200': OpenApiResponse(response=AcceptInvitationReadSerializer, description='The invitation is accepted'), '400': OpenApiResponse(description='The invitation is expired or already accepted'), }), resend=extend_schema( @@ -252,7 +251,7 @@ def perform_update(self, serializer): super().perform_update(serializer) @transaction.atomic - @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[], serializer_class=AcceptInvitationSerializer) + @action(detail=True, methods=['POST'], url_path='accept', permission_classes=[AllowAny], authentication_classes=[]) def accept(self, request, pk): try: invitation = Invitation.objects.get(key=pk) @@ -260,12 +259,12 @@ def accept(self, request, pk): return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is expired. Please contact organization owner to renew it.") if invitation.membership.is_active: return Response(status=status.HTTP_400_BAD_REQUEST, data="Your invitation is already accepted.") - serializer = AcceptInvitationSerializer(data=request.data) - # serializer = RegisterSerializerEx(data=request.data) + serializer = AcceptInvitationWriteSerializer(data=request.data) if serializer.is_valid(raise_exception=True): serializer.save(request, invitation) invitation.accept() - return Response(status=status.HTTP_200_OK, data=invitation.membership.organization.slug) + response_serializer = AcceptInvitationReadSerializer(data={'oranization_slug': invitation.membership.organization.slug}); + return Response(status=status.HTTP_200_OK, data=response_serializer.data) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist. Please contact organization owner.") diff --git a/cvat/schema.yml b/cvat/schema.yml index 4583f2b838d..1e8bf784435 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1548,7 +1548,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AcceptInvitationRequest' + $ref: '#/components/schemas/AcceptInvitationWriteRequest' required: true security: - {} @@ -1557,8 +1557,8 @@ paths: content: application/vnd.cvat+json: schema: - type: string - description: Organization slug + $ref: '#/components/schemas/AcceptInvitationRead' + description: The invitation is accepted '400': description: The invitation is expired or already accepted /api/invitations/{key}/resend: @@ -6154,7 +6154,14 @@ components: - description - name - version - AcceptInvitationRequest: + AcceptInvitationRead: + type: object + properties: + organization_slug: + type: string + required: + - organization_slug + AcceptInvitationWriteRequest: type: object properties: username: From 852bb54450e542269313a6c30da6be74348e1ff4 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 10 Oct 2023 10:48:09 +0300 Subject: [PATCH 63/64] updated ui to object response --- cvat-core/src/server-proxy.ts | 9 +++++---- cvat-core/src/server-response-types.ts | 4 ++++ cvat/apps/organizations/views.py | 5 +++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 35deeda8711..c02742b4415 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -13,7 +13,7 @@ import { SerializedLabel, SerializedAnnotationFormats, ProjectsFilter, SerializedProject, SerializedTask, TasksFilter, SerializedUser, SerializedOrganization, SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, FunctionsResponseBody, - SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, + SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, SerializedAcceptInvitation, } from './server-response-types'; import { SerializedQualityReportData } from './quality-report'; import { SerializedAnalyticsReport } from './analytics-report'; @@ -462,9 +462,9 @@ async function acceptOrganizationInvitation( password: string, confirmations: Record, key: string, -): Promise { +): Promise { let response = null; - + let orgSlug = null; try { response = await Axios.post(`${config.backendAPI}/invitations/${key}/accept`, { username, @@ -474,11 +474,12 @@ async function acceptOrganizationInvitation( password2: password, confirmations, }); + orgSlug = response.data.organization_slug; } catch (errorData) { throw generateError(errorData); } - return response.data; + return orgSlug; } async function getSelf(): Promise { diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 24407f16f1b..2596026c8e8 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -182,6 +182,10 @@ export interface SerializedRegister { username: string; } +export interface SerializedAcceptInvitation { + organization_slug: string; +} + export interface SerializedGuide { id?: number; task_id: number | null; diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index a5060f65321..cc677c5503d 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -263,8 +263,9 @@ def accept(self, request, pk): if serializer.is_valid(raise_exception=True): serializer.save(request, invitation) invitation.accept() - response_serializer = AcceptInvitationReadSerializer(data={'oranization_slug': invitation.membership.organization.slug}); - return Response(status=status.HTTP_200_OK, data=response_serializer.data) + response_serializer = AcceptInvitationReadSerializer(data={'organization_slug': invitation.membership.organization.slug}) + if response_serializer.is_valid(raise_exception=True): + return Response(status=status.HTTP_200_OK, data=response_serializer.data) except Invitation.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data="This invitation does not exist. Please contact organization owner.") From eeea9457124f15a6dfe348c750a5a67be4084822 Mon Sep 17 00:00:00 2001 From: klakhov Date: Tue, 10 Oct 2023 11:27:47 +0300 Subject: [PATCH 64/64] reverted test assets --- tests/python/shared/assets/memberships.json | 156 ++------------------ 1 file changed, 13 insertions(+), 143 deletions(-) diff --git a/tests/python/shared/assets/memberships.json b/tests/python/shared/assets/memberships.json index 038a86dcf0e..9ae6bcc8d95 100644 --- a/tests/python/shared/assets/memberships.json +++ b/tests/python/shared/assets/memberships.json @@ -5,17 +5,7 @@ "results": [ { "id": 15, - "invitation": { - "created_date": "2023-09-15T07:53:52.116000Z", - "key": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX", - "owner": { - "first_name": "User", - "id": 2, - "last_name": "First", - "url": "http://localhost:8080/api/users/2", - "username": "user1" - } - }, + "invitation": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX", "is_active": true, "joined_date": "2023-09-15T07:53:52.116000Z", "organization": 1, @@ -30,17 +20,7 @@ }, { "id": 14, - "invitation": { - "created_date": "2023-09-15T07:53:52.115000Z", - "key": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR", - "owner": { - "first_name": "User", - "id": 2, - "last_name": "First", - "url": "http://localhost:8080/api/users/2", - "username": "user1" - } - }, + "invitation": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR", "is_active": true, "joined_date": "2023-09-15T07:53:52.115000Z", "organization": 1, @@ -55,17 +35,7 @@ }, { "id": 13, - "invitation": { - "created_date": "2022-09-28T13:11:37.839000Z", - "key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", - "owner": { - "first_name": "Admin", - "id": 1, - "last_name": "First", - "url": "http://localhost:8080/api/users/1", - "username": "admin1" - } - }, + "invitation": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9", "is_active": true, "joined_date": "2022-09-28T13:11:37.839000Z", "organization": 1, @@ -80,17 +50,7 @@ }, { "id": 12, - "invitation": { - "created_date": "2022-02-24T21:29:21.978000Z", - "key": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", - "owner": { - "first_name": "Admin", - "id": 1, - "last_name": "First", - "url": "http://localhost:8080/api/users/1", - "username": "admin1" - } - }, + "invitation": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql", "is_active": true, "joined_date": "2022-02-24T21:29:21.978000Z", "organization": 2, @@ -105,17 +65,7 @@ }, { "id": 11, - "invitation": { - "created_date": "2022-01-19T13:54:42.015000Z", - "key": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv", "is_active": true, "joined_date": "2022-01-19T13:54:42.015000Z", "organization": 2, @@ -130,17 +80,7 @@ }, { "id": 10, - "invitation": { - "created_date": "2022-01-19T13:54:42.005000Z", - "key": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1", "is_active": true, "joined_date": "2022-01-19T13:54:42.005000Z", "organization": 2, @@ -155,17 +95,7 @@ }, { "id": 9, - "invitation": { - "created_date": "2021-12-14T19:55:13.745000Z", - "key": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1", "is_active": true, "joined_date": "2021-12-14T19:55:13.745000Z", "organization": 2, @@ -180,17 +110,7 @@ }, { "id": 8, - "invitation": { - "created_date": "2021-12-14T19:54:56.431000Z", - "key": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS", "is_active": true, "joined_date": "2021-12-14T19:54:56.431000Z", "organization": 2, @@ -205,17 +125,7 @@ }, { "id": 7, - "invitation": { - "created_date": "2021-12-14T19:54:46.172000Z", - "key": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH", "is_active": true, "joined_date": "2021-12-14T19:54:46.172000Z", "organization": 2, @@ -230,17 +140,7 @@ }, { "id": 6, - "invitation": { - "created_date": "2021-12-14T19:54:33.591000Z", - "key": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu", - "owner": { - "first_name": "Business", - "id": 10, - "last_name": "First", - "url": "http://localhost:8080/api/users/10", - "username": "business1" - } - }, + "invitation": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu", "is_active": true, "joined_date": "2021-12-14T19:54:33.591000Z", "organization": 2, @@ -270,17 +170,7 @@ }, { "id": 4, - "invitation": { - "created_date": "2021-12-14T18:48:46.579000Z", - "key": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn", - "owner": { - "first_name": "User", - "id": 2, - "last_name": "First", - "url": "http://localhost:8080/api/users/2", - "username": "user1" - } - }, + "invitation": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn", "is_active": true, "joined_date": "2021-12-14T18:48:46.579000Z", "organization": 1, @@ -295,17 +185,7 @@ }, { "id": 3, - "invitation": { - "created_date": "2021-12-14T18:47:49.322000Z", - "key": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej", - "owner": { - "first_name": "User", - "id": 2, - "last_name": "First", - "url": "http://localhost:8080/api/users/2", - "username": "user1" - } - }, + "invitation": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej", "is_active": true, "joined_date": "2021-12-14T18:47:49.322000Z", "organization": 1, @@ -320,17 +200,7 @@ }, { "id": 2, - "invitation": { - "created_date": "2021-12-14T18:47:39.935000Z", - "key": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr", - "owner": { - "first_name": "User", - "id": 2, - "last_name": "First", - "url": "http://localhost:8080/api/users/2", - "username": "user1" - } - }, + "invitation": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr", "is_active": true, "joined_date": "2021-12-14T18:47:39.935000Z", "organization": 1,