Compare commits
10 commits
a07d1fc4e2
...
ac75e964cb
Author | SHA1 | Date | |
---|---|---|---|
ac75e964cb | |||
d81e76e8ab | |||
aa4ec33c93 | |||
928bd0818e | |||
5157d7c781 | |||
66c5d1f8c2 | |||
b11fa2c21b | |||
7f76948875 | |||
be473f903e | |||
cd65a149e5 |
15 changed files with 535 additions and 105 deletions
18
hub/services/migrations/0017_service_tagline.py
Normal file
18
hub/services/migrations/0017_service_tagline.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -116,6 +116,7 @@ class Service(models.Model):
|
|||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(max_length=250, unique=True)
|
||||
description = ProseEditorField()
|
||||
tagline = models.TextField(max_length=500, blank=True, null=True)
|
||||
logo = models.ImageField(
|
||||
upload_to="service_logos/",
|
||||
validators=[validate_image_size],
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
{% if details %}
|
||||
<input type="hidden" name="details" value="{{ details }}">
|
||||
{% 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 %}
|
||||
<input type="hidden" name="service_id" value="{{ service.id }}">
|
||||
<input type="hidden" name="service_name" value="{{ service.name }}">
|
||||
|
@ -54,6 +61,17 @@
|
|||
{% endif %}
|
||||
</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">
|
||||
<label for="id_message" class="form-label">Your Message (Optional)</label>
|
||||
{{ form.message|addclass:"form-control" }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'services/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}The Cloud Native Services Hub{% endblock %}
|
||||
{% block title %}Open Cloud Native Services Hub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section section-hero bg-primary-subtle">
|
||||
|
@ -9,7 +9,7 @@
|
|||
<div class="section-hero-mask"></div>
|
||||
<div class="px-3 px-lg-0 pt-80 pb-120 position-relative">
|
||||
<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">
|
||||
<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>
|
||||
|
@ -169,7 +169,7 @@
|
|||
</div>
|
||||
<div class="col-12 col-lg-8">
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
@ -149,11 +149,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% empty %}
|
||||
|
@ -167,6 +162,18 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
<div>
|
||||
<select class="form-select" id="service" name="service" @change="submitForm()">
|
||||
<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 %}>
|
||||
{{ service.name }}
|
||||
</option>
|
||||
|
@ -94,23 +94,6 @@
|
|||
</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 -->
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'services:partner_list' %}" class="btn btn-outline-secondary btn-sm">Clear</a>
|
||||
|
|
|
@ -106,6 +106,13 @@
|
|||
<button class="btn btn-tertiary btn-sm mr-12">{{ category.full_path }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if service.tagline %}
|
||||
<div class="mt-3">
|
||||
<p class="fst-italic text-muted fs-19">
|
||||
"{{ service.tagline }}"
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
|
|
|
@ -86,12 +86,12 @@
|
|||
<div>
|
||||
<select class="form-select" id="category" name="category" @change="submitForm()">
|
||||
<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 %}>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
{% if category.children.all %}
|
||||
{% for subcategory in category.children.all %}
|
||||
{% if category.available_children %}
|
||||
{% for subcategory in category.available_children %}
|
||||
<option value="{{ subcategory.id }}" {% if request.GET.category == subcategory.id|stringformat:'i' %}selected{% endif %}>
|
||||
{{ subcategory.name }}
|
||||
</option>
|
||||
|
@ -110,7 +110,7 @@
|
|||
<div>
|
||||
<select class="form-select" id="consulting_partner" name="consulting_partner" @change="submitForm()">
|
||||
<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 %}>
|
||||
{{ partner.name }}
|
||||
</option>
|
||||
|
@ -127,7 +127,7 @@
|
|||
<div>
|
||||
<select class="form-select" id="cloud_provider" name="cloud_provider" @change="submitForm()">
|
||||
<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 %}>
|
||||
{{ provider.name }}
|
||||
</option>
|
||||
|
@ -179,6 +179,11 @@
|
|||
<span>{{ category.full_path }}</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% if service.tagline %}
|
||||
<p class="card__tagline fst-italic text-muted">
|
||||
<small>"{{ service.tagline }}"</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card__desc flex-grow-1">
|
||||
<p class="mb-0">{{ service.description|safe|truncatewords:30 }}</p>
|
||||
|
|
|
@ -2,13 +2,21 @@
|
|||
from django import template
|
||||
from hub.services.forms import LeadForm
|
||||
from hub.services.models import Service, ServiceOffering, Plan
|
||||
import time
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("services/embedded_contact_form.html", takes_context=True)
|
||||
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.
|
||||
|
@ -17,14 +25,23 @@ def embedded_contact_form(
|
|||
{% load contact_tags %}
|
||||
{% 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 choices=offering.plans.all choice_label="Select a Plan" %}
|
||||
"""
|
||||
request = context["request"]
|
||||
form = LeadForm()
|
||||
|
||||
# Add timestamp for spam protection
|
||||
timestamp = int(time.time())
|
||||
|
||||
service_obj = None
|
||||
offering_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
|
||||
if service and isinstance(service, str):
|
||||
try:
|
||||
|
@ -56,4 +73,7 @@ def embedded_contact_form(
|
|||
"offering": offering_obj,
|
||||
"plan": plan_obj,
|
||||
"request": request,
|
||||
"choices": processed_choices,
|
||||
"choice_label": choice_label,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
|
228
hub/services/templatetags/json_ld_tags.py
Normal file
228
hub/services/templatetags/json_ld_tags.py
Normal 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>')
|
|
@ -1,5 +1,5 @@
|
|||
from django import template
|
||||
from django.urls import resolve
|
||||
from django.urls import resolve, Resolver404
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
|
@ -12,8 +12,12 @@ def social_meta_tags(context):
|
|||
"""
|
||||
request = context["request"]
|
||||
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)
|
||||
title = context.get("self.title", "Servala")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import time
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
|
||||
|
@ -18,6 +19,27 @@ def thank_you(request):
|
|||
|
||||
def contact_form(request):
|
||||
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)
|
||||
if form.is_valid():
|
||||
from hub.services.models import Lead, Service, ServiceOffering, Plan
|
||||
|
@ -26,7 +48,7 @@ def contact_form(request):
|
|||
lead = Lead(
|
||||
name=form.cleaned_data["name"],
|
||||
email=form.cleaned_data["email"],
|
||||
message=form.cleaned_data["message"],
|
||||
message=form.cleaned_data["message"] or "",
|
||||
company=form.cleaned_data["company"],
|
||||
phone=form.cleaned_data["phone"],
|
||||
)
|
||||
|
@ -87,6 +109,22 @@ def contact_form(request):
|
|||
if 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:
|
||||
context_info.append("Service Information: " + ", ".join(service_info))
|
||||
|
||||
|
|
|
@ -4,35 +4,70 @@ from hub.services.models import ConsultingPartner, CloudProvider, Service
|
|||
|
||||
|
||||
def partner_list(request):
|
||||
partners = (
|
||||
ConsultingPartner.objects.filter(disable_listing=False)
|
||||
.order_by("name")
|
||||
.prefetch_related("services", "cloud_providers")
|
||||
)
|
||||
# Get basic filter parameters
|
||||
search_query = request.GET.get("search", "")
|
||||
service_id = request.GET.get("service", "")
|
||||
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
|
||||
if request.GET.get("cloud_provider"):
|
||||
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")
|
||||
# Apply filters based on request parameters
|
||||
if search_query:
|
||||
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 = {
|
||||
"partners": partners,
|
||||
"services": services,
|
||||
"cloud_providers": CloudProvider.objects.all(),
|
||||
"partners": partners.prefetch_related("services", "cloud_providers"),
|
||||
"services": Service.objects.filter(disable_listing=False).order_by("name"),
|
||||
"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)
|
||||
|
||||
|
|
|
@ -9,55 +9,121 @@ from hub.services.models import (
|
|||
|
||||
|
||||
def service_list(request):
|
||||
services = (
|
||||
Service.objects.filter(disable_listing=False)
|
||||
.order_by("-is_featured", "is_coming_soon", "name")
|
||||
.prefetch_related(
|
||||
"categories",
|
||||
"offerings",
|
||||
"offerings__cloud_provider",
|
||||
"offerings__plans",
|
||||
"consulting_partners",
|
||||
"external_links",
|
||||
# Get basic filter parameters
|
||||
search_query = request.GET.get("search", "")
|
||||
category_id = request.GET.get("category", "")
|
||||
consulting_partner_id = request.GET.get("consulting_partner", "")
|
||||
cloud_provider_id = request.GET.get("cloud_provider", "")
|
||||
|
||||
# Start with all active services
|
||||
# Filter out services with disable_listing=True
|
||||
services = Service.objects.filter(disable_listing=False)
|
||||
|
||||
# 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
|
||||
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)
|
||||
services = services.filter(
|
||||
Q(categories=category) | Q(categories__in=subcategories)
|
||||
).distinct()
|
||||
# Get all available categories from filtered services
|
||||
available_category_ids = services.values_list(
|
||||
"categories__id", flat=True
|
||||
).distinct()
|
||||
available_categories = Category.objects.filter(
|
||||
id__in=available_category_ids, parent=None
|
||||
)
|
||||
|
||||
# Handle cloud provider filter
|
||||
if request.GET.get("cloud_provider"):
|
||||
provider_id = request.GET.get("cloud_provider")
|
||||
services = services.filter(offerings__cloud_provider_id=provider_id).distinct()
|
||||
# For each parent category, get available children
|
||||
for category in available_categories:
|
||||
child_ids = (
|
||||
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
|
||||
if request.GET.get("consulting_partner"):
|
||||
partner_id = request.GET.get("consulting_partner")
|
||||
services = services.filter(consulting_partners__id=partner_id).distinct()
|
||||
# Get available consulting partners from filtered services
|
||||
# Excluding partners with disable_listing=True
|
||||
available_consulting_partner_ids = services.values_list(
|
||||
"consulting_partners__id", flat=True
|
||||
).distinct()
|
||||
available_consulting_partners = ConsultingPartner.objects.filter(
|
||||
id__in=available_consulting_partner_ids, disable_listing=False
|
||||
)
|
||||
|
||||
# Handle search
|
||||
if request.GET.get("search"):
|
||||
query = request.GET.get("search")
|
||||
services = services.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(description__icontains=query)
|
||||
| Q(offerings__description__icontains=query)
|
||||
).distinct()
|
||||
# Get available cloud providers from filtered services via offerings
|
||||
# Excluding providers with disable_listing=True
|
||||
available_cloud_provider_ids = services.values_list(
|
||||
"offerings__cloud_provider__id", flat=True
|
||||
).distinct()
|
||||
available_cloud_providers = CloudProvider.objects.filter(
|
||||
id__in=available_cloud_provider_ids, disable_listing=False
|
||||
)
|
||||
|
||||
# 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 = {
|
||||
"services": services,
|
||||
"cloud_providers": cloud_providers,
|
||||
"categories": categories,
|
||||
"consulting_partners": ConsultingPartner.objects.all(),
|
||||
"categories": Category.objects.filter(parent=None),
|
||||
"consulting_partners": ConsultingPartner.objects.filter(disable_listing=False),
|
||||
"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)
|
||||
|
||||
|
||||
|
|
14
hub/urls.py
14
hub/urls.py
|
@ -42,6 +42,13 @@ urlpatterns = [
|
|||
path("admin/", admin.site.urls),
|
||||
path("", include("hub.services.urls")),
|
||||
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:
|
||||
urlpatterns += [
|
||||
|
@ -50,12 +57,5 @@ if settings.DEBUG:
|
|||
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-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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue