Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
82a1520
Add docker volume for pre-commit cache
mvandenburgh Nov 12, 2025
a7d8913
Merge pull request #2640 from dandi/docker-volume-for-precommit
mvandenburgh Nov 13, 2025
a377ff9
Add create_test_user management command
jjnesbitt Nov 13, 2025
b899432
Merge pull request #2642 from dandi/create-test-user-command
jjnesbitt Nov 13, 2025
de74482
Remove unnecessary setting
jjnesbitt Nov 13, 2025
2000b84
Merge pull request #2643 from dandi/remove-unnecessary-setting
jjnesbitt Nov 13, 2025
1aed085
Add vendorization support (#2584)
candleindark Nov 21, 2025
a633c07
Update management command for schema migration
jjnesbitt Nov 24, 2025
e41c04b
Show more stats in migrate_version_metadata
jjnesbitt Nov 25, 2025
051b89c
Auto approve users from McGill University
kabilar Dec 1, 2025
5e2a50f
Update email address based on suggestion
kabilar Dec 1, 2025
b13dfc9
Remove --target option
jjnesbitt Dec 1, 2025
01bfb52
Replace settings.DANDI_SCHEMA_VERSION with import
jjnesbitt Nov 13, 2025
68b6ed2
Merge pull request #2657 from kabilar/email-ca
yarikoptic Dec 2, 2025
ec5be59
Merge pull request #2644 from dandi/remove-schema-version-setting
jjnesbitt Dec 8, 2025
c64e111
Bump dandischema to 0.12.1
jjnesbitt Dec 2, 2025
851c815
Merge pull request #2650 from dandi/schema-version-update-script
jjnesbitt Dec 8, 2025
f70b392
Empty commit for release
jjnesbitt Dec 17, 2025
4d27eba
Merge pull request #2680 from dandi/vendorization-release-branch
jjnesbitt Dec 17, 2025
879d4a0
auto shipit - CHANGELOG.md etc
dandibot Dec 17, 2025
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
3 changes: 3 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ jobs:
DJANGO_DANDI_WEB_APP_URL: http://localhost:8085
DJANGO_DANDI_API_URL: http://localhost:8000
DJANGO_DANDI_JUPYTERHUB_URL: https://hub.dandiarchive.org/
DJANGO_DANDI_INSTANCE_NAME: DEV-DANDI
DJANGO_DANDI_INSTANCE_IDENTIFIER: "RRID:ABC_123456"
DJANGO_DANDI_DOI_API_PREFIX: "10.80507"
steps:
- uses: actions/checkout@v5
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ jobs:
DJANGO_DANDI_WEB_APP_URL: http://localhost:8085
DJANGO_DANDI_API_URL: http://localhost:8000
DJANGO_DANDI_JUPYTERHUB_URL: https://hub.dandiarchive.org/
DJANGO_DANDI_INSTANCE_NAME: DEV-DANDI
DJANGO_DANDI_INSTANCE_IDENTIFIER: "RRID:ABC_123456"
DJANGO_DANDI_DOI_API_PREFIX: "10.80507"

# Web client env vars
VITE_APP_DANDI_API_ROOT: http://localhost:8000/api/
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# v0.20.0 (Wed Dec 17 2025)

### Release Notes

#### Add vendorization support ([#2584](https://github.com/dandi/dandi-archive/pull/2584))

This PR makes the instance config as defined in `dandischema.conf`, per https://github.com/dandi/dandi-schema/pull/294, available through the `api/info/` endpoint and replaces the hardcoded `DAND:`, the hardcoded RRID, and the supported licenses with the corresponding attributes in schema instance config defined in `dandischema.conf`.

---

#### 🚀 Enhancement

- Add vendorization support [#2584](https://github.com/dandi/dandi-archive/pull/2584) ([@candleindark](https://github.com/candleindark) [@jjnesbitt](https://github.com/jjnesbitt))

#### 🐛 Bug Fix

- Empty commit for release [#2680](https://github.com/dandi/dandi-archive/pull/2680) ([@jjnesbitt](https://github.com/jjnesbitt))
- Update management command for schema migration [#2650](https://github.com/dandi/dandi-archive/pull/2650) ([@jjnesbitt](https://github.com/jjnesbitt))

#### 🏠 Internal

- Remove `DANDI_SCHEMA_VERSION` as a setting [#2644](https://github.com/dandi/dandi-archive/pull/2644) ([@jjnesbitt](https://github.com/jjnesbitt))
- Auto approve users from McGill University [#2657](https://github.com/dandi/dandi-archive/pull/2657) ([@kabilar](https://github.com/kabilar))
- Don't use django setting to store allowed schema versions [#2643](https://github.com/dandi/dandi-archive/pull/2643) ([@jjnesbitt](https://github.com/jjnesbitt))
- Add create_test_user management command [#2642](https://github.com/dandi/dandi-archive/pull/2642) ([@jjnesbitt](https://github.com/jjnesbitt))
- Add docker volume for pre-commit cache [#2640](https://github.com/dandi/dandi-archive/pull/2640) ([@mvandenburgh](https://github.com/mvandenburgh))

#### Authors: 4

- Isaac To ([@candleindark](https://github.com/candleindark))
- Jacob Nesbitt ([@jjnesbitt](https://github.com/jjnesbitt))
- Kabilar Gunalan ([@kabilar](https://github.com/kabilar))
- Mike VanDenburgh ([@mvandenburgh](https://github.com/mvandenburgh))

---

# v0.19.0 (Thu Nov 13 2025)

### Release Notes
Expand Down
6 changes: 4 additions & 2 deletions dandiapi/api/doi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from typing import TYPE_CHECKING

from dandischema.conf import get_instance_config
from django.conf import settings
import requests

Expand All @@ -29,10 +30,11 @@ def _generate_doi_data(version: Version):

publish = settings.DANDI_DOI_PUBLISH
# Use the DANDI test datacite instance as a placeholder if PREFIX isn't set
prefix = settings.DANDI_DOI_API_PREFIX or '10.80507'
prefix = settings.DANDI_DOI_API_PREFIX
instance_name: str = get_instance_config().instance_name
dandiset_id = version.dandiset.identifier
version_id = version.version
doi = f'{prefix}/dandi.{dandiset_id}/{version_id}'
doi = f'{prefix}/{instance_name.lower()}.{dandiset_id}/{version_id}'
metadata = version.metadata
metadata['doi'] = doi
return (doi, to_datacite(metadata, publish=publish))
Expand Down
7 changes: 4 additions & 3 deletions dandiapi/api/management/commands/create_dev_dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from uuid import uuid4

from django.conf import settings
from dandischema.conf import get_instance_config
from dandischema.consts import DANDI_SCHEMA_VERSION
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
import djclick as click
Expand Down Expand Up @@ -32,7 +33,7 @@ def create_dev_dandiset(*, name: str, email: str, num_extra_owners: int):

version_metadata = {
'description': 'An informative description',
'license': ['spdx:CC0-1.0'],
'license': [sorted(x.value for x in get_instance_config().licenses)[0]],
}
dandiset, draft_version = create_open_dandiset(
user=owner, version_name=name, version_metadata=version_metadata
Expand Down Expand Up @@ -68,7 +69,7 @@ def create_dev_dandiset(*, name: str, email: str, num_extra_owners: int):
calculate_sha256(blob_id=asset_blob.blob_id)
asset_blob.refresh_from_db()
asset_metadata = {
'schemaVersion': settings.DANDI_SCHEMA_VERSION,
'schemaVersion': DANDI_SCHEMA_VERSION,
'encodingFormat': 'text/plain',
'schemaKey': 'Asset',
'path': 'foo/bar.txt',
Expand Down
63 changes: 63 additions & 0 deletions dandiapi/api/management/commands/create_test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import User
from django.db import transaction
import djclick as click
import faker

from dandiapi.api.models.user import UserMetadata


@click.command()
@click.option(
'--auto-approve',
is_flag=True,
help='Auto approve this user, skipping the questionnaire.',
)
@click.option(
'--password',
default='password',
show_default=True,
help='The password for this user.',
)
def create_test_user(*, auto_approve: bool, password: str):
fake = faker.Faker()

with transaction.atomic():
email = fake.email()
user = User.objects.create(
first_name=fake.first_name(),
last_name=fake.last_name(),
username=email,
email=email,
)

user.set_password(password)
user.save()

UserMetadata.objects.create(
user=user,
status=UserMetadata.Status.APPROVED if auto_approve else UserMetadata.Status.INCOMPLETE,
)

uid = fake.random_number(digits=8, fix_len=True)
SocialAccount.objects.create(
user=user,
provider='github',
uid=uid,
extra_data={
'id': uid,
'name': user.get_full_name(),
'email': user.email,
'login': fake.user_name(),
},
)

click.echo(
click.style(
f'Created user "{user.email}" with password "{password}"',
fg='green',
bold=True,
)
)
96 changes: 79 additions & 17 deletions dandiapi/api/management/commands/migrate_version_metadata.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,90 @@
from __future__ import annotations

from dandischema import migrate
import logging

from dandischema.consts import DANDI_SCHEMA_VERSION
from dandischema.metadata import migrate
from django.db import transaction
import djclick as click

from dandiapi.api.models import Version
from dandiapi.api.services import audit
from dandiapi.api.services.metadata import validate_version_metadata

logger = logging.getLogger(__name__)


@click.command()
@click.argument('to_version')
def migrate_version_metadata(*, to_version: str):
click.echo(f'Migrating all version metadata to version {to_version}')
for version in Version.objects.filter(version='draft'):
click.echo(f'Migrating {version.dandiset.identifier}/{version.version}')

metadata = version.metadata

try:
metanew = migrate(metadata, to_version=to_version, skip_validation=True)
except Exception as e: # noqa: BLE001
click.echo(f'Failed to migrate {version.dandiset.identifier}/{version.version}')
click.echo(e)
continue

if version.metadata != metanew:
@click.argument(
'dandisets',
type=click.INT,
nargs=-1,
)
@click.option(
'-a',
'--all',
'include_all',
is_flag=True,
help='Run on all dandisets.',
)
def migrate_version_metadata(dandisets: tuple[int, ...], *, include_all: bool):
if bool(dandisets) == include_all:
raise click.ClickException("Must specify exactly one of 'dandisets' or --all")

versions = Version.objects.filter(version='draft')
if dandisets:
versions = versions.filter(dandiset_id__in=dandisets)

logger.info(
'Migrating %s dandiset draft versions to schema version %s',
versions.count(),
DANDI_SCHEMA_VERSION,
)

migrated_count = 0
failed_count = 0
unchanged_count = 0
for version in versions.iterator():
logger.info('-----------------------------------------')
logger.info('Migrating %s', version)

with transaction.atomic():
locked_version = Version.objects.select_for_update().get(id=version.id)

try:
metanew = migrate(locked_version.metadata, skip_validation=True)
except Exception as e:
logger.exception('Failed to migrate %s', version, exc_info=e)
failed_count += 1
continue

if locked_version.metadata == metanew:
logger.info('No change in metadata for %s. Skipping save...', version)
unchanged_count += 1
continue

version.metadata = metanew
version.status = Version.Status.PENDING
version.save()

audit.update_metadata(
dandiset=locked_version.dandiset,
metadata=locked_version.metadata,
user=None,
admin=True,
description=f'Update schema version to {DANDI_SCHEMA_VERSION}',
)

migrated_count += 1
logger.info('Metadata migrated for version %s', version)

# Validate outside of transaction, since this function uses `select_for_update` itself
validate_version_metadata(version=version)

logger.info(
'%d migrated, %d failed, %d left unchanged, out of %d total selected versions',
migrated_count,
failed_count,
unchanged_count,
versions.count(),
)
8 changes: 6 additions & 2 deletions dandiapi/api/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
from typing import TYPE_CHECKING
from uuid import uuid4

from dandischema.conf import get_instance_config

if TYPE_CHECKING:
import datetime


class PublishableMetadataMixin:
@classmethod
def published_by(cls, now: datetime.datetime):
schema_config = get_instance_config()

return {
'id': uuid4().urn,
'name': 'DANDI publish',
Expand All @@ -20,8 +24,8 @@ def published_by(cls, now: datetime.datetime):
'wasAssociatedWith': [
{
'id': uuid4().urn,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
'identifier': schema_config.instance_identifier,
'name': f'{schema_config.instance_name} API',
# TODO: version the API
'version': '0.1.0',
'schemaKey': 'Software',
Expand Down
6 changes: 4 additions & 2 deletions dandiapi/api/models/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
from typing import TypedDict

from dandischema.conf import get_instance_config
from dandischema.models import AccessType
from django.conf import settings
from django.contrib.postgres.indexes import HashIndex
Expand Down Expand Up @@ -229,6 +230,7 @@ def _populate_access_metadata(self):
def _populate_metadata(self):
from dandiapi.api.manifests import manifest_location

schema_config = get_instance_config()
metadata = {
**self.metadata,
'@context': (
Expand All @@ -237,9 +239,9 @@ def _populate_metadata(self):
),
'manifestLocation': manifest_location(self),
'name': self.name,
'identifier': f'DANDI:{self.dandiset.identifier}',
'identifier': (f'{schema_config.instance_name}:{self.dandiset.identifier}'),
'version': self.version,
'id': f'DANDI:{self.dandiset.identifier}/{self.version}',
'id': (f'{schema_config.instance_name}:{self.dandiset.identifier}/{self.version}'),
'repository': settings.DANDI_WEB_APP_URL,
'url': (
f'{settings.DANDI_WEB_APP_URL}/dandiset/{self.dandiset.identifier}/{self.version}'
Expand Down
10 changes: 8 additions & 2 deletions dandiapi/api/services/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING

from celery.utils.log import get_task_logger
from dandischema.conf import get_instance_config
import dandischema.exceptions
from dandischema.metadata import aggregate_assets_summary, validate
from django.conf import settings
Expand Down Expand Up @@ -108,6 +109,8 @@ def version_aggregate_assets_summary(version: Version) -> None:

def validate_version_metadata(*, version: Version) -> None:
def _build_validatable_version_metadata(version: Version) -> dict:
schema_config = get_instance_config()

# since Version.Status.VALID is a proxy for a version being publishable, we need to
# validate against the PublishedDandiset schema even though we lack several things
# at validation time: id, url, doi, and assetsSummary. this tricks the validator into
Expand All @@ -116,13 +119,16 @@ def _build_validatable_version_metadata(version: Version) -> dict:
metadata_for_validation = publishable_version.metadata

metadata_for_validation['id'] = (
f'DANDI:{publishable_version.dandiset.identifier}/{publishable_version.version}'
f'{schema_config.instance_name}:'
f'{publishable_version.dandiset.identifier}/{publishable_version.version}'
)
metadata_for_validation['url'] = (
f'{settings.DANDI_WEB_APP_URL}/dandiset/'
f'{publishable_version.dandiset.identifier}/{publishable_version.version}'
)
metadata_for_validation['doi'] = '10.80507/dandi.123456/0.123456.1234'
metadata_for_validation['doi'] = (
f'{schema_config.doi_prefix}/{schema_config.instance_name.lower()}.123456/0.123456.1234'
)
metadata_for_validation['assetsSummary'] = {
'schemaKey': 'AssetsSummary',
'numberOfBytes': 1
Expand Down
6 changes: 5 additions & 1 deletion dandiapi/api/services/publish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import datetime
from typing import TYPE_CHECKING

from dandischema.conf import get_instance_config
from dandischema.metadata import aggregate_assets_summary, validate
from django.contrib.auth.models import User
from django.db import transaction
Expand Down Expand Up @@ -182,7 +183,10 @@ def _publish_dandiset(dandiset_id: int, user_id: int) -> None:
old_version.save()

# Inject a dummy DOI so the metadata is valid
new_version.metadata['doi'] = '10.80507/dandi.123456/0.123456.1234'
schema_config = get_instance_config()
new_version.metadata['doi'] = (
f'{schema_config.doi_prefix}/{schema_config.instance_name.lower()}.123456/0.123456.1234'
)

validate(new_version.metadata, schema_key='PublishedDandiset', json_validation=True)

Expand Down
Loading
Loading