import re import yaml from decimal import Decimal from django.shortcuts import render, get_object_or_404 from django.db.models import Q from django.http import HttpResponse, JsonResponse from django.template.loader import render_to_string from hub.services.models import ( ServiceOffering, CloudProvider, Category, Service, ComputePlan, VSHNAppCatPrice, StoragePlan, ) from collections import defaultdict from markdownify import markdownify 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 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 def offering_list(request): offerings = ( ServiceOffering.objects.filter(disable_listing=False) .order_by("service") .select_related("service", "cloud_provider") .prefetch_related( "service__categories", "plans", ) ) cloud_providers = CloudProvider.objects.all() categories = Category.objects.filter(parent=None).prefetch_related("children") services = Service.objects.all().order_by("name") # 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() # Handle service filter if request.GET.get("service"): service_id = request.GET.get("service") offerings = offerings.filter(service_id=service_id) # 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, "services": services, } return render(request, "services/offering_list.html", context) def offering_detail(request, provider_slug, service_slug): offering = get_object_or_404( ServiceOffering.objects.select_related( "service", "cloud_provider" ).prefetch_related("plans"), cloud_provider__slug=provider_slug, service__slug=service_slug, ) # 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) pricing_data_by_group_and_service_level = None price_calculator_enabled = False # 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 # 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] context = { "offering": offering, "pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level, "price_calculator_enabled": price_calculator_enabled, "provider_articles": provider_articles, "service_articles": service_articles, } return render(request, "services/offering_detail.html", context) 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" ).strip() # Generate highlights content from offering description and offer_description (convert HTML to Markdown) highlights = "" if offering.description: 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" ).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 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 def generate_pricing_data(offering): """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}