Challenge 1: Cache an Expensive Computation — Possible Solution ==================================================================== # catalog/utils.py from django.db.models import Avg, Count from .models import Book def get_catalog_stats(): return Book.objects.aggregate( avg_price=Avg("price"), total_books=Count("id"), ) # catalog/views.py from django.core.cache import cache from django.shortcuts import render from .utils import get_catalog_stats def catalog_overview(request): stats = cache.get_or_set("book_stats", get_catalog_stats, timeout=600) return render(request, "catalog/overview.html", {"stats": stats}) WHY THIS WORKS -------------- - get_catalog_stats() is a plain function with no arguments — this is required for cache.get_or_set() to be able to call it as a "compute the value" callback if the cache doesn't already have a fresh entry. - cache.get_or_set("book_stats", get_catalog_stats, timeout=600) checks for an existing "book_stats" key first; if it's missing or expired, it calls get_catalog_stats() ONCE, stores the result under that key for 600 seconds (10 minutes), and returns it — all in a single call, rather than writing a separate get()-then-if-None-then-set() sequence by hand. - Within that 10-minute window, every subsequent call to cache.get_or_set("book_stats", ...) returns the cached dict instantly without running the aggregate() query again — the expensive database aggregation only actually executes roughly once every 10 minutes, regardless of how many requests hit catalog_overview in between. - Because get_catalog_stats is passed as a reference (not called as get_catalog_stats()), get_or_set() only invokes it when actually needed — it isn't run eagerly on every request just to check if its result should be discarded.