Skip to content

Commit

Permalink
Analytics access (#8509)
Browse files Browse the repository at this point in the history
Checkbox for granting access to analytics
  • Loading branch information
Eldies authored Oct 23, 2024
1 parent 13fac28 commit 6a26362
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Access to /analytics can now be granted
(<https://github.com/cvat-ai/cvat/pull/8509>)
1 change: 1 addition & 0 deletions cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface SerializedUser {
last_login?: string;
date_joined?: string;
email_verification_required: boolean;
has_analytics_access: boolean;
}

interface SerializedStorage {
Expand Down
8 changes: 7 additions & 1 deletion cvat-core/src/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -18,6 +18,7 @@ export default class User {
public readonly isSuperuser: boolean;
public readonly isActive: boolean;
public readonly isVerified: boolean;
public readonly hasAnalyticsAccess: boolean;

constructor(initialData: SerializedUser) {
const data = {
Expand All @@ -33,6 +34,7 @@ export default class User {
is_superuser: null,
is_active: null,
email_verification_required: null,
has_analytics_access: null,
};

for (const property in data) {
Expand Down Expand Up @@ -80,6 +82,9 @@ export default class User {
isVerified: {
get: () => !data.email_verification_required,
},
hasAnalyticsAccess: {
get: () => data.has_analytics_access,
},
}),
);
}
Expand All @@ -98,6 +103,7 @@ export default class User {
is_superuser: this.isSuperuser,
is_active: this.isActive,
email_verification_required: this.isVerified,
has_analytics_access: this.hasAnalyticsAccess,
};
}
}
2 changes: 1 addition & 1 deletion cvat-ui/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ function HeaderComponent(props: Props): JSX.Element {
Models
</Button>
) : null}
{isAnalyticsPluginActive && user.isSuperuser ? (
{isAnalyticsPluginActive && user.hasAnalyticsAccess ? (
<Button
className={getButtonClassName('analytics', false)}
type='link'
Expand Down
35 changes: 35 additions & 0 deletions cvat/apps/engine/migrations/0086_profile_has_analytics_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.16 on 2024-10-22 08:41

from django.conf import settings
from django.db import migrations, models


def set_has_analytics_access(apps, schema_editor):
User = apps.get_model('auth', 'User')
for user in User.objects.all():
is_admin = user.groups.filter(name=settings.IAM_ADMIN_ROLE).exists()
user.profile.has_analytics_access = user.is_superuser or is_admin
user.profile.save()


class Migration(migrations.Migration):

dependencies = [
("engine", "0085_segment_chunks_updated_date"),
]

operations = [
migrations.AddField(
model_name="profile",
name="has_analytics_access",
field=models.BooleanField(
default=False,
help_text="Designates whether the user can access analytics.",
verbose_name="has access to analytics",
),
),
migrations.RunPython(
set_has_analytics_access,
reverse_code=migrations.RunPython.noop,
),
]
15 changes: 12 additions & 3 deletions cvat/apps/engine/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@

from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage
from django.db import IntegrityError, models, transaction
from django.db.models.fields import FloatField
from django.db.models import Q, TextChoices
from django.db.models.fields import FloatField
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field

from cvat.apps.engine.lazy_list import LazyList
from cvat.apps.engine.model_utils import MaybeUndefined
from cvat.apps.engine.utils import parse_specific_attributes, chunked_list
from cvat.apps.engine.utils import chunked_list, parse_specific_attributes
from cvat.apps.events.utils import cache_deleted


class SafeCharField(models.CharField):
def get_prep_value(self, value):
value = super().get_prep_value(value)
Expand Down Expand Up @@ -1091,9 +1093,16 @@ class TrackedShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(TrackedShape, on_delete=models.DO_NOTHING,
related_name='attributes', related_query_name='attribute')


class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
rating = models.FloatField(default=0.0)
has_analytics_access = models.BooleanField(
_("has access to analytics"),
default=False,
help_text=_("Designates whether the user can access analytics."),
)


class Issue(TimestampedModel):
frame = models.PositiveIntegerField()
Expand Down
15 changes: 13 additions & 2 deletions cvat/apps/engine/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,28 +223,36 @@ class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name')


class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
has_analytics_access = serializers.BooleanField(
source='profile.has_analytics_access',
required=False,
read_only=True,
)

class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email',
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login',
'date_joined')
read_only_fields = ('last_login', 'date_joined')
'date_joined', 'has_analytics_access')
read_only_fields = ('last_login', 'date_joined', 'has_analytics_access')
write_only_fields = ('password', )
extra_kwargs = {
'last_login': { 'allow_null': True }
}


class DelimitedStringListField(serializers.ListField):
def to_representation(self, value):
return super().to_representation(value.split('\n'))

def to_internal_value(self, data):
return '\n'.join(super().to_internal_value(data))


class AttributeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
values = DelimitedStringListField(allow_empty=True,
Expand All @@ -255,6 +263,7 @@ class Meta:
model = models.AttributeSpec
fields = ('id', 'name', 'mutable', 'input_type', 'default_value', 'values')


class SublabelSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
attributes = AttributeSerializer(many=True, source='attributespec_set', default=[],
Expand All @@ -273,6 +282,7 @@ class Meta:
fields = ('id', 'name', 'color', 'attributes', 'type', 'has_parent', )
read_only_fields = ('parent',)


class SkeletonSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
svg = serializers.CharField(allow_blank=True, required=False)
Expand All @@ -281,6 +291,7 @@ class Meta:
model = models.Skeleton
fields = ('id', 'svg',)


class LabelSerializer(SublabelSerializer):
deleted = serializers.BooleanField(required=False, write_only=True,
help_text='Delete the label. Only applicable in the PATCH methods of a project or a task.')
Expand Down
26 changes: 21 additions & 5 deletions cvat/apps/engine/signals.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Copyright (C) 2019-2022 Intel Corporation
# Copyright (C) 2023 CVAT.ai Corporation
# Copyright (C) 2023-2024 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import functools
import shutil

from django.conf import settings
from django.contrib.auth.models import User
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.db.models.signals import m2m_changed, post_delete, post_save
from django.dispatch import receiver

from .models import CloudStorage, Data, Job, Profile, Project, StatusChoice, Task, Asset
Expand Down Expand Up @@ -36,13 +37,28 @@ def __save_job_handler(instance, created, **kwargs):
db_task.status = status
db_task.save(update_fields=["status", "updated_date"])

@receiver(post_save, sender=User,
dispatch_uid=__name__ + ".save_user_handler")
def __save_user_handler(instance, **kwargs):

@receiver(post_save, sender=User, dispatch_uid=__name__ + ".save_user_handler")
def __save_user_handler(instance: User, **kwargs):
should_access_analytics = instance.is_superuser or instance.groups.filter(name=settings.IAM_ADMIN_ROLE).exists()
if not hasattr(instance, 'profile'):
profile = Profile()
profile.user = instance
profile.has_analytics_access = should_access_analytics
profile.save()
elif should_access_analytics and not instance.profile.has_analytics_access:
instance.profile.has_analytics_access = True
instance.profile.save()


@receiver(m2m_changed, sender=User.groups.through, dispatch_uid=__name__ + ".m2m_user_groups_change_handler")
def __m2m_user_groups_change_handler(sender, instance: User, action: str, **kwargs):
if action == 'post_add':
is_admin = instance.groups.filter(name=settings.IAM_ADMIN_ROLE).exists()
if is_admin and hasattr(instance, 'profile') and not instance.profile.has_analytics_access:
instance.profile.has_analytics_access = True
instance.profile.save()


@receiver(post_delete, sender=Project,
dispatch_uid=__name__ + ".delete_project_handler")
Expand Down
6 changes: 6 additions & 0 deletions cvat/apps/engine/tests/test_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,8 @@ def _check_data(self, user, data, is_full):
extra_check("is_active", data)
extra_check("last_login", data)
extra_check("date_joined", data)
extra_check("has_analytics_access", data)


class UserListAPITestCase(UserAPITestCase):
def _run_api_v2_users(self, user):
Expand Down Expand Up @@ -671,6 +673,7 @@ def test_api_v2_users_no_auth(self):
response = self._run_api_v2_users(None)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class UserSelfAPITestCase(UserAPITestCase):
def _run_api_v2_users_self(self, user):
with ForceLogin(user, self.client):
Expand Down Expand Up @@ -698,6 +701,7 @@ def test_api_v2_users_self_no_auth(self):
response = self._run_api_v2_users_self(None)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class UserGetAPITestCase(UserAPITestCase):
def _run_api_v2_users_id(self, user, user_id):
with ForceLogin(user, self.client):
Expand Down Expand Up @@ -740,6 +744,7 @@ def test_api_v2_users_id_no_auth(self):
response = self._run_api_v2_users_id(None, self.user.id)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class UserPartialUpdateAPITestCase(UserAPITestCase):
def _run_api_v2_users_id(self, user, user_id, data):
with ForceLogin(user, self.client):
Expand Down Expand Up @@ -786,6 +791,7 @@ def test_api_v2_users_id_no_auth_partial(self):
response = self._run_api_v2_users_id(None, self.user.id, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class UserDeleteAPITestCase(UserAPITestCase):
def _run_api_v2_users_id(self, user, user_id):
with ForceLogin(user, self.client):
Expand Down
13 changes: 13 additions & 0 deletions cvat/apps/iam/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (C) 2021-2022 Intel Corporation
# Copyright (C) 2024 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

Expand All @@ -7,7 +8,19 @@
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.utils.translation import gettext_lazy as _

from cvat.apps.engine.models import Profile


class ProfileInline(admin.StackedInline):
model = Profile

fieldsets = (
(None, {'fields': ('has_analytics_access', )}),
)


class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
list_display = ("username", "email", "first_name", "last_name", "is_active", "is_staff")
fieldsets = (
(None, {'fields': ('username', 'password')}),
Expand Down
Loading

0 comments on commit 6a26362

Please sign in to comment.