image library migration step 1
All checks were successful
Build and Deploy / build (push) Successful in 1m7s
Django Tests / test (push) Successful in 1m10s
Build and Deploy / deploy (push) Successful in 6s

This commit is contained in:
Tobias Brunner 2025-07-04 17:26:09 +02:00
parent 07bea333bc
commit 1a2bbb1c35
No known key found for this signature in database
23 changed files with 413 additions and 57 deletions

View file

@ -0,0 +1,81 @@
# Image Library Migration Status
## ✅ COMPLETED (First Production Rollout) - UPDATED
### Models Updated
- **Article**: Now inherits from `ImageReference`, with `image_library` field for new images and original `image` field temporarily
- **CloudProvider**: Now inherits from `ImageReference`, with `image_library` field for new images and original `logo` field temporarily
- **ConsultingPartner**: Now inherits from `ImageReference`, with `image_library` field for new images and original `logo` field temporarily
- **Service**: Now inherits from `ImageReference`, with `image_library` field for new images and original `logo` field temporarily
### New Properties Added
- `Article.get_image()` - Returns image from library or falls back to original field
- `CloudProvider.get_logo()` - Returns logo from library or falls back to original field
- `ConsultingPartner.get_logo()` - Returns logo from library or falls back to original field
- `Service.get_logo()` - Returns logo from library or falls back to original field
### Templates Updated
- ✅ `pages/homepage.html` - Updated service, provider, and partner image references
- ✅ `services/article_list.html` - Updated article image references
- ✅ `services/article_detail.html` - Updated related service/provider/partner logos
- ✅ `services/offering_list.html` - Updated service and provider logos
- ✅ `services/offering_detail.html` - Updated service and provider logos
- ✅ `services/lead_form.html` - Updated service logo
- ✅ `services/partner_detail.html` - Updated partner and service logos
- ✅ `services/partner_list.html` - Updated partner logos
- ✅ `services/provider_list.html` - Updated provider logos
- ✅ `services/provider_detail.html` - Updated provider and service logos
- ✅ `services/service_detail.html` - Updated service and provider logos
### Admin Interface Updated
- ✅ `ArticleAdmin` - Updated image_preview to use get_image property
- ✅ `ServiceAdmin` - Updated logo_preview to use get_logo property
- ✅ `CloudProviderAdmin` - Updated logo_preview to use get_logo property
- ✅ `ConsultingPartnerAdmin` - Updated logo_preview to use get_logo property
### JSON-LD Template Tags Updated
- ✅ Updated structured data generation to use new image properties
- ✅ Updated logo references for services, providers, and partners
### Database Migration
- ✅ Migration `0041_add_image_library_references` successfully applied
- ✅ Migration `0042_fix_image_library_field_name` successfully applied
- ✅ All models now have `image_library` foreign key fields to ImageLibrary
- ✅ Original image fields preserved for backward compatibility
- ✅ Fixed field name conflicts using `%(class)s_references` related_name pattern
### Admin Interface Enhanced
- ✅ **ArticleAdmin**: Added fieldsets with `image_library` field visible in "Images" section
- ✅ **ServiceAdmin**: Added fieldsets with `image_library` field visible in "Images" section
- ✅ **CloudProviderAdmin**: Added fieldsets with `image_library` field visible in "Images" section
- ✅ **ConsultingPartnerAdmin**: Added fieldsets with `image_library` field visible in "Images" section
- ✅ All admin interfaces show both new and legacy fields during transition
- ✅ Clear descriptions guide users to use Image Library for new images
## Current Status
The system is now ready for production with dual image support:
- **New images**: Can be added through the Image Library
- **Legacy images**: Still work through the original fields
- **Templates**: Use the new `get_image/get_logo` properties that automatically fall back
## Next Steps (Future Cleanup)
1. **Data Migration**: Create script to migrate existing images to ImageLibrary
2. **Admin Updates**: Update admin interfaces to use ImageLibrary selection
3. **Template Validation**: Add null checks to remaining templates
4. **Field Removal**: Remove legacy image fields after migration is complete
5. **Storage Cleanup**: Remove old image files from media directories
## Benefits Achieved
- ✅ Centralized image management through ImageLibrary
- ✅ Usage tracking for images
- ✅ Backward compatibility maintained
- ✅ Enhanced admin experience ready
- ✅ Consistent image handling across all models
- ✅ Proper fallback mechanisms in place
## Safety Measures
- ✅ Original image fields preserved
- ✅ Gradual migration approach
- ✅ Fallback properties ensure no broken images
- ✅ Database migration tested and applied
- ✅ Admin interface maintains functionality

