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

Simple Review Pipeline (Server) #2338

Merged
merged 40 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6b6ecec
tmp
bsekachev Oct 19, 2020
8c410ca
Removed migration
bsekachev Oct 19, 2020
5c48c35
Rebased
bsekachev Oct 19, 2020
e5a546a
Added signals & rating
bsekachev Oct 19, 2020
23614c9
Updated API views
bsekachev Oct 19, 2020
792afe2
Added reviewer serializer
bsekachev Oct 19, 2020
fbeaf00
Added permissions
bsekachev Oct 21, 2020
6fae70a
Fixed some code issues
bsekachev Oct 21, 2020
4b22095
Fixed swagger docs
bsekachev Oct 21, 2020
a7c2e6a
Some fixes
bsekachev Oct 21, 2020
289921e
Updated api method to create review
bsekachev Oct 21, 2020
b29c775
Added some API tests & some fixes
bsekachev Oct 21, 2020
4e453cb
Added some tests
bsekachev Oct 26, 2020
d5b3b94
Removed extra code
bsekachev Oct 26, 2020
16ba53d
Merge branch 'develop' into bs/review_pipeline
bsekachev Oct 28, 2020
39aac44
Some fixes
bsekachev Oct 28, 2020
7e27d0d
Applied changes from client part
bsekachev Nov 1, 2020
be435af
Fixed comments
bsekachev Nov 2, 2020
a2f2427
Merge branch 'develop' into bs/review_pipeline
bsekachev Nov 2, 2020
36459a4
Removed extra change
bsekachev Nov 2, 2020
322e60d
Merged develop
bsekachev Nov 12, 2020
e6c1206
Merged develop
bsekachev Nov 12, 2020
cdfdf4d
Removed extra changes
bsekachev Nov 12, 2020
e1da609
Removed extra changes
bsekachev Nov 12, 2020
b035dc3
Fixed tests
bsekachev Nov 12, 2020
d259573
Merge branch 'develop' into bs/review_pipeline
bsekachev Nov 18, 2020
ec74ce3
Removed /comments/<id> [PUT]
bsekachev Nov 18, 2020
3fba002
/issue/comments/create [POST] -> /comments [POST]
bsekachev Nov 18, 2020
d895699
/job/<id>/reviews/create [POST] -> /reviews [POST]
bsekachev Nov 18, 2020
44c0ec2
[PATCH] /issue/<id>/resolve(reopen) -> [PATCH] /issue/id
bsekachev Nov 18, 2020
71e1a69
Minor fix
bsekachev Nov 18, 2020
6e09135
Reviewed review summary
bsekachev Nov 18, 2020
43e8d8d
Removed unused import
bsekachev Nov 18, 2020
2bcbb84
Checking job permissions
bsekachev Nov 18, 2020
ae82d61
Fixed permissions for reviewers
bsekachev Nov 24, 2020
e0d455e
Merge branch 'develop' into bs/review_pipeline
bsekachev Nov 24, 2020
ca52ef0
Fixed typos
bsekachev Nov 24, 2020
254ca87
Merged develop
bsekachev Nov 25, 2020
396fca4
Fixed migration
bsekachev Nov 25, 2020
3cdaf1e
Merge branch 'develop' into bs/review_pipeline
bsekachev Nov 27, 2020
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
119 changes: 99 additions & 20 deletions cvat/apps/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ 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_project_reviewer(db_user, db_project):
db_tasks = list(db_project.tasks.prefetch_related('segment_set').all())
return any([is_task_reviewer(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
Expand All @@ -101,6 +106,12 @@ def is_task_owner(db_user, db_task):
def is_task_assignee(db_user, db_task):
return db_task.assignee == db_user or is_project_assignee(db_user, db_task.project)

@rules.predicate
def is_task_reviewer(db_user, db_task):
db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all())
return any([is_job_reviewer(db_user, db_job)
for db_segment in db_segments for db_job in db_segment.job_set.all()])

@rules.predicate
def is_task_annotator(db_user, db_task):
db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all())
Expand All @@ -121,6 +132,33 @@ def is_job_annotator(db_user, db_job):

return has_rights

@rules.predicate
def has_change_permissions(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 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) and (db_job.status == 'annotation')
if db_job.reviewer is not None:
has_rights |= (db_user == db_job.reviewer) and (db_job.status == 'validation')

return has_rights

@rules.predicate
def is_job_reviewer(db_user, db_job):
has_rights = db_job.reviewer == db_user
return has_rights

@rules.predicate
def is_issue_owner(db_user, db_issue):
has_rights = db_issue.owner == db_user
return has_rights

@rules.predicate
def is_comment_author(db_user, db_comment):
has_rights = (db_comment.author == db_user)
return has_rights

# AUTH PERMISSIONS RULES
rules.add_perm('engine.role.user', has_user_role)
rules.add_perm('engine.role.admin', has_admin_role)
Expand All @@ -136,65 +174,71 @@ def is_job_annotator(db_user, db_job):

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)
is_task_owner | is_task_annotator | is_task_reviewer)
rules.add_perm('engine.task.change', has_admin_role | is_task_owner |
is_task_assignee)
rules.add_perm('engine.task.delete', has_admin_role | is_task_owner)

