Skip to content

Commit

Permalink
Don't use .save() in validate_version_metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
jjnesbitt committed Dec 30, 2023
1 parent 65eca4a commit c87aed5
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 19 deletions.
35 changes: 16 additions & 19 deletions dandiapi/api/services/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,36 +129,33 @@ def _build_validatable_version_metadata(version: Version) -> dict:
with transaction.atomic():
# validating version metadata needs to lock the version to avoid racing with
# other modifications e.g. aggregate_assets_summary.
version = (
Version.objects.filter(id=version.id, status=Version.Status.PENDING)
.select_for_update()
.first()
)
version.status = Version.Status.VALIDATING
version.save()
version_qs = Version.objects.filter(id=version.id).select_for_update()
if not version_qs.filter(status=Version.Status.PENDING).exists():
# TODO: Should raise an error here?
return

# Set to validating and continue
version_qs.update(status=Version.Status.VALIDATING)
try:
validate(
_build_validatable_version_metadata(version),
_build_validatable_version_metadata(version_qs.first()),
schema_key='PublishedDandiset',
json_validation=True,
)
except dandischema.exceptions.ValidationError as e:
logger.info('Error while validating version %s', version.id)
version.status = Version.Status.INVALID

validation_errors = _collect_validation_errors(e)
version.validation_errors = validation_errors
version.save()
version_qs.update(
status=Version.Status.INVALID, validation_errors=_collect_validation_errors(e)
)
return
except ValueError as e:
# A bare ValueError is thrown when dandischema generates its own exceptions, like a
# mismatched schemaVersion.
version.status = Version.Status.INVALID
version.validation_errors = [{'field': '', 'message': str(e)}]
version.save()
logger.info('Error while validating version %s', version.id)
version_qs.update(
status=Version.Status.INVALID, validation_errors=[{'field': '', 'message': str(e)}]
)
return

logger.info('Successfully validated version %s', version.id)
version.status = Version.Status.VALID
version.validation_errors = []
version.save()
version_qs.update(status=Version.Status.VALID, validation_errors=[])
23 changes: 23 additions & 0 deletions dandiapi/api/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import Storage
from django.forms.models import model_to_dict
from guardian.shortcuts import assign_perm
import pytest
from rest_framework.test import APIClient
Expand Down Expand Up @@ -193,6 +194,28 @@ def test_validate_version_metadata(draft_version: Version, asset: Asset):
assert draft_version.validation_errors == []


@pytest.mark.django_db()
def test_validate_version_metadata_no_side_effects(draft_version: Version, asset: Asset):
draft_version.assets.add(asset)

# Set the version `status` and `validation_errors` fields to something
# which can't be a result of validate_version_metadata
draft_version.status = Version.Status.PENDING
draft_version.validation_errors = [{'foo': 'bar'}]
draft_version.save()

# Validate version metadata, storing the model data before and after
old_data = model_to_dict(draft_version)
tasks.validate_version_metadata_task(draft_version.id)
draft_version.refresh_from_db()
new_data = model_to_dict(draft_version)

# Check that change is isolated to these two fields
assert old_data.pop('status') != new_data.pop('status')
assert old_data.pop('validation_errors') != new_data.pop('validation_errors')
assert old_data == new_data


@pytest.mark.django_db()
def test_validate_version_metadata_malformed_schema_version(draft_version: Version, asset: Asset):
draft_version.assets.add(asset)
Expand Down

0 comments on commit c87aed5

Please sign in to comment.