Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d18aea8
feat: setup endpoint for top identifiers
annavik May 7, 2026
4ba75dd
feat: add overview section to project summary with top identifiers
annavik May 7, 2026
8dbf006
feat: add top taxa to overview section
annavik May 7, 2026
f450128
feat: add latest occurrences to overview section
annavik May 7, 2026
b9a2c3e
feat: handle loading, error and empty states
annavik May 7, 2026
f42c462
layout: adjust layout for small screens
annavik May 7, 2026
f39ea6c
fix: replace hard coded strings
annavik May 7, 2026
fe1862e
feat: add links to occurrences and taxa
annavik May 7, 2026
2a3a997
Merge branch 'main' into feat/project-overview
annavik May 7, 2026
0f9f1c0
fix: cleanup
annavik May 7, 2026
8494ad3
fix(project-overview): address PR review feedback
mihow May 13, 2026
7758499
fix(api): gate top-identifiers endpoint on project visibility
mihow May 13, 2026
b671d69
fix(api): count distinct occurrences in top-identifiers leaderboard
mihow May 13, 2026
253d5dc
refactor(summary): extract SummaryColumn to dedupe overview columns
mihow May 13, 2026
86d2f90
docs: plan for /occurrences/stats/ endpoint convention + top-identifi…
mihow May 14, 2026
5d20619
docs: lock decisions on /occurrences/stats/ pattern after 2nd review
mihow May 14, 2026
ba00cdd
docs: snapshot Anna's PR #1296 body before adding Claude commits
mihow May 14, 2026
c83ebae
refactor(stats): extract top_identifiers query into models_future
mihow May 14, 2026
a50abd3
feat(stats): add /occurrences/stats/top-identifiers/ endpoint
mihow May 14, 2026
1a176c3
refactor(stats): flip FE to /occurrences/stats/, delete legacy backend
mihow May 14, 2026
de2ddc5
docs(api): convention reference for /<entity>/stats/<kind>/ endpoints
mihow May 14, 2026
d649c1f
Merge remote-tracking branch 'origin/main' into worktree-project-over…
mihow May 14, 2026
e4a0d29
docs(api): expand future-stats examples (deployments, agreement, spec…
mihow May 14, 2026
4c9acd5
refactor(stats): use paginator + add scalar-shape placeholder
mihow May 14, 2026
fbf4382
refactor(stats): simplify to scalar shape, defer paginator pattern
mihow May 14, 2026
7209708
chore(api_router): collapse stats-route ordering comment to one line
mihow May 14, 2026
1da2943
chore(docs): archive PR #1296 stats migration plan [no ci]
mihow May 14, 2026
db0157a
ci: trigger checks on current head
mihow May 14, 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
12 changes: 12 additions & 0 deletions ami/main/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,3 +1704,15 @@ class Meta:
"total_size",
"last_checked",
]


class UserIdentificationCountSerializer(serializers.Serializer):
"""
Serializer for user identification counts.
"""

id = serializers.IntegerField()
name = serializers.CharField()
email = serializers.CharField()
image = serializers.CharField(required=False, allow_null=True)
identification_count = serializers.IntegerField()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
56 changes: 55 additions & 1 deletion ami/main/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.postgres.search import TrigramSimilarity
from django.core import exceptions
from django.db import models
from django.db.models import Prefetch, Q
from django.db.models import Count, Prefetch, Q
from django.db.models.functions import Coalesce
from django.db.models.query import QuerySet
from django.forms import BooleanField, CharField, IntegerField
Expand Down Expand Up @@ -90,6 +90,7 @@
TaxonListSerializer,
TaxonSearchResultSerializer,
TaxonSerializer,
UserIdentificationCountSerializer,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -1851,6 +1852,59 @@ def get(self, request):
return Response(data)


class UserIdentificationCountsView(GenericAPIView, ProjectMixin):
"""
API endpoint that returns identification counts for top 5 users.
Optionally restricted to a specific project via project_id query parameter.
"""

permission_classes = [IsActiveStaffOrReadOnly]
serializer_class = UserIdentificationCountSerializer

@extend_schema(parameters=[project_id_doc_param])
def get(self, request):
"""
Return top 5 users by identification count, optionally filtered by project.
"""
project = self.get_active_project()

# Start with user queryset
user_queryset = User.objects.all()

# Filter by project if provided, then annotate with count
if project:
user_queryset = (
user_queryset.filter(identifications__occurrence__project=project)
.annotate(identification_count=Count("identifications", distinct=True))
.distinct()
)
else:
user_queryset = user_queryset.annotate(identification_count=Count("identifications"))

# Get top 5 users, ordered by identification count (descending)
top_identifiers = user_queryset.filter(identification_count__gt=0).order_by("-identification_count")[:5]

