Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
dfc7101
Add how to cite tab
bendichter Dec 13, 2025
83b748e
Add warning for citing draft dandisets and remove CiteAsDialog component
bendichter Dec 14, 2025
c0f7c75
Enhance draft citation warnings to include links to the latest publis…
bendichter Dec 14, 2025
50ba3b0
Revert accidental web/package-lock.json changes
bendichter Dec 16, 2025
9d7d72b
Implement citation format selection and add CFF utility functions for…
bendichter Dec 16, 2025
9f25a33
Merge pull request #154 from aplbrain/apl-setup
NEStock Dec 23, 2025
20ab4ac
Don't return `null` names or usernames from user search endpoint
jjnesbitt Dec 30, 2025
cf725e6
Update URL for user support
kabilar Jan 9, 2026
afc984d
Fix propagation of embargoed date from dandisets to assets
h-mayorquin Jan 19, 2026
4f98a87
Merge pull request #175 from aplbrain/apl-setup
NEStock Jan 22, 2026
29e3884
Fix dandiset ID parsing during upload initialization
williamjallen Jan 19, 2026
a265243
Merge pull request #2696 from dandi/fix-upload-init-id-parsing
jjnesbitt Jan 27, 2026
cf188ed
Update copyright year in footer
kabilar Jan 30, 2026
c6ec6fa
Merge pull request #2688 from kabilar/support
waxlamp Feb 2, 2026
2a2ebb8
auto shipit - CHANGELOG.md etc
dandibot Feb 2, 2026
241ae9f
Merge pull request #2701 from kabilar/footer-year
waxlamp Feb 5, 2026
538abed
auto shipit - CHANGELOG.md etc
dandibot Feb 5, 2026
c7baa97
Don't use one-liner ifs without braces
jjnesbitt Feb 6, 2026
8e08a2a
Add e2e tests for How to Cite tab
bendichter Feb 6, 2026
ffd4ba2
test: pass -s to pytest so prompts etc would be displayed
yarikoptic Dec 2, 2025
c88d5c3
bf(test): test_nwb2asset_remote_asset which might be stalling here
yarikoptic Dec 3, 2025
a3a1b94
Merge pull request #2662 from dandi/fix-cli-test-halt-s
yarikoptic Feb 7, 2026
b54f73a
Remove handling of missing citation case
jjnesbitt Feb 9, 2026
bca4f76
Merge pull request #2671 from dandi/how-to-cite-tab
bendichter Feb 9, 2026
c89ed0d
Improve setting of asset embargo end date
jjnesbitt Feb 12, 2026
60544b2
Handle zarr and asset blob cases separately
jjnesbitt Feb 13, 2026
220b536
Revert "bf(test): test_nwb2asset_remote_asset which might be stalling…
yarikoptic Feb 14, 2026
042b969
bf: fix GarbageCollectionEvent.__str__ referencing nonexistent field
yarikoptic Feb 16, 2026
0d26df3
doc: fix incorrect manage.py path in e2e README
yarikoptic Feb 16, 2026
ccbd66f
doc: use consistent ./manage.py invocation in DEVELOPMENT.md
yarikoptic Feb 16, 2026
8ce0267
Remove superfluous tests
jjnesbitt Feb 16, 2026
8fe8c50
Merge pull request #2710 from dandi/bf-gc-event-str
jjnesbitt Feb 16, 2026
a25d1bf
Merge pull request #2709 from dandi/rf-docs-fixes
jjnesbitt Feb 16, 2026
090fc71
Merge pull request #2708 from dandi/bf-hang
jjnesbitt Feb 16, 2026
269d033
Add tests for asset access metadata
jjnesbitt Feb 16, 2026
d89b91d
Use zarr dandiset draft version directly
jjnesbitt Feb 17, 2026
25e55d7
Use .save in add_asset_to_version
jjnesbitt Feb 17, 2026
a3993b7
Fix incorrectly formed test
jjnesbitt Feb 17, 2026
a1ec2c2
Don't return excessively
jjnesbitt Feb 17, 2026
3e81184
Remove outdated test
jjnesbitt Feb 17, 2026
b4648e0
Merge pull request #2698 from h-mayorquin/fix_embargo_date_propagation
jjnesbitt Feb 17, 2026
34c0a46
auto shipit - CHANGELOG.md etc
dandibot Feb 17, 2026
28c50d3
Move asset blob unembargo to _add_asset_to_version
jjnesbitt Feb 18, 2026
2f20986
Merge pull request #2713 from dandi/fix-open-asset-update-to-embargoe…
jjnesbitt Feb 18, 2026
836ea7d
auto shipit - CHANGELOG.md etc
dandibot Feb 18, 2026
10bfe33
Remove redundant `DANDI_DANDISETS_BUCKET_NAME` setting
brianhelba Feb 26, 2026
017d48c
Upgrade to Resonant v0.44.0
brianhelba Feb 26, 2026
afb02f3
Upgrade to Resonant v0.47
brianhelba Feb 26, 2026
613c32a
Merge pull request #2681 from dandi/fix-missing-user-names-user-search
jjnesbitt Mar 5, 2026
143924f
Add scrollbar to Meditor component for overflowing content
kabilar Mar 6, 2026
a1e7f18
Merge pull request #2724 from kabilar/meditor
jjnesbitt Mar 6, 2026
6be5a8c
auto shipit - CHANGELOG.md etc
dandibot Mar 6, 2026
08d63be
Merge pull request #2692 from dandi/resonant-upgrade
jjnesbitt Mar 6, 2026
c07610b
auto shipit - CHANGELOG.md etc
dandibot Mar 6, 2026
d7a92f6
Merge commit 'c07610b18d993296f2cef5b968642ed130da5895' into 177-sync…
NEStock Mar 10, 2026
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: 2 additions & 1 deletion .copier-answers.resonant.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
_commit: v0.41.0
_commit: v0.47.1
_src_path: gh:kitware-resonant/cookiecutter-resonant
core_app_name: api
include_example_code: false
project_name: DANDI Archive
project_slug: dandiapi
python_package_name: dandiapi
site_domain: api.dandiarchive.org
use_asgi: false
21 changes: 14 additions & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[*.py]
indent_size = 4
max_line_length = 100
[*.{css,pcss}]
indent_size = 2

