csp detail page

This commit is contained in:
Tobias Brunner 2025-01-27 15:54:37 +01:00
parent eb7485e345
commit 600eee2b4c
No known key found for this signature in database
15 changed files with 159 additions and 7 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ wheels/
# Project specifics # Project specifics
.env .env
hub/db.sqlite3 hub/db.sqlite3
hub/media/

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

View file

@ -14,8 +14,9 @@ class CategoryAdmin(admin.ModelAdmin):
@admin.register(CloudProvider) @admin.register(CloudProvider)
class CloudProviderAdmin(admin.ModelAdmin): class CloudProviderAdmin(admin.ModelAdmin):
list_display = ("name", "logo_preview") list_display = ("name", "slug", "logo_preview")
search_fields = ("name",) search_fields = ("name",)
prepopulated_fields = {"slug": ("name",)}
def logo_preview(self, obj): def logo_preview(self, obj):
if obj.logo: if obj.logo:

View file

@ -0,0 +1,40 @@
# Generated by Django 5.1.5 on 2025-01-27 14:49
from django.db import migrations, models
from django.utils.text import slugify
def generate_provider_slugs(apps, schema_editor):
CloudProvider = apps.get_model("services", "CloudProvider")
for provider in CloudProvider.objects.all():
provider.slug = slugify(provider.name)
counter = 1
while CloudProvider.objects.filter(slug=provider.slug).exists():
provider.slug = f"{slugify(provider.name)}-{counter}"
counter += 1
provider.save()
class Migration(migrations.Migration):
dependencies = [
("services", "0003_category_service_categories"),
]
operations = [
migrations.AddField(
model_name="cloudprovider",
name="slug",
field=models.SlugField(unique=True, null=True),
preserve_default=False,
),
migrations.RunPython(
generate_provider_slugs, reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name="cloudprovider",
name="slug",
field=models.SlugField(unique=True),
preserve_default=False,
),
]

View file

