Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dandiset star functionality with UI components #2123

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2bfa8d1
Add Dandiset star functionality with UI components
bendichter Dec 26, 2024
d5546c3
Delete web/src/components/NavigationDrawer.vue
bendichter Dec 26, 2024
645c326
Delete web/src/stores/auth.ts
bendichter Dec 26, 2024
db92523
Ruff compliance for DandisetStar model and admin integration
bendichter Dec 26, 2024
05ed15b
Update pre-commit configuration to exclude package-lock.json from checks
bendichter Dec 26, 2024
61295ab
Add star count and starred status to Dandiset and Version tests
bendichter Dec 26, 2024
dd9bd64
Add tests for Dandiset star functionality and update API responses
bendichter Dec 26, 2024
effd30b
Enhance Dandiset model and StarredDandisetsView to support star funct…
bendichter Dec 26, 2024
3122129
Refactor Dandiset data handling in StarredDandisetsView
bendichter Dec 26, 2024
a44586d
Refactor Dandiset data handling in StarredDandisetsView
bendichter Dec 26, 2024
9308df8
Delete web/package-lock.json
bendichter Dec 26, 2024
cd74534
Update .pre-commit-config.yaml
bendichter Dec 26, 2024
e4dbf30
Implement star/unstar functionality for Dandiset model
bendichter Dec 26, 2024
ba15139
Update dandiapi/api/services/dandiset/__init__.py
bendichter Dec 29, 2024
655e3a4
Update dandiapi/api/services/dandiset/__init__.py
bendichter Dec 29, 2024
988888d
Update dandiapi/api/views/dandiset.py
bendichter Dec 29, 2024
02dfba1
Fix N+1 query problem for dandiset star info
jjnesbitt Dec 30, 2024
b713e30
Fix linting error
jjnesbitt Dec 30, 2024
b96d9e7
Reorder StarredDandisetView in router.ts
jjnesbitt Dec 30, 2024
78e6295
Add button for "Starred Dandisets" on app bar
jjnesbitt Dec 30, 2024
bb14cbf
Incorporate starred dandisets into dandiset list endpoint
jjnesbitt Dec 30, 2024
c50f705
Fix behavior of starred dandisets page
jjnesbitt Dec 30, 2024
7b4441d
Revert change to yarn.lock
jjnesbitt Dec 31, 2024
226b5ba
Fix missing context for version detail serializer
jjnesbitt Dec 31, 2024
0d1aace
Fix behavior of StarButton
jjnesbitt Dec 31, 2024
3e4008a
Replace unique_together with UniqueConstraint
jjnesbitt Jan 3, 2025
a83ba49
Move authentication check into star service functions
jjnesbitt Jan 3, 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
10 changes: 10 additions & 0 deletions dandiapi/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AssetBlob,
AuditRecord,
Dandiset,
DandisetStar,
Upload,
UserMetadata,
Version,
Expand Down Expand Up @@ -266,3 +267,12 @@ def has_change_permission(self, request, obj=None):

def has_delete_permission(self, request, obj=None):
return False


@admin.register(DandisetStar)
class DandisetStarAdmin(admin.ModelAdmin):
list_display = ('user', 'dandiset', 'created')
list_filter = ('created',)
search_fields = ('user__username', 'dandiset__id')
raw_id_fields = ('user', 'dandiset')
date_hierarchy = 'created'
53 changes: 53 additions & 0 deletions dandiapi/api/migrations/0014_dandisetstar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.17 on 2025-01-03 21:10
from __future__ import annotations

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0013_remove_assetpath_consistent_slash_and_more'),
]

