partner categories

This commit is contained in:
Tobias Brunner 2025-07-11 10:52:44 +02:00
parent 83504f6b7c
commit c6b50da971
No known key found for this signature in database
8 changed files with 75 additions and 4 deletions

View file

@ -107,6 +107,7 @@ class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
list_display = ( list_display = (
"name", "name",
"category",
"website", "website",
"logo_preview", "logo_preview",
"disable_listing", "disable_listing",
@ -114,12 +115,13 @@ class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
"order", "order",
) )
search_fields = ("name", "description") search_fields = ("name", "description")
list_filter = ("category", "is_featured", "disable_listing")
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
filter_horizontal = ("services", "cloud_providers") filter_horizontal = ("services", "cloud_providers")
ordering = ("order",) ordering = ("order",)
fieldsets = ( fieldsets = (
(None, {"fields": ("name", "slug", "description", "order")}), (None, {"fields": ("name", "slug", "description", "category", "order")}),
( (
"Images", "Images",
{ {

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2 on 2025-07-11 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0045_add_og_image_to_article"),
]
operations = [
migrations.AddField(
model_name="consultingpartner",
name="category",
field=models.CharField(
choices=[("CONSULTING", "Consulting"), ("TRAINING", "Training")],
default="CONSULTING",
help_text="Category of the consulting partner",
max_length=20,
),
),
]

View file

@ -106,6 +106,11 @@ class Unit(models.TextChoices):
CPU = "CPU", "vCPU" CPU = "CPU", "vCPU"
class PartnerCategory(models.TextChoices):
CONSULTING = "CONSULTING", "Consulting"
TRAINING = "TRAINING", "Training"
# This should be a relation, but for now this is good enough :TM: # This should be a relation, but for now this is good enough :TM:
class ManagedServiceProvider(models.TextChoices): class ManagedServiceProvider(models.TextChoices):
VS = "VS", "VSHN" VS = "VS", "VSHN"

View file

@ -2,7 +2,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from .base import validate_image_size, get_prose_editor_field from .base import validate_image_size, get_prose_editor_field, PartnerCategory
from .images import ImageReference from .images import ImageReference
@ -51,6 +51,14 @@ class ConsultingPartner(ImageReference):
email = models.EmailField(max_length=254, blank=True, null=True) email = models.EmailField(max_length=254, blank=True, null=True)
address = models.TextField(max_length=250, blank=True, null=True) address = models.TextField(max_length=250, blank=True, null=True)
# Partner category (hardcoded choices as requested)
category = models.CharField(
max_length=20,
choices=PartnerCategory.choices,
default=PartnerCategory.CONSULTING,
help_text="Category of the partner",
)
services = models.ManyToManyField( services = models.ManyToManyField(
"services.Service", related_name="consulting_partners", blank=True "services.Service", related_name="consulting_partners", blank=True
) )
@ -69,7 +77,11 @@ class ConsultingPartner(ImageReference):
ordering = ["order"] ordering = ["order"]
def __str__(self): def __str__(self):
return self.name return f"{self.name} ({self.get_category_display()})"
def get_category_display_badge(self):
"""Returns category display suitable for badges/UI"""
return self.get_category_display()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:

View file

@ -65,6 +65,9 @@
</div> </div>
{% endif %} {% endif %}
<p class="card-text">{{ article.related_consulting_partner.name }}</p> <p class="card-text">{{ article.related_consulting_partner.name }}</p>
<div class="mb-2">
<span class="badge bg-primary">{{ article.related_consulting_partner.get_category_display_badge }}</span>
</div>
<a href="{{ article.related_consulting_partner.get_absolute_url }}" class="btn btn-primary btn-sm">View Partner</a> <a href="{{ article.related_consulting_partner.get_absolute_url }}" class="btn btn-primary btn-sm">View Partner</a>
</div> </div>
</div> </div>

View file

@ -153,7 +153,7 @@
<h2 class="fs-50 fw-semibold lh-1 mb-12">{{ partner.name }}</h2> <h2 class="fs-50 fw-semibold lh-1 mb-12">{{ partner.name }}</h2>
</header> </header>
<div class="fs-19 text-gray-500"> <div class="fs-19 text-gray-500">
<button class="btn btn-tertiary btn-sm mr-12">Servala Consulting Partner</button> <button class="btn btn-tertiary btn-sm mr-12">{{ partner.get_category_display_badge }}</button>
</div> </div>
</div> </div>

View file

@ -94,6 +94,23 @@
</div> </div>
</div> </div>
<!-- Category 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">Category</h3>
</div>
<div>
<select class="form-select" id="category" name="category" @change="submitForm()">
<option value="">All Categories</option>
{% for category_value, category_label in partner_categories %}
<option value="{{ category_value }}" {% if request.GET.category == category_value %}selected{% endif %}>
{{ category_label }}
</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>
@ -126,6 +143,9 @@
<h3 class="card__title"> <h3 class="card__title">
<a href="{{ partner.get_absolute_url }}" class="text-decoration-none clickable-link">{{ partner.name }}</a> <a href="{{ partner.get_absolute_url }}" class="text-decoration-none clickable-link">{{ partner.name }}</a>
</h3> </h3>
<div class="mb-2">
<span class="badge bg-primary">{{ partner.get_category_display_badge }}</span>
</div>
</div> </div>
<div class="card__desc flex-grow-1 rich-text-content"> <div class="card__desc flex-grow-1 rich-text-content">

View file

@ -1,6 +1,7 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.db.models import Q from django.db.models import Q
from hub.services.models import ConsultingPartner, CloudProvider, Service from hub.services.models import ConsultingPartner, CloudProvider, Service
from hub.services.models.base import PartnerCategory
def partner_list(request): def partner_list(request):
@ -8,6 +9,7 @@ def partner_list(request):
search_query = request.GET.get("search", "") search_query = request.GET.get("search", "")
service_id = request.GET.get("service", "") service_id = request.GET.get("service", "")
cloud_provider_id = request.GET.get("cloud_provider", "") cloud_provider_id = request.GET.get("cloud_provider", "")
category = request.GET.get("category", "")
# Start with all active partners # Start with all active partners
partners = ConsultingPartner.objects.filter(disable_listing=False).order_by("order") partners = ConsultingPartner.objects.filter(disable_listing=False).order_by("order")
@ -24,6 +26,9 @@ def partner_list(request):
if cloud_provider_id: if cloud_provider_id:
partners = partners.filter(cloud_providers__id=cloud_provider_id) partners = partners.filter(cloud_providers__id=cloud_provider_id)
if category:
partners = partners.filter(category=category)
# Get available services from filtered partners # Get available services from filtered partners
available_service_ids = partners.values_list("services__id", flat=True).distinct() available_service_ids = partners.values_list("services__id", flat=True).distinct()
available_services = Service.objects.filter( available_services = Service.objects.filter(
@ -68,6 +73,7 @@ def partner_list(request):
), ),
"available_services": available_services, "available_services": available_services,
"available_cloud_providers": available_cloud_providers, "available_cloud_providers": available_cloud_providers,
"partner_categories": PartnerCategory.choices,
} }
return render(request, "services/partner_list.html", context) return render(request, "services/partner_list.html", context)