Compare commits

..

10 commits

15 changed files with 535 additions and 105 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-03-03 16:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0016_cloudprovider_disable_listing_and_more"),
]
operations = [
migrations.AddField(
model_name="service",
name="tagline",
field=models.TextField(blank=True, max_length=500, null=True),
),
]

View file

@ -116,6 +116,7 @@ class Service(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(max_length=250, unique=True) slug = models.SlugField(max_length=250, unique=True)
description = ProseEditorField() description = ProseEditorField()
tagline = models.TextField(max_length=500, blank=True, null=True)
logo = models.ImageField( logo = models.ImageField(
upload_to="service_logos/", upload_to="service_logos/",
validators=[validate_image_size], validators=[validate_image_size],

View file

@ -8,6 +8,13 @@
{% if details %} {% if details %}
<input type="hidden" name="details" value="{{ details }}"> <input type="hidden" name="details" value="{{ details }}">
{% endif %} {% endif %}
<input type="hidden" name="form_timestamp" value="{{ request.timestamp|default:timestamp }}">
<div style="display:none;">
<label for="website">Website (Leave this empty)</label>
<input type="text" name="website" id="website" autocomplete="off">
</div>
{% if service %} {% if service %}
<input type="hidden" name="service_id" value="{{ service.id }}"> <input type="hidden" name="service_id" value="{{ service.id }}">
<input type="hidden" name="service_name" value="{{ service.name }}"> <input type="hidden" name="service_name" value="{{ service.name }}">
@ -54,6 +61,17 @@
{% endif %} {% endif %}
</div> </div>
{% if choices %}
<div class="mb-3">
<label for="id_choice" class="form-label">{{ choice_label|default:"Please Select" }}</label>
<select name="selected_choice" id="id_choice" class="form-control">
{% for choice_id, choice_name in choices %}
<option value="{{ choice_id }}|{{ choice_name }}">{{ choice_name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="mb-3"> <div class="mb-3">
<label for="id_message" class="form-label">Your Message (Optional)</label> <label for="id_message" class="form-label">Your Message (Optional)</label>
{{ form.message|addclass:"form-control" }} {{ form.message|addclass:"form-control" }}

View file

@ -1,7 +1,7 @@
{% extends 'services/base.html' %} {% extends 'services/base.html' %}
{% load static %} {% load static %}
{% block title %}The Cloud Native Services Hub{% endblock %} {% block title %}Open Cloud Native Services Hub{% endblock %}
{% block content %} {% block content %}
<section class="section section-hero bg-primary-subtle"> <section class="section section-hero bg-primary-subtle">
@ -9,7 +9,7 @@
<div class="section-hero-mask"></div> <div class="section-hero-mask"></div>
<div class="px-3 px-lg-0 pt-80 pb-120 position-relative"> <div class="px-3 px-lg-0 pt-80 pb-120 position-relative">
<header class="section-hero__header"> <header class="section-hero__header">
<h1 class="section-h1 fs-40 fs-lg-64">Servala - The Cloud Native Service Hub</h1> <h1 class="section-h1 fs-40 fs-lg-64">Servala - Open Cloud Native Service Hub</h1>
<div class="section-hero__desc"> <div class="section-hero__desc">
<p>Unlock the Power of Cloud Native Applications.</p> <p>Unlock the Power of Cloud Native Applications.</p>
<p>Servala connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.</p> <p>Servala connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.</p>
@ -169,7 +169,7 @@
</div> </div>
<div class="col-12 col-lg-8"> <div class="col-12 col-lg-8">
<header class="section-primary__header"> <header class="section-primary__header">
<h2 class="section-h1 fs-40 fs-lg-60">Servala - The Cloud Native Service Hub</h2> <h2 class="section-h1 fs-40 fs-lg-60">Servala - Open Cloud Native Service Hub</h2>
<div class="section-primary__desc"> <div class="section-primary__desc">
<p>Servala connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.</p> <p>Servala connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.</p>
<p>Discover:</p> <p>Discover:</p>

View file

@ -149,11 +149,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div>
<h4 class="mb-3">Order This Plan</h4>
{% load contact_tags %}
{% embedded_contact_form source="Plan Order" service=offering.service offering_id=offering.id plan_id=plan.id %}
</div>
</div> </div>
</div> </div>
{% empty %} {% empty %}
@ -167,6 +162,18 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if offering.plans.exists %}
<div class="pt-40">
<h4 class="fs-22 fw-semibold lh-1 mb-12">I'm interested in a plan</h4>
<div class="row">
<div class="col-12">
{% load contact_tags %}
{% embedded_contact_form source="Plan Order" service=offering.service offering_id=offering.id choices=offering.plans.all choice_label="Select a Plan" %}
</div>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -85,7 +85,7 @@
<div> <div>
<select class="form-select" id="service" name="service" @change="submitForm()"> <select class="form-select" id="service" name="service" @change="submitForm()">
<option value="">All Services</option> <option value="">All Services</option>
{% for service in services %} {% for service in available_services %}
<option value="{{ service.id }}" {% if request.GET.service == service.id|stringformat:'i' %}selected{% endif %}> <option value="{{ service.id }}" {% if request.GET.service == service.id|stringformat:'i' %}selected{% endif %}>
{{ service.name }} {{ service.name }}
</option> </option>
@ -94,23 +94,6 @@
</div> </div>
</div> </div>
<!-- Cloud Provider Filter -->
<div class="pt-24 mb-24">
<div class="d-flex justify-content-between align-items-center h-33 mb-5px" role="button">
<h3 class="sidebar-title mb-0">Cloud Provider</h3>
</div>
<div>
<select class="form-select" id="cloud_provider" name="cloud_provider" @change="submitForm()">
<option value="">All Providers</option>
{% for provider in cloud_providers %}
<option value="{{ provider.id }}" {% if request.GET.cloud_provider == provider.id|stringformat:'i' %}selected{% endif %}>
{{ provider.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<!-- Filter Actions --> <!-- Filter Actions -->
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'services:partner_list' %}" class="btn btn-outline-secondary btn-sm">Clear</a> <a href="{% url 'services:partner_list' %}" class="btn btn-outline-secondary btn-sm">Clear</a>

View file

@ -106,6 +106,13 @@
<button class="btn btn-tertiary btn-sm mr-12">{{ category.full_path }}</button> <button class="btn btn-tertiary btn-sm mr-12">{{ category.full_path }}</button>
{% endfor %} {% endfor %}
</div> </div>
{% if service.tagline %}
<div class="mt-3">
<p class="fst-italic text-muted fs-19">
"{{ service.tagline }}"
</p>
</div>
{% endif %}
</div> </div>
<!-- Description --> <!-- Description -->

View file

@ -86,12 +86,12 @@
<div> <div>
<select class="form-select" id="category" name="category" @change="submitForm()"> <select class="form-select" id="category" name="category" @change="submitForm()">
<option value="">All Categories</option> <option value="">All Categories</option>
{% for category in categories %} {% for category in available_categories %}
<option value="{{ category.id }}" {% if request.GET.category == category.id|stringformat:'i' %}selected{% endif %}> <option value="{{ category.id }}" {% if request.GET.category == category.id|stringformat:'i' %}selected{% endif %}>
{{ category.name }} {{ category.name }}
</option> </option>
{% if category.children.all %} {% if category.available_children %}
{% for subcategory in category.children.all %} {% for subcategory in category.available_children %}
<option value="{{ subcategory.id }}" {% if request.GET.category == subcategory.id|stringformat:'i' %}selected{% endif %}> <option value="{{ subcategory.id }}" {% if request.GET.category == subcategory.id|stringformat:'i' %}selected{% endif %}>
&nbsp;&nbsp;&nbsp;{{ subcategory.name }} &nbsp;&nbsp;&nbsp;{{ subcategory.name }}
</option> </option>
@ -110,7 +110,7 @@
<div> <div>
<select class="form-select" id="consulting_partner" name="consulting_partner" @change="submitForm()"> <select class="form-select" id="consulting_partner" name="consulting_partner" @change="submitForm()">
<option value="">All Partners</option> <option value="">All Partners</option>
{% for partner in consulting_partners %} {% for partner in available_consulting_partners %}
<option value="{{ partner.id }}" {% if request.GET.consulting_partner == partner.id|stringformat:'i' %}selected{% endif %}> <option value="{{ partner.id }}" {% if request.GET.consulting_partner == partner.id|stringformat:'i' %}selected{% endif %}>
{{ partner.name }} {{ partner.name }}
</option> </option>
@ -127,7 +127,7 @@
<div> <div>
<select class="form-select" id="cloud_provider" name="cloud_provider" @change="submitForm()"> <select class="form-select" id="cloud_provider" name="cloud_provider" @change="submitForm()">
<option value="">All Providers</option> <option value="">All Providers</option>
{% for provider in cloud_providers %} {% for provider in available_cloud_providers %}
<option value="{{ provider.id }}" {% if request.GET.cloud_provider == provider.id|stringformat:'i' %}selected{% endif %}> <option value="{{ provider.id }}" {% if request.GET.cloud_provider == provider.id|stringformat:'i' %}selected{% endif %}>
{{ provider.name }} {{ provider.name }}
</option> </option>
@ -179,6 +179,11 @@
<span>{{ category.full_path }}</span> <span>{{ category.full_path }}</span>
{% endfor %} {% endfor %}
</p> </p>
{% if service.tagline %}
<p class="card__tagline fst-italic text-muted">
<small>"{{ service.tagline }}"</small>
</p>
{% endif %}
</div> </div>
<div class="card__desc flex-grow-1"> <div class="card__desc flex-grow-1">
<p class="mb-0">{{ service.description|safe|truncatewords:30 }}</p> <p class="mb-0">{{ service.description|safe|truncatewords:30 }}</p>

View file

@ -2,13 +2,21 @@
from django import template from django import template
from hub.services.forms import LeadForm from hub.services.forms import LeadForm
from hub.services.models import Service, ServiceOffering, Plan from hub.services.models import Service, ServiceOffering, Plan
import time
register = template.Library() register = template.Library()
@register.inclusion_tag("services/embedded_contact_form.html", takes_context=True) @register.inclusion_tag("services/embedded_contact_form.html", takes_context=True)
def embedded_contact_form( def embedded_contact_form(
context, source=None, details=None, service=None, offering_id=None, plan_id=None context,
source=None,
details=None,
service=None,
offering_id=None,
plan_id=None,
choices=None,
choice_label=None,
): ):
""" """
Renders an embedded contact form with optional service context information. Renders an embedded contact form with optional service context information.
@ -17,14 +25,23 @@ def embedded_contact_form(
{% load contact_tags %} {% load contact_tags %}
{% embedded_contact_form source="Partner Page" details="ACME Corp" %} {% embedded_contact_form source="Partner Page" details="ACME Corp" %}
{% embedded_contact_form service=service offering_id=offering.id plan_id=plan.id %} {% embedded_contact_form service=service offering_id=offering.id plan_id=plan.id %}
{% embedded_contact_form service=service offering_id=offering.id choices=offering.plans.all choice_label="Select a Plan" %}
""" """
request = context["request"] request = context["request"]
form = LeadForm() form = LeadForm()
# Add timestamp for spam protection
timestamp = int(time.time())
service_obj = None service_obj = None
offering_obj = None offering_obj = None
plan_obj = None plan_obj = None
# Process choices if they're QuerySet objects (like plans)
processed_choices = None
if choices:
processed_choices = [(str(choice.id), choice.name) for choice in choices]
# Resolve service/offering/plan objects if IDs provided # Resolve service/offering/plan objects if IDs provided
if service and isinstance(service, str): if service and isinstance(service, str):
try: try:
@ -56,4 +73,7 @@ def embedded_contact_form(
"offering": offering_obj, "offering": offering_obj,
"plan": plan_obj, "plan": plan_obj,
"request": request, "request": request,
"choices": processed_choices,
"choice_label": choice_label,
"timestamp": timestamp,
} }

View file

@ -0,0 +1,228 @@
# hub/services/templatetags/json_ld_tags.py
from django import template
from django.urls import resolve
from django.utils.safestring import mark_safe
import json
register = template.Library()
@register.simple_tag(takes_context=True)
def json_ld_structured_data(context):
"""
Generates appropriate JSON-LD structured data based on the current page.
"""
request = context["request"]
current_url = request.path
resolved_view = resolve(current_url)
view_name = resolved_view.url_name
# Base URL for building absolute URLs
base_url = request.build_absolute_uri("/").rstrip("/")
# Default organization data (for Servala)
organization_data = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Servala",
"url": base_url,
"logo": f"{base_url}/static/img/header-logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+41 44 545 53 00",
"email": "hi@serva.la",
"contactType": "Customer Support",
},
"address": {
"@type": "PostalAddress",
"streetAddress": "Neugasse 10",
"addressLocality": "Zurich",
"postalCode": "8005",
"addressCountry": "CH",
},
}
# Handle different page types
if view_name == "homepage":
data = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Servala - Open Cloud Native Service Hub",
"url": base_url,
"description": "Servala connects businesses, developers, and cloud service providers on one unique hub with secure, scalable, and easy-to-use cloud-native services.",
"potentialAction": {
"@type": "SearchAction",
"target": f"{base_url}/services/?search={{search_term_string}}",
"query-input": "required name=search_term_string",
},
}
elif view_name == "service_list":
data = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": "Cloud Services - Servala",
"url": f"{base_url}/services/",
"description": "Explore all available cloud services on Servala, with new services added regularly.",
"isPartOf": {"@type": "WebSite", "name": "Servala", "url": base_url},
}
elif view_name == "provider_list":
data = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": "Cloud Providers - Servala",
"url": f"{base_url}/providers/",
"description": "Discover cloud providers on Servala offering reliable infrastructure and innovative cloud computing solutions.",
"isPartOf": {"@type": "WebSite", "name": "Servala", "url": base_url},
}
elif view_name == "partner_list":
data = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": "Consulting Partners - Servala",
"url": f"{base_url}/partners/",
"description": "Browse our network of expert consulting partners on Servala who can help implement and optimize cloud services.",
"isPartOf": {"@type": "WebSite", "name": "Servala", "url": base_url},
}
elif view_name == "service_detail" and "service" in context:
service = context["service"]
service_url = request.build_absolute_uri()
data = {
"@context": "https://schema.org",
"@type": "Product",
"name": service.name,
"description": service.description,
"url": service_url,
"category": "Cloud Service",
}
# Add image if available
if hasattr(service, "logo") and service.logo:
data["image"] = request.build_absolute_uri(service.logo.url)
# Add offerings if available
if hasattr(service, "offerings") and service.offerings.exists():
data["offers"] = {
"@type": "AggregateOffer",
"availability": "https://schema.org/InStock",
"offerCount": service.offerings.count(),
}
elif view_name == "provider_detail" and "provider" in context:
provider = context["provider"]
provider_url = request.build_absolute_uri()
data = {
"@context": "https://schema.org",
"@type": "Organization",
"name": provider.name,
"description": provider.description,
"url": provider_url,
}
# Add image if available
if hasattr(provider, "logo") and provider.logo:
data["logo"] = request.build_absolute_uri(provider.logo.url)
# Add contact information if available
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
if hasattr(provider, "website") and provider.website:
contact_point["url"] = provider.website
if hasattr(provider, "email") and provider.email:
contact_point["email"] = provider.email
if hasattr(provider, "phone") and provider.phone:
contact_point["telephone"] = provider.phone
if len(contact_point) > 2: # If we have more than the @type and contactType
data["contactPoint"] = contact_point
# Add address if available
if hasattr(provider, "address") and provider.address:
data["address"] = {
"@type": "PostalAddress",
"addressCountry": "CH", # Default to Switzerland
}
elif view_name == "partner_detail" and "partner" in context:
partner = context["partner"]
partner_url = request.build_absolute_uri()
data = {
"@context": "https://schema.org",
"@type": "Organization",
"name": partner.name,
"description": partner.description,
"url": partner_url,
}
# Add image if available
if hasattr(partner, "logo") and partner.logo:
data["logo"] = request.build_absolute_uri(partner.logo.url)
# Add contact information if available
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
if hasattr(partner, "website") and partner.website:
contact_point["url"] = partner.website
if hasattr(partner, "email") and partner.email:
contact_point["email"] = partner.email
if hasattr(partner, "phone") and partner.phone:
contact_point["telephone"] = partner.phone
if len(contact_point) > 2: # If we have more than the @type and contactType
data["contactPoint"] = contact_point
# Add address if available
if hasattr(partner, "address") and partner.address:
data["address"] = {
"@type": "PostalAddress",
"addressCountry": "CH", # Default to Switzerland
}
elif view_name == "offering_detail" and "offering" in context:
offering = context["offering"]
offering_url = request.build_absolute_uri()
data = {
"@context": "https://schema.org",
"@type": "Product",
"name": f"{offering.service.name} on {offering.cloud_provider.name}",
"description": offering.description or offering.service.description,
"url": offering_url,
"category": "Cloud Service",
}
# Add brand (service)
data["brand"] = {"@type": "Brand", "name": offering.service.name}
# Add image if available
if hasattr(offering.service, "logo") and offering.service.logo:
data["image"] = request.build_absolute_uri(offering.service.logo.url)
# Add offers if available
if hasattr(offering, "plans") and offering.plans.exists():
data["offers"] = {
"@type": "AggregateOffer",
"availability": "https://schema.org/InStock",
"offerCount": offering.plans.count(),
"seller": {
"@type": "Organization",
"name": offering.cloud_provider.name,
"url": request.build_absolute_uri(
offering.cloud_provider.get_absolute_url()
),
},
}
else:
# Default to organization data if no specific page type matches
data = organization_data
# Return the JSON-LD as a script tag
json_ld = json.dumps(data, indent=2)
return mark_safe(f'<script type="application/ld+json">{json_ld}</script>')

View file

@ -1,5 +1,5 @@
from django import template from django import template
from django.urls import resolve from django.urls import resolve, Resolver404
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
@ -12,8 +12,12 @@ def social_meta_tags(context):
""" """
request = context["request"] request = context["request"]
current_url = request.path current_url = request.path
resolved_view = resolve(current_url)
view_name = resolved_view.url_name try:
resolved_view = resolve(current_url)
view_name = resolved_view.view_name
except Resolver404:
view_name = None
# Default values (used for listing pages) # Default values (used for listing pages)
title = context.get("self.title", "Servala") title = context.get("self.title", "Servala")

View file

@ -1,4 +1,5 @@
import logging import logging
import time
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
@ -18,6 +19,27 @@ def thank_you(request):
def contact_form(request): def contact_form(request):
if request.method == "POST": if request.method == "POST":
# Spam protection checks
honeypot_value = request.POST.get("website", "")
timestamp_value = request.POST.get("form_timestamp", "0")
current_time = int(time.time())
# Check 1: Honeypot field should be empty
if honeypot_value:
# Bot detected - silently redirect
return redirect("services:homepage")
# Check 2: Form shouldn't be submitted too quickly (< 3 seconds)
try:
form_time = int(timestamp_value)
if current_time - form_time < 3:
# Too quick submission - likely a bot
return redirect("services:homepage")
except ValueError:
# Invalid timestamp - likely a bot
return redirect("services:homepage")
# Continue with normal form processing
form = LeadForm(request.POST) form = LeadForm(request.POST)
if form.is_valid(): if form.is_valid():
from hub.services.models import Lead, Service, ServiceOffering, Plan from hub.services.models import Lead, Service, ServiceOffering, Plan
@ -26,7 +48,7 @@ def contact_form(request):
lead = Lead( lead = Lead(
name=form.cleaned_data["name"], name=form.cleaned_data["name"],
email=form.cleaned_data["email"], email=form.cleaned_data["email"],
message=form.cleaned_data["message"], message=form.cleaned_data["message"] or "",
company=form.cleaned_data["company"], company=form.cleaned_data["company"],
phone=form.cleaned_data["phone"], phone=form.cleaned_data["phone"],
) )
@ -87,6 +109,22 @@ def contact_form(request):
if plan_name: if plan_name:
service_info.append(f"Plan: {plan_name}") service_info.append(f"Plan: {plan_name}")
# Handle selected choice if present
selected_choice = request.POST.get("selected_choice", "")
if selected_choice:
try:
choice_id, choice_name = selected_choice.split("|", 1)
# Add selected choice to message
service_info.append(f"Selected Plan: {choice_name}")
# Try to set the plan based on the choice_id
try:
lead.plan = Plan.objects.get(id=choice_id)
except Plan.DoesNotExist:
pass
except ValueError:
pass
if service_info: if service_info:
context_info.append("Service Information: " + ", ".join(service_info)) context_info.append("Service Information: " + ", ".join(service_info))