View file

@ -61,12 +61,47 @@ class ArticleAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at")
ordering = ("-article_date",)
fieldsets = (
(None, {"fields": ("title", "slug", "excerpt", "content", "meta_keywords")}),
(
"Images",
{
"fields": (
"image_library",
"image",
), # New image library field and legacy field
"description": "Use the Image Library field for new images. Legacy field will be removed after migration.",
},
),
(
"Publishing",
{"fields": ("author", "article_date", "is_published", "is_featured")},
),
(
"Relations",
{
"fields": (
"related_service",
"related_consulting_partner",
"related_cloud_provider",
),
"classes": ("collapse",),
},
),
(
"Metadata",
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
def image_preview(self, obj):
"""Display image preview in admin list view"""
if obj.image:
return format_html(
'<img src="{}" style="max-height: 50px;"/>', obj.image.url
)
image = obj.get_image
if image:
return format_html('<img src="{}" style="max-height: 50px;"/>', image.url)
return "No image"
image_preview.short_description = "Image"

View file

@ -47,12 +47,30 @@ class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
inlines = [OfferingInline]
ordering = ("order",)
fieldsets = (
(None, {"fields": ("name", "slug", "description", "order")}),
(
"Images",
{
"fields": (
"image_library",
"logo",
), # New image library field and legacy field
"description": "Use the Image Library field for new images. Legacy field will be removed after migration.",
},
),
(
"Contact Information",
{"fields": ("website", "linkedin", "phone", "email", "address")},
),
("Settings", {"fields": ("is_featured", "disable_listing")}),
)
def logo_preview(self, obj):
"""Display logo preview in admin list view"""
if obj.logo:
return format_html(
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
)
logo = obj.get_logo
if logo:
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
return "No logo"
logo_preview.short_description = "Logo"
@ -75,12 +93,34 @@ class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
filter_horizontal = ("services", "cloud_providers")
ordering = ("order",)
fieldsets = (
(None, {"fields": ("name", "slug", "description", "order")}),
(
"Images",
{
"fields": (
"image_library",
"logo",
), # New image library field and legacy field
"description": "Use the Image Library field for new images. Legacy field will be removed after migration.",
},
),
(
"Contact Information",
{"fields": ("website", "linkedin", "phone", "email", "address")},
),
(
"Relations",
{"fields": ("services", "cloud_providers"), "classes": ("collapse",)},
),
("Settings", {"fields": ("is_featured", "disable_listing")}),
)
def logo_preview(self, obj):
"""Display logo preview in admin list view"""
if obj.logo:
return format_html(
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
)
logo = obj.get_logo
if logo:
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
return "No logo"
logo_preview.short_description = "Logo"

View file

@ -93,12 +93,37 @@ class ServiceAdmin(admin.ModelAdmin):
filter_horizontal = ("categories",)
inlines = [ExternalLinkInline, OfferingInline]
fieldsets = (
(None, {"fields": ("name", "slug", "description", "tagline")}),
(
"Images",
{
"fields": (
"image_library",
"logo",
), # New image library field and legacy field
"description": "Use the Image Library field for new images. Legacy field will be removed after migration.",
},
),
(
"Configuration",
{
"fields": (
"categories",
"features",
"is_featured",
"is_coming_soon",
"disable_listing",
)
},
),
)
def logo_preview(self, obj):
"""Display logo preview in admin list view"""
if obj.logo:
return format_html(
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
)
logo = obj.get_logo
if logo:
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
return "No logo"
logo_preview.short_description = "Logo"

View file

@ -0,0 +1,57 @@
# Generated by Django 5.2 on 2025-07-04 15:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0040_add_image_library"),
]
operations = [
migrations.AddField(
model_name="cloudprovider",
name="image",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="services.imagelibrary",
),
),
migrations.AddField(
model_name="consultingpartner",
name="image",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="services.imagelibrary",
),
),
migrations.AddField(
model_name="service",
name="image",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="services.imagelibrary",
),
),
migrations.AlterField(
model_name="article",
name="image",
field=models.ImageField(
blank=True,
help_text="Title picture for the article",
null=True,
upload_to="article_images/",
),
),
]

