image library migration step 1
This commit is contained in:
parent
07bea333bc
commit
1a2bbb1c35
23 changed files with 413 additions and 57 deletions
81
IMAGE_LIBRARY_MIGRATION_STATUS.md
Normal file
81
IMAGE_LIBRARY_MIGRATION_STATUS.md
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
57
hub/services/migrations/0041_add_image_library_references.py
Normal file
57
hub/services/migrations/0041_add_image_library_references.py
Normal 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/",
|
||||
),
|
||||
),
|
||||
]
|
74
hub/services/migrations/0042_fix_image_library_field_name.py
Normal file
74
hub/services/migrations/0042_fix_image_library_field_name.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue