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

Refactor job creation code #3465

Merged
merged 78 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
362d3c4
Refactor algorithm job creation and civ creation
amickan Aug 1, 2024
82ebb57
Remove unused debug statements
amickan Aug 1, 2024
1e595c7
Remove unused code
amickan Aug 1, 2024
724e7f3
Update post serializer to use same validation code
amickan Aug 1, 2024
2ce04b0
Move job creation to form_valid and serializer create methods
amickan Aug 1, 2024
89ad04a
Fix tests
amickan Aug 2, 2024
e5324ee
Small code improvements
amickan Aug 2, 2024
4e89d98
Add API view tests and test for test_retrieve_existing_civs
amickan Aug 2, 2024
85be1c7
Undo commenting out middleware
amickan Aug 2, 2024
1ee3796
Merge main
amickan Aug 21, 2024
8006337
Fix typos
amickan Aug 21, 2024
c42e4a7
Complete API view tests
amickan Aug 21, 2024
b584d08
Consolidate InterfaceFormField logic
amickan Aug 23, 2024
e8d3580
Add tests
amickan Aug 23, 2024
a89b8a3
Add test for inputs_complete
amickan Aug 23, 2024
606818c
Make add_file_to_object idempotent
amickan Aug 23, 2024
1e32d75
Move util methods to serializer and job manager
amickan Aug 26, 2024
18c4521
Small fixes
amickan Aug 26, 2024
0fe7eb8
Add object locking and atomic transaction marker to celery tasks
amickan Aug 26, 2024
6cb1eb9
Revert "Add object locking and atomic transaction marker to celery ta…
amickan Aug 28, 2024
e9ce96d
Address PR review
amickan Aug 28, 2024
49ac398
Catch more errors, add tests
amickan Aug 29, 2024
7afe9d3
Replace TestCase instances with fixture
amickan Aug 29, 2024
6ac0154
Fix tests
amickan Aug 29, 2024
3857d1c
Update reformat inputs
amickan Aug 29, 2024
d59e32e
Do object locking and status update within transaction
amickan Aug 29, 2024
d6ec56e
Add permission checks to file upload view
amickan Aug 29, 2024
ef574ec
Prepend interface slug on file upload view
amickan Aug 29, 2024
52465be
Make widget name a child class property for file upload view
amickan Aug 30, 2024
2209161
Add object locking
amickan Aug 30, 2024
c872738
Catch all exceptions in async tasks
amickan Aug 30, 2024
d38f02d
Fix tests
amickan Aug 30, 2024
230e1a2
Refactor create_civ method
amickan Aug 30, 2024
de459e9
Refactor error handling for civ creation
amickan Sep 2, 2024
69d4b6a
Address merge conflicts
amickan Sep 12, 2024
f9daede
Don't raise error when job already provisioned
amickan Sep 12, 2024
78a04c7
Change log level
amickan Sep 12, 2024
8fc5ea8
Resolve merge conflicts
amickan Sep 12, 2024
edfdfa3
Rerun pre-commit
amickan Sep 12, 2024
172014f
Make sure provision_job gets called only once
amickan Sep 16, 2024
b4d54a5
Fix line ending changes by pycharm
amickan Sep 16, 2024
d7e4545
Reintroduce checking for pending jobs
amickan Sep 16, 2024
501b90c
Refactor build_images notification sending
amickan Sep 16, 2024
35f5149
Fix tests
amickan Sep 16, 2024
9e63eaf
Merge branch 'main' into job_refactoring
jmsmkn Sep 16, 2024
5465984
Undo css changes
jmsmkn Sep 16, 2024
329fdfa
Merge main
amickan Sep 18, 2024
e04402c
Redo migrations
amickan Sep 18, 2024
fc5a8f2
Refactor error handling in build_images
amickan Sep 18, 2024
d2bc25f
Refactor CIV creation and validation
amickan Sep 18, 2024
736a3eb
Merge main
amickan Sep 19, 2024
0f5137f
Merge branch 'main' into job_refactoring
amickan Sep 19, 2024
399f219
Redo migrations
amickan Sep 19, 2024
91940ca
Add proper interface prefix to all interface form fields
amickan Sep 19, 2024
0d3c3b5
Add interface prefix in tests
amickan Sep 19, 2024
6d24448
Redesign CIVData
amickan Sep 19, 2024
cbd9210
Use just one view for file uploads and list all CIVs a user has permi…
amickan Sep 19, 2024
eabb979
Revert ValuesForInterfacesMixin changes
amickan Sep 19, 2024
4f199d1
Add test for file_civs_user_has_permission_to_use
amickan Sep 20, 2024
1556b06
Use VALIDATING_INPUTS status to determine execution readiness
amickan Sep 20, 2024
61038d0
Fix get_metrics test
amickan Sep 20, 2024
ab237f8
Remove leftover non_interface_fields, unused template and test
amickan Sep 20, 2024
9811d01
Refactor error handling and notification sending
amickan Sep 20, 2024
7ab897f
Only update linked_object on upload session failure
amickan Sep 20, 2024
0803ac4
Fix tests
amickan Sep 20, 2024
44c40a2
Subset selectable files by interface and update job session js
amickan Sep 23, 2024
17adc36
Remove leftover use of removed handle_error function
amickan Sep 23, 2024
8cb5901
Improve error message
amickan Sep 23, 2024
d2ce351
Fix tests
amickan Sep 23, 2024
d4788a5
Update test error message
amickan Sep 23, 2024
1500f7f
Merge branch 'main' into job_refactoring
amickan Oct 1, 2024
11d9da2
Addres review
amickan Oct 1, 2024
96959dc
Unify file civ serving permission checks
amickan Oct 2, 2024
cb01743
Switch to local import
amickan Oct 2, 2024
1008630
Move permission checking to serving app
amickan Oct 2, 2024
0267657
Update annotation and test
amickan Oct 3, 2024
0b469c7
Resolve conflicts with main
amickan Oct 3, 2024
6a6c3c5
Redo migrations
amickan Oct 3, 2024
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 app/grandchallenge/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def update_status(self, *, status, error_message=None, linked_object=None):

