diff --git a/hub/services/admin/__init__.py b/hub/services/admin/__init__.py
new file mode 100644
index 0000000..75c0d33
--- /dev/null
+++ b/hub/services/admin/__init__.py
@@ -0,0 +1,9 @@
+# Admin module initialization
+# Import all admin classes to register them with Django admin
+
+from .base import *
+from .content import *
+from .leads import *
+from .pricing import *
+from .providers import *
+from .services import *
diff --git a/hub/services/admin/base.py b/hub/services/admin/base.py
new file mode 100644
index 0000000..779cc5b
--- /dev/null
+++ b/hub/services/admin/base.py
@@ -0,0 +1,38 @@
+"""
+Base admin classes and common functionality
+"""
+
+from django.contrib import admin
+from django.utils.html import format_html
+from adminsortable2.admin import SortableAdminMixin
+
+from ..models import ReusableText, Category, WebsiteFaq
+
+
+@admin.register(ReusableText)
+class ReusableTextAdmin(admin.ModelAdmin):
+ """Admin configuration for ReusableText model"""
+
+ list_display = ("name",)
+ search_fields = ("name", "text")
+ ordering = ("name",)
+
+
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ """Admin configuration for Category model"""
+
+ list_display = ("name", "slug", "parent", "order")
+ list_filter = ("parent",)
+ search_fields = ("name", "description")
+ prepopulated_fields = {"slug": ("name",)}
+ ordering = ("order", "name")
+
+
+@admin.register(WebsiteFaq)
+class WebsiteFaqAdmin(SortableAdminMixin, admin.ModelAdmin):
+ """Admin configuration for WebsiteFaq model"""
+
+ list_display = ("question", "order")
+ search_fields = ("question", "answer")
+ ordering = ("order",)
diff --git a/hub/services/admin/content.py b/hub/services/admin/content.py
new file mode 100644
index 0000000..9d601ae
--- /dev/null
+++ b/hub/services/admin/content.py
@@ -0,0 +1,35 @@
+"""
+Admin classes for content-related models like external links and plans
+"""
+
+from django.contrib import admin
+
+from ..models import ExternalLink, ExternalLinkOffering, Plan
+
+
+class PlanInline(admin.StackedInline):
+ """Inline admin for Plan model"""
+
+ model = Plan
+ extra = 1
+ fieldsets = (
+ (None, {"fields": ("name", "description", "pricing", "plan_description")}),
+ )
+
+
+class ExternalLinkOfferingInline(admin.TabularInline):
+ """Inline admin for ExternalLinkOffering model"""
+
+ model = ExternalLinkOffering
+ extra = 1
+ fields = ("description", "url", "order")
+ ordering = ("order", "description")
+
+
+class ExternalLinkInline(admin.TabularInline):
+ """Inline admin for ExternalLink model"""
+
+ model = ExternalLink
+ extra = 1
+ fields = ("description", "url", "order")
+ ordering = ("order", "description")
diff --git a/hub/services/admin/leads.py b/hub/services/admin/leads.py
new file mode 100644
index 0000000..253eb6c
--- /dev/null
+++ b/hub/services/admin/leads.py
@@ -0,0 +1,15 @@
+"""
+Admin classes for lead management
+"""
+
+from django.contrib import admin
+
+from ..models import Lead
+
+
+@admin.register(Lead)
+class LeadAdmin(admin.ModelAdmin):
+ """Admin configuration for Lead model"""
+
+ list_display = ("name", "company", "created_at", "odoo_lead_id")
+ search_fields = ("name", "company")
diff --git a/hub/services/admin.py b/hub/services/admin/pricing.py
similarity index 63%
rename from hub/services/admin.py
rename to hub/services/admin/pricing.py
index 586f981..ac34e12 100644
--- a/hub/services/admin.py
+++ b/hub/services/admin/pricing.py
@@ -1,3 +1,7 @@
+"""
+Admin classes for pricing models including compute plans, storage plans, and VSHN AppCat pricing
+"""
+
from django.contrib import admin
from django.utils.html import format_html
from adminsortable2.admin import SortableAdminMixin
@@ -6,220 +10,55 @@ from import_export import resources
from import_export.fields import Field
from import_export.widgets import ForeignKeyWidget
-from .models import (
- Category,
- CloudProvider,
+from ..models import (
ComputePlan,
ComputePlanGroup,
ComputePlanPrice,
- ConsultingPartner,
- ExternalLink,
- ExternalLinkOffering,
- Lead,
- Plan,
- ProgressiveDiscountModel,
- DiscountTier,
- ReusableText,
- Service,
- ServiceOffering,
+ CloudProvider,
StoragePlan,
StoragePlanPrice,
VSHNAppCatBaseFee,
VSHNAppCatPrice,
VSHNAppCatUnitRate,
- WebsiteFaq,
+ ProgressiveDiscountModel,
+ DiscountTier,
)
-class PlanInline(admin.StackedInline):
- model = Plan
- extra = 1
- fieldsets = (
- (None, {"fields": ("name", "description", "pricing", "plan_description")}),
- )
-
-
-class ExternalLinkOfferingInline(admin.TabularInline):
- model = ExternalLinkOffering
- extra = 1
- fields = ("description", "url", "order")
- ordering = ("order", "description")
-
-
-class OfferingInline(admin.StackedInline):
- model = ServiceOffering
- extra = 1
- fieldsets = (
- (
- None,
- {
- "fields": (
- "description",
- "service",
- "cloud_provider",
- "offer_description",
- )
- },
- ),
- )
- show_change_link = True
-
-
-@admin.register(ReusableText)
-class ReusableTextAdmin(admin.ModelAdmin):
- list_display = ("name",)
- search_fields = ("name", "text")
- ordering = ("name",)
-
-
-@admin.register(Category)
-class CategoryAdmin(admin.ModelAdmin):
- list_display = ("name", "slug", "parent", "order")
- list_filter = ("parent",)
- search_fields = ("name", "description")
- prepopulated_fields = {"slug": ("name",)}
- ordering = ("order", "name")
-
-
-class ComputePlanItemInline(admin.TabularInline):
- model = ComputePlan
- extra = 1
- fields = ("name", "vcpus", "ram", "active", "valid_from", "valid_to")
-
-
-@admin.register(CloudProvider)
-class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
- list_display = (
- "name",
- "slug",
- "logo_preview",
- "disable_listing",
- "is_featured",
- "order",
- )
- search_fields = ("name", "description")
- prepopulated_fields = {"slug": ("name",)}
- inlines = [OfferingInline]
- ordering = ("order",)
-
- def logo_preview(self, obj):
- if obj.logo:
- return format_html(
- '
', obj.logo.url
- )
- return "No logo"
-
- logo_preview.short_description = "Logo"
-
-
-class ExternalLinkInline(admin.TabularInline):
- model = ExternalLink
- extra = 1
- fields = ("description", "url", "order")
- ordering = ("order", "description")
-
-
-@admin.register(Service)
-class ServiceAdmin(admin.ModelAdmin):
- list_display = (
- "name",
- "logo_preview",
- "category_list",
- "is_featured",
- "is_coming_soon",
- "disable_listing",
- )
- list_filter = ("categories",)
- search_fields = ("name", "description", "slug")
- prepopulated_fields = {"slug": ("name",)}
- filter_horizontal = ("categories",)
- inlines = [ExternalLinkInline, OfferingInline]
-
- def logo_preview(self, obj):
- if obj.logo:
- return format_html(
- '
', obj.logo.url
- )
- return "No logo"
-
- logo_preview.short_description = "Logo"
-
- def category_list(self, obj):
- return ", ".join([cat.name for cat in obj.categories.all()])
-
- category_list.short_description = "Categories"
-
- def partner_list(self, obj):
- return ", ".join([partner.name for partner in obj.consulting_partners.all()])
-
- partner_list.short_description = "Consulting Partners"
-
-
-@admin.register(ServiceOffering)
-class ServiceOfferingAdmin(admin.ModelAdmin):
- list_display = ("service", "cloud_provider")
- list_filter = ("service", "cloud_provider")
- search_fields = ("service__name", "cloud_provider__name", "description")
- inlines = [ExternalLinkOfferingInline, PlanInline]
-
-
-@admin.register(ConsultingPartner)
-class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
- list_display = (
- "name",
- "website",
- "logo_preview",
- "disable_listing",
- "is_featured",
- "order",
- )
- search_fields = ("name", "description")
- prepopulated_fields = {"slug": ("name",)}
- filter_horizontal = ("services", "cloud_providers")
- ordering = ("order",)
-
- def logo_preview(self, obj):
- if obj.logo:
- return format_html(
- '
', obj.logo.url
- )
- return "No logo"
-
- logo_preview.short_description = "Logo"
-
-
-@admin.register(Lead)
-class LeadAdmin(admin.ModelAdmin):
- list_display = ("name", "company", "created_at", "odoo_lead_id")
- search_fields = ("name", "company")
-
-
-@admin.register(WebsiteFaq)
-class WebsiteFaqAdmin(SortableAdminMixin, admin.ModelAdmin):
- list_display = ("question", "order")
- search_fields = ("question", "answer")
- ordering = ("order",)
-
-
class ComputePlanPriceInline(admin.TabularInline):
+ """Inline admin for ComputePlanPrice model"""
+
model = ComputePlanPrice
extra = 1
fields = ("currency", "amount")
+class ComputePlanItemInline(admin.TabularInline):
+ """Inline admin for ComputePlan model"""
+
+ model = ComputePlan
+ extra = 1
+ fields = ("name", "vcpus", "ram", "active", "valid_from", "valid_to")
+
+
@admin.register(ComputePlanGroup)
class ComputePlanGroupAdmin(SortableAdminMixin, admin.ModelAdmin):
+ """Admin configuration for ComputePlanGroup model"""
+
list_display = ("name", "node_label", "compute_plans_count")
search_fields = ("name", "description", "node_label")
ordering = ("order",)
def compute_plans_count(self, obj):
+ """Display count of compute plans in this group"""
return obj.compute_plans.count()
compute_plans_count.short_description = "Compute Plans"
class ComputePlanResource(resources.ModelResource):
+ """Import/Export resource for ComputePlan model"""
+
cloud_provider = Field(
column_name="cloud_provider",
attribute="cloud_provider",
@@ -252,12 +91,14 @@ class ComputePlanResource(resources.ModelResource):
)
def dehydrate_prices(self, compute_plan):
+ """Export prices in a custom format"""
prices = compute_plan.prices.all()
if not prices:
return ""
return "|".join([f"{p.currency} {p.amount}" for p in prices])
def save_m2m(self, instance, row, *args, **kwargs):
+ """Handle many-to-many relationships during import"""
super().save_m2m(instance, row, *args, **kwargs)
# Handle prices
@@ -277,6 +118,8 @@ class ComputePlanResource(resources.ModelResource):
@admin.register(ComputePlan)
class ComputePlansAdmin(ImportExportModelAdmin):
+ """Admin configuration for ComputePlan model with import/export functionality"""
+
resource_class = ComputePlanResource
list_display = (
"name",
@@ -294,6 +137,7 @@ class ComputePlansAdmin(ImportExportModelAdmin):
inlines = [ComputePlanPriceInline]
def display_prices(self, obj):
+ """Display formatted prices for the list view"""
prices = obj.prices.all()
if not prices:
return "No prices set"
@@ -303,18 +147,24 @@ class ComputePlansAdmin(ImportExportModelAdmin):
class VSHNAppCatBaseFeeInline(admin.TabularInline):
+ """Inline admin for VSHNAppCatBaseFee model"""
+
model = VSHNAppCatBaseFee
extra = 1
fields = ("currency", "amount")
class VSHNAppCatUnitRateInline(admin.TabularInline):
+ """Inline admin for VSHNAppCatUnitRate model"""
+
model = VSHNAppCatUnitRate
extra = 1
fields = ("currency", "service_level", "amount")
class DiscountTierInline(admin.TabularInline):
+ """Inline admin for DiscountTier model"""
+
model = DiscountTier
extra = 1
fields = ("min_units", "max_units", "discount_percent")
@@ -323,6 +173,8 @@ class DiscountTierInline(admin.TabularInline):
@admin.register(ProgressiveDiscountModel)
class ProgressiveDiscountModelAdmin(admin.ModelAdmin):
+ """Admin configuration for ProgressiveDiscountModel"""
+
list_display = ("name", "description", "active")
search_fields = ("name", "description")
inlines = [DiscountTierInline]
@@ -330,6 +182,8 @@ class ProgressiveDiscountModelAdmin(admin.ModelAdmin):
@admin.register(VSHNAppCatPrice)
class VSHNAppCatPriceAdmin(admin.ModelAdmin):
+ """Admin configuration for VSHNAppCatPrice model"""
+
list_display = (
"service",
"variable_unit",
@@ -343,6 +197,7 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin):
inlines = [VSHNAppCatBaseFeeInline, VSHNAppCatUnitRateInline]
def admin_display_base_fees(self, obj):
+ """Display base fees in admin list view"""
fees = obj.base_fees.all()
if not fees:
return "No base fees"
@@ -353,6 +208,7 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin):
admin_display_base_fees.short_description = "Base Fees"
def admin_display_unit_rates(self, obj):
+ """Display unit rates in admin list view"""
rates = obj.unit_rates.all()
if not rates:
return "No unit rates"
@@ -369,12 +225,16 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin):
class StoragePlanPriceInline(admin.TabularInline):
+ """Inline admin for StoragePlanPrice model"""
+
model = StoragePlanPrice
extra = 1
fields = ("currency", "amount")
class StoragePlanResource(resources.ModelResource):
+ """Import/Export resource for StoragePlan model"""
+
cloud_provider = Field(
column_name="cloud_provider",
attribute="cloud_provider",
@@ -398,12 +258,14 @@ class StoragePlanResource(resources.ModelResource):
)
def dehydrate_prices(self, storage_plan):
+ """Export prices in a custom format"""
prices = storage_plan.prices.all()
if not prices:
return ""
return "|".join([f"{p.currency} {p.amount}" for p in prices])
def save_m2m(self, instance, row, *args, **kwargs):
+ """Handle many-to-many relationships during import"""
super().save_m2m(instance, row, *args, **kwargs)
# Handle prices
@@ -423,6 +285,8 @@ class StoragePlanResource(resources.ModelResource):
@admin.register(StoragePlan)
class StoragePlanAdmin(ImportExportModelAdmin):
+ """Admin configuration for StoragePlan model with import/export functionality"""
+
resource_class = StoragePlanResource
list_display = (
"name",
@@ -437,6 +301,7 @@ class StoragePlanAdmin(ImportExportModelAdmin):
inlines = [StoragePlanPriceInline]
def display_prices(self, obj):
+ """Display formatted prices for the list view"""
prices = obj.prices.all()
if not prices:
return "No prices set"
diff --git a/hub/services/admin/providers.py b/hub/services/admin/providers.py
new file mode 100644
index 0000000..8ef3ad3
--- /dev/null
+++ b/hub/services/admin/providers.py
@@ -0,0 +1,86 @@
+"""
+Admin classes for cloud providers and consulting partners
+"""
+
+from django.contrib import admin
+from django.utils.html import format_html
+from adminsortable2.admin import SortableAdminMixin
+
+from ..models import CloudProvider, ConsultingPartner, ServiceOffering
+
+
+class OfferingInline(admin.StackedInline):
+ """Inline admin for ServiceOffering model"""
+
+ model = ServiceOffering
+ extra = 1
+ fieldsets = (
+ (
+ None,
+ {
+ "fields": (
+ "description",
+ "service",
+ "cloud_provider",
+ "offer_description",
+ )
+ },
+ ),
+ )
+ show_change_link = True
+
+
+@admin.register(CloudProvider)
+class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
+ """Admin configuration for CloudProvider model"""
+
+ list_display = (
+ "name",
+ "slug",
+ "logo_preview",
+ "disable_listing",
+ "is_featured",
+ "order",
+ )
+ search_fields = ("name", "description")
+ prepopulated_fields = {"slug": ("name",)}
+ inlines = [OfferingInline]
+ ordering = ("order",)
+
+ def logo_preview(self, obj):
+ """Display logo preview in admin list view"""
+ if obj.logo:
+ return format_html(
+ '
', obj.logo.url
+ )
+ return "No logo"
+
+ logo_preview.short_description = "Logo"
+
+
+@admin.register(ConsultingPartner)
+class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
+ """Admin configuration for ConsultingPartner model"""
+
+ list_display = (
+ "name",
+ "website",
+ "logo_preview",
+ "disable_listing",
+ "is_featured",
+ "order",
+ )
+ search_fields = ("name", "description")
+ prepopulated_fields = {"slug": ("name",)}
+ filter_horizontal = ("services", "cloud_providers")
+ ordering = ("order",)
+
+ def logo_preview(self, obj):
+ """Display logo preview in admin list view"""
+ if obj.logo:
+ return format_html(
+ '
', obj.logo.url
+ )
+ return "No logo"
+
+ logo_preview.short_description = "Logo"
diff --git a/hub/services/admin/services.py b/hub/services/admin/services.py
new file mode 100644
index 0000000..44f848b
--- /dev/null
+++ b/hub/services/admin/services.py
@@ -0,0 +1,108 @@
+"""
+Admin classes for services and service offerings
+"""
+
+from django.contrib import admin
+from django.utils.html import format_html
+
+from ..models import Service, ServiceOffering, ExternalLink, ExternalLinkOffering, Plan
+
+
+class ExternalLinkInline(admin.TabularInline):
+ """Inline admin for ExternalLink model"""
+
+ model = ExternalLink
+ extra = 1
+ fields = ("description", "url", "order")
+ ordering = ("order", "description")
+
+
+class ExternalLinkOfferingInline(admin.TabularInline):
+ """Inline admin for ExternalLinkOffering model"""
+
+ model = ExternalLinkOffering
+ extra = 1
+ fields = ("description", "url", "order")
+ ordering = ("order", "description")
+
+
+class PlanInline(admin.StackedInline):
+ """Inline admin for Plan model"""
+
+ model = Plan
+ extra = 1
+ fieldsets = (
+ (None, {"fields": ("name", "description", "pricing", "plan_description")}),
+ )
+
+
+class OfferingInline(admin.StackedInline):
+ """Inline admin for ServiceOffering model"""
+
+ model = ServiceOffering
+ extra = 1
+ fieldsets = (
+ (
+ None,
+ {
+ "fields": (
+ "description",
+ "service",
+ "cloud_provider",
+ "offer_description",
+ )
+ },
+ ),
+ )
+ show_change_link = True
+
+
+@admin.register(Service)
+class ServiceAdmin(admin.ModelAdmin):
+ """Admin configuration for Service model"""
+
+ list_display = (
+ "name",
+ "logo_preview",
+ "category_list",
+ "is_featured",
+ "is_coming_soon",
+ "disable_listing",
+ )
+ list_filter = ("categories",)
+ search_fields = ("name", "description", "slug")
+ prepopulated_fields = {"slug": ("name",)}
+ filter_horizontal = ("categories",)
+ inlines = [ExternalLinkInline, OfferingInline]
+
+ def logo_preview(self, obj):
+ """Display logo preview in admin list view"""
+ if obj.logo:
+ return format_html(
+ '
', obj.logo.url
+ )
+ return "No logo"
+
+ logo_preview.short_description = "Logo"
+
+ def category_list(self, obj):
+ """Display categories as comma-separated list"""
+ return ", ".join([cat.name for cat in obj.categories.all()])
+
+ category_list.short_description = "Categories"
+
+ def partner_list(self, obj):
+ """Display consulting partners as comma-separated list"""
+ return ", ".join([partner.name for partner in obj.consulting_partners.all()])
+
+ partner_list.short_description = "Consulting Partners"
+
+
+@admin.register(ServiceOffering)
+class ServiceOfferingAdmin(admin.ModelAdmin):
+ """Admin configuration for ServiceOffering model"""
+
+ list_display = ("service", "cloud_provider")
+ list_filter = ("service", "cloud_provider")
+ search_fields = ("service__name", "cloud_provider__name", "description")
+ inlines = [ExternalLinkOfferingInline, PlanInline]