227 lines
8.1 KiB
Python
227 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
from wagtail.blocks import StreamValue
|
|
from wagtail.models import Page
|
|
|
|
from mandelstudio.content_hygiene import BLOCKED_DEMO_PAGE_SLUGS, DEMO_MARKERS
|
|
from mandelstudio.idea_marketplace import seed_idea_marketplace_products
|
|
|
|
|
|
HOME_COPY = {
|
|
"nl": {
|
|
"badge": "IDEA MARKETPLACE",
|
|
"headline": "Premium ideeën die je direct kunt uitvoeren",
|
|
"sub_headline": "<p>Ontdek bewezen plannen, koop de strategie en ontgrendel het volledige implementatieplan.</p>",
|
|
"features_title": "Idea Marketplace",
|
|
"features_subtitle": "<p>Preview eerst. Koop alleen wat past. Ontgrendel daarna de complete blueprint.</p>",
|
|
"footer_headline": "Klaar om een premium idee te ontgrendelen?",
|
|
"footer_subheadline": "<p>Kies een plan, rond checkout af en krijg direct toegang tot de volledige strategie.</p>",
|
|
"cta_explore": "Explore Ideas",
|
|
"cta_buy": "Buy Strategy",
|
|
"cta_unlock": "Unlock Full Plan",
|
|
},
|
|
"en": {
|
|
"badge": "IDEA MARKETPLACE",
|
|
"headline": "Premium ideas you can execute immediately",
|
|
"sub_headline": "<p>Explore proven plans, buy the strategy, and unlock the full implementation blueprint.</p>",
|
|
"features_title": "Idea Marketplace",
|
|
"features_subtitle": "<p>Preview first. Buy what fits. Unlock complete execution plans after checkout.</p>",
|
|
"footer_headline": "Ready to unlock a premium idea?",
|
|
"footer_subheadline": "<p>Select a plan, complete checkout, and get full strategy access instantly.</p>",
|
|
"cta_explore": "Explore Ideas",
|
|
"cta_buy": "Buy Strategy",
|
|
"cta_unlock": "Unlock Full Plan",
|
|
},
|
|
}
|
|
|
|
SUPPORTED_LANGUAGES = {"nl", "en", "de", "fr", "es", "it", "pt", "ru"}
|
|
|
|
|
|
def _copy_for(language_code: str) -> dict[str, str]:
|
|
normalized = (language_code or "nl").split("-")[0].lower()
|
|
if normalized not in SUPPORTED_LANGUAGES:
|
|
normalized = "nl"
|
|
return HOME_COPY["en"] if normalized != "nl" else HOME_COPY["nl"]
|
|
|
|
|
|
def _shop_url_for(language_code: str) -> str:
|
|
normalized = (language_code or "nl").split("-")[0].lower()
|
|
if normalized == "nl":
|
|
return "/shop/"
|
|
return f"/{normalized}/shop/"
|
|
|
|
|
|
def _update_homepage_stream(page) -> bool:
|
|
if not hasattr(page, "body"):
|
|
return False
|
|
body = page.body
|
|
if not body:
|
|
return False
|
|
|
|
copy = _copy_for(getattr(page.locale, "language_code", "nl"))
|
|
shop_url = _shop_url_for(getattr(page.locale, "language_code", "nl"))
|
|
|
|
stream_data = list(body.stream_data)
|
|
changed = False
|
|
for block in stream_data:
|
|
block_type = block.get("type")
|
|
value = block.get("value", {})
|
|
if not isinstance(value, dict):
|
|
continue
|
|
|
|
if block_type == "saas_hero_banner":
|
|
updates = {
|
|
"badge_text": copy["badge"],
|
|
"headline": copy["headline"],
|
|
"sub_headline": copy["sub_headline"],
|
|
"primary_cta_text": copy["cta_explore"],
|
|
"primary_cta_url": shop_url,
|
|
"secondary_cta_text": copy["cta_buy"],
|
|
"secondary_cta_url": shop_url,
|
|
}
|
|
for key, new_value in updates.items():
|
|
if value.get(key) != new_value:
|
|
value[key] = new_value
|
|
changed = True
|
|
|
|
if block_type == "saas_features":
|
|
updates = {
|
|
"section_title": copy["features_title"],
|
|
"section_subtitle": copy["features_subtitle"],
|
|
}
|
|
for key, new_value in updates.items():
|
|
if value.get(key) != new_value:
|
|
value[key] = new_value
|
|
changed = True
|
|
|
|
if block_type == "saas_cta_footer":
|
|
updates = {
|
|
"headline": copy["footer_headline"],
|
|
"subheadline": copy["footer_subheadline"],
|
|
"primary_cta_text": copy["cta_unlock"],
|
|
"primary_cta_url": shop_url,
|
|
"secondary_cta_text": copy["cta_explore"],
|
|
"secondary_cta_url": shop_url,
|
|
}
|
|
for key, new_value in updates.items():
|
|
if value.get(key) != new_value:
|
|
value[key] = new_value
|
|
changed = True
|
|
|
|
if not changed:
|
|
return False
|
|
|
|
page.body = StreamValue(page.body.stream_block, stream_data, is_lazy=True)
|
|
page.search_description = (
|
|
"Idea marketplace with premium plans. Preview each strategy and unlock full implementation after purchase."
|
|
)
|
|
page.save()
|
|
return True
|
|
|
|
|
|
def _purge_demo_pages() -> int:
|
|
marker_filter = Q()
|
|
for marker in DEMO_MARKERS:
|
|
marker_filter |= (
|
|
Q(title__icontains=marker)
|
|
| Q(slug__icontains=marker)
|
|
| Q(search_description__icontains=marker)
|
|
)
|
|
candidate_ids = set(
|
|
Page.objects.exclude(depth__lte=2).filter(marker_filter).values_list("id", flat=True)
|
|
)
|
|
candidate_ids.update(
|
|
Page.objects.exclude(depth__lte=2)
|
|
.filter(slug__in=BLOCKED_DEMO_PAGE_SLUGS)
|
|
.values_list("id", flat=True)
|
|
)
|
|
candidates = Page.objects.filter(id__in=candidate_ids).specific()
|
|
deleted = 0
|
|
for page in candidates:
|
|
page.delete()
|
|
deleted += 1
|
|
return deleted
|
|
|
|
|
|
def _update_homepages() -> int:
|
|
updated = 0
|
|
# In this architecture localized homepages are expected at depth=2.
|
|
for page in Page.objects.filter(depth=2).specific():
|
|
if _update_homepage_stream(page):
|
|
updated += 1
|
|
return updated
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Prepare production idea marketplace launch: seed idea products, purge obvious demo pages, "
|
|
"and refresh homepage sections/CTAs to marketplace messaging."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--no-seed",
|
|
action="store_true",
|
|
help="Skip idea product seeding.",
|
|
)
|
|
parser.add_argument(
|
|
"--purge-demo-pages",
|
|
action="store_true",
|
|
help="Delete pages with obvious demo/lorem/sample markers.",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-purge-demo-pages",
|
|
action="store_true",
|
|
help="Skip deleting obvious demo pages (enabled by default).",
|
|
)
|
|
parser.add_argument(
|
|
"--apply-homepage-copy",
|
|
action="store_true",
|
|
help="Update homepage stream blocks to idea marketplace messaging and CTAs.",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-apply-homepage-copy",
|
|
action="store_true",
|
|
help="Skip homepage marketplace copy refresh (enabled by default).",
|
|
)
|
|
|
|
@transaction.atomic
|
|
def handle(self, *args, **options):
|
|
if not options["no_seed"]:
|
|
seed_stats = seed_idea_marketplace_products(
|
|
purge_demo_products=True,
|
|
retire_non_idea_products=True,
|
|
)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
"Seeded idea products: "
|
|
f"created={seed_stats['created']}, "
|
|
f"updated={seed_stats['updated']}, "
|
|
f"deleted_demo_products={seed_stats['deleted_demo']}, "
|
|
f"retired_non_idea_products={seed_stats['retired_non_idea']}"
|
|
)
|
|
)
|
|
|
|
should_purge_demo_pages = (
|
|
options["purge_demo_pages"] or not options["skip_purge_demo_pages"]
|
|
)
|
|
if should_purge_demo_pages:
|
|
deleted_pages = _purge_demo_pages()
|
|
self.stdout.write(
|
|
self.style.SUCCESS(f"Deleted demo pages: {deleted_pages}")
|
|
)
|
|
|
|
should_apply_homepage_copy = (
|
|
options["apply_homepage_copy"] or not options["skip_apply_homepage_copy"]
|
|
)
|
|
if should_apply_homepage_copy:
|
|
updated_pages = _update_homepages()
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"Updated homepages with marketplace copy: {updated_pages}"
|
|
)
|
|
)
|