diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2933c99..f87676285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +# v0.19.0 (Thu Nov 13 2025) + +### Release Notes + +#### Expose allowed schema versions at `/info/` endpoint ([#2625](https://github.com/dandi/dandi-archive/pull/2625)) + +The `/info/` endpoint now exposes the allowed list of DANDI schema version through `allowed_schema_versions` key value. + +--- + +#### ๐Ÿ› Bug Fix + +- Expose allowed schema versions at `/info/` endpoint [#2625](https://github.com/dandi/dandi-archive/pull/2625) ([@candleindark](https://github.com/candleindark)) + +#### ๐Ÿ“ Documentation + +- Update devcontainer section in readme [#2639](https://github.com/dandi/dandi-archive/pull/2639) ([@mvandenburgh](https://github.com/mvandenburgh)) + +#### Authors: 2 + +- Isaac To ([@candleindark](https://github.com/candleindark)) +- Mike VanDenburgh ([@mvandenburgh](https://github.com/mvandenburgh)) + +--- + +# v0.18.1 (Wed Nov 12 2025) + +#### ๐Ÿ› Bug Fix + +- Fix bug in publishing process [#2636](https://github.com/dandi/dandi-archive/pull/2636) ([@mvandenburgh](https://github.com/mvandenburgh)) +- Supply a default name in createsuperuser [#2616](https://github.com/dandi/dandi-archive/pull/2616) ([@jjnesbitt](https://github.com/jjnesbitt)) + +#### ๐Ÿ  Internal + +- Update how DOI settings are checked for configuration [#2634](https://github.com/dandi/dandi-archive/pull/2634) ([@jjnesbitt](https://github.com/jjnesbitt)) + +#### ๐Ÿงช Tests + +- Remove the `test_rest_info` test [#2635](https://github.com/dandi/dandi-archive/pull/2635) ([@jjnesbitt](https://github.com/jjnesbitt)) + +#### Authors: 2 + +- Jacob Nesbitt ([@jjnesbitt](https://github.com/jjnesbitt)) +- Mike VanDenburgh ([@mvandenburgh](https://github.com/mvandenburgh)) + +--- + # v0.18.0 (Tue Nov 04 2025) #### ๐Ÿš€ Enhancement diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0773122d8..8645f271c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,9 +8,32 @@ You would need a local clone of the `dandi-archive` repository to develop on it. 1. Run `cd dandi-archive` 1. Make sure your PostgreSQL port (5432) is available. -## Develop with Docker (recommended quickstart) +## Develop with VSCode Dev Containers (recommended quickstart) This is the simplest configuration for developers to start with. +### Initial Setup +1. Follow the steps for [setting up Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers#_installation) if necessary. +1. If you have previously used the "Develop with Docker" workflow described below, follow the below steps. Otherwise, skip to the next step. + 1. Run `docker compose volumes` + 1. If one of the listed volumes ends with `_uv_cache`, delete it by running `docker volume rm ` (this operation is safe and simply deletes your cached `uv` dependencies to avoid potential permission issues between the devcontainer and "Develop with Docker" configurations). + +1. From VSCode, use `Ctrl-Shift-p` and run the command `Dev Containers: Reopen in Container`. +1. From the VSCode built-in terminal, run `./manage.py migrate`. +1. From the VSCode built-in terminal, run `./manage.py createsuperuser --email $(git config user.email)` and follow the prompts. +1. From the VSCode built-in terminal, run `./manage.py create_dev_dandiset --owner $(git config user.email)` + to create a dummy dandiset to start working with. + +### Run Application +1. Run the following commands in three separate VSCode built-in-terminals: + 1. `./manage.py runserver_plus 0.0.0.0:8000` + 1. `uv run celery --app dandiapi.celery worker --loglevel INFO --without-heartbeat -Q celery,calculate_sha256,ingest_zarr_archive,manifest-worker -B` + 1. `cd web/ && npm install && npm run dev` +1. Access the site, starting at http://localhost:8000/admin/ +1. When finished, use `Ctrl+C` + +## Develop with Docker +This configuration also uses containers, but with Docker Compose instead of VScode Dev Containers. + ### Initial Setup 1. Install [Docker Compose](https://docs.docker.com/compose/install/) 1. Run `docker compose run --rm django ./manage.py migrate` @@ -59,23 +82,6 @@ but allows developers to run Python code on their native system. 1. `uv run celery --app dandiapi.celery worker --loglevel INFO --without-heartbeat -Q celery,calculate_sha256,ingest_zarr_archive,manifest-worker -B` 1. When finished, run `docker compose stop` - -## Develop with VSCode Dev Containers - -### Initial Setup -1. Follow the steps for [setting up Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers#_installation) if necessary. -1. From VSCode, use `Ctrl-Shift-p` and run the command `Dev Containers: Reopen in Container`. -1. From the VSCode built-in terminal, run `./manage.py migrate`. -1. From the VSCode built-in terminal, run `./manage.py createsuperuser --email $(git config user.email)` and follow the prompts. -1. From the VSCode built-in terminal, run `./manage.py create_dev_dandiset --owner $(git config user.email)` - to create a dummy dandiset to start working with. - -### Run Application -1. Run `./manage.py runserver_plus 0.0.0.0:8000` from the VSCode built-in-terminal. -1. Access the site, starting at http://localhost:8000/admin/ -1. When finished, use `Ctrl+C` - - ## Testing ### Initial Setup tox is used to manage the execution of all tests. To set up to run the tests: diff --git a/dandiapi/api/apps.py b/dandiapi/api/apps.py index 51ced7070..c51cfac52 100644 --- a/dandiapi/api/apps.py +++ b/dandiapi/api/apps.py @@ -9,5 +9,4 @@ class ApiConfig(AppConfig): def ready(self): # RUF100 is caused by https://github.com/astral-sh/ruff/issues/60 - import dandiapi.api.checks # noqa: F401, RUF100 import dandiapi.api.signals # noqa: F401 diff --git a/dandiapi/api/checks.py b/dandiapi/api/checks.py deleted file mode 100644 index 8149ed49c..000000000 --- a/dandiapi/api/checks.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from django.conf import settings -from django.core.checks import Error, register - -from dandiapi.api.doi import DANDI_DOI_SETTINGS, doi_configured - - -@register() -def check_doi_settings(app_configs, **kwargs): - if not doi_configured(): - # If no DOI settings are defined, DOIs will not be created on publish. - return [] - errors = [] - for setting, name in DANDI_DOI_SETTINGS: - if setting is None: - errors.append( - Error( - f'Setting {name} is not specified, but other DOI settings are.', - hint=f'Define {name} as an environment variable.', - obj=settings, - ) - ) - return errors diff --git a/dandiapi/api/doi.py b/dandiapi/api/doi.py index 9ff298973..5cd18cbd9 100644 --- a/dandiapi/api/doi.py +++ b/dandiapi/api/doi.py @@ -21,7 +21,7 @@ def doi_configured() -> bool: - return any(setting is not None for setting, _ in DANDI_DOI_SETTINGS) + return all(setting is not None for setting, _ in DANDI_DOI_SETTINGS) def _generate_doi_data(version: Version): diff --git a/dandiapi/api/management/commands/createsuperuser.py b/dandiapi/api/management/commands/createsuperuser.py index ebc281267..2bf5d644b 100644 --- a/dandiapi/api/management/commands/createsuperuser.py +++ b/dandiapi/api/management/commands/createsuperuser.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from django.db.models.signals import post_save +from django.db.models.signals import post_init, post_save from resonant_settings.allauth_support.management.commands import createsuperuser from dandiapi.api.models.user import UserMetadata @@ -11,16 +11,22 @@ from resonant_settings.allauth_support.createsuperuser import EmailAsUsernameProxyUser +def populate_name(instance: EmailAsUsernameProxyUser, *args, **kwargs): + instance.first_name = 'Super' + instance.last_name = 'User' + + def create_usermetadata(instance: EmailAsUsernameProxyUser, *args, **kwargs): UserMetadata.objects.create(user=instance, status=UserMetadata.Status.APPROVED) class Command(createsuperuser.Command): def handle(self, *args, **kwargs) -> str | None: - # Temporarily connect a post_save signal handler so that we can catch the creation of + # Temporarily connect post_* signal handlers so that we can catch the creation of # this superuser. Note, we do this in the handle() method to ensure this only happens # when this management command is actually run. post_save.connect(create_usermetadata, sender=createsuperuser.user_model) + post_init.connect(populate_name, sender=createsuperuser.user_model) # Save the return value of the parent class function so we can return it later return_value = super().handle(*args, **kwargs) @@ -29,5 +35,6 @@ def handle(self, *args, **kwargs) -> str | None: # unexpected behavior if, for example, someone extends this command and doesn't # realize there's a signal handler attached dynamically. post_save.disconnect(create_usermetadata, sender=createsuperuser.user_model) + post_init.disconnect(populate_name, sender=createsuperuser.user_model) return return_value diff --git a/dandiapi/api/services/publish/__init__.py b/dandiapi/api/services/publish/__init__.py index 59042d9a5..216b32c4e 100644 --- a/dandiapi/api/services/publish/__init__.py +++ b/dandiapi/api/services/publish/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import datetime from typing import TYPE_CHECKING @@ -86,24 +87,25 @@ def _lock_dandiset_for_publishing(*, user: User, dandiset: Dandiset) -> None: # def _build_publishable_version_from_draft(draft_version: Version) -> Version: - publishable_version = Version( - dandiset=draft_version.dandiset, - name=draft_version.name, - metadata=draft_version.metadata, - status=Version.Status.VALID, - version=Version.next_published_version(draft_version.dandiset), - ) + # Make a deep copy of the dict to avoid mutating the draft version's metadata. + publishable_version_metadata = copy.deepcopy(draft_version.metadata) now = datetime.datetime.now(datetime.UTC) # inject the publishedBy and datePublished fields - publishable_version.metadata.update( + publishable_version_metadata.update( { 'publishedBy': draft_version.published_by(now), 'datePublished': now.isoformat(), } ) - return publishable_version + return Version( + dandiset=draft_version.dandiset, + name=draft_version.name, + metadata=publishable_version_metadata, + status=Version.Status.VALID, + version=Version.next_published_version(draft_version.dandiset), + ) def _publish_dandiset(dandiset_id: int, user_id: int) -> None: diff --git a/dandiapi/api/tests/test_version.py b/dandiapi/api/tests/test_version.py index 8d3d5057c..9aea61ded 100644 --- a/dandiapi/api/tests/test_version.py +++ b/dandiapi/api/tests/test_version.py @@ -357,20 +357,27 @@ def test_version_aggregate_assets_summary(draft_asset_factory): @pytest.mark.django_db -def test_version_publish_updates_draft_version_assets_summary(draft_asset_factory): +def test_version_publish_draft_version_metadata_updates(draft_asset_factory): user = UserFactory.create() version = DraftVersionFactory.create(status=Version.Status.PUBLISHING) asset = draft_asset_factory(status=Asset.Status.VALID) version.assets.add(asset) tasks.publish_dandiset_task(version.dandiset.id, user.id) + published_version = Version.objects.latest('created') + # Ensure draft assets summary has been updated version.refresh_from_db() draft_asset_summary = version.metadata['assetsSummary'] - published_asset_summary = Version.objects.latest('created').metadata['assetsSummary'] - + published_asset_summary = published_version.metadata['assetsSummary'] assert published_asset_summary == draft_asset_summary + # Ensure published version contains expected new fields, and draft version does not + assert 'publishedBy' in published_version.metadata + assert 'datePublished' in published_version.metadata + assert 'publishedBy' not in version.metadata + assert 'datePublished' not in version.metadata + @pytest.mark.django_db def test_version_aggregate_assets_summary_metadata_modified(draft_asset_factory): diff --git a/dandiapi/api/views/info.py b/dandiapi/api/views/info.py index 561006a0c..fa2c410ac 100644 --- a/dandiapi/api/views/info.py +++ b/dandiapi/api/views/info.py @@ -53,6 +53,7 @@ def __init__(self, *args, **kwargs): # Schema schema_version = serializers.CharField() schema_url = serializers.URLField() + allowed_schema_versions = serializers.ListField(child=serializers.CharField()) # Versions version = serializers.CharField() @@ -73,6 +74,7 @@ def info_view(request): data={ 'schema_version': settings.DANDI_SCHEMA_VERSION, 'schema_url': get_schema_url(), + 'allowed_schema_versions': settings.ALLOWED_DANDI_SCHEMA_VERSIONS, 'version': importlib.metadata.version('dandiapi'), 'cli-minimal-version': '0.60.0', 'cli-bad-versions': [], diff --git a/dandiapi/settings/base.py b/dandiapi/settings/base.py index ec32d4395..7af8db68b 100644 --- a/dandiapi/settings/base.py +++ b/dandiapi/settings/base.py @@ -7,6 +7,7 @@ from urllib.parse import urlunparse from corsheaders.defaults import default_headers +from dandischema.consts import ALLOWED_INPUT_SCHEMAS from dandischema.consts import DANDI_SCHEMA_VERSION as _DEFAULT_DANDI_SCHEMA_VERSION import django_stubs_ext from environ import Env @@ -175,6 +176,7 @@ DANDI_SCHEMA_VERSION: str = env.str( 'DJANGO_DANDI_SCHEMA_VERSION', default=_DEFAULT_DANDI_SCHEMA_VERSION ) +ALLOWED_DANDI_SCHEMA_VERSIONS: list[str] = ALLOWED_INPUT_SCHEMAS DANDI_ZARR_PREFIX_NAME: str = env.str('DJANGO_DANDI_ZARR_PREFIX_NAME', default='zarr')