fix: show product in replacement of sub products in homepage
This commit is contained in:
@@ -21,6 +21,8 @@ MAIN_PRODUCTS = [
|
|||||||
"(IBSI 1.0), and implements image filters standardized against IBSI 2.0."
|
"(IBSI 1.0), and implements image filters standardized against IBSI 2.0."
|
||||||
),
|
),
|
||||||
"order": 1,
|
"order": 1,
|
||||||
|
"show_on_homepage": True,
|
||||||
|
"homepage_order": 1,
|
||||||
"sub_products": [
|
"sub_products": [
|
||||||
{
|
{
|
||||||
"name": "Image Processing",
|
"name": "Image Processing",
|
||||||
@@ -34,8 +36,6 @@ MAIN_PRODUCTS = [
|
|||||||
"for consistent, reproducible research outcomes."
|
"for consistent, reproducible research outcomes."
|
||||||
),
|
),
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"show_on_homepage": True,
|
|
||||||
"homepage_order": 1,
|
|
||||||
"articles": [
|
"articles": [
|
||||||
{
|
{
|
||||||
"title": "Image Filtering Techniques",
|
"title": "Image Filtering Techniques",
|
||||||
@@ -408,10 +408,6 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(self.style.SUCCESS("Content seeded successfully."))
|
self.stdout.write(self.style.SUCCESS("Content seeded successfully."))
|
||||||
|
|
||||||
def _seed_products(self):
|
def _seed_products(self):
|
||||||
SubProduct.objects.filter(show_on_homepage=False, homepage_order=0).update(
|
|
||||||
show_on_homepage=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for product_data in MAIN_PRODUCTS:
|
for product_data in MAIN_PRODUCTS:
|
||||||
sub_products_data = product_data.pop("sub_products")
|
sub_products_data = product_data.pop("sub_products")
|
||||||
main_product, created = MainProduct.objects.get_or_create(
|
main_product, created = MainProduct.objects.get_or_create(
|
||||||
|
|||||||
+64
-15
@@ -46,13 +46,13 @@ class HeroSectionAdmin(admin.ModelAdmin):
|
|||||||
class HomepageSectionItemInline(admin.TabularInline):
|
class HomepageSectionItemInline(admin.TabularInline):
|
||||||
model = HomepageSectionItem
|
model = HomepageSectionItem
|
||||||
extra = 1
|
extra = 1
|
||||||
fields = ("icon", "title", "content", "image", "order")
|
fields = ("icon", "title", "content", "url", "image", "order")
|
||||||
ordering = ("order",)
|
ordering = ("order",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(HomepageSection)
|
@admin.register(HomepageSection)
|
||||||
class HomepageSectionAdmin(admin.ModelAdmin):
|
class HomepageSectionAdmin(admin.ModelAdmin):
|
||||||
list_display = ("section_type", "badge", "title", "order", "is_active")
|
list_display = ("section_type", "title", "badge", "order", "is_active")
|
||||||
list_filter = ("section_type", "is_active")
|
list_filter = ("section_type", "is_active")
|
||||||
list_editable = ("order", "is_active")
|
list_editable = ("order", "is_active")
|
||||||
inlines = [HomepageSectionItemInline]
|
inlines = [HomepageSectionItemInline]
|
||||||
@@ -147,22 +147,33 @@ class CustomPageSectionInline(admin.StackedInline):
|
|||||||
show_change_link = True
|
show_change_link = True
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomPage)
|
class CustomPageSectionItemOnPageInline(admin.TabularInline):
|
||||||
class CustomPageAdmin(admin.ModelAdmin):
|
model = CustomPageSectionItem
|
||||||
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at")
|
fk_name = "page"
|
||||||
list_filter = ("show_in_nav", "is_published")
|
extra = 1
|
||||||
search_fields = ("title", "slug", "menu_label")
|
verbose_name = "Section item"
|
||||||
list_editable = ("show_in_nav", "menu_order", "is_published")
|
verbose_name_plural = "Section items (subsections)"
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
fields = (
|
||||||
inlines = [CustomPageSectionInline]
|
"section",
|
||||||
fieldsets = (
|
"icon",
|
||||||
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}),
|
"badge",
|
||||||
("Navigation", {"fields": ("show_in_nav", "menu_order")}),
|
"title",
|
||||||
("Publishing", {"fields": ("is_published",)}),
|
"content_format",
|
||||||
|
"content",
|
||||||
|
"url",
|
||||||
|
"image",
|
||||||
|
"order",
|
||||||
)
|
)
|
||||||
|
ordering = ("section", "order")
|
||||||
|
|
||||||
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
if db_field.name == "section":
|
||||||
|
page_id = request.resolver_match.kwargs.get("object_id") if request.resolver_match else None
|
||||||
|
if page_id:
|
||||||
|
kwargs["queryset"] = CustomPageSection.objects.filter(page_id=page_id).order_by("order")
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomPageSection)
|
|
||||||
class CustomPageSectionAdmin(admin.ModelAdmin):
|
class CustomPageSectionAdmin(admin.ModelAdmin):
|
||||||
list_display = ("page", "section_type", "title", "order", "is_active")
|
list_display = ("page", "section_type", "title", "order", "is_active")
|
||||||
list_filter = ("section_type", "is_active", "page")
|
list_filter = ("section_type", "is_active", "page")
|
||||||
@@ -175,6 +186,44 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
|
|||||||
("Settings", {"fields": ("order", "is_active")}),
|
("Settings", {"fields": ("order", "is_active")}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save_formset(self, request, form, formset, change):
|
||||||
|
instances = formset.save(commit=False)
|
||||||
|
for instance in instances:
|
||||||
|
if isinstance(instance, CustomPageSectionItem):
|
||||||
|
instance.page = form.instance.page
|
||||||
|
instance.save()
|
||||||
|
for obj in formset.deleted_objects:
|
||||||
|
obj.delete()
|
||||||
|
formset.save_m2m()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomPage)
|
||||||
|
class CustomPageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at")
|
||||||
|
list_filter = ("show_in_nav", "is_published")
|
||||||
|
search_fields = ("title", "slug", "menu_label")
|
||||||
|
list_editable = ("show_in_nav", "menu_order", "is_published")
|
||||||
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
inlines = [CustomPageSectionInline, CustomPageSectionItemOnPageInline]
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}),
|
||||||
|
("Navigation", {"fields": ("show_in_nav", "menu_order")}),
|
||||||
|
("Publishing", {"fields": ("is_published",)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_formset(self, request, form, formset, change):
|
||||||
|
instances = formset.save(commit=False)
|
||||||
|
for instance in instances:
|
||||||
|
if isinstance(instance, CustomPageSectionItem):
|
||||||
|
instance.page = form.instance
|
||||||
|
instance.save()
|
||||||
|
for obj in formset.deleted_objects:
|
||||||
|
obj.delete()
|
||||||
|
formset.save_m2m()
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(DownloadItem)
|
@admin.register(DownloadItem)
|
||||||
class DownloadItemAdmin(admin.ModelAdmin):
|
class DownloadItemAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2026-05-26 12:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pages', '0008_custom_pages'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='homepagesection',
|
||||||
|
name='section_type',
|
||||||
|
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2026-05-26 12:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pages', '0009_alter_homepagesection_section_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='homepagesectionitem',
|
||||||
|
name='url',
|
||||||
|
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custompagesection',
|
||||||
|
name='section_type',
|
||||||
|
field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('features', 'Features Grid'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products Grid'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('supporters', 'Supporters'), ('about_strip', 'About Strip'), ('faq', 'FAQ Accordion')], default='custom', max_length=30),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custompagesectionitem',
|
||||||
|
name='url',
|
||||||
|
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='homepagesection',
|
||||||
|
name='section_type',
|
||||||
|
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def set_custom_page_section_item_page(apps, schema_editor):
|
||||||
|
CustomPageSectionItem = apps.get_model("pages", "CustomPageSectionItem")
|
||||||
|
for item in CustomPageSectionItem.objects.select_related("section").iterator():
|
||||||
|
item.page_id = item.section.page_id
|
||||||
|
item.save(update_fields=["page_id"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pages", "0010_homepagesectionitem_url_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="aboutsectionitem",
|
||||||
|
name="url",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Optional link; makes this item clickable.",
|
||||||
|
max_length=300,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="custompagesectionitem",
|
||||||
|
name="page",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="section_items",
|
||||||
|
to="pages.custompage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_custom_page_section_item_page, migrations.RunPython.noop),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="custompagesectionitem",
|
||||||
|
name="page",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="section_items",
|
||||||
|
to="pages.custompage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+18
-3
@@ -41,6 +41,7 @@ class HomepageSection(models.Model):
|
|||||||
TYPE_FEATURES = "features"
|
TYPE_FEATURES = "features"
|
||||||
TYPE_SCREENSHOTS = "screenshots"
|
TYPE_SCREENSHOTS = "screenshots"
|
||||||
TYPE_PRODUCTS = "products"
|
TYPE_PRODUCTS = "products"
|
||||||
|
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||||||
TYPE_PROBLEMS = "problems"
|
TYPE_PROBLEMS = "problems"
|
||||||
TYPE_ABOUT_STRIP = "about_strip"
|
TYPE_ABOUT_STRIP = "about_strip"
|
||||||
TYPE_SUPPORTERS = "supporters"
|
TYPE_SUPPORTERS = "supporters"
|
||||||
@@ -49,12 +50,13 @@ class HomepageSection(models.Model):
|
|||||||
(TYPE_FEATURES, "Features"),
|
(TYPE_FEATURES, "Features"),
|
||||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||||
(TYPE_PRODUCTS, "Products"),
|
(TYPE_PRODUCTS, "Products"),
|
||||||
|
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||||
(TYPE_SUPPORTERS, "Supporters"),
|
(TYPE_SUPPORTERS, "Supporters"),
|
||||||
]
|
]
|
||||||
|
|
||||||
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, unique=True)
|
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES)
|
||||||
badge = models.CharField(max_length=100, blank=True)
|
badge = models.CharField(max_length=100, blank=True)
|
||||||
title = models.CharField(max_length=300, blank=True)
|
title = models.CharField(max_length=300, blank=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
@@ -77,6 +79,7 @@ class HomepageSectionItem(models.Model):
|
|||||||
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
|
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
|
||||||
title = models.CharField(max_length=300, blank=True)
|
title = models.CharField(max_length=300, blank=True)
|
||||||
content = models.TextField(blank=True)
|
content = models.TextField(blank=True)
|
||||||
|
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||||
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
|
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ class AboutSectionItem(models.Model):
|
|||||||
badge = models.CharField(max_length=100, blank=True)
|
badge = models.CharField(max_length=100, blank=True)
|
||||||
title = models.CharField(max_length=300, blank=True)
|
title = models.CharField(max_length=300, blank=True)
|
||||||
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
|
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
|
||||||
url = models.URLField(blank=True, help_text="Used for History block links.")
|
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||||
image = models.ImageField(upload_to="about/", blank=True, null=True)
|
image = models.ImageField(upload_to="about/", blank=True, null=True)
|
||||||
image_alt = models.CharField(max_length=200, blank=True)
|
image_alt = models.CharField(max_length=200, blank=True)
|
||||||
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
|
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
|
||||||
@@ -267,6 +270,7 @@ class CustomPageSection(models.Model):
|
|||||||
TYPE_FEATURES = "features"
|
TYPE_FEATURES = "features"
|
||||||
TYPE_SCREENSHOTS = "screenshots"
|
TYPE_SCREENSHOTS = "screenshots"
|
||||||
TYPE_PRODUCTS = "products"
|
TYPE_PRODUCTS = "products"
|
||||||
|
TYPE_PRODUCTS_CATALOG = "products_catalog"
|
||||||
TYPE_PROBLEMS = "problems"
|
TYPE_PROBLEMS = "problems"
|
||||||
TYPE_SUPPORTERS = "supporters"
|
TYPE_SUPPORTERS = "supporters"
|
||||||
TYPE_ABOUT_STRIP = "about_strip"
|
TYPE_ABOUT_STRIP = "about_strip"
|
||||||
@@ -281,6 +285,7 @@ class CustomPageSection(models.Model):
|
|||||||
(TYPE_FEATURES, "Features Grid"),
|
(TYPE_FEATURES, "Features Grid"),
|
||||||
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
|
||||||
(TYPE_PRODUCTS, "Products Grid"),
|
(TYPE_PRODUCTS, "Products Grid"),
|
||||||
|
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
|
||||||
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
(TYPE_PROBLEMS, "Problems / Value Proposition"),
|
||||||
(TYPE_SUPPORTERS, "Supporters"),
|
(TYPE_SUPPORTERS, "Supporters"),
|
||||||
(TYPE_ABOUT_STRIP, "About Strip"),
|
(TYPE_ABOUT_STRIP, "About Strip"),
|
||||||
@@ -315,13 +320,18 @@ class CustomPageSection(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class CustomPageSectionItem(models.Model):
|
class CustomPageSectionItem(models.Model):
|
||||||
|
page = models.ForeignKey(
|
||||||
|
CustomPage,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="section_items",
|
||||||
|
)
|
||||||
section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items")
|
section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items")
|
||||||
icon = models.CharField(max_length=20, blank=True)
|
icon = models.CharField(max_length=20, blank=True)
|
||||||
badge = models.CharField(max_length=100, blank=True)
|
badge = models.CharField(max_length=100, blank=True)
|
||||||
title = models.CharField(max_length=300, blank=True)
|
title = models.CharField(max_length=300, blank=True)
|
||||||
content = models.TextField(blank=True)
|
content = models.TextField(blank=True)
|
||||||
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
|
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
|
||||||
url = models.URLField(blank=True)
|
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
|
||||||
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
|
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
|
||||||
image_alt = models.CharField(max_length=200, blank=True)
|
image_alt = models.CharField(max_length=200, blank=True)
|
||||||
is_featured = models.BooleanField(default=False)
|
is_featured = models.BooleanField(default=False)
|
||||||
@@ -336,5 +346,10 @@ class CustomPageSectionItem(models.Model):
|
|||||||
def rendered_content(self):
|
def rendered_content(self):
|
||||||
return render_content(self.content, self.content_format)
|
return render_content(self.content, self.content_format)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.section_id:
|
||||||
|
self.page_id = self.section.page_id
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.section} › {self.title or self.icon or '(item)'}"
|
return f"{self.section} › {self.title or self.icon or '(item)'}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from apps.pages.models import CustomPage, CustomPageSection
|
from apps.pages.models import CustomPage, CustomPageSection, CustomPageSectionItem
|
||||||
|
|
||||||
|
|
||||||
class CustomPageViewTest(TestCase):
|
class CustomPageViewTest(TestCase):
|
||||||
@@ -40,6 +40,26 @@ class CustomPageViewTest(TestCase):
|
|||||||
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"}))
|
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"}))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_section_item_with_url_renders_as_link(self):
|
||||||
|
section = CustomPageSection.objects.create(
|
||||||
|
page=self.page,
|
||||||
|
section_type=CustomPageSection.TYPE_FEATURES,
|
||||||
|
title="Highlights",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
CustomPageSectionItem.objects.create(
|
||||||
|
page=self.page,
|
||||||
|
section=section,
|
||||||
|
title="Documentation",
|
||||||
|
content="Read the docs.",
|
||||||
|
url="/about/",
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "resources"}))
|
||||||
|
self.assertContains(response, 'href="/about/"')
|
||||||
|
self.assertContains(response, "section-item-link")
|
||||||
|
self.assertContains(response, "Documentation")
|
||||||
|
|
||||||
|
|
||||||
class CustomPageNavTest(TestCase):
|
class CustomPageNavTest(TestCase):
|
||||||
def test_nav_custom_pages_in_context(self):
|
def test_nav_custom_pages_in_context(self):
|
||||||
|
|||||||
+21
-7
@@ -1,11 +1,12 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
|
from django.db.models import Prefetch
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import DetailView, ListView, TemplateView
|
from django.views.generic import DetailView, ListView, TemplateView
|
||||||
|
|
||||||
from apps.products.models import SubProduct
|
from apps.products.models import MainProduct, SubProduct
|
||||||
|
|
||||||
from .forms import ContactForm
|
from .forms import ContactForm
|
||||||
from .models import (
|
from .models import (
|
||||||
@@ -18,6 +19,19 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _homepage_products_catalog_queryset():
|
||||||
|
return (
|
||||||
|
MainProduct.objects.filter(is_active=True)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"sub_products",
|
||||||
|
queryset=SubProduct.objects.filter(is_active=True).order_by("order", "name"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("order", "name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
template_name = "pages/home.html"
|
template_name = "pages/home.html"
|
||||||
|
|
||||||
@@ -29,11 +43,11 @@ class HomeView(TemplateView):
|
|||||||
.prefetch_related("items")
|
.prefetch_related("items")
|
||||||
.order_by("order")
|
.order_by("order")
|
||||||
)
|
)
|
||||||
ctx["sub_products"] = (
|
ctx["homepage_products"] = (
|
||||||
SubProduct.objects.filter(is_active=True, show_on_homepage=True)
|
MainProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||||
.select_related("main_product")
|
|
||||||
.order_by("homepage_order", "order")
|
.order_by("homepage_order", "order")
|
||||||
)
|
)
|
||||||
|
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -115,9 +129,9 @@ class CustomPageView(DetailView):
|
|||||||
.prefetch_related("items")
|
.prefetch_related("items")
|
||||||
.order_by("order")
|
.order_by("order")
|
||||||
)
|
)
|
||||||
ctx["sub_products"] = (
|
ctx["homepage_products"] = (
|
||||||
SubProduct.objects.filter(is_active=True, show_on_homepage=True)
|
MainProduct.objects.filter(is_active=True, show_on_homepage=True)
|
||||||
.select_related("main_product")
|
|
||||||
.order_by("homepage_order", "order")
|
.order_by("homepage_order", "order")
|
||||||
)
|
)
|
||||||
|
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -76,16 +76,17 @@ class SubProductInline(admin.StackedInline):
|
|||||||
|
|
||||||
@admin.register(MainProduct)
|
@admin.register(MainProduct)
|
||||||
class MainProductAdmin(admin.ModelAdmin):
|
class MainProductAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "order", "is_active", "created_at")
|
list_display = ("name", "show_on_homepage", "homepage_order", "order", "is_active", "created_at")
|
||||||
list_filter = ("is_active",)
|
list_filter = ("is_active", "show_on_homepage")
|
||||||
search_fields = ("name", "description")
|
search_fields = ("name", "description")
|
||||||
prepopulated_fields = {"slug": ("name",)}
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
list_editable = ("order", "is_active")
|
list_editable = ("order", "is_active", "show_on_homepage", "homepage_order")
|
||||||
inlines = [MainProductArticleInline, MainProductVersionInline, SubProductInline]
|
inlines = [MainProductArticleInline, MainProductVersionInline, SubProductInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name", "slug", "short_description", "distribution")}),
|
(None, {"fields": ("name", "slug", "short_description", "distribution")}),
|
||||||
("Description", {"fields": ("description_format", "description")}),
|
("Description", {"fields": ("description_format", "description")}),
|
||||||
("Media", {"fields": ("image",)}),
|
("Media", {"fields": ("image",)}),
|
||||||
|
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
|
||||||
("Settings", {"fields": ("order", "is_active")}),
|
("Settings", {"fields": ("order", "is_active")}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2026-05-26 12:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0010_release_version_main_product'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mainproduct',
|
||||||
|
name='homepage_order',
|
||||||
|
field=models.PositiveIntegerField(default=0, help_text='Order within the homepage Products section.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mainproduct',
|
||||||
|
name='show_on_homepage',
|
||||||
|
field=models.BooleanField(default=False, help_text='Display this product in the homepage Products section.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def copy_subproduct_homepage_flags(apps, schema_editor):
|
||||||
|
SubProduct = apps.get_model("products", "SubProduct")
|
||||||
|
for sub in (
|
||||||
|
SubProduct.objects.filter(show_on_homepage=True)
|
||||||
|
.select_related("main_product")
|
||||||
|
.order_by("homepage_order", "order")
|
||||||
|
):
|
||||||
|
main = sub.main_product
|
||||||
|
if not main.show_on_homepage:
|
||||||
|
main.show_on_homepage = True
|
||||||
|
main.homepage_order = sub.homepage_order
|
||||||
|
main.save(update_fields=["show_on_homepage", "homepage_order"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("products", "0011_mainproduct_homepage_order_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_subproduct_homepage_flags, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -38,6 +38,8 @@ class MainProduct(models.Model):
|
|||||||
)
|
)
|
||||||
order = models.PositiveIntegerField(default=0)
|
order = models.PositiveIntegerField(default=0)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
show_on_homepage = models.BooleanField(default=False, help_text="Display this product in the homepage Products section.")
|
||||||
|
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@@ -853,6 +853,44 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.section-item-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.section-item-link:hover {
|
||||||
|
border-color: rgba(79, 142, 247, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.supporter-card.section-item-link {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.screenshot-item.section-item-link {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.faq-item--link.section-item-link {
|
||||||
|
display: block;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.faq-item--link .faq-question {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.faq-item--link .faq-link-preview {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Products Grid (Home)
|
Products Grid (Home)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
+1
-152
@@ -48,157 +48,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% for section in homepage_sections %}
|
{% include "partials/_page_sections.html" with sections=homepage_sections %}
|
||||||
|
|
||||||
{% if section.section_type == "features" %}
|
|
||||||
<section class="section features-section" aria-labelledby="features-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="features-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="features-grid">
|
|
||||||
{% for item in section.items.all %}
|
|
||||||
<div class="feature-card glass-card fade-in">
|
|
||||||
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
|
||||||
<h3 class="feature-title">{{ item.title }}</h3>
|
|
||||||
<p class="feature-desc" data-readmore="88">{{ item.content }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% elif section.section_type == "screenshots" %}
|
|
||||||
<section class="section screenshots-section" aria-labelledby="screenshots-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="screenshots-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="screenshots-grid">
|
|
||||||
<div class="screenshot-item fade-in">
|
|
||||||
<img src="{% static 'images/screenshot-2.jpg' %}" alt="Radiuma full workflow interface" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="screenshot-item fade-in">
|
|
||||||
<img src="{% static 'images/screenshot-3.png' %}" alt="Radiuma panel view" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="screenshot-item fade-in">
|
|
||||||
<img src="{% static 'images/screenshot-5.png' %}" alt="Radiuma segmentation view" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="screenshot-item fade-in">
|
|
||||||
<img src="{% static 'images/screenshot-8.jpg' %}" alt="Radiuma radiomics results" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% elif section.section_type == "products" %}
|
|
||||||
{% if sub_products %}
|
|
||||||
<section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="products-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="products-grid">
|
|
||||||
{% for product in sub_products %}
|
|
||||||
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
|
|
||||||
{% if product.image %}
|
|
||||||
<div class="product-card-image">
|
|
||||||
<img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="product-card-body">
|
|
||||||
{% if product.logo %}
|
|
||||||
<img src="{{ product.logo.url }}" alt="" class="product-card-logo" aria-hidden="true" loading="lazy" />
|
|
||||||
{% endif %}
|
|
||||||
<h3 class="product-card-title">{{ product.name }}</h3>
|
|
||||||
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
|
|
||||||
<span class="product-card-link">
|
|
||||||
Explore
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
||||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% elif section.section_type == "problems" %}
|
|
||||||
<section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="problems-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="problems-grid">
|
|
||||||
{% for item in section.items.all %}
|
|
||||||
<div class="problem-item glass-card fade-in">
|
|
||||||
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
|
||||||
<h3 class="problem-title">{{ item.title }}</h3>
|
|
||||||
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% elif section.section_type == "supporters" %}
|
|
||||||
<section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="supporters-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="supporters-grid">
|
|
||||||
{% for item in section.items.all %}
|
|
||||||
<div class="supporter-card glass-card fade-in">
|
|
||||||
{% if item.image %}
|
|
||||||
<div class="supporter-logo">
|
|
||||||
<img src="{{ item.image.url }}" alt="{{ item.title }} logo" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="supporter-body">
|
|
||||||
<h3 class="supporter-name">{{ item.title }}</h3>
|
|
||||||
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% elif section.section_type == "about_strip" %}
|
|
||||||
<section class="section about-strip" aria-labelledby="about-strip-heading-{{ section.pk }}">
|
|
||||||
<div class="container">
|
|
||||||
<div class="about-strip-inner glass-card">
|
|
||||||
<div class="about-strip-content">
|
|
||||||
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
|
||||||
<h2 class="section-title" id="about-strip-heading-{{ section.pk }}">{{ section.title }}</h2>
|
|
||||||
{% if section.description %}<p class="about-strip-text">{{ section.description }}</p>{% endif %}
|
|
||||||
{% if section.link_text and section.link_url %}
|
|
||||||
<a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="about-strip-blobs" aria-hidden="true">
|
|
||||||
<div class="strip-blob strip-blob--1"></div>
|
|
||||||
<div class="strip-blob strip-blob--2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% if url %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% if url %}
|
||||||
|
<a href="{{ url }}" class="{{ card_class }} glass-card fade-in section-item-link">
|
||||||
|
{% else %}
|
||||||
|
<div class="{{ card_class }} glass-card fade-in">
|
||||||
|
{% endif %}
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
{% if items %}
|
{% if items %}
|
||||||
<div class="standards-grid">
|
<div class="standards-grid">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="standard-card glass-card fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="standard-card" %}
|
||||||
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
||||||
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
|
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
|
||||||
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
||||||
</div>
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -107,12 +107,11 @@
|
|||||||
{% if items %}
|
{% if items %}
|
||||||
<div class="standards-grid" style="margin-top: 1.5rem;">
|
<div class="standards-grid" style="margin-top: 1.5rem;">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="standard-card glass-card fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="standard-card" %}
|
||||||
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
|
||||||
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
|
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
|
||||||
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
|
||||||
{% if item.url %}<a href="{{ item.url }}" class="btn-ghost btn--sm" style="margin-top: 0.75rem;" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>{% endif %}
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -130,11 +129,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
{% for item in section.items.all %}
|
{% for item in section.items.all %}
|
||||||
<div class="feature-card glass-card fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="feature-card" %}
|
||||||
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||||
<h3 class="feature-title">{{ item.title }}</h3>
|
<h3 class="feature-title">{{ item.title }}</h3>
|
||||||
<p class="feature-desc" data-readmore="88">{{ item.content }}</p>
|
<p class="feature-desc" data-readmore="88">{{ item.content }}</p>
|
||||||
</div>
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,9 +150,9 @@
|
|||||||
<div class="screenshots-grid">
|
<div class="screenshots-grid">
|
||||||
{% for item in section.items.all %}
|
{% for item in section.items.all %}
|
||||||
{% if item.image %}
|
{% if item.image %}
|
||||||
<div class="screenshot-item fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="screenshot-item" %}
|
||||||
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }}" loading="lazy" />
|
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }}" loading="lazy" />
|
||||||
</div>
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="screenshot-item fade-in">
|
<div class="screenshot-item fade-in">
|
||||||
@@ -174,7 +173,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% elif section.section_type == "products" %}
|
{% elif section.section_type == "products" %}
|
||||||
{% if sub_products %}
|
{% if homepage_products %}
|
||||||
<section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
|
<section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -183,7 +182,7 @@
|
|||||||
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="products-grid">
|
<div class="products-grid">
|
||||||
{% for product in sub_products %}
|
{% for product in homepage_products %}
|
||||||
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
|
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
|
||||||
{% if product.image %}
|
{% if product.image %}
|
||||||
<div class="product-card-image">
|
<div class="product-card-image">
|
||||||
@@ -191,9 +190,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="product-card-body">
|
<div class="product-card-body">
|
||||||
{% if product.logo %}
|
|
||||||
<img src="{{ product.logo.url }}" alt="" class="product-card-logo" aria-hidden="true" loading="lazy" />
|
|
||||||
{% endif %}
|
|
||||||
<h3 class="product-card-title">{{ product.name }}</h3>
|
<h3 class="product-card-title">{{ product.name }}</h3>
|
||||||
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
|
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
|
||||||
<span class="product-card-link">
|
<span class="product-card-link">
|
||||||
@@ -210,6 +206,53 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif section.section_type == "products_catalog" %}
|
||||||
|
{% if homepage_products_catalog %}
|
||||||
|
<section class="section products-catalog-section" aria-labelledby="products-catalog-heading-{{ section.pk }}">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
|
||||||
|
{% if section.title %}<h2 class="section-title" id="products-catalog-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
|
||||||
|
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="products-overview-grid">
|
||||||
|
{% for product in homepage_products_catalog %}
|
||||||
|
<div class="product-overview-card glass-card fade-in">
|
||||||
|
{% if product.image %}
|
||||||
|
<div class="product-overview-image">
|
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="product-overview-body">
|
||||||
|
<h3 class="product-overview-title">{{ product.name }}</h3>
|
||||||
|
<p class="product-overview-short">{{ product.short_description }}</p>
|
||||||
|
<p class="product-overview-desc" data-readmore="110">{{ product.description }}</p>
|
||||||
|
<a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
|
||||||
|
</div>
|
||||||
|
{% if product.sub_products.all %}
|
||||||
|
<div class="product-overview-subs">
|
||||||
|
<h4 class="product-overview-subs-title">Modules</h4>
|
||||||
|
<ul class="product-overview-subs-list" role="list">
|
||||||
|
{% for sub in product.sub_products.all %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ sub.get_absolute_url }}" class="sub-link-chip">
|
||||||
|
{{ sub.name }}
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||||
|
<path d="M2.5 6H9.5M9.5 6L7 3.5M9.5 6L7 8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% elif section.section_type == "problems" %}
|
{% elif section.section_type == "problems" %}
|
||||||
<section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
|
<section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -220,11 +263,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="problems-grid">
|
<div class="problems-grid">
|
||||||
{% for item in section.items.all %}
|
{% for item in section.items.all %}
|
||||||
<div class="problem-item glass-card fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="problem-item" %}
|
||||||
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
|
||||||
<h3 class="problem-title">{{ item.title }}</h3>
|
<h3 class="problem-title">{{ item.title }}</h3>
|
||||||
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
|
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
|
||||||
</div>
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +283,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="supporters-grid">
|
<div class="supporters-grid">
|
||||||
{% for item in section.items.all %}
|
{% for item in section.items.all %}
|
||||||
<div class="supporter-card glass-card fade-in">
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="supporter-card" %}
|
||||||
{% if item.image %}
|
{% if item.image %}
|
||||||
<div class="supporter-logo">
|
<div class="supporter-logo">
|
||||||
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }} logo" loading="lazy" />
|
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }} logo" loading="lazy" />
|
||||||
@@ -250,7 +293,7 @@
|
|||||||
<h3 class="supporter-name">{{ item.title }}</h3>
|
<h3 class="supporter-name">{{ item.title }}</h3>
|
||||||
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
|
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,6 +332,17 @@
|
|||||||
{% if items %}
|
{% if items %}
|
||||||
<div class="faq-list">
|
<div class="faq-list">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
|
{% if item.url %}
|
||||||
|
{% include "partials/_linked_card_open.html" with url=item.url card_class="faq-item faq-item--link" %}
|
||||||
|
<div class="faq-question">
|
||||||
|
{{ item.title }}
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% if item.content %}<div class="rich-content faq-link-preview">{{ item.rendered_content }}</div>{% endif %}
|
||||||
|
{% include "partials/_linked_card_close.html" with url=item.url %}
|
||||||
|
{% else %}
|
||||||
<div class="faq-item glass-card fade-in">
|
<div class="faq-item glass-card fade-in">
|
||||||
<button
|
<button
|
||||||
class="faq-question"
|
class="faq-question"
|
||||||
@@ -311,6 +365,7 @@
|
|||||||
<div class="rich-content">{{ item.rendered_content }}</div>
|
<div class="rich-content">{{ item.rendered_content }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user