710 lines
25 KiB
Python
710 lines
25 KiB
Python
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.
|
|
"""
|