rules.add_perm('engine.job.access', has_admin_role | has_observer_role |
is_job_owner | is_job_annotator)
rules.add_perm('engine.job.change', has_admin_role | is_job_owner |
is_job_annotator)
is_job_owner | is_job_annotator | is_job_reviewer)
rules.add_perm('engine.job.change', has_admin_role | is_job_owner | has_change_permissions)
rules.add_perm('engine.job.review', has_admin_role | (is_job_reviewer & has_change_permissions))

rules.add_perm('engine.issue.change', has_admin_role | is_issue_owner)
rules.add_perm('engine.issue.destroy', has_admin_role | is_issue_owner)

rules.add_perm('engine.comment.change', has_admin_role | is_comment_author)


class AdminRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.admin")
return request.user.has_perm('engine.role.admin')

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

class AnnotatorRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.annotator")
return request.user.has_perm('engine.role.annotator')

class ObserverRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.observer")
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")
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)
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)
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)
return request.user.has_perm('engine.project.delete', obj)

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

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


class ProjectGetQuerySetMixin(object):
Expand All @@ -207,15 +251,16 @@ def get_queryset(self):
else:
return queryset.filter(Q(owner=user) | Q(assignee=user) |
Q(task__owner=user) | Q(task__assignee=user) |
Q(task__segment__job__assignee=user)).distinct()
Q(task__segment__job__assignee=user) |
Q(task__segment__job__reviewer=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

query_filter = Q(owner=user) | Q(assignee=user) | \
Q(segment__job__assignee=user)
Q(segment__job__assignee=user) | Q(segment__job__reviewer=user)
if not settings.RESTRICTIONS['reduce_task_visibility']:
query_filter |= Q(assignee=None)

Expand All @@ -234,19 +279,53 @@ def get_queryset(self):
class TaskChangePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.task.change", obj)
return request.user.has_perm('engine.task.change', obj)

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

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

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

class JobReviewPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm('engine.job.review', obj)

class IssueAccessPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
db_job = obj.job
return request.user.has_perm('engine.job.access', db_job)

class IssueDestroyPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm('engine.issue.destroy', obj)

class IssueChangePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
db_job = obj.job
return (request.user.has_perm('engine.job.change', db_job)
or request.user.has_perm('engine.issue.change', obj))

class CommentCreatePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj): # obj is db_job
return request.user.has_perm('engine.job.access', obj)

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

