diff --git a/hub/services/admin/services.py b/hub/services/admin/services.py
index 8d86c08..c975884 100644
--- a/hub/services/admin/services.py
+++ b/hub/services/admin/services.py
@@ -43,11 +43,14 @@ class PlanPriceInline(admin.TabularInline):
class PlanInline(admin.StackedInline):
- """Inline admin for Plan model"""
+ """Inline admin for Plan model with sortable ordering"""
model = Plan
extra = 1
- fieldsets = ((None, {"fields": ("name", "description", "plan_description")}),)
+ fieldsets = (
+ (None, {"fields": ("name", "description", "plan_description")}),
+ ("Display Options", {"fields": ("is_best",)}),
+ )
show_change_link = True # This allows clicking through to the Plan admin where prices can be managed
@@ -138,12 +141,17 @@ class ServiceOfferingAdmin(admin.ModelAdmin):
@admin.register(Plan)
class PlanAdmin(admin.ModelAdmin):
- """Admin configuration for Plan model"""
+ """Admin configuration for Plan model with sortable ordering"""
- list_display = ("name", "offering", "price_summary")
- list_filter = ("offering__service", "offering__cloud_provider")
+ list_display = ("name", "offering", "is_best", "price_summary", "order")
+ list_filter = ("offering__service", "offering__cloud_provider", "is_best")
search_fields = ("name", "description", "offering__service__name")
+ list_editable = ("is_best",)
inlines = [PlanPriceInline]
+ fieldsets = (
+ (None, {"fields": ("name", "offering", "description", "plan_description")}),
+ ("Display Options", {"fields": ("is_best", "order")}),
+ )
def price_summary(self, obj):
"""Display a summary of prices for this plan"""
diff --git a/hub/services/migrations/0038_add_plan_ordering_and_best.py b/hub/services/migrations/0038_add_plan_ordering_and_best.py
new file mode 100644
index 0000000..b02be24
--- /dev/null
+++ b/hub/services/migrations/0038_add_plan_ordering_and_best.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.2 on 2025-06-23 10:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("services", "0037_remove_plan_pricing_planprice"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="plan",
+ options={"ordering": ["order", "name"]},
+ ),
+ migrations.AddField(
+ model_name="plan",
+ name="is_best",
+ field=models.BooleanField(
+ default=False, help_text="Mark this plan as the best/recommended option"
+ ),
+ ),
+ migrations.AddField(
+ model_name="plan",
+ name="order",
+ field=models.PositiveIntegerField(
+ default=0,
+ help_text="Order of this plan in the offering (lower numbers appear first)",
+ ),
+ ),
+ ]
diff --git a/hub/services/models/services.py b/hub/services/models/services.py
index e5b90cb..8b6984d 100644
--- a/hub/services/models/services.py
+++ b/hub/services/models/services.py
@@ -139,16 +139,42 @@ class Plan(models.Model):
ServiceOffering, on_delete=models.CASCADE, related_name="plans"
)
+ # Ordering and highlighting fields
+ order = models.PositiveIntegerField(
+ default=0,
+ help_text="Order of this plan in the offering (lower numbers appear first)",
+ )
+ is_best = models.BooleanField(
+ default=False, help_text="Mark this plan as the best/recommended option"
+ )
+
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
- ordering = ["name"]
+ ordering = ["order", "name"]
unique_together = [["offering", "name"]]
def __str__(self):
return f"{self.offering} - {self.name}"
+ def clean(self):
+ # Ensure only one plan per offering can be marked as "best"
+ if self.is_best:
+ existing_best = Plan.objects.filter(
+ offering=self.offering, is_best=True
+ ).exclude(pk=self.pk)
+ if existing_best.exists():
+ from django.core.exceptions import ValidationError
+
+ raise ValidationError(
+ "Only one plan per offering can be marked as the best option."
+ )
+
+ def save(self, *args, **kwargs):
+ self.clean()
+ super().save(*args, **kwargs)
+
def get_price(self, currency_code: str):
price_obj = PlanPrice.objects.filter(plan=self, currency=currency_code).first()
if price_obj:
diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html
index b5c946f..ee4f684 100644
--- a/hub/services/templates/services/offering_detail.html
+++ b/hub/services/templates/services/offering_detail.html
@@ -7,6 +7,39 @@
{% block extra_js %}
+