diff --git a/IMAGE_LIBRARY_MIGRATION_STATUS.md b/IMAGE_LIBRARY_MIGRATION_STATUS.md new file mode 100644 index 0000000..d2e27de --- /dev/null +++ b/IMAGE_LIBRARY_MIGRATION_STATUS.md @@ -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 diff --git a/hub/services/admin/articles.py b/hub/services/admin/articles.py index 90298a4..387ec30 100644 --- a/hub/services/admin/articles.py +++ b/hub/services/admin/articles.py @@ -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( - '', obj.image.url - ) + image = obj.get_image + if image: + return format_html('', image.url) return "No image" image_preview.short_description = "Image" diff --git a/hub/services/admin/providers.py b/hub/services/admin/providers.py index 8ef3ad3..d33e291 100644 --- a/hub/services/admin/providers.py +++ b/hub/services/admin/providers.py @@ -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( - '', obj.logo.url - ) + logo = obj.get_logo + if logo: + return format_html('', 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( - '', obj.logo.url - ) + logo = obj.get_logo + if logo: + return format_html('', logo.url) return "No logo" logo_preview.short_description = "Logo" diff --git a/hub/services/admin/services.py b/hub/services/admin/services.py index 41bc97f..b34cf97 100644 --- a/hub/services/admin/services.py +++ b/hub/services/admin/services.py @@ -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( - '', obj.logo.url - ) + logo = obj.get_logo + if logo: + return format_html('', logo.url) return "No logo" logo_preview.short_description = "Logo" diff --git a/hub/services/migrations/0041_add_image_library_references.py b/hub/services/migrations/0041_add_image_library_references.py new file mode 100644 index 0000000..231520b --- /dev/null +++ b/hub/services/migrations/0041_add_image_library_references.py @@ -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/", + ), + ), + ] diff --git a/hub/services/migrations/0042_fix_image_library_field_name.py b/hub/services/migrations/0042_fix_image_library_field_name.py new file mode 100644 index 0000000..0996f8b --- /dev/null +++ b/hub/services/migrations/0042_fix_image_library_field_name.py @@ -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", + ), + ), + ] diff --git a/hub/services/models/articles.py b/hub/services/models/articles.py index b1b85e7..8ea50c0 100644 --- a/hub/services/models/articles.py +++ b/hub/services/models/articles.py @@ -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""" diff --git a/hub/services/models/images.py b/hub/services/models/images.py index 90052ab..3bf72f7 100644 --- a/hub/services/models/images.py +++ b/hub/services/models/images.py @@ -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) diff --git a/hub/services/models/providers.py b/hub/services/models/providers.py index e3257ea..5567cb9 100644 --- a/hub/services/models/providers.py +++ b/hub/services/models/providers.py @@ -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 diff --git a/hub/services/models/services.py b/hub/services/models/services.py index 8b6984d..af4c2e0 100644 --- a/hub/services/models/services.py +++ b/hub/services/models/services.py @@ -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( diff --git a/hub/services/templates/pages/homepage.html b/hub/services/templates/pages/homepage.html index f16da44..a7507c6 100644 --- a/hub/services/templates/pages/homepage.html +++ b/hub/services/templates/pages/homepage.html @@ -48,9 +48,15 @@
- {{ service.name }} + {% else %} +
+ {{ service.name }} +
+ {% endif %}
@@ -105,7 +111,7 @@
- {{ provider.name }} @@ -159,7 +165,7 @@
- {{ partner.name }} diff --git a/hub/services/templates/services/article_detail.html b/hub/services/templates/services/article_detail.html index b3264e5..2f67b43 100644 --- a/hub/services/templates/services/article_detail.html +++ b/hub/services/templates/services/article_detail.html @@ -43,7 +43,7 @@
Service
{% if article.related_service.logo %}
- {{ article.related_service.name }} logo
{% endif %} @@ -60,7 +60,7 @@
Partner
{% if article.related_consulting_partner.logo %}
- {{ article.related_consulting_partner.name }} logo
{% endif %} @@ -77,7 +77,7 @@
Provider
{% if article.related_cloud_provider.logo %}
- {{ article.related_cloud_provider.name }} logo
{% endif %} @@ -100,7 +100,7 @@
{% if related_article.image %} - {{ related_article.title }} + {{ related_article.title }} {% endif %}
{{ related_article.title }}
diff --git a/hub/services/templates/services/article_list.html b/hub/services/templates/services/article_list.html index ac86df4..47ae5ae 100644 --- a/hub/services/templates/services/article_list.html +++ b/hub/services/templates/services/article_list.html @@ -149,7 +149,7 @@
{% if article.image %}
- {{ article.title }} + {{ article.title }}
{% endif %} {% if article.is_featured %} diff --git a/hub/services/templates/services/lead_form.html b/hub/services/templates/services/lead_form.html index 73e0585..bcc26d6 100644 --- a/hub/services/templates/services/lead_form.html +++ b/hub/services/templates/services/lead_form.html @@ -78,7 +78,7 @@
{% if selected_offering.service.logo %} - Service Logo + Service Logo {% endif %}
diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html index e6682e3..2fe0eba 100644 --- a/hub/services/templates/services/offering_detail.html +++ b/hub/services/templates/services/offering_detail.html @@ -34,7 +34,7 @@
{% if offering.service.logo %} - {{ offering.service.name }} logo {% endif %} @@ -50,7 +50,7 @@

Runs on

- {{ offering.cloud_provider.name }} logo + {{ offering.cloud_provider.name }} logo
diff --git a/hub/services/templates/services/offering_list.html b/hub/services/templates/services/offering_list.html index 0f138e3..741352b 100644 --- a/hub/services/templates/services/offering_list.html +++ b/hub/services/templates/services/offering_list.html @@ -151,7 +151,7 @@
{% if offering.service.logo %} - {{ offering.service.name }} {% endif %} @@ -165,7 +165,7 @@
{% if offering.cloud_provider.logo %} - {{ offering.cloud_provider.name }} diff --git a/hub/services/templates/services/partner_detail.html b/hub/services/templates/services/partner_detail.html index 7e90b82..1e78144 100644 --- a/hub/services/templates/services/partner_detail.html +++ b/hub/services/templates/services/partner_detail.html @@ -24,7 +24,7 @@
{% if partner.logo %} - {{ partner.name }} logo + {{ partner.name }} logo {% endif %}
@@ -178,7 +178,7 @@ {% if service.logo %} {% endif %} diff --git a/hub/services/templates/services/partner_list.html b/hub/services/templates/services/partner_list.html index fb196dc..5f554ac 100644 --- a/hub/services/templates/services/partner_list.html +++ b/hub/services/templates/services/partner_list.html @@ -115,7 +115,7 @@
- {{ partner.name }} diff --git a/hub/services/templates/services/provider_detail.html b/hub/services/templates/services/provider_detail.html index 151c986..aa9c360 100644 --- a/hub/services/templates/services/provider_detail.html +++ b/hub/services/templates/services/provider_detail.html @@ -24,7 +24,7 @@
{% if provider.logo %} - {{ provider.name }} logo + {{ provider.name }} logo {% endif %}
@@ -178,7 +178,7 @@ {% if offering.service.logo %} {% endif %} diff --git a/hub/services/templates/services/provider_list.html b/hub/services/templates/services/provider_list.html index 5484b7e..ca27bab 100644 --- a/hub/services/templates/services/provider_list.html +++ b/hub/services/templates/services/provider_list.html @@ -99,7 +99,7 @@
{% if provider.logo %} - {{ provider.name }} diff --git a/hub/services/templates/services/service_detail.html b/hub/services/templates/services/service_detail.html index 5054d61..bdb3748 100644 --- a/hub/services/templates/services/service_detail.html +++ b/hub/services/templates/services/service_detail.html @@ -23,7 +23,7 @@
{% if service.logo %} - {{ service.name }} logo + {{ service.name }} logo {% endif %}
@@ -184,7 +184,7 @@
{% if offering.cloud_provider.logo %}
- {{ offering.cloud_provider.name }} logo
{% else %} diff --git a/hub/services/templates/services/service_list.html b/hub/services/templates/services/service_list.html index da16d3e..0420a74 100644 --- a/hub/services/templates/services/service_list.html +++ b/hub/services/templates/services/service_list.html @@ -156,7 +156,7 @@
{% if service.logo %}
- {{ service.name }} logo + {{ service.name }} logo
{% endif %} {% if service.is_featured %} diff --git a/hub/services/templatetags/json_ld_tags.py b/hub/services/templatetags/json_ld_tags.py index eb18007..c9225d5 100644 --- a/hub/services/templatetags/json_ld_tags.py +++ b/hub/services/templatetags/json_ld_tags.py @@ -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():