@ -46,6 +46,7 @@ class Category(models.Model):
class CloudProvider(models.Model): class CloudProvider(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = ProseEditorField(blank=True) description = ProseEditorField(blank=True)
logo = models.ImageField( logo = models.ImageField(
upload_to="cloud_provider_logos/", upload_to="cloud_provider_logos/",
@ -57,6 +58,14 @@ class CloudProvider(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse("services:provider_detail", kwargs={"slug": self.slug})
class Country(models.Model): class Country(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

View file

@ -11,7 +11,18 @@
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container"> <div class="container">
<a class="navbar-brand" href="{% url 'services:service_list' %}">Servala - The Cloud Native Services Hub</a> <a class="navbar-brand" href="{% url 'services:service_list' %}">Services Marketplace</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.view_name == 'services:service_list' %}active{% endif %}"
href="{% url 'services:service_list' %}">Services</a>
</li>
</ul>
</div>
</div> </div>
</nav> </nav>
@ -28,10 +39,12 @@
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
} }
.rich-text-content img { .rich-text-content img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
.description-preview img { .description-preview img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;

View file

@ -0,0 +1,62 @@
{% extends 'services/base.html' %}
{% block content %}
<div class="card">
<div class="card-body">
<div class="d-flex align-items-start mb-4">
{% if provider.logo %}
<img src="{{ provider.logo.url }}" alt="{{ provider.name }} logo" class="me-4"
style="max-height: 120px; max-width: 240px; object-fit: contain;">
{% endif %}
<div>
<h2 class="card-title mb-3">{{ provider.name }}</h2>
<div class="rich-text-content">
{{ provider.description|safe }}
</div>
</div>
</div>
<h3 class="mb-4">Available Services</h3>
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for service in services %}
<div class="col">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
{% if service.logo %}
<img src="{{ service.logo.url }}" alt="{{ service.name }} logo"
class="me-3" style="max-height: 50px; max-width: 100px; object-fit: contain;">
{% endif %}
<div>
<h5 class="card-title mb-0">{{ service.name }}</h5>
</div>
</div>
<div class="card-text description-preview mb-3">
{{ service.description|safe|truncatewords_html:30 }}
</div>
<div class="mb-2">
{% for category in service.categories.all %}
<span class="badge bg-secondary me-1">{{ category.full_path }}</span>
{% endfor %}
</div>
<p class="card-text">
<small class="text-muted">
Service Level: {{ service.service_level.name }}<br>
Price: ${{ service.price }}
</small>
</p>
<a href="{% url 'services:service_detail' service.pk %}" class="btn btn-primary">View Details</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
No services available from this provider yet.
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View file

@ -17,9 +17,16 @@
<h2 class="card-title">{{ service.name }}</h2> <h2 class="card-title">{{ service.name }}</h2>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
{% if service.cloud_provider.logo %} {% if service.cloud_provider.logo %}
<img src="{{ service.cloud_provider.logo.url }}" alt="{{ service.cloud_provider.name }} logo" class="me-2" style="max-height: 30px; max-width: 60px; object-fit: contain;"> <a href="{% url 'services:provider_detail' service.cloud_provider.slug %}">
<img src="{{ service.cloud_provider.logo.url }}" alt="{{ service.cloud_provider.name }} logo"
class="me-2" style="max-height: 25px; max-width: 200px; object-fit: contain;">
</a>
{% else %}
<h6 class="card-subtitle text-muted mb-0">
<a href="{% url 'services:provider_detail' service.cloud_provider.pk %}"
class="text-decoration-none text-muted">{{ service.cloud_provider.name }}</a>
</h6>
{% endif %} {% endif %}
<h6 class="card-subtitle text-muted mb-0">{{ service.cloud_provider.name }}</h6>
</div> </div>
</div> </div>
</div> </div>

View file

@ -86,18 +86,26 @@
<h5 class="card-title mb-0">{{ service.name }}</h5> <h5 class="card-title mb-0">{{ service.name }}</h5>
<div class="d-flex align-items-center mt-2"> <div class="d-flex align-items-center mt-2">
{% if service.cloud_provider.logo %} {% if service.cloud_provider.logo %}
<img src="{{ service.cloud_provider.logo.url }}" alt="{{ service.cloud_provider.name }} logo" class="me-2" style="max-height: 25px; max-width: 50px; object-fit: contain;"> <a href="{% url 'services:provider_detail' service.cloud_provider.slug %}">
<img src="{{ service.cloud_provider.logo.url }}" alt="{{ service.cloud_provider.name }} logo"
class="me-2" style="max-height: 25px; max-width: 200px; object-fit: contain;">
</a>
{% else %}
<h6 class="card-subtitle text-muted mb-0">
<a href="{% url 'services:provider_detail' service.cloud_provider.pk %}"
class="text-decoration-none text-muted">{{ service.cloud_provider.name }}</a>
</h6>
{% endif %} {% endif %}
<h6 class="card-subtitle text-muted mb-0">{{ service.cloud_provider.name }}</h6>
</div> </div>
</div> </div>
</div> </div>
<p class="card-text">{{ service.description|safe|truncatewords:30 }}</p>
<div class="mb-2"> <div class="mb-2">
{% for category in service.categories.all %} {% for category in service.categories.all %}
<span class="badge bg-secondary me-1">{{ category.full_path }}</span> <span class="badge bg-secondary me-1">{{ category.full_path }}</span>
{% endfor %} {% endfor %}
</div> </div>
<p class="card-text">{{ service.description|safe|truncatewords:30 }}</p>
<p class="card-text"> <p class="card-text">
<small class="text-muted"> <small class="text-muted">
Service Level: {{ service.service_level.name }}<br> Service Level: {{ service.service_level.name }}<br>

View file

@ -6,4 +6,5 @@ app_name = "services"
urlpatterns = [ urlpatterns = [
path("", views.service_list, name="service_list"), path("", views.service_list, name="service_list"),
path("service/<int:pk>/", views.service_detail, name="service_detail"), path("service/<int:pk>/", views.service_detail, name="service_detail"),
path("provider/<slug:slug>/", views.provider_detail, name="provider_detail"),
] ]

View file

@ -49,3 +49,13 @@ def service_list(request):
def service_detail(request, pk): def service_detail(request, pk):
service = get_object_or_404(Service, pk=pk) service = get_object_or_404(Service, pk=pk)
return render(request, "services/service_detail.html", {"service": service}) return render(request, "services/service_detail.html", {"service": service})
def provider_detail(request, slug):
provider = get_object_or_404(CloudProvider, slug=slug)
services = Service.objects.filter(cloud_provider=provider)
context = {
"provider": provider,
"services": services,
}
return render(request, "services/provider_detail.html", context)