3 changes: 2 additions & 1 deletion cvat/apps/documentation/AWS-Deployment-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ services:
environment:
CVAT_HOST: your-instance.amazonaws.com
```
In case of problems with using hostname, you can also use the public IPV4 instead of hostname. For AWS or any cloud based machines where the instances need to be terminated or stopped, the public IPV4 and hostname changes with every stop and reboot. To address this efficiently, avoid using spot instances that cannot be stopped, since copying the EBS to an AMI and restarting it throws problems. On the other hand, when a regular instance is stopped and restarted, the new hostname/IPV4 can be used in the `CVAT_HOST` variable in the `docker-compose.override.yml` and the build can happen instantly with CVAT tasks being available through the new IPV4.

In case of problems with using hostname, you can also use the public IPV4 instead of hostname. For AWS or any cloud based machines where the instances need to be terminated or stopped, the public IPV4 and hostname changes with every stop and reboot. To address this efficiently, avoid using spot instances that cannot be stopped, since copying the EBS to an AMI and restarting it throws problems. On the other hand, when a regular instance is stopped and restarted, the new hostname/IPV4 can be used in the `CVAT_HOST` variable in the `docker-compose.override.yml` and the build can happen instantly with CVAT tasks being available through the new IPV4.
74 changes: 74 additions & 0 deletions cvat/apps/engine/migrations/0034_auto_20201125_1426.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 3.1.1 on 2020-11-25 14:26

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

def create_profile(apps, schema_editor):
User = apps.get_model('auth', 'User')
Profile = apps.get_model('engine', 'Profile')
for user in User.objects.all():
profile = Profile()
profile.user = user
profile.save()

class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('engine', '0033_projects_adjastment'),
]

operations = [
migrations.AddField(
model_name='job',
name='reviewer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='review_job_set', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='Review',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('estimated_quality', models.FloatField()),
('status', models.CharField(choices=[('accepted', 'ACCEPTED'), ('rejected', 'REJECTED'), ('review_further', 'REVIEW_FURTHER')], max_length=16)),
('assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed', to=settings.AUTH_USER_MODEL)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.job')),
('reviewer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviews', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rating', models.FloatField(default=0.0)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Issue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frame', models.PositiveIntegerField()),
('position', cvat.apps.engine.models.FloatArrayField()),
('created_date', models.DateTimeField(auto_now_add=True)),
('resolved_date', models.DateTimeField(blank=True, null=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.job')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues', to=settings.AUTH_USER_MODEL)),
('resolver', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_issues', to=settings.AUTH_USER_MODEL)),
('review', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engine.review')),
],
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField(default='')),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.issue')),
],
),
migrations.RunPython(create_profile),
]
41 changes: 41 additions & 0 deletions cvat/apps/engine/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ class Meta:
class Job(models.Model):
segment = models.ForeignKey(Segment, on_delete=models.CASCADE)
assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
reviewer = models.ForeignKey(User, null=True, blank=True, related_name='review_job_set', on_delete=models.SET_NULL)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)

Expand Down Expand Up @@ -347,6 +348,18 @@ def choices(self):
def __str__(self):
return self.value

class ReviewStatus(str, Enum):
ACCEPTED = 'accepted'
REJECTED = 'rejected'
REVIEW_FURTHER = 'review_further'
bsekachev marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def choices(self):
return tuple((x.value, x.name) for x in self)

def __str__(self):
return self.value

class Annotation(models.Model):
id = models.BigAutoField(primary_key=True)
job = models.ForeignKey(Job, on_delete=models.CASCADE)
Expand Down Expand Up @@ -427,3 +440,31 @@ class TrackedShape(Shape):

class TrackedShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(TrackedShape, on_delete=models.CASCADE)

class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
rating = models.FloatField(default=0.0)

class Review(models.Model):
job = models.ForeignKey(Job, on_delete=models.CASCADE)
reviewer = models.ForeignKey(User, null=True, blank=True, related_name='reviews', on_delete=models.SET_NULL)
assignee = models.ForeignKey(User, null=True, blank=True, related_name='reviewed', on_delete=models.SET_NULL)
estimated_quality = models.FloatField()
status = models.CharField(max_length=16, choices=ReviewStatus.choices())

class Issue(models.Model):
frame = models.PositiveIntegerField()
position = FloatArrayField()
job = models.ForeignKey(Job, on_delete=models.CASCADE)
review = models.ForeignKey(Review, null=True, blank=True, on_delete=models.SET_NULL)
owner = models.ForeignKey(User, null=True, blank=True, related_name='issues', on_delete=models.SET_NULL)
resolver = models.ForeignKey(User, null=True, blank=True, related_name='resolved_issues', on_delete=models.SET_NULL)
bsekachev marked this conversation as resolved.
Show resolved Hide resolved
created_date = models.DateTimeField(auto_now_add=True)
resolved_date = models.DateTimeField(null=True, blank=True)

class Comment(models.Model):
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
author = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
message = models.TextField(default='')
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
Loading