def process_images(
amickan marked this conversation as resolved.
Show resolved Hide resolved
self,
*,
linked_app_label=None,
linked_model_name=None,
linked_object_pk=None,
Expand Down
22 changes: 13 additions & 9 deletions app/grandchallenge/components/form_fields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django import forms
from django.forms import ModelChoiceField
from django.utils.functional import cached_property

from grandchallenge.cases.widgets import (
FlexibleImageField,
Expand All @@ -11,6 +12,9 @@
from grandchallenge.core.guardian import get_objects_for_user
from grandchallenge.core.validators import JSONValidator
from grandchallenge.core.widgets import JSONEditorWidget
from grandchallenge.serving.models import (
get_component_interface_values_for_user,
)
from grandchallenge.subdomains.utils import reverse
from grandchallenge.uploads.models import UserUpload
from grandchallenge.uploads.widgets import (
Expand All @@ -32,7 +36,7 @@ def _join_with_br(a, b):
return b


INTERFACE_FORM_FIELD_PREFIX = "interface_field_"
INTERFACE_FORM_FIELD_PREFIX = "__INTERFACE_FIELD__"


class InterfaceFormField:
Expand Down Expand Up @@ -137,11 +141,7 @@ def get_file_field(self):
key = f"value_type_{INTERFACE_FORM_FIELD_PREFIX}{self.instance.slug}"
if key in self.form_data.keys():
type = self.form_data[key]
elif (
self.user.user_profile.file_civs_user_has_permission_to_use.filter(
interface=self.instance
).exists()
):
elif self.civs_for_user_for_interface.exists():
type = "civ"
else:
type = "uuid"
Expand Down Expand Up @@ -173,15 +173,19 @@ def get_file_field(self):
)

return ModelChoiceField(
queryset=self.user.user_profile.file_civs_user_has_permission_to_use.filter(
interface=self.instance
),
queryset=self.civs_for_user_for_interface,
widget=SelectUploadWidget(
attrs={"upload_link": file_upload_link}
),
**self.kwargs,
)

@cached_property
def civs_for_user_for_interface(self):
return get_component_interface_values_for_user(user=self.user).filter(
interface=self.instance
)

@property
def field(self):
return self._field
2 changes: 1 addition & 1 deletion app/grandchallenge/components/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def __init__(self, *args, instance, base_obj, user, **kwargs):

# add fields for all interfaces that already exist on
# other display sets / archive items
for slug, _ in base_obj.values_for_interfaces.items():
for slug in base_obj.values_for_interfaces.keys():
current_value = None

if instance:
Expand Down
8 changes: 6 additions & 2 deletions app/grandchallenge/components/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,9 @@ def validate_voxel_values(*, civ_pk):
civ.interface._validate_voxel_values(civ.image)


@acks_late_micro_short_task(retry_on=(LockNotAcquiredException,))
@acks_late_micro_short_task(
retry_on=(LockNotAcquiredException,), delayed_retry=False
)
@transaction.atomic
def add_image_to_object(
*,
Expand Down Expand Up @@ -1237,7 +1239,9 @@ def add_image_to_object(
on_commit(signature(linked_task).apply_async)


@acks_late_micro_short_task(retry_on=(LockNotAcquiredException,))
@acks_late_micro_short_task(
retry_on=(LockNotAcquiredException,), delayed_retry=False
)
@transaction.atomic
def add_file_to_object(
*,
Expand Down
38 changes: 1 addition & 37 deletions app/grandchallenge/profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.auth import get_user_model
from django.core.signing import Signer
from django.db import models
from django.db.models import Exists, OuterRef, Q, TextChoices
from django.db.models import TextChoices
from django.db.models.signals import post_save
from django.template.defaultfilters import pluralize
from django.utils.functional import cached_property
Expand All @@ -18,7 +18,6 @@
from guardian.utils import get_anonymous_user
from stdimage import JPEGField

from grandchallenge.core.guardian import get_objects_for_user
from grandchallenge.core.models import UUIDModel
from grandchallenge.core.storage import get_mugshot_path
from grandchallenge.core.templatetags.remove_whitespace import oxford_comma
Expand Down Expand Up @@ -245,41 +244,6 @@ def dispatch_unread_direct_messages_email(
subscription_type=EmailSubscriptionTypes.NOTIFICATION,
)

@cached_property
def file_civs_user_has_permission_to_use(self):
# local import to avoid circular dependency
from grandchallenge.components.models import (
ComponentInterfaceValue,
InterfaceKind,
)

job_query = get_objects_for_user(
self.user, "algorithms.view_job"
).filter(inputs__pk__in=OuterRef("pk"))
display_set_query = get_objects_for_user(
self.user, "reader_studies.change_displayset"
).filter(values__pk__in=OuterRef("pk"))
archive_item_query = get_objects_for_user(
self.user, "archives.change_archiveitem"
).filter(values__pk__in=OuterRef("pk"))
return (
ComponentInterfaceValue.objects.filter(
Q(interface__kind__in=InterfaceKind.interface_type_file())
| (
Q(interface__kind__in=InterfaceKind.interface_type_json())
& Q(interface__store_in_database=False)
)
)
.annotate(has_view_job_perm=Exists(job_query))
.annotate(has_change_ds_perm=Exists(display_set_query))
.annotate(has_change_ai_perm=Exists(archive_item_query))
.filter(
Q(has_view_job_perm=True)
| Q(has_change_ds_perm=True)
| Q(has_change_ai_perm=True)
)
)


class UserProfileUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
Expand Down
35 changes: 35 additions & 0 deletions app/grandchallenge/serving/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.conf import settings
from django.db import models
from django.db.models import Exists, OuterRef, Q

from grandchallenge.algorithms.models import AlgorithmImage, AlgorithmModel
from grandchallenge.cases.models import Image
from grandchallenge.challenges.models import ChallengeRequest
from grandchallenge.components.models import ComponentInterfaceValue
from grandchallenge.core.guardian import get_objects_for_user
from grandchallenge.evaluation.models import Submission
from grandchallenge.workstations.models import Feedback

Expand Down Expand Up @@ -43,3 +45,36 @@ class Download(models.Model):
algorithm_image = models.ForeignKey(
AlgorithmImage, null=True, on_delete=models.CASCADE, editable=False
)


def get_component_interface_values_for_user(*, user):
job_inputs_query = get_objects_for_user(
user=user, perms="algorithms.view_job"
).filter(inputs__pk__in=OuterRef("pk"))

job_outputs_query = get_objects_for_user(
user=user, perms="algorithms.view_job"
).filter(outputs__pk__in=OuterRef("pk"))

display_set_query = get_objects_for_user(
user=user, perms="reader_studies.view_displayset"
).filter(values__pk__in=OuterRef("pk"))

archive_item_query = get_objects_for_user(
user=user, perms="archives.view_archiveitem"
).filter(values__pk__in=OuterRef("pk"))

return (
ComponentInterfaceValue.objects.annotate(
has_view_job_inputs_perm=Exists(job_inputs_query)
)
.annotate(has_view_job_outputs_perm=Exists(job_outputs_query))
.annotate(has_view_ds_perm=Exists(display_set_query))
.annotate(has_view_ai_perm=Exists(archive_item_query))
amickan marked this conversation as resolved.
Show resolved Hide resolved
.filter(
Q(has_view_job_inputs_perm=True)
| Q(has_view_job_outputs_perm=True)
| Q(has_view_ds_perm=True)
| Q(has_view_ai_perm=True)
)
)
29 changes: 8 additions & 21 deletions app/grandchallenge/serving/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
from grandchallenge.cases.models import Image
from grandchallenge.challenges.models import ChallengeRequest
from grandchallenge.components.models import ComponentInterfaceValue
from grandchallenge.core.guardian import get_objects_for_user
from grandchallenge.core.storage import internal_protected_s3_storage
from grandchallenge.evaluation.models import Submission
from grandchallenge.serving.models import Download
from grandchallenge.serving.models import (
Download,
get_component_interface_values_for_user,
)
from grandchallenge.workstations.models import Feedback


Expand Down Expand Up @@ -137,25 +139,10 @@ def serve_component_interface_value(
except (MultipleObjectsReturned, ComponentInterfaceValue.DoesNotExist):
raise Http404("No ComponentInterfaceValue found.")

for perm, lookup in (
("algorithms.view_job", "outputs"),
("algorithms.view_job", "inputs"),
("archives.view_archiveitem", "values"),
("reader_studies.view_displayset", "values"),
):
# Q | Q filters are very slow, this potentially does several db calls
# but each is quite performant. Could be optimised later.
if (
get_objects_for_user(
user=user,
perms=perm,
)
.filter(**{lookup: civ})
.exists()
):
return protected_storage_redirect(
name=civ.file.name, creator=user, component_interface_value=civ
)
if civ in get_component_interface_values_for_user(user=user):
jmsmkn marked this conversation as resolved.
Show resolved Hide resolved
return protected_storage_redirect(
name=civ.file.name, creator=user, component_interface_value=civ
)

raise PermissionDenied

Expand Down
60 changes: 0 additions & 60 deletions app/tests/profiles_tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,14 @@
from django.contrib.sites.models import Site
from django.core import mail

from grandchallenge.components.models import ComponentInterface
from grandchallenge.notifications.models import Notification
from grandchallenge.profiles.models import (
EmailSubscriptionTypes,
NotificationEmailOptions,
)
from grandchallenge.subdomains.utils import reverse
from tests.algorithms_tests.factories import AlgorithmJobFactory
from tests.archives_tests.factories import ArchiveFactory, ArchiveItemFactory
from tests.components_tests.factories import (
ComponentInterfaceFactory,
ComponentInterfaceValueFactory,
)
from tests.factories import UserFactory
from tests.notifications_tests.factories import NotificationFactory
from tests.reader_studies_tests.factories import (
DisplaySetFactory,
ReaderStudyFactory,
)
from tests.utils import get_view_for_user


Expand Down Expand Up @@ -199,52 +188,3 @@ def test_unsubscribe_link(
unsubscribe_viewname,
kwargs={"token": user.user_profile.unsubscribe_token},
)


@pytest.mark.django_db
def test_file_civs_user_has_permission_to_use():
user = UserFactory()
assert list(user.user_profile.file_civs_user_has_permission_to_use) == []

ci_file = ComponentInterfaceFactory(
kind=ComponentInterface.Kind.ANY, store_in_database=False
)
ci_str = ComponentInterfaceFactory(kind=ComponentInterface.Kind.STRING)

civ1, civ2, civ3, civ4, civ5, civ6 = (
ComponentInterfaceValueFactory.create_batch(6, interface=ci_file)
)
civ_str = ComponentInterfaceValueFactory(interface=ci_str)

job_with_perm = AlgorithmJobFactory(creator=user, time_limit=60)
job_without_perm = AlgorithmJobFactory(time_limit=60)

job_with_perm.inputs.set([civ1])
job_without_perm.inputs.set([civ2])

rs = ReaderStudyFactory()
rs.add_editor(user)
ds_with_perm = DisplaySetFactory(reader_study=rs)
ds_without_perm = DisplaySetFactory()

ds_with_perm.values.set([civ3])
ds_without_perm.values.set([civ4])

archive = ArchiveFactory()
archive.add_editor(user)
ai_with_perm = ArchiveItemFactory(archive=archive)
ai_without_perm = ArchiveItemFactory()

ai_with_perm.values.set([civ5])
ai_without_perm.values.set([civ6])

del user.user_profile.file_civs_user_has_permission_to_use
assert civ1 in user.user_profile.file_civs_user_has_permission_to_use
assert civ3 in user.user_profile.file_civs_user_has_permission_to_use
assert civ5 in user.user_profile.file_civs_user_has_permission_to_use
assert civ2 not in user.user_profile.file_civs_user_has_permission_to_use
assert civ4 not in user.user_profile.file_civs_user_has_permission_to_use
assert civ6 not in user.user_profile.file_civs_user_has_permission_to_use
assert (
civ_str not in user.user_profile.file_civs_user_has_permission_to_use
)
6 changes: 3 additions & 3 deletions app/tests/reader_studies_tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,8 +1344,8 @@ def test_display_set_add_interface_form():
auto_id="1",
)
assert sorted(form.fields.keys()) == [
"interface",
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_file.slug}",
"interface",
]
assert isinstance(
form.fields[f"{INTERFACE_FORM_FIELD_PREFIX}{ci_file.slug}"].widget,
Expand All @@ -1361,8 +1361,8 @@ def test_display_set_add_interface_form():
auto_id="1",
)
assert sorted(form.fields.keys()) == [
"interface",
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_value.slug}",
"interface",
]
assert isinstance(
form.fields[f"{INTERFACE_FORM_FIELD_PREFIX}{ci_value.slug}"].widget,
Expand All @@ -1378,8 +1378,8 @@ def test_display_set_add_interface_form():
auto_id="1",
)
assert sorted(form.fields.keys()) == [
"interface",
f"{INTERFACE_FORM_FIELD_PREFIX}{ci_image.slug}",
"interface",
]
assert isinstance(
form.fields[f"{INTERFACE_FORM_FIELD_PREFIX}{ci_image.slug}"].widget,
Expand Down
Loading