View file

@ -4,35 +4,70 @@ from hub.services.models import ConsultingPartner, CloudProvider, Service
def partner_list(request): def partner_list(request):
partners = ( # Get basic filter parameters
ConsultingPartner.objects.filter(disable_listing=False) search_query = request.GET.get("search", "")
.order_by("name") service_id = request.GET.get("service", "")
.prefetch_related("services", "cloud_providers") cloud_provider_id = request.GET.get("cloud_provider", "")
)
services = Service.objects.all().order_by("name") # Start with all active partners
partners = ConsultingPartner.objects.filter(disable_listing=False).order_by("name")
# Handle cloud provider filter # Apply filters based on request parameters
if request.GET.get("cloud_provider"): if search_query:
provider_id = request.GET.get("cloud_provider")
partners = partners.filter(cloud_providers__id=provider_id)
# Handle service filter
if request.GET.get("service"):
service_id = request.GET.get("service")
partners = partners.filter(services__id=service_id)
# Handle search
if request.GET.get("search"):
query = request.GET.get("search")
partners = partners.filter( partners = partners.filter(
Q(name__icontains=query) | Q(description__icontains=query) Q(name__icontains=search_query) | Q(description__icontains=search_query)
) )
if service_id:
partners = partners.filter(services__id=service_id)
if cloud_provider_id:
partners = partners.filter(cloud_providers__id=cloud_provider_id)
# Get available services from filtered partners
available_service_ids = partners.values_list("services__id", flat=True).distinct()
available_services = Service.objects.filter(
id__in=available_service_ids, disable_listing=False
).order_by("name")
# Get available cloud providers from filtered partners
available_cloud_provider_ids = partners.values_list(
"cloud_providers__id", flat=True
).distinct()
available_cloud_providers = CloudProvider.objects.filter(
id__in=available_cloud_provider_ids, disable_listing=False
).order_by("name")
# For the current selection, we need to make sure we include the selected items
# even if they don't match other filters
if service_id:
try:
selected_service_id = int(service_id)
if selected_service_id not in available_service_ids:
selected_service = Service.objects.get(id=selected_service_id)
available_services = list(available_services)
available_services.append(selected_service)
except (ValueError, Service.DoesNotExist):
pass
if cloud_provider_id:
try:
cp_id = int(cloud_provider_id)
if cp_id not in available_cloud_provider_ids:
selected_provider = CloudProvider.objects.get(id=cp_id)
available_cloud_providers = list(available_cloud_providers)
available_cloud_providers.append(selected_provider)
except (ValueError, CloudProvider.DoesNotExist):
pass
context = { context = {
"partners": partners, "partners": partners.prefetch_related("services", "cloud_providers"),
"services": services, "services": Service.objects.filter(disable_listing=False).order_by("name"),
"cloud_providers": CloudProvider.objects.all(), "cloud_providers": CloudProvider.objects.filter(disable_listing=False).order_by(
"name"
),
"available_services": available_services,
"available_cloud_providers": available_cloud_providers,
} }
return render(request, "services/partner_list.html", context) return render(request, "services/partner_list.html", context)

