website/hub/services/views/offerings.py

261 lines
9.7 KiB
Python
Raw Normal View History

import re
2025-05-30 16:07:38 +02:00
import yaml
2025-06-02 16:22:54 +02:00
from decimal import Decimal
2025-01-30 09:49:27 +01:00
from django.shortcuts import render, get_object_or_404
from django.db.models import Q
2025-06-02 16:22:54 +02:00
from django.http import HttpResponse, JsonResponse
from django.template.loader import render_to_string
2025-05-23 17:43:29 +02:00
from hub.services.models import (
ServiceOffering,
CloudProvider,
Category,
Service,
ComputePlan,
VSHNAppCatPrice,
2025-06-04 17:06:32 +02:00
StoragePlan,
2025-05-23 17:43:29 +02:00
)
from collections import defaultdict
from markdownify import markdownify
2025-05-23 17:43:29 +02:00
2025-06-02 16:22:54 +02:00
def decimal_to_float(obj):
"""Convert Decimal objects to float for JSON serialization"""
if isinstance(obj, Decimal):
return float(obj)
elif isinstance(obj, dict):
return {key: decimal_to_float(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [decimal_to_float(item) for item in obj]
return obj
2025-05-23 17:43:29 +02:00
def natural_sort_key(name):
"""Extract numeric part from compute plan name for natural sorting"""
match = re.search(r"compute-std-(\d+)", name)
return int(match.group(1)) if match else 0
2025-01-30 09:49:27 +01:00
def offering_list(request):
offerings = (
2025-03-03 11:34:27 +01:00
ServiceOffering.objects.filter(disable_listing=False)
2025-02-26 10:39:23 +01:00
.order_by("service")
2025-01-30 09:49:27 +01:00
.select_related("service", "cloud_provider")
.prefetch_related(
"service__categories",
"plans",
)
)
cloud_providers = CloudProvider.objects.all()
categories = Category.objects.filter(parent=None).prefetch_related("children")
2025-02-25 13:43:28 +01:00
services = Service.objects.all().order_by("name")
2025-01-30 09:49:27 +01:00
# Handle cloud provider filter
if request.GET.get("cloud_provider"):
provider_id = request.GET.get("cloud_provider")
offerings = offerings.filter(cloud_provider_id=provider_id)
# Handle category filter
if request.GET.get("category"):
category_id = request.GET.get("category")
category = get_object_or_404(Category, id=category_id)
subcategories = Category.objects.filter(parent=category)
offerings = offerings.filter(
Q(service__categories=category) | Q(service__categories__in=subcategories)
).distinct()
2025-02-25 13:43:28 +01:00
# Handle service filter
2025-02-25 10:45:03 +01:00
if request.GET.get("service"):
service_id = request.GET.get("service")
offerings = offerings.filter(service_id=service_id)
2025-01-30 09:49:27 +01:00
# Handle search
if request.GET.get("search"):
query = request.GET.get("search")
offerings = offerings.filter(
Q(service__name__icontains=query)
| Q(description__icontains=query)
| Q(cloud_provider__name__icontains=query)
).distinct()
context = {
"offerings": offerings,
"cloud_providers": cloud_providers,
"categories": categories,
2025-02-25 10:45:03 +01:00
"services": services,
2025-01-30 09:49:27 +01:00
}
return render(request, "services/offering_list.html", context)
2025-02-28 14:25:35 +01:00
def offering_detail(request, provider_slug, service_slug):
2025-01-30 09:49:27 +01:00
offering = get_object_or_404(
ServiceOffering.objects.select_related(
"service", "cloud_provider"
2025-02-28 14:13:51 +01:00
).prefetch_related("plans"),
2025-02-28 14:25:35 +01:00
cloud_provider__slug=provider_slug,
service__slug=service_slug,
2025-01-30 09:49:27 +01:00
)
2025-06-02 16:22:54 +02:00
# Check if JSON pricing data is requested
if request.GET.get("pricing") == "json":
pricing_data = None
if offering.msp == "VS":
pricing_data = generate_pricing_data(offering)
if pricing_data:
# Convert Decimal objects to float for JSON serialization
pricing_data = decimal_to_float(pricing_data)
return JsonResponse(pricing_data or {})
# Check if Exoscale marketplace YAML is requested
if request.GET.get("exo_marketplace") == "true":
return generate_exoscale_marketplace_yaml(offering)
2025-05-23 17:43:29 +02:00
pricing_data_by_group_and_service_level = None
price_calculator_enabled = False
2025-05-23 17:43:29 +02:00
# Generate pricing data for VSHN offerings
if offering.msp == "VS":
try:
appcat_price = offering.service.vshn_appcat_price.get()
price_calculator_enabled = appcat_price.public_display_enabled
# Only generate pricing data if public display is enabled
if price_calculator_enabled:
pricing_data_by_group_and_service_level = generate_pricing_data(
offering
)
except VSHNAppCatPrice.DoesNotExist:
pass
2025-05-23 17:43:29 +02:00
2025-06-06 15:22:02 +02:00
# Get related articles for both cloud provider and service
provider_articles = offering.cloud_provider.articles.filter(
is_published=True
).order_by("-created_at")[:3]
service_articles = offering.service.articles.filter(is_published=True).order_by(
"-created_at"
)[:3]
2025-01-30 09:49:27 +01:00
context = {
"offering": offering,
2025-05-23 17:43:29 +02:00
"pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level,
"price_calculator_enabled": price_calculator_enabled,
2025-06-06 15:22:02 +02:00
"provider_articles": provider_articles,
"service_articles": service_articles,
2025-01-30 09:49:27 +01:00
}
return render(request, "services/offering_detail.html", context)
2025-05-23 17:43:29 +02:00
def generate_exoscale_marketplace_yaml(offering):
"""Generate YAML structure for Exoscale marketplace"""
# Create service name slug for YAML key
service_slug = offering.service.slug.replace("-", "")
yaml_key = f"marketplace_PRODUCTS_servala-{service_slug}"
# Generate product overview content from service description (convert HTML to Markdown)
product_overview = ""
if offering.service.description:
product_overview = markdownify(
offering.service.description, heading_style="ATX"
2025-05-30 16:07:38 +02:00
).strip()
# Generate highlights content from offering description and offer_description (convert HTML to Markdown)
highlights = ""
if offering.description:
2025-05-30 16:07:38 +02:00
highlights += markdownify(offering.description, heading_style="ATX").strip()
if offering.offer_description:
if highlights:
highlights += "\n\n"
highlights += markdownify(
offering.offer_description.get_full_text(), heading_style="ATX"
2025-05-30 16:07:38 +02:00
).strip()
# Build YAML structure
yaml_structure = {
yaml_key: {
"page_class": "tmpl-marketplace-product",
"html_title": f"Managed {offering.service.name} by VSHN via Servala",
"meta_desc": "Servala is the Open Cloud Native Service Hub. It connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.",
"page_header_title": f"Managed {offering.service.name} by VSHN via Servala",
"provider_key": "vshn",
"slug": f"servala-managed-{offering.service.slug}",
"title": f"Managed {offering.service.name} by VSHN via Servala",
"logo": f"img/servala-{offering.service.slug}.svg",
"list_display": [],
"meta": [
{"key": "exoscale-iaas", "value": True},
{"key": "availability", "zones": "all"},
],
"action_link": f"https://servala.com/offering/{offering.cloud_provider.slug}/{offering.service.slug}/?source=exoscale_marketplace",
"action_link_text": "Subscribe now",
"blobs": [
{
"key": "product-overview",
"blob": (
product_overview.strip()
if product_overview
else "Service description not available."
),
},
{
"key": "highlights",
"blob": (
highlights.strip()
if highlights
else "Offering highlights not available."
),
},
"editor",
{
"key": "pricing",
"blob": f"Find all the pricing information on the [Servala website](https://servala.com/offering/{offering.cloud_provider.slug}/{offering.service.slug}/?source=exoscale_marketplace#plans)",
},
{
"key": "service-and-support",
"blob": "Servala is operated by VSHN AG in Zurich, Switzerland.\n\nSeveral SLAs are available on request, offering support 24/7.\n\nMore details can be found in the [VSHN Service Levels Documentation](https://products.vshn.ch/service_levels.html).",
},
{
"key": "terms-of-service",
"blob": "- [Product Description](https://products.vshn.ch/servala/index.html)\n- [General Terms and Conditions](https://products.vshn.ch/legal/gtc_en.html)\n- [SLA](https://products.vshn.ch/service_levels.html)\n- [DPA](https://products.vshn.ch/legal/dpa_en.html)\n- [Privacy Policy](https://products.vshn.ch/legal/privacy_policy_en.html)",
},
],
}
}
# Generate YAML response for browser display
2025-05-30 16:07:38 +02:00
yaml_content = yaml.dump(
yaml_structure,
default_flow_style=False,
allow_unicode=True,
indent=2,
width=120,
sort_keys=False,
default_style=None,
)
# Return as plain text for browser display
response = HttpResponse(yaml_content, content_type="text/plain")
return response
2025-05-23 17:43:29 +02:00
def generate_pricing_data(offering):
2025-06-20 17:40:38 +02:00
"""Generate pricing data for a specific offering and its plans with multi-currency support"""
# Fetch all plans for this offering
plans = offering.plans.prefetch_related("plan_prices")
pricing_data = []
for plan in plans:
for plan_price in plan.plan_prices.all():
pricing_data.append({
"plan_id": plan.id,
"plan_name": plan.name,
"description": plan.description,
"currency": plan_price.currency,
"amount": float(plan_price.amount),
})
return {"plans": pricing_data}