Ability to configure user agreements for the register user form (#1464)

main
Andrey Zhavoronkov 6 years ago committed by GitHub
parent fc2f02a406
commit 380be58edc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- cvat-ui: added cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>)
- Added `datumaro_project` export format (https://github.com/opencv/cvat/pull/1352)
- Ability to configure user agreements for the user registration form (https://github.com/opencv/cvat/pull/1464)
### Changed
- Downloaded file name in annotations export became more informative (https://github.com/opencv/cvat/pull/1352)

@ -4,6 +4,7 @@ ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG PUBLIC_INSTANCE
ENV TERM=xterm \
http_proxy=${http_proxy} \

@ -69,10 +69,15 @@
return new AnnotationFormats(result);
};
cvat.server.userAgreements.implementation = async () => {
const result = await serverProxy.server.userAgreements();
return result;
};
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2) => {
email, password1, password2, userConfirmations) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2);
password1, password2, userConfirmations);
};
cvat.server.login.implementation = async (username, password) => {

@ -119,6 +119,21 @@ function build() {
return result;
},
/**
* Method returns user agreements that the user must accept
* @method userAgreements
* @async
* @memberof module:API.cvat.server
* @returns {Object[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async userAgreements() {
const result = await PluginRegistry
.apiWrapper(cvat.server.userAgreements);
return result;
},
/**
* Method allows to register on a server
* @method register
* @async
@ -129,13 +144,14 @@ function build() {
* @param {string} email A email address for the new account
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @param {Object} userConfirmations An user confirmations of terms of use if needed
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async register(username, firstName, lastName, email, password1, password2) {
async register(username, firstName, lastName, email, password1, password2, userConfirmations) {
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2);
lastName, email, password1, password2, userConfirmations);
return result;
},
/**

@ -154,7 +154,23 @@
return response.data;
}
async function register(username, firstName, lastName, email, password1, password2) {
async function userAgreements() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function register(username, firstName, lastName, email, password1, password2, confirmations) {
let response = null;
try {
const data = JSON.stringify({
@ -164,6 +180,7 @@
email,
password1,
password2,
confirmations,
});
response = await Axios.post(`${config.backendAPI}/auth/register`, data, {
proxy: config.proxy,
@ -657,6 +674,7 @@
authorized,
register,
request: serverRequest,
userAgreements,
}),
writable: false,
},

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { UserConfirmation } from 'components/register-page/register-form';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
@ -44,13 +45,14 @@ export const registerAsync = (
email: string,
password1: string,
password2: string,
confirmations: UserConfirmation[],
): ThunkAction => async (
dispatch,
) => {
dispatch(authActions.register());
try {
await cvat.server.register(username, firstName, lastName, email, password1, password2);
await cvat.server.register(username, firstName, lastName, email, password1, password2, confirmations);
const users = await cvat.users.get({ self: true });
dispatch(authActions.registerSuccess(users[0]));

@ -0,0 +1,38 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { UserAgreement } from 'reducers/interfaces'
const core = getCore();
export enum UserAgreementsActionTypes {
GET_USER_AGREEMENTS = 'GET_USER_AGREEMENTS',
GET_USER_AGREEMENTS_SUCCESS = 'GET_USER_AGREEMENTS_SUCCESS',
GET_USER_AGREEMENTS_FAILED = 'GET_USER_AGREEMENTS_FAILED',
}
const userAgreementsActions = {
getUserAgreements: () => createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS),
getUserAgreementsSuccess: (userAgreements: UserAgreement[]) =>
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS, userAgreements),
getUserAgreementsFailed: (error: any) =>
createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED, { error }),
};
export type UserAgreementsActions = ActionUnion<typeof userAgreementsActions>;
export const getUserAgreementsAsync = (): ThunkAction => async (dispatch): Promise<void> => {
dispatch(userAgreementsActions.getUserAgreements());
try {
const userAgreements = await core.server.userAgreements();
dispatch(
userAgreementsActions.getUserAgreementsSuccess(userAgreements),
);
} catch (error) {
dispatch(userAgreementsActions.getUserAgreementsFailed(error));
}
};

@ -33,6 +33,7 @@ interface CVATAppProps {
loadUsers: () => void;
loadAbout: () => void;
verifyAuthorized: () => void;
loadUserAgreements: () => void;
initPlugins: () => void;
resetErrors: () => void;
resetMessages: () => void;
@ -51,6 +52,8 @@ interface CVATAppProps {
installedAutoAnnotation: boolean;
installedTFAnnotation: boolean;
installedTFSegmentation: boolean;
userAgreementsFetching: boolean,
userAgreementsInitialized: boolean,
notifications: NotificationsState;
user: any;
}
@ -58,7 +61,7 @@ interface CVATAppProps {
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void {
const core = getCore();
const { verifyAuthorized } = this.props;
const { verifyAuthorized, loadUserAgreements } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
// Logger configuration
@ -77,6 +80,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadFormats,
loadUsers,
loadAbout,
loadUserAgreements,
initPlugins,
userInitialized,
userFetching,
@ -89,6 +93,8 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
pluginsInitialized,
pluginsFetching,
user,
userAgreementsFetching,
userAgreementsInitialized,
} = this.props;
this.showErrors();
@ -99,6 +105,11 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
return;
}
if (!userAgreementsInitialized && !userAgreementsFetching) {
loadUserAgreements();
return;
}
if (user == null) {
return;
}

@ -7,9 +7,17 @@ import Form, { FormComponentProps } from 'antd/lib/form/Form';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Input from 'antd/lib/input';
import Checkbox from 'antd/lib/checkbox';
import patterns from 'utils/validation-patterns';
import { UserAgreement } from 'reducers/interfaces'
export interface UserConfirmation {
name: string;
value: boolean;
}
export interface RegisterData {
username: string;
firstName: string;
@ -17,10 +25,12 @@ export interface RegisterData {
email: string;
password1: string;
password2: string;
confirmations: UserConfirmation[];
}
type RegisterFormProps = {
fetching: boolean;
userAgreements: UserAgreement[],
onSubmit(registerData: RegisterData): void;
} & FormComponentProps;
@ -70,15 +80,43 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
callback();
};
private validateAgrement = (agreement: any, value: any, callback: any): void => {
const { userAgreements } = this.props;
let isValid: boolean = true;
for (const userAgreement of userAgreements) {
if (agreement.field === userAgreement.name
&& userAgreement.required && !value) {
isValid = false;
callback(`You must accept the ${userAgreement.displayText} to continue!`);
break;
}
}
if (isValid) {
callback();
}
};
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
const {
form,
onSubmit,
userAgreements,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
values.confirmations = []
for (const userAgreement of userAgreements) {
values.confirmations.push({
name: userAgreement.name,
value: values[userAgreement.name]
});
delete values[userAgreement.name];
}
onSubmit(values);
}
});
@ -214,6 +252,38 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
);
}
private renderUserAgreements(): JSX.Element[] {
const { form, userAgreements } = this.props;
const getUserAgreementsElements = () =>
{
const agreementsList: JSX.Element[] = [];
for (const userAgreement of userAgreements) {
agreementsList.push(
<Form.Item key={userAgreement.name}>
{form.getFieldDecorator(userAgreement.name, {
initialValue: false,
valuePropName: 'checked',
rules: [{
required: true,
message: 'You must accept to continue!',
}, {
validator: this.validateAgrement,
}]
})(
<Checkbox>
I read and accept the <a rel='noopener noreferrer' target='_blank'
href={ userAgreement.url }>{ userAgreement.displayText }</a>
</Checkbox>
)}
</Form.Item>
);
}
return agreementsList;
}
return getUserAgreementsElements();
}
public render(): JSX.Element {
const { fetching } = this.props;
@ -225,6 +295,7 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
{this.renderEmailField()}
{this.renderPasswordField()}
{this.renderPasswordConfirmationField()}
{this.renderUserAgreements()}
<Form.Item>
<Button

@ -9,14 +9,17 @@ import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import RegisterForm, { RegisterData } from './register-form';
import { UserAgreement } from 'reducers/interfaces'
import RegisterForm, { RegisterData, UserConfirmation } from './register-form';
import CookieDrawer from 'components/login-page/cookie-policy-drawer';
interface RegisterPageComponentProps {
fetching: boolean;
userAgreements: UserAgreement[];
onRegister: (username: string, firstName: string,
lastName: string, email: string,
password1: string, password2: string) => void;
password1: string, password2: string,
confirmations: UserConfirmation[]) => void;
}
function RegisterPageComponent(
@ -32,6 +35,7 @@ function RegisterPageComponent(
const {
fetching,
userAgreements,
onRegister,
} = props;
@ -42,6 +46,7 @@ function RegisterPageComponent(
<Title level={2}> Create an account </Title>
<RegisterForm
fetching={fetching}
userAgreements={userAgreements}
onSubmit={(registerData: RegisterData): void => {
onRegister(
registerData.username,
@ -50,6 +55,7 @@ function RegisterPageComponent(
registerData.email,
registerData.password1,
registerData.password2,
registerData.confirmations,
);
}}
/>

@ -6,21 +6,26 @@ import React from 'react';
import { connect } from 'react-redux';
import { registerAsync } from 'actions/auth-actions';
import RegisterPageComponent from 'components/register-page/register-page';
import { CombinedState } from 'reducers/interfaces';
import { UserConfirmation } from 'components/register-page/register-form'
import { CombinedState, UserAgreement } from 'reducers/interfaces';
interface StateToProps {
fetching: boolean;
userAgreements: UserAgreement[];
}
interface DispatchToProps {
onRegister: (username: string, firstName: string,
lastName: string, email: string,
password1: string, password2: string) => void;
password1: string, password2: string,
userAgreement: UserConfirmation[]) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
return {
fetching: state.auth.fetching,
fetching: state.auth.fetching || state.userAgreements.fetching,
userAgreements: state.userAgreements.list,
};
}

@ -19,6 +19,7 @@ import { getFormatsAsync } from 'actions/formats-actions';
import { checkPluginsAsync } from 'actions/plugins-actions';
import { getUsersAsync } from 'actions/users-actions';
import { getAboutAsync } from 'actions/about-actions';
import { getUserAgreementsAsync } from 'actions/useragreements-actions';
import { shortcutsActions } from 'actions/shortcuts-actions';
import {
resetErrors,
@ -44,6 +45,8 @@ interface StateToProps {
aboutFetching: boolean;
formatsInitialized: boolean;
formatsFetching: boolean;
userAgreementsInitialized: boolean;
userAgreementsFetching: boolean;
installedAutoAnnotation: boolean;
installedTFSegmentation: boolean;
installedTFAnnotation: boolean;
@ -61,6 +64,7 @@ interface DispatchToProps {
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
loadUserAgreements: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -70,6 +74,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { users } = state;
const { about } = state;
const { shortcuts } = state;
const { userAgreements } = state;
return {
userInitialized: auth.initialized,
@ -82,6 +87,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
aboutFetching: about.fetching,
formatsInitialized: formats.initialized,
formatsFetching: formats.fetching,
userAgreementsInitialized: userAgreements.initialized,
userAgreementsFetching: userAgreements.fetching,
installedAutoAnnotation: plugins.list.AUTO_ANNOTATION,
installedTFSegmentation: plugins.list.TF_SEGMENTATION,
installedTFAnnotation: plugins.list.TF_ANNOTATION,
@ -95,6 +102,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
loadFormats: (): void => dispatch(getFormatsAsync()),
verifyAuthorized: (): void => dispatch(authorizedAsync()),
loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()),
initPlugins: (): void => dispatch(checkPluginsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
loadAbout: (): void => dispatch(getAboutAsync()),

@ -104,6 +104,19 @@ export interface AboutState {
initialized: boolean;
}
export interface UserAgreement {
name: string;
displayText: string;
url: string;
required: boolean;
}
export interface UserAgreementsState {
list: UserAgreement[];
fetching: boolean;
initialized: boolean;
}
export interface ShareFileInfo { // get this data from cvat-core
name: string;
type: 'DIR' | 'REG';
@ -238,6 +251,9 @@ export interface NotificationsState {
boundaries: {
resetError: null | ErrorState;
};
userAgreements: {
fetching: null | ErrorState;
};
[index: string]: any;
};
@ -464,6 +480,7 @@ export interface CombinedState {
about: AboutState;
share: ShareState;
formats: FormatsState;
userAgreements: UserAgreementsState;
plugins: PluginsState;
models: ModelsState;
notifications: NotificationsState;

@ -14,6 +14,7 @@ import { AboutActionTypes } from 'actions/about-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { NotificationsActionType } from 'actions/notification-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { UserAgreementsActions, UserAgreementsActionTypes } from 'actions/useragreements-actions';
import { NotificationsState } from './interfaces';
@ -80,6 +81,9 @@ const defaultState: NotificationsState = {
boundaries: {
resetError: null,
},
userAgreements: {
fetching: null,
},
},
messages: {
tasks: {
@ -801,6 +805,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED: {
return {
...state,
errors: {
...state.errors,
userAgreements: {
...state.errors.userAgreements,
fetching: {
message: 'Could not get user agreements from the server',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

@ -15,6 +15,7 @@ import notificationsReducer from './notifications-reducer';
import annotationReducer from './annotation-reducer';
import settingsReducer from './settings-reducer';
import shortcutsReducer from './shortcuts-reducer';
import userAgreementsReducer from './useragreements-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
@ -30,5 +31,6 @@ export default function createRootReducer(): Reducer {
annotation: annotationReducer,
settings: settingsReducer,
shortcuts: shortcutsReducer,
userAgreements: userAgreementsReducer,
});
}

@ -0,0 +1,50 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { boundariesActions, BoundariesActionTypes } from 'actions/boundaries-actions';
import { UserAgreementsActions, UserAgreementsActionTypes } from 'actions/useragreements-actions';
import { AuthActions, AuthActionTypes } from 'actions/auth-actions';
import { UserAgreementsState } from './interfaces';
const defaultState: UserAgreementsState = {
list: [],
fetching: false,
initialized: false,
};
export default function (
state: UserAgreementsState = defaultState,
action: UserAgreementsActions | AuthActions | boundariesActions,
): UserAgreementsState {
switch (action.type) {
case UserAgreementsActionTypes.GET_USER_AGREEMENTS: {
return {
...state,
fetching: true,
initialized: false,
};
}
case UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS:
return {
...state,
fetching: false,
initialized: true,
list: action.payload,
};
case UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED:
return {
...state,
fetching: false,
initialized: true,
};
case AuthActionTypes.LOGOUT_SUCCESS:
case BoundariesActionTypes.RESET_AFTER_ERROR: {
return {
...defaultState,
};
}
default:
return state;
}
}

@ -115,7 +115,7 @@ def is_job_owner(db_user, db_job):
def is_job_annotator(db_user, db_job):
db_task = db_job.segment.task
# A job can be annotated by any user if the task's assignee is None.
has_rights = db_task.assignee is None or is_task_assignee(db_user, db_task)
has_rights = (db_task.assignee is None and not settings.RESTRICTIONS['reduce_task_visibility']) or is_task_assignee(db_user, db_task)
if db_job.assignee is not None:
has_rights |= (db_user == db_job.assignee)
@ -213,9 +213,13 @@ def filter_task_queryset(queryset, user):
# Don't filter queryset for admin, observer
if has_admin_role(user) or has_observer_role(user):
return queryset
else:
return queryset.filter(Q(owner=user) | Q(assignee=user) |
Q(segment__job__assignee=user) | Q(assignee=None)).distinct()
query_filter = Q(owner=user) | Q(assignee=user) | \
Q(segment__job__assignee=user)
if not settings.RESTRICTIONS['reduce_task_visibility']:
query_filter |= Q(assignee=None)
return queryset.filter(query_filter).distinct()
class TaskGetQuerySetMixin(object):
def get_queryset(self):

@ -10,3 +10,6 @@ def create_user(sender, instance, created, **kwargs):
if instance.is_superuser and instance.is_staff:
db_group = Group.objects.get(name=AUTH_ROLE.ADMIN)
instance.groups.add(db_group)
for group_name in settings.DJANGO_AUTH_DEFAULT_GROUPS:
db_group = Group.objects.get(name=getattr(AUTH_ROLE, group_name))
instance.groups.add(db_group)

@ -9,6 +9,7 @@ from rest_framework import routers
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from cvat.apps.restrictions.views import RestrictionsViewSet
schema_view = get_schema_view(
openapi.Info(
@ -30,6 +31,7 @@ router.register('jobs', views.JobViewSet)
router.register('users', views.UserViewSet)
router.register('server', views.ServerViewSet, basename='server')
router.register('plugins', views.PluginViewSet)
router.register('restrictions', RestrictionsViewSet, basename='restrictions')
urlpatterns = [
# Entry point for a client

@ -354,9 +354,19 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return [perm() for perm in permissions]
def perform_create(self, serializer):
if self.request.data.get('owner', None):
def validate_task_limit(owner):
admin_perm = auth.AdminRolePermission()
is_admin = admin_perm.has_permission(self.request, self)
if not is_admin and settings.RESTRICTIONS['task_limit'] is not None and \
Task.objects.filter(owner=owner).count() >= settings.RESTRICTIONS['task_limit']:
raise serializers.ValidationError('The user has the maximum number of tasks')
owner = self.request.data.get('owner', None)
if owner:
validate_task_limit(owner)
serializer.save()
else:
validate_task_limit(self.request.user)
serializer.save(owner=self.request.user)
def perform_destroy(self, instance):

@ -0,0 +1,3 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -0,0 +1,10 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.apps import AppConfig
class RestrictionsConfig(AppConfig):
name = 'cvat.apps.restrictions'

@ -0,0 +1,3 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -0,0 +1,43 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
from rest_framework import serializers
from django.conf import settings
from cvat.apps.authentication.serializers import RegisterSerializerEx
class UserAgreementSerializer(serializers.Serializer):
name = serializers.CharField(max_length=256)
display_text = serializers.CharField(max_length=2048, default='')
url = serializers.CharField(max_length=2048, default='')
required = serializers.BooleanField(default=False)
value = serializers.BooleanField(default=False)
# pylint: disable=no-self-use
def to_representation(self, instance):
instance_ = instance.copy()
instance_['displayText'] = instance_.pop('display_text')
return instance_
class RestrictedRegisterSerializer(RegisterSerializerEx):
confirmations = UserAgreementSerializer(many=True, required=False)
def validate(self, data):
validated_data = super().validate(data)
server_user_agreements = settings.RESTRICTIONS['user_agreements']
for server_agreement in server_user_agreements:
if server_agreement['required']:
is_confirmed = False
for confirmation in validated_data['confirmations']:
if confirmation['name'] == server_agreement['name'] \
and confirmation['value']:
is_confirmed = True
if not is_confirmed:
raise serializers.ValidationError(
'Agreement {} must be accepted'.format(server_agreement['name'])
)
return validated_data

@ -0,0 +1,19 @@
<!DOCTYPE html>
<body>
<h1>CVAT terms of use</h1>
<p>
PLEASE READ THE FOLLOWING TERMS CAREFULLY. IF YOU CLICK TO ACCEPT THIS AGREEMENT, DOWNLOAD,
OR INSTALL THE CVAT SOFTWARE, YOU ARE AGREEING TO BE LEGALLY BOUND BY THIS AGREEMENT
IN ADDITION TO ANY OTHER TERMS PROVIDED. IF YOU DO NOT AGREE TO THESE TERMS, DO NOT USE
THE CVAT SOFTWARE AND DESTROY ALL COPIES IN YOUR POSSESSION.
</p>
<p>
You understand and acknowledge that you are responsible for your use of the CVAT Software
and any content which you may alter with use of the CVAT Software.
</p>
<p>
You further understand and acknowledge that any content you may elect to use may be subject to privacy,
data and intellectual property rights and that you are responsible to your compliance with such laws and regulations.
</p>
</body>
</html>

@ -0,0 +1,109 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.conf import settings
class UserAgreementsTest(APITestCase):
def setUp(self):
self.client = APIClient()
self.user_agreements = settings.RESTRICTIONS['user_agreements']
settings.RESTRICTIONS['user_agreements'] = [
{
'name': 'agreement_1',
'display_text': 'some display text 1',
'url': 'https://example.com',
'required': True,
},
{
'name': 'agreement_2',
'display_text': 'some display text 2',
'url': 'https://example2.com',
'required': True,
},
{
'name': 'agreement_3',
'display_text': 'some display text 3',
'url': 'https://example3.com',
'required': False,
},
]
def tearDown(self):
settings.RESTRICTIONS['user_agreements'] = self.user_agreements
def _get_user_agreements(self):
response = self.client.get('/api/v1/restrictions/user-agreements')
assert response.status_code == status.HTTP_200_OK
for agreements in response.data:
assert 'name' in agreements, agreements['name']
assert 'displayText' in agreements
assert 'required' in agreements
return response.data
def _register_user(self, data):
response = self.client.post('/api/v1/auth/register', data=data, format="json")
return response
def test_user_agreements(self):
self._get_user_agreements()
def test_register_user_with_required_confirmations(self):
agreements = self._get_user_agreements()
data = {
'username': 'some_username1',
'first_name': 'some first name 1',
'last_name': 'some last name 1',
'email': 'user1@example.com',
'password1': 'FnvL4YdF24NAmnQ8',
'password2': 'FnvL4YdF24NAmnQ8',
'confirmations':[],
}
for agreement in agreements:
if agreement['required']:
data['confirmations'].append({
'name': agreement['name'],
'value': True,
})
response = self._register_user(data)
assert response.status_code == status.HTTP_201_CREATED
def test_register_user_without_confirmations(self):
data = {
'username': 'some_username2',
'first_name': 'some first name 2',
'last_name': 'some last name 2',
'email': 'user2@example.com',
'password1': 'FnvL4YdF24NAmnQ8',
'password2': 'FnvL4YdF24NAmnQ8',
'confirmations':[],
}
response = self._register_user(data)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_register_user_with_all_confirmations(self):
agreements = self._get_user_agreements()
data = {
'username': 'some_username3',
'first_name': 'some first name 3',
'last_name': 'some last name 3',
'email': 'user3@example.com',
'password1': 'FnvL4YdF24NAmnQ8',
'password2': 'FnvL4YdF24NAmnQ8',
'confirmations':[],
}
for agreement in agreements:
data['confirmations'].append({
'name': agreement['name'],
'value': True,
})
response = self._register_user(data)
assert response.status_code == status.HTTP_201_CREATED

@ -0,0 +1,41 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.conf import settings
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from rest_framework.renderers import TemplateHTMLRenderer
from drf_yasg.utils import swagger_auto_schema
from cvat.apps.restrictions.serializers import UserAgreementSerializer
class RestrictionsViewSet(viewsets.ViewSet):
serializer_class = None
permission_classes = [AllowAny]
authentication_classes = []
# To get nice documentation about ServerViewSet actions it is necessary
# to implement the method. By default, ViewSet doesn't provide it.
def get_serializer(self, *args, **kwargs):
pass
@staticmethod
@swagger_auto_schema(
method='get',
operation_summary='Method provides user agreements that the user must accept to register',
responses={'200': UserAgreementSerializer})
@action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements')
def user_agreements(request):
user_agreements = settings.RESTRICTIONS['user_agreements']
serializer = UserAgreementSerializer(data=user_agreements, many=True)
serializer.is_valid(raise_exception=True)
return Response(data=serializer.data)
@staticmethod
@action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,),
url_path='terms-of-use')
def terms_of_use(request):
return Response(template_name='restrictions/terms_of_use.html')

@ -98,6 +98,7 @@ INSTALLED_APPS = [
'cvat.apps.dataset_manager',
'cvat.apps.engine',
'cvat.apps.git',
'cvat.apps.restrictions',
'django_rq',
'compressor',
'cacheops',
@ -150,7 +151,7 @@ REST_FRAMEWORK = {
}
REST_AUTH_REGISTER_SERIALIZERS = {
'REGISTER_SERIALIZER': 'cvat.apps.authentication.serializers.RegisterSerializerEx'
'REGISTER_SERIALIZER': 'cvat.apps.restrictions.serializers.RestrictedRegisterSerializer'
}
if 'yes' == os.environ.get('TF_ANNOTATION', 'no'):
@ -216,6 +217,7 @@ WSGI_APPLICATION = 'cvat.wsgi.application'
# Django Auth
DJANGO_AUTH_TYPE = 'BASIC'
DJANGO_AUTH_DEFAULT_GROUPS = []
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
AUTH_LOGIN_NOTE = '<p>Have not registered yet? <a href="/auth/register">Register here</a>.</p>'
@ -404,4 +406,14 @@ LOCAL_LOAD_MAX_FILES_COUNT = 500
LOCAL_LOAD_MAX_FILES_SIZE = 512 * 1024 * 1024 # 512 MB
DATUMARO_PATH = os.path.join(BASE_DIR, 'datumaro')
sys.path.append(DATUMARO_PATH)
sys.path.append(DATUMARO_PATH)
RESTRICTIONS = {
"user_agreements": [],
# this setting limits the number of tasks for the user
"task_limit": None,
# this settings reduse task visibility to owner and assignee only
"reduce_task_visibility": False,
}

Loading…
Cancel
Save