[*.html]
indent_size = 2

[*.css]
[*.ini]
indent_size = 4

[*.{js,ts,vue}]
indent_size = 2
max_line_length = 100

[{*.yml,*.yaml}]
[*.{json,jsonc,json5}]
indent_size = 2

[*.ini]
[*.py]
indent_size = 4
max_line_length = 100

[*.toml]
indent_size = 2

[*.sh]
[*.{yml,yaml}]
indent_size = 2
8 changes: 2 additions & 6 deletions .github/workflows/cli-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,15 @@ jobs:
if: matrix.dandi-version == 'release'
run: >
uvx --with dandi[test]
pytest --pyargs -v --dandi-api --deselect=tests/test_keyring.py::test_dandi_authenticate_no_env_var dandi
# TODO: Revert back to
# pytest --pyargs -v --dandi-api dandi
pytest --pyargs -v -s --dandi-api dandi
env:
DANDI_TESTS_PERSIST_DOCKER_COMPOSE: "1"

- name: Run dandi-api tests in dandi-cli
if: matrix.dandi-version == 'prod'
run: >
uvx --with "dandi[test] @ git+https://github.com/dandi/dandi-cli"
pytest --pyargs -v --dandi-api --deselect=tests/test_keyring.py::test_dandi_authenticate_no_env_var dandi
# TODO: Revert back to
# pytest --pyargs -v --dandi-api dandi
pytest --pyargs -v -s --dandi-api dandi
env:
DANDI_TESTS_PERSIST_DOCKER_COMPOSE: "1"

Expand Down
98 changes: 98 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,101 @@
# v0.21.8 (Fri Mar 06 2026)

#### 🐛 Bug Fix

