improve initial page load filter selection

This commit is contained in:
Tobias Brunner 2025-07-07 14:30:05 +02:00
parent 19d9dff83e
commit 381f2f09e6
No known key found for this signature in database
2 changed files with 220 additions and 100 deletions

View file

@ -226,16 +226,18 @@
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3" id="filter-form"> <form method="get" class="row g-3" id="filter-form">
<div class="col-md-3"> <div class="col-md-3">
<label for="cloud_provider" class="form-label">Cloud Provider</label> <label for="cloud_provider" class="form-label">Cloud Provider <span class="text-danger">*</span></label>
<select name="cloud_provider" id="cloud_provider" class="form-select filter-select"> <select name="cloud_provider" id="cloud_provider" class="form-select filter-select">
<option value="">-- Select Cloud Provider --</option>
{% for provider in all_cloud_providers %} {% for provider in all_cloud_providers %}
<option value="{{ provider }}" {% if provider == filter_cloud_provider %}selected{% endif %}>{{ provider }}</option> <option value="{{ provider }}" {% if provider == filter_cloud_provider %}selected{% endif %}>{{ provider }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="service" class="form-label">Service</label> <label for="service" class="form-label">Service <span class="text-danger">*</span></label>
<select name="service" id="service" class="form-select filter-select"> <select name="service" id="service" class="form-select filter-select">
<option value="">-- Select Service --</option>
{% for service in all_services %} {% for service in all_services %}
<option value="{{ service }}" {% if service == filter_service %}selected{% endif %}>{{ service }}</option> <option value="{{ service }}" {% if service == filter_service %}selected{% endif %}>{{ service }}</option>
{% endfor %} {% endfor %}
@ -772,7 +774,7 @@
{% else %} {% else %}
<div class="alert alert-info"> <div class="alert alert-info">
<h4>No pricing data available</h4> <h4>No pricing data available</h4>
<p>{% if filter_cloud_provider or filter_service or filter_compute_plan_group or filter_service_level %}No data matches the selected filters. Try adjusting your filter criteria.{% else %}Please ensure you have active compute plans with prices and VSHNAppCat price configurations.{% endif %}</p> <p>{% if not filter_cloud_provider and not filter_service %}Please select both a <strong>Cloud Provider</strong> and <strong>Service</strong> from the filters above to view pricing data.{% elif not filter_cloud_provider %}Please select a <strong>Cloud Provider</strong> from the filters above.{% elif not filter_service %}Please select a <strong>Service</strong> from the filters above.{% elif filter_cloud_provider or filter_service or filter_compute_plan_group or filter_service_level %}No data matches the selected filters. Try adjusting your filter criteria.{% else %}Please ensure you have active compute plans with prices and VSHNAppCat price configurations.{% endif %}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -2,14 +2,19 @@ import re
from django.shortcuts import render from django.shortcuts import render
from collections import defaultdict from collections import defaultdict
from hub.services.models.pricing import ComputePlan, StoragePlan, ExternalPricePlans, VSHNAppCatPrice from hub.services.models.pricing import (
ComputePlan,
StoragePlan,
ExternalPricePlans,
VSHNAppCatPrice,
)
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.db import models from django.db import models
def natural_sort_key(obj): def natural_sort_key(obj):
"""Extract numeric parts for natural sorting (works for any plan name)""" """Extract numeric parts for natural sorting (works for any plan name)"""
name = obj.name if hasattr(obj, 'name') else str(obj) name = obj.name if hasattr(obj, "name") else str(obj)
parts = re.split(r"(\d+)", name) parts = re.split(r"(\d+)", name)
return [int(part) if part.isdigit() else part for part in parts] return [int(part) if part.isdigit() else part for part in parts]
@ -130,18 +135,61 @@ def pricelist(request):
filter_compute_plan_group = request.GET.get("compute_plan_group", "") filter_compute_plan_group = request.GET.get("compute_plan_group", "")
filter_service_level = request.GET.get("service_level", "") filter_service_level = request.GET.get("service_level", "")
# Get filter options for dropdowns first (needed for initial page load)
all_cloud_providers = (
ComputePlan.objects.all()
.values_list("cloud_provider__name", flat=True)
.distinct()
.order_by("cloud_provider__name")
)
all_services = (
VSHNAppCatPrice.objects.values_list("service__name", flat=True)
.distinct()
.order_by("service__name")
)
all_compute_plan_groups = list(
ComputePlan.objects.filter(group__isnull=False)
.values_list("group__name", flat=True)
.distinct()
.order_by("group__name")
)
all_compute_plan_groups.append("No Group") # Add option for plans without groups
all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices]
# Only process pricing data if both cloud provider and service are selected
if not filter_cloud_provider or not filter_service:
context = {
"pricing_data_by_group_and_service_level": {},
"show_discount_details": show_discount_details,
"show_addon_details": show_addon_details,
"show_price_comparison": show_price_comparison,
"filter_cloud_provider": filter_cloud_provider,
"filter_service": filter_service,
"filter_compute_plan_group": filter_compute_plan_group,
"filter_service_level": filter_service_level,
"all_cloud_providers": all_cloud_providers,
"all_services": all_services,
"all_compute_plan_groups": all_compute_plan_groups,
"all_service_levels": all_service_levels,
"show_empty_state": True, # Flag to show empty state message
}
return render(request, "services/pricelist.html", context)
# Fetch all compute plans (active and inactive) with related data # Fetch all compute plans (active and inactive) with related data
compute_plans_qs = ComputePlan.objects.all() compute_plans_qs = ComputePlan.objects.all()
if filter_cloud_provider: if filter_cloud_provider:
compute_plans_qs = compute_plans_qs.filter(cloud_provider__name=filter_cloud_provider) compute_plans_qs = compute_plans_qs.filter(
cloud_provider__name=filter_cloud_provider
)
if filter_compute_plan_group: if filter_compute_plan_group:
if filter_compute_plan_group == "No Group": if filter_compute_plan_group == "No Group":
compute_plans_qs = compute_plans_qs.filter(group__isnull=True) compute_plans_qs = compute_plans_qs.filter(group__isnull=True)
else: else:
compute_plans_qs = compute_plans_qs.filter(group__name=filter_compute_plan_group) compute_plans_qs = compute_plans_qs.filter(
group__name=filter_compute_plan_group
)
compute_plans = list( compute_plans = list(
compute_plans_qs compute_plans_qs.select_related("cloud_provider", "group")
.select_related("cloud_provider", "group")
.prefetch_related("prices") .prefetch_related("prices")
.order_by("group__order", "group__name", "cloud_provider__name", "name") .order_by("group__order", "group__name", "cloud_provider__name", "name")
) )
@ -186,20 +234,30 @@ def pricelist(request):
units = int(plan.vcpus) units = int(plan.vcpus)
else: else:
continue continue
base_fee_currencies = set(appcat_price.base_fees.values_list("currency", flat=True)) base_fee_currencies = set(
service_levels = appcat_price.unit_rates.values_list("service_level", flat=True).distinct() appcat_price.base_fees.values_list("currency", flat=True)
)
service_levels = appcat_price.unit_rates.values_list(
"service_level", flat=True
).distinct()
# Apply service level filter # Apply service level filter
if filter_service_level: if filter_service_level:
service_levels = [ service_levels = [
sl for sl in service_levels sl
if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl] == filter_service_level for sl in service_levels
if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl]
== filter_service_level
] ]
for service_level in service_levels: for service_level in service_levels:
unit_rate_currencies = set( unit_rate_currencies = set(
appcat_price.unit_rates.filter(service_level=service_level).values_list("currency", flat=True) appcat_price.unit_rates.filter(
service_level=service_level
).values_list("currency", flat=True)
) )
# Find currencies that exist across all pricing components # Find currencies that exist across all pricing components
matching_currencies = plan_currencies.intersection(base_fee_currencies).intersection(unit_rate_currencies) matching_currencies = plan_currencies.intersection(
base_fee_currencies
).intersection(unit_rate_currencies)
if not matching_currencies: if not matching_currencies:
continue continue
for currency in matching_currencies: for currency in matching_currencies:
@ -217,7 +275,10 @@ def pricelist(request):
compute_plan_price = plan.get_price(currency) compute_plan_price = plan.get_price(currency)
base_fee = appcat_price.get_base_fee(currency, service_level) base_fee = appcat_price.get_base_fee(currency, service_level)
unit_rate = appcat_price.get_unit_rate(currency, service_level) unit_rate = appcat_price.get_unit_rate(currency, service_level)
if any(price is None for price in [compute_plan_price, base_fee, unit_rate]): if any(
price is None
for price in [compute_plan_price, base_fee, unit_rate]
):
continue continue
# Calculate replica enforcement based on service level # Calculate replica enforcement based on service level
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED: if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
@ -228,12 +289,27 @@ def pricelist(request):
standard_sla_price = base_fee + (total_units * unit_rate) standard_sla_price = base_fee + (total_units * unit_rate)
# Apply discount if available # Apply discount if available
discount_breakdown = None discount_breakdown = None
if appcat_price.discount_model and appcat_price.discount_model.active: if (
discounted_price = appcat_price.discount_model.calculate_discount(unit_rate, total_units) appcat_price.discount_model
and appcat_price.discount_model.active
):
discounted_price = (
appcat_price.discount_model.calculate_discount(
unit_rate, total_units
)
)
sla_price = base_fee + discounted_price sla_price = base_fee + discounted_price
discount_savings = standard_sla_price - sla_price discount_savings = standard_sla_price - sla_price
discount_percentage = (discount_savings / standard_sla_price) * 100 if standard_sla_price > 0 else 0 discount_percentage = (
discount_breakdown = appcat_price.discount_model.get_discount_breakdown(unit_rate, total_units) (discount_savings / standard_sla_price) * 100
if standard_sla_price > 0
else 0
)
discount_breakdown = (
appcat_price.discount_model.get_discount_breakdown(
unit_rate, total_units
)
)
else: else:
sla_price = standard_sla_price sla_price = standard_sla_price
discounted_price = total_units * unit_rate discounted_price = total_units * unit_rate
@ -259,7 +335,9 @@ def pricelist(request):
if addon.addon_type == "BF": # Base Fee if addon.addon_type == "BF": # Base Fee
addon_price = addon.get_price(currency, service_level) addon_price = addon.get_price(currency, service_level)
elif addon.addon_type == "UR": # Unit Rate elif addon.addon_type == "UR": # Unit Rate
addon_price_per_unit = addon.get_price(currency, service_level) addon_price_per_unit = addon.get_price(
currency, service_level
)
if addon_price_per_unit: if addon_price_per_unit:
addon_price = addon_price_per_unit * total_units addon_price = addon_price_per_unit * total_units
addon_info = { addon_info = {
@ -276,16 +354,23 @@ def pricelist(request):
optional_addons.append(addon_info) optional_addons.append(addon_info)
service_price_with_addons = price_calculation["total_price"] service_price_with_addons = price_calculation["total_price"]
final_price = compute_plan_price + service_price_with_addons final_price = compute_plan_price + service_price_with_addons
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices).get(service_level, service_level) service_level_display = dict(
VSHNAppCatPrice.ServiceLevel.choices
).get(service_level, service_level)
# Get external/internal price comparisons if enabled (unchanged, but could be optimized further) # Get external/internal price comparisons if enabled (unchanged, but could be optimized further)
external_comparisons = [] external_comparisons = []
internal_comparisons = [] internal_comparisons = []
if show_price_comparison: if show_price_comparison:
external_prices = get_external_price_comparisons(plan, appcat_price, currency, service_level) external_prices = get_external_price_comparisons(
plan, appcat_price, currency, service_level
)
for ext_price in external_prices: for ext_price in external_prices:
difference = ext_price.amount - final_price difference = ext_price.amount - final_price
ratio = ext_price.amount / final_price if final_price > 0 else 0 ratio = (
external_comparisons.append({ ext_price.amount / final_price if final_price > 0 else 0
)
external_comparisons.append(
{
"plan_name": ext_price.plan_name, "plan_name": ext_price.plan_name,
"provider": ext_price.cloud_provider.name, "provider": ext_price.cloud_provider.name,
"description": ext_price.description, "description": ext_price.description,
@ -300,12 +385,22 @@ def pricelist(request):
"source": ext_price.source, "source": ext_price.source,
"date_retrieved": ext_price.date_retrieved, "date_retrieved": ext_price.date_retrieved,
"is_internal": False, "is_internal": False,
}) }
internal_price_comparisons = get_internal_cloud_provider_comparisons(plan, appcat_price, currency, service_level) )
internal_price_comparisons = (
get_internal_cloud_provider_comparisons(
plan, appcat_price, currency, service_level
)
)
for int_price in internal_price_comparisons: for int_price in internal_price_comparisons:
difference = int_price["final_price"] - final_price difference = int_price["final_price"] - final_price
ratio = int_price["final_price"] / final_price if final_price > 0 else 0 ratio = (
internal_comparisons.append({ int_price["final_price"] / final_price
if final_price > 0
else 0
)
internal_comparisons.append(
{
"plan_name": int_price["plan_name"], "plan_name": int_price["plan_name"],
"provider": int_price["provider"], "provider": int_price["provider"],
"description": f"Same specs with {int_price['provider']}", "description": f"Same specs with {int_price['provider']}",
@ -314,22 +409,34 @@ def pricelist(request):
"vcpus": int_price["vcpus"], "vcpus": int_price["vcpus"],
"ram": int_price["ram"], "ram": int_price["ram"],
"group_name": int_price["group_name"], "group_name": int_price["group_name"],
"compute_plan_price": int_price["compute_plan_price"], "compute_plan_price": int_price[
"compute_plan_price"
],
"service_price": int_price["service_price"], "service_price": int_price["service_price"],
"difference": difference, "difference": difference,
"ratio": ratio, "ratio": ratio,
"is_internal": True, "is_internal": True,
}) }
)
group_name = plan.group.name if plan.group else "No Group" group_name = plan.group.name if plan.group else "No Group"
# Use prefetched storage plans # Use prefetched storage plans
storage_plans = storage_plans_by_provider.get(plan.cloud_provider_id, []) storage_plans = storage_plans_by_provider.get(
pricing_data_by_group_and_service_level[group_name][service_level_display].append({ plan.cloud_provider_id, []
)
pricing_data_by_group_and_service_level[group_name][
service_level_display
].append(
{
"cloud_provider": plan.cloud_provider.name, "cloud_provider": plan.cloud_provider.name,
"service": appcat_price.service.name, "service": appcat_price.service.name,
"compute_plan": plan.name, "compute_plan": plan.name,
"compute_plan_group": group_name, "compute_plan_group": group_name,
"compute_plan_group_description": (plan.group.description if plan.group else ""), "compute_plan_group_description": (
"compute_plan_group_node_label": (plan.group.node_label if plan.group else ""), plan.group.description if plan.group else ""
),
"compute_plan_group_node_label": (
plan.group.node_label if plan.group else ""
),
"storage_plans": storage_plans, "storage_plans": storage_plans,
"vcpus": plan.vcpus, "vcpus": plan.vcpus,
"ram": plan.ram, "ram": plan.ram,
@ -346,19 +453,32 @@ def pricelist(request):
"sla_per_unit": unit_rate, "sla_per_unit": unit_rate,
"sla_price": service_price_with_addons, "sla_price": service_price_with_addons,
"standard_sla_price": base_sla_price, "standard_sla_price": base_sla_price,
"discounted_sla_price": (base_fee + discounted_price if appcat_price.discount_model and appcat_price.discount_model.active else None), "discounted_sla_price": (
base_fee + discounted_price
if appcat_price.discount_model
and appcat_price.discount_model.active
else None
),
"discount_savings": discount_savings, "discount_savings": discount_savings,
"discount_percentage": discount_percentage, "discount_percentage": discount_percentage,
"discount_breakdown": discount_breakdown, "discount_breakdown": discount_breakdown,
"final_price": final_price, "final_price": final_price,
"discount_model": (appcat_price.discount_model.name if appcat_price.discount_model else None), "discount_model": (
"has_discount": bool(appcat_price.discount_model and appcat_price.discount_model.active), appcat_price.discount_model.name
if appcat_price.discount_model
else None
),
"has_discount": bool(
appcat_price.discount_model
and appcat_price.discount_model.active
),
"external_comparisons": external_comparisons, "external_comparisons": external_comparisons,
"internal_comparisons": internal_comparisons, "internal_comparisons": internal_comparisons,
"mandatory_addons": mandatory_addons, "mandatory_addons": mandatory_addons,
"optional_addons": optional_addons, "optional_addons": optional_addons,
"is_active": plan.active, "is_active": plan.active,
}) }
)
# Order groups correctly, placing "No Group" last # Order groups correctly, placing "No Group" last
ordered_groups_intermediate = {} ordered_groups_intermediate = {}
all_group_names = list(pricing_data_by_group_and_service_level.keys()) all_group_names = list(pricing_data_by_group_and_service_level.keys())
@ -366,7 +486,9 @@ def pricelist(request):
all_group_names.remove("No Group") all_group_names.remove("No Group")
all_group_names.append("No Group") all_group_names.append("No Group")
for group_name_key in all_group_names: for group_name_key in all_group_names:
ordered_groups_intermediate[group_name_key] = pricing_data_by_group_and_service_level[group_name_key] ordered_groups_intermediate[group_name_key] = (
pricing_data_by_group_and_service_level[group_name_key]
)
# Convert defaultdicts to regular dicts for the template # Convert defaultdicts to regular dicts for the template
final_context_data = {} final_context_data = {}
for group_key, service_levels_dict in ordered_groups_intermediate.items(): for group_key, service_levels_dict in ordered_groups_intermediate.items():
@ -394,11 +516,7 @@ def pricelist(request):
) )
all_compute_plan_groups.append("No Group") # Add option for plans without groups all_compute_plan_groups.append("No Group") # Add option for plans without groups
all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices] all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices]
# If no filter is specified, select the first available provider/service by default
if not filter_cloud_provider and all_cloud_providers:
filter_cloud_provider = all_cloud_providers[0]
if not filter_service and all_services:
filter_service = all_services[0]
context = { context = {
"pricing_data_by_group_and_service_level": final_context_data, "pricing_data_by_group_and_service_level": final_context_data,
"show_discount_details": show_discount_details, "show_discount_details": show_discount_details,