View file

@ -0,0 +1,74 @@
# Generated by Django 5.2 on 2025-07-04 15:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0041_add_image_library_references"),
]
operations = [
migrations.RemoveField(
model_name="cloudprovider",
name="image",
),
migrations.RemoveField(
model_name="consultingpartner",
name="image",
),
migrations.RemoveField(
model_name="service",
name="image",
),
migrations.AddField(
model_name="article",
name="image_library",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_references",
to="services.imagelibrary",
),
),
migrations.AddField(
model_name="cloudprovider",
name="image_library",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_references",
to="services.imagelibrary",
),
),
migrations.AddField(
model_name="consultingpartner",
name="image_library",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_references",
to="services.imagelibrary",
),
),
migrations.AddField(
model_name="service",
name="image_library",
field=models.ForeignKey(
blank=True,
help_text="Select an image from the library",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_references",
to="services.imagelibrary",
),
),
]

View file

@ -7,9 +7,10 @@ from django.utils import timezone
from .base import validate_image_size
from .services import Service
from .providers import CloudProvider, ConsultingPartner
from .images import ImageReference
class Article(models.Model):
class Article(ImageReference):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=250, unique=True)
excerpt = models.TextField(
@ -19,9 +20,12 @@ class Article(models.Model):
meta_keywords = models.CharField(
max_length=255, blank=True, help_text="SEO keywords separated by commas"
)
# Original image field - keep temporarily for migration
image = models.ImageField(
upload_to="article_images/",
help_text="Title picture for the article",
null=True,
blank=True,
)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
article_date = models.DateField(
@ -86,6 +90,13 @@ class Article(models.Model):
def get_absolute_url(self):
return reverse("services:article_detail", kwargs={"slug": self.slug})
@property
def get_image(self):
"""Returns the image from library or falls back to legacy image"""
if self.image_library and self.image_library.image:
return self.image_library.image
return self.image
@property
def related_to(self):
"""Returns a string describing what this article is related to"""

View file

@ -182,12 +182,13 @@ class ImageReference(models.Model):
This helps track usage and provides a consistent interface.
"""
image = models.ForeignKey(
image_library = models.ForeignKey(
ImageLibrary,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Select an image from the library",
related_name="%(class)s_references",
)
class Meta:
@ -202,23 +203,23 @@ class ImageReference(models.Model):
if self.pk:
try:
old_instance = self.__class__.objects.get(pk=self.pk)
old_image = old_instance.image
old_image = old_instance.image_library
except self.__class__.DoesNotExist:
pass
super().save(*args, **kwargs)
# Update usage counts
if old_image and old_image != self.image:
if old_image and old_image != self.image_library:
old_image.decrement_usage()
if self.image and self.image != old_image:
self.image.increment_usage()
if self.image_library and self.image_library != old_image:
self.image_library.increment_usage()
def delete(self, *args, **kwargs):
"""
Override delete to update usage count.
"""
if self.image:
self.image.decrement_usage()
if self.image_library:
self.image_library.decrement_usage()
super().delete(*args, **kwargs)

View file

@ -4,9 +4,10 @@ from django.utils.text import slugify
from django_prose_editor.fields import ProseEditorField
from .base import validate_image_size
from .images import ImageReference
class CloudProvider(models.Model):
class CloudProvider(ImageReference):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = ProseEditorField()
@ -15,6 +16,7 @@ class CloudProvider(models.Model):
phone = models.CharField(max_length=25, blank=True, null=True)
email = models.EmailField(max_length=254, blank=True, null=True)
address = models.TextField(max_length=250, blank=True, null=True)
# Original logo field - keep temporarily for migration
logo = models.ImageField(
upload_to="cloud_provider_logos/",
validators=[validate_image_size],
@ -39,11 +41,19 @@ class CloudProvider(models.Model):
def get_absolute_url(self):
return reverse("services:provider_detail", kwargs={"slug": self.slug})
@property
def get_logo(self):
"""Returns the logo from library or falls back to legacy logo"""
if self.image_library and self.image_library.image:
return self.image_library.image
return self.logo
class ConsultingPartner(models.Model):
class ConsultingPartner(ImageReference):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = ProseEditorField()
# Original logo field - keep temporarily for migration
logo = models.ImageField(
upload_to="partner_logos/",
validators=[validate_image_size],
@ -83,3 +93,10 @@ class ConsultingPartner(models.Model):
def get_absolute_url(self):
return reverse("services:partner_detail", kwargs={"slug": self.slug})
@property
def get_logo(self):
"""Returns the logo from library or falls back to legacy logo"""
if self.image_library and self.image_library.image:
return self.image_library.image
return self.logo

View file

@ -13,13 +13,15 @@ from .base import (
Currency,
)
from .providers import CloudProvider
from .images import ImageReference
class Service(models.Model):
class Service(ImageReference):
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)
# Original logo field - keep temporarily for migration
logo = models.ImageField(
upload_to="service_logos/",
validators=[validate_image_size],
@ -58,6 +60,13 @@ class Service(models.Model):
def get_absolute_url(self):
return reverse("services:service_detail", kwargs={"slug": self.slug})
@property
def get_logo(self):
"""Returns the logo from library or falls back to legacy logo"""
if self.image_library and self.image_library.image:
return self.image_library.image
return self.logo
class ServiceOffering(models.Model):
service = models.ForeignKey(

View file

@ -48,9 +48,15 @@
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
<div class="me-3 d-flex align-items-center" style="height: 100%;">
<a href="{{ service.get_absolute_url }}" class="clickable-link">
<img src="{{ service.logo.url }}"
{% if service.get_logo %}
<img src="{{ service.get_logo.url }}"
alt="{{ service.name }}"
style="max-height: 100px; max-width: 250px; object-fit: contain;">
{% else %}
<div class="text-muted" style="height: 100px; width: 250px; display: flex; align-items: center; justify-content: center; border: 1px solid #dee2e6; border-radius: 0.375rem;">
{{ service.name }}
</div>
{% endif %}
</a>
</div>
</div>
@ -105,7 +111,7 @@
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
<div class="me-3 d-flex align-items-center" style="height: 100%;">
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
<img src="{{ provider.logo.url }}"
<img src="{{ provider.get_logo.url }}"
alt="{{ provider.name }}"
style="max-height: 100px; max-width: 250px; object-fit: contain;">
</a>
@ -159,7 +165,7 @@
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
<div class="me-3 d-flex align-items-center" style="height: 100%;">
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
<img src="{{ partner.logo.url }}"
<img src="{{ partner.get_logo.url }}"
alt="{{ partner.name }}"
style="max-height: 100px; max-width: 250px; object-fit: contain;">
</a>

View file

@ -43,7 +43,7 @@
<h5 class="card-title">Service</h5>
{% if article.related_service.logo %}
<div class="mb-3 d-flex" style="height: 60px;">
<img src="{{ article.related_service.logo.url }}" alt="{{ article.related_service.name }} logo"
<img src="{{ article.related_service.get_logo.url }}" alt="{{ article.related_service.name }} logo"
class="img-fluid" style="max-height: 50px; object-fit: contain;">
</div>
{% endif %}
@ -60,7 +60,7 @@
<h5 class="card-title">Partner</h5>
{% if article.related_consulting_partner.logo %}
<div class="mb-3 d-flex" style="height: 60px;">
<img src="{{ article.related_consulting_partner.logo.url }}" alt="{{ article.related_consulting_partner.name }} logo"
<img src="{{ article.related_consulting_partner.get_logo.url }}" alt="{{ article.related_consulting_partner.name }} logo"
class="img-fluid" style="max-height: 50px; object-fit: contain;">
</div>
{% endif %}
@ -77,7 +77,7 @@
<h5 class="card-title">Provider</h5>
{% if article.related_cloud_provider.logo %}
<div class="mb-3 d-flex" style="height: 60px;">
<img src="{{ article.related_cloud_provider.logo.url }}" alt="{{ article.related_cloud_provider.name }} logo"
<img src="{{ article.related_cloud_provider.get_logo.url }}" alt="{{ article.related_cloud_provider.name }} logo"
class="img-fluid" style="max-height: 50px; object-fit: contain;">
</div>
{% endif %}
@ -100,7 +100,7 @@
<div class="col-12 col-md-4 mb-4">
<div class="card h-100 clickable-card" onclick="cardClicked(event, '{{ related_article.get_absolute_url }}')">
{% if related_article.image %}
<img src="{{ related_article.image.url }}" class="card-img-top mb-2" alt="{{ related_article.title }}" style="height: 200px; object-fit: cover;">
<img src="{{ related_article.get_image.url }}" class="card-img-top mb-2" alt="{{ related_article.title }}" style="height: 200px; object-fit: cover;">
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ related_article.title }}</h5>

View file

@ -149,7 +149,7 @@
<div class="d-flex justify-content-between mb-3">
{% if article.image %}
<div class="card__image flex-shrink-0">
<img src="{{ article.image.url }}" alt="{{ article.title }}" class="img-fluid">
<img src="{{ article.get_image.url }}" alt="{{ article.title }}" class="img-fluid">
</div>
{% endif %}
{% if article.is_featured %}

View file

@ -78,7 +78,7 @@
<div class="d-flex align-items-center mb-24">
<div class="card__image mb-0">
{% if selected_offering.service.logo %}
<img class="img-fluid" src="{{ selected_offering.service.logo.url }}" alt="Service Logo">
<img class="img-fluid" src="{{ selected_offering.service.get_logo.url }}" alt="Service Logo">
{% endif %}
</div>
<div class="card__header ps-16">

View file

@ -34,7 +34,7 @@
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
{% if offering.service.logo %}
<a href="{{ offering.service.get_absolute_url }}">
<img class="img-fluid w-100 w-lg-auto" src="{{ offering.service.logo.url }}"
<img class="img-fluid w-100 w-lg-auto" src="{{ offering.service.get_logo.url }}"
alt="{{ offering.service.name }} logo" style="max-height: 120px; object-fit: contain;">
</a>
{% endif %}
@ -50,7 +50,7 @@
<div class="mb-40">
<h3 class="fw-semibold mb-12">Runs on</h3>
<a href="{{ offering.cloud_provider.get_absolute_url }}">
<img class="img-fluid" src="{{ offering.cloud_provider.logo.url }}" alt="{{ offering.cloud_provider.name }} logo" style="max-height: 40px;">
<img class="img-fluid" src="{{ offering.cloud_provider.get_logo.url }}" alt="{{ offering.cloud_provider.name }} logo" style="max-height: 40px;">
</a>
</div>

View file

@ -151,7 +151,7 @@
<div class="d-flex align-items-start mb-3">
<div class="me-3">
{% if offering.service.logo %}
<img src="{{ offering.service.logo.url }}"
<img src="{{ offering.service.get_logo.url }}"
alt="{{ offering.service.name }}"
style="max-height: 50px; max-width: 100px; object-fit: contain;">
{% endif %}
@ -165,7 +165,7 @@
<div class="d-flex align-items-center">
{% if offering.cloud_provider.logo %}
<a href="{{ offering.get_absolute_url }}" class="me-2">
<img src="{{ offering.cloud_provider.logo.url }}"
<img src="{{ offering.cloud_provider.get_logo.url }}"
alt="{{ offering.cloud_provider.name }}"
style="max-height: 30px; max-width: 100px; object-fit: contain;">
</a>

View file

@ -24,7 +24,7 @@
<!-- Logo -->
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
{% if partner.logo %}
<img class="img-fluid w-100 w-lg-auto" src="{{ partner.logo.url }}" alt="{{ partner.name }} logo" style="max-height: 120px; object-fit: contain;">
<img class="img-fluid w-100 w-lg-auto" src="{{ partner.get_logo.url }}" alt="{{ partner.name }} logo" style="max-height: 120px; object-fit: contain;">
{% endif %}
</div>
@ -178,7 +178,7 @@
{% if service.logo %}
<div class="card__image flex-shrink-0">
<a href="{{ service.get_absolute_url }}">
<img src="{{ service.logo.url }}" alt="{{ service.name }} logo" class="img-fluid">
<img src="{{ service.get_logo.url }}" alt="{{ service.name }} logo" class="img-fluid">
</a>
</div>
{% endif %}

View file

@ -115,7 +115,7 @@
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
<div class="me-3">
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
<img src="{{ partner.logo.url }}"
<img src="{{ partner.get_logo.url }}"
alt="{{ partner.name }}"
style="max-height: 100px; max-width: 250px; object-fit: contain;">
</a>

View file

@ -24,7 +24,7 @@
<!-- Logo -->
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
{% if provider.logo %}
<img class="img-fluid w-100 w-lg-auto" src="{{ provider.logo.url }}" alt="{{ provider.name }} logo" style="max-height: 120px; object-fit: contain;">
<img class="img-fluid w-100 w-lg-auto" src="{{ provider.get_logo.url }}" alt="{{ provider.name }} logo" style="max-height: 120px; object-fit: contain;">
{% endif %}
</div>
@ -178,7 +178,7 @@
{% if offering.service.logo %}
<div class="card__image flex-shrink-0">
<a href="{{ offering.get_absolute_url }}">
<img src="{{ offering.service.logo.url }}" alt="{{ offering.service.name }} logo" class="img-fluid">
<img src="{{ offering.service.get_logo.url }}" alt="{{ offering.service.name }} logo" class="img-fluid">
</a>
</div>
{% endif %}

View file

@ -99,7 +99,7 @@
<div class="me-3 d-flex align-items-center" style="height: 100%;">
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
{% if provider.logo %}
<img src="{{ provider.logo.url }}"
<img src="{{ provider.get_logo.url }}"
alt="{{ provider.name }}"
style="max-height: 100px; max-width: 250px; object-fit: contain;">
</a>

View file

@ -23,7 +23,7 @@
<!-- Logo -->
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
{% if service.logo %}
<img class="img-fluid w-100 w-lg-auto" src="{{ service.logo.url }}" alt="{{ service.name }} logo" style="max-height: 120px; object-fit: contain;">
<img class="img-fluid w-100 w-lg-auto" src="{{ service.get_logo.url }}" alt="{{ service.name }} logo" style="max-height: 120px; object-fit: contain;">
{% endif %}
</div>
@ -184,7 +184,7 @@
<div class="card-body text-center">
{% if offering.cloud_provider.logo %}
<div class="mb-3 d-flex align-items-center justify-content-center" style="height: 80px;">
<img src="{{ offering.cloud_provider.logo.url }}" alt="{{ offering.cloud_provider.name }} logo"
<img src="{{ offering.cloud_provider.get_logo.url }}" alt="{{ offering.cloud_provider.name }} logo"
class="img-fluid" style="max-height: 60px; object-fit: contain;">
</div>
{% else %}

View file

@ -156,7 +156,7 @@
<div class="d-flex justify-content-between mb-3">
{% if service.logo %}
<div class="card__image flex-shrink-0">
<img src="{{ service.logo.url }}" alt="{{ service.name }} logo" class="img-fluid">
<img src="{{ service.get_logo.url }}" alt="{{ service.name }} logo" class="img-fluid">
</div>
{% endif %}
{% if service.is_featured %}

View file

@ -119,8 +119,8 @@ def json_ld_structured_data(context):
}
# Add image if available
if hasattr(service, "logo") and service.logo:
data["image"] = request.build_absolute_uri(service.logo.url)
if hasattr(service, "get_logo") and service.get_logo:
data["image"] = request.build_absolute_uri(service.get_logo.url)
# Add offerings if available
if hasattr(service, "offerings") and service.offerings.exists():
@ -143,8 +143,8 @@ def json_ld_structured_data(context):
}
# Add image if available
if hasattr(provider, "logo") and provider.logo:
data["logo"] = request.build_absolute_uri(provider.logo.url)
if hasattr(provider, "get_logo") and provider.get_logo:
data["logo"] = request.build_absolute_uri(provider.get_logo.url)
# Add contact information if available
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
@ -179,8 +179,8 @@ def json_ld_structured_data(context):
}
# Add image if available
if hasattr(partner, "logo") and partner.logo:
data["logo"] = request.build_absolute_uri(partner.logo.url)
if hasattr(partner, "get_logo") and partner.get_logo:
data["logo"] = request.build_absolute_uri(partner.get_logo.url)
# Add contact information if available
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
@ -219,8 +219,8 @@ def json_ld_structured_data(context):
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)
if hasattr(offering.service, "get_logo") and offering.service.get_logo:
data["image"] = request.build_absolute_uri(offering.service.get_logo.url)
# Add offers if available
if hasattr(offering, "plans") and offering.plans.exists():