- Upgrade to Resonant v0.47 [#2692](https://github.com/dandi/dandi-archive/pull/2692) ([@brianhelba](https://github.com/brianhelba))

#### Authors: 1

- Brian Helba ([@brianhelba](https://github.com/brianhelba))

---

# v0.21.7 (Fri Mar 06 2026)

#### 🐛 Bug Fix

- Add scrollbar to meditor component for overflowing content [#2724](https://github.com/dandi/dandi-archive/pull/2724) ([@kabilar](https://github.com/kabilar))
- Don't return `null` names or usernames from user search endpoint [#2681](https://github.com/dandi/dandi-archive/pull/2681) ([@jjnesbitt](https://github.com/jjnesbitt))

#### Authors: 2

- Jacob Nesbitt ([@jjnesbitt](https://github.com/jjnesbitt))
- Kabilar Gunalan ([@kabilar](https://github.com/kabilar))

---

# v0.21.6 (Wed Feb 18 2026)

#### 🐛 Bug Fix

- Properly unembargo blobs when asset is updated [#2713](https://github.com/dandi/dandi-archive/pull/2713) ([@jjnesbitt](https://github.com/jjnesbitt))

#### Authors: 1

- Jacob Nesbitt ([@jjnesbitt](https://github.com/jjnesbitt))

---

# v0.21.5 (Tue Feb 17 2026)

:tada: This release contains work from a new contributor! :tada:

Thank you, Heberto Mayorquin ([@h-mayorquin](https://github.com/h-mayorquin)), for all your work!

#### 🐛 Bug Fix

- Fix propagation of embargoed date from dandisets to assets [#2698](https://github.com/dandi/dandi-archive/pull/2698) ([@h-mayorquin](https://github.com/h-mayorquin) [@jjnesbitt](https://github.com/jjnesbitt))
- bf: fix GarbageCollectionEvent.__str__ referencing nonexistent field [#2710](https://github.com/dandi/dandi-archive/pull/2710) ([@yarikoptic](https://github.com/yarikoptic) [@jjnesbitt](https://github.com/jjnesbitt))
- Add "How to Cite" Tab to Dandiset Landing Page [#2671](https://github.com/dandi/dandi-archive/pull/2671) ([@bendichter](https://github.com/bendichter) [@jjnesbitt](https://github.com/jjnesbitt))

#### 📝 Documentation

- Fixes of minor inconsistencies in docs claude identified [#2709](https://github.com/dandi/dandi-archive/pull/2709) ([@yarikoptic](https://github.com/yarikoptic))

#### 🧪 Tests

- Revert "bf(test): test_nwb2asset_remote_asset which might be stalling here" [#2708](https://github.com/dandi/dandi-archive/pull/2708) ([@yarikoptic](https://github.com/yarikoptic))
- ci(tests): pass -s to pytest and skip hanging test_nwb2asset_remote_asset [#2662](https://github.com/dandi/dandi-archive/pull/2662) ([@yarikoptic](https://github.com/yarikoptic))

#### Authors: 4

- Ben Dichter ([@bendichter](https://github.com/bendichter))
- Heberto Mayorquin ([@h-mayorquin](https://github.com/h-mayorquin))
- Jacob Nesbitt ([@jjnesbitt](https://github.com/jjnesbitt))
- Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic))

---

# v0.21.4 (Thu Feb 05 2026)

#### 🐛 Bug Fix

- Update copyright year in footer [#2701](https://github.com/dandi/dandi-archive/pull/2701) ([@kabilar](https://github.com/kabilar))

#### Authors: 1

- Kabilar Gunalan ([@kabilar](https://github.com/kabilar))

---

# v0.21.3 (Mon Feb 02 2026)

:tada: This release contains work from a new contributor! :tada:

Thank you, William Allen ([@williamjallen](https://github.com/williamjallen)), for all your work!

#### 🐛 Bug Fix

- Update URL for user support [#2688](https://github.com/dandi/dandi-archive/pull/2688) ([@kabilar](https://github.com/kabilar))
- Fix dandiset ID parsing during upload initialization [#2696](https://github.com/dandi/dandi-archive/pull/2696) ([@williamjallen](https://github.com/williamjallen))

#### Authors: 2

- Kabilar Gunalan ([@kabilar](https://github.com/kabilar))
- William Allen ([@williamjallen](https://github.com/williamjallen))

---

# v0.21.2 (Tue Jan 13 2026)

#### 🐛 Bug Fix
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ For frequent deployment administration tasks, `django-extensions` provides a con
### create_dev_dandiset

```
python manage.py create_dev_dandiset --owner $(git config user.email) --name My Dummy Dandiset
./manage.py create_dev_dandiset --owner $(git config user.email) --name My Dummy Dandiset
```

This creates a dummy dandiset with valid metadata and a single dummy asset.
Expand Down
Empty file.
Empty file.
19 changes: 6 additions & 13 deletions dandiapi/api/management/commands/cleanup_blobs.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
from __future__ import annotations

from django.conf import settings
from django.core.files.storage import default_storage
import djclick as click
from storages.backends.s3 import S3Storage

from dandiapi.api.models.upload import AssetBlob

BUCKET = settings.DANDI_DANDISETS_BUCKET_NAME


def s3_client():
storage = S3Storage(bucket_name=BUCKET)
return storage.connection.meta.client


@click.command()
@click.option('--delete', is_flag=True, default=False)
def cleanup_blobs(*, delete: bool):
client = s3_client()
client = default_storage.s3_client
bucket_name = default_storage.bucket_name
# Ignore pagination for now, hopefully there aren't enough objects to matter
objs = client.list_object_versions(Bucket=BUCKET, Prefix='dev/')
objs = client.list_object_versions(Bucket=bucket_name, Prefix='dev/')
for version in objs['Versions']:
if not AssetBlob.objects.filter(etag=version['ETag'][1:-1]).exists():
click.echo(f'Deleting version {version["Key"]}')
if delete:
client.delete_object(
Bucket=BUCKET, Key=version['Key'], VersionId=version['VersionId']
Bucket=bucket_name, Key=version['Key'], VersionId=version['VersionId']
)
for delete_marker in objs['DeleteMarkers']:
click.echo(f'Deleting delete marker {delete_marker["Key"]}')
if delete:
client.delete_object(
Bucket=BUCKET, Key=delete_marker['Key'], VersionId=delete_marker['VersionId']
Bucket=bucket_name, Key=delete_marker['Key'], VersionId=delete_marker['VersionId']
)
3 changes: 2 additions & 1 deletion dandiapi/api/management/commands/createsuperuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from typing import TYPE_CHECKING

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

from resonant_settings.allauth_support.management.commands import createsuperuser

if TYPE_CHECKING:
from resonant_settings.allauth_support.createsuperuser import EmailAsUsernameProxyUser

Expand Down
6 changes: 3 additions & 3 deletions dandiapi/api/migrations/0001_default_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db.migrations.state import StateApps


def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor):
def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
Site = apps.get_model('sites', 'Site')

# A default site object may or may not exist.
Expand All @@ -27,7 +27,7 @@ def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
)


def rollback_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor):
def rollback_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
Site = apps.get_model('sites', 'Site')

# This is the initial value of the default site object, as populated by the sites app.
Expand All @@ -42,5 +42,5 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(update_default_site, rollback_default_site),
migrations.RunPython(update_default_site, rollback_default_site, elidable=False),
]
74 changes: 65 additions & 9 deletions dandiapi/api/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.db.models import CharField, DateField, Min, Q
from django.db.models.functions import Cast
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel

from dandiapi.api.models.dandiset import Dandiset
from dandiapi.api.models.metadata import PublishableMetadataMixin

from .version import Version


class EmbargoedAssetWithinOpenDandisetError(Exception):
"""Raised when an embargoed asset exists in an open dandiset."""


ASSET_CHARS_REGEX = r'[A-z0-9(),&\s#+~_=-]'
ASSET_PATH_REGEX = rf'^({ASSET_CHARS_REGEX}?\/?\.?{ASSET_CHARS_REGEX})+$'
ASSET_COMPUTED_FIELDS = [
Expand Down Expand Up @@ -236,23 +243,72 @@ def is_different_from(
def dandi_asset_id(asset_id: str | uuid.UUID):
return f'dandiasset:{asset_id}'

def access_metadata(self):
# Default to open access
embargoed = self.is_embargoed
access = {
'schemaKey': 'AccessRequirements',
'status': AccessType.EmbargoedAccess.value
if embargoed
else AccessType.OpenAccess.value,
}

# Nothing else to do if not embargoed
if not embargoed:
return access

# For zarr assets, is_embargoed is based on the dandiset directly, and a zarr can
# only be associated with one dandiset, so we know this dandiset is embargoed
if self.zarr is not None:
draft_version = self.zarr.dandiset.draft_version

# EmbargoedUntil isn't guaranteed to be set
embargo_end_date: str | None = draft_version.metadata['access'][0].get('embargoedUntil')
if embargo_end_date is not None:
access['embargoedUntil'] = embargo_end_date

return access

# In the blob case, we need to consider all dandisets this blob might be associated with,
# and take the minimum embargo end date

# This blob should only be associated with embargoed dandisets
open_dandisets = self.blob.assets.filter(
versions__dandiset__embargo_status=Dandiset.EmbargoStatus.OPEN
)
if open_dandisets.exists():
raise EmbargoedAssetWithinOpenDandisetError(
'Embargoed asset contained within OPEN dandiset'
)

# Retrieve the minimum embargo end date, across all dandisets
embargo_end_date: datetime.date | None = (
self.blob.assets.filter(versions__isnull=False)
.annotate(
embargo_end_date=Cast(
Cast('versions__metadata__access__0__embargoedUntil', output_field=CharField()),
output_field=DateField(),
)
)
.aggregate(min_embargo_end_date=Min('embargo_end_date'))['min_embargo_end_date']
)

if embargo_end_date is not None:
access['embargoedUntil'] = embargo_end_date.isoformat()

return access

@property
def full_metadata(self):
download_url = settings.DANDI_API_URL + reverse(
'asset-download',
kwargs={'asset_id': str(self.asset_id)},
)

metadata = {
**self.metadata,
'id': self.dandi_asset_id(self.asset_id),
'access': [
{
'schemaKey': 'AccessRequirements',
'status': AccessType.EmbargoedAccess.value
if self.is_embargoed
else AccessType.OpenAccess.value,
}
],
'access': [self.access_metadata()],
'path': self.path,
'identifier': str(self.asset_id),
'contentUrl': [download_url, self.s3_url],
Expand Down
2 changes: 1 addition & 1 deletion dandiapi/api/models/garbage_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class GarbageCollectionEvent(models.Model):
)

def __str__(self) -> str:
return f'{self.type} ({self.created})'
return f'{self.type} ({self.timestamp})'


class GarbageCollectionEventRecord(models.Model):
Expand Down
Loading
Loading