-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Allow quality reports to regular users #8511
Changes from 18 commits
9eb30de
1bd5686
2311992
4e62b80
c466ffb
4522b96
adac3ca
92f9667
b4232dc
ee9e930
234211a
145d0fe
2556e7d
1ace06f
678cae5
7ee0f3e
0bad5f4
e1eda0e
13ce04b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
### Changed | ||
|
||
- \[Server API\] Quality report computation is now allowed to regular users | ||
(<https://github.com/cvat-ai/cvat/pull/8511>) |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -11,17 +11,21 @@ | |||||||||||||
from datetime import timedelta | ||||||||||||||
from functools import cached_property, partial | ||||||||||||||
from typing import Any, Callable, Dict, Hashable, List, Optional, Sequence, Tuple, Union, cast | ||||||||||||||
from uuid import uuid4 | ||||||||||||||
|
||||||||||||||
import datumaro as dm | ||||||||||||||
import datumaro.util.mask_tools | ||||||||||||||
import django_rq | ||||||||||||||
import numpy as np | ||||||||||||||
import rq | ||||||||||||||
from attrs import asdict, define, fields_dict | ||||||||||||||
from datumaro.util import dump_json, parse_json | ||||||||||||||
from django.conf import settings | ||||||||||||||
from django.db import transaction | ||||||||||||||
from django.utils import timezone | ||||||||||||||
from django_rq.queues import DjangoRQ as RqQueue | ||||||||||||||
from rest_framework.request import Request | ||||||||||||||
from rq.job import Job as RqJob | ||||||||||||||
from rq_scheduler import Scheduler as RqScheduler | ||||||||||||||
from scipy.optimize import linear_sum_assignment | ||||||||||||||
|
||||||||||||||
from cvat.apps.dataset_manager.bindings import ( | ||||||||||||||
|
@@ -48,6 +52,7 @@ | |||||||||||||
User, | ||||||||||||||
ValidationMode, | ||||||||||||||
) | ||||||||||||||
from cvat.apps.engine.utils import define_dependent_job, get_rq_job_meta, get_rq_lock_by_user | ||||||||||||||
from cvat.apps.profiler import silk_profile | ||||||||||||||
from cvat.apps.quality_control import models | ||||||||||||||
from cvat.apps.quality_control.models import ( | ||||||||||||||
|
@@ -2115,25 +2120,30 @@ def generate_report(self) -> ComparisonReport: | |||||||||||||
|
||||||||||||||
|
||||||||||||||
class QualityReportUpdateManager: | ||||||||||||||
_QUEUE_JOB_PREFIX = "update-quality-metrics-task-" | ||||||||||||||
_QUEUE_AUTOUPDATE_JOB_PREFIX = "update-quality-metrics-" | ||||||||||||||
_QUEUE_CUSTOM_JOB_PREFIX = "quality-check-" | ||||||||||||||
_RQ_CUSTOM_QUALITY_CHECK_JOB_TYPE = "custom_quality_check" | ||||||||||||||
_JOB_RESULT_TTL = 120 | ||||||||||||||
|
||||||||||||||
@classmethod | ||||||||||||||
def _get_quality_check_job_delay(cls) -> timedelta: | ||||||||||||||
return timedelta(seconds=settings.QUALITY_CHECK_JOB_DELAY) | ||||||||||||||
|
||||||||||||||
def _get_scheduler(self): | ||||||||||||||
def _get_scheduler(self) -> RqScheduler: | ||||||||||||||
return django_rq.get_scheduler(settings.CVAT_QUEUES.QUALITY_REPORTS.value) | ||||||||||||||
|
||||||||||||||
def _get_queue(self): | ||||||||||||||
def _get_queue(self) -> RqQueue: | ||||||||||||||
return django_rq.get_queue(settings.CVAT_QUEUES.QUALITY_REPORTS.value) | ||||||||||||||
|
||||||||||||||
def _make_queue_job_id_base(self, task: Task) -> str: | ||||||||||||||
return f"{self._QUEUE_JOB_PREFIX}{task.id}" | ||||||||||||||
return f"{self._QUEUE_AUTOUPDATE_JOB_PREFIX}task-{task.id}" | ||||||||||||||
|
||||||||||||||
def _make_custom_quality_check_job_id(self) -> str: | ||||||||||||||
return uuid4().hex | ||||||||||||||
def _make_custom_quality_check_job_id(self, task_id: int, user_id: int) -> str: | ||||||||||||||
# FUTURE-TODO: it looks like job ID template should not include user_id because: | ||||||||||||||
# 1. There is no need to compute quality reports several times for different users | ||||||||||||||
# 2. Each user (not only rq job owner) that has permission to access a task should | ||||||||||||||
# be able to check the status of the computation process | ||||||||||||||
return f"{self._QUEUE_CUSTOM_JOB_PREFIX}task-{task_id}-user-{user_id}" | ||||||||||||||
|
||||||||||||||
@classmethod | ||||||||||||||
def _get_last_report_time(cls, task: Task) -> Optional[timezone.datetime]: | ||||||||||||||
|
@@ -2186,28 +2196,52 @@ def schedule_quality_autoupdate_job(self, task: Task): | |||||||||||||
task_id=task.id, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
def schedule_quality_check_job(self, task: Task, *, user_id: int) -> str: | ||||||||||||||
class JobAlreadyExists(QualityReportsNotAvailable): | ||||||||||||||
def __str__(self): | ||||||||||||||
return "Quality computation job for this task already enqueued" | ||||||||||||||
Comment on lines
+2199
to
+2201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Adjust inheritance of The Apply this diff to adjust the inheritance: -class JobAlreadyExists(QualityReportsNotAvailable):
+class JobAlreadyExists(Exception): 📝 Committable suggestion
Suggested change
|
||||||||||||||
|
||||||||||||||
def schedule_custom_quality_check_job( | ||||||||||||||
self, request: Request, task: Task, *, user_id: int | ||||||||||||||
) -> str: | ||||||||||||||
""" | ||||||||||||||
Schedules a quality report computation job, supposed for updates by a request. | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
self._check_quality_reporting_available(task) | ||||||||||||||
|
||||||||||||||
rq_id = self._make_custom_quality_check_job_id() | ||||||||||||||
|
||||||||||||||
queue = self._get_queue() | ||||||||||||||
queue.enqueue( | ||||||||||||||
self._check_task_quality, | ||||||||||||||
task_id=task.id, | ||||||||||||||
job_id=rq_id, | ||||||||||||||
meta={"user_id": user_id, "job_type": self._RQ_CUSTOM_QUALITY_CHECK_JOB_TYPE}, | ||||||||||||||
result_ttl=self._JOB_RESULT_TTL, | ||||||||||||||
failure_ttl=self._JOB_RESULT_TTL, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
with get_rq_lock_by_user(queue, user_id=user_id): | ||||||||||||||
rq_id = self._make_custom_quality_check_job_id(task_id=task.id, user_id=user_id) | ||||||||||||||
rq_job = queue.fetch_job(rq_id) | ||||||||||||||
if rq_job: | ||||||||||||||
if rq_job.get_status(refresh=False) in ( | ||||||||||||||
rq.job.JobStatus.QUEUED, | ||||||||||||||
rq.job.JobStatus.STARTED, | ||||||||||||||
rq.job.JobStatus.SCHEDULED, | ||||||||||||||
rq.job.JobStatus.DEFERRED, | ||||||||||||||
): | ||||||||||||||
raise self.JobAlreadyExists() | ||||||||||||||
|
||||||||||||||
rq_job.delete() | ||||||||||||||
|
||||||||||||||
dependency = define_dependent_job( | ||||||||||||||
queue, user_id=user_id, rq_id=rq_id, should_be_dependent=True | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
queue.enqueue( | ||||||||||||||
self._check_task_quality, | ||||||||||||||
task_id=task.id, | ||||||||||||||
job_id=rq_id, | ||||||||||||||
meta=get_rq_job_meta(request=request, db_obj=task), | ||||||||||||||
result_ttl=self._JOB_RESULT_TTL, | ||||||||||||||
failure_ttl=self._JOB_RESULT_TTL, | ||||||||||||||
depends_on=dependency, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
return rq_id | ||||||||||||||
|
||||||||||||||
def get_quality_check_job(self, rq_id: str): | ||||||||||||||
def get_quality_check_job(self, rq_id: str) -> Optional[RqJob]: | ||||||||||||||
queue = self._get_queue() | ||||||||||||||
rq_job = queue.fetch_job(rq_id) | ||||||||||||||
|
||||||||||||||
|
@@ -2216,8 +2250,8 @@ def get_quality_check_job(self, rq_id: str): | |||||||||||||
|
||||||||||||||
return rq_job | ||||||||||||||
|
||||||||||||||
def is_custom_quality_check_job(self, rq_job) -> bool: | ||||||||||||||
return rq_job.meta.get("job_type") == self._RQ_CUSTOM_QUALITY_CHECK_JOB_TYPE | ||||||||||||||
def is_custom_quality_check_job(self, rq_job: RqJob) -> bool: | ||||||||||||||
return isinstance(rq_job.id, str) and rq_job.id.startswith(self._QUEUE_CUSTOM_JOB_PREFIX) | ||||||||||||||
zhiltsov-max marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
@classmethod | ||||||||||||||
@silk_profile() | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package quality_utils | ||
|
||
import rego.v1 | ||
|
||
|
||
is_task_owner(task_data, auth_data) if { | ||
task_data.owner.id == auth_data.user.id | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to move to engine at some point |
||
|
||
is_task_assignee(task_data, auth_data) if { | ||
task_data.assignee.id == auth_data.user.id | ||
} | ||
|
||
is_project_owner(project_data, auth_data) if { | ||
project_data.owner.id == auth_data.user.id | ||
} | ||
|
||
is_project_assignee(project_data, auth_data) if { | ||
project_data.assignee.id == auth_data.user.id | ||
} | ||
|
||
is_project_staff(project_data, auth_data) if { | ||
is_project_owner(project_data, auth_data) | ||
} | ||
|
||
is_project_staff(project_data, auth_data) if { | ||
is_project_assignee(project_data, auth_data) | ||
} | ||
|
||
is_task_staff(task_data, project_data, auth_data) if { | ||
is_project_staff(project_data, auth_data) | ||
} | ||
|
||
is_task_staff(task_data, project_data, auth_data) if { | ||
is_task_owner(task_data, auth_data) | ||
} | ||
|
||
is_task_staff(task_data, project_data, auth_data) if { | ||
is_task_assignee(task_data, auth_data) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete implementation of PR objectives
While the addition of the
VIEW_STATUS
constant is a step in the right direction, it appears that the implementation is incomplete based on the PR objectives. The PR aims to "allow regular users to compute quality reports for tasks," but the current changes only add a constant for viewing status.To fully implement the desired functionality, consider the following:
VIEW_STATUS
constant to grant permissions to regular users.Would you like assistance in drafting the additional rules or functions needed to complete this implementation?