operations = [
migrations.CreateModel(
name='DandisetStar',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('created', models.DateTimeField(auto_now_add=True)),
(
'dandiset',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='stars',
to='api.dandiset',
),
),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='starred_dandisets',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddConstraint(
model_name='dandisetstar',
constraint=models.UniqueConstraint(
fields=('user', 'dandiset'), name='unique-user-dandiset-star'
),
),
]
3 changes: 2 additions & 1 deletion dandiapi/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .asset import Asset, AssetBlob
from .asset_paths import AssetPath, AssetPathRelation
from .audit import AuditRecord
from .dandiset import Dandiset
from .dandiset import Dandiset, DandisetStar
from .oauth import StagingApplication
from .upload import Upload
from .user import UserMetadata
Expand All @@ -16,6 +16,7 @@
'AssetPathRelation',
'AuditRecord',
'Dandiset',
'DandisetStar',
'StagingApplication',
'Upload',
'UserMetadata',
Expand Down
24 changes: 24 additions & 0 deletions dandiapi/api/models/dandiset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from django.contrib.auth.models import User
from django.db import models
from django_extensions.db.models import TimeStampedModel
from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase
Expand Down Expand Up @@ -112,10 +113,33 @@ def published_count(cls):
def __str__(self) -> str:
return self.identifier

@property
def star_count(self):
return self.stars.count()

def is_starred_by(self, user):
if not user.is_authenticated:
return False
return self.stars.filter(user=user).exists()


class DandisetUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(Dandiset, on_delete=models.CASCADE)


class DandisetGroupObjectPermission(GroupObjectPermissionBase):
content_object = models.ForeignKey(Dandiset, on_delete=models.CASCADE)


class DandisetStar(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='starred_dandisets')
dandiset = models.ForeignKey(Dandiset, on_delete=models.CASCADE, related_name='stars')
created = models.DateTimeField(auto_now_add=True)

class Meta:
constraints = [
models.UniqueConstraint(name='unique-user-dandiset-star', fields=['user', 'dandiset'])
]

def __str__(self) -> str:
return f'Star {self.user.username} ★ {self.dandiset.identifier}'
44 changes: 42 additions & 2 deletions dandiapi/api/services/dandiset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
from django.db import transaction
from guardian.shortcuts import assign_perm

from dandiapi.api.models.dandiset import Dandiset
from dandiapi.api.models.dandiset import Dandiset, DandisetStar
from dandiapi.api.models.version import Version
from dandiapi.api.services import audit
from dandiapi.api.services.dandiset.exceptions import DandisetAlreadyExistsError
from dandiapi.api.services.embargo.exceptions import DandisetUnembargoInProgressError
from dandiapi.api.services.exceptions import AdminOnlyOperationError, NotAllowedError
from dandiapi.api.services.exceptions import (
AdminOnlyOperationError,
NotAllowedError,
NotAuthenticatedError,
)
from dandiapi.api.services.version.metadata import _normalize_version_metadata


Expand Down Expand Up @@ -74,3 +78,39 @@ def delete_dandiset(*, user, dandiset: Dandiset) -> None:

dandiset.versions.all().delete()
dandiset.delete()


def star_dandiset(*, user, dandiset: Dandiset) -> int:
"""
Star a Dandiset for a user.

Args:
user: The user starring the Dandiset.
dandiset: The Dandiset to star.

Returns:
The new star count for the Dandiset.
"""
if not user.is_authenticated:
raise NotAuthenticatedError

DandisetStar.objects.get_or_create(user=user, dandiset=dandiset)
return dandiset.star_count


def unstar_dandiset(*, user, dandiset: Dandiset) -> int:
"""
Unstar a Dandiset for a user.

Args:
user: The user unstarring the Dandiset.
dandiset: The Dandiset to unstar.

Returns:
The new star count for the Dandiset.
"""
if not user.is_authenticated:
raise NotAuthenticatedError

DandisetStar.objects.filter(user=user, dandiset=dandiset).delete()
return dandiset.star_count
5 changes: 5 additions & 0 deletions dandiapi/api/services/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ class NotAllowedError(DandiError):
http_status_code = status.HTTP_403_FORBIDDEN


class NotAuthenticatedError(DandiError):
message = 'Action requires authentication.'
http_status_code = status.HTTP_401_UNAUTHORIZED


class AdminOnlyOperationError(DandiError):
pass
Loading