diff --git a/IMAGE_LIBRARY_MIGRATION_STATUS.md b/IMAGE_LIBRARY_MIGRATION_STATUS.md
deleted file mode 100644
index d2e27de..0000000
--- a/IMAGE_LIBRARY_MIGRATION_STATUS.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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/__init__.py b/hub/services/admin/__init__.py
index fba7be9..3c79092 100644
--- a/hub/services/admin/__init__.py
+++ b/hub/services/admin/__init__.py
@@ -4,7 +4,6 @@
from .articles import *
from .base import *
from .content import *
-from .images import *
from .leads import *
from .pricing import *
from .providers import *
diff --git a/hub/services/admin/articles.py b/hub/services/admin/articles.py
index 387ec30..e6dad8c 100644
--- a/hub/services/admin/articles.py
+++ b/hub/services/admin/articles.py
@@ -45,7 +45,7 @@ class ArticleAdmin(admin.ModelAdmin):
"image_preview",
"is_published",
"is_featured",
- "article_date",
+ "created_at",
)
list_filter = (
"is_published",
@@ -54,54 +54,18 @@ class ArticleAdmin(admin.ModelAdmin):
"related_service",
"related_consulting_partner",
"related_cloud_provider",
- "article_date",
+ "created_at",
)
search_fields = ("title", "excerpt", "content", "meta_keywords")
prepopulated_fields = {"slug": ("title",)}
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"""
- image = obj.get_image
- if image:
- return format_html('
', image.url)
+ if obj.image:
+ return format_html(
+ '
', obj.image.url
+ )
return "No image"
image_preview.short_description = "Image"
diff --git a/hub/services/admin/images.py b/hub/services/admin/images.py
deleted file mode 100644
index 917d0db..0000000
--- a/hub/services/admin/images.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from django.contrib import admin
-from django.utils.html import format_html
-from django.urls import reverse
-from django.utils.safestring import mark_safe
-from ..models.images import ImageLibrary
-
-
-@admin.register(ImageLibrary)
-class ImageLibraryAdmin(admin.ModelAdmin):
- """
- Admin interface for the Image Library.
- """
-
- list_display = [
- "image_thumbnail",
- "name",
- "category",
- "get_dimensions",
- "get_file_size_display",
- "usage_count",
- "uploaded_by",
- "uploaded_at",
- ]
-
- list_filter = [
- "category",
- "uploaded_at",
- "uploaded_by",
- ]
-
- search_fields = [
- "name",
- "description",
- "alt_text",
- "tags",
- ]
-
- readonly_fields = [
- "width",
- "height",
- "file_size",
- "usage_count",
- "uploaded_at",
- "updated_at",
- "image_preview",
- ]
-
- prepopulated_fields = {"slug": ("name",)}
-
- fieldsets = (
- ("Image Information", {"fields": ("name", "slug", "description", "alt_text")}),
- ("Image File", {"fields": ("image", "image_preview")}),
- ("Categorization", {"fields": ("category", "tags")}),
- (
- "Metadata",
- {
- "fields": ("width", "height", "file_size", "usage_count"),
- "classes": ("collapse",),
- },
- ),
- (
- "Timestamps",
- {
- "fields": ("uploaded_by", "uploaded_at", "updated_at"),
- "classes": ("collapse",),
- },
- ),
- )
-
- def image_thumbnail(self, obj):
- """
- Display small thumbnail in list view.
- """
- if obj.image:
- return format_html(
- '
',
- obj.image.url,
- )
- return "No Image"
-
- image_thumbnail.short_description = "Thumbnail"
-
- def image_preview(self, obj):
- """
- Display larger preview in detail view.
- """
- if obj.image:
- return format_html(
- '
',
- obj.image.url,
- )
- return "No Image"
-
- image_preview.short_description = "Preview"
-
- def get_dimensions(self, obj):
- """
- Display image dimensions.
- """
- if obj.width and obj.height:
- return f"{obj.width} × {obj.height}"
- return "Unknown"
-
- get_dimensions.short_description = "Dimensions"
-
- def save_model(self, request, obj, form, change):
- """
- Set uploaded_by field to current user if not already set.
- """
- if not change: # Only set on creation
- obj.uploaded_by = request.user
- super().save_model(request, obj, form, change)
-
- class Media:
- css = {"all": ("admin/css/image_library.css",)}
diff --git a/hub/services/admin/providers.py b/hub/services/admin/providers.py
index d33e291..8ef3ad3 100644
--- a/hub/services/admin/providers.py
+++ b/hub/services/admin/providers.py
@@ -47,30 +47,12 @@ 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"""
- logo = obj.get_logo
- if logo:
- return format_html('
', logo.url)
+ if obj.logo:
+ return format_html(
+ '
', obj.logo.url
+ )
return "No logo"
logo_preview.short_description = "Logo"
@@ -93,34 +75,12 @@ 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"""
- logo = obj.get_logo
- if logo:
- return format_html('
', logo.url)
+ if obj.logo:
+ return format_html(
+ '
', obj.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 b34cf97..c975884 100644
--- a/hub/services/admin/services.py
+++ b/hub/services/admin/services.py
@@ -49,9 +49,9 @@ class PlanInline(admin.StackedInline):
extra = 1
fieldsets = (
(None, {"fields": ("name", "description", "plan_description")}),
- ("Display Options", {"fields": ("is_best", "order")}),
+ ("Display Options", {"fields": ("is_best",)}),
)
- show_change_link = True
+ show_change_link = True # This allows clicking through to the Plan admin where prices can be managed
class OfferingInline(admin.StackedInline):
@@ -93,37 +93,12 @@ 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"""
- logo = obj.get_logo
- if logo:
- return format_html('
', logo.url)
+ if obj.logo:
+ return format_html(
+ '
', obj.logo.url
+ )
return "No logo"
logo_preview.short_description = "Logo"
diff --git a/hub/services/forms/lead.py b/hub/services/forms.py
similarity index 94%
rename from hub/services/forms/lead.py
rename to hub/services/forms.py
index d02c876..a608d44 100644
--- a/hub/services/forms/lead.py
+++ b/hub/services/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from ..models import Lead
+from .models import Lead, Plan
class LeadForm(forms.ModelForm):
diff --git a/hub/services/forms/__init__.py b/hub/services/forms/__init__.py
deleted file mode 100644
index 920f500..0000000
--- a/hub/services/forms/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .lead import LeadForm
-from .image_library import ImageLibraryField, ImageLibraryWidget
diff --git a/hub/services/forms/image_library.py b/hub/services/forms/image_library.py
deleted file mode 100644
index 1770456..0000000
--- a/hub/services/forms/image_library.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from django import forms
-from django.utils.safestring import mark_safe
-from django.utils.html import format_html
-from django.urls import reverse
-from ..models.images import ImageLibrary
-
-
-class ImageLibraryWidget(forms.Select):
- """
- Custom widget for selecting images from the library with thumbnails.
- """
-
- def __init__(self, attrs=None, choices=(), show_thumbnails=True):
- self.show_thumbnails = show_thumbnails
- super().__init__(attrs, choices)
-
- def format_value(self, value):
- """
- Format the selected value for display.
- """
- if value is None:
- return ""
- return str(value)
-
- def render(self, name, value, attrs=None, renderer=None):
- """
- Render the widget with thumbnails.
- """
- if attrs is None:
- attrs = {}
-
- # Add CSS class for styling
- attrs["class"] = attrs.get("class", "") + " image-library-select"
-
- # Get all images for the select options
- images = ImageLibrary.objects.all().order_by("name")
-
- # Build choices with thumbnails
- choices = [("", "--- Select an image ---")]
- for image in images:
- thumbnail_html = ""
- if self.show_thumbnails and image.image:
- thumbnail_html = format_html(
- '
',
- image.image.url,
- )
-
- choice_text = (
- f"{image.name} ({image.get_category_display()}){thumbnail_html}"
- )
- choices.append((image.pk, choice_text))
-
- # Build the select element
- select_html = format_html(
- '',
- name,
- attrs.get("id", ""),
- self._build_attrs_string(attrs),
- self._build_options(choices, value),
- )
-
- # Add preview area
- preview_html = ""
- if value:
- try:
- image = ImageLibrary.objects.get(pk=value)
- preview_html = format_html(
- '
'
- '

'
- '
{} - {}x{} - {}
'
- "
",
- image.image.url,
- image.name,
- image.width or "?",
- image.height or "?",
- image.get_file_size_display(),
- )
- except ImageLibrary.DoesNotExist:
- pass
-
- # Add JavaScript for preview updates
- js_html = format_html(
- "",
- attrs.get("id", ""),
- )
-
- return mark_safe(select_html + preview_html + js_html)
-
- def _build_attrs_string(self, attrs):
- """
- Build HTML attributes string.
- """
- attr_parts = []
- for key, value in attrs.items():
- if key != "id": # id is handled separately
- attr_parts.append(f'{key}="{value}"')
- return " " + " ".join(attr_parts) if attr_parts else ""
-
- def _build_options(self, choices, selected_value):
- """
- Build option elements for the select.
- """
- options = []
- for value, text in choices:
- selected = "selected" if str(value) == str(selected_value) else ""
- options.append(f'')
- return "".join(options)
-
-
-class ImageLibraryField(forms.ModelChoiceField):
- """
- Custom form field for selecting images from the library.
- """
-
- def __init__(self, queryset=None, widget=None, show_thumbnails=True, **kwargs):
- if queryset is None:
- queryset = ImageLibrary.objects.all()
-
- if widget is None:
- widget = ImageLibraryWidget(show_thumbnails=show_thumbnails)
-
- super().__init__(queryset=queryset, widget=widget, **kwargs)
-
- def label_from_instance(self, obj):
- """
- Return the label for an image instance.
- """
- return f"{obj.name} ({obj.get_category_display()})"
diff --git a/hub/services/management/__init__.py b/hub/services/management/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/hub/services/management/commands/__init__.py b/hub/services/management/commands/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/hub/services/management/commands/migrate_images.py b/hub/services/management/commands/migrate_images.py
deleted file mode 100644
index ca90dcb..0000000
--- a/hub/services/management/commands/migrate_images.py
+++ /dev/null
@@ -1,293 +0,0 @@
-from django.core.management.base import BaseCommand
-from django.core.files.base import ContentFile
-from django.utils.text import slugify
-from hub.services.models import (
- ImageLibrary,
- Service,
- CloudProvider,
- ConsultingPartner,
- Article,
-)
-import os
-import shutil
-
-
-class Command(BaseCommand):
- help = "Migrate existing images to the Image Library"
-
- def add_arguments(self, parser):
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="Show what would be migrated without actually doing it",
- )
- parser.add_argument(
- "--force",
- action="store_true",
- help="Force migration even if images already exist in library",
- )
-
- def handle(self, *args, **options):
- """
- Main command handler to migrate existing images to the library.
- """
- dry_run = options["dry_run"]
- force = options["force"]
-
- self.stdout.write(
- self.style.SUCCESS(
- f'Starting image migration {"(DRY RUN)" if dry_run else ""}'
- )
- )
-
- # Migrate different types of images
- self.migrate_service_logos(dry_run, force)
- self.migrate_cloud_provider_logos(dry_run, force)
- self.migrate_partner_logos(dry_run, force)
- self.migrate_article_images(dry_run, force)
-
- self.stdout.write(
- self.style.SUCCESS(
- f'Image migration completed {"(DRY RUN)" if dry_run else ""}'
- )
- )
-
- def migrate_service_logos(self, dry_run, force):
- """
- Migrate service logos to the image library.
- """
- self.stdout.write("Migrating service logos...")
-
- services = Service.objects.filter(logo__isnull=False).exclude(logo="")
-
- for service in services:
- if not service.logo:
- continue
-
- # Check if image already exists in library
- existing_image = ImageLibrary.objects.filter(
- name=f"{service.name} Logo"
- ).first()
-
- if existing_image and not force:
- self.stdout.write(
- self.style.WARNING(
- f" - Skipping {service.name} logo (already exists)"
- )
- )
- continue
-
- if dry_run:
- self.stdout.write(
- self.style.SUCCESS(f" - Would migrate: {service.name} logo")
- )
- continue
-
- # Create image library entry
- image_lib = ImageLibrary(
- name=f"{service.name} Logo",
- slug=slugify(f"{service.name}-logo"),
- description=f"Logo for {service.name} service",
- alt_text=f"{service.name} logo",
- category="logo",
- tags=f"service, logo, {service.name.lower()}",
- )
-
- # Copy the image file
- if service.logo and os.path.exists(service.logo.path):
- with open(service.logo.path, "rb") as f:
- image_lib.image.save(
- os.path.basename(service.logo.name),
- ContentFile(f.read()),
- save=True,
- )
-
- self.stdout.write(
- self.style.SUCCESS(f" - Migrated: {service.name} logo")
- )
- else:
- self.stdout.write(
- self.style.ERROR(
- f" - Failed to migrate: {service.name} logo (file not found)"
- )
- )
-
- def migrate_cloud_provider_logos(self, dry_run, force):
- """
- Migrate cloud provider logos to the image library.
- """
- self.stdout.write("Migrating cloud provider logos...")
-
- providers = CloudProvider.objects.filter(logo__isnull=False).exclude(logo="")
-
- for provider in providers:
- if not provider.logo:
- continue
-
- # Check if image already exists in library
- existing_image = ImageLibrary.objects.filter(
- name=f"{provider.name} Logo"
- ).first()
-
- if existing_image and not force:
- self.stdout.write(
- self.style.WARNING(
- f" - Skipping {provider.name} logo (already exists)"
- )
- )
- continue
-
- if dry_run:
- self.stdout.write(
- self.style.SUCCESS(f" - Would migrate: {provider.name} logo")
- )
- continue
-
- # Create image library entry
- image_lib = ImageLibrary(
- name=f"{provider.name} Logo",
- slug=slugify(f"{provider.name}-logo"),
- description=f"Logo for {provider.name} cloud provider",
- alt_text=f"{provider.name} logo",
- category="logo",
- tags=f"cloud, provider, logo, {provider.name.lower()}",
- )
-
- # Copy the image file
- if provider.logo and os.path.exists(provider.logo.path):
- with open(provider.logo.path, "rb") as f:
- image_lib.image.save(
- os.path.basename(provider.logo.name),
- ContentFile(f.read()),
- save=True,
- )
-
- self.stdout.write(
- self.style.SUCCESS(f" - Migrated: {provider.name} logo")
- )
- else:
- self.stdout.write(
- self.style.ERROR(
- f" - Failed to migrate: {provider.name} logo (file not found)"
- )
- )
-
- def migrate_partner_logos(self, dry_run, force):
- """
- Migrate consulting partner logos to the image library.
- """
- self.stdout.write("Migrating consulting partner logos...")
-
- partners = ConsultingPartner.objects.filter(logo__isnull=False).exclude(logo="")
-
- for partner in partners:
- if not partner.logo:
- continue
-
- # Check if image already exists in library
- existing_image = ImageLibrary.objects.filter(
- name=f"{partner.name} Logo"
- ).first()
-
- if existing_image and not force:
- self.stdout.write(
- self.style.WARNING(
- f" - Skipping {partner.name} logo (already exists)"
- )
- )
- continue
-
- if dry_run:
- self.stdout.write(
- self.style.SUCCESS(f" - Would migrate: {partner.name} logo")
- )
- continue
-
- # Create image library entry
- image_lib = ImageLibrary(
- name=f"{partner.name} Logo",
- slug=slugify(f"{partner.name}-logo"),
- description=f"Logo for {partner.name} consulting partner",
- alt_text=f"{partner.name} logo",
- category="logo",
- tags=f"consulting, partner, logo, {partner.name.lower()}",
- )
-
- # Copy the image file
- if partner.logo and os.path.exists(partner.logo.path):
- with open(partner.logo.path, "rb") as f:
- image_lib.image.save(
- os.path.basename(partner.logo.name),
- ContentFile(f.read()),
- save=True,
- )
-
- self.stdout.write(
- self.style.SUCCESS(f" - Migrated: {partner.name} logo")
- )
- else:
- self.stdout.write(
- self.style.ERROR(
- f" - Failed to migrate: {partner.name} logo (file not found)"
- )
- )
-
- def migrate_article_images(self, dry_run, force):
- """
- Migrate article images to the image library.
- """
- self.stdout.write("Migrating article images...")
-
- articles = Article.objects.filter(image__isnull=False).exclude(image="")
-
- for article in articles:
- if not article.image:
- continue
-
- # Check if image already exists in library
- existing_image = ImageLibrary.objects.filter(
- name=f"{article.title} Image"
- ).first()
-
- if existing_image and not force:
- self.stdout.write(
- self.style.WARNING(
- f" - Skipping {article.title} image (already exists)"
- )
- )
- continue
-
- if dry_run:
- self.stdout.write(
- self.style.SUCCESS(f" - Would migrate: {article.title} image")
- )
- continue
-
- # Create image library entry
- image_lib = ImageLibrary(
- name=f"{article.title} Image",
- slug=slugify(f"{article.title}-image"),
- description=f"Feature image for article: {article.title}",
- alt_text=f"{article.title} feature image",
- category="article",
- tags=f"article, {article.title.lower()}",
- )
-
- # Copy the image file
- if article.image and os.path.exists(article.image.path):
- with open(article.image.path, "rb") as f:
- image_lib.image.save(
- os.path.basename(article.image.name),
- ContentFile(f.read()),
- save=True,
- )
-
- self.stdout.write(
- self.style.SUCCESS(f" - Migrated: {article.title} image")
- )
- else:
- self.stdout.write(
- self.style.ERROR(
- f" - Failed to migrate: {article.title} image (file not found)"
- )
- )
diff --git a/hub/services/migrations/0039_article_article_date.py b/hub/services/migrations/0039_article_article_date.py
deleted file mode 100644
index 69acc86..0000000
--- a/hub/services/migrations/0039_article_article_date.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Generated by Django 5.2 on 2025-07-04 13:48
-
-import django.utils.timezone
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("services", "0038_add_plan_ordering_and_best"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="article",
- name="article_date",
- field=models.DateField(
- default=django.utils.timezone.now,
- help_text="Date of the article publishing",
- ),
- ),
- ]
diff --git a/hub/services/migrations/0040_add_image_library.py b/hub/services/migrations/0040_add_image_library.py
deleted file mode 100644
index bc06e33..0000000
--- a/hub/services/migrations/0040_add_image_library.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# Generated by Django 5.2 on 2025-07-04 14:19
-
-import django.db.models.deletion
-import hub.services.models.base
-import hub.services.models.images
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("services", "0039_article_article_date"),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name="ImageLibrary",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "name",
- models.CharField(
- help_text="Descriptive name for the image", max_length=200
- ),
- ),
- (
- "slug",
- models.SlugField(
- help_text="URL-friendly version of the name",
- max_length=250,
- unique=True,
- ),
- ),
- (
- "description",
- models.TextField(
- blank=True, help_text="Optional description of the image"
- ),
- ),
- (
- "alt_text",
- models.CharField(
- help_text="Alternative text for accessibility", max_length=255
- ),
- ),
- (
- "image",
- models.ImageField(
- help_text="Upload image file (max 1MB)",
- upload_to=hub.services.models.images.get_image_upload_path,
- validators=[hub.services.models.base.validate_image_size],
- ),
- ),
- (
- "width",
- models.PositiveIntegerField(
- blank=True, help_text="Image width in pixels", null=True
- ),
- ),
- (
- "height",
- models.PositiveIntegerField(
- blank=True, help_text="Image height in pixels", null=True
- ),
- ),
- (
- "file_size",
- models.PositiveIntegerField(
- blank=True, help_text="File size in bytes", null=True
- ),
- ),
- (
- "category",
- models.CharField(
- choices=[
- ("logo", "Logo"),
- ("article", "Article Image"),
- ("banner", "Banner"),
- ("icon", "Icon"),
- ("screenshot", "Screenshot"),
- ("photo", "Photo"),
- ("other", "Other"),
- ],
- default="other",
- help_text="Category of the image",
- max_length=20,
- ),
- ),
- (
- "tags",
- models.CharField(
- blank=True,
- help_text="Comma-separated tags for searching",
- max_length=500,
- ),
- ),
- (
- "uploaded_at",
- models.DateTimeField(
- auto_now_add=True,
- help_text="Date and time when image was uploaded",
- ),
- ),
- (
- "updated_at",
- models.DateTimeField(
- auto_now=True,
- help_text="Date and time when image was last updated",
- ),
- ),
- (
- "usage_count",
- models.PositiveIntegerField(
- default=0, help_text="Number of times this image is referenced"
- ),
- ),
- (
- "uploaded_by",
- models.ForeignKey(
- blank=True,
- help_text="User who uploaded the image",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- options={
- "verbose_name": "Image",
- "verbose_name_plural": "Image Library",
- "ordering": ["-uploaded_at"],
- },
- ),
- ]
diff --git a/hub/services/migrations/0041_add_image_library_references.py b/hub/services/migrations/0041_add_image_library_references.py
deleted file mode 100644
index 231520b..0000000
--- a/hub/services/migrations/0041_add_image_library_references.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# 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
deleted file mode 100644
index 0996f8b..0000000
--- a/hub/services/migrations/0042_fix_image_library_field_name.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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/__init__.py b/hub/services/models/__init__.py
index 68159a4..b29a71e 100644
--- a/hub/services/models/__init__.py
+++ b/hub/services/models/__init__.py
@@ -1,7 +1,6 @@
from .articles import *
from .base import *
from .content import *
-from .images import *
from .leads import *
from .pricing import *
from .providers import *
diff --git a/hub/services/models/articles.py b/hub/services/models/articles.py
index 8ea50c0..781c54c 100644
--- a/hub/services/models/articles.py
+++ b/hub/services/models/articles.py
@@ -3,14 +3,12 @@ from django.urls import reverse
from django.utils.text import slugify
from django.contrib.auth.models import User
from django_prose_editor.fields import ProseEditorField
-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(ImageReference):
+class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=250, unique=True)
excerpt = models.TextField(
@@ -20,17 +18,11 @@ class Article(ImageReference):
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(
- default=timezone.now, help_text="Date of the article publishing"
- )
# Relations to other models
related_service = models.ForeignKey(
@@ -90,13 +82,6 @@ class Article(ImageReference):
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/base.py b/hub/services/models/base.py
index a047330..9a7abce 100644
--- a/hub/services/models/base.py
+++ b/hub/services/models/base.py
@@ -4,10 +4,10 @@ from django.utils.text import slugify
from django_prose_editor.fields import ProseEditorField
-def validate_image_size(value, mb=1):
+def validate_image_size(value):
filesize = value.size
- if filesize > mb * 1024 * 1024:
- raise ValidationError(f"Maximum file size is {mb} MB")
+ if filesize > 1 * 1024 * 1024: # 1MB
+ raise ValidationError("Maximum file size is 1MB")
class Currency(models.TextChoices):
diff --git a/hub/services/models/images.py b/hub/services/models/images.py
deleted file mode 100644
index 3bf72f7..0000000
--- a/hub/services/models/images.py
+++ /dev/null
@@ -1,225 +0,0 @@
-import os
-
-from django.db import models
-from django.utils import timezone
-from django.contrib.auth.models import User
-from django.core.exceptions import ValidationError
-from django.utils.text import slugify
-from PIL import Image as PILImage
-from .base import validate_image_size
-
-
-def get_image_upload_path(instance, filename):
- """
- Generate upload path for images based on the image library structure.
- """
- return f"image_library/{filename}"
-
-
-class ImageLibrary(models.Model):
- """
- Generic image library model that can be referenced by other models
- to avoid duplicate uploads and provide centralized image management.
- """
-
- # Image metadata
- name = models.CharField(max_length=200, help_text="Descriptive name for the image")
- slug = models.SlugField(
- max_length=250, unique=True, help_text="URL-friendly version of the name"
- )
- description = models.TextField(
- blank=True, help_text="Optional description of the image"
- )
- alt_text = models.CharField(
- max_length=255, help_text="Alternative text for accessibility"
- )
-
- # Image file
- image = models.ImageField(
- upload_to=get_image_upload_path,
- validators=[validate_image_size],
- help_text="Upload image file (max 1MB)",
- )
-
- # Image properties (automatically populated)
- width = models.PositiveIntegerField(
- null=True, blank=True, help_text="Image width in pixels"
- )
- height = models.PositiveIntegerField(
- null=True, blank=True, help_text="Image height in pixels"
- )
- file_size = models.PositiveIntegerField(
- null=True, blank=True, help_text="File size in bytes"
- )
-
- # Categorization
- CATEGORY_CHOICES = [
- ("logo", "Logo"),
- ("article", "Article Image"),
- ("banner", "Banner"),
- ("icon", "Icon"),
- ("screenshot", "Screenshot"),
- ("photo", "Photo"),
- ("other", "Other"),
- ]
- category = models.CharField(
- max_length=20,
- choices=CATEGORY_CHOICES,
- default="other",
- help_text="Category of the image",
- )
-
- # Tags for easier searching
- tags = models.CharField(
- max_length=500, blank=True, help_text="Comma-separated tags for searching"
- )
-
- # Metadata
- uploaded_by = models.ForeignKey(
- User,
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- help_text="User who uploaded the image",
- )
- uploaded_at = models.DateTimeField(
- auto_now_add=True, help_text="Date and time when image was uploaded"
- )
- updated_at = models.DateTimeField(
- auto_now=True, help_text="Date and time when image was last updated"
- )
-
- # Usage tracking
- usage_count = models.PositiveIntegerField(
- default=0, help_text="Number of times this image is referenced"
- )
-
- class Meta:
- ordering = ["-uploaded_at"]
- verbose_name = "Image"
- verbose_name_plural = "Image Library"
-
- def __str__(self):
- return self.name
-
- def save(self, *args, **kwargs):
- """
- Override save to automatically populate image properties and slug.
- """
- # Generate slug if not provided
- if not self.slug:
- self.slug = slugify(self.name)
-
- # Save the model first to get the image file
- super().save(*args, **kwargs)
-
- # Update image properties if image exists
- if self.image:
- self._update_image_properties()
-
- def _update_image_properties(self):
- """
- Update image properties like width, height, and file size.
- """
- try:
- # Get image dimensions
- with PILImage.open(self.image.path) as img:
- self.width = img.width
- self.height = img.height
-
- # Get file size
- self.file_size = self.image.size
-
- # Save without calling the full save method to avoid recursion
- ImageLibrary.objects.filter(pk=self.pk).update(
- width=self.width, height=self.height, file_size=self.file_size
- )
- except Exception as e:
- # Log error but don't fail the save
- print(f"Error updating image properties: {e}")
-
- def get_file_size_display(self):
- """
- Return human-readable file size.
- """
- if not self.file_size:
- return "Unknown"
-
- size = self.file_size
- for unit in ["B", "KB", "MB", "GB"]:
- if size < 1024.0:
- return f"{size:.1f} {unit}"
- size /= 1024.0
- return f"{size:.1f} TB"
-
- def get_tags_list(self):
- """
- Return tags as a list.
- """
- if not self.tags:
- return []
- return [tag.strip() for tag in self.tags.split(",") if tag.strip()]
-
- def increment_usage(self):
- """
- Increment usage count when image is referenced.
- """
- self.usage_count += 1
- self.save(update_fields=["usage_count"])
-
- def decrement_usage(self):
- """
- Decrement usage count when reference is removed.
- """
- if self.usage_count > 0:
- self.usage_count -= 1
- self.save(update_fields=["usage_count"])
-
-
-class ImageReference(models.Model):
- """
- Abstract base class for models that want to reference images from the library.
- This helps track usage and provides a consistent interface.
- """
-
- 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:
- abstract = True
-
- def save(self, *args, **kwargs):
- """
- Override save to update usage count.
- """
- # Track if image changed
- old_image = None
- if self.pk:
- try:
- old_instance = self.__class__.objects.get(pk=self.pk)
- 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_library:
- old_image.decrement_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_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 5567cb9..e3257ea 100644
--- a/hub/services/models/providers.py
+++ b/hub/services/models/providers.py
@@ -4,10 +4,9 @@ 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(ImageReference):
+class CloudProvider(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = ProseEditorField()
@@ -16,7 +15,6 @@ class CloudProvider(ImageReference):
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],
@@ -41,19 +39,11 @@ class CloudProvider(ImageReference):
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(ImageReference):
+class ConsultingPartner(models.Model):
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],
@@ -93,10 +83,3 @@ class ConsultingPartner(ImageReference):
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 af4c2e0..8b6984d 100644
--- a/hub/services/models/services.py
+++ b/hub/services/models/services.py
@@ -13,15 +13,13 @@ from .base import (
Currency,
)
from .providers import CloudProvider
-from .images import ImageReference
-class Service(ImageReference):
+class Service(models.Model):
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],
@@ -60,13 +58,6 @@ class Service(ImageReference):
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/static/admin/css/image_library.css b/hub/services/static/admin/css/image_library.css
deleted file mode 100644
index 6d589bb..0000000
--- a/hub/services/static/admin/css/image_library.css
+++ /dev/null
@@ -1,79 +0,0 @@
-/* CSS for Image Library Admin */
-
-/* Thumbnail styling in list view */
-.image-thumbnail {
- border-radius: 4px;
- object-fit: cover;
-}
-
-/* Preview styling in detail view */
-.image-preview {
- border-radius: 4px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-/* Form styling */
-.image-library-form .form-row {
- margin-bottom: 15px;
-}
-
-.image-library-form .help {
- font-size: 11px;
- color: #666;
- margin-top: 5px;
-}
-
-/* Usage count styling */
-.usage-count {
- font-weight: bold;
- color: #0066cc;
-}
-
-.usage-count.high {
- color: #cc0000;
-}
-
-/* Category badges */
-.category-badge {
- display: inline-block;
- padding: 2px 6px;
- border-radius: 3px;
- font-size: 10px;
- font-weight: bold;
- text-transform: uppercase;
-}
-
-.category-badge.logo {
- background-color: #e8f4f8;
- color: #2c6e92;
-}
-
-.category-badge.article {
- background-color: #f0f8e8;
- color: #5a7c3a;
-}
-
-.category-badge.banner {
- background-color: #fef4e8;
- color: #d2691e;
-}
-
-.category-badge.icon {
- background-color: #f8e8f8;
- color: #8b4c8b;
-}
-
-.category-badge.screenshot {
- background-color: #e8f8f4;
- color: #3a7c5a;
-}
-
-.category-badge.photo {
- background-color: #f4e8f8;
- color: #923c92;
-}
-
-.category-badge.other {
- background-color: #f0f0f0;
- color: #666;
-}
\ No newline at end of file
diff --git a/hub/services/static/css/price-calculator.css b/hub/services/static/css/price-calculator.css
index a65d8cc..5efe5f6 100644
--- a/hub/services/static/css/price-calculator.css
+++ b/hub/services/static/css/price-calculator.css
@@ -49,9 +49,9 @@
/* Best choice badge styling */
.badge.bg-success {
+ background: linear-gradient(135deg, #198754 0%, #20c997 100%) !important;
border: 2px solid white;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.151);
- color: rgb(255, 255, 255);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
white-space: nowrap;
font-size: 0.75rem;
padding: 0.5rem 0.75rem;
diff --git a/hub/services/templates/pages/homepage.html b/hub/services/templates/pages/homepage.html
index a7507c6..f16da44 100644
--- a/hub/services/templates/pages/homepage.html
+++ b/hub/services/templates/pages/homepage.html
@@ -48,15 +48,9 @@
@@ -111,7 +105,7 @@
-
@@ -165,7 +159,7 @@
-
diff --git a/hub/services/templates/services/article_detail.html b/hub/services/templates/services/article_detail.html
index 2f67b43..44af754 100644
--- a/hub/services/templates/services/article_detail.html
+++ b/hub/services/templates/services/article_detail.html
@@ -16,13 +16,25 @@
By {{ article.author.get_full_name|default:article.author.username }}
•
- {{ article.article_date|date:"M d, Y" }}
+ {{ article.created_at|date:"M d, Y" }}
+ {% if article.updated_at != article.created_at %}
+ {% endif %}
+{% if article.image %}
+
+
+
+

+
+
+
+{% endif %}
+
@@ -43,7 +55,7 @@
Service
{% if article.related_service.logo %}
-
{% endif %}
@@ -60,7 +72,7 @@
Partner
{% if article.related_consulting_partner.logo %}
-
{% endif %}
@@ -77,7 +89,7 @@
Provider
{% if article.related_cloud_provider.logo %}
-
{% endif %}
@@ -100,7 +112,7 @@
{% if related_article.image %}
-

+

{% endif %}
{{ related_article.title }}
diff --git a/hub/services/templates/services/article_list.html b/hub/services/templates/services/article_list.html
index 47ae5ae..edb0989 100644
--- a/hub/services/templates/services/article_list.html
+++ b/hub/services/templates/services/article_list.html
@@ -149,7 +149,7 @@
{% if article.image %}
-

+
{% endif %}
{% if article.is_featured %}
@@ -169,7 +169,7 @@
By {{ article.author.get_full_name|default:article.author.username }}
- {{ article.article_date|date:"M d, Y" }}
+ {{ article.created_at|date:"M d, Y" }}
diff --git a/hub/services/templates/services/lead_form.html b/hub/services/templates/services/lead_form.html
index bcc26d6..73e0585 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 %}
-

+

{% endif %}