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")
|
readonly_fields = ("created_at", "updated_at")
|
||||||
ordering = ("-article_date",)
|
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):
|
def image_preview(self, obj):
|
||||||
"""Display image preview in admin list view"""
|
"""Display image preview in admin list view"""
|
||||||
if obj.image:
|
image = obj.get_image
|
||||||
return format_html(
|
if image:
|
||||||
'<img src="{}" style="max-height: 50px;"/>', obj.image.url
|
return format_html('<img src="{}" style="max-height: 50px;"/>', image.url)
|
||||||
)
|
|
||||||
return "No image"
|
return "No image"
|
||||||
|
|
||||||
image_preview.short_description = "Image"
|
image_preview.short_description = "Image"
|
||||||
|
|
|
@ -47,12 +47,30 @@ class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||||
inlines = [OfferingInline]
|
inlines = [OfferingInline]
|
||||||
ordering = ("order",)
|
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):
|
def logo_preview(self, obj):
|
||||||
"""Display logo preview in admin list view"""
|
"""Display logo preview in admin list view"""
|
||||||
if obj.logo:
|
logo = obj.get_logo
|
||||||
return format_html(
|
if logo:
|
||||||
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
|
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
|
||||||
)
|
|
||||||
return "No logo"
|
return "No logo"
|
||||||
|
|
||||||
logo_preview.short_description = "Logo"
|
logo_preview.short_description = "Logo"
|
||||||
|
@ -75,12 +93,34 @@ class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||||
filter_horizontal = ("services", "cloud_providers")
|
filter_horizontal = ("services", "cloud_providers")
|
||||||
ordering = ("order",)
|
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):
|
def logo_preview(self, obj):
|
||||||
"""Display logo preview in admin list view"""
|
"""Display logo preview in admin list view"""
|
||||||
if obj.logo:
|
logo = obj.get_logo
|
||||||
return format_html(
|
if logo:
|
||||||
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
|
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
|
||||||
)
|
|
||||||
return "No logo"
|
return "No logo"
|
||||||
|
|
||||||
logo_preview.short_description = "Logo"
|
logo_preview.short_description = "Logo"
|
||||||
|
|
|
@ -93,12 +93,37 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||||
filter_horizontal = ("categories",)
|
filter_horizontal = ("categories",)
|
||||||
inlines = [ExternalLinkInline, OfferingInline]
|
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):
|
def logo_preview(self, obj):
|
||||||
"""Display logo preview in admin list view"""
|
"""Display logo preview in admin list view"""
|
||||||
if obj.logo:
|
logo = obj.get_logo
|
||||||
return format_html(
|
if logo:
|
||||||
'<img src="{}" style="max-height: 50px;"/>', obj.logo.url
|
return format_html('<img src="{}" style="max-height: 50px;"/>', logo.url)
|
||||||
)
|
|
||||||
return "No logo"
|
return "No logo"
|
||||||
|
|
||||||
logo_preview.short_description = "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 .base import validate_image_size
|
||||||
from .services import Service
|
from .services import Service
|
||||||
from .providers import CloudProvider, ConsultingPartner
|
from .providers import CloudProvider, ConsultingPartner
|
||||||
|
from .images import ImageReference
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(ImageReference):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
slug = models.SlugField(max_length=250, unique=True)
|
slug = models.SlugField(max_length=250, unique=True)
|
||||||
excerpt = models.TextField(
|
excerpt = models.TextField(
|
||||||
|
@ -19,9 +20,12 @@ class Article(models.Model):
|
||||||
meta_keywords = models.CharField(
|
meta_keywords = models.CharField(
|
||||||
max_length=255, blank=True, help_text="SEO keywords separated by commas"
|
max_length=255, blank=True, help_text="SEO keywords separated by commas"
|
||||||
)
|
)
|
||||||
|
# Original image field - keep temporarily for migration
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
upload_to="article_images/",
|
upload_to="article_images/",
|
||||||
help_text="Title picture for the article",
|
help_text="Title picture for the article",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
|
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
|
||||||
article_date = models.DateField(
|
article_date = models.DateField(
|
||||||
|
@ -86,6 +90,13 @@ class Article(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("services:article_detail", kwargs={"slug": self.slug})
|
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
|
@property
|
||||||
def related_to(self):
|
def related_to(self):
|
||||||
"""Returns a string describing what this article is related to"""
|
"""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.
|
This helps track usage and provides a consistent interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
image = models.ForeignKey(
|
image_library = models.ForeignKey(
|
||||||
ImageLibrary,
|
ImageLibrary,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Select an image from the library",
|
help_text="Select an image from the library",
|
||||||
|
related_name="%(class)s_references",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -202,23 +203,23 @@ class ImageReference(models.Model):
|
||||||
if self.pk:
|
if self.pk:
|
||||||
try:
|
try:
|
||||||
old_instance = self.__class__.objects.get(pk=self.pk)
|
old_instance = self.__class__.objects.get(pk=self.pk)
|
||||||
old_image = old_instance.image
|
old_image = old_instance.image_library
|
||||||
except self.__class__.DoesNotExist:
|
except self.__class__.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Update usage counts
|
# Update usage counts
|
||||||
if old_image and old_image != self.image:
|
if old_image and old_image != self.image_library:
|
||||||
old_image.decrement_usage()
|
old_image.decrement_usage()
|
||||||
|
|
||||||
if self.image and self.image != old_image:
|
if self.image_library and self.image_library != old_image:
|
||||||
self.image.increment_usage()
|
self.image_library.increment_usage()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Override delete to update usage count.
|
Override delete to update usage count.
|
||||||
"""
|
"""
|
||||||
if self.image:
|
if self.image_library:
|
||||||
self.image.decrement_usage()
|
self.image_library.decrement_usage()
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
|
@ -4,9 +4,10 @@ from django.utils.text import slugify
|
||||||
from django_prose_editor.fields import ProseEditorField
|
from django_prose_editor.fields import ProseEditorField
|
||||||
|
|
||||||
from .base import validate_image_size
|
from .base import validate_image_size
|
||||||
|
from .images import ImageReference
|
||||||
|
|
||||||
|
|
||||||
class CloudProvider(models.Model):
|
class CloudProvider(ImageReference):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
description = ProseEditorField()
|
description = ProseEditorField()
|
||||||
|
@ -15,6 +16,7 @@ class CloudProvider(models.Model):
|
||||||
phone = models.CharField(max_length=25, blank=True, null=True)
|
phone = models.CharField(max_length=25, blank=True, null=True)
|
||||||
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)
|
||||||
|
# Original logo field - keep temporarily for migration
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to="cloud_provider_logos/",
|
upload_to="cloud_provider_logos/",
|
||||||
validators=[validate_image_size],
|
validators=[validate_image_size],
|
||||||
|
@ -39,11 +41,19 @@ class CloudProvider(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("services:provider_detail", kwargs={"slug": self.slug})
|
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)
|
name = models.CharField(max_length=200)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
description = ProseEditorField()
|
description = ProseEditorField()
|
||||||
|
# Original logo field - keep temporarily for migration
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to="partner_logos/",
|
upload_to="partner_logos/",
|
||||||
validators=[validate_image_size],
|
validators=[validate_image_size],
|
||||||
|
@ -83,3 +93,10 @@ class ConsultingPartner(models.Model):
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("services:partner_detail", kwargs={"slug": self.slug})
|
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,
|
Currency,
|
||||||
)
|
)
|
||||||
from .providers import CloudProvider
|
from .providers import CloudProvider
|
||||||
|
from .images import ImageReference
|
||||||
|
|
||||||
|
|
||||||
class Service(models.Model):
|
class Service(ImageReference):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
slug = models.SlugField(max_length=250, unique=True)
|
slug = models.SlugField(max_length=250, unique=True)
|
||||||
description = ProseEditorField()
|
description = ProseEditorField()
|
||||||
tagline = models.TextField(max_length=500, blank=True, null=True)
|
tagline = models.TextField(max_length=500, blank=True, null=True)
|
||||||
|
# Original logo field - keep temporarily for migration
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to="service_logos/",
|
upload_to="service_logos/",
|
||||||
validators=[validate_image_size],
|
validators=[validate_image_size],
|
||||||
|
@ -58,6 +60,13 @@ class Service(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("services:service_detail", kwargs={"slug": self.slug})
|
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):
|
class ServiceOffering(models.Model):
|
||||||
service = models.ForeignKey(
|
service = models.ForeignKey(
|
||||||
|
|
|
@ -48,9 +48,15 @@
|
||||||
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
<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%;">
|
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
||||||
<a href="{{ service.get_absolute_url }}" class="clickable-link">
|
<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 }}"
|
alt="{{ service.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,7 +111,7 @@
|
||||||
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
<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%;">
|
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
||||||
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
||||||
<img src="{{ provider.logo.url }}"
|
<img src="{{ provider.get_logo.url }}"
|
||||||
alt="{{ provider.name }}"
|
alt="{{ provider.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
|
@ -159,7 +165,7 @@
|
||||||
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
<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%;">
|
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
||||||
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
|
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
|
||||||
<img src="{{ partner.logo.url }}"
|
<img src="{{ partner.get_logo.url }}"
|
||||||
alt="{{ partner.name }}"
|
alt="{{ partner.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<h5 class="card-title">Service</h5>
|
<h5 class="card-title">Service</h5>
|
||||||
{% if article.related_service.logo %}
|
{% if article.related_service.logo %}
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
<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;">
|
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<h5 class="card-title">Partner</h5>
|
<h5 class="card-title">Partner</h5>
|
||||||
{% if article.related_consulting_partner.logo %}
|
{% if article.related_consulting_partner.logo %}
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
<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;">
|
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
<h5 class="card-title">Provider</h5>
|
<h5 class="card-title">Provider</h5>
|
||||||
{% if article.related_cloud_provider.logo %}
|
{% if article.related_cloud_provider.logo %}
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
<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;">
|
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
<div class="col-12 col-md-4 mb-4">
|
<div class="col-12 col-md-4 mb-4">
|
||||||
<div class="card h-100 clickable-card" onclick="cardClicked(event, '{{ related_article.get_absolute_url }}')">
|
<div class="card h-100 clickable-card" onclick="cardClicked(event, '{{ related_article.get_absolute_url }}')">
|
||||||
{% if related_article.image %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<h5 class="card-title">{{ related_article.title }}</h5>
|
<h5 class="card-title">{{ related_article.title }}</h5>
|
||||||
|
|
|
@ -149,7 +149,7 @@
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
{% if article.image %}
|
{% if article.image %}
|
||||||
<div class="card__image flex-shrink-0">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if article.is_featured %}
|
{% if article.is_featured %}
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
<div class="d-flex align-items-center mb-24">
|
<div class="d-flex align-items-center mb-24">
|
||||||
<div class="card__image mb-0">
|
<div class="card__image mb-0">
|
||||||
{% if selected_offering.service.logo %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__header ps-16">
|
<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;">
|
<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 %}
|
{% if offering.service.logo %}
|
||||||
<a href="{{ offering.service.get_absolute_url }}">
|
<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;">
|
alt="{{ offering.service.name }} logo" style="max-height: 120px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<div class="mb-40">
|
<div class="mb-40">
|
||||||
<h3 class="fw-semibold mb-12">Runs on</h3>
|
<h3 class="fw-semibold mb-12">Runs on</h3>
|
||||||
<a href="{{ offering.cloud_provider.get_absolute_url }}">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@
|
||||||
<div class="d-flex align-items-start mb-3">
|
<div class="d-flex align-items-start mb-3">
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
{% if offering.service.logo %}
|
{% if offering.service.logo %}
|
||||||
<img src="{{ offering.service.logo.url }}"
|
<img src="{{ offering.service.get_logo.url }}"
|
||||||
alt="{{ offering.service.name }}"
|
alt="{{ offering.service.name }}"
|
||||||
style="max-height: 50px; max-width: 100px; object-fit: contain;">
|
style="max-height: 50px; max-width: 100px; object-fit: contain;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
{% if offering.cloud_provider.logo %}
|
{% if offering.cloud_provider.logo %}
|
||||||
<a href="{{ offering.get_absolute_url }}" class="me-2">
|
<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 }}"
|
alt="{{ offering.cloud_provider.name }}"
|
||||||
style="max-height: 30px; max-width: 100px; object-fit: contain;">
|
style="max-height: 30px; max-width: 100px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
||||||
{% if partner.logo %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
{% if service.logo %}
|
{% if service.logo %}
|
||||||
<div class="card__image flex-shrink-0">
|
<div class="card__image flex-shrink-0">
|
||||||
<a href="{{ service.get_absolute_url }}">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
|
<a href="{{ partner.get_absolute_url }}" class="clickable-link">
|
||||||
<img src="{{ partner.logo.url }}"
|
<img src="{{ partner.get_logo.url }}"
|
||||||
alt="{{ partner.name }}"
|
alt="{{ partner.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
||||||
{% if provider.logo %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
{% if offering.service.logo %}
|
{% if offering.service.logo %}
|
||||||
<div class="card__image flex-shrink-0">
|
<div class="card__image flex-shrink-0">
|
||||||
<a href="{{ offering.get_absolute_url }}">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
||||||
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
||||||
{% if provider.logo %}
|
{% if provider.logo %}
|
||||||
<img src="{{ provider.logo.url }}"
|
<img src="{{ provider.get_logo.url }}"
|
||||||
alt="{{ provider.name }}"
|
alt="{{ provider.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
<div class="mb-40 border rounded-4 p-4 d-flex align-items-center justify-content-center" style="min-height: 160px;">
|
||||||
{% if service.logo %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
{% if offering.cloud_provider.logo %}
|
{% if offering.cloud_provider.logo %}
|
||||||
<div class="mb-3 d-flex align-items-center justify-content-center" style="height: 80px;">
|
<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;">
|
class="img-fluid" style="max-height: 60px; object-fit: contain;">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
{% if service.logo %}
|
{% if service.logo %}
|
||||||
<div class="card__image flex-shrink-0">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if service.is_featured %}
|
{% if service.is_featured %}
|
||||||
|
|
|
@ -119,8 +119,8 @@ def json_ld_structured_data(context):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add image if available
|
# Add image if available
|
||||||
if hasattr(service, "logo") and service.logo:
|
if hasattr(service, "get_logo") and service.get_logo:
|
||||||
data["image"] = request.build_absolute_uri(service.logo.url)
|
data["image"] = request.build_absolute_uri(service.get_logo.url)
|
||||||
|
|
||||||
# Add offerings if available
|
# Add offerings if available
|
||||||
if hasattr(service, "offerings") and service.offerings.exists():
|
if hasattr(service, "offerings") and service.offerings.exists():
|
||||||
|
@ -143,8 +143,8 @@ def json_ld_structured_data(context):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add image if available
|
# Add image if available
|
||||||
if hasattr(provider, "logo") and provider.logo:
|
if hasattr(provider, "get_logo") and provider.get_logo:
|
||||||
data["logo"] = request.build_absolute_uri(provider.logo.url)
|
data["logo"] = request.build_absolute_uri(provider.get_logo.url)
|
||||||
|
|
||||||
# Add contact information if available
|
# Add contact information if available
|
||||||
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
|
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
|
||||||
|
@ -179,8 +179,8 @@ def json_ld_structured_data(context):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add image if available
|
# Add image if available
|
||||||
if hasattr(partner, "logo") and partner.logo:
|
if hasattr(partner, "get_logo") and partner.get_logo:
|
||||||
data["logo"] = request.build_absolute_uri(partner.logo.url)
|
data["logo"] = request.build_absolute_uri(partner.get_logo.url)
|
||||||
|
|
||||||
# Add contact information if available
|
# Add contact information if available
|
||||||
contact_point = {"@type": "ContactPoint", "contactType": "Customer Support"}
|
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}
|
data["brand"] = {"@type": "Brand", "name": offering.service.name}
|
||||||
|
|
||||||
# Add image if available
|
# Add image if available
|
||||||
if hasattr(offering.service, "logo") and offering.service.logo:
|
if hasattr(offering.service, "get_logo") and offering.service.get_logo:
|
||||||
data["image"] = request.build_absolute_uri(offering.service.logo.url)
|
data["image"] = request.build_absolute_uri(offering.service.get_logo.url)
|
||||||
|
|
||||||
# Add offers if available
|
# Add offers if available
|
||||||
if hasattr(offering, "plans") and offering.plans.exists():
|
if hasattr(offering, "plans") and offering.plans.exists():
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue