Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projects (server only, REST API) #754

Merged
merged 6 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
exclude_paths:
- '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js'
- CHANGELOG.md
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0.alpha] - 2020-02-XX
### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*).

### Changed
-

### Deprecated
-

### Removed
-

### Fixed
-

### Security
-

## [0.5.0] - 2019-10-12
### Added
- A converter to YOLO format
Expand Down
72 changes: 66 additions & 6 deletions cvat/apps/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,33 @@ def authenticate(self, request):
has_annotator_role = rules.is_group_member(str(AUTH_ROLE.ANNOTATOR))
has_observer_role = rules.is_group_member(str(AUTH_ROLE.OBSERVER))

@rules.predicate
def is_project_owner(db_user, db_project):
# If owner is None (null) the task can be accessed/changed/deleted
# only by admin. At the moment each task has an owner.
return db_project is not None and db_project.owner == db_user

@rules.predicate
def is_project_assignee(db_user, db_project):
return db_project is not None and db_project.assignee == db_user

@rules.predicate
def is_project_annotator(db_user, db_project):
db_tasks = list(db_project.tasks.prefetch_related('segment_set').all())
return any([is_task_annotator(db_user, db_task) for db_task in db_tasks])

@rules.predicate
def is_task_owner(db_user, db_task):
# If owner is None (null) the task can be accessed/changed/deleted
# only by admin. At the moment each task has an owner.
return db_task.owner == db_user
return db_task.owner == db_user or is_project_owner(db_user, db_task.project)

@rules.predicate
def is_task_assignee(db_user, db_task):
return db_task.assignee == db_user
return db_task.assignee == db_user or is_project_assignee(db_user, db_task.project)

@rules.predicate
def is_task_annotator(db_user, db_task):
from functools import reduce

db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all())
return any([is_job_annotator(db_user, db_job)
for db_segment in db_segments for db_job in db_segment.job_set.all()])
Expand All @@ -101,6 +114,13 @@ def is_job_annotator(db_user, db_job):
rules.add_perm('engine.role.annotator', has_annotator_role)
rules.add_perm('engine.role.observer', has_observer_role)

rules.add_perm('engine.project.create', has_admin_role | has_user_role)
rules.add_perm('engine.project.access', has_admin_role | has_observer_role |
is_project_owner | is_project_annotator)
rules.add_perm('engine.project.change', has_admin_role | is_project_owner |
is_project_assignee)
rules.add_perm('engine.project.delete', has_admin_role | is_project_owner)

rules.add_perm('engine.task.create', has_admin_role | has_user_role)
rules.add_perm('engine.task.access', has_admin_role | has_observer_role |
is_task_owner | is_task_annotator)
Expand Down Expand Up @@ -133,6 +153,26 @@ class ObserverRolePermission(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm("engine.role.observer")

class ProjectCreatePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.project.create")

class ProjectAccessPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.project.access", obj)

class ProjectChangePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.project.change", obj)

class ProjectDeletePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.project.delete", obj)

class TaskCreatePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
Expand All @@ -143,7 +183,8 @@ class TaskAccessPermission(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.task.access", obj)

class TaskGetQuerySetMixin(object):

class ProjectGetQuerySetMixin(object):
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
Expand All @@ -152,7 +193,26 @@ def get_queryset(self):
return queryset
else:
return queryset.filter(Q(owner=user) | Q(assignee=user) |
Q(segment__job__assignee=user) | Q(assignee=None)).distinct()
Q(task__owner=user) | Q(task__assignee=user) |
Q(task__segment__job__assignee=user)).distinct()

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()

class TaskGetQuerySetMixin(object):
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
# Don't filter queryset for detail methods
if self.detail:
return queryset
else:
return filter_task_queryset(queryset, user)

class TaskChangePermission(BasePermission):
# pylint: disable=no-self-use
Expand Down
38 changes: 38 additions & 0 deletions cvat/apps/engine/migrations/0022_auto_20191004_0817.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 2.2.3 on 2019-10-04 08:17

import cvat.apps.engine.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('engine', '0021_auto_20190826_1827'),
]

operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', cvat.apps.engine.models.SafeCharField(max_length=256)),
('bug_tracker', models.CharField(blank=True, default='', max_length=2000)),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(choices=[('annotation', 'ANNOTATION'), ('validation', 'VALIDATION'), ('completed', 'COMPLETED')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32)),
('assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': (),
},
),
migrations.AddField(
model_name='task',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', related_query_name='task', to='engine.Project'),
),
]
19 changes: 19 additions & 0 deletions cvat/apps/engine/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,26 @@ def choices(self):
def __str__(self):
return self.value

class Project(models.Model):
name = SafeCharField(max_length=256)
owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="+")
assignee = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="+")
bug_tracker = models.CharField(max_length=2000, blank=True, default="")
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)

# Extend default permission model
class Meta:
default_permissions = ()

class Task(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE,
null=True, blank=True, related_name="tasks",
related_query_name="task")
name = SafeCharField(max_length=256)
size = models.PositiveIntegerField()
mode = models.CharField(max_length=32)
Expand Down
12 changes: 11 additions & 1 deletion cvat/apps/engine/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ class Meta:
fields = ('url', 'id', 'name', 'size', 'mode', 'owner', 'assignee',
'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'z_order', 'status', 'labels', 'segments',
'image_quality', 'start_frame', 'stop_frame', 'frame_filter')
'image_quality', 'start_frame', 'stop_frame', 'frame_filter',
'project')
read_only_fields = ('size', 'mode', 'created_date', 'updated_date',
'status')
write_once_fields = ('overlap', 'segment_size', 'image_quality')
Expand Down Expand Up @@ -245,6 +246,7 @@ def update(self, instance, validated_data):
instance.start_frame = validated_data.get('start_frame', instance.start_frame)
instance.stop_frame = validated_data.get('stop_frame', instance.stop_frame)
instance.frame_filter = validated_data.get('frame_filter', instance.frame_filter)
instance.project = validated_data.get('project', instance.project)
labels = validated_data.get('label_set', [])
for label in labels:
attributes = label.pop('attributespec_set', [])
Expand Down Expand Up @@ -276,6 +278,14 @@ def update(self, instance, validated_data):
instance.save()
return instance

class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = models.Project
fields = ('url', 'id', 'name', 'owner', 'assignee', 'bug_tracker',
'created_date', 'updated_date', 'status')
read_only_fields = ('created_date', 'updated_date', 'status')
ordering = ['-id']

class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
Expand Down
Loading