learning-website-django1-1 Exercise 1: Create the Project Layout ================================================================== Create the Django project layout from this chapter as real files, then prove it works. The script writes 25 files: manage.py, a config package with a settings package (base, dev, prod), the site map, four apps (core, content, theme, navigation), requirements files and a .gitignore. Create a virtual environment and install Django first: python -m venv venv venv\Scripts\activate (Linux: source venv/bin/activate) pip install "Django>=6.1,<6.2" (On Windows, create the venv in a SHORT path such as C:\lw. Django's installer failed on a very long folder name, because of the 260-character path limit.) Save as make_skeleton.py and run it with the project folder as its argument: python make_skeleton.py learning-site """make_skeleton.py Creates the Learning Website Django project layout (Chapter 1) as real, runnable files.""" import os, sys, textwrap target = sys.argv[1] FILES = {} FILES["manage.py"] = '''\ #!/usr/bin/env python import os import sys def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) if __name__ == "__main__": main() ''' FILES["config/__init__.py"] = "" FILES["config/sites_config.py"] = '''\ """The site map: the ONE place that says which site exists and which content folders it serves. It is the same map as Learning Website: Framework & Architecture 2.""" DOMAIN = "osztromok.com" SITES = { "languages": {"title": "Languages", "folders": ["france", "germany", "hungary", "japan", "culture"]}, "webdevelopment": {"title": "Web Development", "folders": ["web-development", "web-platforms", "web-servers"]}, "programming": {"title": "Programming", "folders": ["programming", "software-development", "maths-for-programmers", "retro-computing", "game-development", "android-development", "ios-development", "databases", "blockchain-and-web3", "developer-tools", "projects"]}, "systems": {"title": "Systems", "folders": ["linux", "operating-systems", "networking", "cloud-and-devops", "security", "technical-support", "raspberry-pi", "windows"]}, "ai": {"title": "AI and Data", "folders": ["ai", "data-science-and-ml"]}, "humanities": {"title": "Humanities", "folders": ["history", "politics", "philosophy", "art-history", "classic-literature", "science-fiction", "sci-fi-project", "music", "football", "ethics", "study-methodologies", "science"]}, "lifeskills": {"title": "Life Skills", "folders": ["practical-life-skills", "cookery", "freelancing-and-business-skills", "youtube"]}, "creative": {"title": "Creative Tools", "folders": ["creative-and-design-tools", "audio-video-production", "office-and-productivity-software"]}, } def site_hosts(env, only=None): """Host names a process may answer to. env="prod": languages.osztromok.com, ... env="dev": languages.localhost, ... only=: pin the process to that single site (one process per site is possible).""" if only is not None and only not in SITES: raise ValueError(f"unknown site: {only!r}") names = [only] if only else list(SITES) suffix = DOMAIN if env == "prod" else "localhost" return [f"{name}.{suffix}" for name in names] ''' FILES["config/settings/__init__.py"] = "" FILES["config/settings/base.py"] = '''\ """Settings shared by every environment. dev.py and prod.py import * from here.""" import os from pathlib import Path from config.sites_config import SITES BASE_DIR = Path(__file__).resolve().parent.parent.parent # Where the course content lives (the site's "database" is a folder of files). CONTENT_ROOT = Path(os.environ.get("LW_CONTENT_ROOT", BASE_DIR / "content")) # Pin this process to ONE site (for example LW_SITE=languages), or leave unset to serve all sites. PINNED_SITE = os.environ.get("LW_SITE") or None if PINNED_SITE is not None and PINNED_SITE not in SITES: raise RuntimeError(f"LW_SITE={PINNED_SITE!r} is not one of {sorted(SITES)}") INSTALLED_APPS = [ "django.contrib.staticfiles", "apps.core", "apps.content", "apps.theme", "apps.navigation", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "config.urls" TEMPLATES = [{ "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": {"context_processors": ["django.template.context_processors.request"]}, }] DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}} STATIC_URL = "static/" STATICFILES_DIRS = [BASE_DIR / "static"] STATIC_ROOT = BASE_DIR / "staticfiles" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" USE_TZ = True LANGUAGE_CODE = "en-gb" # One login per site by default: the cookie is NOT shared across subdomains (see Chapter 9 of the # framework course). Set SESSION_COOKIE_DOMAIN only if you decide on a shared login. SESSION_COOKIE_DOMAIN = None ''' FILES["config/settings/dev.py"] = '''\ from .base import * # noqa: F401,F403 from config.sites_config import site_hosts DEBUG = True SECRET_KEY = "dev-only-not-secret" ALLOWED_HOSTS = site_hosts("dev", PINNED_SITE) # languages.localhost, ... (no wildcard) ''' FILES["config/settings/prod.py"] = '''\ import os from .base import * # noqa: F401,F403 from config.sites_config import site_hosts DEBUG = False SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # required: no default in production ALLOWED_HOSTS = site_hosts("prod", PINNED_SITE) CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] # the scheme is required SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Apache terminates HTTPS SECURE_SSL_REDIRECT = True # belt and braces: Apache already redirects HTTP to HTTPS SECURE_HSTS_SECONDS = 3600 # start small: browsers remember HSTS. Raise it step by step. ''' FILES["config/urls.py"] = '''\ from django.http import HttpResponse from django.urls import path def placeholder(request): return HttpResponse("Learning Website: skeleton") urlpatterns = [path("", placeholder)] ''' FILES["config/wsgi.py"] = '''\ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.prod") application = get_wsgi_application() ''' for app, label in [("core", "Shared helpers and middleware (Chapter 2)"), ("content", "Pages imported from the content folder (Chapters 3 and 4)"), ("theme", "Shared templates, static files and design tokens (Chapter 5)"), ("navigation", "Menus and breadcrumbs (Chapter 6)")]: FILES[f"apps/{app}/__init__.py"] = "" FILES[f"apps/{app}/apps.py"] = ( "from django.apps import AppConfig\n\n\n" f"class {app.capitalize()}Config(AppConfig):\n" f" name = \"apps.{app}\"\n" f" verbose_name = \"{label}\"\n") FILES["apps/__init__.py"] = "" FILES["templates/.gitkeep"] = "" FILES["static/.gitkeep"] = "" FILES["tests/__init__.py"] = "" FILES["requirements/base.txt"] = "Django>=6.1,<6.2\n" FILES["requirements/dev.txt"] = "-r base.txt\n" FILES["requirements/prod.txt"] = "-r base.txt\ngunicorn\n" FILES[".gitignore"] = "db.sqlite3\nstaticfiles/\n__pycache__/\n.env\n*.pyc\n" for rel, text in FILES.items(): path = os.path.join(target, *rel.split("/")) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8", newline="\n") as fh: fh.write(text) print(f"created {len(FILES)} files in {target}") Check it (from inside the new folder): python manage.py check set LW_SITE=languages (Linux: export LW_SITE=languages) then python manage.py check set LW_SITE=nonsense then python manage.py check Output (checked by running it with Django 6.1.2): python manage.py check System check identified no issues (0 silenced). with LW_SITE=languages System check identified no issues (0 silenced). with LW_SITE=nonsense (last line only) RuntimeError: LW_SITE='nonsense' is not one of ['ai', 'creative', 'humanities', 'languages', 'lifesk The files created: .gitignore manage.py apps/__init__.py apps/content/__init__.py apps/content/apps.py apps/core/__init__.py apps/core/apps.py apps/navigation/__init__.py apps/navigation/apps.py apps/theme/__init__.py apps/theme/apps.py config/__init__.py config/sites_config.py config/urls.py config/wsgi.py config/settings/__init__.py config/settings/base.py config/settings/dev.py config/settings/prod.py requirements/base.txt requirements/dev.txt requirements/prod.txt static/.gitkeep templates/.gitkeep tests/__init__.py Why each piece is where it is ----------------------------- - config/sites_config.py is the ONE place that lists the sites and their folders (the map from Learning Website: Framework & Architecture 2). Settings, menus, redirects and sitemaps will all read it. - config/settings is a package: base.py is shared, dev.py and prod.py only add what differs. prod.py has no default secret key, so it refuses to start without one. - LW_SITE lets one process serve every site (the default) or be pinned to a single site, so you can start with one process and split later without changing code. - Every site-independent piece of code lives under apps/, one app per job, and each app is named for the chapter that fills it in. WHY THIS WORKS AS AN ANSWER --------------------------- A layout is only a good decision if it runs. The script makes the project reproducible, and the three checks show that the settings load, that pinning works, and that a typo in a site name stops the start-up instead of serving the wrong site.