sum-cli 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sum/__init__.py +1 -0
- sum/boilerplate/.env.example +124 -0
- sum/boilerplate/.gitea/workflows/ci.yml +33 -0
- sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
- sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
- sum/boilerplate/.github/workflows/ci.yml +36 -0
- sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
- sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
- sum/boilerplate/.gitignore +45 -0
- sum/boilerplate/README.md +259 -0
- sum/boilerplate/manage.py +34 -0
- sum/boilerplate/project_name/__init__.py +5 -0
- sum/boilerplate/project_name/home/__init__.py +5 -0
- sum/boilerplate/project_name/home/apps.py +20 -0
- sum/boilerplate/project_name/home/management/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
- sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
- sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
- sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
- sum/boilerplate/project_name/home/models.py +13 -0
- sum/boilerplate/project_name/settings/__init__.py +5 -0
- sum/boilerplate/project_name/settings/base.py +348 -0
- sum/boilerplate/project_name/settings/local.py +78 -0
- sum/boilerplate/project_name/settings/production.py +106 -0
- sum/boilerplate/project_name/urls.py +33 -0
- sum/boilerplate/project_name/wsgi.py +16 -0
- sum/boilerplate/pytest.ini +5 -0
- sum/boilerplate/requirements.txt +25 -0
- sum/boilerplate/static/client/.gitkeep +3 -0
- sum/boilerplate/templates/overrides/.gitkeep +3 -0
- sum/boilerplate/tests/__init__.py +3 -0
- sum/boilerplate/tests/test_health.py +51 -0
- sum/cli.py +42 -0
- sum/commands/__init__.py +10 -0
- sum/commands/backup.py +308 -0
- sum/commands/check.py +128 -0
- sum/commands/init.py +265 -0
- sum/commands/promote.py +758 -0
- sum/commands/run.py +96 -0
- sum/commands/themes.py +56 -0
- sum/commands/update.py +301 -0
- sum/config.py +61 -0
- sum/docs/USER_GUIDE.md +663 -0
- sum/exceptions.py +45 -0
- sum/setup/__init__.py +17 -0
- sum/setup/auth.py +184 -0
- sum/setup/database.py +58 -0
- sum/setup/deps.py +73 -0
- sum/setup/git_ops.py +463 -0
- sum/setup/infrastructure.py +576 -0
- sum/setup/orchestrator.py +354 -0
- sum/setup/remote_themes.py +371 -0
- sum/setup/scaffold.py +500 -0
- sum/setup/seed.py +110 -0
- sum/setup/site_orchestrator.py +441 -0
- sum/setup/venv.py +89 -0
- sum/system_config.py +330 -0
- sum/themes_registry.py +180 -0
- sum/utils/__init__.py +25 -0
- sum/utils/django.py +97 -0
- sum/utils/environment.py +76 -0
- sum/utils/output.py +78 -0
- sum/utils/project.py +110 -0
- sum/utils/prompts.py +36 -0
- sum/utils/validation.py +313 -0
- sum_cli-3.0.0.dist-info/METADATA +127 -0
- sum_cli-3.0.0.dist-info/RECORD +72 -0
- sum_cli-3.0.0.dist-info/WHEEL +5 -0
- sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
- sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
- sum_cli-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Name: Populate Demo Content Management Command
|
|
3
|
+
Path: project_name/home/management/commands/populate_demo_content.py
|
|
4
|
+
Purpose: Generates realistic dummy content for Wagtail pages to enable fast iteration during development.
|
|
5
|
+
Creates HomePage and multiple StandardPages with varied StreamField blocks.
|
|
6
|
+
Family: Called by Django management command system (python manage.py populate_demo_content)
|
|
7
|
+
Dependencies:
|
|
8
|
+
- wagtail.models (Page)
|
|
9
|
+
- sum_core.pages.StandardPage
|
|
10
|
+
- project_name.home.models.HomePage (dynamically imported)
|
|
11
|
+
- Faker for realistic content generation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import random
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from django.apps import apps
|
|
20
|
+
from django.core.management.base import BaseCommand, CommandParser
|
|
21
|
+
from wagtail.models import Page, Site
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from faker import Faker
|
|
25
|
+
except ImportError:
|
|
26
|
+
Faker = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Command(BaseCommand):
|
|
30
|
+
"""
|
|
31
|
+
Management command to populate demo content for fast development iteration.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
python manage.py populate_demo_content
|
|
35
|
+
python manage.py populate_demo_content --clear # Remove existing demo pages first
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
help = "Populate demo content for Wagtail pages with realistic dummy data"
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
41
|
+
super().__init__(*args, **kwargs)
|
|
42
|
+
self.fake: Any = Faker() if Faker is not None else None
|
|
43
|
+
|
|
44
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--clear",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Delete existing demo content before creating new content",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--no-images",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Skip creating image blocks (useful if no sample images available)",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def handle(self, *args: Any, **options: dict[str, Any]) -> None:
|
|
57
|
+
if not self.fake:
|
|
58
|
+
self.stdout.write(
|
|
59
|
+
self.style.ERROR(
|
|
60
|
+
"Faker library not installed. Install with: pip install faker --break-system-packages"
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
self.no_images = options["no_images"]
|
|
66
|
+
|
|
67
|
+
if options["clear"]:
|
|
68
|
+
self.clear_demo_content()
|
|
69
|
+
|
|
70
|
+
self.create_demo_content()
|
|
71
|
+
|
|
72
|
+
self.stdout.write(self.style.SUCCESS("\n✓ Demo content created successfully!"))
|
|
73
|
+
|
|
74
|
+
def get_models(self) -> tuple[Any, Any]:
|
|
75
|
+
"""Dynamically import models based on Django app registry."""
|
|
76
|
+
# Find HomePage model using app label pattern
|
|
77
|
+
home_page_model = None
|
|
78
|
+
for app_config in apps.get_app_configs():
|
|
79
|
+
if "home" in app_config.label:
|
|
80
|
+
try:
|
|
81
|
+
home_page_model = apps.get_model(app_config.label, "HomePage")
|
|
82
|
+
break
|
|
83
|
+
except LookupError:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if not home_page_model:
|
|
87
|
+
self.stdout.write(
|
|
88
|
+
self.style.ERROR(
|
|
89
|
+
"HomePage model not found. Make sure your home app is in INSTALLED_APPS"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return None, None
|
|
93
|
+
|
|
94
|
+
# Get StandardPage
|
|
95
|
+
try:
|
|
96
|
+
from sum_core import pages
|
|
97
|
+
|
|
98
|
+
standard_page_model = pages.StandardPage
|
|
99
|
+
except ImportError:
|
|
100
|
+
self.stdout.write(self.style.ERROR("sum_core.pages not installed"))
|
|
101
|
+
return None, None
|
|
102
|
+
|
|
103
|
+
return home_page_model, standard_page_model
|
|
104
|
+
|
|
105
|
+
def clear_demo_content(self) -> None:
|
|
106
|
+
"""Remove existing demo pages."""
|
|
107
|
+
self.stdout.write("Clearing existing demo content...")
|
|
108
|
+
|
|
109
|
+
home_page_model, standard_page_model = self.get_models()
|
|
110
|
+
if not home_page_model or not standard_page_model:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Delete StandardPages
|
|
114
|
+
deleted_standard = standard_page_model.objects.all().delete()
|
|
115
|
+
self.stdout.write(f" Deleted {deleted_standard[0]} StandardPages")
|
|
116
|
+
|
|
117
|
+
# Delete HomePages (but not the root page)
|
|
118
|
+
deleted_home = home_page_model.objects.exclude(depth=2).delete()
|
|
119
|
+
self.stdout.write(f" Deleted {deleted_home[0]} HomePages")
|
|
120
|
+
|
|
121
|
+
def create_demo_content(self) -> None:
|
|
122
|
+
"""Create demo pages with varied content."""
|
|
123
|
+
home_page_model, standard_page_model = self.get_models()
|
|
124
|
+
if not home_page_model or not standard_page_model:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Get or create site root
|
|
128
|
+
root_page = Page.objects.get(depth=1)
|
|
129
|
+
|
|
130
|
+
# Check if HomePage already exists
|
|
131
|
+
home_page = home_page_model.objects.first()
|
|
132
|
+
if not home_page:
|
|
133
|
+
self.stdout.write("Creating HomePage...")
|
|
134
|
+
home_page = self._create_home_page(home_page_model, root_page)
|
|
135
|
+
else:
|
|
136
|
+
self.stdout.write(f"Using existing HomePage: {home_page.title}")
|
|
137
|
+
|
|
138
|
+
# Create StandardPages with varied content
|
|
139
|
+
self.stdout.write("\nCreating StandardPages...")
|
|
140
|
+
|
|
141
|
+
pages_config = [
|
|
142
|
+
("About Us", self._build_about_page),
|
|
143
|
+
("Our Services", self._build_services_page),
|
|
144
|
+
("Portfolio", self._build_portfolio_page),
|
|
145
|
+
("FAQ", self._build_faq_page),
|
|
146
|
+
("Contact", self._build_contact_page),
|
|
147
|
+
("How It Works", self._build_process_page),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for title, builder_func in pages_config:
|
|
151
|
+
# Check if page already exists to avoid duplicates
|
|
152
|
+
existing_page = (
|
|
153
|
+
root_page.get_children().filter(slug=self._slugify(title)).first()
|
|
154
|
+
)
|
|
155
|
+
if existing_page:
|
|
156
|
+
self.stdout.write(f" ⊙ Skipping (already exists): {title}")
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
page = standard_page_model(
|
|
160
|
+
title=title,
|
|
161
|
+
slug=self._slugify(title),
|
|
162
|
+
body=builder_func(),
|
|
163
|
+
)
|
|
164
|
+
root_page.add_child(instance=page)
|
|
165
|
+
page.save_revision().publish()
|
|
166
|
+
self.stdout.write(f" ✓ Created: {title}")
|
|
167
|
+
|
|
168
|
+
def _create_home_page(self, home_page_model: Any, parent: Page) -> Page:
|
|
169
|
+
"""Create a HomePage with hero and feature content."""
|
|
170
|
+
import uuid
|
|
171
|
+
|
|
172
|
+
body_content = [
|
|
173
|
+
self._build_hero_gradient(),
|
|
174
|
+
self._build_stats_block(),
|
|
175
|
+
self._build_service_cards(card_count=3),
|
|
176
|
+
self._build_testimonials_block(count=3),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
# Create with temporary unique slug to avoid conflicts
|
|
180
|
+
temp_slug = f"home-{uuid.uuid4().hex[:8]}"
|
|
181
|
+
home_page = home_page_model(
|
|
182
|
+
title="Home",
|
|
183
|
+
slug=temp_slug,
|
|
184
|
+
intro=f"<p>{self.fake.paragraph(nb_sentences=2)}</p>",
|
|
185
|
+
body=body_content,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
parent.add_child(instance=home_page)
|
|
189
|
+
home_page.save_revision().publish()
|
|
190
|
+
|
|
191
|
+
# Get the site and old root page
|
|
192
|
+
site = Site.objects.first()
|
|
193
|
+
old_root = None
|
|
194
|
+
if site:
|
|
195
|
+
old_root = site.root_page
|
|
196
|
+
# Set new page as site root
|
|
197
|
+
site.root_page = home_page
|
|
198
|
+
site.save()
|
|
199
|
+
|
|
200
|
+
# Delete the old default Wagtail page if it exists
|
|
201
|
+
if old_root and old_root.id != parent.id:
|
|
202
|
+
old_slug = old_root.slug
|
|
203
|
+
old_title = old_root.title
|
|
204
|
+
old_root.delete()
|
|
205
|
+
self.stdout.write(f" ✓ Removed old page: '{old_title}' (slug: {old_slug})")
|
|
206
|
+
|
|
207
|
+
# Now update slug to 'home' since the old one is gone
|
|
208
|
+
home_page.slug = "home"
|
|
209
|
+
home_page.save()
|
|
210
|
+
|
|
211
|
+
self.stdout.write(" ✓ Created HomePage")
|
|
212
|
+
return home_page
|
|
213
|
+
|
|
214
|
+
def _build_about_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
215
|
+
"""Build About page with editorial content."""
|
|
216
|
+
return [
|
|
217
|
+
self._build_hero_gradient(
|
|
218
|
+
headline="About <em>Our Company</em>",
|
|
219
|
+
subheadline="Building sustainable solutions since 2020",
|
|
220
|
+
),
|
|
221
|
+
self._build_rich_text_content(paragraphs=3),
|
|
222
|
+
self._build_stats_block(),
|
|
223
|
+
self._build_rich_text_content(paragraphs=2, center=True),
|
|
224
|
+
self._build_testimonials_block(count=2),
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
def _build_services_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
228
|
+
"""Build Services page with service cards and features."""
|
|
229
|
+
return [
|
|
230
|
+
self._build_hero_gradient(
|
|
231
|
+
headline="Our <em>Services</em>",
|
|
232
|
+
subheadline="Comprehensive solutions tailored to your needs",
|
|
233
|
+
),
|
|
234
|
+
self._build_service_cards(card_count=6),
|
|
235
|
+
self._build_divider(),
|
|
236
|
+
self._build_rich_text_content(paragraphs=2, center=True),
|
|
237
|
+
self._build_testimonials_block(count=3),
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
def _build_portfolio_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
241
|
+
"""Build Portfolio page with project showcase."""
|
|
242
|
+
return [
|
|
243
|
+
self._build_editorial_header(
|
|
244
|
+
heading="Our <em>Portfolio</em>",
|
|
245
|
+
eyebrow="Recent Work",
|
|
246
|
+
),
|
|
247
|
+
self._build_rich_text_content(paragraphs=1, center=True),
|
|
248
|
+
self._build_portfolio_block(item_count=4),
|
|
249
|
+
self._build_spacer("large"),
|
|
250
|
+
(
|
|
251
|
+
"buttons",
|
|
252
|
+
{
|
|
253
|
+
"alignment": "center",
|
|
254
|
+
"buttons": [
|
|
255
|
+
{
|
|
256
|
+
"label": "View All Projects",
|
|
257
|
+
"url": "/portfolio/",
|
|
258
|
+
"style": "primary",
|
|
259
|
+
}
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
def _build_faq_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
266
|
+
"""Build FAQ page with questions and answers."""
|
|
267
|
+
return [
|
|
268
|
+
self._build_editorial_header(
|
|
269
|
+
heading="Frequently Asked <em>Questions</em>",
|
|
270
|
+
eyebrow="Help Centre",
|
|
271
|
+
),
|
|
272
|
+
self._build_faq_block(),
|
|
273
|
+
self._build_spacer("large"),
|
|
274
|
+
self._build_rich_text_content(paragraphs=1, center=True),
|
|
275
|
+
(
|
|
276
|
+
"buttons",
|
|
277
|
+
{
|
|
278
|
+
"alignment": "center",
|
|
279
|
+
"buttons": [
|
|
280
|
+
{
|
|
281
|
+
"label": "Contact Support",
|
|
282
|
+
"url": "/contact/",
|
|
283
|
+
"style": "primary",
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
),
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
def _build_contact_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
291
|
+
"""Build Contact page with form."""
|
|
292
|
+
return [
|
|
293
|
+
self._build_editorial_header(
|
|
294
|
+
heading="Get in <em>Touch</em>",
|
|
295
|
+
eyebrow="Contact Us",
|
|
296
|
+
),
|
|
297
|
+
self._build_rich_text_content(paragraphs=1, center=True),
|
|
298
|
+
self._build_contact_form(),
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
def _build_process_page(self) -> list[tuple[str, dict[str, Any]]]:
|
|
302
|
+
"""Build How It Works page with process steps."""
|
|
303
|
+
return [
|
|
304
|
+
self._build_hero_gradient(
|
|
305
|
+
headline="How It <em>Works</em>",
|
|
306
|
+
subheadline="Our proven process for delivering results",
|
|
307
|
+
),
|
|
308
|
+
self._build_process_steps(),
|
|
309
|
+
self._build_spacer("large"),
|
|
310
|
+
self._build_testimonials_block(count=2),
|
|
311
|
+
self._build_spacer("medium"),
|
|
312
|
+
(
|
|
313
|
+
"buttons",
|
|
314
|
+
{
|
|
315
|
+
"alignment": "center",
|
|
316
|
+
"buttons": [
|
|
317
|
+
{
|
|
318
|
+
"label": "Get Started",
|
|
319
|
+
"url": "/contact/",
|
|
320
|
+
"style": "primary",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"label": "Learn More",
|
|
324
|
+
"url": "/services/",
|
|
325
|
+
"style": "secondary",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
# ========== Block Builders ==========
|
|
333
|
+
|
|
334
|
+
def _build_hero_gradient(
|
|
335
|
+
self,
|
|
336
|
+
headline: str | None = None,
|
|
337
|
+
subheadline: str | None = None,
|
|
338
|
+
) -> tuple[str, dict[str, Any]]:
|
|
339
|
+
"""Build a gradient hero block."""
|
|
340
|
+
if not headline:
|
|
341
|
+
company = self.fake.company()
|
|
342
|
+
headline = f"Welcome to <em>{company}</em>"
|
|
343
|
+
|
|
344
|
+
if not subheadline:
|
|
345
|
+
subheadline = self.fake.catch_phrase()
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
"hero_gradient",
|
|
349
|
+
{
|
|
350
|
+
"headline": f"<p>{headline}</p>",
|
|
351
|
+
"subheadline": subheadline,
|
|
352
|
+
"ctas": [
|
|
353
|
+
{
|
|
354
|
+
"label": "Get Started",
|
|
355
|
+
"url": "/contact/",
|
|
356
|
+
"style": "primary",
|
|
357
|
+
"open_in_new_tab": False,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"label": "Learn More",
|
|
361
|
+
"url": "/about/",
|
|
362
|
+
"style": "secondary",
|
|
363
|
+
"open_in_new_tab": False,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
"status": "",
|
|
367
|
+
"gradient_style": random.choice(["primary", "secondary", "accent"]),
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _build_service_cards(
|
|
372
|
+
self,
|
|
373
|
+
card_count: int = 3,
|
|
374
|
+
) -> tuple[str, dict[str, Any]]:
|
|
375
|
+
"""Build service cards block."""
|
|
376
|
+
services = [
|
|
377
|
+
("🔧", "Installation", "Professional installation services"),
|
|
378
|
+
("⚡", "Maintenance", "Regular maintenance and support"),
|
|
379
|
+
("📊", "Consulting", "Expert consulting and strategy"),
|
|
380
|
+
("🎯", "Training", "Comprehensive training programs"),
|
|
381
|
+
("🔒", "Security", "Advanced security solutions"),
|
|
382
|
+
("🚀", "Optimization", "Performance optimization services"),
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
cards = []
|
|
386
|
+
for i in range(min(card_count, len(services))):
|
|
387
|
+
icon, title, desc = services[i]
|
|
388
|
+
cards.append(
|
|
389
|
+
{
|
|
390
|
+
"icon": icon,
|
|
391
|
+
"image": None,
|
|
392
|
+
"title": title,
|
|
393
|
+
"description": f"<p>{desc}. {self.fake.sentence()}</p>",
|
|
394
|
+
"link_url": f"/services/{self._slugify(title)}/",
|
|
395
|
+
"link_label": "Learn more",
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
"service_cards",
|
|
401
|
+
{
|
|
402
|
+
"eyebrow": "What We Offer",
|
|
403
|
+
"heading": "<p>Our <em>Services</em></p>",
|
|
404
|
+
"intro": self.fake.paragraph(nb_sentences=2),
|
|
405
|
+
"cards": cards,
|
|
406
|
+
"layout_style": "default",
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _build_testimonials_block(
|
|
411
|
+
self,
|
|
412
|
+
count: int = 3,
|
|
413
|
+
) -> tuple[str, dict[str, Any]]:
|
|
414
|
+
"""Build testimonials block."""
|
|
415
|
+
testimonials = []
|
|
416
|
+
for _ in range(count):
|
|
417
|
+
testimonials.append(
|
|
418
|
+
{
|
|
419
|
+
"quote": self.fake.paragraph(nb_sentences=3),
|
|
420
|
+
"author_name": self.fake.name(),
|
|
421
|
+
"company": self.fake.company(),
|
|
422
|
+
"photo": None,
|
|
423
|
+
"rating": random.randint(4, 5),
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
"testimonials",
|
|
429
|
+
{
|
|
430
|
+
"eyebrow": "Client Stories",
|
|
431
|
+
"heading": "<p>What Our <em>Clients Say</em></p>",
|
|
432
|
+
"testimonials": testimonials,
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _build_stats_block(self) -> tuple[str, dict[str, Any]]:
|
|
437
|
+
"""Build statistics block."""
|
|
438
|
+
return (
|
|
439
|
+
"stats",
|
|
440
|
+
{
|
|
441
|
+
"eyebrow": "By the Numbers",
|
|
442
|
+
"intro": self.fake.sentence(),
|
|
443
|
+
"items": [
|
|
444
|
+
{
|
|
445
|
+
"value": "500",
|
|
446
|
+
"label": "Projects Completed",
|
|
447
|
+
"prefix": ">",
|
|
448
|
+
"suffix": "+",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"value": "15",
|
|
452
|
+
"label": "Years Experience",
|
|
453
|
+
"prefix": "",
|
|
454
|
+
"suffix": "yrs",
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
"value": "98",
|
|
458
|
+
"label": "Client Satisfaction",
|
|
459
|
+
"prefix": "",
|
|
460
|
+
"suffix": "%",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"value": "24",
|
|
464
|
+
"label": "Support Available",
|
|
465
|
+
"prefix": "",
|
|
466
|
+
"suffix": "/7",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _build_process_steps(self) -> tuple[str, dict[str, Any]]:
|
|
473
|
+
"""Build process steps block."""
|
|
474
|
+
steps = [
|
|
475
|
+
("Initial Consultation", "We discuss your needs and objectives"),
|
|
476
|
+
("Planning & Design", "Our team creates a customized solution"),
|
|
477
|
+
("Implementation", "We execute the plan with precision"),
|
|
478
|
+
("Testing & QA", "Rigorous testing ensures quality"),
|
|
479
|
+
("Launch & Support", "Go live with ongoing support"),
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
step_blocks = []
|
|
483
|
+
for i, (title, desc) in enumerate(steps, 1):
|
|
484
|
+
step_blocks.append(
|
|
485
|
+
{
|
|
486
|
+
"number": i,
|
|
487
|
+
"title": title,
|
|
488
|
+
"description": f"<p>{desc}. {self.fake.sentence()}</p>",
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
"process",
|
|
494
|
+
{
|
|
495
|
+
"eyebrow": "Our Approach",
|
|
496
|
+
"heading": "<p>How We <em>Work</em></p>",
|
|
497
|
+
"intro": f"<p>{self.fake.paragraph(nb_sentences=2)}</p>",
|
|
498
|
+
"steps": step_blocks,
|
|
499
|
+
},
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def _build_faq_block(self) -> tuple[str, dict[str, Any]]:
|
|
503
|
+
"""Build FAQ accordion block."""
|
|
504
|
+
faqs = [
|
|
505
|
+
(
|
|
506
|
+
"How do I get started?",
|
|
507
|
+
"Getting started is easy! Simply contact us through our form or give us a call.",
|
|
508
|
+
),
|
|
509
|
+
(
|
|
510
|
+
"What are your pricing plans?",
|
|
511
|
+
"We offer flexible pricing based on your specific needs. Contact us for a custom quote.",
|
|
512
|
+
),
|
|
513
|
+
(
|
|
514
|
+
"Do you offer support?",
|
|
515
|
+
"Yes, we provide 24/7 support to all our clients with comprehensive assistance.",
|
|
516
|
+
),
|
|
517
|
+
(
|
|
518
|
+
"How long does implementation take?",
|
|
519
|
+
"Timeline varies by project, but typical implementations take 2-4 weeks.",
|
|
520
|
+
),
|
|
521
|
+
(
|
|
522
|
+
"What makes you different?",
|
|
523
|
+
"Our commitment to quality, customer service, and innovative solutions sets us apart.",
|
|
524
|
+
),
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
faq_items = []
|
|
528
|
+
for question, answer in faqs:
|
|
529
|
+
faq_items.append(
|
|
530
|
+
{
|
|
531
|
+
"question": question,
|
|
532
|
+
"answer": f"<p>{answer} {self.fake.sentence()}</p>",
|
|
533
|
+
}
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
"faq",
|
|
538
|
+
{
|
|
539
|
+
"eyebrow": "Common Questions",
|
|
540
|
+
"heading": "<p><em>FAQ</em></p>",
|
|
541
|
+
"intro": f"<p>{self.fake.sentence()}</p>",
|
|
542
|
+
"items": faq_items,
|
|
543
|
+
},
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def _build_portfolio_block(
|
|
547
|
+
self,
|
|
548
|
+
item_count: int = 4,
|
|
549
|
+
) -> tuple[str, dict[str, Any]]:
|
|
550
|
+
"""Build portfolio items block."""
|
|
551
|
+
items = []
|
|
552
|
+
locations = ["London", "Manchester", "Birmingham", "Leeds", "Bristol"]
|
|
553
|
+
services = [
|
|
554
|
+
"Installation • Maintenance",
|
|
555
|
+
"Consulting • Training",
|
|
556
|
+
"Design • Implementation",
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
for i in range(item_count):
|
|
560
|
+
items.append(
|
|
561
|
+
{
|
|
562
|
+
"image": None, # Would need actual images
|
|
563
|
+
"alt_text": f"Project {i+1}",
|
|
564
|
+
"title": self.fake.catch_phrase(),
|
|
565
|
+
"location": f"{self.fake.city()}, {random.choice(locations)}",
|
|
566
|
+
"services": random.choice(services),
|
|
567
|
+
"link_url": f"/portfolio/project-{i+1}/",
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
"portfolio",
|
|
573
|
+
{
|
|
574
|
+
"eyebrow": "Recent Work",
|
|
575
|
+
"heading": "<p>Featured <em>Projects</em></p>",
|
|
576
|
+
"intro": self.fake.paragraph(nb_sentences=1),
|
|
577
|
+
"items": items,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def _build_contact_form(self) -> tuple[str, dict[str, Any]]:
|
|
582
|
+
"""Build contact form block."""
|
|
583
|
+
return (
|
|
584
|
+
"contact_form",
|
|
585
|
+
{
|
|
586
|
+
"eyebrow": "Get In Touch",
|
|
587
|
+
"heading": "<p>Send us a <em>Message</em></p>",
|
|
588
|
+
"intro": f"<p>{self.fake.paragraph(nb_sentences=2)}</p>",
|
|
589
|
+
"success_message": "Thank you for your enquiry! We'll get back to you soon.",
|
|
590
|
+
"submit_label": "Send Enquiry",
|
|
591
|
+
},
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def _build_rich_text_content(
|
|
595
|
+
self,
|
|
596
|
+
paragraphs: int = 2,
|
|
597
|
+
center: bool = False,
|
|
598
|
+
) -> tuple[str, dict[str, Any]]:
|
|
599
|
+
"""Build rich text content block."""
|
|
600
|
+
content = "".join(
|
|
601
|
+
[f"<p>{self.fake.paragraph(nb_sentences=3)}</p>" for _ in range(paragraphs)]
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
"content",
|
|
606
|
+
{
|
|
607
|
+
"align": "center" if center else "left",
|
|
608
|
+
"body": content,
|
|
609
|
+
},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def _build_editorial_header(
|
|
613
|
+
self,
|
|
614
|
+
heading: str,
|
|
615
|
+
eyebrow: str = "",
|
|
616
|
+
) -> tuple[str, dict[str, Any]]:
|
|
617
|
+
"""Build editorial header block."""
|
|
618
|
+
return (
|
|
619
|
+
"editorial_header",
|
|
620
|
+
{
|
|
621
|
+
"align": "center",
|
|
622
|
+
"eyebrow": eyebrow,
|
|
623
|
+
"heading": f"<p>{heading}</p>",
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _build_spacer(self, size: str = "medium") -> tuple[str, dict[str, Any]]:
|
|
628
|
+
"""Build spacer block."""
|
|
629
|
+
return ("spacer", {"size": size})
|
|
630
|
+
|
|
631
|
+
def _build_divider(self, style: str = "muted") -> tuple[str, dict[str, Any]]:
|
|
632
|
+
"""Build divider block."""
|
|
633
|
+
return ("divider", {"style": style})
|
|
634
|
+
|
|
635
|
+
# ========== Helpers ==========
|
|
636
|
+
|
|
637
|
+
def _slugify(self, text: str) -> str:
|
|
638
|
+
"""Convert text to URL-friendly slug."""
|
|
639
|
+
import re
|
|
640
|
+
|
|
641
|
+
text = text.lower()
|
|
642
|
+
text = re.sub(r"[^\w\s-]", "", text)
|
|
643
|
+
text = re.sub(r"[-\s]+", "-", text)
|
|
644
|
+
return text.strip("-")
|