inital commit
This commit is contained in:
1
builder/__init__.py
Normal file
1
builder/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
17
builder/admin.py
Normal file
17
builder/admin.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import ClientSite, TemplateOption
|
||||
|
||||
|
||||
@admin.register(TemplateOption)
|
||||
class TemplateOptionAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "slug", "supports_basic", "supports_django", "is_active")
|
||||
list_filter = ("supports_basic", "supports_django", "is_active")
|
||||
search_fields = ("name", "slug")
|
||||
|
||||
|
||||
@admin.register(ClientSite)
|
||||
class ClientSiteAdmin(admin.ModelAdmin):
|
||||
list_display = ("business_name", "owner", "status", "template_option", "created_at")
|
||||
list_filter = ("status", "template_option")
|
||||
search_fields = ("business_name", "owner__username", "business_phone")
|
||||
6
builder/apps.py
Normal file
6
builder/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BuilderConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "builder"
|
||||
29
builder/forms.py
Normal file
29
builder/forms.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .models import ClientSite
|
||||
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "email", "password1", "password2")
|
||||
|
||||
|
||||
class ClientSiteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ClientSite
|
||||
fields = [
|
||||
"business_name",
|
||||
"business_address",
|
||||
"business_phone",
|
||||
"business_description",
|
||||
"old_website",
|
||||
"template_option",
|
||||
]
|
||||
widgets = {
|
||||
"business_description": forms.Textarea(attrs={"rows": 4}),
|
||||
}
|
||||
1
builder/management/__init__.py
Normal file
1
builder/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
builder/management/commands/__init__.py
Normal file
1
builder/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
49
builder/management/commands/seed_templates.py
Normal file
49
builder/management/commands/seed_templates.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from builder.models import TemplateOption
|
||||
from builder.template_registry import discover_client_templates
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed template options from templates/template_* folders and site_catalog/templates.json"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
root = Path(__file__).resolve().parents[4]
|
||||
catalog_path = root / "site_catalog" / "templates.json"
|
||||
|
||||
discovered = {item["slug"]: item for item in discover_client_templates()}
|
||||
catalog_items: dict[str, dict] = {}
|
||||
|
||||
if catalog_path.exists():
|
||||
for item in json.loads(catalog_path.read_text(encoding="utf-8")):
|
||||
catalog_items[item["slug"]] = item
|
||||
|
||||
merged_slugs = set(discovered) | set(catalog_items)
|
||||
count = 0
|
||||
|
||||
for slug in sorted(merged_slugs):
|
||||
discovered_item = discovered.get(slug, {})
|
||||
catalog_item = catalog_items.get(slug, {})
|
||||
merged = {**discovered_item, **catalog_item}
|
||||
if not merged.get("source_folder"):
|
||||
self.stdout.write(self.style.WARNING(f"Skipping {slug}: no source_folder."))
|
||||
continue
|
||||
|
||||
TemplateOption.objects.update_or_create(
|
||||
slug=slug,
|
||||
defaults={
|
||||
"name": merged.get("name", slug),
|
||||
"description": merged.get("description", ""),
|
||||
"supports_basic": merged.get("supports_basic", True),
|
||||
"supports_django": merged.get("supports_django", True),
|
||||
"source_folder": merged["source_folder"],
|
||||
"is_active": merged.get("is_active", True),
|
||||
},
|
||||
)
|
||||
count += 1
|
||||
|
||||
TemplateOption.objects.exclude(slug__in=merged_slugs).update(is_active=False)
|
||||
self.stdout.write(self.style.SUCCESS(f"Seeded {count} template option(s)."))
|
||||
77
builder/migrations/0001_initial.py
Normal file
77
builder/migrations/0001_initial.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="TemplateOption",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=120)),
|
||||
("slug", models.SlugField(unique=True)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("supports_basic", models.BooleanField(default=True)),
|
||||
("supports_django", models.BooleanField(default=True)),
|
||||
("source_folder", models.CharField(blank=True, max_length=255)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={"ordering": ["name"]},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ClientSite",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
("business_name", models.CharField(max_length=160)),
|
||||
("business_address", models.CharField(max_length=255)),
|
||||
("business_phone", models.CharField(max_length=40)),
|
||||
("business_description", models.TextField()),
|
||||
("old_website", models.URLField(blank=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("draft", "Draft"),
|
||||
("generating", "Generating"),
|
||||
("ready", "Ready"),
|
||||
("failed", "Failed"),
|
||||
],
|
||||
default="draft",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("ai_payload", models.JSONField(blank=True, default=dict)),
|
||||
("basic_zip_path", models.CharField(blank=True, max_length=255)),
|
||||
("django_zip_path", models.CharField(blank=True, max_length=255)),
|
||||
("last_error", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="client_sites",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"template_option",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="client_sites",
|
||||
to="builder.templateoption",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["-created_at"]},
|
||||
),
|
||||
]
|
||||
1
builder/migrations/__init__.py
Normal file
1
builder/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
51
builder/models.py
Normal file
51
builder/models.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TemplateOption(models.Model):
|
||||
name = models.CharField(max_length=120)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
supports_basic = models.BooleanField(default=True)
|
||||
supports_django = models.BooleanField(default=True)
|
||||
source_folder = models.CharField(max_length=255, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class ClientSite(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "draft", "Draft"
|
||||
GENERATING = "generating", "Generating"
|
||||
READY = "ready", "Ready"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="client_sites")
|
||||
template_option = models.ForeignKey(TemplateOption, on_delete=models.PROTECT, related_name="client_sites")
|
||||
business_name = models.CharField(max_length=160)
|
||||
business_address = models.CharField(max_length=255)
|
||||
business_phone = models.CharField(max_length=40)
|
||||
business_description = models.TextField()
|
||||
old_website = models.URLField(blank=True)
|
||||
status = models.CharField(max_length=20, choices=Status.choices, default=Status.DRAFT)
|
||||
ai_payload = models.JSONField(default=dict, blank=True)
|
||||
basic_zip_path = models.CharField(max_length=255, blank=True)
|
||||
django_zip_path = models.CharField(max_length=255, blank=True)
|
||||
last_error = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.business_name} ({self.owner})"
|
||||
709
builder/services.py
Normal file
709
builder/services.py
Normal file
@@ -0,0 +1,709 @@
|
||||
import html
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .models import ClientSite
|
||||
from .template_registry import (
|
||||
COPY_EXCLUDE_DIR_NAMES,
|
||||
get_template_config,
|
||||
resolve_source_path,
|
||||
)
|
||||
|
||||
PERSONALIZE_EXTENSIONS = {".html", ".htm", ".css", ".js", ".jsx", ".md", ".txt", ".php"}
|
||||
|
||||
|
||||
class SiteGenerator:
|
||||
def __init__(self) -> None:
|
||||
self.ollama_url = settings.OLLAMA_URL.rstrip("/")
|
||||
self.ollama_model = settings.OLLAMA_MODEL
|
||||
|
||||
def build_all(self, site: ClientSite) -> ClientSite:
|
||||
site_root = Path(settings.MEDIA_ROOT) / "generated" / str(site.id)
|
||||
if site_root.exists():
|
||||
shutil.rmtree(site_root)
|
||||
site_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ai_payload = self._generate_ai_payload(site)
|
||||
basic_zip = self._build_basic_site(site, ai_payload, site_root)
|
||||
django_zip = self._build_django_demo(site, ai_payload, site_root)
|
||||
|
||||
site.ai_payload = ai_payload
|
||||
site.basic_zip_path = str(basic_zip.relative_to(settings.MEDIA_ROOT))
|
||||
site.django_zip_path = str(django_zip.relative_to(settings.MEDIA_ROOT))
|
||||
site.status = ClientSite.Status.READY
|
||||
site.last_error = ""
|
||||
site.save(
|
||||
update_fields=[
|
||||
"ai_payload",
|
||||
"basic_zip_path",
|
||||
"django_zip_path",
|
||||
"status",
|
||||
"last_error",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return site
|
||||
|
||||
def _generate_ai_payload(self, site: ClientSite) -> dict:
|
||||
fallback = {
|
||||
"tagline": f"{site.business_name} helps local customers with trusted service.",
|
||||
"hero_headline": f"Welcome to {site.business_name}",
|
||||
"hero_subheadline": site.business_description,
|
||||
"services": [
|
||||
"Professional support tailored to your business goals",
|
||||
"Friendly service with clear communication",
|
||||
"Fast turnaround with quality-focused delivery",
|
||||
],
|
||||
"testimonials": [
|
||||
{"name": "Jordan T.", "quote": f"{site.business_name} made process simple and stress-free."},
|
||||
{"name": "Casey M.", "quote": "Great communication, clear pricing, excellent final result."},
|
||||
{"name": "Alex R.", "quote": "Reliable team. We will keep using them for future needs."},
|
||||
],
|
||||
"contact_prompt": "Tell us what you need and we will respond quickly.",
|
||||
}
|
||||
|
||||
prompt = (
|
||||
"You are website copy assistant. Respond in valid JSON only. "
|
||||
"Use keys: tagline, hero_headline, hero_subheadline, services, testimonials, contact_prompt. "
|
||||
"services must be 3 short strings. testimonials must be array of 3 objects with name and quote."
|
||||
f"\nBusiness name: {site.business_name}"
|
||||
f"\nBusiness description: {site.business_description}"
|
||||
f"\nAddress: {site.business_address}"
|
||||
f"\nPhone: {site.business_phone}"
|
||||
f"\nOld website: {site.old_website or 'n/a'}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.ollama_url}/api/generate",
|
||||
json={"model": self.ollama_model, "prompt": prompt, "stream": False},
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
model_text = response.json().get("response", "").strip()
|
||||
parsed = json.loads(model_text)
|
||||
if not isinstance(parsed, dict):
|
||||
return fallback
|
||||
return {
|
||||
"tagline": parsed.get("tagline") or fallback["tagline"],
|
||||
"hero_headline": parsed.get("hero_headline") or fallback["hero_headline"],
|
||||
"hero_subheadline": parsed.get("hero_subheadline") or fallback["hero_subheadline"],
|
||||
"services": parsed.get("services") or fallback["services"],
|
||||
"testimonials": parsed.get("testimonials") or fallback["testimonials"],
|
||||
"contact_prompt": parsed.get("contact_prompt") or fallback["contact_prompt"],
|
||||
}
|
||||
except Exception:
|
||||
return fallback
|
||||
|
||||
def _build_basic_site(self, site: ClientSite, ai_payload: dict, root: Path) -> Path:
|
||||
basic_dir = root / "basic_site"
|
||||
if basic_dir.exists():
|
||||
shutil.rmtree(basic_dir)
|
||||
|
||||
source_path = resolve_source_path(site.template_option.source_folder)
|
||||
if source_path.exists() and source_path.is_dir():
|
||||
self._copy_client_template(source_path, basic_dir)
|
||||
config = get_template_config(site.template_option.slug) or {}
|
||||
self._personalize_template_tree(basic_dir, site, ai_payload, config)
|
||||
self._write_client_readme(basic_dir, site, ai_payload, config)
|
||||
basic_zip = root / "basic_site.zip"
|
||||
self._zip_dir(basic_dir, basic_zip)
|
||||
return basic_zip
|
||||
|
||||
basic_dir.mkdir(parents=True, exist_ok=True)
|
||||
safe_name = html.escape(site.business_name)
|
||||
safe_phone = html.escape(site.business_phone)
|
||||
safe_address = html.escape(site.business_address)
|
||||
safe_description = html.escape(site.business_description)
|
||||
safe_tagline = html.escape(ai_payload["tagline"])
|
||||
hero_title = html.escape(ai_payload["hero_headline"])
|
||||
hero_sub = html.escape(ai_payload["hero_subheadline"])
|
||||
services = ai_payload.get("services", [])
|
||||
testimonials = ai_payload.get("testimonials", [])
|
||||
|
||||
nav = """
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="about.html">About</a>
|
||||
<a href="testimonials.html">Testimonials</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
"""
|
||||
index_html = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{safe_name} | Home</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
{nav}
|
||||
<header class="hero">
|
||||
<h1>{hero_title}</h1>
|
||||
<p>{hero_sub}</p>
|
||||
<span class="tagline">{safe_tagline}</span>
|
||||
</header>
|
||||
<section>
|
||||
<h2>What We Offer</h2>
|
||||
<ul>
|
||||
<li>{html.escape(services[0] if len(services) > 0 else "Quality-focused support")}</li>
|
||||
<li>{html.escape(services[1] if len(services) > 1 else "Local-first service model")}</li>
|
||||
<li>{html.escape(services[2] if len(services) > 2 else "Clear process from start to finish")}</li>
|
||||
</ul>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
about_html = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{safe_name} | About</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
{nav}
|
||||
<main>
|
||||
<h1>About {safe_name}</h1>
|
||||
<p>{safe_description}</p>
|
||||
<p>Address: {safe_address}</p>
|
||||
<p>Phone: {safe_phone}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
t1 = testimonials[0] if len(testimonials) > 0 and isinstance(testimonials[0], dict) else {"name": "Client", "quote": "Great result."}
|
||||
t2 = testimonials[1] if len(testimonials) > 1 and isinstance(testimonials[1], dict) else {"name": "Client", "quote": "Excellent service."}
|
||||
t3 = testimonials[2] if len(testimonials) > 2 and isinstance(testimonials[2], dict) else {"name": "Client", "quote": "Would recommend."}
|
||||
testimonials_html = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{safe_name} | Testimonials</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
{nav}
|
||||
<main>
|
||||
<h1>Testimonials</h1>
|
||||
<blockquote>"{html.escape(str(t1.get("quote", "Great result.")))}" <cite>- {html.escape(str(t1.get("name", "Client")))}</cite></blockquote>
|
||||
<blockquote>"{html.escape(str(t2.get("quote", "Excellent service.")))}" <cite>- {html.escape(str(t2.get("name", "Client")))}</cite></blockquote>
|
||||
<blockquote>"{html.escape(str(t3.get("quote", "Would recommend.")))}" <cite>- {html.escape(str(t3.get("name", "Client")))}</cite></blockquote>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
contact_html = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{safe_name} | Contact</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
{nav}
|
||||
<main>
|
||||
<h1>Contact Us</h1>
|
||||
<p>{html.escape(ai_payload["contact_prompt"])}</p>
|
||||
<p>Phone: {safe_phone}</p>
|
||||
<p>Address: {safe_address}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
styles = """
|
||||
body{font-family:Arial,sans-serif;margin:0;background:#f6f7fb;color:#1f2937;}
|
||||
nav{display:flex;gap:1rem;padding:1rem 2rem;background:#111827;}
|
||||
nav a{color:#fff;text-decoration:none;font-weight:600;}
|
||||
.hero{padding:4rem 2rem;background:#2563eb;color:white;}
|
||||
main,section{max-width:900px;margin:2rem auto;padding:0 1rem;}
|
||||
blockquote{background:#fff;padding:1rem;border-left:4px solid #2563eb;border-radius:6px;}
|
||||
.tagline{display:inline-block;margin-top:1rem;background:#1d4ed8;padding:.25rem .75rem;border-radius:999px;}
|
||||
"""
|
||||
|
||||
(basic_dir / "index.html").write_text(index_html, encoding="utf-8")
|
||||
(basic_dir / "about.html").write_text(about_html, encoding="utf-8")
|
||||
(basic_dir / "testimonials.html").write_text(testimonials_html, encoding="utf-8")
|
||||
(basic_dir / "contact.html").write_text(contact_html, encoding="utf-8")
|
||||
(basic_dir / "styles.css").write_text(styles, encoding="utf-8")
|
||||
|
||||
basic_zip = root / "basic_site.zip"
|
||||
self._zip_dir(basic_dir, basic_zip)
|
||||
return basic_zip
|
||||
|
||||
def _copy_client_template(self, source_path: Path, destination: Path) -> None:
|
||||
def ignore(_directory: str, names: list[str]) -> list[str]:
|
||||
ignored: list[str] = []
|
||||
for name in names:
|
||||
if name in COPY_EXCLUDE_DIR_NAMES:
|
||||
ignored.append(name)
|
||||
elif name.endswith(".zip"):
|
||||
ignored.append(name)
|
||||
return ignored
|
||||
|
||||
shutil.copytree(source_path, destination, ignore=ignore)
|
||||
|
||||
def _personalize_template_tree(
|
||||
self,
|
||||
target_dir: Path,
|
||||
site: ClientSite,
|
||||
ai_payload: dict,
|
||||
config: dict,
|
||||
) -> None:
|
||||
replacements = self._build_replacements(site, ai_payload, config)
|
||||
for file_path in target_dir.rglob("*"):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
if file_path.suffix.lower() not in PERSONALIZE_EXTENSIONS:
|
||||
continue
|
||||
if "bootstrap-icons" in file_path.parts and file_path.suffix == ".json":
|
||||
continue
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
updated = content
|
||||
for old, new in replacements.items():
|
||||
if old and new and old in updated:
|
||||
updated = updated.replace(old, new)
|
||||
if updated != content:
|
||||
file_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
info_path = target_dir / "client-site-info.json"
|
||||
info_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"business_name": site.business_name,
|
||||
"business_address": site.business_address,
|
||||
"business_phone": site.business_phone,
|
||||
"business_description": site.business_description,
|
||||
"old_website": site.old_website,
|
||||
"ai_payload": ai_payload,
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def _build_replacements(self, site: ClientSite, ai_payload: dict, config: dict) -> dict[str, str]:
|
||||
replacements: dict[str, str] = {}
|
||||
business_name = site.business_name
|
||||
hero_headline = ai_payload.get("hero_headline", business_name)
|
||||
hero_subheadline = ai_payload.get("hero_subheadline", site.business_description)
|
||||
|
||||
for token in config.get("brand_tokens", []):
|
||||
replacements[token] = business_name
|
||||
|
||||
hero_strings = config.get("hero_strings", [])
|
||||
if hero_strings:
|
||||
replacements[hero_strings[0]] = hero_headline
|
||||
for hero_string in hero_strings[1:]:
|
||||
replacements[hero_string] = hero_subheadline
|
||||
|
||||
replacements.update(
|
||||
{
|
||||
"555-0100": site.business_phone,
|
||||
"555-0101": site.business_phone,
|
||||
"(555) 010-0100": site.business_phone,
|
||||
"123 Main Street": site.business_address,
|
||||
"San Francisco, California": site.business_address,
|
||||
}
|
||||
)
|
||||
|
||||
if site.old_website:
|
||||
replacements["https://example.com"] = site.old_website
|
||||
replacements["http://example.com"] = site.old_website
|
||||
|
||||
return replacements
|
||||
|
||||
def _write_client_readme(
|
||||
self,
|
||||
target_dir: Path,
|
||||
site: ClientSite,
|
||||
ai_payload: dict,
|
||||
config: dict,
|
||||
) -> None:
|
||||
kind = config.get("kind", "static_html")
|
||||
run_hint = "Open index.html in a browser."
|
||||
if kind == "nextjs":
|
||||
run_hint = "Run: npm install && npm run dev"
|
||||
elif kind == "static_html":
|
||||
run_hint = "Open index.html (or another index-*.html page) in a browser."
|
||||
|
||||
readme = f"""# {site.business_name} — generated site package
|
||||
|
||||
Template: {site.template_option.name}
|
||||
Type: {kind}
|
||||
|
||||
## Business details
|
||||
|
||||
- Name: {site.business_name}
|
||||
- Phone: {site.business_phone}
|
||||
- Address: {site.business_address}
|
||||
- Description: {site.business_description}
|
||||
"""
|
||||
if site.old_website:
|
||||
readme += f"- Previous website: {site.old_website}\n"
|
||||
|
||||
readme += f"""
|
||||
## AI-generated copy
|
||||
|
||||
- Tagline: {ai_payload.get("tagline", "")}
|
||||
- Headline: {ai_payload.get("hero_headline", "")}
|
||||
- Subheadline: {ai_payload.get("hero_subheadline", "")}
|
||||
|
||||
## How to preview
|
||||
|
||||
{run_hint}
|
||||
|
||||
See client-site-info.json for full generation payload.
|
||||
"""
|
||||
(target_dir / "README-CLIENT.md").write_text(readme, encoding="utf-8")
|
||||
|
||||
def _build_django_demo(self, site: ClientSite, ai_payload: dict, root: Path) -> Path:
|
||||
project_slug = slugify(site.business_name)[:30] or "client-site"
|
||||
django_dir = root / "django_demo"
|
||||
django_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files = {
|
||||
"manage.py": self._django_manage_py(),
|
||||
"client_demo/__init__.py": "",
|
||||
"client_demo/settings.py": self._django_settings_py(site),
|
||||
"client_demo/urls.py": self._django_urls_py(),
|
||||
"client_demo/wsgi.py": self._django_wsgi_py(),
|
||||
"shop/__init__.py": "",
|
||||
"shop/apps.py": self._shop_apps_py(),
|
||||
"shop/models.py": self._shop_models_py(),
|
||||
"shop/forms.py": self._shop_forms_py(),
|
||||
"shop/urls.py": self._shop_urls_py(),
|
||||
"shop/views.py": self._shop_views_py(ai_payload),
|
||||
"shop/admin.py": self._shop_admin_py(),
|
||||
"shop/migrations/__init__.py": "",
|
||||
"templates/base.html": self._shop_base_template(site),
|
||||
"templates/shop/home.html": self._shop_home_template(),
|
||||
"templates/shop/products.html": self._shop_products_template(),
|
||||
"templates/shop/cart.html": self._shop_cart_template(),
|
||||
"templates/shop/contact.html": self._shop_contact_template(),
|
||||
"README.md": self._django_readme(project_slug),
|
||||
}
|
||||
|
||||
for relative_path, content in files.items():
|
||||
destination = django_dir / relative_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(content, encoding="utf-8")
|
||||
|
||||
django_zip = root / "django_demo.zip"
|
||||
self._zip_dir(django_dir, django_zip)
|
||||
return django_zip
|
||||
|
||||
def _zip_dir(self, directory: Path, output_zip: Path) -> None:
|
||||
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in directory.rglob("*"):
|
||||
if file_path.is_file():
|
||||
zipf.write(file_path, file_path.relative_to(directory))
|
||||
|
||||
def _django_manage_py(self) -> str:
|
||||
return """#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings")
|
||||
from django.core.management import execute_from_command_line
|
||||
execute_from_command_line(sys.argv)
|
||||
"""
|
||||
|
||||
def _django_settings_py(self, site: ClientSite) -> str:
|
||||
return f"""from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
SECRET_KEY = "replace-me"
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"shop",
|
||||
]
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
ROOT_URLCONF = "client_demo.urls"
|
||||
TEMPLATES = [{{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {{"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]}},
|
||||
}}]
|
||||
WSGI_APPLICATION = "client_demo.wsgi.application"
|
||||
DATABASES = {{"default": {{"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}}}
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
STATIC_URL = "static/"
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
DEFAULT_FROM_EMAIL = "no-reply@example.com"
|
||||
BUSINESS_NAME = {site.business_name!r}
|
||||
BUSINESS_PHONE = {site.business_phone!r}
|
||||
BUSINESS_ADDRESS = {site.business_address!r}
|
||||
"""
|
||||
|
||||
def _django_urls_py(self) -> str:
|
||||
return """from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("shop.urls")),
|
||||
]
|
||||
"""
|
||||
|
||||
def _django_wsgi_py(self) -> str:
|
||||
return """import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings")
|
||||
application = get_wsgi_application()
|
||||
"""
|
||||
|
||||
def _shop_apps_py(self) -> str:
|
||||
return """from django.apps import AppConfig
|
||||
|
||||
class ShopConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "shop"
|
||||
"""
|
||||
|
||||
def _shop_models_py(self) -> str:
|
||||
return """from django.db import models
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=120)
|
||||
description = models.TextField(blank=True)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
"""
|
||||
|
||||
def _shop_forms_py(self) -> str:
|
||||
return """from django import forms
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
name = forms.CharField(max_length=100)
|
||||
email = forms.EmailField()
|
||||
message = forms.CharField(widget=forms.Textarea(attrs={"rows": 5}))
|
||||
"""
|
||||
|
||||
def _shop_urls_py(self) -> str:
|
||||
return """from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.home, name="home"),
|
||||
path("products/", views.products, name="products"),
|
||||
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
||||
path("cart/", views.cart, name="cart"),
|
||||
path("contact/", views.contact, name="contact"),
|
||||
]
|
||||
"""
|
||||
|
||||
def _shop_views_py(self, ai_payload: dict) -> str:
|
||||
headline = ai_payload.get("hero_headline", "Welcome")
|
||||
subheadline = ai_payload.get("hero_subheadline", "")
|
||||
return f"""from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.mail import send_mail
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from .forms import ContactForm
|
||||
from .models import Product
|
||||
|
||||
def home(request):
|
||||
return render(request, "shop/home.html", {{
|
||||
"headline": {headline!r},
|
||||
"subheadline": {subheadline!r},
|
||||
}})
|
||||
|
||||
def products(request):
|
||||
return render(request, "shop/products.html", {{"products": Product.objects.filter(is_active=True)}})
|
||||
|
||||
def add_to_cart(request, product_id):
|
||||
product = get_object_or_404(Product, id=product_id, is_active=True)
|
||||
cart_data = request.session.get("cart", {{}})
|
||||
key = str(product.id)
|
||||
cart_data[key] = cart_data.get(key, 0) + 1
|
||||
request.session["cart"] = cart_data
|
||||
messages.success(request, f"Added {{product.name}} to cart.")
|
||||
return redirect("products")
|
||||
|
||||
def cart(request):
|
||||
cart_data = request.session.get("cart", {{}})
|
||||
items = []
|
||||
total = 0
|
||||
for product_id, qty in cart_data.items():
|
||||
product = Product.objects.filter(id=product_id).first()
|
||||
if not product:
|
||||
continue
|
||||
line_total = product.price * qty
|
||||
items.append({{"product": product, "qty": qty, "line_total": line_total}})
|
||||
total += line_total
|
||||
return render(request, "shop/cart.html", {{"items": items, "total": total}})
|
||||
|
||||
def contact(request):
|
||||
if request.method == "POST":
|
||||
form = ContactForm(request.POST)
|
||||
if form.is_valid():
|
||||
subject = f"Contact request for {{settings.BUSINESS_NAME}}"
|
||||
body = (
|
||||
f"Name: {{form.cleaned_data['name']}}\\n"
|
||||
f"Email: {{form.cleaned_data['email']}}\\n\\n"
|
||||
f"Message:\\n{{form.cleaned_data['message']}}"
|
||||
)
|
||||
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [settings.DEFAULT_FROM_EMAIL])
|
||||
messages.success(request, "Message sent.")
|
||||
return redirect("contact")
|
||||
else:
|
||||
form = ContactForm()
|
||||
return render(request, "shop/contact.html", {{"form": form}})
|
||||
"""
|
||||
|
||||
def _shop_admin_py(self) -> str:
|
||||
return """from django.contrib import admin
|
||||
from .models import Product
|
||||
|
||||
admin.site.register(Product)
|
||||
"""
|
||||
|
||||
def _shop_base_template(self, site: ClientSite) -> str:
|
||||
return f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{html.escape(site.business_name)} Demo</title>
|
||||
<style>
|
||||
body{{font-family:Arial,sans-serif;margin:0;background:#f5f6fb;color:#1f2937}}
|
||||
nav{{background:#111827;padding:1rem;display:flex;gap:1rem}}
|
||||
nav a{{color:white;text-decoration:none}}
|
||||
.wrap{{max-width:960px;margin:0 auto;padding:2rem 1rem}}
|
||||
.card{{background:white;padding:1rem;border-radius:8px;margin-bottom:1rem}}
|
||||
.btn{{display:inline-block;background:#2563eb;color:white;padding:.5rem .75rem;border-radius:6px;text-decoration:none;border:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/products/">Products</a>
|
||||
<a href="/cart/">Cart</a>
|
||||
<a href="/contact/">Contact</a>
|
||||
</nav>
|
||||
<div class="wrap">
|
||||
{{% if messages %}}
|
||||
{{% for message in messages %}}<p class="card">{{{{ message }}}}</p>{{% endfor %}}
|
||||
{{% endif %}}
|
||||
{{% block content %}}{{% endblock %}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def _shop_home_template(self) -> str:
|
||||
return """{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="card">
|
||||
<h1>{{ headline }}</h1>
|
||||
<p>{{ subheadline }}</p>
|
||||
<a class="btn" href="/products/">Shop now</a>
|
||||
</section>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
def _shop_products_template(self) -> str:
|
||||
return """{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Products</h1>
|
||||
{% for product in products %}
|
||||
<article class="card">
|
||||
<h3>{{ product.name }}</h3>
|
||||
<p>{{ product.description }}</p>
|
||||
<p><strong>${{ product.price }}</strong></p>
|
||||
<a class="btn" href="/cart/add/{{ product.id }}/">Add to cart</a>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p class="card">No products yet. Add from admin panel.</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
def _shop_cart_template(self) -> str:
|
||||
return """{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Your Cart</h1>
|
||||
{% for item in items %}
|
||||
<div class="card">
|
||||
<strong>{{ item.product.name }}</strong> x {{ item.qty }} = ${{ item.line_total }}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="card">Cart empty.</p>
|
||||
{% endfor %}
|
||||
<p><strong>Total: ${{ total }}</strong></p>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
def _shop_contact_template(self) -> str:
|
||||
return """{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Contact</h1>
|
||||
<form method="post" class="card">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="btn" type="submit">Send message</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
def _django_readme(self, project_slug: str) -> str:
|
||||
return f"""# {project_slug}
|
||||
|
||||
Generated Django demo storefront.
|
||||
|
||||
## Run
|
||||
|
||||
1. pip install django
|
||||
2. python manage.py migrate
|
||||
3. python manage.py createsuperuser
|
||||
4. python manage.py runserver
|
||||
|
||||
## Basic static site
|
||||
|
||||
Download the basic HTML package from the builder dashboard for the matching template design.
|
||||
"""
|
||||
139
builder/template_registry.py
Normal file
139
builder/template_registry.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Client site template folders under project templates/template_*."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
CLIENT_TEMPLATES_ROOT = Path(settings.BASE_DIR) / "templates"
|
||||
|
||||
COPY_EXCLUDE_DIR_NAMES = frozenset(
|
||||
{
|
||||
"__MACOSX",
|
||||
"Documentation",
|
||||
"documentation",
|
||||
"node_modules",
|
||||
".git",
|
||||
".next",
|
||||
".gitignore",
|
||||
}
|
||||
)
|
||||
|
||||
# Per-folder config: site path relative to templates/template_N/
|
||||
TEMPLATE_FOLDER_CONFIG: dict[str, dict] = {
|
||||
"template_1": {
|
||||
"name": "NeuralSync",
|
||||
"description": "AI SaaS marketing template with multi-page layout.",
|
||||
"site_path": "Main File/NeuralSync",
|
||||
"kind": "static_html",
|
||||
"supports_basic": True,
|
||||
"supports_django": True,
|
||||
"brand_tokens": ["NeuralSync"],
|
||||
"hero_strings": ["Transform Your Business with AI Powered Solutions"],
|
||||
},
|
||||
"template_2": {
|
||||
"name": "Eventio Conference",
|
||||
"description": "Next.js event and conference site (requires npm to run).",
|
||||
"site_path": "eventio/eventio-HTML",
|
||||
"kind": "nextjs",
|
||||
"supports_basic": True,
|
||||
"supports_django": True,
|
||||
"brand_tokens": ["Eventio", "EVENTIO"],
|
||||
"hero_strings": [
|
||||
"Global Business Conference",
|
||||
"San Francisco, California",
|
||||
],
|
||||
},
|
||||
"template_3": {
|
||||
"name": "Waves Business",
|
||||
"description": "Multi-layout business one-page template pack.",
|
||||
"site_path": "site",
|
||||
"kind": "static_html",
|
||||
"supports_basic": True,
|
||||
"supports_django": True,
|
||||
"brand_tokens": ["Waves", "Welcome to Waves"],
|
||||
"hero_strings": ["Welcome to Waves", "A perfect template for both corporate and creative projects."],
|
||||
},
|
||||
"template_4": {
|
||||
"name": "Multipurpose Business",
|
||||
"description": "Bootstrap business landing template with services and pricing.",
|
||||
"site_path": "site",
|
||||
"kind": "static_html",
|
||||
"supports_basic": True,
|
||||
"supports_django": True,
|
||||
"brand_tokens": ["Business template", "multipurpose business template"],
|
||||
"hero_strings": ["Business template"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _auto_detect_site_path(folder: Path) -> str | None:
|
||||
site_dir = folder / "site"
|
||||
if (site_dir / "index.html").exists():
|
||||
return "site"
|
||||
|
||||
candidates: list[tuple[int, str]] = []
|
||||
for index_file in folder.rglob("index.html"):
|
||||
parts_lower = {part.lower() for part in index_file.parts}
|
||||
if "documentation" in parts_lower:
|
||||
continue
|
||||
rel_parent = index_file.parent.relative_to(folder)
|
||||
depth = len(rel_parent.parts)
|
||||
candidates.append((depth, str(rel_parent)))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda item: (item[0], len(item[1])))
|
||||
return candidates[0][1]
|
||||
|
||||
|
||||
def discover_client_templates() -> list[dict]:
|
||||
definitions: list[dict] = []
|
||||
|
||||
for folder in sorted(CLIENT_TEMPLATES_ROOT.glob("template_*")):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
|
||||
folder_key = folder.name
|
||||
config = TEMPLATE_FOLDER_CONFIG.get(folder_key, {})
|
||||
site_path = config.get("site_path") or _auto_detect_site_path(folder)
|
||||
if not site_path:
|
||||
continue
|
||||
|
||||
source = CLIENT_TEMPLATES_ROOT / folder_key / site_path
|
||||
if not source.exists():
|
||||
continue
|
||||
|
||||
slug = folder_key.replace("_", "-")
|
||||
definitions.append(
|
||||
{
|
||||
"slug": slug,
|
||||
"name": config.get("name", folder_key.replace("_", " ").title()),
|
||||
"description": config.get(
|
||||
"description",
|
||||
f"Client template from templates/{folder_key}/",
|
||||
),
|
||||
"source_folder": str(Path("templates") / folder_key / site_path),
|
||||
"kind": config.get("kind", "static_html"),
|
||||
"supports_basic": config.get("supports_basic", True),
|
||||
"supports_django": config.get("supports_django", True),
|
||||
"brand_tokens": config.get("brand_tokens", []),
|
||||
"hero_strings": config.get("hero_strings", []),
|
||||
"is_active": True,
|
||||
}
|
||||
)
|
||||
|
||||
return definitions
|
||||
|
||||
|
||||
def get_template_config(slug: str) -> dict | None:
|
||||
for item in discover_client_templates():
|
||||
if item["slug"] == slug:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def resolve_source_path(source_folder: str) -> Path:
|
||||
return Path(settings.BASE_DIR) / source_folder
|
||||
11
builder/urls.py
Normal file
11
builder/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.dashboard_view, name="dashboard"),
|
||||
path("create/", views.create_site_view, name="create_site"),
|
||||
path("sites/<uuid:site_id>/", views.site_detail_view, name="site_detail"),
|
||||
path("sites/<uuid:site_id>/download/basic/", views.download_basic_site, name="download_basic_site"),
|
||||
path("sites/<uuid:site_id>/download/django/", views.download_django_site, name="download_django_site"),
|
||||
]
|
||||
95
builder/views.py
Normal file
95
builder/views.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import FileResponse, Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .forms import ClientSiteForm, SignUpForm
|
||||
from .models import ClientSite, TemplateOption
|
||||
from .services import SiteGenerator
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
template_name = "home.html"
|
||||
|
||||
|
||||
def signup_view(request: HttpRequest) -> HttpResponse:
|
||||
if request.user.is_authenticated:
|
||||
return redirect("dashboard")
|
||||
if request.method == "POST":
|
||||
form = SignUpForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
form = SignUpForm()
|
||||
return render(request, "registration/signup.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_view(request: HttpRequest) -> HttpResponse:
|
||||
sites = ClientSite.objects.filter(owner=request.user).select_related("template_option")
|
||||
form = ClientSiteForm()
|
||||
form.fields["template_option"].queryset = TemplateOption.objects.filter(is_active=True)
|
||||
return render(request, "builder/dashboard.html", {"sites": sites, "form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
def create_site_view(request: HttpRequest) -> HttpResponse:
|
||||
if request.method != "POST":
|
||||
return redirect("dashboard")
|
||||
|
||||
form = ClientSiteForm(request.POST)
|
||||
form.fields["template_option"].queryset = TemplateOption.objects.filter(is_active=True)
|
||||
if not form.is_valid():
|
||||
sites = ClientSite.objects.filter(owner=request.user).select_related("template_option")
|
||||
return render(request, "builder/dashboard.html", {"sites": sites, "form": form})
|
||||
|
||||
site = form.save(commit=False)
|
||||
site.owner = request.user
|
||||
site.status = ClientSite.Status.GENERATING
|
||||
site.save()
|
||||
|
||||
generator = SiteGenerator()
|
||||
try:
|
||||
generator.build_all(site)
|
||||
messages.success(request, "Site generated successfully.")
|
||||
except Exception as exc:
|
||||
site.status = ClientSite.Status.FAILED
|
||||
site.last_error = str(exc)
|
||||
site.save(update_fields=["status", "last_error", "updated_at"])
|
||||
messages.error(request, "Generation failed. Check site detail for error info.")
|
||||
|
||||
return redirect("site_detail", site_id=site.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def site_detail_view(request: HttpRequest, site_id) -> HttpResponse:
|
||||
site = get_object_or_404(ClientSite, id=site_id, owner=request.user)
|
||||
return render(request, "builder/site_detail.html", {"site": site})
|
||||
|
||||
|
||||
def _download_site_file(site: ClientSite, relative_path: str) -> FileResponse:
|
||||
if not relative_path:
|
||||
raise Http404("File not generated yet.")
|
||||
full_path = Path(settings.MEDIA_ROOT) / relative_path
|
||||
if not full_path.exists():
|
||||
raise Http404("File missing from storage.")
|
||||
return FileResponse(full_path.open("rb"), as_attachment=True, filename=full_path.name)
|
||||
|
||||
|
||||
@login_required
|
||||
def download_basic_site(request: HttpRequest, site_id) -> FileResponse:
|
||||
site = get_object_or_404(ClientSite, id=site_id, owner=request.user)
|
||||
return _download_site_file(site, site.basic_zip_path)
|
||||
|
||||
|
||||
@login_required
|
||||
def download_django_site(request: HttpRequest, site_id) -> FileResponse:
|
||||
site = get_object_or_404(ClientSite, id=site_id, owner=request.user)
|
||||
return _download_site_file(site, site.django_zip_path)
|
||||
Reference in New Issue
Block a user