# Prepare serialized data
data = []
for user in top_identifiers:
data.append(
{
"id": user.id,
"name": user.name if user.name else None,
"email": user.email,
"image": user.image.url if user.image else None,
"identification_count": user.identification_count,
}
)

return Response(
{
"project_id": project.id if project else None,
"top_identifiers": data,
}
)
Comment thread
mihow marked this conversation as resolved.
Outdated


_STORAGE_CONNECTION_STATUS = [
# These come from the ConnetionStatus react component
# @TODO use ENUM
Expand Down
3 changes: 3 additions & 0 deletions config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
path("auth/", include("djoser.urls.authtoken")),
path("status/summary/", views.SummaryView.as_view(), name="status-summary"),
path("status/storage/", views.StorageStatus.as_view(), name="status-storage"),
path(
"users/identifications/top/", views.UserIdentificationCountsView.as_view(), name="user-identification-counts"
Comment thread
mihow marked this conversation as resolved.
Outdated
),
path(
"users/roles/",
RolesAPIView.as_view(),
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/error-state/error-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const ErrorState = ({ compact, error }: ErrorStateProps) => {
return (
<div className="flex items-center gap-2">
<AlertCircleIcon className="w-4 h-4 text-destructive" />
<span className="pt-0.5 body-small text-muted-foreground">{title}</span>
<span className="pt-0.5 text-muted-foreground">{title}</span>
</div>
)
}
Expand Down
29 changes: 29 additions & 0 deletions ui/src/data-services/hooks/identifications/useTopIdentifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { API_ROUTES, API_URL } from 'data-services/constants'
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'

interface Response {
project_id?: number
top_identifiers: {
id: number
name?: string
email: string
image?: string
identification_count: number
}[]
}

export const useTopIdentifiers = (projectId?: string) => {
const url = `${API_URL}/${API_ROUTES.USERS}/${API_ROUTES.IDENTIFICATIONS}/top/`

const { data, isLoading, isFetching, error } = useAuthorizedQuery<Response>({
queryKey: [API_ROUTES.IDENTIFICATIONS, 'top', projectId],
url: projectId ? `${url}?project_id=${projectId}` : url,
})

return {
data,
isLoading,
isFetching,
error,
}
}
11 changes: 11 additions & 0 deletions ui/src/data-services/hooks/occurrences/useLatestOccurrences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useOccurrences } from './useOccurrences'

export const useLatestOccurrences = (projectId: string) => {
const result = useOccurrences({
pagination: { page: 0, perPage: 5 },
projectId,
sort: { field: 'first_appearance_timestamp', order: 'desc' },
})

return result
}
11 changes: 11 additions & 0 deletions ui/src/data-services/hooks/species/useTopSpecies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useSpecies } from './useSpecies'

export const useTopSpecies = (projectId: string) => {
const result = useSpecies({
pagination: { page: 0, perPage: 5 },
projectId,
sort: { field: 'occurrences_count', order: 'desc' },
})

return result
}
2 changes: 1 addition & 1 deletion ui/src/pages/project/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Sidebar = ({ project }: { project: ProjectDetails }) => {
}, [activeItem])

return (
<div className="w-full h-min shrink-0 p-0 rounded-md border border-border overflow-hidden bg-background md:w-72">
<div className="w-full shrink-0 p-0 rounded-md border border-border overflow-hidden bg-background md:w-72">
{project.image ? (
<img src={project.image} alt="" />
) : (
Expand Down
46 changes: 46 additions & 0 deletions ui/src/pages/project/summary/list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ImageIcon, UserIcon } from 'lucide-react'

export const ListItem = ({
count,
item,
}: {
count?: number | string
item: {
image: { src?: string; variant?: 'default' | 'user' }
text: string
title?: string
}
}) => (
<div className="flex items-center gap-4 p-2 pr-4">
{item.image.variant === 'user' ? (
<UserImage image={item.image.src} />
) : (
<Image image={item.image.src} />
)}
<div className="flex flex-col overflow-hidden">
<div className="flex items-center gap-4">
{item.title ? (
<span className="truncate font-medium">{item.title}</span>
) : null}
</div>
<span className="truncate">{item.text}</span>
</div>
{count !== undefined ? (
<span className="body-small grow text-right">
{count.toLocaleString()}
</span>
) : null}
</div>
)

const Image = ({ image }: { image?: string }) => (
<div className="shrink-0 flex items-center justify-center w-12 h-12 border border-border rounded-md text-muted-foreground overflow-hidden">
{image ? <img alt="" src={image} /> : <ImageIcon className="w-4 h-4" />}
</div>
)

const UserImage = ({ image }: { image?: string }) => (
<div className="shrink-0 flex items-center justify-center w-12 h-12 border border-border rounded-full text-muted-foreground overflow-hidden">
{image ? <img alt="" src={image} /> : <UserIcon className="w-4 h-4" />}
</div>
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading