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 Jan 1, 2024
1 parent 65eca4a commit 1298ea2
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 20 deletions.
41 changes: 22 additions & 19 deletions dandiapi/api/services/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,36 +129,39 @@ 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),
modified=timezone.now(),
)
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)}],
modified=timezone.now(),
)
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=[], modified=timezone.now()
)
33 changes: 32 additions & 1 deletion dandiapi/api/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
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 django.utils import timezone
from guardian.shortcuts import assign_perm
import pytest
from rest_framework.test import APIClient
Expand Down Expand Up @@ -185,13 +187,42 @@ def test_validate_asset_metadata_saves_version(draft_asset: Asset, draft_version
def test_validate_version_metadata(draft_version: Version, asset: Asset):
draft_version.assets.add(asset)

tasks.validate_version_metadata_task(draft_version.id)
# Bypass .save to manually set an older timestamp
old_modified = timezone.now() - datetime.timedelta(minutes=10)
updated = Version.objects.filter(id=draft_version.id).update(modified=old_modified)
assert updated == 1

tasks.validate_version_metadata_task(draft_version.id)
draft_version.refresh_from_db()

assert draft_version.status == Version.Status.VALID
assert draft_version.validation_errors == []

# Ensure modified field was updated
assert draft_version.modified > old_modified


@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):
Expand Down

0 comments on commit 1298ea2

Please sign in to comment.