inital commit
This commit is contained in:
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.
|
||||
"""
|
||||
Reference in New Issue
Block a user