Skip to content
2 changes: 1 addition & 1 deletion src/backend/api/database/categories.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"Categories": [
"Summary",
"Installability",
"Popularity",
"Activity",
Expand All @@ -16,5 +17,4 @@
"Raw Metrics (Measured via scc)",
"Repo Metrics (Measured via GitHub)"
]

}
30 changes: 29 additions & 1 deletion src/backend/api/database/library_metric_values/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ...utils.analysis import enqueue_library_analysis
from ..domain.models import Domain
from ..libraries.models import Library
from ..metrics.models import Metric
from ..metrics.models import Metric, MetricOrder
from .models import LibraryMetricValue


Expand Down Expand Up @@ -184,10 +184,38 @@ def analyze_domain_libraries(request, domain_id):
@api_view(["GET"])
def domain_comparison(request, domain_id):
domain = get_object_or_404(Domain, pk=domain_id)
path = os.path.join(settings.BASE_DIR, "api", "database", "categories.json")

libraries = Library.objects.filter(domain=domain)
metrics = Metric.objects.all()

categories_order = []
with open(path, "r") as f:
categories_order = json.load(f).get("Categories", [])

# Sort metrics based on MetricOrder
metric_order_obj = MetricOrder.objects.first()
if metric_order_obj and metric_order_obj.category_order:
metrics_category = metric_order_obj.category_order
category_ordered = [
cat
for cat in categories_order
if cat in metrics_category and metrics_category[cat]
]
ordered_metric_ids = []

for cat in category_ordered:
ordered_metric_ids.extend(metrics_category[cat])
# Create position dict
position = {mid: i for i, mid in enumerate(ordered_metric_ids)}
# Sort metrics
metrics = sorted(
metrics,
key=lambda m: position.get(str(m.metric_ID), len(ordered_metric_ids)),
)
else:
metrics = list(metrics)

table = []
by_lib = {}

Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.core.management.base import BaseCommand

from api.database.metrics.models import Metric, MetricOrder


class Command(BaseCommand):
help = "Populate MetricOrder with current metrics organized by category"

def handle(self, *args, **options):
try:
# Get all metrics grouped by category
metrics = Metric.objects.all().order_by("category", "metric_name")

category_order = {}
for metric in metrics:
category = metric.category or "Uncategorized"
if category not in category_order:
category_order[category] = []
category_order[category].append(str(metric.metric_ID))

# Get or create the MetricOrder instance
metric_order, created = MetricOrder.objects.get_or_create(pk=1)
metric_order.category_order = category_order
metric_order.save()

if created:
self.stdout.write(
self.style.SUCCESS(
f"✓ Created MetricOrder with {len(metrics)} metrics"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f"✓ Updated MetricOrder with {len(metrics)} metrics"
)
)

# Print summary
self.stdout.write("\nMetrics by category:")
for category, metric_ids in sorted(category_order.items()):
self.stdout.write(f" {category}: {len(metric_ids)} metrics")

except Exception as e:
self.stdout.write(
self.style.ERROR(f"✗ Error populating MetricOrder: {str(e)}")
)
38 changes: 38 additions & 0 deletions src/backend/api/database/metrics/migrations/0009_metricorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.2.7 on 2026-05-04 02:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("metrics", "0008_alter_metric_value_type"),
]

operations = [
migrations.CreateModel(
name="MetricOrder",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"category_order",
models.JSONField(
default=dict,
help_text="JSON object mapping category names to ordered lists of metric IDs",
),
),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name_plural": "Metric Orders",
},
),
]
16 changes: 16 additions & 0 deletions src/backend/api/database/metrics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,19 @@ class Metric(models.Model):

def __str__(self):
return self.metric_name


class MetricOrder(models.Model):
"""Stores the display order of metrics by category"""

category_order = models.JSONField(
default=dict,
help_text="JSON object mapping category names to ordered lists of metric IDs",
)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name_plural = "Metric Orders"

def __str__(self):
return f"Metric Order (updated {self.updated_at})"
2 changes: 2 additions & 0 deletions src/backend/api/database/metrics/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
MetricCategoryView,
MetricListCreateView,
MetricListFlatView,
MetricReorderView,
MetricRetrieveUpdateDestroyView,
MetricRulesView,
MetricUpdateWeightView,
Expand All @@ -14,6 +15,7 @@
path("", MetricListCreateView.as_view(), name="metric-list-create"),
path("auto-options/", AutoMetricOptionsView.as_view(), name="metric-auto-options"),
path("all/", MetricListFlatView.as_view(), name="metric-all-flat"),
path("reorder/", MetricReorderView.as_view(), name="metric-reorder"),
path("rules/", MetricRulesView.as_view(), name="metric-rules"),
path("categories/", MetricCategoryView.as_view(), name="metric-categories"),
path(
Expand Down
80 changes: 79 additions & 1 deletion src/backend/api/database/metrics/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import os

from django.conf import settings
Expand All @@ -8,9 +9,11 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Metric
from .models import Metric, MetricOrder
from .serializers import FlatMetricSerializer, MetricSerializer

logger = logging.getLogger(__name__)


def load_auto_metric_definitions():
path = os.path.join(settings.BASE_DIR, "api", "database", "auto_metrics.json")
Expand Down Expand Up @@ -134,3 +137,78 @@ def patch(self, request, metric_id):
class MetricListFlatView(generics.ListAPIView):
serializer_class = FlatMetricSerializer
queryset = Metric.objects.all().order_by("metric_name")


class MetricReorderView(APIView):
permission_classes = [IsAuthenticatedOrReadOnly] # noqa: F811

def get(self, request):
"""Retrieve the current metric display order"""
try:
metric_order = MetricOrder.objects.first()
if metric_order:
return Response(
{"category_order": metric_order.category_order},
status=status.HTTP_200_OK,
)
else:
return Response(
{"category_order": {}},
status=status.HTTP_200_OK,
)
except Exception as e:
logger.exception("Failed to retrieve metric display order: %s", e)
return Response(
{"error": "An internal error has occurred."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

def post(self, request):
"""Save the metric display order"""
try:
category_order = request.data.get("category_order")
if category_order is None:
return Response(
{"error": "category_order field is required"},
status=status.HTTP_400_BAD_REQUEST,
)

# Validate that all metric IDs exist
all_metric_ids = set()
for category_metrics in category_order.values():
if isinstance(category_metrics, list):
all_metric_ids.update(category_metrics)

# Check if all metric IDs exist
existing_metrics = set(
str(m_id)
for m_id in Metric.objects.filter(
metric_ID__in=all_metric_ids
).values_list("metric_ID", flat=True)
)

invalid_ids = all_metric_ids - existing_metrics
if invalid_ids:
return Response(
{"error": f"Invalid metric IDs: {list(invalid_ids)}"},
status=status.HTTP_400_BAD_REQUEST,
)

# Get or create the single MetricOrder instance
metric_order, _ = MetricOrder.objects.get_or_create(pk=1)
metric_order.category_order = category_order
metric_order.save()

return Response(
{
"message": "Metric display order updated successfully",
"category_order": metric_order.category_order,
},
status=status.HTTP_200_OK,
)
except Exception as e:
logger.exception("Failed to save metric display order %s", e)
return Response(
{"error": "An internal error has occurred."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
Loading
Loading