View file

@ -9,55 +9,121 @@ from hub.services.models import (
def service_list(request): def service_list(request):
services = ( # Get basic filter parameters
Service.objects.filter(disable_listing=False) search_query = request.GET.get("search", "")
.order_by("-is_featured", "is_coming_soon", "name") category_id = request.GET.get("category", "")
.prefetch_related( consulting_partner_id = request.GET.get("consulting_partner", "")
"categories", cloud_provider_id = request.GET.get("cloud_provider", "")
"offerings",
"offerings__cloud_provider", # Start with all active services
"offerings__plans", # Filter out services with disable_listing=True
"consulting_partners", services = Service.objects.filter(disable_listing=False)
"external_links",
# Apply filters based on request parameters
if search_query:
services = services.filter(
Q(name__icontains=search_query) | Q(description__icontains=search_query)
) )
if category_id:
services = services.filter(categories__id=category_id)
if consulting_partner_id:
services = services.filter(consulting_partners__id=consulting_partner_id)
if cloud_provider_id:
# Filter through offerings instead of direct cloud_providers relation
services = services.filter(offerings__cloud_provider__id=cloud_provider_id)
# Order services: featured first, then regular services, then coming soon
services = services.order_by(
"-is_featured", # Featured first (True before False)
"is_coming_soon", # Coming soon last (False before True)
"name", # Alphabetically within each group
) )
cloud_providers = CloudProvider.objects.all()
categories = Category.objects.filter(parent=None).prefetch_related("children")
# Handle category filter # Get all available categories from filtered services
if request.GET.get("category"): available_category_ids = services.values_list(
category_id = request.GET.get("category") "categories__id", flat=True
category = get_object_or_404(Category, id=category_id) ).distinct()
subcategories = Category.objects.filter(parent=category) available_categories = Category.objects.filter(
services = services.filter( id__in=available_category_ids, parent=None
Q(categories=category) | Q(categories__in=subcategories) )
).distinct()
# Handle cloud provider filter # For each parent category, get available children
if request.GET.get("cloud_provider"): for category in available_categories:
provider_id = request.GET.get("cloud_provider") child_ids = (
services = services.filter(offerings__cloud_provider_id=provider_id).distinct() services.filter(categories__parent=category)
.values_list("categories__id", flat=True)
.distinct()
)
category.available_children = Category.objects.filter(id__in=child_ids)
# Handle consulting partner filter # Get available consulting partners from filtered services
if request.GET.get("consulting_partner"): # Excluding partners with disable_listing=True
partner_id = request.GET.get("consulting_partner") available_consulting_partner_ids = services.values_list(
services = services.filter(consulting_partners__id=partner_id).distinct() "consulting_partners__id", flat=True
).distinct()
available_consulting_partners = ConsultingPartner.objects.filter(
id__in=available_consulting_partner_ids, disable_listing=False
)
# Handle search # Get available cloud providers from filtered services via offerings
if request.GET.get("search"): # Excluding providers with disable_listing=True
query = request.GET.get("search") available_cloud_provider_ids = services.values_list(
services = services.filter( "offerings__cloud_provider__id", flat=True
Q(name__icontains=query) ).distinct()
| Q(description__icontains=query) available_cloud_providers = CloudProvider.objects.filter(
| Q(offerings__description__icontains=query) id__in=available_cloud_provider_ids, disable_listing=False
).distinct() )
# For the current selection, we need to make sure we include the selected items
# even if they don't match other filters
if category_id:
try:
selected_category = Category.objects.get(id=category_id)
if selected_category.parent:
parent_category = selected_category.parent
if parent_category not in available_categories:
available_categories = list(available_categories)
available_categories.append(parent_category)
parent_category.available_children = [selected_category]
elif selected_category not in available_categories:
available_categories = list(available_categories)
available_categories.append(selected_category)
except Category.DoesNotExist:
pass
if consulting_partner_id:
try:
cp_id = int(consulting_partner_id)
if cp_id not in available_consulting_partner_ids:
selected_partner = ConsultingPartner.objects.get(id=cp_id)
available_consulting_partners = list(available_consulting_partners)
available_consulting_partners.append(selected_partner)
except (ValueError, ConsultingPartner.DoesNotExist):
pass
if cloud_provider_id:
try:
cp_id = int(cloud_provider_id)
if cp_id not in available_cloud_provider_ids:
selected_provider = CloudProvider.objects.get(id=cp_id)
available_cloud_providers = list(available_cloud_providers)
available_cloud_providers.append(selected_provider)
except (ValueError, CloudProvider.DoesNotExist):
pass
context = { context = {
"services": services, "services": services,
"cloud_providers": cloud_providers, "categories": Category.objects.filter(parent=None),
"categories": categories, "consulting_partners": ConsultingPartner.objects.filter(disable_listing=False),
"consulting_partners": ConsultingPartner.objects.all(), "cloud_providers": CloudProvider.objects.filter(disable_listing=False),
"available_categories": available_categories,
"available_consulting_partners": available_consulting_partners,
"available_cloud_providers": available_cloud_providers,
} }
return render(request, "services/service_list.html", context) return render(request, "services/service_list.html", context)

View file

@ -42,6 +42,13 @@ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", include("hub.services.urls")), path("", include("hub.services.urls")),
path("broker/", include("hub.broker.urls", namespace="broker")), path("broker/", include("hub.broker.urls", namespace="broker")),
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
path("robots.txt", robots_txt, name="robots_txt"),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
@ -50,12 +57,5 @@ if settings.DEBUG:
path("test-400/", lambda request: render(request, "400.html"), name="test_400"), path("test-400/", lambda request: render(request, "400.html"), name="test_400"),
path("test-404/", lambda request: render(request, "404.html"), name="test_404"), path("test-404/", lambda request: render(request, "404.html"), name="test_404"),
path("test-500/", lambda request: render(request, "500.html"), name="test_500"), path("test-500/", lambda request: render(request, "500.html"), name="test_500"),
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
path("robots.txt", robots_txt, name="robots_txt"),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)