[
  {
    "path": "!README_FOR_PRODUCTION.txt",
    "content": "Note for production editor about page-breaking: the author has requested a global solution to keep code blocks from breaking across pages in the PDF, so there is CSS in place to that effect.\n"
  },
  {
    "path": ".dockerignore",
    "content": ".venv\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# matt hacker bulk edit for prod\n2cb0ed8c264cee682303288ba5a5cea80956fb8d\n2735e383f1281c5f200e64cfb7cda0457cfe8d1e\n6fce793a9a4d701313684de11ca2cd3f5e89a041\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "---\nname: Book tests\n\non:\n  schedule:\n    - cron: \"45 15 * * *\"\n  push:\n    branches:\n      main\n  pull_request:\n\njobs:\n  chapter-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        test_chapter: [\n          test_chapter_01,\n          test_chapter_02_unittest,\n          test_chapter_03_unit_test_first_view,\n          test_chapter_04_philosophy_and_refactoring,\n          test_chapter_05_post_and_database,\n          test_chapter_06_explicit_waits_1,\n          test_chapter_07_working_incrementally,\n          test_chapter_08_prettification,\n          test_chapter_09_docker,\n          test_chapter_10_production_readiness,\n          test_chapter_11_server_prep,\n          test_chapter_12_ansible,\n          test_chapter_13_organising_test_files,\n          test_chapter_14_database_layer_validation,\n          test_chapter_15_simple_form,\n          test_chapter_16_advanced_forms,\n          test_chapter_17_javascript,\n          test_chapter_19_spiking_custom_auth,\n          test_chapter_20_mocking_1,\n          test_chapter_21_mocking_2,\n          test_chapter_22_fixtures_and_wait_decorator,\n          test_chapter_23_debugging_prod,\n          test_chapter_24_outside_in,\n          test_chapter_25_CI,\n          test_chapter_26_page_pattern,\n        ]\n\n    env:\n      PY_COLORS: \"1\"  # enable coloured output in pytest\n      EMAIL_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: checkout submodules\n        shell: bash\n        run: |\n          sed -i 's_git@github.com:_https://github.com/_' .gitmodules\n          git submodule init\n          git submodule status | cut -d\" \" -f2 | xargs -n1 -P0 git submodule update\n\n      - name: setup Git\n        shell: bash\n        run: |\n          git config --global user.email \"elspeth@example.com\"\n          git config --global user.name \"Elspeth See-Eye\"\n          git config --global init.defaultBranch main\n\n      - name: Set up Python 3.14\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.14'\n\n      - name: Install apt stuff and other dependencies\n        shell: bash\n        run: |\n          sudo apt remove -y --purge firefox\n          sudo add-apt-repository ppa:mozillateam/ppa\n          sudo apt update -y\n          sudo apt install -y \\\n            asciidoctor \\\n            language-pack-en \\\n            ruby-coderay \\\n            ruby-pygments.rb \\\n            firefox-esr \\\n            tree\n          # fix failed to install firefox bin/symlink\n          which firefox || sudo ln -s /usr/bin/firefox-esr /usr/bin/firefox\n          # remove old geckodriver\n          which geckodriver && sudo rm $(which geckodriver) || exit 0\n          pip install uv\n\n      - name: Install Python requirements.txt globally\n        shell: bash\n        run: |\n          uv pip install --system  .\n\n      - name: Install Python requirements.txt into virtualenv\n        shell: bash\n        run: |\n          make .venv/bin\n\n      - name: Display firefox version\n        shell: bash\n        run: |\n          apt show firefox-esr\n          dpkg -L firefox-esr\n          firefox --version\n          which geckodriver && geckodriver --version || exit 0\n\n      - name: Run chapter test\n        shell: bash\n        run: |\n          make  ${{ matrix.test_chapter }}\n\n      - name: Save tempdir path to an env var\n        if: always()\n        shell: bash\n        run: |\n          TMPDIR_PATH=$(cat .tmpdir.${{ matrix.test_chapter }})\n          echo \"TMPDIR_PATH=$TMPDIR_PATH\" >> $GITHUB_ENV\n\n      - name: Archive the temp dir\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: test-source-${{ matrix.test_chapter }}\n          path: ${{ env.TMPDIR_PATH }}\n\n      - name: Archive the built html files\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: built-html-${{ matrix.test_chapter }}\n          path: |\n            *.html\n            *.css\n\n\n  other-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install apt stuff and other dependencies\n        shell: bash\n        run: |\n          sudo apt update -y\n          sudo apt install -y \\\n            asciidoctor \\\n            language-pack-en \\\n            ruby-coderay \\\n            ruby-pygments.rb \\\n            tree \\\n            libxml2-utils\n          pip install uv\n\n      - name: Install Python requirements.txt into virtualenv\n        shell: bash\n        run: |\n          make .venv/bin\n\n      - name: setup Git\n        shell: bash\n        run: |\n          git config --global user.email \"elspeth@example.com\"\n          git config --global user.name \"Elspeth See-Eye\"\n          git config --global init.defaultBranch main\n\n      - name: prep tests submodule\n        shell: bash\n        run: |\n          sed -i 's_git@github.com:_https://github.com/_' .gitmodules\n          git submodule init\n          git submodule update tests/testrepo\n\n      - name: Run unit tests\n        shell: bash\n        run: |\n          make unit-test\n\n      - name: Run xml linter\n        shell: bash\n        run: |\n          make xmllint_book\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n.cache\n.vagrant\n*-cloudimg-console.log\n.venv\n.pytest_cache\n\n/chapter_*.html\n/appendix_*.html\n/part*.html\n/outline_and_future*.html\n/pre-requisite-installations.html\n/preface.html\n/epilogue.html\n/bibliography.html\n/acknowledgments.html\n/video_plug.html\n/part*.forbook.asciidoc\n/praise.forbook.html\n/ai_preface.html\n\n/misc/abandoned_roman_numerals_example\n/docs/atlas_docs/\n/proposals\n/tags\n/pdf_drafts\n/pycon\n/source/*/static\n/source/*/database\n/wordcounts.*\n/feedback\n/downloads/*.js\n/downloads/mock*\n/misc/promo/\n/misc/Vagrantfile\n/misc/superlists-repo-django16-backup.zip\n/tdd-tutorial-materials\n/misc/Invoice-Percival-1\n/video\n/misc/*conference_report.md\n/tests/.cache/\n/workshops/js-testing-with-jasmine.html\n/tech review/\n.vagrant.d\n*.egg-info\n.tmpdir.*\n.env\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"source/chapter_01/superlists\"]\n\tpath = source/chapter_01/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_02/superlists\"]\n\tpath = source/chapter_02_unittest/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_03/superlists\"]\n\tpath = source/chapter_03_unit_test_first_view/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_04/superlists\"]\n\tpath = source/chapter_04_philosophy_and_refactoring/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_05/superlists\"]\n\tpath = source/chapter_05_post_and_database/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_06/superlists\"]\n\tpath = source/chapter_06_explicit_waits_1/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_07/superlists\"]\n\tpath = source/chapter_07_working_incrementally/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_08/superlists\"]\n\tpath = source/chapter_08_prettification/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_09/superlists\"]\n\tpath = source/chapter_09_docker/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_10_production_readiness/superlists\"]\n\tpath = source/chapter_10_production_readiness/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_11_server_prep/superlists\"]\n\tpath = source/chapter_11_server_prep/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_12_ansible/superlists\"]\n\tpath = source/chapter_12_ansible/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_13_organising_test_files/superlists\"]\n\tpath = source/chapter_13_organising_test_files/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_13/superlists\"]\n\tpath = source/chapter_14_database_layer_validation/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_14/superlists\"]\n\tpath = source/chapter_15_simple_form/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_15/superlists\"]\n\tpath = source/chapter_16_advanced_forms/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_16/superlists\"]\n\tpath = source/chapter_17_javascript/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_17/superlists\"]\n\tpath = source/chapter_18_second_deploy/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_18/superlists\"]\n\tpath = source/chapter_19_spiking_custom_auth/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_19/superlists\"]\n\tpath = source/chapter_20_mocking_1/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_21_mocking_2/superlists\"]\n\tpath = source/chapter_21_mocking_2/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_22_fixtures_and_wait_decorator/superlists\"]\n\tpath = source/chapter_22_fixtures_and_wait_decorator/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_21/superlists\"]\n\tpath = source/chapter_23_debugging_prod/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_24_outside_in/superlists\"]\n\tpath = source/chapter_24_outside_in/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_25_CI/superlists\"]\n\tpath = source/chapter_25_CI/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/chapter_26_page_pattern/superlists\"]\n\tpath = source/chapter_26_page_pattern/superlists\n\turl = git@github.com:hjwp/book-example.git\n\n[submodule \"tests/testrepo\"]\n    path = tests/testrepo\n    url = git@github.com:hjwp/booktesttestrepo.git\n\n[submodule \"source/appendix_II/superlists\"]\n\tpath = source/appendix_Django_Class-Based_Views/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/appendix_III/superlists\"]\n\tpath = source/appendix_III/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/appendix_bdd/superlists\"]\n\tpath = source/appendix_bdd/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/appendix_VI_rest_api_backend/superlists\"]\n\tpath = source/appendix_rest_api/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/appendix_VIII_DjangoRestFramework/superlists\"]\n\tpath = source/appendix_DjangoRestFramework/superlists\n\turl = git@github.com:hjwp/book-example.git\n[submodule \"source/appendix_purist_unit_tests/superlists\"]\n\tpath = source/appendix_purist_unit_tests/superlists\n\turl = git@github.com:hjwp/book-example.git\n"
  },
  {
    "path": ".python-version",
    "content": "3.14\n"
  },
  {
    "path": "CITATION.md",
    "content": "Bibtex:\n```TeX\n@BOOK{percival:tdd:python,\n    AUTHOR       = \"{Harry J.W.} Percival\",\n    TITLE        = \"Test-Driven Development with Python\",\n    SUBTITLE     = \"Obey the Testing Goat!\",\n    DATE         = \"2014\",\n    PUBLISHER    = \"O'Reilly Media, Inc.\",\n    ISBN         = \"9781449365141\"\n}\n```\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:slim\n\n# -- WIP --\n# this dockerfile is a work in progress,\n# the vague intention is to use it for CI.\n\n# RUN add-apt-repository ppa:mozillateam/ppa && \\\nRUN apt-get update -y\n\nRUN apt-get install -y \\\n    git \\\n    asciidoctor \\\n    # language-pack-en \\\n    ruby-pygments.rb \\\n    firefox-esr \\\n    tree \\\n    locales \\\n    vim\n\nRUN apt-get install -y \\\n    make \\\n    curl\n\nRUN locale-gen en_GB.UTF-8\n# RUN pip install uv\nADD --chmod=755 https://astral.sh/uv/install.sh /install.sh\nRUN /install.sh && rm /install.sh\nRUN ln -s $HOME/.local/bin/uv /usr/bin/uv\n\nRUN git config --global user.email \"elspeth@example.com\" && \\\n    git config --global user.name \"Elspeth See-Eye\" && \\\n    git config --global init.defaultBranch main\n\nWORKDIR /app\nRUN uv venv .venv\n\nCOPY pyproject.toml pyproject.toml\nRUN uv pip install .\nRUN uv pip install selenium\nENV PATH=\".venv/bin:$PATH\"\n\n\nCMD bash\n"
  },
  {
    "path": "ER_sampleTOC.html",
    "content": "<section data-type=\"preface\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<h1>Brief Table of Contents (<em>Not Yet Final</em>)</h1>\n\n<p>Preface (AVAILABLE)</p>\n\n<p>Prerequisites and Assumptions (AVAILABLE)</p>\n\n<p>Companion Video (AVAILABLE)</p>\n\n<p><em>Acknowledgments (UNAVAILABLE)</em></p>\n\n<p>Part 1: The Basics of TDD and Django (AVAILABLE)</p>\n\n<p>Chapter 1: Getting Django Set Up Using a Functional Test (AVAILABLE)</p>\n\n<p>Chapter 2: Extending Our Functional Test Using the unittest Module (AVAILABLE)</p>\n\n<p>Chapter 3: Testing a Simple Home Page with Unit Tests (AVAILABLE)</p>\n\n<p>Chapter 4: What Are We Doing with All These Tests? (And, Refactoring) (AVAILABLE)</p>\n\n<p>Chapter 5: Saving User Input: Testing the Database (AVAILABLE)</p>\n\n<p>Chapter 6: Improving Functional Tests: Ensuring Isolation and Removing Voodoo Sleeps (AVAILABLE)</p>\n\n<p>Chapter 7: Working Incrementally (AVAILABLE)</p>\n\n<p>Part 2: Web Development Sine Qua Nons (AVAILABLE)</p>\n\n<p>Chapter 8: Prettification: Layout and Styling, and What to Test About It (AVAILABLE)</p>\n\n<p>Chapter 9: Containerization akaDocker (AVAILABLE)</p>\n\n<p>Chapter 10: Making our App Production-Ready (AVAILABLE)</p>\n\n<p>Chapter 11: Getting A Server Ready for Deployment (AVAILABLE)</p>\n\n<p>Chapter 12: Infrastructure As Code: Automated Deployments With Ansible (AVAILABLE)</p>\n\n<p>Chapter 13: Splitting Our Tests into Multiple Files, and a Generic\nWait Helper (AVAILABLE)</p>\n\n<p>Chapter 14: Validation at the Database Layer (AVAILABLE)</p>\n\n<p>Chapter 15: A Simple Form (AVAILABLE)</p>\n\n<p>Chapter 16: More Advanced Forms (AVAILABLE)</p>\n\n<p>Chapter 17: A Gentle Excursion into JavaScript (AVAILABLE)</p>\n\n<p>Chapter 18: Deploying Our New Code (AVAILABLE)</p>\n\n<p><em>Part 3: More Advanced Topics in Testing (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 19: User Authentication, Spiking, and De-Spiking (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 20: Mocks and Mocking 1: Using Mocks to Test External\nDependencies (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 21: Mocks and Mocking 2: Using Mocks for Test Isolation (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 22: Test Fixtures and a Decorator for Explicit Waits (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 23: Server-Side Debugging (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 24: Finishing “My Lists”: Outside-In TDD (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 25: Continuous Integration (CI) (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 26: The Token Social Bit, the Page Pattern, and an Exercise\nfor the Reader (UNAVAILABLE)</em></p>\n\n<p><em>Chapter 27: Fast Tests, Slow Tests, and Hot Lava (UNAVAILABLE)</em></p>\n\n<p><em>Back Matter: Obey the Testing Goat! (UNAVAILABLE)</em></p>\n\n<p><em>App A: PythonAnywhere (UNAVAILABLE)</em></p>\n\n<p><em>App B: Django Class-Based Views (UNAVAILABLE)</em></p>\n\n<p><em>App C: Provisioning with Ansible (UNAVAILABLE)</em></p>\n\n<p><em>App D: Testing Database Migrations (UNAVAILABLE)</em></p>\n\n<p><em>App E: Behaviour-Driven Development (BDD) (UNAVAILABLE)</em></p>\n\n<p><em>App F: Building a REST API: JSON, Ajax, and Mocking with JavaScript (UNAVAILABLE)</em></p>\n\n<p><em>App G: Django-Rest-Framework (UNAVAILABLE)</em></p>\n\n<p><em>App H: Cheat Sheet (UNAVAILABLE)</em></p>\n\n<p><em>App I: What to Do Next (UNAVAILABLE)</em></p>\n\n<p><em>App J: Source Code Examples (UNAVAILABLE)</em></p>\n\n<p><em>Bibliography (UNAVAILABLE)</em></p>\n</section>\n"
  },
  {
    "path": "LICENSE.md",
    "content": "This book, and all the associated source files, are being made available under\nthe Creative Commons Attribution-NonCommercial-ShareAlike License (v3.0 United\nStates)\n\nFull info at https://creativecommons.org/licenses/by-nc-sa/3.0/us/\n\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n\nSOURCES := $(wildcard *.asciidoc)\nHTML_PAGES := $(patsubst %.asciidoc, %.html, ${SOURCES})\nTESTS := $(patsubst %.asciidoc, test_%, ${SOURCES})\nVENV ?= .venv\n\nRUN_ASCIIDOCTOR = asciidoctor -a source-highlighter=pygments -a pygments-style=default -a stylesheet=asciidoctor.css -a linkcss -a icons=font -a compat-mode -a '!example-caption' -a last-update-label='License: Creative Commons <a href=\"https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode\">CC-BY-NC-ND</a>. Last updated:'\n\nexport PYTHONHASHSEED = 0\nexport PYTHONDONTWRITEBYTECODE = 1\nexport MOZ_HEADLESS = 1\n# for warning introduce in selenium 4.10\nexport PYTHONWARNINGS=ignore::ResourceWarning\n\nexport TMPDIR_CLEANUP = false\n\npart%.forbook.asciidoc: part%.asciidoc\n\tcat $(subst .forbook.,.,$@)  \\\n\t\t| sed 's/^== /= /' \\\n\t\t| sed '/partintro/d' \\\n\t\t| sed '/^--$$/d' \\\n\t\t> $@\n\n\n\nbook.html: part1.forbook.asciidoc\nbook.html: part2.forbook.asciidoc\nbook.html: part3.forbook.asciidoc\nbook.html: part4.forbook.asciidoc\nbook.html: $(SOURCES)\n\n\n%.html: %.asciidoc  # build an individual chapter's html page\n\t$(RUN_ASCIIDOCTOR) $<\n\n.PHONY: build\nbuild: $(HTML_PAGES) $(TMPDIR)\n\n\n$(VENV)/bin:\n\twhich uv && uv venv $(VENV)|| python3 -m venv $(VENV)\n\twhich uv && uv pip install -e . || $(VENV)/bin/pip install -e .\n\n.PHONY: install\ninstall: $(VENV)/bin\n\twhich brew && brew install asciidoctor tree || apt install -y asciidoctor tree\n\n.PHONY: update-submodules\nupdate-submodules:\n\tgit submodule update --init --recursive\n\t$(VENV)/bin/python tests/update_source_repo.py\n\n# this is to allow for a git remote called \"local\" for eg ./source/feed-thru-cherry-pick.sh\n../book-example.git:\n\tmkdir -p ../book-example.git\n\tgit init --bare ../book-example.git\n\n.PHONY: test\ntest: build update-submodules $(VENV)/bin\n\t$(VENV)/bin/pytest tests/\n\n.PHONY: testall\ntestall: build\n\t$(VENV)/bin/pytest --numprocesses=auto tests/test_chapter_*\n\n.PHONY: testall4\ntestall4: build\n\t$(VENV)/bin/pytest --numprocesses=4 tests/test_chapter_*\n\n\n.PHONY: test_%\ntest_%: %.html $(TMPDIR)\n\t$(VENV)/bin/pytest -s --no-summary ./tests/$@.py\n\n.PHONY: xmllint_%\nxmllint_%: %.asciidoc\n\tasciidoctor -b docbook $< -o - | sed \\\n\t\t-e 's/&mdash;/\\&#8212;/g' \\\n\t\t-e 's/&ldquo;/\\&#8220;/g' \\\n\t\t-e 's/&rdquo;/\\&#8221;/g' \\\n\t\t-e 's/&lsquo;/\\&#8216;/g' \\\n\t\t-e 's/&rsquo;/\\&#8217;/g' \\\n\t\t-e 's/&hellip;/\\&#8230;/g' \\\n\t\t-e 's/&nbsp;/\\&#160;/g' \\\n\t\t-e 's/&times;/\\&#215;/g' \\\n\t\t| xmllint --noent --noout -\n\n\n%.xml: %.asciidoc\n\tasciidoctor -b docbook $<\n\n.PHONY: check-links\ncheck-links: book.html\n\tpython check-links.py book.html\n\n.PHONY: clean-docker\nclean-docker:\n\t-docker kill $$(docker ps -q)\n\tdocker rmi -f busybox\n\tdocker rmi -f superlists\n\t# env PATH=misc:$PATH\n\n.PHONY: get-sudo\nget-sudo:\n\tsudo echo 'need sudo access for this test'\n\n.PHONY: no-runservers\nno-runservers:\n\t-pkill -f runserver\n\n# exhaustively list all test targets for nice tab-completion\n.PHONY: test_chapter_01\ntest_chapter_01: chapter_01.html $(TMPDIR) $(VENV)/bin no-runservers\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_01.py\n.PHONY: test_chapter_02_unittest\ntest_chapter_02_unittest: chapter_02_unittest.html $(TMPDIR) $(VENV)/bin no-runservers\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_02_unittest.py\n.PHONY: test_chapter_03_unit_test_first_view\ntest_chapter_03_unit_test_first_view: chapter_03_unit_test_first_view.html $(TMPDIR) $(VENV)/bin no-runservers\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_03_unit_test_first_view.py\n.PHONY: test_chapter_04_philosophy_and_refactoring\ntest_chapter_04_philosophy_and_refactoring: chapter_04_philosophy_and_refactoring.html $(TMPDIR) $(VENV)/bin no-runservers\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_04_philosophy_and_refactoring.py\n.PHONY: test_chapter_05_post_and_database\ntest_chapter_05_post_and_database: chapter_05_post_and_database.html $(TMPDIR) $(VENV)/bin no-runservers\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_05_post_and_database.py\n.PHONY: test_chapter_06_explicit_waits_1\ntest_chapter_06_explicit_waits_1: chapter_06_explicit_waits_1.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_06_explicit_waits_1.py\n.PHONY: test_chapter_07_working_incrementally\ntest_chapter_07_working_incrementally: chapter_07_working_incrementally.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_07_working_incrementally.py\n.PHONY: test_chapter_08_prettification\ntest_chapter_08_prettification: chapter_08_prettification.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_08_prettification.py\n.PHONY: test_chapter_09_docker\ntest_chapter_09_docker: chapter_09_docker.html $(TMPDIR) $(VENV)/bin clean-docker\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_09_docker.py\n.PHONY: test_chapter_10_production_readiness\ntest_chapter_10_production_readiness: get-sudo chapter_10_production_readiness.html $(TMPDIR) $(VENV)/bin clean-docker\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_10_production_readiness.py\n.PHONY: test_chapter_11_server_prep\ntest_chapter_11_server_prep: chapter_11_server_prep.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_11_server_prep.py\n.PHONY: test_chapter_13_organising_test_files\n.PHONY: test_chapter_12_ansible\ntest_chapter_12_ansible: chapter_12_ansible.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_12_ansible.py\n.PHONY: test_chapter_13_organising_test_files\ntest_chapter_13_organising_test_files: chapter_13_organising_test_files.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_13_organising_test_files.py\n.PHONY: test_chapter_14_database_layer_validation\ntest_chapter_14_database_layer_validation: chapter_14_database_layer_validation.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_14_database_layer_validation.py\n.PHONY: test_chapter_15_simple_form\ntest_chapter_15_simple_form: chapter_15_simple_form.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_15_simple_form.py\n.PHONY: test_chapter_16_advanced_forms\ntest_chapter_16_advanced_forms: chapter_16_advanced_forms.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_16_advanced_forms.py\n.PHONY: test_chapter_17_javascript\ntest_chapter_17_javascript: chapter_17_javascript.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_17_javascript.py\n.PHONY: test_chapter_18_second_deploy\ntest_chapter_18_second_deploy: chapter_18_second_deploy.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_18_second_deploy.py\n.PHONY: test_chapter_19_spiking_custom_auth\ntest_chapter_19_spiking_custom_auth: chapter_19_spiking_custom_auth.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_19_spiking_custom_auth.py\n.PHONY: test_chapter_20_mocking_1\ntest_chapter_20_mocking_1: chapter_20_mocking_1.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_20_mocking_1.py\n.PHONY: test_chapter_21_mocking_2\ntest_chapter_21_mocking_2: chapter_21_mocking_2.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_21_mocking_2.py\n.PHONY: test_chapter_22_fixtures_and_wait_decorator\ntest_chapter_22_fixtures_and_wait_decorator: chapter_22_fixtures_and_wait_decorator.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_22_fixtures_and_wait_decorator.py\n.PHONY: test_chapter_23_debugging_prod\ntest_chapter_23_debugging_prod: get-sudo chapter_23_debugging_prod.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_23_debugging_prod.py\n.PHONY: test_chapter_24_outside_in\ntest_chapter_24_outside_in: chapter_24_outside_in.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_24_outside_in.py\n.PHONY: test_chapter_25_CI\ntest_chapter_25_CI: chapter_25_CI.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_25_CI.py\n.PHONY: test_chapter_26_page_pattern\ntest_chapter_26_page_pattern: chapter_26_page_pattern.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_chapter_26_page_pattern.py\n\n\n.PHONY: test_appendix_purist_unit_tests\ntest_appendix_purist_unit_tests: appendix_purist_unit_tests.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest -s ./tests/test_appendix_purist_unit_tests.py\n\n.PHONY: silent_test_%\nsilent_test_%: %.html $(TMPDIR) $(VENV)/bin\n\t$(VENV)/bin/pytest ./tests/$(subst silent_,,$@).py\n\n.PHONY: unit-test\nunit-test: chapter_01.html $(VENV)/bin\n\tSKIP_CHAPTER_SUBMODULES=1 ./tests/update_source_repo.py\n\tsource $(VENV)/bin/activate && ./run_test_tests.sh\n\t# this is a hack to make 'Archive the temp dir' step work in CI\n\techo \"tests\" > .tmpdir.unit-test\n\n.PHONY: clean\nclean:\n\trm -rf $(TMPDIR)\n\trm -v $(HTML_PAGES)\n"
  },
  {
    "path": "README.md",
    "content": "# Test-Driven Web Development With Python, the book.\n\n# License\n\nThe sources for this book are published under the Creative Commons Attribution\nNon-Commercial No-Derivs license (CC-BY-NC-ND).\n\n*I wouldn't recommend using this version to read the book.  Head over to\n[obeythetestinggoat.com](https://www.obeythetestinggoat.com/pages/book.html)\nwhen you can access a nicely formatted version of the full thing, still free\nand under CC license.  And you'll also be able to buy an ebook or print version\nif you feel like it.*\n\nThese sources are being made available for the purposes of curiosity\n(although if you're curious about the way the listings are tested,\ni would definitely recommend https://github.com/cosmicpython/book instead)\nand collaboration (typo-fixes by pull request are very much encouraged!).\n\n\n# Building the book as HTML\n\n- install [asciidoctor](http://asciidoctor.org/), and the *pygments/pygmentize* gem.\n- `make build` will build each chapter as its own html file\n- `make book.html` will create a single file\n- `make chapter_05_post_and_database.html`, eg, will build chapter 5\n\n# Running the tests\n\n* Pre-requisites for the test suite:\n\n```console\nmake install\n```\n\n* Full test suite (probably, don't use this, it would take ages.)\n\n```console\n$ make test\n```\n\n* To test an individual chapter, eg:\n\n```console\n$ make test_chapter_06_explicit_waits_1\n```\n\nIf you see a problem that seems to be related to submodules, try:\n\n```console\nmake update-submodules\n```\n\n\n* Unit tests (tests for the tests for the tests in the testing book)\n\n```console\n$ ./run_test_tests.sh\n```\n\n# Windows / WSL notes\n\n* `vagrant plugin install virtualbox_WSL2` is required\n\n\n# Making changes to the book's code examples\n\nBrief explanation:  each chapter's code examples are reflected in a branch of the example code repository,\nhttps://github.com/hjwp/book-example\nin branches named after the chapter, so eg chapter_02_unittest.asciidoc has a branch called chapter_02_unittest.\n\nThese branches are actually checked out, one by one, as submodules in source/<chapter-name>/superlists.\nEach branch starts at the end of the previous chapter's branch.\n\nCode listings _mostly_ map 1 to 1 with commits in the repo,\nand they are sometimes marked with little tags, eg ch03l007,\nmeaning theoretically, the 7th listing in chapter 3, but that's not always accurate.\n\nWhen the tests run, they start by creating a new folder in /tmp\nchecked out with the code from the end of the last chapter.\n\nThen they go through the code listings in the book one by one,\nand simulate typing them into to an editor.\nIf the code listing  has one of those little tags,\nthe tests check the commit in the repo to see if the listing matches the commit exactly.\n(if there's no tag, there's some fiddly code based on string manipulation\nthat tries to figure out how to insert the code listing into the existing file contents at the right place)\n\nWhen the tests come across a command, eg \"ls\",\nthey actually run \"ls\" in the temp directory,\nto check whether the output that's printed in the book matches what would actually happen.\n\nOne of the most common commands is to run the tests, obviously,\nso much so that if there is some console output in the book with no explicit command,\nthe tests assume it's a test run, so they run \"./manage.py test\" or equivalent.\n\nIn any case, back to our code listings - the point is that,\nif we want to change one of our code listings, we also need to change the commit in the branch / submodule...\n\n...and all of the commits that come after it.\n\n...for that chapter and every subsequent chapter.\n\n\nThis is called \"feeding through the changes\"\n\n\n## Changing a code listing\n\n1. change the listing in the book, eg in in _chapter_03_unit_test_first_view.asciidoc_\n2. open up ./source/chapter_03_unit_test_first_view/superlists in a terminal\n3. do a `git rebase --interactive $previous-chapter-name`\n4. identify the commit that matches the listing that you've changed, and mark it for `edit`\n5. edit the file when prompted, make it match the book\n6. continue the rebase, and deal with an merge conflicts as you go, woo.\n7. `git push local` once you're happy.\n\n## feeding thru the changes\n\nBecause we don't want to push WIP to github every time we change a chapter,\nwe use a local bare repository to push and pull chapters\n\n\n```console\nmake ../book-example.git\n```\n\nwill create it for you.\n\nTODO:  helper to do `git remote add local` to each chapter/submodule\n\nNow you can attempt to feed thru the latest changes to this branch/chapter with\n\n```console\ncd source\n./feed_thru.sh chapter_03_unit_test_first_view chapter_04_philosophy_and_refactoring\n# chapter/branch names will tab complete, helpfully.\n```\n\nif all goes well, you can then run\n\n```console\n./push-back.sh chapter_04_philosophy_and_refactoring\n```\n\nand move on to the next chapter. woo!\n\n\nThis may all seem a bit OTT,\nbut the point is that if we change a variable early on in the book,\ngit (along with the tests) will help us to make sure that it changes\nall the way through all the subsequent chapters.\n"
  },
  {
    "path": "Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n# All Vagrant configuration is done below. The \"2\" in Vagrant.configure\n# configures the configuration version (we support older styles for\n# backwards compatibility). Please don't change it unless you know what\n# you're doing.\nVagrant.configure(\"2\") do |config|\n  # The most common configuration options are documented and commented below.\n  # For a complete reference, please see the online documentation at\n  # https://docs.vagrantup.com.\n\n  # Every Vagrant development environment requires a box. You can search for\n  # boxes at https://vagrantcloud.com/search.\n  # config.vm.box = \"ubuntu/jammy64\"   # virtualbox only\n  # config.vm.box = \"generic/ubuntu2204\"  # amd64\n  config.vm.box = \"bento/ubuntu-22.04\"\n\n  # Disable automatic box update checking. If you disable this, then\n  # boxes will only be checked for updates when the user runs\n  # `vagrant box outdated`. This is not recommended.\n  # config.vm.box_check_update = false\n\n  # Create a forwarded port mapping which allows access to a specific port\n  # within the machine from a port on the host machine. In the example below,\n  # accessing \"localhost:8080\" will access port 80 on the guest machine.\n  # NOTE: This will enable public access to the opened port\n  # config.vm.network \"forwarded_port\", guest: 80, host: 8080\n\n  # Create a forwarded port mapping which allows access to a specific port\n  # within the machine from a port on the host machine and only allow access\n  # via 127.0.0.1 to disable public access\n  # config.vm.network \"forwarded_port\", guest: 80, host: 8080, host_ip: \"127.0.0.1\"\n\n  # Create a private network, which allows host-only access to the machine\n  # using a specific IP.\n  config.vm.network \"private_network\", ip: \"192.168.56.10\"\n\n  # Create a public network, which generally matched to bridged network.\n  # Bridged networks make the machine appear as another physical device on\n  # your network.\n  # config.vm.network \"public_network\"\n\n  # prevent socket thingie to stop wsl /dev/null issue\n  # config.vm.provider \"virtualbox\" do |vb|\n  #   vb.customize [ \"modifyvm\", :id, \"--uartmode1\", \"disconnected\" ]\n  # end\n\n  # Share an additional folder to the guest VM. The first argument is\n  # the path on the host to the actual folder. The second argument is\n  # the path on the guest to mount the folder. And the optional third\n  # argument is a set of non-required options.\n  # config.vm.synced_folder \"../data\", \"/vagrant_data\"\n\n  # Disable the default share of the current code directory. Doing this\n  # provides improved isolation between the vagrant box and your host\n  # by making sure your Vagrantfile isn't accessable to the vagrant box.\n  # If you use this you may want to enable additional shared subfolders as\n  # shown above.\n  config.vm.synced_folder \".\", \"/vagrant\", disabled: true\n\n  # Provider-specific configuration so you can fine-tune various\n  # backing providers for Vagrant. These expose provider-specific options.\n  # Example for VirtualBox:\n  #\n  # config.vm.provider \"virtualbox\" do |vb|\n  #   # Display the VirtualBox GUI when booting the machine\n  #   vb.gui = true\n  #\n  #   # Customize the amount of memory on the VM:\n  #   vb.memory = \"1024\"\n  # end\n  #\n  # View the documentation for the provider you are using for more\n  # information on available options.\n\n  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies\n  # such as FTP and Heroku are also available. See the documentation at\n  # https://docs.vagrantup.com/v2/push/atlas.html for more information.\n  # config.push.define \"atlas\" do |push|\n  #   push.app = \"YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME\"\n  # end\n\n  # Enable provisioning with a shell script. Additional provisioners such as\n  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the\n  # documentation for more information about their specific syntax and use.\n  ssh_pub_key = File.readlines(\"#{Dir.home}/.ssh/id_rsa.pub\").first.strip#\n\n  config.vm.provision \"shell\", inline: <<-SHELL\n    apt update\n    apt upgrade -y\n    apt install -y dtach tree\n\n    useradd -m -s /bin/bash elspeth\n    usermod -a -G sudo elspeth\n    echo 'elspeth:elspieelspie' | chpasswd\n    mkdir -p /home/elspeth/.ssh\n    cp ~/.ssh/authorized_keys /home/elspeth/.ssh\n    chown elspeth /home/elspeth/.ssh/authorized_keys\n    echo 'elspeth ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/elspeth\n    echo 'export DJANGO_COLORS=nocolor' >> /home/elspeth/.profile\n    echo '#{ssh_pub_key}' >> /home/elspeth/.ssh/authorized_keys\n  SHELL\nend\n"
  },
  {
    "path": "acknowledgments.asciidoc",
    "content": "[preface]\n== Acknowledgments\n\nLots of people to thank, without whom this book would never have happened,\nand/or would have been even worse than it is.\n\nThanks first to \"Greg\" at `$OTHER_PUBLISHER`, who was the first person to\nencourage me to believe it really could be done. Even though your employers\nturned out to have overly regressive views on copyright, I'm forever grateful\nthat you believed in me.\n\nThanks to Michael Foord, another ex-employee of Resolver Systems, for providing\nthe original inspiration by writing a book himself, and thanks for his ongoing\nsupport for the project.  Thanks also to my boss Giles Thomas, for foolishly\nallowing another one of his employees to write a book (although I believe he's\nnow changed the standard employment contract to say \"no books\").  Thanks also\nfor your ongoing wisdom and for setting me off on the testing path.\n\nThanks to my other colleagues, Glenn Jones and Hansel Dunlop, for being\ninvaluable sounding boards, and for your patience with my one-track-record\nconversation over the last year.\n\nThanks to my wife, Clementine, and to both my families—without whose support\nand patience I would never have made it.  I apologise for all the time spent\nwith my nose in the computer on what should have been memorable family occasions. I\nhad no idea when I set out what the book would do to my life (\"Write it in my\nspare time, you say?  That sounds reasonable...\").  I couldn't have done it\nwithout you.\n\nThanks to my tech reviewers, Jonathan Hartley, Nicholas Tollervey, and Emily\nBache, for your encouragements and invaluable feedback.   Especially Emily,\nwho actually conscientiously read every single chapter.  Partial credit\nto Nick and Jon, but that should still be read as eternal gratitude. Having\ny'all around made the whole thing less of a lonely endeavour. Without all of\nyou, the book would have been little more than the nonsensical ramblings of an\nidiot.\n\nThanks to everyone else who's given up their time to give some\nfeedback on the book, out of nothing more than the goodness of their heart:\nGary Bernhardt, Mark Lavin, Matt O'Donnell, Michael Foord, Hynek Schlawack,\nRussell Keith-Magee, Andrew Godwin, Kenneth Reitz, and Nathan Stocks.  Thanks\nfor being much smarter than I am, and for preventing me from saying several\nstupid things.  Naturally, there are still plenty of stupid things left in the\nbook, for which y'all can absolutely not be held responsible.\n\nThanks to my editor, Meghan Blanchette, for being a very friendly and likeable\nslave driver, and for keeping the book on track, both in terms of timescales\nand by restraining my sillier ideas.  Thanks to all the others at\nO'Reilly for your help, including Sarah Schneider, Kara Ebrahim, and\nDan Fauxsmith for letting me keep British English. Thanks to Charles\nRoumeliotis for your help with style and grammar.  We may never see eye-to-eye\non the merits of Chicago School quotation/punctuation rules, but I sure am\nglad you were around.  And thanks to the design department for giving us a goat\nfor the cover!\n\nAnd thanks most especially to all my early release readers, for all your help\npicking out typos, for your feedback and suggestions, for all the ways in\nwhich you helped to smooth out the learning curve in the book, and most of\nall for your kind words of encouragement and support that kept me going.\nThank you Jason Wirth, Dave Pawson, Jeff Orr, Kevin De Baere, crainbf,\ndsisson, Galeran, Michael Allan, James O'Donnell, Marek Turnovec, SoonerBourne,\njulz, Cody Farmer, William Vincent, Trey Hunner, David Souther, Tom Perkin,\nSorcha Bowler, Jon Poler, Charles Quast, Siddhartha Naithani, Steve Young,\nRoger Camargo, Wesley Hansen, Johansen Christian Vermeer, Ian Laurain, Sean\nRobertson, Hari Jayaram, Bayard Randel, Konrad Korżel, Matthew Waller, Julian\nHarley, Barry McClendon, Simon Jakobi, Angelo Cordon, Jyrki Kajala, Manish\nJain, Mahadevan Sreenivasan, Konrad Korżel, Deric Crago, Cosmo Smith, Markus\nKemmerling, Andrea Costantini, Daniel Patrick, Ryan Allen, Jason Selby, Greg\nVaughan, Jonathan Sundqvist, Richard Bailey, Diane Soini, Dale Stewart, Mark\nKeaton, Johan Wärlander, Simon Scarfe, Eric Grannan, Marc-Anthony Taylor,\nMaria McKinley, John McKenna, Rafał Szymański, Roel van der Goot,\nIgnacio Reguero, TJ Tolton, Jonathan Means, Theodor Nolte, Jungsoo Moon,\nCraig Cook, Gabriel Ewilazarus, Vincenzo Pandolfo, David \"farbish2\", Nico\nCoetzee, Daniel Gonzalez, Jared Contrascere, Zhao 赵亮,\nand many, many more. If I've missed your name, you have an absolute right to be\naggrieved; I am incredibly grateful to you too, so write to me and I will try\nand make it up to you in any way I can.\n\nAnd finally thanks to you, the latest reader, for deciding to check out\nthe book!  I hope you enjoy it.\n\n[role=\"pagebreak-before less_space\"]\n=== Additional Thanks for the Second Edition\n\nThanks to my wonderful editor for the second edition, Nan Barber, and to\nSusan Conant, Kristen Brown, and the whole team at O'Reilly.\nThanks once again to Emily and Jonathan for tech reviewing, as well as to\nEdward Wong for his very thorough notes.  Any remaining errors and\ninadequacies are all my own.\n\nThanks also to the readers of the free edition who contributed comments,\nsuggestions, and even some pull requests. I have definitely missed some of\nyou on this list,  so apologies if your name isn't here, but thanks to Emre\nGonulates, Jésus Gómez, Jordon Birk, James Evans, Iain Houston, Jason DeWitt,\nRonnie Raney, Spencer Ogden, Suresh Nimbalkar, Darius, Caco,\nLeBodro, Jeff, Duncan Betts, wasabigeek, joegnis, Lars, Mustafa, Jared, Craig,\nSorcha, TJ, Ignacio, Roel, Justyna, Nathan, Andrea, Alexandr, bilyanhadzhi,\nmosegontar, sfarzy, henziger, hunterji, das-g, juanriaza, GeoWill, Windsooon,\ngonulate, Margie Roswell, Ben Elliott, Ramsay Mayka, peterj, 1hx, Wi, Duncan\nBetts, Matthew Senko, Neric \"Kasu\" Kaz, Dominic Scotto, Andrey Makarov,\nand many, many more.\n\n=== Additional Thanks for the Third Edition\n\nThanks to my editor, Rita Fernando,\nthanks to my tech reviewers\nBéres Csanád,\nDavid Seddon,\nSebastian Buczyński,\nand Jan Giacomelli,\nand thanks to all the early release readers for your feedback,\nbig and small, including\nJonathan H.,\nJames Evans,\nPatrick Cantwell,\nDevin Schumacher,\nNick Nielsen,\nTeemu Viikeri,\nAndrew Zipperer,\nartGonza,\nJoy Denebeim,\nmshob23,\nRomilly Cocking,\nZachary Kerbo,\nStephanie Goulet,\nDavid Carter,\nJim Win Man,\nAlex Kennett,\nIvan Schneider,\nLars Berberich,\nRodrigo Jacznik,\nTom Nguyen,\nrokbot,\nNikita Durne,\nand to anyone I've missed off this list,\nmy sincere apologies, ping me and I'll add you,\nand thank you thank you once again.\n\n\n.Extra Thanks for Csanàd\n*******************************************************************************\nEvery single one of the tech reviewers for this edition was invaluable,\nand they all contributed in different and complementary ways.\n\nBut I want to give extra thanks to Csanàd,\nwho went beyond the normal remit of a tech reviewer,\nso far as to do substantial actual rewrites of several chapters in <<part3>>.\n\nYou can't blame him for anything in there though,\nbecause I've been over them since,\nso any errors or problems you might spot are definitely things I've added since.\n\nAnyways, thanks so much Csanàd,\nyou helped me feel like I wasn't entirely alone.\n\n*******************************************************************************\n\n"
  },
  {
    "path": "ai_preface.asciidoc",
    "content": "[[ai_preface]]\n[preface]\n== Preface to the Third Edition: [.keep-together]#TDD in the Age of AI#\n\nIs there any point in learning TDD now that AI can write code for you?\nA single prompt could probably generate the entire example application in this book,\nincluding all its tests, and the infrastructure config for automated deployment too.\n\nThe truth is that it's too early to tell.  AI is still in its infancy,\nand who knows what it'll be able to do in a few years or even months' time.\n\n\n=== AI Is Both Insanely Impressive and Incredibly Unreliable\n\nWhat we do know is that right now,\nAI is both insanely impressive and incredibly unreliable.\n\nBeyond being able to understand and respond to prompts in normal human language--it's\neasy to forget how absolutely extraordinary that is; literally science-fiction\na few years ago--AI tools can generate working code, they can generate tests,\nthey can help us to break down requirements, brainstorm solutions,\nquickly prototype new technologies.  It's genuinely astonishing.\n\nAs we're all finding out though, this all comes with a massive \"but\".\nAI outputs are frequently plagued by hallucinations,\nand in the world of code, that means things that just won't work,\neven if they look plausible.\nWorse than that, they can produce code that appears to work,\nbut is full of subtle bugs, security issues, or performance nightmares.\nFrom a code quality point of view, we know that AI tools will often produce\ncode that's filled with copy-paste and duplication, weird hacks,\nand undecipherable spaghetti code that spells a maintenance nightmare.\n\n\n=== Mitigations for AI's Shortcomings Sure Look a Lot Like TDD\n\nIf you read the advice, even from AI companies themselves,\nabout the best way to work with AI, you'll find that it\nperforms best when working in small, well-defined contexts,\nwith frequent checks for correctness.\nWhen taking on larger tasks, the advice is to break them down into smaller,\nwell-defined pieces, with clearly defined success criteria.\n\nWhen we're thinking about the problem of hallucinations,\nit sure seems like having a comprehensive test suite and running it frequently,\nis going to be a must-have.\n\nWhen we're thinking about code quality, the idea of having a human in the loop,\nwith frequent pauses for review and refactoring,\nagain seems like a key mitigation.\n\nIn short, all of the techniques of test-driven development that are outlined in this book:\n\n* Defining a test that describes each small change of functionality,\n  before we write the code for it\n\n* Breaking our problem down into small pieces and working incrementally,\n  with frequent test runs to catch bugs, regressions, and hallucinations\n\n* The \"refactor\" step in TDD's red/green/refactor cycle,\n  which gives us a regular reminder for the human in the loop to review and improve the code.\n\n\nTDD is all about finding a structured, safer way of developing software,\nreducing the risk of bugs and regressions and improving code quality,\nand these are very much the exact same things that we need to achieve\nwhen working with AI.\n\n\n=== Leaky Abstractions and the Importance of Experience\n\nhttps://oreil.ly/PgWjL[\"Leaky abstractions\"]\nare a diagnosis of a common problem in software development,\nwhereby higher-level abstractions fail in subtle ways,\nand the complexities of the underlying system leak through.\n\nIn the presence of leaky abstractions, you need to understand the lower-level system\nto be able to work effectively.\nIt's for this reason that, when the switch to third-generation languages (3GLs) happened,\nprogrammers who understood the underlying machine code were often the most effective\nat using the new languages like C and Fortran.\n\nIn a similar way, AI offers us a new, higher-level abstraction around writing code,\nbut we can already see the \"leaks\" in the form of hallucinations and poor code quality.\nAnd by analogy to the 3GLs, the programmers who are going to be most effective with AI\nare going to be the ones who \"know what good looks like\",\nboth in terms of code quality, test structure, and so on,\nbut also in terms of what a safe and reliable workflow for software development looks like.\n\n\n==== My Own Experiences with AI\n\nIn my own experiences of working with AI,\nI've been very impressed at its ability to write tests, for example...\nas long as there was already a good first example test to copy from.\nIts ability to write that _first_ test,\nthe one where, as we'll see, a lot of the design (and thinking) happens in TDD,\nwas much more mixed.\n\n// SEBASTIAN: Idea for a frame with actionable advice: \"Make sure there are at least few examples of tests before you'll make AI assistant write more. Show it what 'good' looks like.\"\n\n// SEBASTIAN: Another idea for a frame with actionable advice: \"Simple things like writing a meaningful test name or describing in comment what you want to test can help immensely AI working in autocomplete mode.\"\n\nSimilarly when working in a less \"autocomplete\" and more \"agentic\" mode,\nI saw AI tools do very well on simple problems with clear instructions,\nbut when trying to deal with more complex logic and requirements with ambiguity,\nI've seen it get dreadfully stuck in loops and dead ends.\n\nWhen that happened, I found that trying to guide the AI agent\nback towards taking small steps, working on a single piece at a time,\nand clarifying requirements in tests, was the best way to get things back on track.\n\nI've also been able to experiment with using the \"refactor\" step\nto try and improve the often-terrible code that the AI produced.\nHere again I had mixed results, where the AI would need a lot of nudging\nbefore settling on a solution that felt sufficiently readable and maintainable, to me.\n\nSo I'd echo what many others are saying, which is that AI works best\nwhen you, the user, are a discerning partner rather than passive recipient.\n\nNOTE: Ultimately, as software developers,\n  we need to be able to stand by the code we produce,\n  and be accountable for it,\n  no matter what tools were used to write it.\n\n\n\n=== The AI-Enabled Workflow of the Future\n\n\nThe AI-enabled workflow of the future\nwill look very different to what we have now,\nbut all the indications are that the most effective approach is going to be\nincremental, have checks and balances to avoid hallucinations,\nand systematically involve humans in the loop to ensure quality.\n\nAnd the closest workflow we have to that today, is TDD.\nI'm excited to share it with you!\n"
  },
  {
    "path": "analytics.html",
    "content": "<html><head><script>   (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){   (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),   m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)   })(window,document,'script','//www.google-analytics.com/analytics.js','ga');    ga('create', 'UA-40928035-1', 'obeythetestinggoat.com');   ga('send', 'pageview');  </script>\n"
  },
  {
    "path": "appendix_CD.asciidoc",
    "content": "[[appendix_CD]]\n[appendix]\n== Continuous Deployment (CD)\n\n.Warning\n*******************************************************************************\nThis appendix is just a placeholder / rough sketch.\n\nIt should have the outline of what you need to set up automated deploys tho!\nWhy not give it a try?\n\n*******************************************************************************\n\n(((\"continuous delivery (CD)\")))\nThis is the next step after CI.\nOnce we have a server that automatically does things every time we push,\nwe can take the next step in automating our deploys,\nand deploy our code to staging (and even production!)\nwith every push.\n\nNOTE: \"CD\" sometimes stands for Continuous Deployment,\n    when used to contrast with \"CI\",\n    and sometimes it stands for \"Continuous Delivery\",\n    which is basically a combination of CI and CD.\n    Never forget, the purpose of acronyms\n    is to differentiate insiders from outsiders,\n    so the confusion _is_ the point.\n\n\n* This is an appendix because we get even more tied in to the particularities\n  of an individual platform\n\n* It's also incredibly fiddly. the feedback cycle is annoying slow,\n  and you have to commit and push with every small change.\n  just look at my commit history!\n\n[role=\"skipme\"]\n----\nf5d58736 some tidyup\nf28411a0 disable host key checking again\na2933ad4 dammit forgot curl\nfb4132ec use private keyfile in ssh commands\nce7219e3 install ssh for fts\n957ca269 fix stage name\ndae47804 run fts against staging after deploy\n17999c65 fix the way we get env vars in ansible script\n87aecc62 make secrets files private for ssh\na06d24e9 switch off host key checking\n059fc15e lets try for superverbose debug output\n021843db Revert \"quick look at end of keypair\"\n56d79af4 quick look at end of keypair\nbc5664c6 fix path to secure files\n857c803a install curl\nc37a538c get ssh key from secure files\n5ffbf80f install ssh on python image\nd4f39755 duh stupid typo\nc34cf933 try to deploy using gitlab registry. add stages\n62486de1 docker login using password from env\n4bdc6f53 fix tags in docker push to gitlab registry\nc5a0056c try pushing to gitlab\n81c8601f temporarily dont moujnt db\n6bd41a1f forgot dind\n2de01bf0 move python before-script stuff in to test step\nd11c21fe try to build docker\n76f15efb temporarily dont run fts\n16db3dc1 debug finding path to playbook\n1f3f77f5 remove backslashes\nad46cd12 just do it inline\n1c887270 add deploy step\n6f77b2df venv paths\n801c8373 try and make actual ci work\nba8be943 Gitlab yaml config\n----\n\n\nTricky!\n\nBuilding and running a docker image can only be done on a `docker.git` image,\nbut we want `python:slim` to run our tests,\nand to actually have Ansible installed\n\n*idea 1:*\n\n- build and push a docker image to gitlab registry after each ci run\n- deploy to staging using the new image tag\n- run tests against staging\n\n\n*idea 2:*\n\n- run tests inside docker  (needs an image with firefox tho)\n- run fts inside docker against _another_ docker container\n- deploy from inside docker\n\n\nI've seen variants on both of these.  Gave idea 1 a go first,\nand it worked out:\n\n\nfirst (or, very quickly), i commented out the fts part of the tests.\none of the worst things about fiddling with ci is how slow it is to get feedback:\n\n\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml\n====\n[source,yaml]\n----\ntest:\n  image: python:slim\n\n  before_script:\n    # TODO temporarily commented out\n    # - apt update -y && apt install -y firefox-esr\n    - python --version ; pip --version  # For debugging\n    - pip install virtualenv\n    - virtualenv .venv\n    - source .venv/bin/activate\n\n  script:\n    [...]\n----\n====\n\nrecap:\n1. run tests in python image (with firefox and our virtualenv / requirements.txt)\n2. build docker image in a docker-in-docker image\n3. deploy to staging (from the python image once again, needs ansible)\n4. run fts against staging (from the python image, with firefox)\n\nnow, deploy playbook currently assumes we're building the docker image\nas part of the deploy, but we can't do that because it happened on a different image\n\nwe could use cache / \"build artifacts\" to move the image around,\nbut we may as well do something that's more like real life.\nyou remember i said the `docker push / docker load` dance was a simulation\nof `push+pull` from a \"container registry\"?  well let's do that.\n\n1. run tests (python image)\n2. build our image AND push to registry (docker image)\n3. deploy to staging referencing our image in the registry (python image)\n4. run fts against staging (python image, with firefox)\n\n=== Building our docker image and pushing it to Gitlab registry\n\nTODO: gitlab container registry screnshot\n\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml\n====\n[source,yaml]\n----\nbuild:\n  image: docker:git\n  services:\n    - docker:dind\n\n  script:\n    - docker build\n      -t registry.gitlab.com/hjwp/book-example:$CI_COMMIT_SHA\n      .\n    - echo \"$CI_REGISTRY_PASSWORD\" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin\n    - docker push\n      registry.gitlab.com/hjwp/book-example:$CI_COMMIT_SHA\n----\n====\n\nlink to gitlab registry docs, explain docker login, image tags.\n\n\n=== Deploying from CI, working with secrets\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml\n====\n[source,yaml]\n----\ndeploy:\n  stage: staging-deploy\n  image: python:slim\n  variables:\n    ANSIBLE_HOST_KEY_CHECKING: \"False\"  # <1>\n\n  before_script:\n    - apt update -y && apt install -y\n      curl\n      openssh-client\n    - python --version ; pip --version  # For debugging\n    - pip install virtualenv\n    - virtualenv .venv\n    - source .venv/bin/activate\n\n  script:\n    - pip install -r requirements.txt\n    - pip install ansible\n    # download secure files to get private key  # <2>\n    - curl -s https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer | bash\n    - chmod 600 .secure_files/*\n\n    - ansible-playbook\n      --private-key=.secure_files/keypair-for-gitlab  # <2>\n      --user=elspeth\n      -i staging.ottg.co.uk,\n      -vvv  # <3>\n      ${PWD}/infra/deploy-playbook.yaml\n----\n====\n\n<1> \"known hosts\" checking doesnt work well in ci\n<2> we needed a way to give the ci server permission to access our server.\n    I used a new ssh key\n<3> super-verbose was necessary\n\nTODO: explain generating ssh key, adding to `/home/elpseth/.ssh/authorized_keys` on server.\n\n\nshort listing, couple of hours of pain!\n\neg had to run thru about 200 lines of verbose logs to find this, \nand then a bit of web-searching, to figure out that known-hosts was the problem:\n\n[role=\"skipme\"]\n----\ndebug1: Server host key: ssh-ed25519 SHA256:4kXU5nf93OCxgBMuhr+OC8OUct6xb8yGsRjrqmLTJ7g\ndebug1: load_hostkeys: fopen /root/.ssh/known_hosts: No such file or directory\ndebug1: load_hostkeys: fopen /root/.ssh/known_hosts2: No such file or directory\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory\ndebug1: hostkeys_find_by_key_hostfile: hostkeys file /root/.ssh/known_hosts does not exist\ndebug1: hostkeys_find_by_key_hostfile: hostkeys file /root/.ssh/known_hosts2 does not exist\ndebug1: hostkeys_find_by_key_hostfile: hostkeys file /etc/ssh/ssh_known_hosts does not exist\ndebug1: hostkeys_find_by_key_hostfile: hostkeys file /etc/ssh/ssh_known_hosts2 does not exist\ndebug1: read_passphrase: can't open /dev/tty: No such device or address\nHost key verification failed.\", \"unreachable\": true}\n----\n\n\n=== Updating deploy playbook to use the container registry:\n\nWe delete all the stages to do with building locally and uploading and re-importing:\n\n[role=\"sourcecode skipme\"]\n.infra/deploy-playbook.yaml \n====\n[source,diff]\n----\n@@ -19,37 +19,6 @@\n     - name: Reset ssh connection to allow the user/group change to take effect\n       ansible.builtin.meta: reset_connection\n\n-    - name: Build container image locally\n-    - name: Export container image locally\n-    - name: Upload image to server\n-    - name: Import container image on server\n----\n====\n\nAnd instead, we can just use the full path to the image in our `docker run`\n(with a login to the registry first):\n\n\n[role=\"sourcecode skipme\"]\n.infra/deploy-playbook.yaml \n====\n[source,yaml]\n----\n    - name: Login to gitlab container registry\n      community.docker.docker_login:\n        registry_url: \"{{ lookup('env', 'CI_REGISTRY') }}\"  # <1>\n        username: \"{{ lookup('env', 'CI_REGISTRY_USER') }}\"  # <1>\n        password: \"{{ lookup('env', 'CI_REGISTRY_PASSWORD') }}\"  # <1>\n\n    - name: Run container\n      community.docker.docker_container:\n        name: superlists\n        image: registry.gitlab.com/hjwp/book-example:{{ lookup('env', 'CI_COMMIT_SHA') }}  # <2>\n        state: started\n        recreate: true\n        [...]\n----\n====\n\n<1> just like in the ci script, we use the env vars to get the login details\n<2> and we spell out the registry, with the commit sha, in the image name\n\n\n\n=== Running Fts against staging\n\nAdd explicit \"stages\" to make things run in order:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml\n====\n[source,yaml]\n----\nstages:\n  - build-and-test\n  - staging-deploy\n  - staging-test\n\ntest:\n  image: python:slim\n  stage: build-and-test\n\n  [...]\n\nbuild:\n  image: docker:git\n  services:\n    - docker:dind\n  stage: build-and-test\n\n  script:\n    [...]\n\ntest-staging:\n  image: python:slim\n  stage: staging-test\n  [...]\n----\n====\n\n\nAnd here's how we run the tests against staging:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml\n====\n[source,yaml]\n----\ntest-staging:\n  image: python:slim\n  stage: staging-test\n\n  before_script:\n    - apt update -y && apt install -y\n      curl\n      firefox-esr  # <1>\n      openssh-client\n    - python --version ; pip --version  # For debugging\n    - pip install virtualenv\n    - virtualenv .venv\n    - source .venv/bin/activate\n\n  script:\n    - pip install -r requirements.txt\n    - pip install selenium\n    - curl -s https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer | bash\n    - chmod 600 .secure_files/*  # <2>\n    - env\n      TEST_SERVER=staging.ottg.co.uk\n      SSH_PRIVATE_KEY_PATH=.secure_files/keypair-for-gitlab  # <2>\n      python src/manage.py test functional_tests\n\n----\n====\n\n<1> we need firefox for the fts\n<2> we needed the ssh key again, because as you might remember (i forgot!)\n    the fts use ssh to talk to the db on the server,\n    to manage the database.\n\n\nSo we need some changes in the base FT too:\n\n\n\n[role=\"sourcecode\"]\n.lists.tests.py (ch04l004)\n====\n[source,python]\n----\n\ndef _exec_in_container_on_server(host, commands):\n    print(f\"Running {commands!r} on {host} inside docker container\")\n    keyfile = os.environ.get(\"SSH_PRIVATE_KEY_PATH\")\n    keyfile_arg = [\"-i\", keyfile, \"-o\", \"StrictHostKeyChecking=no\"] if keyfile else []  # <1><2>\n    return _run_commands(\n        [\"ssh\"]\n        + keyfile_arg\n        + [f\"{USER}@{host}\", \"docker\", \"exec\", \"superlists\"]\n        + commands\n    )\n----\n====\n\n\n<1> `-i` tells ssh to use a specific private key\n<2> `-o StrictHostKeyChecking=no` is how we disable known_hosts for the ssh client\n    at the command-line\n\n\n\nand that works\n\nTODO it works deploy screenshot\n\n.CD Recap\n*******************************************************************************\n\nFeedback cycles::\n    Slow.  try to make faster.\n\nSecrets::\n    secret key, email password.\n    each platform is different but there's always a way.\n    careful not to print things out!\n\n\n\n*******************************************************************************\n\n"
  },
  {
    "path": "appendix_DjangoRestFramework.asciidoc",
    "content": "[[appendix_DjangoRestFramework]]\n[appendix]\nDjango-Rest-Framework\n---------------------\n\n\n\n\n(((\"Django-Rest-Framework (DRF)\", id=\"DRF33\")))Having\n\"rolled our own\" REST API in the last appendix, it's time to take\na look at http://www.django-rest-framework.org/[Django-Rest-Framework],\nwhich is a go-to choice for many Python/Django developers building APIs.\nJust as Django aims to give you all the basic tools that you'll need to\nbuild a database-driven website (an ORM, templates, and so on), so DRF\naims to give you all the tools you need to build an API, and thus avoid\nyou having to write boilerplate code over and over again.\n\nWriting this appendix, one of the main things I struggled with was getting the\nexact same API that I'd just implemented manually to be replicated by DRF.\nGetting the same URL layout and the same JSON data structures I'd defined\nproved to be quite a challenge, and I felt like I was fighting the framework.\n\nThat's always a warning sign.  The people who built Django-Rest-Framework\nare a lot smarter than I am, and they've seen a lot more REST APIs than I\nhave, and if they're opinionated about the way that things \"should\" look,\nthen maybe my time would be better spent seeing if I can adapt and work\nwith their view of the world, rather than forcing my own preconceptions\nonto it.\n\n\"Don't fight the framework\" is one of the great pieces of advice I've heard.\nEither go with the flow, or perhaps reassess whether you want to be using\na framework at all.\n\nWe'll work from the API we had at the end of the\nhttps://www.obeythetestinggoat.com/book/appendix_rest_api.html[Online Appendix: Building a REST API]\nand see if we can rewrite it to use DRF.\n\n\n\nInstallation\n~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"installation\")))A\nquick `pip install` gets us DRF.  I'm just using the latest version, which\nwas 3.5.4 at the time of writing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip install djangorestframework*\n----\n\n\nAnd we add `rest_framework` to `INSTALLED_APPS` in 'settings.py':\n\n\n[role=\"sourcecode\"]\n.superlists/settings.py\n====\n[source,python]\n----\nINSTALLED_APPS = [\n    #'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    'lists',\n    'accounts',\n    'functional_tests',\n    'rest_framework',\n]\n----\n====\n\n\nSerializers (Well, ModelSerializers, Really)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"tutorials\")))(((\"Django-Rest-Framework (DRF)\", \"ModelSerializers\")))The\nhttp://bit.ly/2t6T6eX[Django-Rest-Framework tutorial]\nis a pretty good resource to learn DRF.  The first thing you'll come across\nis serializers, and specifically in our case, \"ModelSerializers\". They are\nDRF's way of converting from Django database models to JSON (or possibly other\nformats) that you can send over the wire:\n\n// IDEA: add an explicit unit test or two for serialization\n\n\n\n[role=\"sourcecode\"]\n.lists/api.py (ch37l003)\n====\n[source,python]\n----\nfrom lists.models import List, Item\n[...]\nfrom rest_framework import routers, serializers, viewsets\n\n\nclass ItemSerializer(serializers.ModelSerializer):\n\n    class Meta:\n        model = Item\n        fields = ('id', 'text')\n\n\nclass ListSerializer(serializers.ModelSerializer):\n    items = ItemSerializer(many=True, source='item_set')\n\n    class Meta:\n        model = List\n        fields = ('id', 'items',)\n----\n====\n\n[role=\"pagebreak-before\"]\nViewsets (Well, ModelViewsets, Really) and Routers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"ModelViewsets\")))A\nModelViewSet is DRF's way of defining all the different ways you can interact\nwith the objects for a particular model via your API. Once you tell it which\nmodels you're interested in (via the `queryset` attribute) and how to serialize\nthem (`serializer_class`), it will then do the rest--automatically building\nviews for you that will let you list, retrieve, update, and even delete objects.\n\nHere's all we need to do for a ViewSet that'll be able to retrieve items for\na particular list:\n\n\n[role=\"sourcecode\"]\n.lists/api.py (ch37l004)\n====\n[source,python]\n----\nclass ListViewSet(viewsets.ModelViewSet):\n    queryset = List.objects.all()\n    serializer_class = ListSerializer\n\n\nrouter = routers.SimpleRouter()\nrouter.register(r'lists', ListViewSet)\n----\n====\n\nA 'router' is DRF's way of building URL configuration automatically, and\nmapping them to the functionality provided by the ViewSet.\n\nAt this point we can start pointing our 'urls.py' at our new router,\nbypassing the old API code and seeing how our tests do with the new stuff:\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch37l005)\n====\n[source,python]\n----\n[...]\n# from lists.api import urls as api_urls\nfrom lists.api import router\n\nurlpatterns = [\n    url(r'^$', list_views.home_page, name='home'),\n    url(r'^lists/', include(list_urls)),\n    url(r'^accounts/', include(accounts_urls)),\n    # url(r'^api/', include(api_urls)),\n    url(r'^api/', include(router.urls)),\n]\n----\n====\n\nThat makes loads of our tests fail:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\ndjango.urls.exceptions.NoReverseMatch: Reverse for 'api_list' not found.\n'api_list' is not a valid view function or pattern name.\n[...]\nAssertionError: 405 != 400\n[...]\nAssertionError: {'id': 2, 'items': [{'id': 2, 'text': 'item 1'}, {'id': 3,\n'text': 'item 2'}]} != [{'id': 2, 'text': 'item 1'}, {'id': 3, 'text': 'item\n2'}]\n\n ---------------------------------------------------------------------\nRan 54 tests in 0.243s\n\nFAILED (failures=4, errors=10)\n----\n\nLet's take a look at those 10 errors first, all saying they cannot reverse\n`api_list`.  It's because the DRF router uses a different naming convention\nfor URLs than the one we used when we coded it manually. You'll see from the\ntracebacks that they're happening when we render a template.  It's 'list.html'.\nWe can fix that in just one place; `api_list` becomes `list-detail`:\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch37l006)\n====\n[source,html]\n----\n  <script>\n$(document).ready(function () {\n  var url = \"{% url 'list-detail' list.id %}\";\n});\n  </script>\n----\n====\n\n\nThat will get us down to just four failures:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nFAIL: test_POSTing_a_new_item (lists.tests.test_api.ListAPITest)\n[...]\nFAIL: test_duplicate_items_error (lists.tests.test_api.ListAPITest)\n[...]\nFAIL: test_for_invalid_input_returns_error_code\n(lists.tests.test_api.ListAPITest)\n[...]\nFAIL: test_get_returns_items_for_correct_list\n(lists.tests.test_api.ListAPITest)\n[...]\nFAILED (failures=4)\n----\n\n\n//TODO use @skip\n\nLet's DONT-ify all the validation tests for now, and save that complexity\nfor later:\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch37l007)\n====\n[source,python]\n----\n[...]\n    def DONTtest_for_invalid_input_nothing_saved_to_db(self):\n        [...]\n    def DONTtest_for_invalid_input_returns_error_code(self):\n        [...]\n    def DONTtest_duplicate_items_error(self):\n        [...]\n----\n====\n\nAnd now we have just two failures:\n\n[subs=\"specialcharacters,macros\"]\n----\nFAIL: test_POSTing_a_new_item (lists.tests.test_api.ListAPITest)\n[...]\n    self.assertEqual(response.status_code, 201)\nAssertionError: 405 != 201\n[...]\nFAIL: test_get_returns_items_for_correct_list\n(lists.tests.test_api.ListAPITest)\n[...]\nAssertionError: {'id': 2, 'items': [{'id': 2, 'text': 'item 1'}, {'id': 3,\n'text': 'item 2'}]} != [{'id': 2, 'text': 'item 1'}, {'id': 3, 'text': 'item\n2'}]\n[...]\nFAILED (failures=2)\n----\n\nLet's take a look at that last one first.\n\nDRF's default configuration does provide a slightly different data structure\nto the one we built by hand--doing a GET for a list gives you its ID, and\nthen the list items are inside a key called \"items\".  That means a slight\nmodification to our unit test, before it gets back to passing:\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch37l008)\n====\n[source,diff]\n----\n@@ -23,10 +23,10 @@ class ListAPITest(TestCase):\n         response = self.client.get(self.base_url.format(our_list.id))\n         self.assertEqual(\n             json.loads(response.content.decode('utf8')),\n-            [\n+            {'id': our_list.id, 'items': [\n                 {'id': item1.id, 'text': item1.text},\n                 {'id': item2.id, 'text': item2.text},\n-            ]\n+            ]}\n         )\n----\n====\n\nThat's the GET for retrieving list items sorted (and, as we'll see later, we've\ngot a bunch of other stuff for free too).  How about adding new ones, using\nPOST?\n\n\nA Different URL for POST Item\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"POST requests\")))This\nis the point at which I gave up on fighting the framework and just saw\nwhere DRF wanted to take me.  Although it's possible, it's quite torturous to\ndo a POST to the \"lists\" ViewSet in order to add an item to a list.\n\nInstead, the simplest thing is to post to an item view, not a list view:\n\n\n[role=\"sourcecode\"]\n.lists/api.py (ch37l009)\n====\n[source,python]\n----\nclass ItemViewSet(viewsets.ModelViewSet):\n    serializer_class = ItemSerializer\n    queryset = Item.objects.all()\n\n\n[...]\nrouter.register(r'items', ItemViewSet)\n----\n====\n\n\nSo that means we change the test slightly, moving all the POST tests\nout of the [keep-together]#`ListAPITest`# and into a new test class, `ItemsAPITest`:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch37l010)\n====\n[source,python]\n----\n@@ -1,3 +1,4 @@\n import json\n+from django.core.urlresolvers import reverse\n from django.test import TestCase\n from lists.models import List, Item\n@@ -31,9 +32,13 @@ class ListAPITest(TestCase):\n\n\n+\n+class ItemsAPITest(TestCase):\n+    base_url = reverse('item-list')\n+\n     def test_POSTing_a_new_item(self):\n         list_ = List.objects.create()\n         response = self.client.post(\n-            self.base_url.format(list_.id),\n-            {'text': 'new item'},\n+            self.base_url,\n+            {'list': list_.id, 'text': 'new item'},\n         )\n         self.assertEqual(response.status_code, 201)\n\n----\n====\n\nThat will give us:\n\n----\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n----\n\n\nUntil we add the list ID to our serialization of items; otherwise, we don't know\nwhat list it's for:\n\n\n[role=\"sourcecode\"]\n.lists/api.py (ch37l011)\n====\n[source,python]\n----\nclass ItemSerializer(serializers.ModelSerializer):\n\n    class Meta:\n        model = Item\n        fields = ('id', 'list', 'text')\n----\n====\n\n\nAnd that causes another small associated test change:\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch37l012)\n====\n[source,python]\n----\n@@ -25,8 +25,8 @@ class ListAPITest(TestCase):\n         self.assertEqual(\n             json.loads(response.content.decode('utf8')),\n             {'id': our_list.id, 'items': [\n-                {'id': item1.id, 'text': item1.text},\n-                {'id': item2.id, 'text': item2.text},\n+                {'id': item1.id, 'list': our_list.id, 'text': item1.text},\n+                {'id': item2.id, 'list': our_list.id, 'text': item2.text},\n             ]}\n         )\n----\n====\n\n\nAdapting the Client Side\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"client-side adaptations\")))Our\nAPI no longer returns a flat array of the items in a list.  It returns an\nobject, with a `.items` attribute that represents the items.  That means a\nsmall tweak to our +update&#x200b;Items+ function:\n\n[role=\"sourcecode\"]\n.lists/static/list.js (ch37l013)\n====\n[source,diff]\n----\n@@ -3,8 +3,8 @@ window.Superlists = {};\n window.Superlists.updateItems = function (url) {\n   $.get(url).done(function (response) {\n     var rows = '';\n-    for (var i=0; i<response.length; i++) {\n-      var item = response[i];\n+    for (var i=0; i<response.items.length; i++) {\n+      var item = response.items[i];\n       rows += '\\n<tr><td>' + (i+1) + ': ' + item.text + '</td></tr>';\n     }\n     $('#id_list_table').html(rows);\n\n----\n====\n\nAnd because we're using different URLs for GETing lists and POSTing items,\nwe tweak the `initialize` function slightly too.  Rather than multiple\narguments, we'll switch to using a `params` object containing the required\nconfig:\n\n[role=\"sourcecode small-code\"]\n.lists/static/list.js\n====\n[source,diff]\n----\n@@ -11,23 +11,24 @@ window.Superlists.updateItems = function (url) {\n   });\n };\n\n-window.Superlists.initialize = function (url) {\n+window.Superlists.initialize = function (params) {\n   $('input[name=\"text\"]').on('keypress', function () {\n     $('.has-error').hide();\n   });\n\n-  if (url) {\n-    window.Superlists.updateItems(url);\n+  if (params) {\n+    window.Superlists.updateItems(params.listApiUrl);\n\n     var form = $('#id_item_form');\n     form.on('submit', function(event) {\n       event.preventDefault();\n-      $.post(url, {\n+      $.post(params.itemsApiUrl, {\n+        'list': params.listId,\n         'text': form.find('input[name=\"text\"]').val(),\n         'csrfmiddlewaretoken': form.find('input[name=\"csrfmiddlewaretoken\"]').val(),\n       }).done(function () {\n         $('.has-error').hide();\n-        window.Superlists.updateItems(url);\n+        window.Superlists.updateItems(params.listApiUrl);\n       }).fail(function (xhr) {\n         $('.has-error').show();\n         if (xhr.responseJSON && xhr.responseJSON.error) {\n----\n====\n\nWe reflect that in 'list.html':\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch37l014)\n====\n[source,html]\n----\n$(document).ready(function () {\n  window.Superlists.initialize({\n    listApiUrl: \"{% url 'list-detail' list.id %}\",\n    itemsApiUrl: \"{% url 'item-list' %}\",\n    listId: {{ list.id }},\n  });\n});\n----\n====\n\n\nAnd that's actually enough to get the basic FT working again:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_simple_list_creation*]\n[...]\nRan 2 tests in 15.635s\n\nOK\n----\n\n\nThere's a few more changes to do with error handling, which you can explore in\nthe\nhttps://github.com/hjwp/book-example/blob/appendix_DjangoRestFramework/lists/api.py[repo\nfor this appendix] if you're curious.\n\n\n\nWhat Django-Rest-Framework Gives You\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"Django-Rest-Framework (DRF)\", \"benefits of\")))You\nmay be wondering what the point of using this framework was.\n\n\nConfiguration Instead of Code\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWell, the first advantage is that I've transformed my old procedural view\nfunction into a more declarative syntax:\n\n\n[role=\"sourcecode currentcontents dofirst-ch37l016\"]\n.lists/api.py\n====\n[source,python]\n----\ndef list(request, list_id):\n    list_ = List.objects.get(id=list_id)\n    if request.method == 'POST':\n        form = ExistingListItemForm(for_list=list_, data=request.POST)\n        if form.is_valid():\n            form.save()\n            return HttpResponse(status=201)\n        else:\n            return HttpResponse(\n                json.dumps({'error': form.errors['text'][0]}),\n                content_type='application/json',\n                status=400\n            )\n    item_dicts = [\n        {'id': item.id, 'text': item.text}\n        for item in list_.item_set.all()\n    ]\n    return HttpResponse(\n        json.dumps(item_dicts),\n        content_type='application/json'\n    )\n----\n====\n\n\nIf you compare this to the final DRF version, you'll notice that we are\nactually now entirely configured:\n\n\n\n[role=\"sourcecode currentcontents dofirst-ch37l019\"]\n.lists/api.py\n====\n[source,python]\n----\nclass ItemSerializer(serializers.ModelSerializer):\n    text = serializers.CharField(\n        allow_blank=False, error_messages={'blank': EMPTY_ITEM_ERROR}\n    )\n\n    class Meta:\n        model = Item\n        fields = ('id', 'list', 'text')\n        validators = [\n            UniqueTogetherValidator(\n                queryset=Item.objects.all(),\n                fields=('list', 'text'),\n                message=DUPLICATE_ITEM_ERROR\n            )\n        ]\n\n\nclass ListSerializer(serializers.ModelSerializer):\n    items = ItemSerializer(many=True, source='item_set')\n\n    class Meta:\n        model = List\n        fields = ('id', 'items',)\n\n\nclass ListViewSet(viewsets.ModelViewSet):\n    queryset = List.objects.all()\n    serializer_class = ListSerializer\n\n\nclass ItemViewSet(viewsets.ModelViewSet):\n    serializer_class = ItemSerializer\n    queryset = Item.objects.all()\n\n\nrouter = routers.SimpleRouter()\nrouter.register(r'lists', ListViewSet)\nrouter.register(r'items', ItemViewSet)\n\n----\n====\n\n\n\nFree Functionality\n^^^^^^^^^^^^^^^^^^\n\nThe second advantage is that, by using DRF's ModelSerializer, ViewSet, and\nrouters, I've actually ended up with a much more extensive API than the one I'd\nrolled by hand.\n\n* All the HTTP methods, GET, POST, PUT, PATCH, DELETE, and OPTIONS, now work,\n  out of the box, for all list and items URLs.\n\n* And a browsable/self-documenting version of the API is available at\n  pass:[<em>http://localhost:8000/api/lists/</em>] and pass:[<em>http://localhost:8000/api/items</em>]. (<<figag01>>; try it!)\n\n[[figag01]]\n.A free browsable API for your users\nimage::images/twp2_ag01.png[\"Screenshot of DRF browsable api page at http://localhost:8000/api/items/\"]\n\n\nThere's more information in\nhttp://www.django-rest-framework.org/topics/documenting-your-api/#self-describing-apis[the\nDRF docs], but those are both seriously neat features to be able to offer the\nend users of your API.\n\n\nIn short, DRF is a great way of generating APIs, almost automatically, based on\nyour existing models structure.  If you're using Django, definitely check it\nout before you start hand-rolling your own API code.\n\n\n.Django-Rest-Framework Tips\n*******************************************************************************\n\n(((\"Django-Rest-Framework (DRF)\", \"tips for\")))Don't fight the framework::\n    Going with the flow is often the best way to stay productive.  That, or\n    maybe don't use the framework.  Or use it at a lower level.\n\nRouters and ViewSets for the principle of least surprise::\n    One of the advantages of DRF is that its generic tools like routers and\n    ViewSets will give you a very predictable API, with sensible defaults\n    for its endpoints, URL structure, and responses for different HTTP methods.\n\nCheck out the self-documenting, browsable version::\n    Check out your API endpoints in a browser. DRF responds differently when it\n    detects your API is being accessed by a \"normal\" web browser, and displays\n    a very nice, self-documenting version of itself, which you can share with\n    your users.(((\"\", startref=\"DRF33\")))\n\n*******************************************************************************\n\n"
  },
  {
    "path": "appendix_Django_Class-Based_Views.asciidoc",
    "content": "[[appendix_Django_Class-Based_Views]]\n[appendix]\nDjango Class-Based Views\n------------------------\n\n(((\"Django framework\", \"class-based generic views\", id=\"DJFclass28\")))This\nappendix follows on from <<chapter_16_advanced_forms>>, in which we\nimplemented Django forms for validation and refactored our views.  By the end\nof that chapter, our views were still using functions.\n\n\n\n\nThe new shiny in the Django world, however, is class-based views. In this\nappendix, we'll refactor our application to use them instead of view functions.\nMore specifically, we'll have a go at using class-based 'generic' views.\n\n\nClass-Based Generic Views\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"class-based generic views (CBGVs)\", \"vs. class-based views\", secondary-sortas=\"class-based views\")))There's\na difference between class-based views and class-based 'generic' views.\nClass-based views (CBVs) are just another way of defining view functions.  They make\nfew assumptions about what your views will do, and they offer one main\nadvantage over view functions, which is that they can be subclassed.  This\ncomes, arguably, at the expense of being less readable than traditional\nfunction-based views.  The main use case for 'plain' class-based views is when\nyou have several views that reuse the same logic. We want to obey the DRY\nprinciple. With function-based views, you would use helper functions or\ndecorators.  The theory is that using a class structure may give you a more\nelegant solution.\n\nClass-based 'generic' views (CBGVs) are class-based views that attempt to provide\nready-made solutions to common use cases:  fetching an object from the\ndatabase and passing it to a template, fetching a list of objects, saving\nuser input from a POST request using a +ModelForm+, and so on.  These sound very\nmuch like our use cases, but as we'll soon see, the devil is in the details.\n\nI should say at this point that I've not used either kind of class-based views\nmuch. I can definitely see the sense in them, and there are potentially many\nuse cases in Django apps where CBGVs would fit in perfectly. However, as soon\nas your use case is slightly outside the basics--as soon as you have more\nthan one model you want to use, for example--I find that using class-based views\ncan (again, debatably) lead to code that's much harder to read than a classic\nview function.  \n\nStill, because we're forced to use several of the customisation options for\nclass-based views, implementing them in this case can teach us a lot about\nhow they work, and how we can unit test them.\n\nMy hope is that the same unit tests we use for function-based views should\nwork just as well for class-based views.  Let's see how we get on.\n\n\nThe Home Page as a FormView\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"class-based generic views (CBGVs)\", \"home page as a FormView\")))Our\nhome page just displays a form on a template:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\ndef home_page(request):\n    return render(request, 'home.html', {'form': ItemForm()})\n----\n====\n\n\nhttps://docs.djangoproject.com/en/5.2/ref/class-based-views/[Looking through\nthe options], Django has a generic view called `FormView`&mdash;let's see how\nthat goes:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l001)\n====\n[source,python]\n----\nfrom django.views.generic import FormView\n[...]\n\nclass HomePageView(FormView):\n    template_name = 'home.html'\n    form_class = ItemForm\n----\n====\n\nWe tell it what template we want to use, and which form. Then, we\njust need to update 'urls.py', replacing the line that used to say\n`lists.views.home_page`:\n\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch31l002)\n====\n[source,python]\n----\n[...]\nurlpatterns = [\n    url(r'^$', list_views.HomePageView.as_view(), name='home'),\n    url(r'^lists/', include(list_urls)),\n]\n----\n====\n\nAnd the tests all check out! That was easy...\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n\nRan 34 tests in 0.119s\nOK\n----\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\nRan 5 tests in 15.160s\nOK\n----\n\nSo far, so good. We've replaced a one-line view function with a two-line class,\nbut it's still very readable. This would be a good time for a commit...\n\n\nUsing form_valid to Customise a CreateView\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"class-based generic views (CBGVs)\", \"customizing a CreateView\", id=\"CBGVcreate28\")))(((\"form_valid\")))Next\nwe have a crack at the view we use to create a brand new list, currently\nthe `new_list` function. Here's what it looks like now:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List.objects.create()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n----\n====\n\n\nLooking through the possible CBGVs, we probably want a `CreateView`, and we\nknow we're using the `ItemForm` class, so let's see how we get on with them,\nand whether the tests will help us:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l003)\n====\n[source,python]\n----\nfrom django.views.generic import FormView, CreateView\n[...]\n\nclass NewListView(CreateView):\n    form_class = ItemForm\n\ndef new_list(request):\n    [...]\n----\n====\n\nI'm going to leave the old view function in 'views.py', so that we can copy\ncode across from it.  We can delete it once everything is working.  It's\nharmless as soon as we switch over the URL mappings, this time in:\n\n[role=\"sourcecode\"]\n.lists/urls.py (ch31l004)\n====\n[source,python]\n----\n[...]\nurlpatterns = [\n    url(r'^new$', views.NewListView.as_view(), name='new_list'),\n    url(r'^(\\d+)/$', views.view_list, name='view_list'),\n]\n----\n====\n\nNow running the tests gives six errors:\n\n[subs=\"specialcharacters,macros\"]\n[role=\"small-code\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n\nERROR: test_can_save_a_POST_request (lists.tests.test_views.NewListTest)\nTypeError: save() missing 1 required positional argument: 'for_list'\n\nERROR: test_for_invalid_input_passes_form_to_template\n(lists.tests.test_views.NewListTest)\ndjango.core.exceptions.ImproperlyConfigured: TemplateResponseMixin requires\neither a definition of 'template_name' or an implementation of\n'get_template_names()'\n\nERROR: test_for_invalid_input_renders_home_template\n(lists.tests.test_views.NewListTest)\ndjango.core.exceptions.ImproperlyConfigured: TemplateResponseMixin requires\neither a definition of 'template_name' or an implementation of\n'get_template_names()'\n\nERROR: test_invalid_list_items_arent_saved (lists.tests.test_views.NewListTest)\ndjango.core.exceptions.ImproperlyConfigured: TemplateResponseMixin requires\neither a definition of 'template_name' or an implementation of\n'get_template_names()'\n\nERROR: test_redirects_after_POST (lists.tests.test_views.NewListTest)\nTypeError: save() missing 1 required positional argument: 'for_list'\n\nERROR: test_validation_errors_are_shown_on_home_page\n(lists.tests.test_views.NewListTest)\ndjango.core.exceptions.ImproperlyConfigured: TemplateResponseMixin requires\neither a definition of 'template_name' or an implementation of\n'get_template_names()'\n\n\nFAILED (errors=6)\n----\n\nLet's start with the third--maybe we can just add the template?\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l005)\n====\n[source,python]\n----\nclass NewListView(CreateView):\n    form_class = ItemForm\n    template_name = 'home.html'\n----\n====\n\nThat gets us down to just two failures: we can see they're both happening\nin the generic view's `form_valid` function, and that's one of the ones that\nyou can override to provide custom behaviour in a CBGV.  As its name implies,\nit's run when the view has detected a valid form.  We can just copy some of\nthe code from our old view function, that used to live after \n`if form.is_valid():`:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l006)\n====\n[source,python]\n----\nclass NewListView(CreateView):\n    template_name = 'home.html'\n    form_class = ItemForm\n\n    def form_valid(self, form):\n        list_ = List.objects.create()\n        form.save(for_list=list_)\n        return redirect(list_)\n----\n====\n\nThat gets us a full pass!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\nRan 34 tests in 0.119s\nOK\n$ pass:quotes[*python manage.py test functional_tests*]\nRan 5 tests in 15.157s\nOK\n----\n\n\nAnd we 'could' even save two more lines, trying to obey \"DRY\", by using one of\nthe main advantages of CBVs: inheritance!\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l007)\n====\n[source,python]\n----\nclass NewListView(CreateView, HomePageView):\n\n    def form_valid(self, form):\n        list_ = List.objects.create()\n        form.save(for_list=list_)\n        return redirect(list_)\n----\n====\n\nAnd all the tests would still pass:\n\n----\nOK\n----\n\nWARNING: This is not really good object-oriented practice.  Inheritance implies\n    an \"is-a\" relationship, and it's probably not meaningful to say that our \n    new list view \"is-a\" home page view...so, probably best not to do this.\n\nWith or without that last step, how does it compare to the old version? I'd say\nthat's not bad.   We save some boilerplate code, and the view is still fairly\nlegible.  So far, I'd say we've got one point for CBGVs, and one draw.(((\"\", startref=\"CBGVcreate28\")))\n\n\nA More Complex View to Handle Both Viewing and Adding to a List\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"class-based generic views (CBGVs)\", \"duplicate views\", id=\"CBGVduplicate28\")))This\ntook me 'several' attempts.  And I have to say that, although the tests\ntold me when I got it right, they didn't really help me to figure out the\nsteps to get there...mostly it was just trial and error, hacking about\nin functions like `get_context_data`, `get_form_kwargs`, and so on.\n\nOne thing it did made me realise was the value of having lots of individual\ntests, each testing one thing.  I went back and rewrote some of Chapters pass:[<a data-type=\"xref\" data-xrefstyle=\"select:labelnumber\" href=\"#chapter_11_server_prep\">#chapter_11_server_prep</a>–<a data-type=\"xref\" data-xrefstyle=\"select:labelnumber\" href=\"#chapter_13_organising_test_files\">#chapter_13_organising_test_files</a>]\nas a result.\n\n\nThe Tests Guide Us, for a While\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHere's how things might go.  Start by thinking we want a `DetailView`,\nsomething that shows you the detail of an object:\n\n\n[role=\"sourcecode dofirst-ch31l008\"]\n.lists/views.py (ch31l009)\n====\n[source,python]\n----\nfrom django.views.generic import FormView, CreateView, DetailView\n[...]\n\nclass ViewAndAddToList(DetailView):\n    model = List\n----\n====\n\nAnd wiring it up in 'urls.py':\n\n\n[role=\"sourcecode\"]\n.lists/urls.py (ch31l010)\n====\n[source,python]\n----\n    url(r'^(\\d+)/$', views.ViewAndAddToList.as_view(), name='view_list'),\n----\n====\n\n\nThat gives:\n\n----\n[...]\nAttributeError: Generic detail view ViewAndAddToList must be called with either\nan object pk or a slug.\n\n\nFAILED (failures=5, errors=6)\n----\n\nNot totally obvious, but a bit of Googling around led me to understand that\nI needed to use a \"named\" regex capture group:\n\n[role=\"sourcecode\"]\n.lists/urls.py (ch31l011)\n====\n[source,diff]\n----\n@@ -3,6 +3,6 @@ from lists import views\n \n urlpatterns = [\n     url(r'^new$', views.NewListView.as_view(), name='new_list'),\n-    url(r'^(\\d+)/$', views.view_list, name='view_list'),\n+    url(r'^(?P<pk>\\d+)/$', views.ViewAndAddToList.as_view(), name='view_list')\n ]\n\n----\n====\n\nThe next set of errors had one that was fairly helpful:\n\n----\n[...]\ndjango.template.exceptions.TemplateDoesNotExist: lists/list_detail.html\n\nFAILED (failures=5, errors=6)\n----\n\nThat's easily solved:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l012)\n====\n[source,python]\n----\nclass ViewAndAddToList(DetailView):\n    model = List\n    template_name = 'list.html'\n----\n====\n\nThat takes us down five and two:\n\n----\n[...]\nERROR: test_displays_item_form (lists.tests.test_views.ListViewTest)\nKeyError: 'form'\n\nFAILED (failures=5, errors=2)\n----\n\n\nUntil We're Left with Trial and Error\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSo I figured, our view doesn't just show us the detail of an object,\nit also allows us to create new ones.  Let's make it both a \n`DetailView` 'and' a `CreateView`, and maybe add the `form_class`:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l013)\n====\n[source,python]\n----\nclass ViewAndAddToList(DetailView, CreateView):\n    model = List\n    template_name = 'list.html'\n    form_class = ExistingListItemForm\n----\n====\n\nBut that gives us a lot of errors saying:\n\n----\n[...]\nTypeError: __init__() missing 1 required positional argument: 'for_list'\n----\n\nAnd the `KeyError: 'form'` was still there too!\n\nAt this point the errors stopped being quite as helpful, and it was no longer\nobvious what to do next.  I had to resort to trial and error.  Still, the \ntests did at least tell me when I was getting things more right or more wrong.\n\nMy first attempts to use `get_form_kwargs` didn't really work, but I found\nthat I could use `get_form`:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch31l014)\n====\n[source,python]\n----\n    def get_form(self):\n        self.object = self.get_object()\n        return self.form_class(for_list=self.object, data=self.request.POST)\n----\n====\n\nBut it would only work if I also assigned to `self.object`, as a side effect,\nalong the way, which was a bit upsetting.  Still, that takes us down\nto just three errors, but we're still apparently not quite there!\n\n----\ndjango.core.exceptions.ImproperlyConfigured: No URL to redirect to.  Either\nprovide a url or define a get_absolute_url method on the Model.\n----\n\n\nBack on Track\n^^^^^^^^^^^^^\n\nAnd for this final failure, the tests are being helpful again.\nIt's quite easy to define a `get_absolute_url` on the `Item` class, such\nthat items point to their parent list's page:\n\n\n[role=\"sourcecode\"]\n.lists/models.py (ch31l015)\n====\n[source,python]\n----\nclass Item(models.Model):\n    [...]\n\n    def get_absolute_url(self):\n        return reverse('view_list', args=[self.list.id])\n----\n====\n\n\nIs That Your Final Answer?\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n(((\"\", startref=\"CBGVduplicate28\")))We\nend up with a view class that looks like this:\n\n[role=\"sourcecode currentcontens\"]\n.lists/views.py\n====\n[source,python]\n----\nclass ViewAndAddToList(DetailView, CreateView):\n    model = List\n    template_name = 'list.html'\n    form_class = ExistingListItemForm\n\n    def get_form(self):\n        self.object = self.get_object()\n        return self.form_class(for_list=self.object, data=self.request.POST)\n----\n====\n\n\nCompare Old and New\n~~~~~~~~~~~~~~~~~~~\n\n(((\"class-based generic views (CBGVs)\", \"comparing old and new versions\")))Let's\nsee the old version for comparison?\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    list_ = List.objects.get(id=list_id)\n    form = ExistingListItemForm(for_list=list_)\n    if request.method == 'POST':\n        form = ExistingListItemForm(for_list=list_, data=request.POST)\n        if form.is_valid():\n            form.save()\n            return redirect(list_)\n    return render(request, 'list.html', {'list': list_, \"form\": form})\n----\n====\n\nWell, it has reduced the number of lines of code from nine to seven.  Still, I find\nthe function-based version a little easier to understand, in that it has a\nlittle bit less magic&mdash;\"explicit is better than implicit\", as the Zen of\nPython would have it. I mean...[keep-together]#`SingleObjectMixin`#?  What?  And, more\noffensively, the whole thing falls apart if we don't assign to `self.object`\ninside `get_form`?  Yuck.\n\nStill, I guess some of it is in the eye of the beholder.\n\n\nBest Practices for Unit Testing CBGVs?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n(((\"class-based generic views (CBGVs)\", \"best practices for\")))As\nI was working through this, I felt like my \"unit\" tests were sometimes a \nlittle too high-level.  This is no surprise, since tests for views that involve\nthe Django Test Client are probably more properly called integrated tests.\n\nThey told me whether I was getting things right or wrong, but they didn't\nalways offer enough clues on exactly how to fix things.\n\nI occasionally wondered whether there might be some mileage in a test that\nwas closer to the implementation--something like this:\n\n[role=\"sourcecode skipme\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\ndef test_cbv_gets_correct_object(self):\n    our_list = List.objects.create()\n    view = ViewAndAddToList()\n    view.kwargs = dict(pk=our_list.id)\n    self.assertEqual(view.get_object(), our_list)\n----\n====\n\nBut the problem is that it requires a lot of knowledge of the internals of\nDjango CBVs to be able to do the right test setup for these kinds of tests.\nAnd you still end up getting very confused by the complex inheritance \nhierarchy.\n\n\nTake-Home: Having Multiple, Isolated View Tests with Single Assertions Helps\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nOne thing I definitely did conclude from this appendix was that having many\nshort unit tests for views was much more helpful than having a few tests with\na narrative series of assertions.\n\nConsider this monolithic test:\n\n\n[role=\"sourcecode skipme\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\ndef test_validation_errors_sent_back_to_home_page_template(self):\n    response = self.client.post('/lists/new', data={'text': ''})\n    self.assertEqual(List.objects.all().count(), 0)\n    self.assertEqual(Item.objects.all().count(), 0)\n    self.assertTemplateUsed(response, 'home.html')\n    expected_error = escape(\"You can't have an empty list item\")\n    self.assertContains(response, expected_error)\n----\n====\n\nThat is definitely less useful than having three individual tests, like this:\n\n[role=\"sourcecode skipme\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\n    def test_invalid_input_means_nothing_saved_to_db(self):\n        self.post_invalid_input()\n        self.assertEqual(List.objects.all().count(), 0)\n        self.assertEqual(Item.objects.all().count(), 0)\n\n    def test_invalid_input_renders_list_template(self):\n        response = self.post_invalid_input()\n        self.assertTemplateUsed(response, 'list.html')\n\n    def test_invalid_input_renders_form_with_errors(self):\n        response = self.post_invalid_input()\n        self.assertIsinstance(response.context['form'], ExistingListItemForm)\n        self.assertContains(response, escape(empty_list_error))\n----\n====\n\nThe reason is that, in the first case, an early failure means not all the\nassertions are checked.  So, if the view was accidentally saving to the\ndatabase on invalid POST, you would get an early fail, and so you wouldn't\nfind out whether it was using the right template or rendering the form.  The\nsecond formulation makes it much easier to pick out exactly what was or wasn't\nworking.\n\n\n[role=\"pagebreak-before\"]\n.Lessons Learned from CBGVs\n*******************************************************************************\n\nClass-based generic views can do anything::\n    It might not always be clear what's going on, but you can do just about\n    anything with class-based generic views.\n\nSingle-assertion unit tests help refactoring::\n    (((\"single-assertion unit tests\")))(((\"unit tests\", \"testing only one thing\")))(((\"testing best practices\")))With\neach unit test providing individual guidance on what works and what\n    doesn't, it's much easier to change the implementation of our views to\n    using this fundamentally different paradigm.(((\"\", startref=\"DJFclass28\")))\n\n*******************************************************************************\n\n"
  },
  {
    "path": "appendix_IV_testing_migrations.asciidoc",
    "content": "[[data-migrations-appendix]]\n[appendix]\nTesting Database Migrations\n---------------------------\n\n\n\n(((\"database migrations\", id=\"dbmig30\")))(((\"database testing\", \"migrations\", id=\"DBTmig30\")))Django-migrations and its predecessor South have been around for ages,\nso it's not usually necessary to test database migrations.  But it just\nso happens that we're introducing a dangerous type of migration--that is, one\nthat introduces a new integrity constraint on our data.  When I first ran\nthe migration script against staging, I saw an error.\n\nOn larger projects, where you have sensitive data, you may want the additional\nconfidence that comes from testing your migrations in a safe environment\nbefore applying them to production data, so this toy example will hopefully\nbe a useful rehearsal.\n\nAnother common reason to want to test migrations is for speed--migrations\noften involve downtime, and sometimes, when they're applied to very large\ndatasets, they can take time.  It's good to know in advance how long that\nmight be.\n\n\nAn Attempted Deploy to Staging\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\nHere's what happened to me when I first tried to deploy our new validation\nconstraints in <<chapter_18_second_deploy>>:\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*cd deploy_tools*]\n$ pass:quotes[*fab deploy:host=elspeth@staging.ottg.co.uk*]\n[...]\nRunning migrations:\n  Applying lists.0005_list_item_unique_together...Traceback (most recent call\nlast):\n  File \"/usr/local/lib/python3.7/dist-packages/django/db/backends/utils.py\",\nline 61, in execute\n    return self.cursor.execute(sql, params)\n  File\n\"/usr/local/lib/python3.7/dist-packages/django/db/backends/sqlite3/base.py\",\nline 475, in execute\n    return Database.Cursor.execute(self, query, params)\nsqlite3.IntegrityError: columns list_id, text are not unique\n[...]\n----\n\n\nWhat happened was that some of the existing data in the database violated\nthe integrity constraint, so the database was complaining when I tried to \napply it.\n\nIn order to deal with this sort of problem, we'll need to build a \"data\nmigration\".  Let's first set up a local environment to test against.\n\n\nRunning a Test Migration Locally\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWe'll use a copy of the live database to test our migration against.\n\nWARNING: Be very, very, very careful when using real data for testing.  For \n    example, you may have real customer email addresses in there, and you don't\n    want to accidentally send them a bunch of test emails.  Ask me how I know\n    this.\n\n\nEntering Problematic Data\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nStart a list with some duplicate items on your live site, as shown in\n<<dupe-data>>.\n\n[[dupe-data]]\n.A list with duplicate items\nimage::images/twp2_ad01.png[\"This list has 3 identical items\"]\n\n\nCopying Test Data from the Live Site\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nCopy the database down from live:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *scp elspeth@superlists.ottg.co.uk:\\\n/home/elspeth/sites/superlists.ottg.co.uk/database/db.sqlite3 .*\n$ *mv ../database/db.sqlite3 ../database/db.sqlite3.bak*\n$ *mv db.sqlite3 ../database/db.sqlite3*\n----\n\n\nConfirming the Error\n^^^^^^^^^^^^^^^^^^^^\n\nWe now have a local database that has not been migrated, and that contains\nsome problematic data.  We should see an error if we try to run `migrate`:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py migrate --migrate*\npython manage.py migrate\nOperations to perform:\n[...]\nRunning migrations:\n[...]\n  Applying lists.0005_list_item_unique_together...Traceback (most recent call\nlast):\n[...]\n    return Database.Cursor.execute(self, query, params)\nsqlite3.IntegrityError: columns list_id, text are not unique\n----\n\n\nInserting a Data Migration\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nhttps://docs.djangoproject.com/en/5.2/topics/migrations/#data-migrations[Data\nmigrations] are a special type of migration that modifies data in the database\nrather than changing the schema.  We need to create one that will run before\nwe apply the integrity constraint, to preventively remove any duplicates.\nHere's how we can do that:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*git rm lists/migrations/0005_list_item_unique_together.py*]\n$ pass:quotes[*python manage.py makemigrations lists --empty*]\nMigrations for 'lists':\n  0005_auto_20140414_2325.py:\n$ pass:[<strong>mv lists/migrations/0005_*.py lists/migrations/0005_remove_duplicates.py</strong>]\n----\n\nCheck out https://docs.djangoproject.com/en/5.2/topics/migrations/#data-migrations[the\nDjango docs on data migrations] for more info, but here's how we add some\ninstructions to change existing data:\n\n[role=\"sourcecode\"]\n.lists/migrations/0005_remove_duplicates.py\n====\n[source,python]\n----\n# encoding: utf8\nfrom django.db import models, migrations\n\ndef find_dupes(apps, schema_editor):\n    List = apps.get_model(\"lists\", \"List\")\n    for list_ in List.objects.all():\n        items = list_.item_set.all()\n        texts = set()\n        for ix, item in enumerate(items):\n            if item.text in texts:\n                item.text = '{} ({})'.format(item.text, ix)\n                item.save()\n            texts.add(item.text)\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('lists', '0004_item_list'),\n    ]\n\n    operations = [\n        migrations.RunPython(find_dupes),\n    ]\n----\n====\n\n\nRe-creating the Old Migration\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWe re-create the old migration using `makemigrations`, which will ensure it\nis now the sixth migration and has an explicit dependency on `0005`, the\ndata migration:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  0006_auto_20140415_0018.py:\n    - Alter unique_together for item (1 constraints)\n$ pass:[<strong>mv lists/migrations/0006_* lists/migrations/0006_unique_together.py</strong>]\n----\n\n\nTesting the New Migrations Together\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWe're now ready to run our test against the live data:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*cd deploy_tools*]\n$ pass:quotes[*fab deploy:host=elspeth@staging.ottg.co.uk*]\n[...]\n----\n\nWe'll need to restart the live Gunicorn job too:\n\n[role=\"server-commands skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:$ *sudo systemctl restart gunicorn-superlists.ottg.co.uk*\n----\n\n\nAnd we can now run our FTs against staging:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*STAGING_SERVER=staging.ottg.co.uk python manage.py test functional_tests*]\n[...]\n....\n ---------------------------------------------------------------------\nRan 4 tests in 17.308s\n\nOK\n----\n\n\nEverything seems in order!  Let's do it against live:\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*fab deploy:host=superlists.ottg.co.uk*]\n[superlists.ottg.co.uk] Executing task 'deploy'\n[...]\n----\n\n\nAnd that's a wrap.  `git add lists/migrations`, `git commit`, and so on.\n\n\nConclusions\n~~~~~~~~~~~\n\nThis exercise was primarily aimed at building a data migration and testing it\nagainst some real data.  Inevitably, this is only a drop in the ocean of the \npossible testing you could do for a migration.  You could imagine building\nautomated tests to check that all your data was preserved, comparing the\ndatabase contents before and after.  You could write individual unit tests\nfor the helper functions in a data migration.  You could spend more time\nmeasuring the time taken for migrations, and experiment with ways to speed\nit up by, for example, breaking up migrations into more or fewer component steps.\n\nRemember that this should be a relatively rare case. In my experience, I\nhaven't felt the need to test 99% of the migrations I've worked on.  But,\nshould you ever feel the need on your project, I hope you've found a few\npointers here to get started with.(((\"\", startref=\"dbmig30\")))(((\"\", startref=\"DBTmig30\")))\n\n\n\n[role=\"pagebreak-before less_space\"]\n.On Testing Database Migrations\n******************************************************************************\n\nBe wary of migrations which introduce constraints::\n    99% of migrations happen without a hitch, but be wary of any situations,\n    like this one, where you are introducing a new constraint on columns that\n    already exist.\n\n\nTest migrations for speed::\n    Once you have a larger project, you should think about testing how long\n    your migrations are going to take. Database migrations typically involve\n    downtime, as, depending on your database, the schema update operation may\n    lock the table it's working on until it completes.  It's a good idea to use\n    your staging site to find out how long a migration will take.\n\n\nBe extremely careful if using a dump of production data::\n    In order to do so, you'll want fill your staging site's database with an\n    amount of data that's commensurate to the size of your production data.\n    Explaining how to do that is outside of the scope of this book, but I will\n    say this:  if you're tempted to just take a dump of your production\n    database and load it into staging, be 'very' careful.  Production data\n    contains real customer details, and I've personally been responsible for\n    accidentally sending out a few hundred incorrect invoices after an\n    automated process on my staging server started processing the copied\n    production data I'd just loaded into it. Not a fun afternoon.\n\n\n\n******************************************************************************\n\n"
  },
  {
    "path": "appendix_IX_cheat_sheet.asciidoc",
    "content": "[[cheat-sheet]]\n[appendix]\n== Cheat Sheet\n\nBy popular demand, this \"cheat sheet\" is loosely based on the recap/summary boxes\nfrom the end of each chapter.\nThe idea is to provide a few reminders,\nand links to the chapters where you can find out more to jog your memory.\nI hope you find it useful!\n\n\n=== Initial Project Setup\n\n\n* Start with a _user story_ and map it to a first _functional test_.(((\"cheat sheet\", \"project setup\")))(((\"Django framework\", \"set up\", \"project creation\")))\n\n* Pick a test framework&mdash;`unittest` is fine, and options like `py.test`,\n  `nose`, or `Green` can also offer some advantages.\n\n* Run the functional test and see your first 'expected failure'.\n\n* Pick a web framework such as Django, and find out how to run\n  _unit tests_ against it.\n\n* Create your first _unit test_ to address the current FT failure,\n  and see it fail.\n\n* Do  your _first commit_ to a VCS like _Git_.\n\nRelevant chapters:\n<<chapter_01>>,\n<<chapter_02_unittest>>,\n<<chapter_03_unit_test_first_view>>.\n\n[role=\"pagebreak-before less_space\"]\n=== The Basic TDD Workflow: Red/Green/Refactor\n\n[role=\"two-col\"]\n* Red, Green, Refactor(((\"cheat sheet\", \"TDD workflow\")))(((\"Test-Driven Development (TDD)\", \"overall process of\")))\n* Double-loop TDD (<<Double-Loop-TDD-diagram2>>)\n* Triangulation\n* The scratchpad\n* \"3 Strikes and Refactor\"\n* \"Working State to Working State\"\n* \"YAGNI\"\n\n\n[[Double-Loop-TDD-diagram2]]\n.Double-loop TDD\nimage::images/tdd3_0405.png[\"An inner red/green/refactor loop surrounded by an outer red/green of FTs\"]\n\n\nRelevant chapters:\n<<chapter_04_philosophy_and_refactoring>>,\n<<chapter_05_post_and_database>>,\n<<chapter_07_working_incrementally>>.\n\n\n\n=== Moving Beyond Dev-Only Testing\n\n* Start system testing early.\n  Ensure your components work together: web server, static content, database.(((\"cheat sheet\", \"moving beyond dev-only testing\")))\n\n* Build a production environment early, and automate deployment to it.\n    - PaaS versus VPS\n    - Docker\n    - Ansible versus Terraform\n\n* Think through deployment pain points: the database, static files,\n  dependencies, how to customise settings, and so on.\n\n* Build a CI server as soon as possible, so that you don't have to rely\n  on self-discipline to see the tests run.\n\nRelevant chapters:\n<<part2>>,\n<<chapter_25_CI>>.\n\n[role=\"pagebreak-before less_space\"]\n=== General Testing Best Practices\n\n* Each test should test one thing.(((\"cheat sheet\", \"testing best practices\")))(((\"testing best practices\")))\n\n* Test behaviour rather than implementation.\n\n* \"Don't test constants\".\n\n* Try to think beyond the charmed path through the code,\n  and think through edge cases and error cases.\n\n* Balance the \"test desiderata\".\n\n\nRelevant chapters:\n<<chapter_04_philosophy_and_refactoring>>,\n<<chapter_14_database_layer_validation>>,\n<<chapter_15_simple_form>>,\n<<chapter_27_hot_lava>>.\n\n\n=== Selenium/Functional Testing Best Practices\n\n* Use explicit rather than implicit waits, and the interaction/wait pattern.\n\n* Avoid duplication of test code--helper methods in a base class and the\n  page pattern are possible solutions.\n\n* Avoid double-testing functionality.\n  If you have a test that covers a time-consuming process (e.g., login),\n  consider ways of skipping it in other tests\n  (but be aware of unexpected interactions between seemingly unrelated bits of functionality).\n\n* Look into BDD tools as another way of structuring your FTs.\n\nRelevant chapters:\n<<chapter_23_debugging_prod>>,\n<<chapter_25_CI>>,\n<<chapter_26_page_pattern>>.\n\n\n=== Outside-In\n\nDefault to working outside-in.  Use double-loop TDD to drive your development,\nstart at the UI/outside layers, and work your way down to the infrastructure layers.\nThis helps ensure that you write only the code you need,\nand flushes out integration issues early.\n\nRelevant chapter: <<chapter_24_outside_in>>.\n\n\n=== The Test Pyramid\n\nBe aware that integration tests will get slower and slower over time.\nFind ways to shift the bulk of your testing to unit tests\nas your project grows in size and complexity.\n\nRelevant chapter:\n<<chapter_27_hot_lava>>.\n\n"
  },
  {
    "path": "appendix_X_what_to_do_next.asciidoc",
    "content": "[[appendix4]]\n[appendix]\n== What to Do Next\n\n(((\"Test-Driven Development (TDD)\", \"future investigations\", id=\"TDDfuture35\")))\nHere I offer a few suggestions for things to investigate next,\nto develop your testing skills,\nand to apply them to some of the cool new technologies in web development\n(at the time of writing!).\n\nI might write an article about some of these in the future.\nBut why not try to beat me to it,\nand write your own blog post chronicling your attempt at any one of these?\n\n(((\"getting help\")))\nI'm very happy to answer questions and provide tips and guidance\non all these topics,\nso if you find yourself attempting one and getting stuck,\nplease don't hesitate to get in touch at obeythetestinggoat@gmail.com!\n\n\n\n=== Switch to Postgres\n\nSQLite is a wonderful little database, but it won't deal well once you\nhave more than one web worker process fielding your site's requests.\nPostgres is everyone's favourite database these days,\nso find out how to install and configure it.\n\nYou'll need to figure out a place to store the usernames and passwords\nfor your local, staging, and production Postgres servers.\nTake a look at <<chapter_12_ansible>> for inspiration.\n\nExperiment with keeping your unit tests running with SQLite,\nand compare how much faster they are than running against Postgres.\nSet it up so that your local machine uses SQLite for testing,\nbut your CI server uses Postgres.\n\nDoes any of your functionality actually depend on Postgres-specific features?\nWhat should you do then?\n\n\n=== Run Your Tests Against Different Browsers\n\nSelenium supports all sorts of different browsers,\nincluding Chrome, Safari, and Internet Exploder.\nTry them all out and see if your FT suite behaves any differently.\n\nIn my experience, switching browsers tends to expose all sorts of race\nconditions in Selenium tests, and you will probably need to use the\ninteraction/wait pattern a lot more.\n\n\n\n=== The Django Admin Site\n\nImagine a story where a user emails you wanting to \"claim\" an anonymous list.\nLet's say we implement a manual solution to this,\ninvolving the site administrator manually changing the record using the Django admin site.\n\nFind out how to switch on the admin site, and have a play with it.\nWrite an FT that shows a normal, non–logged-in user creating a list,\nthen have an admin user log in, go to the admin site, and assign the list to the user.\nThe user can then see it in their \"My Lists\" page.\n\n\n\n=== Write Some Security Tests\n\nExpand on the login, my lists, and sharing tests--what do you need to write to\nassure yourself that users can only do what they're authorized to?\n\n\n\n=== Test for Graceful Degradation\n\nWhat would happen if our email server goes down?\nCan we at least show an apologetic error message to our users?\n\n\n\n=== Caching and Performance Testing\n\n\nFind out how to install and configure `memcached`.\nFind out how to use Apache's `ab` to run a performance test.\nHow does it perform with and without caching?\nCan you write an automated test that will fail if caching is not enabled?\nWhat about the dreaded problem of cache invalidation?\nCan tests help you to make sure your cache invalidation logic is solid?\n\n\n\n=== JavaScript Frameworks\n\nCheck out React, Vue.js, or perhaps my old favourite, Elm.\n\n\n\n\n=== Async and Websockets\n\nSupposing two users are working on the same list at the same time.\nWouldn't it be nice to see real-time updates,\nso if the other person adds an item to the list, you see it immediately?\nA persistent connection between client and server using websockets\nis the way to get this to work.\n\nCheck out Django's async features and see if you can use them to implement dynamic notifications.\n\nTo test it, you'll need two browser instances\n(like we used for the list sharing tests),\nand check that notifications of the actions from one appear in the other,\nwithout needing to refresh the page...\n\n\n\n=== Switch to Using pytest\n\n\n`pytest` lets you write unit tests with less boilerplate.\nTry converting some of your unit tests to using 'py.test'.\nYou may need to use a plugin to get it to play nicely with Django.\n\n\n=== Check Out coverage.py\n\nNed Batchelder's `coverage.py` will tell you what your 'test coverage' is--what\npercentage of your code is covered by tests.\nNow, in theory, because we've been using rigorous TDD,\nwe should always have 100% coverage.\nBut it's nice to know for sure,\nand it's also a very useful tool for working on projects\nthat didn't have tests from the beginning.\n\n\n=== Client-Side Encryption\n\nHere's a fun one: what if our users are paranoid about the NSA, and decide they\nno longer want to trust their lists to The Cloud?  Can you build a JavaScript\nencryption system, where the user can enter a password to encypher their list\nitem text before it gets sent to the server?\n\nOne way of testing it might be to have an \"administrator\" user that goes to\nthe Django admin view to inspect users' lists, and checks that they are stored\nencrypted in the database.\n\n\n\n=== Your Suggestion Here\n\nWhat do you think I should put here?\nSuggestions, please!\n(((\"\", startref=\"TDDfuture35\")))\n\n"
  },
  {
    "path": "appendix_bdd.asciidoc",
    "content": "[[appendix_bdd]]\n[appendix]\n== Behaviour-Driven Development (BDD) Tools\n\n\n.Warning, Content From Second Edition\n*******************************************************************************\nThis appendix is from the second edition of the book,\nso the listings have not been updated for the latest versions\nof Django and Python.\n\nAs always, feedback is welcome, but especially-especially\nsince this stuff is all so new.\nLet me know how you get on :)\n\n*******************************************************************************\n\nNow I haven't used the BDD tools in this appendix\nfor more than a few weeks in a production project,\nso I can't claim any deep expertise.\nBut, I did like what I have seen of it,\nand I thought that you deserved at least a whirlwind tour.\n\nIn this appendix, we'll take some of the tests we wrote in a\n\"normal\" FT, and convert them to using BDD tools.\n\n=== What Is BDD and What are BDD Tools?\n\n(((\"behavior-driven development (BDD)\", \"defined\")))\n(((\"behavior-driven development (BDD)\", id=\"bdd31\")))\nBDD itself is a practice rather than a toolset--it's\nthe approach of testing your application by testing the _behaviour_ that we expect it\nto have, from the point of view of a user (the\nhttps://en.wikipedia.org/wiki/Behavior-driven_development[Wikipedia entry]\nhas quite a good overview).\nEssentially, whenever you've seen me say\n\"it's better to test behaviour rather than implementation\",\nI've been advocating for BDD.\n\n\n==== Gherkin and Cucumber\n\n(((\"behavior-driven development (BDD)\", \"tools for\")))\n(((\"Gherkin\", id=\"gherkin31\")))\n(((\"Cucumber\")))\nBut the term has become closely associated with a particular set of tools\nfor doing BDD, and particularly the\nhttps://github.com/cucumber/cucumber/wiki/Gherkin[Gherkin syntax],\nwhich is a human-readable DSL for writing functional (or acceptance) tests.\nGherkin originally came out of the Ruby world,\nwhere it's associated with a test runner called\nhttps://cucumber.io/[Cucumber].\n\nWe'll be talking about these tools in this appendix.\n\n\nTIP:  BDD as a practice is not the same as the toolset and the Gherkin syntax\n\n(((\"Lettuce\")))\n(((\"Behave\")))\nIn the Python world, we have a couple of equivalent test running tools,\nhttp://lettuce.it/[Lettuce] and http://pythonhosted.org/behave/[Behave].\nOf these, only Behave was compatible with Python 3 at the time of writing,\nso that's what we'll use.\nWe'll also use a plugin called\nhttps://pythonhosted.org/behave-django/[behave-django].\n\n\n[role=\"pagebreak-before\"]\n.Getting the Code for These Examples\n**********************************************************************\n\n(((\"code examples, obtaining and using\")))\nI'm going to use the example from <<chapter_24_outside_in>>.\nWe have a basic to-do lists site, and we want to add a new feature:\nlogged-in users should be able to view the lists they've authored in one place.\nUp until this point, all lists are effectively anonymous.\n\nIf you've been following along with the book, I'm going to assume you can skip\nback to the code for that point.  If you want to pull it from my repo, the\nplace to go is the\nhttps://github.com/hjwp/book-example/tree/chapter_17[chapter_17 branch].\n\n**********************************************************************\n\n\n=== Basic Housekeeping\n\n(((\"behavior-driven development (BDD)\", \"directory creation\")))We\nmake a directory for our BDD \"features,\" add a _steps_ directory (we'll find\nout what these are shortly!), and placeholder for our first feature:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *mkdir -p features/steps*\n$ *touch features/my_lists.feature*\n$ *touch features/steps/my_lists.py*\n$ *tree features*\nfeatures\n├── my_lists.feature\n└── steps\n    └── my_lists.py\n----\n\n\nWe install `behave-django`, and add it to _settings.py_:\n\n\n[role=\"dofirst-ch35l000\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip install behave-django*\n----\n\n[role=\"sourcecode\"]\n.superlists/settings.py\n====\n[source,diff]\n----\n--- a/superlists/settings.py\n+++ b/superlists/settings.py\n@@ -40,6 +40,7 @@ INSTALLED_APPS = [\n     'lists',\n     'accounts',\n     'functional_tests',\n+    'behave_django',\n ]\n----\n====\n\nAnd then run `python manage.py behave` as a sanity check:\n\n[subs=\"\"]\n----\n$ <strong>python manage.py behave</strong>\nCreating test database for alias 'default'...\n0 features passed, 0 failed, 0 skipped\n0 scenarios passed, 0 failed, 0 skipped\n0 steps passed, 0 failed, 0 skipped, 0 undefined\nTook 0m0.000s\nDestroying test database for alias 'default'...\n----\n\n\n=== Writing an FT as a \"Feature\" Using Gherkin Syntax\n\n(((\"behavior-driven development (BDD)\", \"functional test using Gherkin syntax\")))\n(((\"functional tests (FTs)\", \"using Gherkin syntax\", secondary-sortas=\"Gherkin syntax\")))\nUp until now, we've been writing our FTs using human-readable comments\nthat describe the new feature in terms of a user story, interspersed\nwith the Selenium code required to execute each step in the story.\n\nBDD enforces a distinction between those two--we write our human-readable\nstory using a human-readable (if occasionally somewhat awkward) syntax\ncalled \"Gherkin\", and that is called the \"Feature\".  Later, we'll map\neach line of Gherkin to a function that contains the Selenium code necessary\nto implement that \"step.\"\n\nHere's what a Feature for our new \"My lists\" page could look like:\n\n[role=\"sourcecode\"]\n.features/my_lists.feature\n====\n[source,gherkin]\n----\nFeature: My Lists\n    As a logged-in user\n    I want to be able to see all my lists in one page\n    So that I can find them all after I've written them\n\n    Scenario: Create two lists and see them on the My Lists page\n\n        Given I am a logged-in user\n\n        When I create a list with first item \"Reticulate Splines\"\n            And I add an item \"Immanentize Eschaton\"\n            And I create a list with first item \"Buy milk\"\n\n        Then I will see a link to \"My lists\"\n\n        When I click the link to \"My lists\"\n        Then I will see a link to \"Reticulate Splines\"\n        And I will see a link to \"Buy milk\"\n\n        When I click the link to \"Reticulate Splines\"\n        Then I will be on the \"Reticulate Splines\" list page\n----\n====\n\n[role=\"pagebreak-before\"]\n==== As-a /I want to/So that\n\nAt the top you'll notice the As-a/I want to/So that clause.  This is\noptional, and it has no executable counterpart--it's just a slightly\nformalised way of capturing the \"who and why?\" aspects of a user story,\ngently encouraging the team to think about the justifications for each\nfeature.\n\n\n==== Given/When/Then\n\nGiven/When/Then is the real core of a BDD test.  This trilobite formulation\nmatches the setup/exercise/assert pattern we've seen in our unit tests, and\nit represents the setup and assumptions phase, an exercise/action phase, and\na subsequent assertion/observation phase.  There's more info on the\nhttps://github.com/cucumber/cucumber/wiki/Given-When-Then[Cucumber wiki].\n\n\n==== Not Always a Perfect Fit!\n\nAs you can see, it's not always easy to shoe-horn a user story into exactly\nthree steps!  We can use the `And` clause to expand on a step, and I've\nadded multiple `When` steps and subsequent `Then`'s to illustrate further\naspects of our \"My lists\" page.(((\"\", startref=\"gherkin31\")))\n\n\n=== Coding the Step Functions\n\n(((\"behavior-driven development (BDD)\", \"step functions\")))\nWe now build the counterpart to our Gherkin-syntax feature,\nwhich are the \"step\" functions that will actually implement them in code.\n\n\n==== Generating Placeholder Steps\n\nWhen we run `behave`, it helpfully tells us about all the steps we need to\nimplement:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py behave*\nFeature: My Lists # features/my_lists.feature:1\n  As a logged-in user\n  I want to be able to see all my lists in one page\n  So that I can find them all after I've written them\n  Scenario: Create two lists and see them on the My Lists page  #\nfeatures/my_lists.feature:6\n    Given I am a logged-in user                                 # None\n    Given I am a logged-in user                                 # None\n    When I create a list with first item \"Reticulate Splines\"   # None\n    And I add an item \"Immanentize Eschaton\"                    # None\n    And I create a list with first item \"Buy milk\"              # None\n    Then I will see a link to \"My lists\"                        # None\n    When I click the link to \"My lists\"                         # None\n    Then I will see a link to \"Reticulate Splines\"              # None\n    And I will see a link to \"Buy milk\"                         # None\n    When I click the link to \"Reticulate Splines\"               # None\n    Then I will be on the \"Reticulate Splines\" list page        # None\n\n\nFailing scenarios:\n  features/my_lists.feature:6  Create two lists and see them on the My Lists\npage\n\n\n0 features passed, 1 failed, 0 skipped\n0 scenarios passed, 1 failed, 0 skipped\n0 steps passed, 0 failed, 0 skipped, 10 undefined\nTook 0m0.000s\n\nYou can implement step definitions for undefined steps with these snippets:\n\n@given(u'I am a logged-in user')\ndef step_impl(context):\n    raise NotImplementedError(u'STEP: Given I am a logged-in user')\n\n@when(u'I create a list with first item \"Reticulate Splines\"')\ndef step_impl(context):\n[...]\n----\n\nAnd you'll notice all this output is nicely coloured, as shown in\n<<behave-output>>.\n\n[[behave-output]]\n.Behave with coloured console ouptut\n\nimage::images/twp2_ae01.png[\"Colourful console output\"]\n\nIt's encouraging us to copy and paste these snippets, and use them as\nstarting points to build our steps.\n\n\n=== First Step Definition\n\nHere's a first stab at making a step for our \"Given I am a logged-in user\"\nstep. I started by stealing the code for `self.create_pre_authenticated_session`\nfrom 'functional_tests/test_my_lists.py', and adapting it slightly (removing\nthe server-side version, for example, although it would be easy to re-add\nlater).\n\n[role=\"sourcecode small-code\"]\n.features/steps/my_lists.py\n====\n[source,python]\n----\nfrom behave import given, when, then\nfrom functional_tests.management.commands.create_session import \\\n    create_pre_authenticated_session\nfrom django.conf import settings\n\n\n@given('I am a logged-in user')\ndef given_i_am_logged_in(context):\n    session_key = create_pre_authenticated_session(email='edith@example.com')\n    ## to set a cookie we need to first visit the domain.\n    ## 404 pages load the quickest!\n    context.browser.get(context.get_url(\"/404_no_such_url/\"))\n    context.browser.add_cookie(dict(\n        name=settings.SESSION_COOKIE_NAME,\n        value=session_key,\n        path='/',\n    ))\n----\n====\n//ch35l004\n\nThe 'context' variable needs a little explaining—it's a sort of global\nvariable, in the sense that it's passed to each step that's executed, and it\ncan be used to store information that we need to share between steps. Here\nwe've assumed we'll be storing a browser object on it, and the `server_url`.\nWe end up using it a lot like we used `self` when we were writing `unittest`\nFTs.\n\n\n=== setUp and tearDown Equivalents in environment.py\n\nSteps can make changes to state in the `context`, but the place to do\npreliminary set-up, the equivalent of `setUp`, is in a file called\n_environment.py_:\n\n\n[role=\"sourcecode\"]\n.features/environment.py\n====\n[source,python]\n----\nfrom selenium import webdriver\n\ndef before_all(context):\n    context.browser = webdriver.Firefox()\n\ndef after_all(context):\n    context.browser.quit()\n\ndef before_feature(context, feature):\n    pass\n----\n====\n//ch35l005\n\n\n=== Another Run\n\nAs a sanity check, we can do another run, to see if the new step works and\nthat we really can start a browser:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py behave*\n[...]\n1 step passed, 0 failed, 0 skipped, 9 undefined\n----\n\nThe usual reams of output, but we can see that it seems to have made it through\nthe first step; let's define the rest of them.\n\n\n=== Capturing Parameters in Steps\n\n(((\"behavior-driven development (BDD)\", \"capturing parameters in steps\")))We'll\nsee how Behave allows you to capture parameters from step descriptions.\nOur next step says:\n\n[role=\"sourcecode currentcontents\"]\n.features/my_lists.feature\n====\n[source,gherkin]\n----\n    When I create a list with first item \"Reticulate Splines\"\n----\n====\n\nAnd the autogenerated step definition looked like this:\n\n[role=\"sourcecode small-code skipme\"]\n.features/steps/my_lists.py\n====\n[source,python]\n----\n@given('I create a list with first item \"Reticulate Splines\"')\ndef step_impl(context):\n    raise NotImplementedError(\n        u'STEP: When I create a list with first item \"Reticulate Splines\"'\n    )\n----\n====\n\nWe want to be able to create lists with arbitrary first items, so it would be\nnice to somehow capture whatever is between those quotes, and pass them in as\nan argument to a more generic function.  That's a common requirement in BDD,\nand Behave has a nice syntax for it, reminiscent of the new-style Python string\nformatting syntax:\n\n\n[role=\"sourcecode\"]\n.features/steps/my_lists.py (ch35l006)\n====\n[source,python]\n----\n[...]\n\n@when('I create a list with first item \"{first_item_text}\"')\ndef create_a_list(context, first_item_text):\n    context.browser.get(context.get_url('/'))\n    context.browser.find_element(By.ID, 'id_text').send_keys(first_item_text)\n    context.browser.find_element(By.ID, 'id_text').send_keys(Keys.ENTER)\n    wait_for_list_item(context, first_item_text)\n----\n====\n\nNeat, huh?\n\nNOTE: Capturing parameters for steps is one of the most powerful features\n    of the BDD syntax.\n\n\nAs usual with Selenium tests, we will need an explicit wait.  Let's re-use\nour `@wait` decorator from 'base.py':\n\n\n[role=\"sourcecode\"]\n.features/steps/my_lists.py (ch35l007)\n====\n[source,python]\n----\nfrom functional_tests.base import wait\n[...]\n\n\n@wait\ndef wait_for_list_item(context, item_text):\n    context.test.assertIn(\n        item_text,\n        context.browser.find_element_by_css_selector('#id_list_table').text\n    )\n----\n====\n\n\nSimilarly, we can add to an existing list, and see or click on links:\n\n\n[role=\"sourcecode\"]\n.features/steps/my_lists.py (ch35l008)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.keys import Keys\n[...]\n\n\n@when('I add an item \"{item_text}\"')\ndef add_an_item(context, item_text):\n    context.browser.find_element(By.ID, 'id_text').send_keys(item_text)\n    context.browser.find_element(By.ID, 'id_text').send_keys(Keys.ENTER)\n    wait_for_list_item(context, item_text)\n\n\n@then('I will see a link to \"{link_text}\"')\n@wait\ndef see_a_link(context, link_text):\n    context.browser.find_element_by_link_text(link_text)\n\n\n@when('I click the link to \"{link_text}\"')\ndef click_link(context, link_text):\n    context.browser.find_element_by_link_text(link_text).click()\n----\n====\n\nNotice we can even use our `@wait` decorator on steps themselves.\n\n\nAnd finally the slightly more complex step that says I am on the\npage for a particular list:\n\n[role=\"sourcecode\"]\n.features/steps/my_lists.py (ch35l009)\n====\n[source,python]\n----\n@then('I will be on the \"{first_item_text}\" list page')\n@wait\ndef on_list_page(context, first_item_text):\n    first_row = context.browser.find_element_by_css_selector(\n        '#id_list_table tr:first-child'\n    )\n    expected_row_text = '1: ' + first_item_text\n    context.test.assertEqual(first_row.text, expected_row_text)\n----\n====\n\n[role=\"pagebreak-before\"]\nNow we can run it and see our first expected failure:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py behave*]\n\nFeature: My Lists # features/my_lists.feature:1\n  As a logged-in user\n  I want to be able to see all my lists in one page\n  So that I can find them all after I've written them\n  Scenario: Create two lists and see them on the My Lists page  #\nfeatures/my_lists.feature:6\n    Given I am a logged-in user                                 #\nfeatures/steps/my_lists.py:19\n    When I create a list with first item \"Reticulate Splines\"   #\nfeatures/steps/my_lists.py:31\n    And I add an item \"Immanentize Eschaton\"                    #\nfeatures/steps/my_lists.py:39\n    And I create a list with first item \"Buy milk\"              #\nfeatures/steps/my_lists.py:31\n    Then I will see a link to \"My lists\"                        #\nfunctional_tests/base.py:12\n      Traceback (most recent call last):\n[...]\n        File \"features/steps/my_lists.py\", line 49, in see_a_link\n          context.browser.find_element_by_link_text(link_text)\n[...]\n      selenium.common.exceptions.NoSuchElementException: Message: Unable to\nlocate element: My lists\n\n[...]\n\nFailing scenarios:\n  features/my_lists.feature:6  Create two lists and see them on the My Lists\npage\n\n0 features passed, 1 failed, 0 skipped\n0 scenarios passed, 1 failed, 0 skipped\n4 steps passed, 1 failed, 5 skipped, 0 undefined\n----\n\nYou can see how the output really gives you a sense of how far through the\n\"story\" of the test we got: we manage to create our two lists successfully, but\nthe \"My lists\" link does not appear.\n\n\n=== Comparing the Inline-Style FT\n\n(((\"behavior-driven development (BDD)\", \"comparing inline-style FT\")))\nI'm not going to run through the implementation of the feature,\nbut you can see how the test will drive development\njust as well as the inline-style FT would have.\n\nLet's have a look at it, for comparison:\n\n[role=\"sourcecode skipme\"]\n.functional_tests/test_my_lists.py\n====\n[source,python]\n----\ndef test_logged_in_users_lists_are_saved_as_my_lists(self):\n    # Edith is a logged-in user\n    self.create_pre_authenticated_session('edith@example.com')\n\n    # She goes to the homepage and starts a list\n    self.browser.get(self.live_server_url)\n    self.add_list_item('Reticulate splines')\n    self.add_list_item('Immanentize eschaton')\n    first_list_url = self.browser.current_url\n\n    # She notices a \"My lists\" link, for the first time.\n    self.browser.find_element_by_link_text('My lists').click()\n\n    # She sees that her list is in there, named according to its\n    # first list item\n    self.wait_for(\n        lambda: self.browser.find_element_by_link_text('Reticulate splines')\n    )\n    self.browser.find_element_by_link_text('Reticulate splines').click()\n    self.wait_for(\n        lambda: self.assertEqual(self.browser.current_url, first_list_url)\n    )\n\n    # She decides to start another list, just to see\n    self.browser.get(self.live_server_url)\n    self.add_list_item('Click cows')\n    second_list_url = self.browser.current_url\n\n    # Under \"my lists\", her new list appears\n    self.browser.find_element_by_link_text('My lists').click()\n    self.wait_for(\n        lambda: self.browser.find_element_by_link_text('Click cows')\n    )\n    self.browser.find_element_by_link_text('Click cows').click()\n    self.wait_for(\n        lambda: self.assertEqual(self.browser.current_url, second_list_url)\n    )\n\n    # She logs out.  The \"My lists\" option disappears\n    self.browser.find_element_by_link_text('Log out').click()\n    self.wait_for(lambda: self.assertEqual(\n        self.browser.find_elements_by_link_text('My lists'),\n        []\n    ))\n----\n====\n\nIt's not entirely an apples-to-apples comparison, but we can look at the\nnumber of lines of code in <<table-code-compare>>.\n\n[[table-code-compare]]\n.Lines of code comparison\n[options=\"header\"]\n|==============================================================================\n|BDD                            |Standard FT\n|Feature file: 20 (3 optional)  |test function body: 45\n|Steps file: 56 lines           |helper functions: 23\n|==============================================================================\n\nThe comparison isn't perfect, but you might say that the feature file and the\nbody of a \"standard FT\" test function are equivalent in that they present the\nmain \"story\" of a test, while the steps and helper functions represent the\n\"hidden\" implementation details.  If you add them up, the total numbers are\npretty similar, but notice that they're spread out differently: the BDD tests\nhave made the story more concise, and pushed more work out into the hidden\nimplementation details.\n\n\n=== BDD Encourages Structured Test Code\n\n(((\"behavior-driven development (BDD)\", \"structured test code encouraged by\")))\n(((\"functional tests (FTs)\", \"structuring test code\")))\nThis is the real appeal, for me: the BDD tool has _forced_ us to structure our\ntest code.  In the inline-style FT, we're free to use as many lines as we want\nto implement a step, as described by its comment line.  It's very hard to\nresist the urge to just copy-and-paste code from elsewhere, or just from\nearlier on in the test.   You can see that, by this point in the book, I've\nbuilt just a couple of helper functions (like `get_item_input_box`).\n\nIn contrast, the BDD syntax has immediately forced me to have a separate\nfunction for each step, so I've already built some very reusable code to:\n\n* Start a new list\n* Add an item to an existing list\n* Click on a link with particular text\n* Assert that I'm looking at a particular list's page\n\nBDD really encourages you to write test code that seems to match well with\nthe business domain, and to use a layer of abstraction between the story of\nyour FT and its implementation in code.\n\nThe ultimate expression of this is that, theoretically, if you wanted to\nchange programming languages, you could keep all your features in Gherkin\nsyntax exactly as they are, and throw away the Python steps and replace them\nwith steps implemented in another language.\n\n\n=== The Page Pattern as an Alternative\n\n(((\"behavior-driven development (BDD)\", \"page pattern\")))\n(((\"page pattern\")))\nIn <<chapter_26_page_pattern>> of the book,\nI present an example of the \"Page pattern\",\nwhich is an object-oriented approach to structuring your Selenium tests.\nHere's a reminder of what it looks like:\n\n[role=\"sourcecode skipme\"]\n.functional_tests/test_sharing.py\n====\n[source,python]\n----\nfrom .my_lists_page import MyListsPage\n[...]\n\nclass SharingTest(FunctionalTest):\n\n    def test_can_share_a_list_with_another_user(self):\n        # [...]\n        self.browser.get(self.live_server_url)\n        list_page = ListPage(self).add_list_item('Get help')\n\n        # She notices a \"Share this list\" option\n        share_box = list_page.get_share_box()\n        self.assertEqual(\n            share_box.get_attribute('placeholder'),\n            'your-friend@example.com'\n        )\n\n        # She shares her list.\n        # The page updates to say that it's shared with Oniciferous:\n        list_page.share_list_with('oniciferous@example.com')\n----\n====\n\n//TODO: all these skipmes could be tested by doing a checkout of the page_pattern branch\n\nAnd the +Page+ class looks like this:\n\n[role=\"sourcecode small-code skipme\"]\n.functional_tests/lists_pages.py\n====\n[source,python]\n----\nclass ListPage(object):\n\n    def __init__(self, test):\n        self.test = test\n\n\n    def get_table_rows(self):\n        return self.test.browser.find_elements_by_css_selector('#id_list_table tr')\n\n\n    @wait\n    def wait_for_row_in_list_table(self, item_text, item_number):\n        row_text = '{}: {}'.format(item_number, item_text)\n        rows = self.get_table_rows()\n        self.test.assertIn(row_text, [row.text for row in rows])\n\n\n    def get_item_input_box(self):\n        return self.test.browser.find_element(By.ID, 'id_text')\n----\n====\n\nSo it's definitely possible to implement a similar layer of abstraction,\nand a sort of DSL, in inline-style FTs, whether it's by using the Page\npattern or whatever structure you prefer--but now it's a matter of\nself-discipline, rather than having a framework that pushes you towards\nit.\n\nNOTE: In fact, you can actually use the Page pattern with BDD as well, as\n    a resource for your steps to use when navigating the pages of your site.\n\n\n=== BDD Might Be Less Expressive than Inline Comments\n\n(((\"behavior-driven development (BDD)\", \"vs. inline comments\", secondary-sortas=\"inline comments\")))\n(((\"inline comments\")))\nOn the other hand, I can also see potential for the Gherkin syntax to\nfeel somewhat restrictive.  Compare how expressive and readable the\ninline-style comments are, with the slightly awkward BDD feature:\n\n\n[role=\"sourcecode skipme\"]\n.functional_tests/test_my_lists.py\n====\n[source,python]\n----\n    # Edith is a logged-in user\n    # She goes to the homepage and starts a list\n    # She notices a \"My lists\" link, for the first time.\n    # She sees that her list is in there, named according to its\n    # first list item\n    # She decides to start another list, just to see\n    # Under \"my lists\", her new list appears\n    # She logs out.  The \"My lists\" option disappears\n[...]\n----\n====\n\nThat's much more readable and natural than our slightly forced Given/Then/When\nincantations, and, in a way, might encourage more user-centric thinking. (There\nis a syntax in Gherkin for including \"comments\" in a feature file, which would\nmitigate this somewhat, but I gather that it's not widely used.)\n\n\n=== Will Nonprogrammers Write Tests?\n\n(((\"behavior-driven development (BDD)\", \"benefits and drawbacks of\")))I\nhaven't touched on one of the original promises of BDD, which is that\nnonprogrammers--business or client representatives perhaps--might actually\nwrite the Gherkin syntax.  I'm quite skeptical about whether this would\nactually work in the real world, but I don't think that detracts from the other\npotential benefits of BDD.\n\n\n=== Some Tentative Conclusions\n\nI've only dipped my toes into the BDD world, so I'm hesitant to draw any firm\nconclusions. I find the \"forced\" structuring of FTs into steps very appealing\nthough--in that it looks like it has the potential to encourage a lot of reuse in your\nFT code, and that it neatly separates concerns between describing the story\nand implementing it, and that it forces us to think about things in terms of\nthe business domain, rather than in terms of \"what we need to do with\nSelenium.\"\n\nBut there's no free lunch. The Gherkin syntax is restrictive, compared to\nthe total freedom offered by inline FT comments.\n\nI also would like to see how BDD scales once you have not just one or two\nfeatures, and four or five steps, but several dozen features and hundreds of\nlines of steps code.\n\nOverall, I would say it's definitely worth investigating, and I will probably\nuse BDD for my next personal project.\n\nMy thanks to Daniel Pope, Rachel Willmer, and Jared Contrascere for their\nfeedback on this chapter.\n\n\n.BDD Conclusions\n*******************************************************************************\n\nEncourages structured, reusable test code::\n    By separating concerns, breaking your FTs out into the human-readable,\n    Gherkin syntax \"feature\" file and a separate implementation of steps\n    functions, BDD has the potential to encourage more reusable and manageable\n    test code.\n\nIt may come at the expense of readability::\n    The Gherkin syntax, for all its attempt to be human-readable, is ultimately\n    a constraint on human language, and so it may not capture nuance and\n    intention as well as inline comments do.\n\nTry it! I will::\n    As I keep saying, I haven't used BDD on a real project, so you should take\n    my words with a heavy pinch of salt, but I'd like to give it a hearty\n    endorsement.  I'm going to try it out on the next project I can, and I'd\n    encourage you to do so as well.(((\"\", startref=\"bdd31\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "appendix_fts_for_external_dependencies.asciidoc",
    "content": "[[appendix_fts_for_external_dependencies]]\n[appendix]\n== The Subtleties of Functionally Testing External Dependencies\n\nYou might remember from <<options-for-testing-real-email>>\na point at which we wanted to test sending email from the server.\n\nHere were the options we considered:\n\n1. We could build a \"real\" end-to-end test, and have our tests\n   log in to an email server, and retrieve the email from there.\n   That's what I did in the first and second edition.\n\n2. You can use a service like Mailinator or Mailsac,\n   which give you an email account to send to,\n   and some APIs for checking what mail has been delivered.\n\n3. We can use an alternative, fake email backend,\n   whereby Django will save the emails to a file on disk for example,\n   and we can inspect them there.\n\n4. Or we could give up on testing email on the server.\n   If we have a minimal smoke test that the server _can_ send emails,\n   then we don't need to test that they are _actually_ delivered.\n\nIn the end we decided not to bother,\nbut let's spend a bit of time in this appendix trying out options 1 and 3,\njust to see some of the fiddliness and trade-offs involved.\n\n\n=== How to Test Email End-To-End with POP3\n\nHere's an example helper function that can retrieve a real email\nfrom a real POP3 email server,\nusing the horrifically tortuous Python standard library POP3 client.\n\nTo make it work, we'll need an email address to receive the email.\nI signed up for a Yahoo account for testing,\nbut you can use any email service you like, as long as it offers POP3 access.\n\nYou will need to set the\n`RECEIVER_EMAIL_PASSWORD` environment variable in the console that's running the FT.\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *export RECEIVER_EMAIL_PASSWORD=otheremailpasswordhere*\n----\n\n[role=\"sourcecode skipme\"]\n.src/functional_tests/test_login.py (ch23l001)\n====\n[source,python]\n----\nimport os\nimport poplib\nimport re\nimpot time\n[...]\n\ndef retrieve_pop3_email(receiver_email, subject, pop3_server, pop3_password):\n    email_id = None\n    start = time.time()\n    inbox = poplib.POP3_SSL(pop3_server)\n    try:\n        inbox.user(receiver_email)\n        inbox.pass_(pop3_password)\n        while time.time() - start < POP3_TIMEOUT:\n            # get 10 newest messages\n            count, _ = inbox.stat()\n            for i in reversed(range(max(1, count - 10), count + 1)):\n                print(\"getting msg\", i)\n                _, lines, __ = inbox.retr(i)\n                lines = [l.decode(\"utf8\") for l in lines]\n                print(lines)\n                if f\"Subject: {subject}\" in lines:\n                    email_id = i\n                    body = \"\\n\".join(lines)\n                    return body\n            time.sleep(5)\n    finally:\n        if email_id:\n            inbox.dele(email_id)\n        inbox.quit()\n----\n====\n\nIf you're curious, I'd encourage you to try this out in your FTs.\nIt definitely _can_ work.\nBut, having tried it in the first couple of editions of the book.\nI have to say it's fiddly to get right,\nand often flaky, which is a highly undesirable property for a testing tool.\nSo let's leave that there for now.\n\n\n=== Using a Fake Email Backend For Django\n\nNext let's investigate using a filesystem-based email backend.\nAs we'll see, although it definitely has the advantage\nthat everything stays local on our own machine\n(there are no calls over the internet),\nthere are quite a few things to watch out for.\n\nLet's say that, if we detect an environment variable `EMAIL_FILE_PATH`,\nwe switch to Django's file-based backend:\n\n\n.src/superlists/settings.py (ch23l002)\n====\n[source,python]\n----\nEMAIL_HOST = \"smtp.gmail.com\"\nEMAIL_HOST_USER = \"obeythetestinggoat@gmail.com\"\nEMAIL_HOST_PASSWORD = os.environ.get(\"EMAIL_PASSWORD\")\nEMAIL_PORT = 587\nEMAIL_USE_TLS = True\n# Use fake file-based backend if EMAIL_FILE_PATH is set\nif \"EMAIL_FILE_PATH\" in os.environ:\n    EMAIL_BACKEND = \"django.core.mail.backends.filebased.EmailBackend\"\n    EMAIL_FILE_PATH = os.environ[\"EMAIL_FILE_PATH\"]\n----\n====\n\nHere's how we can adapt our tests to conditionally use the email file,\ninstead of Django's `mail.outbox`, if the env var is set when running our tests:\n\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_login.py (ch23l003)\n====\n[source,python]\n----\nclass LoginTest(FunctionalTest):\n    def retrieve_email_from_file(self, sent_to, subject, emails_dir):  # <1>\n        latest_emails_file = sorted(Path(emails_dir).iterdir())[-1]  # <2>\n        latest_email = latest_emails_file.read_text().split(\"-\" * 80)[-1]  # <3>\n        self.assertIn(subject, latest_email)\n        self.assertIn(sent_to, latest_email)\n        return latest_email\n\n    def retrieve_email_from_django_outbox(self, sent_to, subject):  # <4>\n        email = mail.outbox.pop()\n        self.assertIn(sent_to, email.to)\n        self.assertEqual(email.subject, subject)\n        return email.body\n\n    def wait_for_email(self, sent_to, subject):  # <5>\n        \"\"\"\n        Retrieve email body,\n        from a file if the right env var is set,\n        or get it from django.mail.outbox by default\n        \"\"\"\n        if email_file_path := os.environ.get(\"EMAIL_FILE_PATH\"):  # <6>\n            return self.wait_for(  # <7>\n                lambda: self.retrieve_email_from_file(sent_to, subject, email_file_path)\n            )\n        else:\n            return self.retrieve_email_from_django_outbox(sent_to, subject)\n\n    def test_login_using_magic_link(self):\n        [...]\n----\n====\n\n<1> Here's our helper method for getting email contents from a file.\n    It takes the configured email directory as an argument,\n    as well as the sent-to address and expected subject.\n\n<2> Django saves a new file with emails every time you restart the server.\n    The filename has a timestamp in it,\n    so we can get the latest one by sorting the files in our test directory.\n    Check out the https://docs.python.org/3/library/pathlib.html[Pathlib] docs\n    if you haven't used it before, it's a nice, relatively new way of working with files in Python.\n\n<3> The emails in the file are separated by a line of 80 hyphens.\n\n<4> This is the matching helper for getting the email from `mail.outbox`.\n\n<5> Here's where we dispatch to the right helper based on whether the env\n    var is set.\n\n<6> Checking whether an environment variable is set, and using its value if so,\n    is one of the (relatively few) places where it's nice to use the walrus operator.\n\n<7> I'm using a `wait_for()` here because anything involving reading and writing from files,\n    especially across the filesystem mounts inside and outside of Docker,\n    has a potential race condition.\n\n\nWe'll need a couple more minor changes to the FT, to use the helper:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_login.py (ch23l004)\n====\n[source,diff]\n----\n@@ -59,15 +59,12 @@ class LoginTest(FunctionalTest):\n         )\n\n         # She checks her email and finds a message\n-        email = mail.outbox.pop()\n-        self.assertIn(TEST_EMAIL, email.to)\n-        self.assertEqual(email.subject, SUBJECT)\n+        email_body = self.wait_for_email(TEST_EMAIL, SUBJECT)\n\n         # It has a URL link in it\n-        self.assertIn(\"Use this link to log in\", email.body)\n-        url_search = re.search(r\"http://.+/.+$\", email.body)\n-        if not url_search:\n-            self.fail(f\"Could not find url in email body:\\n{email.body}\")\n+        self.assertIn(\"Use this link to log in\", email_body)\n+        if not (url_search := re.search(r\"http://.+/.+$\", email_body, re.MULTILINE)):\n+            self.fail(f\"Could not find url in email body:\\n{email_body}\")\n         url = url_search.group(0)\n         self.assertIn(self.live_server_url, url)\n----\n====\n\n// TODO backport that walrus\n\nNow let's set that file path, and mount it inside our docker container,\nso that it's available both inside and outside the container:\n\n[subs=\"attributes+,specialcharacters,quotes\"]\n----\n# set a local env var for our path to the emails file\n$ *export EMAIL_FILE_PATH=/tmp/superlists-emails*\n# make sure the file exists\n$ *mkdir -p $EMAIL_FILE_PATH*\n# re-run our container, with the EMAIL_FILE_PATH as an env var, and mounted.\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=./src/db.sqlite3,target=/src/db.sqlite3 \\\n    --mount type=bind,source=$EMAIL_FILE_PATH,target=$EMAIL_FILE_PATH \\  <1>\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e EMAIL_PASSWORD \\\n    -e EMAIL_FILE_PATH \\  <2>\n    -it superlists*\n----\n\n<1> Here's where we mount the emails file so we can see it\n    both inside and outside the container\n\n<2> And here's where we pass the path as an env var,\n    once again re-exporting the variable from the current shell.\n\n\nAnd we can rerun our FT, first without using Docker or the EMAIL_FILE_PATH,\njust to check we didn't break anything:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*./src/manage.py test functional_tests.test_login*]\n[...]\nOK\n----\n\nAnd now _with_ Docker and the EMAIL_FILE_PATH:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *TEST_SERVER=localhost:8888 EMAIL_FILE_PATH=/tmp/superlists-emails \\\n  python src/manage.py test functional_tests*\n[...]\nOK\n----\n\n\nIt works!  Hooray.\n\n\n=== Double-Checking our Test and Our Fix\n\nAs always, we should be suspicious of any test that we've only ever seen pass!\nLet's see if we can make this test fail.\n\nBefore we do--we've been in the detail for a bit,\nit's worth reminding ourselves of what the actual bug was,\nand how we're fixing it!\nThe bug was, the server was crashing when it tried to send an email.\nThe reason was, we hadn't set the `EMAIL_PASSWORD` environment variable.\nWe managed to repro the bug in Docker.\nThe actual _fix_ is to set that env var,\nboth in Docker and eventually on the server.\nNow we want to have a _test_ that our fix works,\nand we looked in to a few different options,\nsettling on using the `filebased.EmailBackend\"\n`EMAIL_BACKEND` setting using the `EMAIL_FILE_PATH` environment variable.\n\nNow, I say we haven't seen the test fail,\nbut actually we have, when we repro'd the bug.\nIf we unset the `EMAIL_PASSWORD` env var, it will fail again.\nI'm more worried about the new parts of our tests,\nthe bits where we go and read from the file at `EMAIL_FILE_PATH`.\nHow can we make that part fail?\n\nWell, how about if we deliberately break our email-sending code?\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch23l005)\n====\n[source,python]\n----\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    token = Token.objects.create(email=email)\n    url = request.build_absolute_uri(\n        reverse(\"login\") + \"?token=\" + str(token.uid),\n    )\n    message_body = f\"Use this link to log in:\\n\\n{url}\"\n    # send_mail(  <1>\n    #     \"Your login link for Superlists\",\n    #     message_body,\n    #     \"noreply@superlists\",\n    #     [email],\n    # )\n    messages.success(\n        request,\n        \"Check your email, we've sent you a link you can use to log in.\",\n    )\n    return redirect(\"/\")\n----\n====\n\n<1> We just comment out the entire send_email block.\n\n\nWe rebuild our docker image:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n# check our env var is set\n$ *echo $EMAIL_FILE_PATH*\n/tmp/superlists-emails\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=./src/db.sqlite3,target=/src/db.sqlite3 \\\n    --mount type=bind,source=$EMAIL_FILE_PATH,target=$EMAIL_FILE_PATH \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e EMAIL_PASSWORD \\\n    -e EMAIL_FILE_PATH \\\n    -it superlists*\n----\n\n// TODO: aside on moujnting /src/?\n\nAnd we rerun our test:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *TEST_SERVER=localhost:8888 EMAIL_FILE_PATH=/tmp/superlists-emails \\\n  ./src/manage.py test functional_tests.test_login\n[...]\nRan 1 test in 2.513s\n\nOK\n----\n\n\nEh?  How did that pass?\n\n\n=== Testing side-effects is fiddly!\n\nWe've run into an example of the kinds of problems you often encounter\nwhen our tests involve side-effects.\n\nLet's have a look in our test emails directory:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ls $EMAIL_FILE_PATH*\n20241120-153150-262004991022080.log\n20241120-153154-262004990980688.log\n20241120-153301-272143941669888.log\n----\n\nEvery time we restart the server, it opens a new file,\nbut only when it first tries to send an email.\nBecause we've commented out the whole email-sending block,\nour test instead picks up on an old email,\nwhich still has a valid url in it,\nbecause the token is still in the database.\n\nNOTE: You'll run into a similar issue if you test with \"real\" emails in POP3.\n    How do you make sure you're not picking up an email from a previous test run?\n\nLet's clear out the db:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *rm src/db.sqlite3 && ./src/manage.py migrate*\nOperations to perform:\n  Apply all migrations: accounts, auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying accounts.0001_initial... OK\n  Applying accounts.0002_token... OK\n  Applying contenttypes.0001_initial... OK\n  Applying contenttypes.0002_remove_content_type_name... OK\n  Applying auth.0001_initial... OK\n----\n\n\nAnd...\n\ncmdgg\n[subs=\"specialcharacters,quotes\"]\n----\n$ *TEST_SERVER=localhost:8888 ./src/manage.py test functional_tests.test_login*\n[...]\nERROR: test_login_using_magic_link (functional_tests.test_login.LoginTest.test_login_using_magic_link)\n    self.wait_to_be_logged_in(email=TEST_EMAIL)\n    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: #id_logout; [...]\n----\n\nOK sure enough, the `wait_to_be_logged_in()` helper is failing,\nbecause now, although we have found an email, its token is invalid.\n\n\nHere's another way to make the tests fail:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:[<strong>rm $EMAIL_FILE_PATH/*</strong>]\n----\n\nNow when we run the FT:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *TEST_SERVER=localhost:8888 ./src/manage.py test functional_tests.test_login*\nERROR: test_login_using_magic_link\n(functional_tests.test_login.LoginTest.test_login_using_magic_link)\n[...]\n    email_body = self.wait_for_email(TEST_EMAIL, SUBJECT)\n[...]\n    return self.wait_for(\n           ~~~~~~~~~~~~~^\n        lambda: self.retrieve_email_from_file(sent_to, subject, email_file_path)\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n[...]\n    latest_emails_file = sorted(Path(emails_dir).iterdir())[-1]\n                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^\nIndexError: list index out of range\n----\n\nWe see there are no email files, because we're not sending one.\n\nNOTE: In this configuration of Docker + `filebase.EmailBackend`,\n  we now have to manage side effects in two locations:\n  the database at _src/db.sqlite3_, and the email files in _/tmp_.\n  What Django used to do for us thanks to LiveServerTestCase\n  is now all our responsibility, and as you can see, it's hard to get right.\n  This is a tradeoff to be aware of when writing tests against \"real\" systems.\n\n\nStill, this isn't quite satisfactory.\nLet's try a different way to make our tests fail,\nwhere we _will_ send an email, but we'll give it the wrong contents:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch23l006)\n====\n[source,python]\n----\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    token = Token.objects.create(email=email)\n    url = request.build_absolute_uri(\n        reverse(\"login\") + \"?token=\" + str(token.uid),\n    )\n    message_body = f\"Use this link to log in:\\n\\n{url}\"\n    send_mail(\n        \"Your login link for Superlists\",\n        \"HAHA NO LOGIN URL FOR U\",  # <1>\n        \"noreply@superlists\",\n        [email],\n    )\n    messages.success(\n        request,\n        \"Check your email, we've sent you a link you can use to log in.\",\n    )\n    return redirect(\"/\")\n----\n====\n\n<1> We _do_  send an email, but it won't contain a login URL.\n\nLet's rebuild again:\n\n[subs=\"specialcharacters,quotes\"]\n----\n# check our env var is set\n$ *echo $EMAIL_FILE_PATH*\n/tmp/superlists-emails\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=./src/db.sqlite3,target=/src/db.sqlite3 \\\n    --mount type=bind,source=$EMAIL_FILE_PATH,target=$EMAIL_FILE_PATH \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e EMAIL_PASSWORD \\\n    -e EMAIL_FILE_PATH \\\n    -it superlists*\n----\n\nNow how do our tests look?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests*]\nFAIL: test_login_using_magic_link\n(functional_tests.test_login.LoginTest.test_login_using_magic_link)\n[...]\n    email_body = self.wait_for_email(TEST_EMAIL, SUBJECT)\n[...]\n    self.assertIn(\"Use this link to log in\", email_body)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'Use this link to log in' not found in 'Content-Type:\ntext/plain; charset=\"utf-8\"\\nMIME-Version: 1.0\\nContent-Transfer-Encoding:\n7bit\\nSubject: Your login link for Superlists\\nFrom: noreply@superlists\\nTo:\nedith@example.com\\nDate: Wed, 13 Nov 2024 18:00:55 -0000\\nMessage-ID:\n[...]\\n\\nHAHA NO LOGIN URL FOR\nU\\n-------------------------------------------------------------------------------\\n'\n----\n\nOK good, that's the error we wanted!\nI think we can be fairly confident that this testing setup\ncan genuinely test that emails are sent properly.\nLet's revert our temporarily-broken _views.py_,\nrebuild, and make sure the tests pass once again.\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git stash*\n$ *docker build [...]*\n# separate terminal\n$ *TEST_SERVER=localhost:8888 EMAIL_FILE_PATH=/tmp/superlists-emails [...]\n[...]\nOK\n----\n\n\nNOTE: It may seem like I've gone through a lot of back-and-forth,\n  but I wanted to give you a flavour of the fiddliness involved\n  in these kinds of tests that involve a lot of side-effects.\n\n\n=== Decision Time: Which Test Strategy Will We Keep\n\nLet's recap our three options:\n\n\n.Testing Strategy Tradeoffs\n[cols=\"1,1,1\"]\n|=======\n| Strategy | Pros | Cons\n| End-to-end with POP3 | Maximally realistic, tests the whole system | Slow, fiddly, unreliable\n| File-based fake email backend | Faster, more reliable, no network calls, tests end-to-end (albeit with fake components) | Still Fiddly, requires managing db & filesystem side-effects\n| Give up on testing email on the server/Docker | Fast, simple | Less confidence that things work \"for real\"\n|=======\n\nThis is a common problem in testing integration with external systems,\nhow far should we go?  How realistic should we make our tests?\n\nIn the book in the end, I suggested we go for the last option,\nie give up. Email itself is a well-understood protocol\n(reader, it's been around since _before I was born_, and that's a whiles ago now)\nand Django has supported sending email for more than a decade,\nso I think we can afford to say, in this case,\nthat the costs of building testing tools for email outweigh the benefits.\n\nBut not all external dependencies are as well-understood as email.\nIf you're working with a new API, or a new service,\nyou may well decide it's worth putting in the effort to get a \"real\" end-to-end functional test to work.\n\nAs always, it's tradeoffs all the way down, folks.\n"
  },
  {
    "path": "appendix_github_links.asciidoc",
    "content": "[[appendix_github_links]]\n[appendix]\n== Source Code Examples\n\n(((\"code examples, obtaining and using\")))\nAll\nof the code examples I've used in the book are available in\nhttps://github.com/hjwp/book-example/[my repo on GitHub].\nSo, if you ever want to compare your code against mine,\nyou can take a look at it there.\n\nEach chapter has its own branch named after it, like so:\nhttps://github.com/hjwp/book-example/tree/chapter_01.\n\nBe aware that each branch contains all of the commits for that chapter,\nso its state represents the code at the 'end' of the chapter.\n\n=== Full List of Links for Each Chapter\n\n|===\n| Chapter | GitHub branch name & hyperlink\n\n| <<chapter_01>>\n| https://github.com/hjwp/book-example/tree/chapter_01[chapter_01]\n\n| <<chapter_02_unittest>>\n| https://github.com/hjwp/book-example/tree/chapter_02_unittest[chapter_02_unittest]\n\n| <<chapter_03_unit_test_first_view>>\n| https://github.com/hjwp/book-example/tree/chapter_03_unit_test_first_view[chapter_03_unit_test_first_view]\n\n| <<chapter_04_philosophy_and_refactoring>>\n| https://github.com/hjwp/book-example/tree/chapter_04_philosophy_and_refactoring[chapter_04_philosophy_and_refactoring]\n\n| <<chapter_05_post_and_database>>\n| https://github.com/hjwp/book-example/tree/chapter_05_post_and_database[chapter_05_post_and_database]\n\n| <<chapter_06_explicit_waits_1>>\n| https://github.com/hjwp/book-example/tree/chapter_06_explicit_waits_1[chapter_06_explicit_waits_1]\n\n| <<chapter_07_working_incrementally>>\n| https://github.com/hjwp/book-example/tree/chapter_07_working_incrementally[chapter_07_working_incrementally]\n\n| <<chapter_08_prettification>>\n| https://github.com/hjwp/book-example/tree/chapter_08_prettification[chapter_08_prettification]\n\n| <<chapter_09_docker>>\n| https://github.com/hjwp/book-example/tree/chapter_09_docker[chapter_09_docker]\n\n| <<chapter_10_production_readiness>>\n| https://github.com/hjwp/book-example/tree/chapter_10_production_readiness[chapter_10_production_readiness]\n\n| <<chapter_11_server_prep>>\n| https://github.com/hjwp/book-example/tree/chapter_11_server_prep[chapter_11_server_prep]\n\n| <<chapter_13_organising_test_files>>\n| https://github.com/hjwp/book-example/tree/chapter_13_organising_test_files[chapter_13_organising_test_files]\n\n| <<chapter_14_database_layer_validation>>\n| https://github.com/hjwp/book-example/tree/chapter_14_database_layer_validation[chapter_14_database_layer_validation]\n\n| <<chapter_15_simple_form>>\n| https://github.com/hjwp/book-example/tree/chapter_15_simple_form[chapter_15_simple_form]\n\n| <<chapter_16_advanced_forms>>\n| https://github.com/hjwp/book-example/tree/chapter_16_advanced_forms[chapter_16_advanced_forms]\n\n| <<chapter_17_javascript>>\n| https://github.com/hjwp/book-example/tree/chapter_17_javascript[chapter_17_javascript]\n\n| <<chapter_18_second_deploy>>\n| https://github.com/hjwp/book-example/tree/chapter_18_second_deploy[chapter_18_second_deploy]\n\n| <<chapter_19_spiking_custom_auth>>\n| https://github.com/hjwp/book-example/tree/chapter_19_spiking_custom_auth[chapter_19_spiking_custom_auth]\n\n| <<chapter_20_mocking_1>>\n| https://github.com/hjwp/book-example/tree/chapter_20_mocking_1[chapter_20_mocking_1]\n\n| <<chapter_21_mocking_2>>\n| https://github.com/hjwp/book-example/tree/chapter_21_mocking_2[chapter_21_mocking_2]\n\n| <<chapter_22_fixtures_and_wait_decorator>>\n| https://github.com/hjwp/book-example/tree/chapter_22_fixtures_and_wait_decorator[chapter_22_fixtures_and_wait_decorator]\n\n| <<chapter_23_debugging_prod>>\n| https://github.com/hjwp/book-example/tree/chapter_23_debugging_prod[chapter_23_debugging_prod]\n\n| <<chapter_24_outside_in>>\n| https://github.com/hjwp/book-example/tree/chapter_24_outside_in[chapter_24_outside_in]\n\n| <<chapter_25_CI>>\n| https://github.com/hjwp/book-example/tree/chapter_25_CI[chapter_25_CI]\n\n| <<chapter_26_page_pattern>>\n| https://github.com/hjwp/book-example/tree/chapter_26_page_pattern[chapter_26_page_pattern]\n\n| Online Appendix: Test Isolation, and Listening to Your Tests\n| https://github.com/hjwp/book-example/tree/appendix_purist_unit_tests[appendix_purist_unit_tests]\n\n| Online Appendix: BDD\n| https://github.com/hjwp/book-example/tree/appendix_bdd[appendix_bdd]\n\n| Online Apendix: Building a REST API\n| https://github.com/hjwp/book-example/tree/appendix_rest_api[appendix_rest_api]\n\n|===\n\n\n\n=== Using Git to Check Your Progress\n\nIf you feel like developing your Git-Fu a little further, you can add\nmy repo as a 'remote':\n\n[role=\"skipme\"]\n-----\ngit remote add harry https://github.com/hjwp/book-example.git\ngit fetch harry\n-----\n\nAnd then, to check your difference from the 'end' of <<chapter_04_philosophy_and_refactoring>>:\n\n[role=\"skipme\"]\n----\ngit diff harry/chapter_04_philosophy_and_refactoring\n----\n\nGit can handle multiple remotes, so you can still do this even if you're\nalready pushing your code up to GitHub or Bitbucket.\n\nBe aware that the precise order of, say, methods in a class may differ\nbetween your version and mine.  It may make diffs hard to read.\n\n\n=== Downloading a ZIP File for a Chapter\n\nIf, for whatever reason, you want to \"start from scratch\" for a chapter,\nor skip ahead,footnote:[\nI don't recommend skipping ahead.\nI haven't designed the chapters to stand on their own;\neach relies on the previous ones, so it may be more confusing than anything else...]\nand/or you're just not comfortable with Git,\nyou can download a version of my code as a ZIP file,\nfrom URLs following this pattern:\n\nhttps://github.com/hjwp/book-example/archive/chapter_01.zip\n\nhttps://github.com/hjwp/book-example/archive/chapter_04_philosophy_and_refactoring.zip\n\n\n=== Don't Let it Become a Crutch!\n\nTry not to sneak a peek at the answers unless you're really, really stuck.\nLike I said at the beginning of <<chapter_03_unit_test_first_view>>,\nthere's a lot of value in debugging errors all by yourself,\nand in real life, there's no \"harrys repo\" to check against\nand find all the answers.\n\nHappy coding!\n"
  },
  {
    "path": "appendix_logging.asciidoc",
    "content": "[[appendix_logging]]\r\n[apendix]\r\nLogging\r\n~~~~~~~\r\n\r\nUsing Hierarchical Logging Config\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nNOTE: this content is left over from the first edition, and has not been integrated\r\n    into the new edition\r\n\r\n\r\nWhen we hacked in the `logging.warning` earlier, we were using the root logger.\r\nThat's not normally a good idea, since any third-party package can mess with the\r\nroot logger.  The normal pattern is to use a logger named after the file you're\r\nin, by using:\r\n\r\n[role=\"skipme\"]\r\n[source,python]\r\n----\r\nlogger = logging.getLogger(__name__)\r\n----\r\n\r\nLogging configuration is hierarchical, so you can define \"parent\" loggers for\r\ntop-level modules, and all the Python modules inside them will inherit that\r\nconfig.\r\n\r\nHere's how we add a logger for both our apps into 'settings.py':\r\n\r\n[role=\"sourcecode skipme\"]\r\n.superlists/settings.py\r\n====\n[source,python]\r\n----\r\nLOGGING = {\r\n   'version': 1,\r\n   'disable_existing_loggers': False,\r\n   'handlers': {\r\n       'console': {\r\n           'level': 'DEBUG',\r\n           'class': 'logging.StreamHandler',\r\n       },\r\n   },\r\n   'loggers': {\r\n        'django': {\r\n            'handlers': ['console'],\r\n        },\r\n        'accounts': {\r\n            'handlers': ['console'],\r\n        },\r\n        'lists': {\r\n            'handlers': ['console'],\r\n        },\r\n    },\r\n    'root': {'level': 'INFO'},\r\n}\r\n----\r\n====\n\r\nNow 'accounts.models', 'accounts.views', 'accounts.authentication', and all \r\nthe others will inherit the `logging.StreamHandler` from the parent 'accounts'\r\nlogger.  \r\n\r\nUnfortunately, because of Django's project structure, there's no \r\nway of defining a top-level logger for your whole project (aside from using\r\nthe root logger), so you have to define one logger per app.\r\n\r\n\r\nHere's how to write a test for logging behaviour:\r\n\r\n[role=\"sourcecode skipme\"]\r\n.accounts/tests/test_authentication.py (ch18l023)\n====\r\n[source,python]\r\n----\r\nimport logging\r\n[...]\r\n\r\n@patch('accounts.authentication.requests.post')\r\nclass AuthenticateTest(TestCase):\r\n    [...]\r\n\r\n    def test_logs_non_okay_responses_from_persona(self, mock_post):\r\n        response_json = {\r\n            'status': 'not okay', 'reason': 'eg, audience mismatch'\r\n        }\r\n        mock_post.return_value.ok = True\r\n        mock_post.return_value.json.return_value = response_json  #<1>\r\n\r\n        logger = logging.getLogger('accounts.authentication')  #<2>\r\n        with patch.object(logger, 'warning') as mock_log_warning:  #<3>\r\n            self.backend.authenticate('an assertion')\r\n\r\n        mock_log_warning.assert_called_once_with(\r\n            'Persona says no. Json was: {}'.format(response_json)  #<4>\r\n        )\r\n----\r\n====\n\r\n<1> We set up our test with some data that should cause some logging.\r\n\r\n<2> We retrieve the actual logger for the module we're testing.\r\n\r\n<3> We use `patch.object` to temporarily mock out its warning function,\r\n    by using `with` to make it a 'context manager' around the function we're\r\n    testing.\r\n\r\n<4> And then it's available for us to make assertions against.\r\n\r\nThat gives us:\r\n\r\n[role=\"skipme\"]\r\n[subs=\"specialcharacters,macros\"]\r\n----\r\nAssertionError: Expected 'warning' to be called once. Called 0 times.\r\n----\r\n\r\nLet's just try it out, to make sure we really are testing what we think\r\nwe are:\r\n\r\n[role=\"sourcecode skipme\"]\r\n.accounts/authentication.py (ch18l024)\n====\r\n[source,python]\r\n----\r\nimport logging\r\nlogger = logging.getLogger(__name__)\r\n[...]\r\n\r\n        if response.ok and response.json()['status'] == 'okay':\r\n            [...]\r\n        else:\r\n            logger.warning('foo')\r\n----\r\n====\n\r\nWe get the expected failure:\r\n\r\n\r\n[role=\"skipme\"]\r\n[subs=\"specialcharacters,macros\"]\r\n----\r\nAssertionError: Expected call: warning(\"Persona says no. Json was: {'status':\r\n'not okay', 'reason': 'eg, audience mismatch'}\")\r\nActual call: warning('foo')\r\n----\r\n\r\nAnd so we settle in with our real implementation:\r\n\r\n[role=\"sourcecode skipme\"]\r\n.accounts/authentication.py (ch18l025)\n====\r\n[source,python]\r\n----\r\n    else:\r\n        logger.warning(\r\n            'Persona says no. Json was: {}'.format(response.json())\r\n        )\r\n----\r\n====\n\r\n\r\n[role=\"skipme\"]\r\n[subs=\"specialcharacters,macros\"]\r\n----\r\n$ pass:quotes[*python manage.py test accounts*]\r\n[...]\r\nRan 15 tests in 0.033s\r\n\r\nOK\r\n----\r\n\r\nYou can easily imagine how you could test more combinations at this point,\r\nif you wanted different error messages for `response.ok != True`, and so on.\r\n\r\n.More notes\r\n*******************************************************************************\r\n\r\nUse loggers named after the module you're in::\r\n    The root logger is a single global object, available to any library that's\r\n    loaded in your Python process, so you're never quite in control of it. \r\n    Instead, follow the `logging.getLogger(__name__)` pattern to get one that's\r\n    unique to your module, but that inherits from a top-level configuration you\r\n    control.\r\n\r\nTest important log messages::\r\n    As we saw, log messages can be critical to debugging issues in production.\r\n    If a log message is important enough to keep in your codebase, it's\r\n    probably important enough to test.  We follow the rule of thumb that\r\n    anything above `logging.INFO` definitely needs a test.  Using\r\n    `patch.object` on the logger for the module you're testing is one\r\n    convenient way of unit testing it.\r\n\r\n*******************************************************************************\r\n\r\n"
  },
  {
    "path": "appendix_purist_unit_tests.asciidoc",
    "content": "[[appendix_purist_unit_tests]]\n[appendix]\n== Test Isolation, and \"Listening to Your Tests\"\n\n\n.Warning, Appendix Not Updated\n*******************************************************************************\n🚧 Warning, this appendix is the 2e version, and uses Django 1.11\n\nThis appendix and all the following ones are the second edition versions, so they still use Django 1.11, Python 3.8, and so on.\n\nTo follow along with this appendix, it’s probably easiest to reset your code to match my example code as it was in the 2e, by resetting to: https://github.com/hjwp/book-example/tree/chapter_outside_in\n\nAnd you should also probably delete and re-create your virtualenv with\n* Python 3.8 or 3.9\n* and Django 1.11 (pip install \"django <2\")\n\nAlternatively, you can muddle through\nand try and figure out how to make things work with Django 5 etc,\nbut be aware that the listings below won’t be quite right.\n\n*******************************************************************************\n\n(((\"functional tests (FTs)\", \"ensuring isolation\", id=\"FTisolat23\")))\nThis appendix picks up from a point about half-way through <<chapter_24_outside_in>>,\nwhen we made the decision to leave a unit test failing in the views layer\nwhile we proceeded to write more tests and more code at the models layer\nto get it to pass: <<revisit_this_point_with_isolated_tests>>.\n\nWe got away with it because our app is simple,\nbut in a more complex application,\nthis would feel more risky.\nIs there a way to \"finish\" a higher level,\neven when the lower levels don't exist yet?footnote:[\nI'm grateful to Gary Bernhardt,\nwho took a look at an early draft of the <<chapter_24_outside_in>>,\nand encouraged me to get into a longer discussion of test isolation.]\n\nIn this appendix we'll explore the using mocks\nto stand in for parts of the code we haven't written yet,\nenabling a form of outside-in development with isolated tests at each layer.\n\nWARNING: This is an example of \"London-school\" TDD,\n    which is not the style I usually use,\n    which means I'm not necessarily the best guide to the topic.\n    If you're intrigued, the seminal book on the topic is\n    \"GOOSGBT\", aka \n    http://www.growing-object-oriented-software.com/[Growing Object-Oriented Software Guided by Tests]\n    by Steve Freeman and Nat Pryce,\n    and I enthusiastically recommend you read that\n    as a better guide to London-style TDD.\n\n(((\"isolation, ensuring\", \"benefits and drawbacks of\")))\nAs we'll see, using mocks in this way can be a lot of work,\nbut it can be a way to use our tests to give us feedback on design,\nand thus encourage us to write better code.\n\nNOTE: I revisited some of the tradeoffs outlined here in my\n    my https://www.cosmicpython.com[second book on architecture patterns].\n\n\n\n=== Revisiting Our Decision Point: The Views Layer Depends on Unwritten Models Code\n\n\n(((\"isolation, ensuring\", \"failed test example\")))\nLet's revisit the point we were at halfway through the outside-in chapter,\nwhen we couldn't get the `new_list` view to work\nbecause lists didn't have the `.owner` attribute yet.\n\nWe'll actually go back in time and check out the old codebase using the tag we\nsaved earlier, so that we can see how things would have worked if we'd used\nmore isolated tests:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git switch -c more-isolation*  # a branch for this experiment\n$ *git reset --hard revisit_this_point_with_isolated_tests*\n----\n\nHere's what our failing test looks like:\n\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n        self.client.post('/lists/new', data={'text': 'new item'})\n        list_ = List.objects.first()\n        self.assertEqual(list_.owner, user)\n----\n====\n\nAnd here's what our attempted solution looked like:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List()\n        list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n----\n====\n\nAnd at this point, the view test is failing because we don't have the model\nlayer yet:\n\n----\n    self.assertEqual(list_.owner, user)\nAttributeError: 'List' object has no attribute 'owner'\n----\n\nNOTE: You won't see this error unless you actually check out the old code\n    and revert 'lists/models.py'.  You should definitely do this; part of\n    the objective of this appendix is to see whether we really can write\n    tests for a models layer that doesn't exist yet.\n\n\nA First Attempt at Using Mocks for Isolation\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n\n\n\n(((\"isolation, ensuring\", \"using mocks for\", secondary-sortas=\"mocks for\", id=\"IEmock23\")))(((\"mocks\", \"isolating tests using\", id=\"Misolate23\")))Lists\ndon't have owners yet, but we can let the views layer tests pretend they\ndo by using a bit of mocking:\n\n//IDEA: rename all \"mockList\" to \"mockListClass\"...\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l003)\n====\n[source,python]\n----\nfrom unittest.mock import patch\n[...]\n\n    @patch('lists.views.List')  #<1>\n    @patch('lists.views.ItemForm')  #<2>\n    def test_list_owner_is_saved_if_user_is_authenticated(\n        self, mockItemFormClass, mockListClass  #<3>\n    ):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n\n        self.client.post('/lists/new', data={'text': 'new item'})\n\n        mock_list = mockListClass.return_value  #<4>\n        self.assertEqual(mock_list.owner, user)  #<5>\n----\n====\n\n<1> We mock out the `List` class to be able to get access to any lists\n    that might be created by the view.\n\n<2> We also mock out the `ItemForm`. Otherwise, our form will\n    raise an error when we call `form.save()`, because it can't use a\n    mock object as the foreign key for the +Item+ it wants to create.\n    Once you start mocking, it can be hard to stop!\n\n<3> The mock objects are injected into the test's arguments in the\n    opposite order to which they're declared. Tests with lots of mocks\n    often have this strange signature, with the dangling `):`.  You get\n    used to it!\n\n<4> The list instance that the view will have access to\n    will be the return value of the mocked `List` class.\n\n<5> And we can make assertions about whether the `.owner` attribute is set on\n    it.\n\nIf we try to run this test now, it should pass:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 37 tests in 0.145s\nOK\n----\n\nIf you don't see a pass, make sure that your views code in 'views.py' is\nexactly as I've shown it, using `List()`, not `List.objects.create`.\n\n\nNOTE: Using mocks does tie you to specific ways of using an API.  This is one\n    of the many trade-offs involved in the use of mock objects.\n\n\nUsing Mock side_effects to Check the Sequence of Events\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\nThe trouble with this test is that it can still let us get away with writing\nthe wrong code by mistake.  Imagine if we accidentally call +save+ before we\nwe assign the owner:\n\n[role=\"sourcecode\"]\n.lists/views.py\n====\n[source,python]\n----\n    if form.is_valid():\n        list_ = List()\n        list_.save()\n        list_.owner = request.user\n        form.save(for_list=list_)\n        return redirect(list_)\n----\n====\n\nThe test, as it's written now, still passes:\n\n----\nOK\n----\n\nSo strictly speaking, we need to check not just that the owner is assigned, but that\nit's assigned 'before' we call +save+ on our list object.\n\nHere's how we could test the sequence of events using mocks--you can mock out\na function, and use it as a spy to check on the state of the world at the\nmoment it's called:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l005)\n====\n[source,python]\n----\n    @patch('lists.views.List')\n    @patch('lists.views.ItemForm')\n    def test_list_owner_is_saved_if_user_is_authenticated(\n        self, mockItemFormClass, mockListClass\n    ):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n        mock_list = mockListClass.return_value\n\n        def check_owner_assigned():  #<1>\n            self.assertEqual(mock_list.owner, user)\n        mock_list.save.side_effect = check_owner_assigned  #<2>\n\n        self.client.post('/lists/new', data={'text': 'new item'})\n\n        mock_list.save.assert_called_once_with()  #<3>\n----\n====\n\n\n<1> We define a function that makes the assertion about the thing we\n    want to happen first: checking that the list's owner has been set.\n\n<2> We assign that check function as a `side_effect` to the thing we\n    want to check happened second.  When the view calls our mocked\n    save function, it will go through this assertion.  We make sure to\n    set this up before we actually call the function we're testing.\n\n<3> Finally, we make sure that the function with the `side_effect` was\n    actually triggered--that is, that we did `.save()`.  Otherwise, our\n    assertion may actually never have been run.\n\nTIP: Two common mistakes when you're using mock side effects are assigning the\n    side effect too late (i.e., 'after' you call the function under test), and\n    forgetting to check that the side-effect function was actually called. And\n    by common, I mean, \"I made both these mistakes several times _while writing\n    this chapter_.&rdquo;\n\nAt this point, if you've still got the \"broken\" code from earlier, where we\nassign the owner but call +save+ in the wrong order, you should now see a\nfail:\n\n----\nFAIL: test_list_owner_is_saved_if_user_is_authenticated\n(lists.tests.test_views.NewListTest)\n[...]\n  File \"...goat-book/lists/views.py\", line 17, in new_list\n    list_.save()\n[...]\n  File \"...goat-book/lists/tests/test_views.py\", line 74, in\ncheck_owner_assigned\n    self.assertEqual(mock_list.owner, user)\nAssertionError: <MagicMock name='List().owner' id='140691452447208'> != <User:\nUser object>\n----\n\nNotice how the failure happens when we try to save, and then go inside\nour `side_effect` function.\n\nWe can get it passing again like this:\n\n[role=\"sourcecode\"]\n.lists/views.py\n====\n[source,python]\n----\n    if form.is_valid():\n        list_ = List()\n        list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n----\n====\n//006\n\n\n...\n\n----\nOK\n----\n\n(((\"\", startref=\"IEmock23\")))(((\"\", startref=\"Misolate23\")))But, boy, that's getting to be an ugly test!\n\n\n\nListen to Your Tests: Ugly Tests Signal a Need to Refactor\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n\n(((\"isolation, ensuring\", \"refactoring ugly tests\")))(((\"refactoring\")))Whenever\nyou find yourself having to write a test like this, and you're finding\nit hard work, it's likely that your tests are trying to tell you something.\nEight lines of setup (two lines for mocks, three to set up a user, and three more for our side-effect function) is way too many.\n\nWhat this test is trying to tell us is that our view is doing too much work,\ndealing with creating a form, creating a new list object, 'and' deciding whether\nor not to save an owner for the list.\n\nWe've already seen that we can make our views simpler and easier to understand\nby pushing some of the work down to a form class. Why does the view need to\ncreate the list object?  Perhaps our `ItemForm.save` could do that?  And why\ndoes the view need to make decisions about whether or not to save the\n`request.user`?  Again, the form could do that.\n\nWhile we're giving this form more responsibilities, it feels like it should\nprobably get a new name too.  We could call it `NewListForm` instead, since\nthat's a better representation of what it does...something like this?\n\n[role=\"sourcecode skipme\"]\n.lists/views.py\n====\n[source,python]\n----\n# don't enter this code yet, we're only imagining it.\n\ndef new_list(request):\n    form = NewListForm(data=request.POST)\n    if form.is_valid():\n        list_ = form.save(owner=request.user)  # creates both List and Item\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n----\n====\n\nThat would be neater!  Let's see how we'd get to that state by using\nfully isolated tests.\n\n\nRewriting Our Tests for the View to Be Fully Isolated\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"isolation, ensuring\", \"view layer\", id=\"IEviews23\")))Our\nfirst attempt at a test suite for this view was highly 'integrated'.  It\nneeded the database layer and the forms layer to be fully functional in order\nfor it to pass.   We've started trying to make it more isolated, so let's now go\nall the way.\n\n\nKeep the Old Integrated Test Suite Around as a Sense Check\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's rename our old `NewListTest` class to `NewListViewIntegratedTest`,\nand throw away our attempt at a mocky test for saving the owner, putting\nback the integrated version, with a skip on it for now:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l008)\n====\n[source,python]\n----\nimport unittest\n[...]\n\nclass NewListViewIntegratedTest(TestCase):\n\n    def test_can_save_a_POST_request(self):\n        [...]\n\n    @unittest.skip\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n        self.client.post('/lists/new', data={'text': 'new item'})\n        list_ = List.objects.first()\n        self.assertEqual(list_.owner, user)\n----\n====\n\nTIP: Have you heard the term \"integration test\" and are wondering what the\n    difference is from an \"integrated test\"?  Go and take a peek at the\n    definitions box in <<chapter_27_hot_lava>>.\n\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 37 tests in 0.139s\nOK\n----\n\n\nA New Test Suite with Full Isolation\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's start with a blank slate, and see if we can use isolated tests to drive\na replacement of our `new_list` view.  We'll call it `new_list2`, build it\nalongside the old view, and when we're ready, swap it in and see if\nthe old integrated tests all still pass:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l009)\n====\n[source,python]\n----\ndef new_list(request):\n    [...]\n\ndef new_list2(request):\n    pass\n----\n====\n\n\nThinking in Terms of Collaborators\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\nIn order to rewrite our tests to be fully isolated, we need to throw out our\nold way of thinking about the tests in terms of the \"real\" effects of the view\non things like the database, and instead think of it in terms of the objects it\ncollaborates with, and how it interacts with them.\n\nIn the new world, the view's main collaborator will be a form object, so we\nmock that out in order to be able to fully control it, and in order to be able\nto define, by wishful thinking, the way we want our form to work:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l010)\n====\n[source,python]\n----\nfrom unittest.mock import patch\nfrom django.http import HttpRequest\nfrom lists.views import new_list2\n[...]\n\n@patch('lists.views.NewListForm')  #<2>\nclass NewListViewUnitTest(unittest.TestCase):  #<1>\n\n    def setUp(self):\n        self.request = HttpRequest()\n        self.request.POST['text'] = 'new list item'  #<3>\n\n    def test_passes_POST_data_to_NewListForm(self, mockNewListForm):\n        new_list2(self.request)\n        mockNewListForm.assert_called_once_with(data=self.request.POST)  #<4>\n----\n====\n\n<1> The Django `TestCase` class makes it too easy to write integrated tests.\n    As a way of making sure we're writing \"pure\", isolated unit tests, we'll\n    only use `unittest.TestCase`.\n\n<2> We mock out the +NewListForm+ class (which doesn't even exist yet). It's\n    going to be used in all the tests, so we mock it out at the class level.\n\n<3> We set up a basic POST request in `setUp`, building up the request by\n    hand rather than using the (overly integrated) Django Test Client.\n\n<4> And we check the first thing about our new view: it initialises its\n    collaborator, the `NewListForm`, with the correct constructor--the\n    data from the request.\n\nThat will start with a failure, saying we don't have a `NewListForm` in\nour view yet:\n\n\n----\nAttributeError: <module 'lists.views' from '...goat-book/lists/views.py'>\ndoes not have the attribute 'NewListForm'\n----\n\nLet's create a placeholder for it:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l011)\n====\n[source,python]\n----\nfrom lists.forms import ExistingListItemForm, ItemForm, NewListForm\n[...]\n----\n====\n\nand:\n\n[role=\"sourcecode\"]\n.lists/forms.py (ch20l012)\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    [...]\n\nclass NewListForm(object):\n    pass\n\nclass ExistingListItemForm(ItemForm):\n    [...]\n----\n====\n\nNext we get a real failure:\n\n\n----\nAssertionError: Expected 'NewListForm' to be called once. Called 0 times.\n----\n\nAnd we implement like this:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l012-2)\n====\n[source,python]\n----\ndef new_list2(request):\n    NewListForm(data=request.POST)\n----\n====\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 38 tests in 0.143s\nOK\n----\n\n\nLet's continue.  If the form is valid, we want to call +save+ on it:\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l013)\n====\n[source,python]\n----\nfrom unittest.mock import patch, Mock\n[...]\n\n@patch('lists.views.NewListForm')\nclass NewListViewUnitTest(unittest.TestCase):\n\n    def setUp(self):\n        self.request = HttpRequest()\n        self.request.POST['text'] = 'new list item'\n        self.request.user = Mock()\n\n\n    def test_passes_POST_data_to_NewListForm(self, mockNewListForm):\n        new_list2(self.request)\n        mockNewListForm.assert_called_once_with(data=self.request.POST)\n\n\n    def test_saves_form_with_owner_if_form_valid(self, mockNewListForm):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = True\n        new_list2(self.request)\n        mock_form.save.assert_called_once_with(owner=self.request.user)\n----\n====\n\n[role=\"pagebreak-before\"]\nThat takes us to this:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l014)\n====\n[source,python]\n----\ndef new_list2(request):\n    form = NewListForm(data=request.POST)\n    form.save(owner=request.user)\n----\n====\n\n\nIn the case where the form is valid, we want the view to return a redirect,\nto send us to see the object that the form has just created.  So we mock out\nanother of the view's collaborators, the `redirect` function:\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l015)\n====\n[source,python]\n----\n    @patch('lists.views.redirect')  #<1>\n    def test_redirects_to_form_returned_object_if_form_valid(\n        self, mock_redirect, mockNewListForm  #<2>\n    ):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = True  #<3>\n\n        response = new_list2(self.request)\n\n        self.assertEqual(response, mock_redirect.return_value)  #<4>\n        mock_redirect.assert_called_once_with(mock_form.save.return_value)  #<5>\n----\n====\n\n<1> We mock out the `redirect` function, this time at the method level.\n\n<2> `patch` decorators are applied innermost first, so the new mock is injected\n    to our method before the `mockNewListForm`.\n\n<3> We specify that we're testing the case where the form is valid.\n\n<4> We check that the response from the view is the result of the `redirect`\n    function.\n\n<5> And we check that the redirect function was called with the object that\n    the form returns on save.\n\nThat takes us to here:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l016)\n====\n[source,python]\n----\ndef new_list2(request):\n    form = NewListForm(data=request.POST)\n    list_ = form.save(owner=request.user)\n    return redirect(list_)\n----\n====\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 40 tests in 0.163s\nOK\n----\n\nAnd now the failure case--if the form is invalid, we want to render\nthe home page template:\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l017)\n====\n[source,python]\n----\n    @patch('lists.views.render')\n    def test_renders_home_template_with_form_if_form_invalid(\n        self, mock_render, mockNewListForm\n    ):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = False\n\n        response = new_list2(self.request)\n\n        self.assertEqual(response, mock_render.return_value)\n        mock_render.assert_called_once_with(\n            self.request, 'home.html', {'form': mock_form}\n        )\n----\n====\n\n\nThat gives us:\n\n----\nAssertionError: <HttpResponseRedirect status_code=302, \"te[114 chars]%3E\"> !=\n<MagicMock name='render()' id='140244627467408'>\n----\n\nTIP: When using assert methods on mocks, like +assert_called_&#8203;once_with+,\n    it's doubly important to make sure you run the test and see it fail.\n    It's all too easy to make a typo in your assert function name and\n    end up calling a mock method that does nothing (mine was to write\n    `asssert_called_once_with` with three essses; try it!).\n\n//TODO: this is now a duplicate warning compared to mocking chapter.\n// replace all assert_calleds with self.assertEquals?\n\nWe make a deliberate mistake, just to make sure our tests are comprehensive:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l018)\n====\n[source,python]\n----\ndef new_list2(request):\n    form = NewListForm(data=request.POST)\n    list_ = form.save(owner=request.user)\n    if form.is_valid():\n        return redirect(list_)\n    return render(request, 'home.html', {'form': form})\n----\n====\n\nThat passes, but it shouldn't!  One more test then:\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l019)\n====\n[source,python]\n----\n    def test_does_not_save_if_form_invalid(self, mockNewListForm):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = False\n        new_list2(self.request)\n        self.assertFalse(mock_form.save.called)\n----\n====\n\n\nWhich fails:\n\n----\n    self.assertFalse(mock_form.save.called)\nAssertionError: True is not false\n----\n\n\n\n(((\"\", startref=\"IEviews23\")))And\nwe get to to our neat, small finished view:\n\n\n[role=\"sourcecode\"]\n.lists/views.py\n====\n[source,python]\n----\ndef new_list2(request):\n    form = NewListForm(data=request.POST)\n    if form.is_valid():\n        list_ = form.save(owner=request.user)\n        return redirect(list_)\n    return render(request, 'home.html', {'form': form})\n----\n====\n\n...\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 42 tests in 0.163s\nOK\n----\n\nMoving Down to the Forms Layer\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n\n(((\"isolation, ensuring\", \"forms layer\", id=\"IEforms23\")))So\nwe've built up our view function based on a \"wishful thinking\" version\nof a form called `NewListForm`, which doesn't even exist yet.\n\nWe'll need the form's save method to create a new list, and a new item based on\nthe text from the form's validated POST data.  If we were to just dive in and\nuse the ORM, the code might look something a bit like this:\n\n\n[role=\"skipme\"]\n[source,python]\n----\nclass NewListForm(forms.models.ModelForm):\n\n    def save(self, owner):\n        list_ = List()\n        if owner:\n            list_.owner = owner\n        list_.save()\n        item = Item()\n        item.list = list_\n        item.text = self.cleaned_data['text']\n        item.save()\n----\n\nThis implementation depends on two classes from the model layer, `Item` and\n`List`.  So, what would a well-isolated test look like?\n\n\n[role=\"skipme\"]\n[source,python]\n----\nclass NewListFormTest(unittest.TestCase):\n\n    @patch('lists.forms.List')  #<1>\n    @patch('lists.forms.Item')  #<1>\n    def test_save_creates_new_list_and_item_from_post_data(\n        self, mockItem, mockList  #<1>\n    ):\n        mock_item = mockItem.return_value\n        mock_list = mockList.return_value\n        user = Mock()\n        form = NewListForm(data={'text': 'new item text'})\n        form.is_valid() #<2>\n\n        def check_item_text_and_list():\n            self.assertEqual(mock_item.text, 'new item text')\n            self.assertEqual(mock_item.list, mock_list)\n            self.assertTrue(mock_list.save.called)\n        mock_item.save.side_effect = check_item_text_and_list  #<3>\n\n        form.save(owner=user)\n\n        self.assertTrue(mock_item.save.called)  #<4>\n----\n\n<1> We mock out the two collaborators for our form from the models layer below.\n\n<2> We need to call `is_valid()` so that the form populates the `.cleaned_data`\n    dictionary where it stores validated data.\n\n<3> We use the `side_effect` method to make sure that, when we save the new\n    item object, we're doing so with a saved `List` and with the correct item\n    text.\n\n<4> As always, we double-check that our side-effect function was actually\n    called.\n\nYuck!  What an ugly test!  Let's not even bother saving that to disk,\nwe can do better.\n\n\nKeep Listening to Your Tests: Removing ORM Code from Our Application\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n(((\"object-relational mapper (ORM)\")))Again, these tests are trying to tell us something:  the Django ORM\nis hard to mock out, and our form class needs to know too much about\nhow it works.  Programming by wishful thinking again, what would\nbe a simpler API that our form could use?  How about something like\nthis:\n\n\n[role=\"skipme\"]\n[source,python]\n----\n    def save(self):\n        List.create_new(first_item_text=self.cleaned_data['text'])\n----\n\nOur wishful thinking says: how about a helper method that\nwould live on the `List`\nclassfootnote:[It could easily just be a standalone function, but hanging it on\nthe model class is a nice way to keep track of where it lives, and gives a bit\nmore of a hint as to what it will do.]\nand encapsulate all the logic of saving a new list object and\nits associated first item?\n\nSo let's write a test for that instead:\n\n[role=\"sourcecode\"]\n.lists/tests/test_forms.py (ch20l021)\n====\n[source,python]\n----\nimport unittest\nfrom unittest.mock import patch, Mock\nfrom django.test import TestCase\n\nfrom lists.forms import (\n    DUPLICATE_ITEM_ERROR, EMPTY_ITEM_ERROR,\n    ExistingListItemForm, ItemForm, NewListForm\n)\nfrom lists.models import Item, List\n[...]\n\n\nclass NewListFormTest(unittest.TestCase):\n\n    @patch('lists.forms.List.create_new')\n    def test_save_creates_new_list_from_post_data_if_user_not_authenticated(\n        self, mock_List_create_new\n    ):\n        user = Mock(is_authenticated=False)\n        form = NewListForm(data={'text': 'new item text'})\n        form.is_valid()\n        form.save(owner=user)\n        mock_List_create_new.assert_called_once_with(\n            first_item_text='new item text'\n        )\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd while we're at it, we can test the case where the user is an authenticated\nuser too:\n\n[role=\"sourcecode\"]\n.lists/tests/test_forms.py (ch20l022)\n====\n[source,python]\n----\n    @patch('lists.forms.List.create_new')\n    def test_save_creates_new_list_with_owner_if_user_authenticated(\n        self, mock_List_create_new\n    ):\n        user = Mock(is_authenticated=True)\n        form = NewListForm(data={'text': 'new item text'})\n        form.is_valid()\n        form.save(owner=user)\n        mock_List_create_new.assert_called_once_with(\n            first_item_text='new item text', owner=user\n        )\n----\n====\n\nYou can see this is a much more readable test. Let's start implementing\nour new form.  We start with the import:\n\n[role=\"sourcecode\"]\n.lists/forms.py (ch20l023)\n====\n[source,python]\n----\nfrom lists.models import Item, List\n----\n====\n\nNow mock tells us to create a placeholder for our `create_new` method:\n\n[subs=\"specialcharacters,macros\"]\n----\nAttributeError: <class 'lists.models.List'> does not have the attribute\n'create_new'\n----\n\n[role=\"sourcecode\"]\n.lists/models.py\n====\n[source,python]\n----\nclass List(models.Model):\n\n    def get_absolute_url(self):\n        return reverse('view_list', args=[self.id])\n\n    def create_new():\n        pass\n----\n====\n//24\n\n\nAnd after a few steps, we should end up with a form save method like this:\n\n[role=\"sourcecode small-code\"]\n.lists/forms.py (ch20l025)\n====\n[source,python]\n----\nclass NewListForm(ItemForm):\n\n    def save(self, owner):\n        if owner.is_authenticated:\n            List.create_new(first_item_text=self.cleaned_data['text'], owner=owner)\n        else:\n            List.create_new(first_item_text=self.cleaned_data['text'])\n----\n====\n\n\nAnd passing tests:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\nRan 44 tests in 0.192s\nOK\n----\n\n\n.Hiding ORM Code Behind Helper Methods\n*******************************************************************************\n(((\"helper methods\")))One\nof the techniques that emerged from our use of isolated tests was the\n\"ORM helper method\".\n\nDjango's ORM lets you get things done quickly with a reasonably readable\nsyntax (it's certainly much nicer than raw SQL!).  But some people like to\ntry to minimise the amount of ORM code in the application--particularly\nremoving it from the views and forms layers.\n\nOne reason is that it makes it much easier to test those layers.  But another\nis that it forces us to build helper functions that express our domain\nlogic more clearly. [keep-together]#Compare#:\n\n\n[role=\"skipme\"]\n[source,python]\n----\n        list_ = List()\n        list_.save()\n        item = Item()\n        item.list = list_\n        item.text = self.cleaned_data['text']\n        item.save()\n----\n\nWith:\n\n[role=\"skipme\"]\n[source,python]\n----\n    List.create_new(first_item_text=self.cleaned_data['text'])\n----\n\nThis applies to read queries as well as write. Imagine something like\nthis:\n\n[role=\"skipme\"]\n[source,python]\n----\n    Book.objects.filter(in_print=True, pub_date__lte=datetime.today())\n----\n\nVersus a helper method, like:\n\n[role=\"skipme\"]\n[source,python]\n----\n    Book.all_available_books()\n----\n\nWhen we build helper functions, we can give them names that express what we\nare doing in terms of the business domain, which can actually make our code\nmore legible, as well as giving us the benefit of keeping all ORM calls at\nthe model layer, and thus making our whole application more loosely coupled.(((\"\", startref=\"IEforms23\")))\n\n*******************************************************************************\n\n\n\nFinally, Moving Down to the Models Layer\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"isolation, ensuring\", \"models layer\", id=\"IEmodels23\")))At\nthe models layer, we no longer need to write isolated tests--the whole\npoint of the models layer is to integrate with the database, so it's appropriate\nto write integrated tests:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l026)\n====\n[source,python]\n----\nclass ListModelTest(TestCase):\n\n    def test_get_absolute_url(self):\n        list_ = List.objects.create()\n        self.assertEqual(list_.get_absolute_url(), f'/lists/{list_.id}/')\n\n\n    def test_create_new_creates_list_and_first_item(self):\n        List.create_new(first_item_text='new item text')\n        new_item = Item.objects.first()\n        self.assertEqual(new_item.text, 'new item text')\n        new_list = List.objects.first()\n        self.assertEqual(new_item.list, new_list)\n----\n====\n\nWhich gives:\n\n[subs=\"specialcharacters,macros\"]\n----\nTypeError: create_new() got an unexpected keyword argument 'first_item_text'\n----\n\nAnd that will take us to a first cut implementation that looks like this:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch20l027)\n====\n[source,python]\n----\nclass List(models.Model):\n\n    def get_absolute_url(self):\n        return reverse('view_list', args=[self.id])\n\n    @staticmethod\n    def create_new(first_item_text):\n        list_ = List.objects.create()\n        Item.objects.create(text=first_item_text, list=list_)\n----\n====\n\nNotice we've been able to get all the way down to the models layer,\ndriving a nice design for the views and forms layers, and the `List`\nmodel still doesn't support having an owner!\n\nNow let's test the case where the list should have an owner, and\nadd:\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l028)\n====\n[source,python]\n----\nfrom django.contrib.auth import get_user_model\nUser = get_user_model()\n[...]\n\n    def test_create_new_optionally_saves_owner(self):\n        user = User.objects.create()\n        List.create_new(first_item_text='new item text', owner=user)\n        new_list = List.objects.first()\n        self.assertEqual(new_list.owner, user)\n----\n====\n\nAnd while we're at it, we can write the tests for the new owner attribute:\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l029)\n====\n[source,python]\n----\nclass ListModelTest(TestCase):\n    [...]\n\n    def test_lists_can_have_owners(self):\n        List(owner=User())  # should not raise\n\n\n    def test_list_owner_is_optional(self):\n        List().full_clean()  # should not raise\n----\n====\n\nThese two are almost exactly the same tests we used in the outside-in chapter,\nbut I've re-written them slightly so they don't actually save objects--just\nhaving them as in-memory objects is enough for this test.\n\nTIP:  Use in-memory (unsaved) model objects in your tests whenever you can; it\n    makes your tests faster.\n\n\nThat gives:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nERROR: test_create_new_optionally_saves_owner\nTypeError: create_new() got an unexpected keyword argument 'owner'\n[...]\nERROR: test_lists_can_have_owners (lists.tests.test_models.ListModelTest)\nTypeError: 'owner' is an invalid keyword argument for this function\n[...]\nRan 48 tests in 0.204s\nFAILED (errors=2)\n----\n\n\nWe implement, just like we did in the chapter:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch20l030-1)\n====\n[source,python]\n----\nfrom django.conf import settings\n[...]\n\n\nclass List(models.Model):\n    owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)\n    [...]\n----\n====\n\nThat will give us the usual integrity failures, until we do a migration:\n\n----\ndjango.db.utils.OperationalError: no such column: lists_list.owner_id\n----\n\nBuilding the migration will get us down to three failures:\n\n[role=\"dofirst-ch20l030-2\"]\n[subs=\"specialcharacters,macros\"]\n----\nERROR: test_create_new_optionally_saves_owner\nTypeError: create_new() got an unexpected keyword argument 'owner'\n[...]\nValueError: Cannot assign \"<SimpleLazyObject:\n<django.contrib.auth.models.AnonymousUser object at 0x7f5b2380b4e0>>\":\n\"List.owner\" must be a \"User\" instance.\nValueError: Cannot assign \"<SimpleLazyObject:\n<django.contrib.auth.models.AnonymousUser object at 0x7f5b237a12e8>>\":\n\"List.owner\" must be a \"User\" instance.\n----\n\nLet's deal with the first one, which is for our `create_new` method:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch20l030-3)\n====\n[source,python]\n----\n    @staticmethod\n    def create_new(first_item_text, owner=None):\n        list_ = List.objects.create(owner=owner)\n        Item.objects.create(text=first_item_text, list=list_)\n----\n====\n\n\n\n\nBack to Views\n^^^^^^^^^^^^^\n\n\n\nTwo of our old integrated tests for the views layer are failing. What's happening?\n\n----\nValueError: Cannot assign \"<SimpleLazyObject:\n<django.contrib.auth.models.AnonymousUser object at 0x7fbad1cb6c10>>\":\n\"List.owner\" must be a \"User\" instance.\n----\n\nAh, the old view isn't discerning enough about what it does with list\nowners yet:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\n    if form.is_valid():\n        list_ = List()\n        list_.owner = request.user\n        list_.save()\n----\n====\n\n\nThis is the point at which we realise that our old code wasn't fit for purpose.\nLet's fix it to get all our tests passing:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l031)\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List()\n        if request.user.is_authenticated:\n            list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n\n\ndef new_list2(request):\n    [...]\n----\n====\n\nNOTE:  (((\"\", startref=\"IEmodels23\")))(((\"integrated tests\", \"benefits and drawbacks of\")))One\nof the benefits of integrated tests is that they help you to catch\n    less predictable interactions like this.  We'd forgotten to write a test\n    for the case where the user is not authenticated, but because the\n    integrated tests use the stack all the way down, errors from the model\n    layer came up to let us know we'd forgotten something:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 48 tests in 0.175s\nOK\n----\n\n\nThe Moment of Truth (and the Risks of Mocking)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"mocks\", \"benefits and drawbacks of\")))(((\"isolation, ensuring\", \"risks of mocking\")))So\nlet's try switching out our old view, and activating our new view. We\ncan make the swap in 'urls.py':\n\n[role=\"sourcecode\"]\n.lists/urls.py\n====\n[source,python]\n----\n[...]\n    url(r'^new$', views.new_list2, name='new_list'),\n----\n====\n\nWe should also remove the `unittest.skip` from our integrated test class, to\nsee if our new code for list owners really works:\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l033)\n====\n[source,python]\n----\nclass NewListViewIntegratedTest(TestCase):\n\n    def test_can_save_a_POST_request(self):\n        [...]\n\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        [...]\n        self.assertEqual(list_.owner, user)\n----\n====\n\nSo what happens when we run our tests? Oh no!\n\n\n----\nERROR: test_list_owner_is_saved_if_user_is_authenticated\n[...]\nERROR: test_can_save_a_POST_request\n[...]\nERROR: test_redirects_after_POST\n(lists.tests.test_views.NewListViewIntegratedTest)\n  File \"...goat-book/lists/views.py\", line 30, in new_list2\n    return redirect(list_)\n[...]\nTypeError: argument of type 'NoneType' is not iterable\n\nFAILED (errors=3)\n----\n\n\nHere's an important lesson to learn about test isolation: it might help you\nto drive out good design for individual layers, but it won't automatically\nverify the integration 'between' your layers.\n\nWhat's happened here is that the view was expecting the form to return\na list item:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\n        list_ = form.save(owner=request.user)\n        return redirect(list_)\n----\n====\n\nBut we forgot to make it return anything:\n\n[role=\"sourcecode currentcontents small-code\"]\n.lists/forms.py\n====\n[source,python]\n----\n    def save(self, owner):\n        if owner.is_authenticated:\n            List.create_new(first_item_text=self.cleaned_data['text'], owner=owner)\n        else:\n            List.create_new(first_item_text=self.cleaned_data['text'])\n----\n====\n\n\n\nThinking of Interactions Between Layers as \"Contracts\"\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"isolation, ensuring\", \"layer interactions as contracts\", id=\"IEinteract23\")))Ultimately, even if we had been writing nothing but isolated unit tests, our\nfunctional tests would have picked up this particular slip-up.  But ideally\nwe'd want our feedback cycle to be quicker--functional tests may take a\ncouple of minutes to run, or even a few hours once your app starts to grow.  Is\nthere any way to avoid this sort of problem before it happens?\n\nMethodologically, the way to do it is to think about the interaction between\nyour layers in terms of contracts.  Whenever we mock out the behaviour of one\nlayer, we have to make a mental note that there is now an implicit contract\nbetween the layers, and that a mock on one layer should probably translate into\na test at the layer below.\n\nHere's the part of the contract that we missed:\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\n    @patch('lists.views.redirect')\n    def test_redirects_to_form_returned_object_if_form_valid(\n        self, mock_redirect, mockNewListForm\n    ):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = True\n\n        response = new_list2(self.request)\n\n        self.assertEqual(response, mock_redirect.return_value)\n        mock_redirect.assert_called_once_with(mock_form.save.return_value)  #<1>\n----\n====\n\n<1> The mocked `form.save` function is returning an object, which we expect\n    our view to be able to use.\n\n\nIdentifying Implicit Contracts\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n\nIt's worth reviewing each of the tests in `NewListViewUnitTest` and seeing\nwhat each mock is saying about the implicit contract:\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests/test_views.py\n====\n[source,python]\n----\n    def test_passes_POST_data_to_NewListForm(self, mockNewListForm):\n        [...]\n        mockNewListForm.assert_called_once_with(data=self.request.POST)  #<1>\n\n\n    def test_saves_form_with_owner_if_form_valid(self, mockNewListForm):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = True  #<2>\n        new_list2(self.request)\n        mock_form.save.assert_called_once_with(owner=self.request.user)  #<3>\n\n\n    def test_does_not_save_if_form_invalid(self, mockNewListForm):\n        [...]\n        mock_form.is_valid.return_value = False  #<2>\n        [...]\n\n\n    @patch('lists.views.redirect')\n    def test_redirects_to_form_returned_object_if_form_valid(\n        self, mock_redirect, mockNewListForm\n    ):\n        [...]\n        mock_redirect.assert_called_once_with(mock_form.save.return_value)  #<4>\n\n\n    @patch('lists.views.render')\n    def test_renders_home_template_with_form_if_form_invalid(\n        [...]\n----\n====\n\n<1> We need to be able to initialise our form by passing it a POST request\n    as data.\n\n<2> It should have an `is_valid()` function which returns +True+ or +False+\n    appropriately, based on the input data.\n\n<3> The form should have a `.save` method which will accept a `request.user`,\n    which may or may not be a logged-in user, and deal with it appropriately.\n\n<4> The form's `.save` method should return a new list object, for our view\n    to redirect the user to.\n\nIf we have a look through our form tests, we'll see that, actually, only item (3)\nis tested explicitly.  On items (1) and (2) we were lucky--they're default\nfeatures of a Django `ModelForm`, and they are actually covered by our\ntests for the parent `ItemForm` class.\n\nBut contract clause number (4) managed to slip through the net.\n\nNOTE: When doing Outside-In TDD with isolated tests, you need to keep track of\n    each test's implicit assumptions about the contract which the next layer\n    should implement, and remember to test each of those in turn later.  You\n    could use our scratchpad for this, or create a placeholder test with\n    a `self.fail`.\n\n\nFixing the Oversight\n^^^^^^^^^^^^^^^^^^^^\n\nLet's add a new test that our form should return the new saved list:\n\n[role=\"sourcecode\"]\n.lists/tests/test_forms.py (ch20l038-1)\n====\n[source,python]\n----\n    @patch('lists.forms.List.create_new')\n    def test_save_returns_new_list_object(self, mock_List_create_new):\n        user = Mock(is_authenticated=True)\n        form = NewListForm(data={'text': 'new item text'})\n        form.is_valid()\n        response = form.save(owner=user)\n        self.assertEqual(response, mock_List_create_new.return_value)\n----\n====\n\nAnd, actually, this is a good example--we have an implicit contract\nwith the `List.create_new`; we want it to return the new list object.\nLet's add a placeholder test for that:\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l038-2)\n====\n[source,python]\n----\nclass ListModelTest(TestCase):\n    [...]\n\n    def test_create_returns_new_list_object(self):\n        self.fail()\n----\n====\n\nSo, we have one test failure that's telling us to fix the form save:\n\n----\nAssertionError: None != <MagicMock name='create_new()' id='139802647565536'>\nFAILED (failures=2, errors=3)\n----\n\nLike this:\n\n\n[role=\"sourcecode small-code\"]\n.lists/forms.py (ch20l039-1)\n====\n[source,python]\n----\nclass NewListForm(ItemForm):\n\n    def save(self, owner):\n        if owner.is_authenticated:\n            return List.create_new(first_item_text=self.cleaned_data['text'], owner=owner)\n        else:\n            return List.create_new(first_item_text=self.cleaned_data['text'])\n----\n====\n\nThat's a start; now we should look at our placeholder test:\n\n----\n[...]\nFAIL: test_create_returns_new_list_object\n    self.fail()\nAssertionError: None\n\nFAILED (failures=1, errors=3)\n----\n\nWe flesh it out:\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l039-2)\n====\n[source,python]\n----\n    def test_create_returns_new_list_object(self):\n        returned = List.create_new(first_item_text='new item text')\n        new_list = List.objects.first()\n        self.assertEqual(returned, new_list)\n----\n====\n\n...\n\n----\nAssertionError: None != <List: List object>\n----\n\nAnd we add our return value:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch20l039-3)\n====\n[source,python]\n----\n    @staticmethod\n    def create_new(first_item_text, owner=None):\n        list_ = List.objects.create(owner=owner)\n        Item.objects.create(text=first_item_text, list=list_)\n        return list_\n----\n====\n\n(((\"\", startref=\"IEinteract23\")))And\nthat gets us to a fully passing test suite:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nRan 50 tests in 0.169s\n\nOK\n----\n\n\nOne More Test\n~~~~~~~~~~~~~\n\nThat's our code for saving list owners, test-driven all the way down and\nworking.  But our functional test isn't passing quite yet:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_my_lists*]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: Reticulate splines\n----\n\n\nIt's because we have one last feature to implement, the `.name` attribute on list\nobjects.  Again, we can grab the test and code from the outside-in chapter:\n\n[role=\"sourcecode\"]\n.lists/tests/test_models.py (ch20l040)\n====\n[source,python]\n----\n    def test_list_name_is_first_item_text(self):\n        list_ = List.objects.create()\n        Item.objects.create(list=list_, text='first item')\n        Item.objects.create(list=list_, text='second item')\n        self.assertEqual(list_.name, 'first item')\n\n----\n====\n\n(Again, since this is a model-layer test, it's OK to use the ORM. You could\nconceivably write this test using mocks, but there wouldn't be much point.)\n\n[role=\"sourcecode\"]\n.lists/models.py (ch20l041)\n====\n[source,python]\n----\n    @property\n    def name(self):\n        return self.item_set.first().text\n----\n====\n\n\nAnd that gets us to a passing FT!\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_my_lists*]\n\nRan 1 test in 21.428s\n\nOK\n----\n\n\nTidy Up: What to Keep from Our Integrated Test Suite\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\n(((\"isolation, ensuring\", \"removing redundant code\", id=\"IEredund23\")))Now\neverything is working, we can remove some redundant tests, and decide\nwhether we want to keep any of our old integrated tests.\n\n\nRemoving Redundant Code at the Forms Layer\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWe can get rid of the test for the old save method on the `ItemForm`:\n\n[role=\"sourcecode\"]\n.lists/tests/test_forms.py\n====\n[source,diff]\n----\n--- a/lists/tests/test_forms.py\n+++ b/lists/tests/test_forms.py\n@@ -23,14 +23,6 @@ class ItemFormTest(TestCase):\n\n         self.assertEqual(form.errors['text'], [EMPTY_ITEM_ERROR])\n\n\n-    def test_form_save_handles_saving_to_a_list(self):\n-        list_ = List.objects.create()\n-        form = ItemForm(data={'text': 'do me'})\n-        new_item = form.save(for_list=list_)\n-        self.assertEqual(new_item, Item.objects.first())\n-        self.assertEqual(new_item.text, 'do me')\n-        self.assertEqual(new_item.list, list_)\n-\n----\n====\n\nAnd in our actual code, we can get rid of two redundant save methods in\n'forms.py':\n\n[role=\"sourcecode\"]\n.lists/forms.py\n====\n[source,diff]\n----\n--- a/lists/forms.py\n+++ b/lists/forms.py\n@@ -22,11 +22,6 @@ class ItemForm(forms.models.ModelForm):\n\n         self.fields['text'].error_messages['required'] = EMPTY_ITEM_ERROR\n\n\n-    def save(self, for_list):\n-        self.instance.list = for_list\n-        return super().save()\n-\n-\n\n class NewListForm(ItemForm):\n\n@@ -52,8 +47,3 @@ class ExistingListItemForm(ItemForm):\n\n             e.error_dict = {'text': [DUPLICATE_ITEM_ERROR]}\n             self._update_errors(e)\n-\n-\n-    def save(self):\n-        return forms.models.ModelForm.save(self)\n-\n----\n====\n\n\nRemoving the Old Implementation of the View\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWe can now completely remove the old `new_list` view, and rename `new_list2` to\n`new_list`:\n\n[role=\"sourcecode skipme\"]\n.lists/tests/test_views.py\n====\n[source,diff]\n----\n-from lists.views import new_list, new_list2\n+from lists.views import new_list\n\n\n class HomePageTest(TestCase):\n@@ -75,7 +75,7 @@ class NewListViewIntegratedTest(TestCase):\n         request = HttpRequest()\n         request.user = User.objects.create(email='a@b.com')\n         request.POST['text'] = 'new list item'\n-        new_list2(request)\n+        new_list(request)\n         list_ = List.objects.first()\n         self.assertEqual(list_.owner, request.user)\n\n@@ -91,21 +91,21 @@ class NewListViewUnitTest(unittest.TestCase):\n\n     def test_passes_POST_data_to_NewListForm(self, mockNewListForm):\n-        new_list2(self.request)\n+        new_list(self.request)\n\n[.. several more]\n\n----\n====\n\n[role=\"sourcecode dofirst-ch20l045\"]\n.lists/urls.py\n====\n[source,diff]\n----\n--- a/lists/urls.py\n+++ b/lists/urls.py\n@@ -3,7 +3,7 @@ from django.conf.urls import url\n from lists import views\n\n urlpatterns = [\n-    url(r'^new$', views.new_list2, name='new_list'),\n+    url(r'^new$', views.new_list, name='new_list'),\n     url(r'^(\\d+)/$', views.view_list, name='view_list'),\n     url(r'^users/(.+)/$', views.my_lists, name='my_lists'),\n ]\n----\n====\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch20l047)\n====\n[source,python]\n----\ndef new_list(request):\n    form = NewListForm(data=request.POST)\n    if form.is_valid():\n        list_ = form.save(owner=request.user)\n        [...]\n----\n====\n\n\nAnd a quick check that all the tests still pass:\n\n----\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\nRemoving Redundant Code at the Forms Layer\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nFinally, we have to decide what (if anything) to keep from our integrated test\nsuite.\n\nOne option is to throw them all away, and decide that the FTs will pick up any\nintegration problems.  That's perfectly valid.\n\nOn the other hand, we saw how integrated tests can warn you when you've made\nsmall mistakes in integrating your layers.  We could keep just a couple of\ntests around as \"sense checks\", to give us a quicker feedback cycle.\n\nHow about these three:\n\n[role=\"sourcecode\"]\n.lists/tests/test_views.py (ch20l048)\n====\n[source,python]\n----\nclass NewListViewIntegratedTest(TestCase):\n\n    def test_can_save_a_POST_request(self):\n        self.client.post('/lists/new', data={'text': 'A new list item'})\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.first()\n        self.assertEqual(new_item.text, 'A new list item')\n\n\n    def test_for_invalid_input_doesnt_save_but_shows_errors(self):\n        response = self.client.post('/lists/new', data={'text': ''})\n        self.assertEqual(List.objects.count(), 0)\n        self.assertContains(response, escape(EMPTY_ITEM_ERROR))\n\n\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n        self.client.post('/lists/new', data={'text': 'new item'})\n        list_ = List.objects.first()\n        self.assertEqual(list_.owner, user)\n----\n====\n\nIf you're going to keep any intermediate-level tests at all,  I like these\nthree because they feel like they're doing the most \"integration\" jobs:  they\ntest the full stack, from the request down to the actual database, and they\ncover the three most important use cases of our view.(((\"\", startref=\"IEredund23\")))\n\n\n\nConclusions: When to Write Isolated Versus Integrated Tests\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n\nTIP: I explored some of these issues in more detail in my\n    https://www.cosmicpython.com[second book]\n\n(((\"isolation, ensuring\", \"vs. integrated tests\", secondary-sortas=\"integrated tests\", id=\"IEinteg23\")))(((\"integrated tests\", \"vs. isolated\", secondary-sortas=\"isolated\", id=\"IEisol23\")))Django's\ntesting tools make it very easy to quickly put together integrated\ntests.  The test runner helpfully creates a fast, in-memory version of your\ndatabase and resets it for you in between each test.  The `TestCase` class\nand the test client make it easy to test your views, from checking whether\ndatabase objects are modified, confirming that your URL mappings work, and\ninspecting the rendering of the templates.  This lets you get started with\ntesting very easily and get good coverage across your whole stack.\n\nOn the other hand, these kinds of integrated tests won't necessarily deliver\nthe full benefit that rigorous unit testing and Outside-In TDD are meant to\nconfer in terms of design.\n\nIf we look at the example in this appendix, compare the code we had before and\nafter:\n\n\n[role=\"sourcecode skipme\"]\n.Before\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List()\n        if not isinstance(request.user, AnonymousUser):\n            list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n----\n\n[role=\"sourcecode skipme\"]\n.After\n[source,python]\n----\ndef new_list(request):\n    form = NewListForm(data=request.POST)\n    if form.is_valid():\n        list_ = form.save(owner=request.user)\n        return redirect(list_)\n    return render(request, 'home.html', {'form': form})\n----\n\n\nIf we hadn't bothered to go down the isolation route, would we have bothered to\nrefactor the view function?  I know I didn't in the first draft of this book.\nI'd like to think I would have \"in real life\", but it's hard to be sure.  But\nwriting isolated tests does make you very aware of where the complexities in\nyour code lie.\n\n\n\n\nLet Complexity Be Your Guide\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\nI'd say the point at which isolated tests start to become worth it is to do\nwith complexity.  The example in this book is extremely simple, so it's not\nusually been worth it so far.  Even in the example in this appendix, I can\nconvince myself I didn't really 'need' to write those isolated tests.\n\nBut once an application gains a little more complexity--if it starts growing\nany more layers between views and models, if you find yourself writing  helper\nmethods, or if you're writing your own classes, then you will probably gain from writing more\nisolated tests.\n\n\nShould You Do Both?\n^^^^^^^^^^^^^^^^^^^\n\nWe already have our suite of functional tests, which will serve the purpose\nof telling us if we ever make any mistakes in integrating the different parts\nof our code together.  Writing isolated tests can help us to drive out better\ndesign for our code, and to verify correctness in finer detail.  Would a\nmiddle layer of integration tests serve any additional purpose?\n\nI think the answer is potentially yes, if they can provide a faster feedback\ncycle, and help you identify more clearly what integration problems you suffer\nfrom--their tracebacks may provide you with better debug information than you\nwould get from a functional test, for example.\n\nThere may even be a case for building them as a separate test suite--you\ncould have one suite of fast, isolated unit tests that don't even use\n`manage.py`, because they don't need any of the database cleanup and teardown\nthat the Django test runner gives you, and then the intermediate layer that\nuses Django, and finally the functional tests layer that, say, talks to a\nstaging server.  It may be worth it if each layer delivers incremental\nbenefits.\n\nIt's a judgement call.  I hope that, by going through this appendix, I've given\nyou a feel for what the trade-offs are. There's more discussion on this in\n<<chapter_27_hot_lava>>.(((\"\", startref=\"IEinteg23\")))(((\"\", startref=\"IEisol23\")))\n\n\nOnwards!\n^^^^^^^^\n\nWe're happy with our new version, so let's bring it across to master:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add .*\n$ *git commit -m \"add list owners via forms. more isolated tests\"*\n$ *git switch master*\n$ *git switch -c master-noforms-noisolation-bak* # optional backup\n$ *git switch -*\n$ *git reset --hard more-isolation*  # reset master to our branch.\n----\n\nIn the meantime--those FTs are taking an annoyingly long time to run.  I\nwonder if there's something we can do about that?\n\n\n\n[role=\"pagebreak-before less_space\"]\n.On the Pros and Cons of Different Types of Tests, pass:[<br/>]and Decoupling ORM Code\n****\n\nFunctional tests::\n    * (((\"functional tests (FTs)\", \"benefits and drawbacks of\")))Provide\nthe best guarantee that your application really works correctly,\n    from the point of view of the user\n    * But: it's a slower feedback cycle\n    * And they don't necessarily help you write clean code\n\n\n\nIntegrated tests (reliant on, for example, the ORM or the Django Test Client)::\n    * (((\"integrated tests\", \"benefits and drawbacks of\")))Are\nquick to write\n    * Are easy to understand\n    * Will warn you of any integration issues\n    * But: may not always drive good design (that's up to you!)\n    * And are usually slower than isolated tests\n\n\nIsolated (\"mocky\") tests::\n    * (((\"mocks\", \"benefits and drawbacks of\")))(((\"isolation, ensuring\", \"benefits and drawbacks of\")))Involve\nthe most hard work\n    * Can be harder to read and understand\n    * But: are the best ones for guiding you towards better design\n    * And run the fastest\n\n\nDecoupling our application from ORM code::\n    (((\"object-relational mapper (ORM)\")))One\nof the consequences of striving to write isolated tests is that we\n    find ourselves forced to remove ORM code from places like views and forms,\n    by hiding it behind helper functions or methods.  This can be beneficial in\n    terms of decoupling your application from the ORM, but also just because it\n    makes your code more readable. As with all things, it's a judgement call as\n    to whether the additional effort is worth it in particular circumstances.(((\"\", startref=\"FTisolat23\")))\n****\n\n"
  },
  {
    "path": "appendix_rest_api.asciidoc",
    "content": "[[appendix_rest_api]]\n[appendix]\nBuilding a REST API: JSON, Ajax, and Mocking with JavaScript\n------------------------------------------------------------\n\n\n\n(((\"Representational State Transfer (REST)\", \"defined\")))Representational\nState Transfer (REST) is an approach to designing a web\nservice to allow a user to retrieve and update information about \"resources\". It's \nbecome the dominant approach when designing APIs for use over the web.\n\nWe've built a working web app without needing an API so far.  Why might we want\none?  One motivation might be to improve the user experience by making the site\nmore dynamic.  Rather than waiting for the page to refresh after each addition\nto a list, we can use JavaScript to fire off those requests asynchronously to our\nAPI, and give the user a more interactive feeling.\n\nPerhaps more interestingly, once we've built an API, we can interact with our\nback-end application via other mechanisms than the browser.  A mobile app might\nbe one new candidate client application, another might be some sort of\ncommand-line application, or other developers might be able to build libraries\nand tools around your backend.\n\nIn this chapter we'll see how to build an API \"by hand\".  In the next, I'll\ngive an overview of how to use a popular tool from the Django ecosystem called\nDjango-Rest-Framework.\n\n\nOur Approach for This Appendix\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nI won't convert the entirety of the app for now; we'll start by assuming we\nhave an existing list.  REST defines a relationship between URLs and the \nHTTP methods (GET and POST, but also the more funky ones like PUT and DELETE)\nwhich will guide us in our design.(((\"Representational State Transfer (REST)\", \"additional resources\")))\n\nhttp://bit.ly/2u6qeYw[The\nWikipedia entry on REST]\nhas a good overview.  In brief:\n\n* Our new URL structure will be '/api/lists/{id}/'\n* GET will give you details of a list (including all its items) in JSON format\n* POST lets you add an item\n\nWe'll take the code from its state at the end of <<chapter_26_page_pattern>>.\n\n\nChoosing Our Test Approach\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf we (((\"Representational State Transfer (REST)\", \"building a REST API\", id=\"RESTbuild32\")))were\nbuilding an API that was entirely agnostic about its clients, we might\nwant to think about what levels to test it at.  The equivalent of functional\ntests would perhaps spin up a real server (maybe using `LiveServerTestCase`)\nand interact with it using the `requests` library. We'd have to think carefully\nabout how to set up fixtures (if we use the API itself, that introduces a lot\nof dependencies between tests) and what additional layer of lower-level/unit\ntests might be most useful to us.  Or we might decide that a single layer of\ntests using the Django Test Client would be enough.\n\nAs it is, we're building an API in the context of a browser-based client side.\nWe want to start using it on our production site, and have the app continue to\nprovide the same functionality as it did before.  So our functional tests will\ncontinue to serve the role of being the highest-level tests, and of checking\nthe integration between our JavaScript and our API.\n\nThat leaves the Django Test Client as a natural place to site our lower-level\ntests.  Let's start there.\n\n\n\nBasic Piping\n~~~~~~~~~~~~\n\nWe start with a unit test that just checks that our new URL structure returns\na 200 response to GET requests, and that it uses the JSON format (instead of HTML):\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py\n====\n[source,python]\n----\nimport json\nfrom django.test import TestCase\n\nfrom lists.models import List, Item\n\n\nclass ListAPITest(TestCase):\n    base_url = '/api/lists/{}/'  #<1>\n\n    def test_get_returns_json_200(self):\n        list_ = List.objects.create()\n        response = self.client.get(self.base_url.format(list_.id))\n        self.assertEqual(response.status_code, 200)\n        self.assertEqual(response['content-type'], 'application/json')\n----\n====\n\n\n<1> Using a class-level constant for the URL under test is a new pattern we'll\n    introduce for this appendix. It'll help us to remove duplication of\n    hardcoded URLs.  You could even use a call to `reverse` to reduce\n    duplication even further.\n\nFirst we wire up a couple of 'urls' files:\n\n[role=\"sourcecode\"]\n.superlists/urls.py\n====\n[source,python]\n----\nfrom django.conf.urls import include, url\nfrom accounts import urls as accounts_urls\nfrom lists import views as list_views\nfrom lists import api_urls\nfrom lists import urls as list_urls\n\nurlpatterns = [\n    url(r'^$', list_views.home_page, name='home'),\n    url(r'^lists/', include(list_urls)),\n    url(r'^accounts/', include(accounts_urls)),\n    url(r'^api/', include(api_urls)),\n]\n----\n====\n\nand:\n\n[role=\"sourcecode\"]\n.lists/api_urls.py\n====\n[source,python]\n----\nfrom django.conf.urls import url\nfrom lists import api\n\nurlpatterns = [\n    url(r'^lists/(\\d+)/$', api.list, name='api_list'),\n]\n----\n====\n\n\nAnd the actual core of our API can live in a file called 'api.py'.  Just\nthree lines should be enough:\n\n\n[role=\"sourcecode\"]\n.lists/api.py\n====\n[source,python]\n----\nfrom django.http import HttpResponse\n\ndef list(request, list_id):\n    return HttpResponse(content_type='application/json')\n----\n====\n\nThe tests should pass, and we have the basic piping together:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n..................................................\n ---------------------------------------------------------------------\nRan 50 tests in 0.177s\n\nOK\n----\n\n\nActually Responding with Something\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOur next step is to get our API to actually respond with some content--specifically, a JSON representation of our list items:\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch36l002)\n====\n[source,python]\n----\n    def test_get_returns_items_for_correct_list(self):\n        other_list = List.objects.create()\n        Item.objects.create(list=other_list, text='item 1')\n        our_list = List.objects.create()\n        item1 = Item.objects.create(list=our_list, text='item 1')\n        item2 = Item.objects.create(list=our_list, text='item 2')\n        response = self.client.get(self.base_url.format(our_list.id))\n        self.assertEqual(\n            json.loads(response.content.decode('utf8')),  #<1>\n            [\n                {'id': item1.id, 'text': item1.text},\n                {'id': item2.id, 'text': item2.text},\n            ]\n        )\n----\n====\n\n<1> This is the main thing to notice about this test. We expect our\n    response to be in JSON format; we use `json.loads()` because testing\n    Python objects is easier than messing about with raw JSON strings.\n\n\nAnd the implementation, conversely, uses `json.dumps()`:\n\n[role=\"sourcecode\"]\n.lists/api.py\n====\n[source,python]\n----\nimport json\nfrom django.http import HttpResponse\nfrom lists.models import List, Item\n\n\ndef list(request, list_id):\n    list_ = List.objects.get(id=list_id)\n    item_dicts = [\n        {'id': item.id, 'text': item.text}\n        for item in list_.item_set.all()\n    ]\n    return HttpResponse(\n        json.dumps(item_dicts),\n        content_type='application/json'\n    )\n----\n====\n\nA nice opportunity to use a list comprehension!\n\n\n\nAdding POST\n~~~~~~~~~~~\n\nThe second thing we need from our API is the ability to add new items\nto our list by using a POST request. We'll start with the \"happy path\":\n\n\n[role=\"sourcecode\"]\n.lists/tests/test_api.py (ch36l004)\n====\n[source,python]\n----\n    def test_POSTing_a_new_item(self):\n        list_ = List.objects.create()\n        response = self.client.post(\n            self.base_url.format(list_.id),\n            {'text': 'new item'},\n        )\n        self.assertEqual(response.status_code, 201)\n        new_item = list_.item_set.get()\n        self.assertEqual(new_item.text, 'new item')\n----\n====\n\n\nAnd the implementation is similarly simple--basically the same as what we do\nin our normal view, but we return a 201 rather than a redirect:\n\n\n[role=\"sourcecode\"]\n.lists/api.py (ch36l005)\n====\n[source,python]\n----\ndef list(request, list_id):\n    list_ = List.objects.get(id=list_id)\n    if request.method == 'POST':\n        Item.objects.create(list=list_, text=request.POST['text'])\n        return HttpResponse(status=201)\n    item_dicts = [\n        [...]\n----\n====\n//ch36l005\n\nAnd that should get us started:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n\nRan 52 tests in 0.177s\n\nOK\n----\n\nNOTE: One of the fun things about building a REST API is that you get\n    to use a few more of the full range of \n    https://en.wikipedia.org/wiki/List_of_HTTP_status_codes[HTTP status codes].\n\n\n\nTesting the Client-Side Ajax with Sinon.js\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDon't even 'think' of doing Ajax testing without a mocking library.  Different\ntest frameworks and tools have their own; 'Sinon' is generic.  It also provides\nJavaScript mocks, as we'll see...\n\nStart by downloading it from its site, http://sinonjs.org/, and putting it into\nour 'lists/static/tests/' folder.\n\nThen we can write our first Ajax test:\n\n[role=\"sourcecode dofirst-ch36l006\"]\n.lists/static/tests/tests.html (ch36l007)\n====\n[source,html]\n----\n  <div id=\"qunit-fixture\">\n    <form>\n      <input name=\"text\" />\n      <div class=\"has-error\">Error text</div>\n    </form>\n    <table id=\"id_list_table\">  <1>\n    </table>\n  </div>\n\n  <script src=\"../jquery-3.1.1.min.js\"></script>\n  <script src=\"../list.js\"></script>\n  <script src=\"qunit-2.0.1.js\"></script>\n  <script src=\"sinon-1.17.6.js\"></script>  <2>\n\n  <script>\n/* global sinon */\n\nvar server;\nQUnit.testStart(function () {\n  server = sinon.fakeServer.create();  //<3>\n});\nQUnit.testDone(function () {\n  server.restore();  //<3>\n});\n\nQUnit.test(\"errors should be hidden on keypress\", function (assert) {\n[...]\n\n\nQUnit.test(\"should get items by ajax on initialize\", function (assert) {\n  var url = '/getitems/';\n  window.Superlists.initialize(url);\n\n  assert.equal(server.requests.length, 1); //<4>\n  var request = server.requests[0];\n  assert.equal(request.url, url);\n  assert.equal(request.method, 'GET');\n});\n\n  </script>\n----\n====\n\n<1> We add a new item to the fixture `div` to represent our list table.\n\n<2> We import 'sinon.js' (you'll need to download it and put it in the\n    right folder).\n\n<3> `testStart` and `testDone` are the QUnit equivalents of `setUp` and\n    `tearDown`.  We use them to tell Sinon to start up its Ajax testing\n    tool, the `fakeServer`, and make it available via a globally scoped\n    variable called `server`.\n\n<4> That lets us make assertions about any Ajax requests that were made\n    by our code.  In this case, we test what URL the request went to,\n    and what HTTP method it used.\n\n\nTo actually make our Ajax request, we'll use the\nhttps://api.jquery.com/jQuery.get/[jQuery Ajax helpers], which are 'much'\neasier than trying to use the low-level browser standard `XMLHttpRequest` objects:\n\n[role=\"sourcecode\"]\n.lists/static/list.js\n====\n[source,diff]\n----\n@@ -1,6 +1,10 @@\n window.Superlists = {};\n-window.Superlists.initialize = function () {\n+window.Superlists.initialize = function (url) {\n   $('input[name=\"text\"]').on('keypress', function () {\n     $('.has-error').hide();\n   });\n+\n+  $.get(url);\n+\n };\n+\n----\n====\n\n\nThat should get our test passing:\n\n\n[role=\"qunit-output\"]\n----\n5 assertions of 5 passed, 0 failed.\n1. errors should be hidden on keypress (1)\n2. errors aren't hidden if there is no keypress (1)\n3. should get items by ajax on initialize (3)\n----\n\nWell, we might be pinging out a GET request to the server, but what about\nactually 'doing' something?  How do we test the actual \"async\" part, where we\ndeal with the (eventual) response?\n\n\nSinon and Testing the Asynchronous Part of Ajax\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThis is a major reason to love Sinon.  `server.respond()` allows us to exactly\ncontrol the flow of the asynchronous code.\n\n\n[role=\"sourcecode\"]\n.lists/static/tests/tests.html (ch36l009)\n====\n[source,javascript]\n----\nQUnit.test(\"should fill in lists table from ajax response\", function (assert) {\n  var url = '/getitems/';\n  var responseData = [\n    {'id': 101, 'text': 'item 1 text'},\n    {'id': 102, 'text': 'item 2 text'},\n  ];\n  server.respondWith('GET', url, [\n    200, {\"Content-Type\": \"application/json\"}, JSON.stringify(responseData) //<1>\n  ]);\n  window.Superlists.initialize(url); //<2>\n\n  server.respond(); //<3>\n\n  var rows = $('#id_list_table tr');  //<4>\n  assert.equal(rows.length, 2);\n  var row1 = $('#id_list_table tr:first-child td');\n  assert.equal(row1.text(), '1: item 1 text');\n  var row2 = $('#id_list_table tr:last-child td');\n  assert.equal(row2.text(), '2: item 2 text');\n});\n----\n====\n\n<1> We set up some response data for Sinon to use, telling it what status code, headers,\n    and importantly what kind of response JSON we want to simulate coming from the\n    server.\n\n<2> Then we call the function under test.\n\n<3> Here's the magic.  'Then' we can call `server.respond()`, whenever we like, and that\n    will kick off all the async part of the Ajax loop—that is, any callback we've assigned\n    to deal with the response.\n\n<4> Now we can quietly check whether our Ajax callback has actually populated our table\n    with the new list rows... \n\nThe implementation might look something like this:\n\n[role=\"sourcecode\"]\n.lists/static/list.js (ch36l010)\n====\n[source,javascript]\n----\n  if (url) {\n    $.get(url).done(function (response) {  //<1>\n      var rows = '';\n      for (var i=0; i<response.length; i++) {  //<2>\n        var item = response[i];\n        rows += '\\n<tr><td>' + (i+1) + ': ' + item.text + '</td></tr>';\n      }\n      $('#id_list_table').html(rows);\n    });\n  }\n----\n====\n\n\nTIP: We're lucky because of the way jQuery registers its callbacks for Ajax when we use\n    the `.done()` function.  If you want to switch to the more standard JavaScript Promise\n    `.then()` callback, we get one more \"level\" of async.  QUnit does have a\n    way of dealing with that.  Check out the docs for the\n    http://api.qunitjs.com/async/[async] function.\n    Other test frameworks have something similar.\n\n\n\n\nWiring It All Up in the Template to See If It Really Works\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWe break it first, by removing the list table `{% for %}` loop from the \n_lists.html_ [keep-together]#template#:\n\n[role=\"sourcecode\"]\n.lists/templates/list.html\n====\n[source,diff]\n----\n@@ -6,9 +6,6 @@\n \n {% block table %}\n   <table id=\"id_list_table\" class=\"table\">\n-    {% for item in list.item_set.all %}\n-      <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n-    {% endfor %}\n   </table>\n \n   {% if list.owner %}\n----\n====\n\nNOTE: This will cause one of the unit tests to fail.  It's OK to delete that\n    test at this point.\n\n.Graceful Degradation and Progressive Enhancement\n*******************************************************************************\nBy removing the non-Ajax version of the lists page, I've removed the option of\nhttps://www.w3.org/wiki/Graceful_degradation_versus_progressive_enhancement[graceful\ndegradation]—that is, keeping a version of the site that will still work without\n[keep-together]#JavaScript#.\n\nThis used to be an accessibility issue: \"screen reader\" browsers for visually\nimpaired people used not to have JavaScript, so relying entirely on JavaScript would\nexclude those users.  That's not so much of an issue any more, as I understand\nit.  But some users will block JavaScript for security reasons.\n\nAnother common problem is differing levels of JavaScript support in different\nbrowsers.  This is a particular issue if you start adventuring off in the\ndirection of \"modern\" frontend development and ES2015.\n\n[role=\"pagebreak-before\"]\nIn short, it's always nice to have a non-JavaScript \"backup\".  Particularly\nif you've built a site that works fine without it, don't throw away your\nworking \"plain old\" HTML version too hastily. I'm just doing it because it's\nconvenient for what I want to [keep-together]#demonstrate#.\n*******************************************************************************\n\nThat causes our basic FT to fail:\n\n[role=\"dofirst-ch36l015\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_simple_list_creation*]\n[...]\nFAIL: test_can_start_a_list_for_one_user\n[...]\n  File \"...goat-book/functional_tests/test_simple_list_creation.py\", line\n32, in test_can_start_a_list_for_one_user\n    self.wait_for_row_in_list_table('1: Buy peacock feathers')\n[...]\nAssertionError: '1: Buy peacock feathers' not found in []\n[...]\nFAIL: test_multiple_users_can_start_lists_at_different_urls\n\nFAILED (failures=2)\n----\n\n\nLet's add a block called `{% scripts %}` to the base template, which we\ncan selectively override later in our lists page:\n\n[role=\"sourcecode\"]\n.lists/templates/base.html\n====\n[source,html]\n----\n    <script src=\"/static/list.js\"></script>\n\n    {% block scripts %}\n      <script>\n$(document).ready(function () {\n  window.Superlists.initialize();\n});\n      </script>\n    {% endblock scripts %}\n\n  </body>\n----\n====\n\n\nAnd now in 'list.html' we add a slightly different call to `initialize`, with\nthe correct URL:\n\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch36l016)\n====\n[source,html]\n----\n{% block scripts %}\n  <script>\n$(document).ready(function () {\n  var url = \"{% url 'api_list' list.id %}\";\n  window.Superlists.initialize(url);\n});\n  </script>\n{% endblock scripts %}\n----\n====\n\nAnd guess what? The test passes!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_simple_list_creation*]\n[...]\nRan 2 test in 11.730s\n\nOK\n----\n\nThat's a pretty good start!\n\nNow if you run all the FTs you'll see we've got some failures in \nother FTs, so we'll have to deal with them. Also, we're using an old-fashioned\nPOST from the form, with page refresh, so we're not at our trendy hipster\nsingle-page app yet.  But we'll get there!\n\n\n//TODO: which FTs fail exactly?\n\n\n\nImplementing Ajax POST, Including the CSRF Token\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nFirst we give our list form an `id` so we can pick it up easily in our JS:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/base.html\n====\n[source,html]\n----\n  <h1>{% block header_text %}{% endblock %}</h1>\n  {% block list_form %}\n    <form id=\"id_item_form\" method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n      {{ form.text }}\n      [...]\n----\n====\n\nNext tweak the fixture in our JavaScript test to reflect that ID, as well as the\nCSRF token that's currently on the page:\n\n[role=\"sourcecode\"]\n.lists/static/tests/tests.html\n====\n[source,diff]\n----\n@@ -9,9 +9,14 @@\n <body>\n   <div id=\"qunit\"></div>\n   <div id=\"qunit-fixture\">\n-    <form>\n+    <form id=\"id_item_form\">\n       <input name=\"text\" />\n-      <div class=\"has-error\">Error text</div>\n+      <input type=\"hidden\" name=\"csrfmiddlewaretoken\" value=\"tokey\" />\n+      <div class=\"has-error\">\n+        <div class=\"help-block\">\n+          Error text\n+        </div>\n+      </div>\n     </form>\n\n----\n====\n\n\nAnd here's our test:\n\n\n[role=\"sourcecode\"]\n.lists/static/tests/tests.html (ch36l019)\n====\n[source,javascript]\n----\nQUnit.test(\"should intercept form submit and do ajax post\", function (assert) {\n  var url = '/listitemsapi/';\n  window.Superlists.initialize(url);\n\n  $('#id_item_form input[name=\"text\"]').val('user input');  //<1>\n  $('#id_item_form input[name=\"csrfmiddlewaretoken\"]').val('tokeney');  //<1>\n  $('#id_item_form').submit();  //<1>\n\n  assert.equal(server.requests.length, 2);  //<2>\n  var request = server.requests[1];\n  assert.equal(request.url, url);\n  assert.equal(request.method, \"POST\");\n  assert.equal(\n    request.requestBody,\n    'text=user+input&csrfmiddlewaretoken=tokeney'  //<3>\n  );\n});\n----\n====\n\n<1> We simulate the user filling in the form and hitting Submit.\n\n<2> We now expect that there should be a second Ajax request (the\n    first one is the GET for the list items table).\n\n<3> We check our POST `requestBody`.  As you can see, it's\n    URL-encoded, which isn't the most easy value to test, but it's still just\n    about readable.\n\nAnd here's how we implement it:\n\n[role=\"sourcecode\"]\n.lists/static/list.js\n====\n[source,javascript]\n----\n[...]\n  $('#id_list_table').html(rows);\n});\n\nvar form = $('#id_item_form');\nform.on('submit', function(event) {\n  event.preventDefault();\n  $.post(url, {\n    'text': form.find('input[name=\"text\"]').val(),\n    'csrfmiddlewaretoken': form.find('input[name=\"csrfmiddlewaretoken\"]').val(),\n  });\n});\n----\n====\n\nThat gets our JavaScript tests passing but it breaks our FTs, because, although we're\ndoing our POST all right, we're not updating the page after the POST to show\nthe new list item:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests.test_simple_list_creation*]\n[...]\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers']\n----\n\n\n\nMocking in JavaScript\n~~~~~~~~~~~~~~~~~~~~~\n\nWe want our client side to update the table of items after the Ajax POST\ncompletes. Essentially it'll do the same work as we do as soon as the page\nloads, retrieving the current list of items from the server, and filling in the\nitem table.\n\nSounds like a helper function is in order!\n\n[role=\"sourcecode\"]\n.lists/static/list.js\n====\n[source,javascript]\n----\nwindow.Superlists = {};\n\nwindow.Superlists.updateItems = function (url) {\n  $.get(url).done(function (response) {\n    var rows = '';\n    for (var i=0; i<response.length; i++) {\n      var item = response[i];\n      rows += '\\n<tr><td>' + (i+1) + ': ' + item.text + '</td></tr>';\n    }\n    $('#id_list_table').html(rows);\n  });\n};\n\nwindow.Superlists.initialize = function (url) {\n  $('input[name=\"text\"]').on('keypress', function () {\n    $('.has-error').hide();\n  });\n\n  if (url) {\n    window.Superlists.updateItems(url);\n\n    var form = $('#id_item_form');\n    [...]\n----\n====\n\nThat was just a refactor; now we check that the JavaScript tests all still pass:\n\n\n[role=\"qunit-output\"]\n----\n12 assertions of 12 passed, 0 failed.\n1. errors should be hidden on keypress (1)\n2. errors aren't hidden if there is no keypress (1)\n3. should get items by ajax on initialize (3)\n4. should fill in lists table from ajax response (3)\n5. should intercept form submit and do ajax post (4)\n----\n\nNow how to test that our Ajax POST calls `updateItems` on POST success?  We\ndon't want to dumbly duplicate the code that simulates a server response\nand checks the items table manually...how about a mock?\n\n\nFirst we set up a thing called a \"sandbox\".  It will keep track of all\nthe mocks we create, and make sure to un-monkeypatch all the things that\nhave been mocked after each test:\n\n[role=\"sourcecode\"]\n.lists/static/tests/tests.html (ch36l023)\n====\n[source,html]\n----\nvar server, sandbox;\nQUnit.testStart(function () {\n  server = sinon.fakeServer.create();\n  sandbox = sinon.sandbox.create();\n});\nQUnit.testDone(function () {\n  server.restore();\n  sandbox.restore(); //<1>\n});\n----\n====\n\n\n<1> This `.restore()` is the important part; it undoes all the\n    mocking we've done in each test.\n\n\n[role=\"sourcecode\"]\n.lists/static/tests/tests.html (ch36l024)\n====\n[source,html]\n----\nQUnit.test(\"should call updateItems after successful post\", function (assert) {\n  var url = '/listitemsapi/';\n  window.Superlists.initialize(url); //<1>\n  var response = [\n    201,\n    {\"Content-Type\": \"application/json\"},\n    JSON.stringify({}),\n  ];\n  server.respondWith('POST', url, response); //<1>\n  $('#id_item_form input[name=\"text\"]').val('user input');\n  $('#id_item_form input[name=\"csrfmiddlewaretoken\"]').val('tokeney');\n  $('#id_item_form').submit();\n\n  sandbox.spy(window.Superlists, 'updateItems');  //<2>\n  server.respond();  //<2>\n\n  assert.equal(\n    window.Superlists.updateItems.lastCall.args,  //<3>\n    url\n  );\n});\n----\n====\n\n<1> First important thing to notice:  We only set up our server response\n    'after' we do the initialize.  We want this to be the response to the\n    POST request that happens on form submit, not the response to the\n    initial GET request. (Remember our lesson from <<chapter_17_javascript>>?\n    One of the most challenging things about JavaScript testing is controlling the\n    order of execution.)\n\n<2> Similarly, we only start mocking our helper function 'after' we know the\n    first call for the initial GET has already happened.  The `sandbox.spy`\n    call is what does the job that `patch` does in Python tests.  It replaces\n    the given object with a mock [keep-together]#version#.\n\n<3> Our `updateItems` function has now grown some mocky extra attributes, like\n    `lastCall` and `lastCall.args`, which are like the Python mock's `call_args`.\n\n\nTo get it passing, we first make a deliberate mistake, to check that our tests really\ndo test what we think they do:\n\n\n[role=\"sourcecode\"]\n.lists/static/list.js\n====\n[source,javascript]\n----\n$.post(url, {\n  'text': form.find('input[name=\"text\"]').val(),\n  'csrfmiddlewaretoken': form.find('input[name=\"csrfmiddlewaretoken\"]').val(),\n}).done(function () {\n  window.Superlists.updateItems();\n});\n----\n====\n\nYep, we're almost there but not quite:\n\n[role=\"qunit-output\"]\n----\n12 assertions of 13 passed, 1 failed.\n[...]\n6. should call updateItems after successful post (1, 0, 1)\n    1. failed\n        Expected: \"/listitemsapi/\"\n        Result: []\n        Diff: \"/listitemsapi/\"[]\n        Source: file://...goat-book/lists/static/tests/tests.html:124:15\n----\n\nAnd we fix it thusly:\n\n[role=\"sourcecode\"]\n.lists/static/list.js\n====\n[source,javascript]\n----\n      }).done(function () {\n        window.Superlists.updateItems(url);\n      });\n----\n====\n\n\nAnd our FT passes!  Or at least one of them does. The others have problems, and we'll come back to them shortly.\n\n\n\nFinishing the Refactor: Getting the Tests to Match the Code\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nFirst, I'm not happy until we've seen through this refactor, and made\nour unit tests match the code a little more:\n\n//TODO: fix long lines in this listing\n\n[role=\"sourcecode small-code\"]\n.lists/static/tests/tests.html\n====\n[source,diff]\n----\n@@ -50,9 +50,19 @@ QUnit.testDone(function () {\n });\n \n \n-QUnit.test(\"should get items by ajax on initialize\", function (assert) {\n+QUnit.test(\"should call updateItems on initialize\", function (assert) {\n   var url = '/getitems/';\n+  sandbox.spy(window.Superlists, 'updateItems');\n   window.Superlists.initialize(url);\n+  assert.equal(\n+    window.Superlists.updateItems.lastCall.args,\n+    url\n+  );\n+});\n+\n+QUnit.test(\"updateItems should get correct url by ajax\", function (assert) {\n+  var url = '/getitems/';\n+  window.Superlists.updateItems(url);\n \n   assert.equal(server.requests.length, 1);\n   var request = server.requests[0];\n@@ -60,7 +70,7 @@ QUnit.test(\"should get items by ajax on initialize\", function (assert) {\n   assert.equal(request.method, 'GET');\n });\n \n-QUnit.test(\"should fill in lists table from ajax response\", function (assert) {\n+QUnit.test(\"updateItems should fill in lists table from ajax response\", function (assert) {\n   var url = '/getitems/';\n   var responseData = [\n     {'id': 101, 'text': 'item 1 text'},\n@@ -69,7 +79,7 @@ QUnit.test(\"should fill in lists table from ajax response\", function [...]\n   server.respondWith('GET', url, [\n     200, {\"Content-Type\": \"application/json\"}, JSON.stringify(responseData)\n   ]);\n-  window.Superlists.initialize(url);\n+  window.Superlists.updateItems(url);\n \n   server.respond();\n----\n====\n//ch36l026\n\n\nAnd that should give us a test run that looks like this instead:\n\n[role=\"qunit-output\"]\n----\n14 assertions of 14 passed, 0 failed.\n1. errors should be hidden on keypress (1)\n2. errors aren't hidden if there is no keypress (1)\n3. should call updateItems on initialize (1)\n4. updateItems should get correct url by ajax (3)\n5. updateItems should fill in lists table from ajax response (3)\n6. should intercept form submit and do ajax post (4)\n7. should call updateItems after successful post (1)\n----\n\n[role=\"pagebreak-before less_space\"]\nData Validation:  An Exercise for the Reader?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you do a full test run, you should find two of the validation FTs are failing:\n\n\n[role=\"dofirst-ch36l017\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\nERROR: test_cannot_add_duplicate_items\n(functional_tests.test_list_item_validation.ItemValidationTest)\n[...]\nERROR: test_error_messages_are_cleared_on_input\n(functional_tests.test_list_item_validation.ItemValidationTest)\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .has-error\n----\n\nI won't spell this all out for you, but here's at least the unit\ntests you'll need:\n\n[role=\"sourcecode dofirst-ch36l028 small-code\"]\n.lists/tests/test_api.py (ch36l027)\n====\n[source,python]\n----\nfrom lists.forms import DUPLICATE_ITEM_ERROR, EMPTY_ITEM_ERROR \n[...]\n    def post_empty_input(self):\n        list_ = List.objects.create()\n        return self.client.post(\n            self.base_url.format(list_.id),\n            data={'text': ''}\n        )\n\n\n    def test_for_invalid_input_nothing_saved_to_db(self):\n        self.post_empty_input()\n        self.assertEqual(Item.objects.count(), 0)\n\n\n    def test_for_invalid_input_returns_error_code(self):\n        response = self.post_empty_input()\n        self.assertEqual(response.status_code, 400)\n        self.assertEqual(\n            json.loads(response.content.decode('utf8')),\n            {'error': EMPTY_ITEM_ERROR}\n        )\n\n\n    def test_duplicate_items_error(self):\n        list_ = List.objects.create()\n        self.client.post(\n            self.base_url.format(list_.id), data={'text': 'thing'}\n        )\n        response = self.client.post(\n            self.base_url.format(list_.id), data={'text': 'thing'}\n        )\n        self.assertEqual(response.status_code, 400)\n        self.assertEqual(\n            json.loads(response.content.decode('utf8')),\n            {'error': DUPLICATE_ITEM_ERROR}\n        )\n\n----\n====\n\nAnd on the JavaScript side:\n\n[role=\"sourcecode dofirst-ch36l029-1\"]\n.lists/static/tests/tests.html (ch36l029-2)\n====\n[source,python]\n----\nQUnit.test(\"should display errors on post failure\", function (assert) {\n  var url = '/listitemsapi/';\n  window.Superlists.initialize(url);\n  server.respondWith('POST', url, [\n    400,\n    {\"Content-Type\": \"application/json\"},\n    JSON.stringify({'error': 'something is amiss'})\n  ]);\n  $('.has-error').hide();\n\n  $('#id_item_form').submit();\n  server.respond(); // post\n\n  assert.equal($('.has-error').is(':visible'), true);\n  assert.equal($('.has-error .help-block').text(), 'something is amiss');\n});\n\nQUnit.test(\"should hide errors on post success\", function (assert) {\n    [...]\n----\n====\n\nYou'll also want some modifications to 'base.html' to make it compatible with\nboth displaying Django errors (which the home page still uses for now) and\nerrors from [keep-together]#JavaScript#:\n\n[role=\"sourcecode dofirst-ch36l030\"]\n.lists/templates/base.html (ch36l031)\n====\n[source,diff]\n----\n@@ -51,17 +51,21 @@\n         <div class=\"col-md-6 col-md-offset-3 jumbotron\">\n           <div class=\"text-center\">\n             <h1>{% block header_text %}{% endblock %}</h1>\n+\n             {% block list_form %}\n               <form id=\"id_item_form\" method=\"POST\" action=\"{% block [...]\n                 {{ form.text }}\n                 {% csrf_token %}\n-                {% if form.errors %}\n-                  <div class=\"form-group has-error\">\n-                    <div class=\"help-block\">{{ form.text.errors }}</div>\n+                <div class=\"form-group has-error\">\n+                  <div class=\"help-block\">\n+                    {% if form.errors %}\n+                      {{ form.text.errors }}\n+                    {% endif %}\n                   </div>\n-                {% endif %}\n+                </div>\n               </form>\n             {% endblock %}\n+\n           </div>\n         </div>\n       </div>\n----\n====\n//ch36l031\n\n\nBy the end you should get to a JavaScript test run a bit like this:\n\n[role=\"qunit-output dofirst-ch36l033\"]\n----\n20 assertions of 20 passed, 0 failed.\n1. errors should be hidden on keypress (1)\n2. errors aren't hidden if there is no keypress (1)\n3. should call updateItems on initialize (1)\n4. updateItems should get correct url by ajax (3)\n5. updateItems should fill in lists table from ajax response (3)\n6. should intercept form submit and do ajax post (4)\n7. should call updateItems after successful post (1)\n8. should not intercept form submit if no api url passed in (1)\n9. should display errors on post failure (2)\n10. should hide errors on post success (1)\n11. should display generic error if no error json (2)\n----\n\nAnd a full test run should pass, including all the FTs:\n\n//TODO: there's a possible race condition here, line 56 in the test_sharing\n// sometimes fails because oni tries to add his list before the table has\n// loaded\n\n[role=\"dofirst-ch36l032\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\nRan 81 tests in 62.029s\nOK\n----\n\n\nLaaaaaahvely.footnote:[Put on your best cockney accent for this one.]\n\nAnd there's your hand-rolled REST API with Django. If you need a hint finishing\nit off yourself, check out\nhttps://github.com/hjwp/book-example/tree/appendix_rest_api[the repo].\n\n\nBut I would never suggest building a REST API in Django without at least\nchecking out 'Django-Rest-Framework'.  Which is the topic of the next appendix!\nRead on, [keep-together]#Macduff#.(((\"\", startref=\"RESTbuild32\")))\n\n\n.REST API Tips\n*******************************************************************************\n\nDedupe URLs::\n    (((\"Representational State Transfer (REST)\", \"tips for REST APIs\")))URLs \n    are more important, in a way, to an API than they are to a\n    browser-facing app.  Try to reduce the amount of times you hardcode them\n    in your tests.\n\nDon't work with raw JSON strings::\n    `json.loads` and `json.dumps` are your friend.\n\nAlways use an Ajax mocking library for your JavaScript tests::\n    Sinon is fine.  Jasmine has its own, as does Angular.\n\nBear graceful degradation and progressive enhancement in mind::\n    Especially if you're moving from a static site to a more JavaScript-driven\n    one, consider keeping at least the core of your site's functionality\n    working without JavaScript.\n\n*******************************************************************************\n\n"
  },
  {
    "path": "appendix_tradeoffs.asciidoc",
    "content": "[[appendix_tradeoffs]]\n[appendix]\n== Testing Tradeoffs: Choosing the Right Place to Test From\n\npick up in chapter 16, refactor form, move html back to template\n\ntests were in the wrong place, better to have them in test_views.py\n\nmind you, it's also tested in the FT.\n\n-> 3 places we could/do test.\n\ndiscuss contract between FE and BE.  it's the `name=` basically\n\nwhere do we want to keep the logic about bootstrap css?\n\n* show moving tests from test_forms.py to test_views.py\n* get rid of assertions about `isinstance(... Form)`\n\n\nlesson:  tests at higher level enable more refactoring\n\n\n=== Deleting some FTs\n\nvalidation test is doing quite a lot of work, tests integration w/ backend and bootstrap, good\nmain todos test is the key smoke test\ntest multiple lists?  less wortwhile.\ntest list sharing?  maybe not worth it.\ntest login? maybe not adding value\n\n\n=== FT speedup techniques\n\n`with self.subTest`.\n\n\n=== Testing at a Lower Level\n\nimagine a new feature, \"strict todos\":\n\n- rule about duplicate items is relaxed for non-strict todos\n- strict todos must start with capital letter and end with full stop\n- use linguistic analysis to check for imperative mood (?)\n\n=> justitify some proper unit tests?\n\nat the very least, testing for the regex _could_ happen at a lower level\n\n"
  },
  {
    "path": "asciidoc.conf",
    "content": "[replacements]\n%1%=&#x278a;\n%2%=&#x278b;\n%3%=&#x278c;\n%4%=&#x278d;\n%5%=&#x278e;\n%6%=&#x278f;\n\n"
  },
  {
    "path": "asciidoctor.css",
    "content": "/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */\n/* Remove comment around @import statement below when using as a custom stylesheet */\n\n/*@import \"https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700\";*/\n@import url('https://fonts.googleapis.com/css?family=Kalam');\n\narticle,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}\naudio,canvas,video{display:inline-block}\naudio:not([controls]){display:none;height:0}\n[hidden],template{display:none}\nscript{display:none!important}\nhtml{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}\nbody{margin:0}\na{background:transparent}\na:focus{outline:thin dotted}\na:active,a:hover{outline:0}\nh1{font-size:2em;margin:.67em 0}\nabbr[title]{border-bottom:1px dotted}\nb,strong{font-weight:bold}\ndfn{font-style:italic}\nhr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}\nmark{background:#ff0;color:#000}\ncode,kbd,pre,samp{font-family:monospace;font-size:1em}\npre{white-space:pre-wrap}\nq{quotes:\"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\"}\nsmall{font-size:80%}\nsub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}\nsup{top:-.5em}\nsub{bottom:-.25em}\nimg{border:0}\nsvg:not(:root){overflow:hidden}\nfigure{margin:0}\nfieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}\nlegend{border:0;padding:0}\nbutton,input,select,textarea{font-family:inherit;font-size:100%;margin:0}\nbutton,input{line-height:normal}\nbutton,select{text-transform:none}\nbutton,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}\nbutton[disabled],html input[disabled]{cursor:default}\ninput[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}\ninput[type=\"search\"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}\ninput[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}\nbutton::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}\ntextarea{overflow:auto;vertical-align:top}\ntable{border-collapse:collapse;border-spacing:0}\n*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}\nhtml,body{font-size:100%}\nbody{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:\"Noto Serif\",\"DejaVu Serif\",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto}\na:hover{cursor:pointer}\nimg,object,embed{max-width:100%;height:auto}\nobject,embed{height:100%}\nimg{-ms-interpolation-mode:bicubic}\n.left{float:left!important}\n.right{float:right!important}\n.text-left{text-align:left!important}\n.text-right{text-align:right!important}\n.text-center{text-align:center!important}\n.text-justify{text-align:justify!important}\n.hide{display:none}\nbody{-webkit-font-smoothing:antialiased}\nimg,object,svg{display:inline-block;vertical-align:middle}\ntextarea{height:auto;min-height:50px}\nselect{width:100%}\n.center{margin-left:auto;margin-right:auto}\n.spread{width:100%}\np.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6}\n.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}\ndiv,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}\na{color:#2156a5;text-decoration:underline;line-height:inherit}\na:hover,a:focus{color:#1d4b8f}\na img{border:none}\np{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}\np aside{font-size:.875em;line-height:1.35;font-style:italic}\nh1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:\"Open Sans\",\"DejaVu Sans\",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}\nh1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}\nh1{font-size:2.125em}\nh2{font-size:1.6875em}\nh3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}\nh4,h5{font-size:1.125em}\nh6{font-size:1em}\nhr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}\nem,i{font-style:italic;line-height:inherit}\nstrong,b{font-weight:bold;line-height:inherit}\nsmall{font-size:60%;line-height:inherit}\ncode{font-family:\"Droid Sans Mono\",\"DejaVu Sans Mono\",monospace;font-weight:400;color:rgba(0,0,0,.9)}\nul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}\nul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em}\nul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}\nul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}\nul.square{list-style-type:square}\nul.circle{list-style-type:circle}\nul.disc{list-style-type:disc}\nul.no-bullet{list-style:none}\nol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}\ndl dt{margin-bottom:.3125em;font-weight:bold}\ndl dd{margin-bottom:1.25em}\nabbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}\nabbr{text-transform:none}\nblockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}\nblockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}\nblockquote cite:before{content:\"\\2014 \\0020\"}\nblockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}\nblockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}\n@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}\nh1{font-size:2.75em}\nh2{font-size:2.3125em}\nh3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}\nh4{font-size:1.4375em}}\ntable{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}\ntable thead,table tfoot{background:#f7f8f7;font-weight:bold}\ntable thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}\ntable tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}\ntable tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}\ntable thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}\nbody{tab-size:4}\nh1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}\nh1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}\n.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:\" \";display:table}\n.clearfix:after,.float-group:after{clear:both}\n*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}\npre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:\"Droid Sans Mono\",\"DejaVu Sans Mono\",monospace;font-weight:400;text-rendering:optimizeSpeed}\n.keyseq{color:rgba(51,51,51,.8)}\nkbd{font-family:\"Droid Sans Mono\",\"DejaVu Sans Mono\",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}\n.keyseq kbd:first-child{margin-left:0}\n.keyseq kbd:last-child{margin-right:0}\n.menuseq,.menu{color:rgba(0,0,0,.8)}\nb.button:before,b.button:after{position:relative;top:-1px;font-weight:400}\nb.button:before{content:\"[\";padding:0 3px 0 2px}\nb.button:after{content:\"]\";padding:0 2px 0 3px}\np a>code:hover{color:rgba(0,0,0,.9)}\n#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}\n#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:\" \";display:table}\n#header:after,#content:after,#footnotes:after,#footer:after{clear:both}\n#content{margin-top:1.25em}\n#content:before{content:none}\n#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}\n#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8}\n#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px}\n#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}\n#header .details span:first-child{margin-left:-.125em}\n#header .details span.email a{color:rgba(0,0,0,.85)}\n#header .details br{display:none}\n#header .details br+span:before{content:\"\\00a0\\2013\\00a0\"}\n#header .details br+span.author:before{content:\"\\00a0\\22c5\\00a0\";color:rgba(0,0,0,.85)}\n#header .details br+span#revremark:before{content:\"\\00a0|\\00a0\"}\n#header #revnumber{text-transform:capitalize}\n#header #revnumber:after{content:\"\\00a0\"}\n#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}\n#toc{border-bottom:1px solid #efefed;padding-bottom:.5em}\n#toc>ul{margin-left:.125em}\n#toc ul.sectlevel0>li>a{font-style:italic}\n#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}\n#toc ul{font-family:\"Open Sans\",\"DejaVu Sans\",sans-serif;list-style-type:none}\n#toc li{line-height:1.3334;margin-top:.3334em}\n#toc a{text-decoration:none}\n#toc a:active{text-decoration:underline}\n#toctitle{color:#7a2518;font-size:1.2em}\n@media only screen and (min-width:768px){#toctitle{font-size:1.375em}\nbody.toc2{padding-left:15em;padding-right:0}\n#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}\n#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}\n#toc.toc2>ul{font-size:.9em;margin-bottom:0}\n#toc.toc2 ul ul{margin-left:0;padding-left:1em}\n#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}\nbody.toc2.toc-right{padding-left:0;padding-right:15em}\nbody.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}\n@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}\n#toc.toc2{width:20em}\n#toc.toc2 #toctitle{font-size:1.375em}\n#toc.toc2>ul{font-size:.95em}\n#toc.toc2 ul ul{padding-left:1.25em}\nbody.toc2.toc-right{padding-left:0;padding-right:20em}}\n#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}\n#content #toc>:first-child{margin-top:0}\n#content #toc>:last-child{margin-bottom:0}\n#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}\n#footer-text{color:rgba(255,255,255,.8);line-height:1.44}\n/* harry addition march 2025 for better contrast (a11y) in footer hyperlink text */\n#footer-text a {color: lightblue}\n.sect1{padding-bottom:.625em}\n@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}\n.sect1+.sect1{border-top:1px solid #efefed}\n#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}\n#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:\"\\00A7\";font-size:.85em;display:block;padding-top:.1em}\n#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}\n#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}\n#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}\n.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}\n.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:\"Noto Serif\",\"DejaVu Serif\",serif;font-size:1rem;font-style:italic}\ntable.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0}\n.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)}\ntable.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit}\n.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}\n.admonitionblock>table td.icon{text-align:center;width:80px}\n.admonitionblock>table td.icon img{max-width:none}\n.admonitionblock>table td.icon .title{font-weight:bold;font-family:\"Open Sans\",\"DejaVu Sans\",sans-serif;text-transform:uppercase}\n.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)}\n.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}\n.exampleblock>.content{\n    /* harry modif for new exampleblock source code listings */\n    /*  border-style:solid;\n        border-width:1px;\n        border-color:#e6e6e6;  */\n    margin-bottom:1.25em;\n    /* padding:1.25em; */\n    background:#fff;\n    -webkit-border-radius:4px;\n    border-radius:4px\n}\n.exampleblock>.content>:first-child{margin-top:0}\n.exampleblock>.content>:last-child{margin-bottom:0}\n.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}\n.sidebarblock>:first-child{margin-top:0}\n.sidebarblock>:last-child{margin-bottom:0}\n.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}\n.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}\n.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class=\"highlight\"],.listingblock pre[class^=\"highlight \"],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}\n.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class=\"highlight\"],.sidebarblock .listingblock pre[class^=\"highlight \"],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}\n\n\ndiv.sidebarblock.scratchpad:before {\n  content: \" \";\n  background-image: url('images/papertop.png');\n  position: relative;\n  background-repeat: no-repeat;\n  background-size: 4in auto;\n  display: block;\n  height: 0.386in;\n  margin-bottom: 0;\n  padding-bottom: 0;\n  width: 4in;\n  margin-left: -25px;\n  top: -0.386in;\n  margin-top: 0.386in;\n}\n\ndiv.sidebarblock.scratchpad {\n  background-image: url('images/papermiddle.png');\n  background-position: left top;\n  background-repeat: repeat-y; \n  background-size: 4in auto;\n  padding-top: 0;\n  padding-bottom: 0;\n  padding-left: 25px;\n  width: 4in;\n  page-break-inside: avoid;\n  border: none;\n  font-family: 'Kalam', cursive;\n}\ndiv.sidebarblock.scratchpad:after {\n  content: \" \";\n  background-image: url('images/paperbottom.png');\n  background-repeat: no-repeat; \n  background-size: 4in auto;\n  display: block;\n  height: 0.377in;\n  margin-top: 0;\n  width: 4in;\n  margin-left: -25px;\n  margin-top: 3pt;\n}\n\n.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em}\n.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal}\n@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}\n@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}\n.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}\n.listingblock pre.highlightjs{padding:0}\n.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}\n.listingblock pre.prettyprint{border-width:0}\n.listingblock>.content{position:relative}\n.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}\n.listingblock:hover code[data-lang]:before{display:block}\n.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999}\n.listingblock.terminal pre .command:not([data-prompt]):before{content:\"$\"}\ntable.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}\ntable.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45}\ntable.pyhltable td.code{padding-left:.75em;padding-right:0}\npre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8}\npre.pygments .lineno{display:inline-block;margin-right:.25em}\ntable.pyhltable .linenodiv{background:none!important;padding-right:0!important}\n.quoteblock{margin:0 1em 1.25em 1.5em;display:table}\n.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}\n.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}\n.quoteblock blockquote{margin:0;padding:0;border:0}\n.quoteblock blockquote:before{content:\"\\201c\";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}\n.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}\n.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right}\n.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)}\n.quoteblock .quoteblock blockquote{padding:0 0 0 .75em}\n.quoteblock .quoteblock blockquote:before{display:none}\n.verseblock{margin:0 1em 1.25em 1em}\n.verseblock pre{font-family:\"Open Sans\",\"DejaVu Sans\",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}\n.verseblock pre strong{font-weight:400}\n.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}\n.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}\n.quoteblock .attribution br,.verseblock .attribution br{display:none}\n.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}\n.quoteblock.abstract{margin:0 0 1.25em 0;display:block}\n.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0}\n.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none}\ntable.tableblock{max-width:100%;border-collapse:separate}\ntable.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0}\ntable.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}\ntable.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0}\ntable.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0}\ntable.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0}\ntable.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0}\ntable.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0}\ntable.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0}\ntable.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0}\ntable.frame-all{border-width:1px}\ntable.frame-sides{border-width:0 1px}\ntable.frame-topbot{border-width:1px 0}\nth.halign-left,td.halign-left{text-align:left}\nth.halign-right,td.halign-right{text-align:right}\nth.halign-center,td.halign-center{text-align:center}\nth.valign-top,td.valign-top{vertical-align:top}\nth.valign-bottom,td.valign-bottom{vertical-align:bottom}\nth.valign-middle,td.valign-middle{vertical-align:middle}\ntable thead th,table tfoot th{font-weight:bold}\ntbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}\ntbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}\np.tableblock>code:only-child{background:none;padding:0}\np.tableblock{font-size:1em}\ntd>div.verse{white-space:pre}\nol{margin-left:1.75em}\nul li ol{margin-left:1.5em}\ndl dd{margin-left:1.125em}\ndl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}\nol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}\nul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none}\nul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em}\nul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em}\nul.checklist li>p:first-child>input[type=\"checkbox\"]:first-child{width:1em;position:relative;top:1px}\nul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden}\nul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block}\nul.inline>li>*{display:block}\n.unstyled dl dt{font-weight:400;font-style:normal}\nol.arabic{list-style-type:decimal}\nol.decimal{list-style-type:decimal-leading-zero}\nol.loweralpha{list-style-type:lower-alpha}\nol.upperalpha{list-style-type:upper-alpha}\nol.lowerroman{list-style-type:lower-roman}\nol.upperroman{list-style-type:upper-roman}\nol.lowergreek{list-style-type:lower-greek}\n.hdlist>table,.colist>table{border:0;background:none}\n.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}\ntd.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}\ntd.hdlist1{font-weight:bold;padding-bottom:1.25em}\n.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}\n/* harry vertical-align callout buttons top */\n.colist>table tr>td:first-of-type{\n    padding:0.4em .75em;\n    line-height:1;\n    vertical-align:top;\n}\n.colist>table tr>td:last-of-type{padding:.25em 0}\n.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}\n.imageblock.left,.imageblock[style*=\"float: left\"]{margin:.25em .625em 1.25em 0}\n.imageblock.right,.imageblock[style*=\"float: right\"]{margin:.25em 0 1.25em .625em}\n.imageblock>.title{margin-bottom:0}\n.imageblock.thumb,.imageblock.th{border-width:6px}\n.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}\n.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}\n.image.left{margin-right:.625em}\n.image.right{margin-left:.625em}\na.image{text-decoration:none;display:inline-block}\na.image object{pointer-events:none}\nsup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}\nsup.footnote a,sup.footnoteref a{text-decoration:none}\nsup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}\n#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}\n#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0}\n#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;text-indent:-1.05em;margin-bottom:.2em}\n#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none}\n#footnotes .footnote:last-of-type{margin-bottom:0}\n#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}\n.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}\n.gist .file-data>table td.line-data{width:99%}\ndiv.unbreakable{page-break-inside:avoid}\n.big{font-size:larger}\n.small{font-size:smaller}\n.underline{text-decoration:underline}\n.overline{text-decoration:overline}\n.line-through{text-decoration:line-through}\n.aqua{color:#00bfbf}\n.aqua-background{background-color:#00fafa}\n.black{color:#000}\n.black-background{background-color:#000}\n.blue{color:#0000bf}\n.blue-background{background-color:#0000fa}\n.fuchsia{color:#bf00bf}\n.fuchsia-background{background-color:#fa00fa}\n.gray{color:#606060}\n.gray-background{background-color:#7d7d7d}\n.green{color:#006000}\n.green-background{background-color:#007d00}\n.lime{color:#00bf00}\n.lime-background{background-color:#00fa00}\n.maroon{color:#600000}\n.maroon-background{background-color:#7d0000}\n.navy{color:#000060}\n.navy-background{background-color:#00007d}\n.olive{color:#606000}\n.olive-background{background-color:#7d7d00}\n.purple{color:#600060}\n.purple-background{background-color:#7d007d}\n.red{color:#bf0000}\n.red-background{background-color:#fa0000}\n.silver{color:#909090}\n.silver-background{background-color:#bcbcbc}\n.teal{color:#006060}\n.teal-background{background-color:#007d7d}\n.white{color:#bfbfbf}\n.white-background{background-color:#fafafa}\n.yellow{color:#bfbf00}\n.yellow-background{background-color:#fafa00}\nspan.icon>.fa{cursor:default}\n.admonitionblock td.icon [class^=\"fa icon-\"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}\n.admonitionblock td.icon .icon-note:before{content:\"\\f05a\";color:#19407c}\n.admonitionblock td.icon .icon-tip:before{content:\"\\f0eb\";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}\n.admonitionblock td.icon .icon-warning:before{content:\"\\f071\";color:#bf6900}\n.admonitionblock td.icon .icon-caution:before{content:\"\\f06d\";color:#bf3400}\n.admonitionblock td.icon .icon-important:before{content:\"\\f06a\";color:#bf0000}\n.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:\"Open Sans\",\"DejaVu Sans\",sans-serif;font-style:normal;font-weight:bold}\n.conum[data-value] *{color:#fff!important}\n.conum[data-value]+b{display:none}\n.conum[data-value]:after{content:attr(data-value)}\npre .conum[data-value]{position:relative;top:-.125em}\nb.conum *{color:inherit!important}\n.conum:not([data-value]):empty{display:none}\ndt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}\nh1,h2,p,td.content,span.alt{letter-spacing:-.01em}\np strong,td.content strong,div.footnote strong{letter-spacing:-.005em}\np,blockquote,dt,td.content,span.alt{font-size:1.0625rem}\np{margin-bottom:1.25rem}\n.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}\n.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}\n.print-only{display:none!important}\n@media print{@page{margin:1.25cm .75cm}\n*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}\na{color:inherit!important;text-decoration:underline!important}\na.bare,a[href^=\"#\"],a[href^=\"mailto:\"]{text-decoration:none!important}\na[href^=\"http:\"]:not(.bare):after,a[href^=\"https:\"]:not(.bare):after{content:\"(\" attr(href) \")\";display:inline-block;font-size:.875em;padding-left:.25em}\nabbr[title]:after{content:\" (\" attr(title) \")\"}\npre,blockquote,tr,img,object,svg{page-break-inside:avoid}\nthead{display:table-header-group}\nsvg{max-width:100%}\np,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}\nh2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}\n#toc,.sidebarblock,.exampleblock>.content{background:none!important}\n#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important}\n.sect1{padding-bottom:0!important}\n.sect1+.sect1{border:0!important}\n#header>h1:first-child{margin-top:1.25rem}\nbody.book #header{text-align:center}\nbody.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0}\nbody.book #header .details{border:0!important;display:block;padding:0!important}\nbody.book #header .details span:first-child{margin-left:0!important}\nbody.book #header .details br{display:block}\nbody.book #header .details br+span:before{content:none!important}\nbody.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}\nbody.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}\n.listingblock code[data-lang]:before{display:block}\n#footer{background:none!important;padding:0 .9375em}\n#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em}\n.hide-on-print{display:none!important}\n.print-only{display:block!important}\n.hide-for-print{display:none!important}\n.show-for-print{display:inherit!important}}\n"
  },
  {
    "path": "atlas.json",
    "content": "{\n  \"branch\": \"main\",\n  \"files\": [\n    \"cover.html\",\n    \"praise.html\",\n    \"titlepage.html\",\n    \"copyright.html\",\n    \"toc.html\",\n    \"preface.asciidoc\",\n    \"ai_preface.asciidoc\",\n    \"pre-requisite-installations.asciidoc\",\n    \"acknowledgments.asciidoc\",\n    \"part1.asciidoc\",\n    \"chapter_01.asciidoc\",\n    \"chapter_02_unittest.asciidoc\",\n    \"chapter_03_unit_test_first_view.asciidoc\",\n    \"chapter_04_philosophy_and_refactoring.asciidoc\",\n    \"chapter_05_post_and_database.asciidoc\",\n    \"chapter_06_explicit_waits_1.asciidoc\",\n    \"chapter_07_working_incrementally.asciidoc\",\n    \"chapter_08_prettification.asciidoc\",\n    \"part2.asciidoc\",\n    \"chapter_09_docker.asciidoc\",\n    \"chapter_10_production_readiness.asciidoc\",\n    \"chapter_11_server_prep.asciidoc\",\n    \"chapter_12_ansible.asciidoc\",\n    \"part3.asciidoc\",\n    \"chapter_13_organising_test_files.asciidoc\",\n    \"chapter_14_database_layer_validation.asciidoc\",\n    \"chapter_15_simple_form.asciidoc\",\n    \"chapter_16_advanced_forms.asciidoc\",\n    \"part4.asciidoc\",\n    \"chapter_17_javascript.asciidoc\",\n    \"chapter_18_second_deploy.asciidoc\",\n    \"chapter_19_spiking_custom_auth.asciidoc\",\n    \"chapter_20_mocking_1.asciidoc\",\n    \"chapter_21_mocking_2.asciidoc\",\n    \"chapter_22_fixtures_and_wait_decorator.asciidoc\",\n    \"chapter_23_debugging_prod.asciidoc\",\n    \"chapter_24_outside_in.asciidoc\",\n    \"chapter_25_CI.asciidoc\",\n    \"chapter_26_page_pattern.asciidoc\",\n    \"chapter_27_hot_lava.asciidoc\",\n    \"epilogue.asciidoc\",\n    \"bibliography.asciidoc\",\n    \"appendix_IX_cheat_sheet.asciidoc\",\n    \"appendix_X_what_to_do_next.asciidoc\",\n    \"appendix_github_links.asciidoc\",\n    \"ix.html\",\n    \"author_bio.html\",\n    \"colo.html\"\n  ],\n  \"formats\": {\n    \"pdf\": {\n      \"version\": \"print\",\n      \"toc\": true,\n      \"index\": true,\n      \"antennahouse_version\": \"AHFormatterV71_64-MR2\",\n      \"syntaxhighlighting\": true,\n      \"show_comments\": false,\n      \"color_count\": \"1\",\n      \"trim_size\": \"7inx9.1875in\"\n    },\n    \"epub\": {\n      \"index\": true,\n      \"toc\": true,\n      \"epubcheck\": true,\n      \"syntaxhighlighting\": true,\n      \"show_comments\": false,\n      \"downsample_images\": false,\n      \"mathmlreplacement\": false\n    },\n    \"mobi\": {\n      \"index\": true,\n      \"toc\": true,\n      \"syntaxhighlighting\": true,\n      \"show_comments\": false,\n      \"downsample_images\": false\n    },\n    \"html\": {\n      \"index\": true,\n      \"toc\": true,\n      \"consolidated\": false,\n      \"syntaxhighlighting\": true,\n      \"show_comments\": false\n    }\n  },\n  \"theme\": \"oreillymedia/animal_theme_sass\",\n  \"title\": \"Test-Driven Development with Python\",\n  \"print_isbn13\": \"9781098148713\",\n  \"templating\": false,\n  \"lang\": \"en\",\n  \"accent_color\": \"\",\n  \"preprocessing\": \"none\"\n}"
  },
  {
    "path": "author_bio.html",
    "content": "<section data-type=\"colophon\" xmlns=\"http://www.w3.org/1999/xhtml\" class=\"abouttheauthor\">\n  <h1>About the Author</h1>\n  <p>After an idyllic childhood spent playing with BASIC on French 8-bit computers like the Thomson T07 whose keys go \"boop\" when you press them, <strong>Harry Percival</strong> spent a few years being deeply unhappy with economics and management consultancy. Soon, he rediscovered his true geek nature, and was lucky enough to fall in with a bunch of XP fanatics, working on the pioneering but sadly defunct Resolver One spreadsheet. He now works at PythonAnywhere LLP, and spreads the gospel of test-driven development worldwide at talks, workshops, and conferences, with all the passion and enthusiasm of a recent convert.</p>\n</section>\n"
  },
  {
    "path": "bibliography.asciidoc",
    "content": "[role=\"bibliography\":\"]\n[appendix]\n\n== Bibliography\n\nA few books about TDD and software development that I've mentioned in the book,\nand which I enthusiastically recommend.\n\n++++\n<ul class=\"simplelist\">\n<li>Kent Beck, <em>Test Driven Development: By Example</em>, Addison-Wesley</li>\n<li>Martin Fowler, <em>Refactoring, Addison-Wesley</em></li>\n<li>Ross Anderson, <em>Security Engineering, Third Edition</em>, Addison-Wesley: <em>https://www.cl.cam.ac.uk/archive/rja14/book.html</em></li>\n<li id=\"GOOSGBT\">Steve Freeman and Nat Pryce, <em>Growing Object-Oriented Software Guided by Tests</em>, Addison-Wesley</li>\n<li>Hal Abelson, Jerry Sussman and Julie Sussman, <em>Structure and Interpretation of Computer Programs</em> (SICP), MIT Press</li>\n<li>Dave Farley, <em>Modern Software Engineering</em>,  Addison-Wesley</li>\n</ul>\n++++"
  },
  {
    "path": "book.asciidoc",
    "content": ":doctype: book\n:bookseries: pythonbook\n:source-highlighter: pygments\n:pygments-style: manni\n:icons: font\n\n= Test-Driven Development with Python\n:toc:\n\n\n:sectnums!:\n\ninclude::praise.forbook.asciidoc[]\ninclude::preface.asciidoc[]\ninclude::ai_preface.asciidoc[]\ninclude::pre-requisite-installations.asciidoc[]\n//include::video_plug.asciidoc[]\ninclude::acknowledgments.asciidoc[]\n\ninclude::part1.forbook.asciidoc[]\n\n:sectnums:\n\ninclude::chapter_01.asciidoc[]\ninclude::chapter_02_unittest.asciidoc[]\ninclude::chapter_03_unit_test_first_view.asciidoc[]\ninclude::chapter_04_philosophy_and_refactoring.asciidoc[]\ninclude::chapter_05_post_and_database.asciidoc[]\ninclude::chapter_06_explicit_waits_1.asciidoc[]\ninclude::chapter_07_working_incrementally.asciidoc[]\ninclude::chapter_08_prettification.asciidoc[]\n\ninclude::part2.forbook.asciidoc[]\ninclude::chapter_09_docker.asciidoc[]\ninclude::chapter_10_production_readiness.asciidoc[]\ninclude::chapter_11_server_prep.asciidoc[]\ninclude::chapter_12_ansible.asciidoc[]\n\ninclude::part3.forbook.asciidoc[]\ninclude::chapter_13_organising_test_files.asciidoc[]\ninclude::chapter_14_database_layer_validation.asciidoc[]\ninclude::chapter_15_simple_form.asciidoc[]\ninclude::chapter_16_advanced_forms.asciidoc[]\n\ninclude::part4.forbook.asciidoc[]\ninclude::chapter_17_javascript.asciidoc[]\ninclude::chapter_18_second_deploy.asciidoc[]\ninclude::chapter_19_spiking_custom_auth.asciidoc[]\ninclude::chapter_20_mocking_1.asciidoc[]\ninclude::chapter_21_mocking_2.asciidoc[]\ninclude::chapter_22_fixtures_and_wait_decorator.asciidoc[]\ninclude::chapter_23_debugging_prod.asciidoc[]\ninclude::chapter_24_outside_in.asciidoc[]\ninclude::chapter_25_CI.asciidoc[]\ninclude::chapter_26_page_pattern.asciidoc[]\ninclude::chapter_27_hot_lava.asciidoc[]\n\ninclude::epilogue.asciidoc[]\n\ninclude::appendix_fts_for_external_dependencies.asciidoc[]\ninclude::appendix_CD.asciidoc[]\ninclude::appendix_bdd.asciidoc[]\ninclude::appendix_purist_unit_tests.asciidoc[]\ninclude::appendix_rest_api.asciidoc[]\ninclude::appendix_IX_cheat_sheet.asciidoc[]\ninclude::appendix_X_what_to_do_next.asciidoc[]\ninclude::appendix_github_links.asciidoc[]\n\ninclude::bibliography.asciidoc[]\n\n"
  },
  {
    "path": "buy_the_book_banner.html",
    "content": "<div id=\"buy_the_book\" style=\"position: absolute; top: 0; right: 0; z-index:100\">\n  <a href=\"/pages/book.html\">\n    <img src=\"images/buy_the_book.svg\"  alt=\"buy the book ribbon\"/>\n  </a>\n</div>\n"
  },
  {
    "path": "chapter_01.asciidoc",
    "content": "[[chapter_01]]\n== Getting Django Set Up Using a [keep-together]#Functional Test#\n\nTest-driven development isn't something that comes naturally.\nIt's a discipline, like a martial art, and just like in a Kung Fu movie,\nyou need a bad-tempered and unreasonable master to force you to learn the discipline.\nOurs is the Testing Goat.\n\n\n=== Obey the Testing Goat! Do Nothing Until You Have a Test\n\n\n(((\"Testing Goat\", \"defined\")))\nThe Testing Goat is the unofficial mascotfootnote:[\nOK more of a minor running joke from PyCon in the mid 2010s,\nwhich I am single-handedly trying to make into a thing.]\nof TDD in the Python testing community.\nIt probably means different things to different people,\nbut, to me, the Testing Goat is a voice inside my head\nthat keeps me on the True Path of Testing--like\none of those little angels or demons that pops up by your shoulder in the cartoons,\nbut with a very niche set of concerns.\nI hope, with this book, to install the Testing Goat inside your head too.\n\nSo we've decided to build a web app, even if we're not quite sure what it's going to do yet.\nNormally, the first step in web development is getting your web framework installed and configured.\n__Download this, install that, configure the other, run the script__...but TDD requires a different mindset.\nWhen you're doing TDD,\nyou always have the Testing Goat inside\nyour head--single-minded as goats are--bleating\n``Test first, test first!''\n\nIn TDD the first step is always the same: _write a test_.\n\n_First_ we write the test; _then_ we run it and check that it fails as expected.\n_Only then_ do we go ahead and build some of our app.\nRepeat that to yourself in a goat-like voice.  I know I do.\n\nAnother thing about goats is that they take one step at a time.\nThat's why they seldom fall off things, see, no matter how steep they are—as you can see in <<tree_goat>>.\n[[tree_goat]]\n.Goats are more agile than you think (source: Caitlin Stewart, on Flickr)\nimage::images/tdd3_0101.png[\"A picture of a goat up a tree\", scale=\"50\"]\n\n\nWe'll proceed with nice small steps;\nwe're going to use _Django_, which is a popular Python web framework, to build our app.\n\n\n(((\"Django framework\", \"set up\", id=\"DJFsetup01\")))\nThe first thing we want to do is check that we've got Django installed\nand that it's ready for us to work with.\nThe _way_ we'll check is by confirming that we can spin up Django's development server\nand actually see it serving up a web page, in our web browser, on our local computer.\nWe'll use the _Selenium_ browser automation tool for this.(((\"Selenium\")))\n\n[[first-FT]]\n(((\"functional tests (FTs)\", \"creating\")))\nCreate a new Python file called _functional_tests.py_\nwherever you want to keep the code for your project, and enter the following code.\nIf you feel like making a few little goat noises as you do it, it may help:\n\n[role=\"sourcecode\"]\n.functional_tests.py\n====\n[source,python]\n----\nfrom selenium import webdriver\n\nbrowser = webdriver.Firefox()\nbrowser.get(\"http://localhost:8000\")\n\nassert \"Congratulations!\" in browser.title\nprint(\"OK\")\n----\n====\n\nThat's our first _functional test_ (FT);\nI'll talk more about what I mean by functional tests,\nand how they contrast with unit tests, in a bit.\nFor now, it's enough to assure ourselves that we understand what it's doing:\n\n- Starting a Selenium WebDriver to pop up a real Firefox browser window.(((\"Selenium WebDriver\")))(((\"Firefox\")))\n\n- Using it to open up a web page, which we're expecting to be served from the local computer.\n\n- Checking (making a test assertion) that the page has the word \"Congratulations!\" in its title.\n\n- If all goes well, we print OK.\n\nLet's try running it:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 4, in <module>\n    browser.get(\"http://localhost:8000\")\n  File \".../selenium/webdriver/remote/webdriver.py\", line 483, in get\n    self.execute(Command.GET, {\"url\": url})\n  File \".../selenium/webdriver/remote/webdriver.py\", line 458, in execute\n    self.error_handler.check_response(response)\n  File \".../selenium/webdriver/remote/errorhandler.py\", line 232, in\ncheck_response\n    raise exception_class(message, screen, stacktrace)\nselenium.common.exceptions.WebDriverException: Message: Reached error page: abo\nut:neterror?e=connectionFailure&u=http%3A//localhost%3A8000/[...]\nStacktrace:\nRemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8\nWebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:182:5\nUnknownError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:530:5\n[...]\n----\n\n[role=\"pagebreak-before\"]\nYou should see a browser window pop up trying to open _localhost:8000_,\nshowing the \"Unable to connect\" error page.\nIf you switch back to your console,\nyou'll see the big, ugly error message\ntelling us that Selenium ran into an error page.\nAnd then, you will probably be irritated\nat the fact that it left the Firefox window lying around your desktop for you to tidy up.\nWe'll fix that later!\n\nNOTE: If, instead, you see an error trying to import Selenium, or an error\n    trying to find something called \"geckodriver\", you might need\n    to go back and have another look at the &#x201c;<<pre-requisites>>&#x201d;.\n\n[[firefox_upgrade_popup_aside]]\n.What to Do If You Get a Firefox Upgrade Pop-up\n*******************************************************************************\n(((\"Selenium\", \"upgrading\")))\n(((\"geckodriver\", \"upgrading\")))\n(((\"Firefox\", \"upgrading\")))\n(((\"functional tests (FTs)\", \"troubleshooting hung tests\")))\n(((\"troubleshooting\", \"hung functional tests\")))\nNow and again, when running Selenium tests,\nyou might encounter a strange pop-up window, such as the one shown in <<firefox_upgrade_popup>>.\n\n[[firefox_upgrade_popup]]\n.Firefox wants to install a new what now?\nimage::images/tdd3_0102.png[\"A pop-up window saying 'Firefox is trying to install a new helper tool.' and prompting for a username and password\"]\n\nThis happens when Firefox has automatically downloaded a new version,\nin the background.\nWhen Selenium tries to load a fresh Firefox session,\nit wants to install the latest version of its \"geckodriver\" plugin.\n\nTo resolve the issue, you have to close the Selenium browser window,\ngo back to your main browser window\nand tell it to install the upgrade and restart itself,\nand then try again.\n\nNOTE: If something strange is going on with your FTs,\n    it's worth checking if there's a Firefox upgrade pending.\n*******************************************************************************\n\n\nFor now though, we have a _failing test_,\nso that means we're allowed to start building our app.\n\n\n\n\n=== Getting Django Up and Running\n\n(((\"Django framework\", \"set up\", \"project creation\")))\nAs you've definitely read &#x201c;<<pre-requisites>>&#x201d; by now,\nyou've already got Django installed (right?).\nThe first step in getting Django up and running is to create a _project_,\nwhich will be the main container for our site.\nDjango provides a little command-line tool for this:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *django-admin startproject superlists .*\n----\n//002\n\nDon't forget that \".\" at the end; it's important!\n\n(((\"superlists folder\")))\nThat will create a file called _manage.py_ in(((\"manage.py file\"))) your current folder,\nand a subfolder called “_superlists_”, with more stuff inside it:\n\n----\n.\n├── functional_tests.py\n├── manage.py\n└── superlists\n    ├── __init__.py\n    ├── asgi.py\n    ├── settings.py\n    ├── urls.py\n    └── wsgi.py\n----\n\nNOTE: Make sure your project folder looks exactly like this!\n    If you see two nested folders called \"superlists\",\n    it's because you forgot the \".\" in the command.\n    Delete them and try again,\n    or there will be lots of confusion\n    with paths and working directories.\n\nThe _superlists_ folder is intended for stuff that applies to the whole project--like _settings.py_, which is used to store global configuration information for the site.\n\n[role=\"pagebreak-before\"]\nBut the main thing to notice is _manage.py_.\nThat's Django's Swiss Army knife,\nand one of the things it can do is run(((\"development server\", \"running with manage.py\")))(((\"manage.py file\", \"running a development server\"))) a development server.\nLet's try that now:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py runserver*]\nWatching for file changes with StatReloader\nPerforming system checks...\n\nSystem check identified no issues (0 silenced).\n\nYou have 18 unapplied migration(s). Your project may not work properly until\nyou apply the migrations for app(s): admin, auth, contenttypes, sessions.\nRun 'python manage.py migrate' to apply them.\nMarch 17, 2023 - 18:07:30\nDjango version 5.2.4, using settings 'superlists.settings'\nStarting development server at http://127.0.0.1:8000/\nQuit the server with CONTROL-C.\n----\n\n// IDEA: get this under test\n\nThat's Django's development server now up and running on our machine.\n\nNOTE: It's safe to ignore that message about \"unapplied migrations\" for now.\n    We'll look at migrations in <<chapter_05_post_and_database>>.\n\nLeave it there and open(((\"virtualenv (virtual environment)\", \"activating and using in functional test\"))) another command shell.  Navigate to your project\nfolder, activate your virtualenv, and then try running our test again:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nOK\n----\n\nNot much action on the command line, but you should notice two things: firstly,\nthere was no ugly `AssertionError` and, secondly, the Firefox window that\nSelenium popped up had a different-looking page on it.(((\"AssertionError\")))\n\n\nTIP: If you see an error saying \"ModuleNotFoundError: No module named selenium\",\n    you've forgotten to activate your virtualenv.\n    Check the &#x201c;<<pre-requisites>>&#x201d; section again, if you need to.\n\nWell, it may not look like much, but that was our first ever passing test!\nHooray!\n\n\nIf it all feels a bit too much like magic, like it wasn't quite real,\nwhy not go and take a look at the dev server manually,\nby opening a web browser yourself and visiting pass:[<em>http://localhost:8000</em>]?\nYou should see something like <<installed_successfully_screenshot>>.\n\nYou can quit the development server now if you like,\nback in the original shell, using Ctrl+C.\n\n[[installed_successfully_screenshot]]\n.It worked!\nimage::images/tdd3_0103.png[\"Screenshot of Django Installed Successfully Screen\"]\n\n\n.Adieu to Roman Numerals!\n*******************************************************************************\nSo many introductions to TDD (((\"Roman numerals in examples\")))use Roman numerals in their examples\nthat it has become a running joke--I even started writing one myself.\nIf you're curious, you can find it\non https://github.com/hjwp/tdd-roman-numeral-calculator[my GitHub page].\n\nRoman numerals, as an example, are both good and bad.\nIt's a nice \"toy\" problem, reasonably limited in scope,\nand you can explain the core of TDD quite well with it.\n\nThe problem is that it can be hard to relate to the real world.\nThat's why I've decided to use the building of a real web app,\nstarting from nothing, as my example.\nAlthough it's a simple web app,\nmy hope is that it will be easier for you to carry across to your next real project.\n\nIn addition, it means we can start out using functional tests\nas well as unit tests, and demonstrate a TDD workflow that's\nmore like real life, and less like that of a toy project.\n\n*******************************************************************************\n\n[role=\"pagebreak-before less_space\"]\n=== Starting a Git Repository\n\n(((\"Git\", \"starting repositories\")))\n(((\"version control systems (VCSs)\", seealso=\"Git\")))\nThere's one last thing to do before we finish the chapter:\nstart to commit our work to a _version control system_ (VCS).\nIf you're an experienced programmer, you don't need to hear me preaching about version control. But if you're new to it, please believe me when I say that VCS is a must-have.\nAs soon as your project gets to be more than a few weeks old and a few lines of code,\nhaving a tool available to look back over old versions of code,\nrevert changes, explore new ideas safely, even just as a backup...It's hard to overstate how useful that is.\nTDD goes hand in hand with version control,\nso I want to make sure I impart how it fits into the workflow.\n\n.Our Working Directory Is Always the Folder That Contains manage.py\n******************************************************************************\nWe'll be using this same folder throughout the book\nas our working directory--if in doubt, it's(((\"manage.py file\", \"working directory containing\"))) the one that contains _manage.py_.\n\n(For simplicity, in my command listings, I'll always show it as: _...goat-book/_. Although it will probably actually be something like:\n_/home/kind-reader-username/my-python-projects/goat-book/_.)\n\nWhenever I show a command to type in, I will assume we're in this directory.\nSimilarly, if I mention a path to a file, it will be relative to this directory.\nSo, for example, _superlists/settings.py_ means the _settings.py_ inside the _superlists_ folder.\n\n******************************************************************************\n\n\nSo, our first commit!\nIf anything, it's a bit late; shame on us.\nWe're using _Git_ as our VCS, ’cos it's the best.\n\nLet's start by doing the `git init` to start the repository:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ls*\ndb.sqlite3  functional_tests.py  manage.py  superlists\n\n$ *git init .*\nInitialised empty Git repository in ...goat-book/.git/\n----\n\n[role=\"pagebreak-before less_space\"]\n.Setting the Default Branch Name in Git\n*******************************************************************************\n\nIf you see this message:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\nhint: Using 'master' as the name for the initial branch. This default branch\nhint: name is subject to change. To configure the initial branch name to use\nhint: in all of your new repositories, which will suppress this warning, call:\nhint:\nhint: \tgit config --global init.defaultBranch <name>\nhint:\nhint: Names commonly chosen instead of 'master' are 'main', 'trunk' and\nhint: 'development'. The just-created branch can be renamed via this command:\nhint:\nhint: \tgit branch -m <name>\nInitialised empty Git repository in ...goat-book/.git/\n----\n\nConsider following the advice and choosing an explicit default branch name.(((\"default branch name in Git\")))(((\"Git\", \"default branch name, choosing\")))\nI chose `main`. It's a popular choice, and you might see it here and there in the book.\nSo if you want to match that, do:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git config --global init.defaultBranch main*\n# then let's re-create our git repo by deleting and starting again:\n$ *rm -rf .git*\n$ *git init .*\nInitialised empty Git repository in ...goat-book/.git/\n----\n\n*******************************************************************************\n\n\n\n(((\"Git\", \"commits\")))\nNow let's take a look and see what files we want to commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ls*\ndb.sqlite3 functional_tests.py manage.py superlists\n----\n\nThere are a few things in here that we _don't_ want under version control:\n_db.sqlite3_ is the database file, and our virtualenv shouldn't be in Git either.\nWe'll add all of (((\".gitignore file\", primary-sortas=\"gitignore\")))them to a special file called _.gitignore_ which, um, tells Git what to ignore:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo \"db.sqlite3\" >> .gitignore*\n$ *echo \".venv\" >> .gitignore*\n----\n\n[role=\"pagebreak-before\"]\nNext we can add the rest of the contents of the current \".\" folder:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*git add .*]\n$ pass:quotes[*git status*]\nOn branch main\n\nNo commits yet\n\nChanges to be committed:\n  (use \"git rm --cached <file>...\" to unstage)\n\n        new file:   .gitignore\n        new file:   functional_tests.py\n        new file:   manage.py\n        new file:   superlists/__init__.py\n        new file:   superlists/__pycache__/__init__.cpython-314.pyc\n        new file:   superlists/__pycache__/settings.cpython-314.pyc\n        new file:   superlists/__pycache__/urls.cpython-314.pyc\n        new file:   superlists/__pycache__/wsgi.cpython-314.pyc\n        new file:   superlists/asgi.py\n        new file:   superlists/settings.py\n        new file:   superlists/urls.py\n        new file:   superlists/wsgi.py\n----\n\nOops!  We've got a bunch of '.pyc' files in there;\nit's pointless to commit those.\nLet's remove them from Git and add them to '.gitignore' too:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:[<strong>git rm -r --cached superlists/__pycache__</strong>]\nrm 'superlists/__pycache__/__init__.cpython-314.pyc'\nrm 'superlists/__pycache__/settings.cpython-314.pyc'\nrm 'superlists/__pycache__/urls.cpython-314.pyc'\nrm 'superlists/__pycache__/wsgi.cpython-314.pyc'\n$ pass:[<strong>echo \"__pycache__\" >> .gitignore</strong>]\n$ pass:[<strong>echo \"*.pyc\" >> .gitignore</strong>]\n----\n\n[role=\"pagebreak-before\"]\nNow let's see where we are...\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:[<strong>git status</strong>]\nOn branch main\n\nInitial commit\n\nChanges to be committed:\n  (use \"git rm --cached <file>...\" to unstage)\n\n        new file:   .gitignore\n        new file:   functional_tests.py\n        new file:   manage.py\n        new file:   superlists/__init__.py\n        new file:   superlists/asgi.py\n        new file:   superlists/settings.py\n        new file:   superlists/urls.py\n        new file:   superlists/wsgi.py\n\nChanges not staged for commit:\n  (use \"git add <file>...\" to update what will be committed)\n  (use \"git restore <file>...\" to discard changes in working directory)\n\n        modified:   .gitignore\n----\n\n\nTIP: You'll see I'm using `git status` a lot--so much so that\n    I often alias it to `git st`...I'm not telling you how to do that though;\n    I leave you to discover the secrets of Git aliases on your own!(((\"aliases in Git\")))\n\n\nLooking good--we're ready to do our first commit!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add .gitignore*\n$ *git commit*\n----\n\n[role=\"pagebreak-before\"]\nWhen you type `git commit`, it will pop up an editor window for you to write your commit message in.\nMine looked like <<first_git_commit>>.footnote:[\nDid a strange terminal-based editor (the dreaded Vim) pop up and you had no idea what to do?\nOr did you see a message about account identity and `git config --global\nuser.username`?\nCheck out the Git manual and its\nhttp://git-scm.com/book/en/Customizing-Git-Git-Configuration[basic configuration section].\nPS: To quit Vim, it's Esc, then `:q!`]\n\n[[first_git_commit]]\n.First Git commit\nimage::images/tdd3_0104.png[\"Screenshot of git commit vi window\"]\n\n\nNOTE: If you want to really go to town on Git,\n    this is the time to also learn about how to push your work\n    to a cloud-based VCS hosting service like GitHub or GitLab.\n    They'll be useful if you think you want to follow along with this book on different computers.(((\"GitHub or GitLab VCS, cloud-based, pushing work to\")))\n    I leave it to you to find out how they work; they have excellent documentation.\n    Alternatively, you can wait until <<chapter_25_CI>>, where we'll use one.\n\nThat's it for the VCS lecture. Congratulations!\nYou've written a functional test using Selenium,\nand you've gotten Django installed and running,\nin a certifiable, test-first, goat-approved TDD way.\nGive yourself a well-deserved pat on the back\nbefore moving on to <<chapter_02_unittest>>.(((\"\", startref=\"DJFsetup01\")))\n"
  },
  {
    "path": "chapter_02_unittest.asciidoc",
    "content": "[[chapter_02_unittest]]\n== Extending Our Functional Test Using [keep-together]#the unittest Module#\n\n(((\"functional tests (FTs)\", \"using unittest module\", id=\"FTunittest02\")))\n(((\"unittest module\", \"basic functional test creation\", id=\"UTMbasic02\")))\nLet's adapt our test, which currently checks for the default Django \"it worked\" page,\nand check instead for some of the things we want to see on the real front page of our site.\n\nTime to reveal what kind of web app we're building: a to-do lists site!(((\"to-do lists website, building\")))\nI know, I know, every other web dev tutorial online is also a to-do lists app,\nor maybe a blog or a polls app.\nI'm very much following fashion.\n\nThe reason is that a to-do list is a really nice example.\nAt its most basic, it is very simple indeed--just a list of text strings--so\nit's easy to get a \"minimum viable\" list app up and running.\nBut it can be extended in all sorts of ways--different persistence models,\nadding deadlines, reminders, sharing with other users, and improving the client-side UI.\nThere's no reason to be limited to just \"to-do\" lists either;\nthey could be any kind of lists.\nBut the point is that it should enable me to demonstrate\nall of the main aspects of web programming,\nand how you apply TDD to them.\n\n[role=\"pagebreak-before less_space\"]\n=== Using a Functional Test to Scope Out a Minimum [keep-together]#Viable App#\n\nTests that use Selenium let us drive a real web browser,\nso they really let us see how the application _functions_ from the user's point of view.(((\"minimum viable app, using functional tests as spec\")))\nThat's why they're called _functional tests_.\n\n(((\"user stories\")))\nThis means that an FT can be a sort of specification for your application.\nIt tends to track what you might call a _user story_,\nand follows how the user might work with a particular feature\nand how the app should respond to them.footnote:[\nIf you want to read more about user stories,\ncheck out Gojko Adzic's _Fifty Quick Ideas to Improve Your User Stories_\nor Mike Cohn's _User Stories Applied: For Agile Software Development_.]\n\n\n.Terminology: pass:[<br/>]Functional Test == End-to-End Test == Acceptance Test\n*******************************************************************************************\n\n(((\"end-to-end tests\", see=\"functional tests\")))\n(((\"system tests\", see=\"functional tests\")))\n(((\"acceptance tests\", seealso=\"functional tests\")))\n(((\"black box tests\", see=\"functional tests\")))\nWhat I call functional tests, some people prefer to call _end-to-end tests_,\nor, slightly less commonly, _system tests_.\n\nThe main point is that these kinds of tests look at how the whole application functions,\nfrom the outside.\nAnother name is _black box test_, or _closed box test_,\nbecause the test doesn't know anything about the internals of the system under test.\n\nOthers also like the name _acceptance tests_ (see <<acceptance_tests>>). This distinction is less about the level of granularity of the test or the system, but more about whether the test is checking on the “acceptance criteria” for a feature (i.e., _behaviour_), as visible to the user.(((\"behaviour\", \"acceptance criteria for\")))(((\"features\")))\n*******************************************************************************************\n\nFeature tests should have a human-readable story that we can follow.\nWe make it explicit using comments that accompany the test code.(((\"feature tests\")))\nWhen creating a new FT, we can write the comments first,\nto capture the key points of the user story.\nBeing human-readable, you could even share them with nonprogrammers,\nas a way of discussing the requirements and features of your app.\n\nTest-driven development and Agile or Lean software development methodologies often go together,\nand one of the things we tend to talk about is the minimum viable app:\nwhat is the simplest thing we can build that is still useful?\nLet's start by building that, so that we can test the water as quickly as possible.\n\nA minimum viable to-do list really only needs to let the user enter some to-do items,\nand remember them for their next visit.\n\n[role=\"pagebreak-before\"]\nOpen up _functional_tests.py_ and write a story a bit like this one:\n\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch02l001)\n====\n[source,python]\n----\nfrom selenium import webdriver\n\nbrowser = webdriver.Firefox()\n\n# Edith has heard about a cool new online to-do app.\n# She goes to check out its homepage\nbrowser.get(\"http://localhost:8000\")\n\n# She notices the page title and header mention to-do lists\nassert \"To-Do\" in browser.title\n\n# She is invited to enter a to-do item straight away\n\n# She types \"Buy peacock feathers\" into a text box\n# (Edith's hobby is tying fly-fishing lures)\n\n# When she hits enter, the page updates, and now the page lists\n# \"1: Buy peacock feathers\" as an item in a to-do list\n\n# There is still a text box inviting her to add another item.\n# She enters \"Use peacock feathers to make a fly\" (Edith is very methodical)\n\n# The page updates again, and now shows both items on her list\n\n# Satisfied, she goes back to sleep\n\nbrowser.quit()\n----\n====\n\n.We Have a Word for Comments...\n*******************************************************************************\n\nWhen I first started at PythonAnywhere,\nI used to virtuously pepper my code with nice descriptive comments.\nMy colleagues said to me:\n\"Harry, we have a word for comments.(((\"comments\", \"usefulness (or lack of)\"))) We call them lies.\"\nI was shocked!\nI learned in school that comments are good practice?\n\nThey were exaggerating for effect.\nThere is definitely a place for comments that add context and intention.\nBut my colleagues were pointing out that comments aren't always as useful as you hope.\nFor starters, it's pointless to write a comment that just repeats what you're doing with the code:\n\n[role=\"skipme\"]\n[source,python]\n----\n# increment wibble by 1\nwibble += 1\n----\n\nNot only is it pointless,\nbut there's a danger that you'll forget to update the comments when you update the code,\nand they end up being misleading--lies!\nThe ideal is to strive to make your code so readable,\nto use such good variable names and function names,\nand to structure it so well\nthat you no longer need any comments to explain _what_ the code is doing.\nJust a few here and there to explain _why_.\n\nThere are other places where comments are very useful.\nWe'll see that Django uses them a lot in the files it generates for us\nto use as a way of suggesting helpful bits of its API.\n\nAnd, of course, we use comments to explain the user story in our functional tests--by\nforcing us to make a coherent story out of the test,\nit makes sure we're always testing from the point of view of the user.\n\nThere is more fun to be had in this area,\nthings like _Behaviour-Driven Development_\n(see https://www.obeythetestinggoat.com/book/appendix_bdd.html[Online Appendix: BDD])\nand building domain-specific languages (DSLs) for testing,\nbut they're topics for other books.footnote:[Check out this video by the great Dave Farley if you want a taste:\nhttps://oreil.ly/bbawE.]\n\nFor more on comments, I recommend John Ousterhout's _A Philosophy of Software Design_,\nwhich you can get a taste of by reading\nhis https://oreil.ly/1cdgY[lecture notes from the chapter on comments].\n*******************************************************************************\n\nYou'll notice that, apart from writing the test out as comments,\nI've updated the `assert` to look for \"To-Do\" instead of\nDjango's \"Congratulations\".\nThat means we expect the test to fail now.  Let's try running it.\n\nFirst, start up the server:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py runserver*\n----\n\nAnd then, in another terminal, run the tests:\n\n\n[role=\"pause-first\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 10, in <module>\n    assert \"To-Do\" in browser.title\nAssertionError\n----\n\n\n(((\"expected failures\")))\nThat's what we call an 'expected fail',\nwhich is actually good news--not quite as good as a test that passes,\nbut at least it's failing for the right reason;\nwe can have some confidence we've written the test correctly.\n\n\n[role=\"pagebreak-before less_space\"]\n=== The Python Standard Library's unittest Module\n\nThere are a couple of little annoyances we should probably deal with.(((\"unittest module\", \"contents of\")))\nFirstly, the message \"AssertionError\" isn't very helpful--it would be nice\nif the test told us what it actually found as the browser title.  Also, it's\nleft a Firefox window hanging around the desktop, so it would be nice if that\ngot cleared up for us automatically.\n\nOne option would be to use the second parameter of the `assert` keyword,\nsomething like:\n\n[role=\"skipme\"]\n[source,python]\n----\nassert \"To-Do\" in browser.title, f\"Browser title was {browser.title}\"\n----\n\nAnd we could also use `try/finally` to clean up the old Firefox window.\n\nBut these sorts of problems are quite common in testing,\nand there are some ready-made [keep-together]#solutions# for us\nin the standard library's `unittest` module.\nLet's use that!  In [keep-together]#_functional_tests.py_#:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch02l003)\n====\n[source,python]\n----\nimport unittest\nfrom selenium import webdriver\n\n\nclass NewVisitorTest(unittest.TestCase):  # <1>\n    def setUp(self):  # <3>\n        self.browser = webdriver.Firefox()  #<4>\n\n    def tearDown(self):  # <3>\n        self.browser.quit()\n\n    def test_can_start_a_todo_list(self):  # <2>\n        # Edith has heard about a cool new online to-do app.\n        # She goes to check out its homepage\n        self.browser.get(\"http://localhost:8000\")  # <4>\n\n        # She notices the page title and header mention to-do lists\n        self.assertIn(\"To-Do\", self.browser.title)  # <5>\n\n        # She is invited to enter a to-do item straight away\n        self.fail(\"Finish the test!\")  # <6>\n\n        [...]\n\n        # Satisfied, she goes back to sleep\n\n\nif __name__ == \"__main__\":  # <7>\n    unittest.main()  # <7>\n----\n====\n\nYou'll probably notice a few things here:\n\n<1> Tests are organised into classes, which (((\"unittest.TestCase class\")))(((\"tests\", \"organized into classes in unittest\")))inherit from `unittest.TestCase`.\n\n<2> The main body of the test is in a method called\n    pass:[<code>test_can_start_a_todo_list</code>].\n    Any method whose name starts with `test_` is a test method,\n    and will be run by the test runner.\n    You can have more than one `test_` method per class.\n    Nice descriptive names for our test methods are a good idea too.\n\n<3> `setUp` and `tearDown` are special methods that are\n    run before and after each test.  I'm using them to start and stop our\n    browser. They're a bit like `try/finally`, in that `tearDown` will\n    run even if there's an error during the test\n    itself.footnote:[The only exception is that if you have an exception inside\n    `setUp`, then `tearDown` doesn't run.]\n    No more Firefox windows left lying around!\n\n<4> `browser`, which was previously a global variable, becomes `self.browser`,\n    an attribute of the test class.\n    This lets us pass it between `setUp`, `tearDown`, and the test method itself.\n\n<5> We use `self.assertIn` instead of just `assert` to make our test\n    assertions. [.keep-together]#+unittest+# provides lots of helper functions like this to make\n    test assertions, like `assertEqual`, `assertTrue`, `assertFalse`, and so\n    on.(((\"assertions\", \"helper functions in unittest for\"))) You can find more in the\n    http://docs.python.org/3/library/unittest.html[`unittest` documentation].\n\n<6> `self.fail` just fails no matter what, producing the error message given.\n    I'm using it as a reminder to finish the test.\n\n<7> Finally, we have the `if __name__ == \"__main__\"` clause.\n    (If you've not seen it before,\n    that's how a Python script checks if it's been executed from the command line,\n    rather than just imported by another script.)\n    We call `unittest.main()`,\n    which launches the `unittest` test runner,\n    which will automatically find test classes and methods in the file and run them.\n\n[role=\"pagebreak-before\"]\nNOTE: If you've read the Django testing documentation,\n    you might have seen something called `LiveServerTestCase`,\n    and are wondering whether we should use it now.\n    Full points to you for reading the friendly manual!\n    `LiveServerTestCase` is a bit too complicated for now,\n    but I promise I'll use it in a later chapter.\n\nLet's try out our new and improved FT!footnote:[\nAre you unable to move on because you're wondering what those\n'ch02l00x' things are, next to some of the code listings?  They refer to\nspecific https://github.com/hjwp/book-example/commits/chapter_02_unittest[commits]\nin the book's example repo.  It's all to do with my book's own\nhttps://github.com/hjwp/Book-TDD-Web-Dev-Python/tree/main/tests[tests].  You\nknow, the tests for the tests in the book about testing. They have tests of\ntheir own, naturally.]\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nF\n======================================================================\nFAIL: test_can_start_a_todo_list\n(__main__.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 18, in\ntest_can_start_a_todo_list\n    self.assertIn(\"To-Do\", self.browser.title)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'To-Do' not found in 'The install worked successfully!\nCongratulations!'\n\n ---------------------------------------------------------------------\nRan 1 test in 1.747s\n\nFAILED (failures=1)\n----\n\nThat's a bit nicer, isn't it?\nIt tidied up our Firefox window,\nit gives us a nicely formatted report of how many tests were run and how many failed,\nand the `assertIn` has given us a helpful error message with useful debugging info.\nBonzer!\n\n[role=\"pagebreak-before\"]\nNOTE: If you see some error messages saying `ResourceWarning`\n    about \"unclosed files\", it's safe to ignore those.\n    They seem to come and go, every few Selenium releases.\n    They don't affect the important things to look for in\n    our tracebacks and test results.\n\n.pytest Versus unittest\n*******************************************************************************\nThe Python world is increasingly turning from the standard-library provided [.keep-together]#+unittest+# module\ntowards a third-party tool called `pytest`.\nI'm a big fan too!(((\"pytest\", \"versus unittest\")))(((\"unittest module\", \"pytest versus\")))\n\nThe Django project has a bunch of helpful tools designed to work with `unittest`.\nAlthough it is possible to get them to work with `pytest`,\nit felt like one thing too many to include in this book.\n\nRead Brian Okken's https://pythontest.com/pytest-book[Python Testing with pytest]\nfor an excellent, comprehensive guide to Pytest instead.\n*******************************************************************************\n\n\n\n=== Commit\n\n(((\"Git\", \"commits\")))\nThis is a good point to do a commit; it's a nicely self-contained change.\nWe've expanded our functional test\nto include comments that describe the task we're setting ourselves,\nour minimum viable to-do list.\nWe've also rewritten it to use the Python `unittest` module\nand its various testing helper functions.\n\nDo a **`git status`**&mdash;that\nshould assure you that the only file that has changed is 'functional_tests.py'.\nThen do a **`git diff -w`**,\nwhich shows you the difference between the last commit and what's currently on disk,\nwith the `-w` saying \"ignore whitespace changes\".\n\n[role=\"pagebreak-before\"]\nThat should tell you that 'functional_tests.py' has changed quite substantially:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*git diff -w*]\ndiff --git a/functional_tests.py b/functional_tests.py\nindex d333591..b0f22dc 100644\n--- a/functional_tests.py\n+++ b/functional_tests.py\n@@ -1,15 +1,24 @@\n+import unittest\n from selenium import webdriver\n\n-browser = webdriver.Firefox()\n\n+class NewVisitorTest(unittest.TestCase):\n+    def setUp(self):\n+        self.browser = webdriver.Firefox()\n+\n+    def tearDown(self):\n+        self.browser.quit()\n+\n+    def test_can_start_a_todo_list(self):\n         # Edith has heard about a cool new online to-do app.\n         # She goes to check out its homepage\n-browser.get(\"http://localhost:8000\")\n+        self.browser.get(\"http://localhost:8000\")\n\n         # She notices the page title and header mention to-do lists\n-assert \"To-Do\" in browser.title\n+        self.assertIn(\"To-Do\", self.browser.title)\n\n         # She is invited to enter a to-do item straight away\n+        self.fail(\"Finish the test!\")\n\n[...]\n----\n\nNow let's do a:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -a*\n----\n\nThe `-a` means \"automatically add any changes to tracked files\"\n(i.e., any files that we've committed before).\nIt won't add any brand new files\n(you have to explicitly `git add` them yourself),\nbut often, as in this case, there aren't any new files,\nso it's a useful shortcut.\n\nWhen the editor pops up, add a descriptive commit message,\nlike \"First FT specced out in comments, and now uses unittest\".\n\nNow that our FT uses a real test framework,\nand that we've got placeholder comments for what we want it to do,\nwe're in an excellent position to start writing some real code for our lists app.\nRead on!\n(((\"\", startref=\"FTunittest02\")))\n(((\"\", startref=\"UTMbasic02\")))\n\n.Useful TDD Concepts\n*******************************************************************************\nUser story::\n    A description of how the application will work\n    from the point of view of the user; used to structure a functional test\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"user stories\")))\n    (((\"user stories\")))\n\nExpected failure::\n    When a test fails in the way that we expected it to\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"expected failures\")))\n    (((\"expected failures\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_03_unit_test_first_view.asciidoc",
    "content": "[[chapter_03_unit_test_first_view]]\n== Testing a Simple Home Page [keep-together]#with Unit Tests#\n\nWe finished the last chapter with a functional test (FT) failing,\ntelling us that it wanted the home page for our site to have ``To-Do'' in its title.\nTime to start working on our application.\nIn this chapter, we'll build our first HTML page, find out about URL handling,\nand create responses to HTTP requests with Django's view functions.\n\n.Warning: Things Are About to Get Real\n*******************************************************************************\nThe first two chapters were intentionally nice and light.  From now on, we\nget into some more meaty coding.  Here's a prediction:  at some point, things\nare going to go wrong.  You're going to see different results from what I say\nyou should see. This is a Good Thing, because it will be a genuine\ncharacter-building Learning Experience(TM).\n\nOne possibility is that I've given some ambiguous explanations, and you've\ndone something different from what I intended. Step back and have a think about\nwhat we're trying to achieve at this point in the book. Which file are we\nediting, what do we want the user to be able to do, what are we testing and\nwhy?  It may be that you've edited the wrong file or function, or are running\nthe wrong tests.  I reckon you'll learn more about TDD from these \"stop and think\"\nmoments than you do from all the times when following instructions and\ncopy-pasting goes smoothly.\n\nOr it may be a real bug. Be tenacious, read the error message carefully\n(see <<reading_tracebacks>>),\nand you'll get to the bottom of it.\nIt's probably just a missing comma,\nor trailing slash, or a missing _s_ in one of the Selenium find methods.\nBut, as Zed Shaw memorably insisted in\nhttps://learnpythonthehardway.org[_Learn Python The Hard Way_], debugging is also an absolutely vital part of learning,\nso do stick it out!(((\"Test-Driven Development (TDD)\", \"additional resources\")))(((\"getting help\"))) You can always drop me an mailto:obeythetestinggoat@gmail.com[email]\nif you get really stuck. Happy debugging!\n*******************************************************************************\n\n\n\n=== Our First Django App and Our First Unit Test\n\n(((\"Django framework\", \"code structure in\")))\n(((\"Django framework\", \"unit testing in\", id=\"DJFunit03\")))\nDjango encourages you to structure your code into _apps_.\nThe theory is that one project can have many apps;\nyou can use third-party apps developed by other people,\nand you might even reuse one of your own apps in a different project...although\nI have to say, I've never actually managed the latter, myself!\nStill, apps are a good way to keep your code organised.\n\nLet's start an app for our to-do lists:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py startapp lists*\n----\n\nThat will create a folder called _lists_, next to _manage.py_ and the existing\n_superlists_ folder, and within it a number of placeholder files for things\nlike models, views, and, of immediate interest to us, tests:\n\n----\n.\n├── db.sqlite3\n├── functional_tests.py\n├── lists\n│   ├── __init__.py\n│   ├── admin.py\n│   ├── apps.py\n│   ├── migrations\n│   │   └── __init__.py\n│   ├── models.py\n│   ├── tests.py\n│   └── views.py\n├── manage.py\n└── superlists\n    ├── __init__.py\n    ├── asgi.py\n    ├── settings.py\n    ├── urls.py\n    └── wsgi.py\n----\n\n\n\n=== Unit Tests, and How They Differ from Functional Tests\n\n(((\"unit tests\", \"versus functional tests\", secondary-sortas=\"functional\")))\n(((\"functional tests (FTs)\", \"versus unit tests\", secondary-sortas=\"unit\")))\nAs with so many of the labels we put on things,\nthe line between unit tests and FTs can become a little blurry at times.\nThe basic distinction, though, is that\nFTs test the application from the outside, from the user's point of view.\nUnit tests on the other hand test the application from the inside, from the programmer's point of view.\n\n[role=\"pagebreak-before\"]\nThe TDD approach I'm demonstrating uses both types of test\nto drive the development of our application, and ensure its correctness.\nOur workflow will look a bit like this:\n\n1.  We start by writing a _functional test_, describing a typical\n    example of our new functionality from the user's point of view.\n\n2.  Once we have an FT that fails,\n    we start to think about how to write code that can get it to pass\n    (or at least to get past its current failure).\n    We now use one or more _unit tests_ to define\n    how we want our code to behave--the idea is that\n    each line of production code we write should be tested\n    by (at least) one of our unit tests.\n\n3.  Once we have a failing unit test,\n    we write the smallest amount of _application code_ we can—just enough to get the unit test to pass.\n    We may iterate between steps 2 and 3 a few times,\n    until we think the FT will get a little further.\n\n4.  Now we can rerun our FTs and see if they pass,\n    or get a little further.\n    That may prompt us to write some new unit tests,\n    and some new code, and so on.\n\n5.  Once we're comfortable that the core functionality works end-to-end,\n    we can extend out to cover more permutations and edge cases,\n    using just unit tests now.\n\nYou can see that, all the way through,\nthe FTs are driving what development we do from a high level,\nwhile the unit tests drive what we do at a low level.\n\nThe FTs don't aim to cover every single tiny detail of our app's behaviour;\nthey are there to reassure us that everything is wired up correctly.\nThe unit tests are there to exhaustively check all the lower-level details and corner cases. See <<fts_vs_unit_tests_table>>.\n\n[[fts_vs_unit_tests_table]]\n[options=\"header\"]\n.Functional tests versus unit tests\n|===\n|Functional tests|Unit tests\n\n|One test per feature/user story\n|Many tests per feature\n\n|Tests from the user's point of view\n|Tests the code (i.e., the programmer's point of view)\n\n|Can test that the UI \"really\" works\n|Tests the internals—individual functions or classes\n\n|Provides confidence that everything is wired together correctly and works end-to-end\n|Can exhaustively check permutations, details, and edge cases\n\n|Can warn about problems without telling you exactly what's wrong\n|Can point at exactly where the problem is\n\n|Slow\n|Fast\n|===\n\n[role=\"pagebreak-before\"]\nNOTE: Functional tests should help you build an application that actually works,\n    and guarantee you never accidentally break it.\n    Unit tests should help you to write code that's clean and bug free.\n\nEnough theory for now—let's see how it looks in practice.\n\n\n\n=== Unit Testing in Django\n\n(((\"unit tests\", \"in Django\", \"writing basic\", secondary-sortas=\"Django\", id=\"UTdjango03\")))\nLet's see how to write a unit test for our home page view.\nOpen up the new file at _lists/tests.py_, and you'll see something like this:\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests.py\n====\n[source,python]\n----\nfrom django.test import TestCase\n\n# Create your tests here.\n----\n====\n\n\nDjango has helpfully suggested we use a special version of `TestCase`, which it provides.(((\"unittest.TestCase class\", \"using augmented version of\")))\nIt's an augmented version of the standard `unittest.TestCase`,\nwith some additional Django-specific features,\nwhich we'll discover over the next few chapters.\n\nYou've already seen that the TDD cycle involves starting with a test that fails,\nthen writing code to get it to pass.\nWell, before we can even get that far,\nwe want to know that the unit test we're writing\nwill definitely be run by our automated test runner, whatever it is.\nIn the case of _functional_tests.py_ we're running it directly,\nbut this file made by Django is a bit more like magic.\nSo, just to make sure, let's make a deliberately silly failing test:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch03l002)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\n\nclass SmokeTest(TestCase):\n    def test_bad_maths(self):\n        self.assertEqual(1 + 1, 3)\n----\n====\n\n[role=\"pagebreak-before\"]\nNow, let's invoke this mysterious Django test runner.\nAs usual, it's a _manage.py_ [keep-together]#command#:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nCreating test database for alias 'default'...\nFound 1 test(s).\nSystem check identified no issues (0 silenced).\nF\n======================================================================\nFAIL: test_bad_maths (lists.tests.SmokeTest.test_bad_maths)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/lists/tests.py\", line 6, in test_bad_maths\n    self.assertEqual(1 + 1, 3)\n    ~~~~~~~~~~~~~~~~^^^^^^^^^^\nAssertionError: 2 != 3\n\n ---------------------------------------------------------------------\nRan 1 test in 0.001s\n\nFAILED (failures=1)\nDestroying test database for alias 'default'...\n----\n\nExcellent.  The machinery seems to be working.\nThis is a good point for a commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*  # should show you lists/ is untracked\n$ *git add lists*\n$ *git diff --staged*  # will show you the diff that you're about to commit\n$ *git commit -m \"Add app for lists, with deliberately failing unit test\"*\n----\n\n\nAs you've no doubt guessed,\nthe `-m` flag lets you pass in a commit message at the command line,\nso you don't need to use an editor.\nIt's up to you to pick the way you like to use the Git command line;\nI'll just show you the main ones I've seen used.\nFor me, the key rule of VCS hygiene is:\n_make sure you always review what you're about to commit before you do it_.\n\n[[django-mvc]]\n=== Django's MVC, URLs, and View Functions\n\n(((\"model-view-controller (MVC) pattern\")))(((\"MVC (model-view-controller) pattern\")))\nDjango is structured along a classic _model-view-controller_ (MVC) pattern—well, _broadly_.\nIt definitely does have models,\nbut what Django calls \"views\" are really controllers,\nand the view part is actually provided by the templates,\nbut you can see the general idea is there!\n\nIf you're interested, you can look up the finer points of the discussion\nhttps://oreil.ly/fz-ne[in the Django FAQs].\n\n[role=\"pagebreak-before\"]\nIrrespective of any of that, as with any web server, Django's main job is to\ndecide what to do when a user asks for a particular URL on our site.\nDjango's workflow goes something like this:\n\n. An HTTP _request_ comes in for a particular URL.\n. Django uses some rules to decide which _view_ function should deal with\n  the request (this is referred to as _resolving_ the URL).\n. The view function processes the request and returns an HTTP _response_.\n\n\nSo, we want to test two things:\n\n. Can we make this view function return the HTML we need?\n\n. Can we tell Django to use this view function\n  when we make a request for the root of the site (``/'')?\n\n\nLet's start with the first.\n\n\n\n=== Unit Testing a View\n\n(((\"unit tests\", \"in Django\", \"unit testing a view\", secondary-sortas=\"Django\")))\nOpen up _lists/tests.py_, and change our silly test to something like this:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch03l003)\n====\n[source,python]\n----\nfrom django.test import TestCase\nfrom django.http import HttpRequest  # <1>\nfrom lists.views import home_page\n\n\nclass HomePageTest(TestCase):\n    def test_home_page_returns_correct_html(self):\n        request = HttpRequest()  # <1>\n        response = home_page(request)  # <2>\n        html = response.content.decode(\"utf8\")  # <3>\n        self.assertIn(\"<title>To-Do lists</title>\", html)  # <4>\n        self.assertTrue(html.startswith(\"<html>\"))  # <5>\n        self.assertTrue(html.endswith(\"</html>\"))  # <5>\n----\n====\n\n[role=\"pagebreak-before\"]\nWhat's going on in this new test?\nWell, remember, a view function takes an HTTP request as input,\nand produces an HTTP response.\nSo, to test that:\n\n<1> We import the `HttpRequest` class\n    so that we can then create a request object within our test.\n    This is the kind of object that Django will create when a user's browser asks for a page.\n\n<2> We pass the `HttpRequest` object to our `home_page` view,\n    which gives us a response.\n    You won't be surprised to hear that the response is an instance\n    of a class called `HttpResponse`.\n\n<3> Then, we extract the `.content` of the response.\n    These are the raw bytes,\n    the ones and zeros that would be sent down the wire to the user's browser.\n    We call `.decode()` to convert them into the string of HTML that's being sent to the user.\n\n<4> Now we can make some assertions: we know we want an HTML `<title>` tag somewhere in there,\n    with the words \"To-Do lists\" in it--because\n    that's what we specified in our FT.\n\n<5> And we can do a vague sense-check that it's valid HTML by checking\n    that it starts with an `<html>` tag, which gets closed at the end.\n\n\nSo, what do you think will happen when we run the tests?\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nFound 1 test(s).\nSystem check identified no issues (0 silenced).\nE\n======================================================================\nERROR: lists.tests (unittest.loader._FailedTest.lists.tests)\n ---------------------------------------------------------------------\nImportError: Failed to import test module: lists.tests\nTraceback (most recent call last):\n[...]\n  File \"...goat-book/lists/tests.py\", line 3, in <module>\n    from lists.views import home_page\nImportError: cannot import name 'home_page' from 'lists.views'\n----\n\nIt's a very predictable and uninteresting error: we tried to import something\nwe haven't even written yet. But it's still good news--for the purposes of\nTDD, an exception that was predicted counts as an expected failure.\nBecause we have both a failing FT and a failing unit test, we have\nthe Testing Goat's full blessing to code away.\n\n\n==== At Last! We Actually Write Some Application Code!\n\nIt is exciting, isn't it?\nBe warned, TDD means that long periods of anticipation are only defused very gradually,\nand by tiny increments.\nEspecially as we're learning and only just starting out,\nwe only allow ourselves to change (or add) one line of code at a time—and each time,\nwe make just the minimal change required to address the current test failure.\n\nI'm being deliberately extreme here, but what's our current test failure?\nWe can't import `home_page` from `lists.views`?\nOK, let's fix that--and only that.\nIn _lists/views.py_:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch03l004)\n====\n[source,python]\n----\nfrom django.shortcuts import render\n\n# Create your views here.\nhome_page = None\n----\n====\n\n\"You must be joking!\" I can hear you say.\n\nI can hear you because it's what I used to say (with feeling)\nwhen my colleagues first demonstrated TDD to me.\nWell, bear with me,\nand we'll talk about whether or not this is all taking it too far in a little while.\nBut for now, let yourself follow along, even if it's with some exasperation,\nand see if our tests can help us write the correct code,\none tiny step at a time.\n\nLet's run the tests again:\n\n----\n[...]\n  File \"...goat-book/lists/tests.py\", line 9, in\ntest_home_page_returns_correct_html\n    response = home_page(request)\nTypeError: 'NoneType' object is not callable\n----\n\n\nWe still get an error, but it's moved on a bit.\nInstead of an import error,\nour tests are telling us that our `home_page` \"function\" is not callable.\nThat gives us a justification for\nchanging it from being `None` to being an actual function. At the very smallest\nlevel of detail, every single code change can be driven by the tests!\n\nBack in _lists/views.py_:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch03l005)\n====\n[source,python]\n----\nfrom django.shortcuts import render\n\n\ndef home_page():\n    pass\n----\n====\n\nAgain, we're making the smallest, simplest change we can possibly make,\nthat addresses precisely the current test failure.  Our tests wanted\nsomething callable, so we gave them the simplest possible callable thing:\na function that takes no arguments and returns nothing.\n\nLet's run the tests again and see what they think:\n\n----\n    response = home_page(request)\nTypeError: home_page() takes 0 positional arguments but 1 was given\n----\n\nOnce more, our error message has changed slightly,\nand is guiding us towards fixing the next thing that's wrong.\n\n\nThe Unit-Test/Code Cycle\n^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n(((\"unit tests\", \"in Django\", \"unit-test/code cycle\", secondary-sortas=\"Django\")))\n(((\"unit-test/code cycle\")))\n(((\"Test-Driven Development (TDD)\", \"concepts\", \"unit-test/code cycle\")))\nWe can start to settle into the TDD _unit-test/code cycle_ now:\n\n1. In the terminal, run the unit tests and see how they fail.\n2. In the editor, make a minimal code change to address the current test failure.\n\nAnd repeat!\n\nThe more nervous we are about getting our code right, the smaller and more\nminimal we make each code change--the idea is to be absolutely sure that each\nbit of code is justified by a test.\n\nThis may seem laborious—and at first, it will be.  But once you get into the\nswing of things, you'll find yourself coding quickly even if you take\nmicroscopic steps--this is how we write all of our production code at work.\n\nLet's see how fast we can get this cycle going:\n\n[role=\"simplelist\"]\n* Minimal code change:\n+\n[role=\"sourcecode\"]\n.lists/views.py (ch03l006)\n====\n[source,python]\n----\ndef home_page(request):\n    pass\n----\n====\n\n* Tests:\n+\n----\n    html = response.content.decode(\"utf8\")\n           ^^^^^^^^^^^^^^^^\nAttributeError: 'NoneType' object has no attribute 'content'\n\n----\n\n[role=\"pagebreak-before simplelist\"]\n* Code--we use `django.http.HttpResponse`, as predicted:\n+\n[role=\"sourcecode\"]\n.lists/views.py (ch03l007)\n====\n[source,python]\n----\nfrom django.http import HttpResponse\n\n\ndef home_page(request):\n    return HttpResponse()\n----\n====\n\n* Tests again:\n+\n----\nAssertionError: '<title>To-Do lists</title>' not found in ''\n----\n\n* Code again:\n+\n[role=\"sourcecode\"]\n.lists/views.py (ch03l008)\n====\n[source,python]\n----\ndef home_page(request):\n    return HttpResponse(\"<title>To-Do lists</title>\")\n----\n====\n\n\n* Tests yet again:\n+\n----\n    self.assertTrue(html.startswith(\"<html>\"))\nAssertionError: False is not true\n----\n\n\n* Code yet again:\n+\n[role=\"sourcecode\"]\n.lists/views.py (ch03l009)\n====\n[source,python]\n----\ndef home_page(request):\n    return HttpResponse(\"<html><title>To-Do lists</title>\")\n----\n====\n\n\n* Tests--almost there?\n+\n----\n    self.assertTrue(html.endswith(\"</html>\"))\nAssertionError: False is not true\n----\n\n* Come on, one last effort:\n+\n[role=\"sourcecode\"]\n.lists/views.py (ch03l010)\n====\n[source,python]\n----\ndef home_page(request):\n    return HttpResponse(\"<html><title>To-Do lists</title></html>\")\n----\n====\n\n[role=\"pagebreak-before simplelist\"]\n* Surely?\n+\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nCreating test database for alias 'default'...\nFound 1 test(s).\nSystem check identified no issues (0 silenced).\n.\n ---------------------------------------------------------------------\nRan 1 test in 0.001s\n\nOK\nDestroying test database for alias 'default'...\n----\n\nHooray! Our first ever unit test pass!  That's so momentous that I think it's\nworthy of a commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*  # should show changes to tests.py, and views.py\n$ *git commit -am \"First unit test and view function\"*\n----\n\n\nThat was the last variation on `git commit` I'll show,\nthe `a` and `m` flags together,\nwhich adds all changes to tracked files\nand uses the commit message from the command line.footnote:[\nI'm quite casual about my commit messages in this book,\nbut in professional organisations or open source projects,\npeople often want to be a bit more formal.\nCheck out https://cbea.ms/git-commit and\nhttps://www.conventionalcommits.org.]\n\n\nWARNING: `git commit -am` is the quickest formulation, but also gives you the\n    least feedback about what's being committed, so make sure you've done a\n    `git status` and a `git diff` beforehand, and are clear on what changes are\n    about to go in.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Our Functional Tests Tell Us We're Not Quite Done Yet\n\nWe've got our unit test passing,\nso let's go back to running our FTs to see if we've made progress.\nDon't forget to spin up the dev server again, if it's not still running.\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nF\n======================================================================\nFAIL: test_can_start_a_todo_list\n(__main__.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 18, in\ntest_can_start_a_todo_list\n    self.assertIn(\"To-Do\", self.browser.title)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'To-Do' not found in 'The install worked successfully!\nCongratulations!'\n\n ---------------------------------------------------------------------\nRan 1 test in 1.609s\n\nFAILED (failures=1)\n----\n\nLooks like something isn't quite right.  This is the reason we have functional\ntests!\n\nDo you remember at the beginning of the chapter, we said we needed to do two things:\nfirstly, create a view function to produce responses for requests,\nand secondly, tell the server which functions should respond to which URLs?\nThanks to our FT, we have been reminded that we still need to do the second thing.\n\n(((\"Django framework\", \"Test Client\", id=\"DJFtestclient04\")))\n(((\"Test Client (Django)\", id=\"testclient04\")))\nHow can we write a test for URL resolution?\nAt the moment, we just test the view function directly by importing it and calling it.\nBut we want to test more layers of the Django stack.\nDjango, like most web frameworks, supplies a tool for doing just that, called the\nhttps://docs.djangoproject.com/en/5.2/topics/testing/tools/#the-test-client[Django test client].\n\n[role=\"pagebreak-before\"]\nLet's see how to use it by adding a second, alternative test to our unit tests:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch03l011)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_home_page_returns_correct_html(self):  <1>\n        request = HttpRequest()\n        response = home_page(request)\n        html = response.content.decode(\"utf8\")\n        self.assertIn(\"<title>To-Do lists</title>\", html)\n        self.assertTrue(html.startswith(\"<html>\"))\n        self.assertTrue(html.endswith(\"</html>\"))\n\n    def test_home_page_returns_correct_html_2(self):\n        response = self.client.get(\"/\")  # <2>\n        self.assertContains(response, \"<title>To-Do lists</title>\")  # <3>\n----\n====\n\n<1> This is our existing test.\n\n<2> In our new test, we access the test client via `self.client`,\n    which is available on any test that uses `django.test.TestCase`.\n    It provides methods like `.get()`, which simulates a browser making HTTP requests,\n    and takes a URL as its first parameter.\n    We use this instead of manually creating a request object\n    and calling the view function directly.\n\n<3> Django also provides some assertion helpers like `assertContains`,\n    which save us from having to manually extract and decode response content,\n    and have some other nice properties besides, as we'll see.\n\n[role=\"pagebreak-before\"]\nLet's see how that works:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nFound 2 test(s).\nCreating test database for alias 'default'...\nSystem check identified no issues (0 silenced).\n.F\n======================================================================\nFAIL: test_home_page_returns_correct_html_2\n(lists.tests.HomePageTest.test_home_page_returns_correct_html_2)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/lists/tests.py\", line 17, in\ntest_home_page_returns_correct_html_2\n    self.assertContains(response, \"<title>To-Do lists</title>\")\n[...]\nAssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404\n(expected 200)\n\n ---------------------------------------------------------------------\nRan 2 tests in 0.004s\n\nFAILED (failures=1)\nDestroying test database for alias 'default'...\n----\n\nHmm, something about 404s?  Let's dig into it.\n\n\n[[reading_tracebacks]]\n=== Reading Tracebacks\n\n(((\"tracebacks\")))\nLet's spend a moment talking about how to read tracebacks, as it's something\nwe have to do a lot in TDD. You soon learn to scan through them and pick up\nrelevant clues:\n\n----\n======================================================================\nFAIL: test_home_page_returns_correct_html_2  <2>\n(lists.tests.HomePageTest.test_home_page_returns_correct_html_2)  <2>\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/lists/tests.py\", line 17, in\ntest_home_page_returns_correct_html_2\n    self.assertContains(response, \"<title>To-Do lists</title>\")  <3>\n    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  <4>\nAssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404  <1>\n(expected 200)\n\n ---------------------------------------------------------------------\n[...]\n----\n\n[role=\"pagebreak-before\"]\n<1> The first place you look is usually _the error itself_. Sometimes that's\n    all you need to see, and it will let you identify the problem immediately.\n    But sometimes, like in this case, it's not quite self-evident.\n\n<2> The next thing to double-check is: _which test is failing?_ Is it\n    definitely the one we expected--that is, the one we just wrote?  In this case,\n    the answer is yes.\n\n<3> Then we look for the place in _our test code_ that kicked off the failure.\n    We work our way down from the top of the traceback, looking for the\n    filename of the tests file to check which test function, and what line of\n    code, the failure is coming from.\n    In this case, it's the line where we call the `assertContains` method.\n\n<4> In Python 3.11 and later, you can also look out for the string of carets,\n    which try to tell you exactly where the exception came from.\n    This is more useful for unexpected exceptions than for assertion failures\n    like we have now.\n\nThere is ordinarily a fifth step, where we look further down for any\nof _our own application code_ that was involved with the problem.  In this\ncase, it's all Django code, but we'll see plenty of examples of this fifth step\nlater in the book.\n\nPulling it all together, we interpret the traceback as telling us that:\n\n* When we tried to do our assertion on the content of the response.\n* Django's test helpers failed, saying that they could not do that.\n* Because the response is an HTML 404 Not Found error, instead of a normal 200 OK response.\n\nIn other words, Django isn't yet configured to respond to requests for the\nroot URL (\"/\") of our site.  Let's make that happen now.\n\n[role=\"pagebreak-before less_space\"]\n=== urls.py\n\n\n(((\"URL mappings\")))\nDjango uses a file called _urls.py_ to map URLs to view functions. This mapping is also called _routing_.(((\"routing\", seealso=\"URL mappings\")))\nThere's a main _urls.py_ for the whole site in the _superlists_ folder.\nLet's go take a look:\n\n[role=\"sourcecode currentcontents\"]\n.superlists/urls.py\n====\n[source,python]\n----\n\"\"\"\nURL configuration for superlists project.\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https://docs.djangoproject.com/en/5.2/topics/http/urls/\nExamples:\nFunction views\n    1. Add an import:  from my_app import views\n    2. Add a URL to urlpatterns:  path('', views.home, name='home')\nClass-based views\n    1. Add an import:  from other_app.views import Home\n    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')\nIncluding another URLconf\n    1. Import the include() function: from django.urls import include, path\n    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))\n\"\"\"\n\nfrom django.contrib import admin\nfrom django.urls import path\n\nurlpatterns = [\n    path(\"admin/\", admin.site.urls),\n]\n----\n====\n\nAs usual, lots of helpful comments and default suggestions from Django.\nIn fact, that very first example is pretty much exactly what we want!\nLet's use that, with some minor changes:\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch03l012)\n====\n[source,python]\n----\nfrom django.urls import path  # <1>\nfrom lists.views import home_page  # <2>\n\nurlpatterns = [\n    path(\"\", home_page, name=\"home\"),  # <3>\n]\n----\n====\n\n<1> No need to import `admin` from `django.contrib`. Django's admin site is amazing,\n    but it's a topic for another book.\n\n<2> But we will import our home page view function.\n\n<3> And we wire it up here, as a `path()` entry in the `urlpatterns` global.\n    Django strips the leading slash from all URLs,\n    so `\"/url/path/to\"` becomes `\"url/path/to\"`\n    and the base URL is just the empty string, `\"\"`.\n    So this config says, the \"base URL should point to our home page view\".\n\nNow we can run our unit tests again, with *`python manage.py test`*:\n\n----\n[...]\n..\n ---------------------------------------------------------------------\nRan 2 tests in 0.003s\n\nOK\n----\n\nHooray!\n\n\nTime for a little tidy-up.  We don't need two separate tests, so\nlet's move everything out of our low-level test that calls the view\nfunction directly, into the test that uses the Django test client:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch03l013)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_home_page_returns_correct_html(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, \"<title>To-Do lists</title>\")\n        self.assertContains(response, \"<html>\")\n        self.assertContains(response, \"</html>\")\n----\n====\n\n\n.Why Didn't We Just Use the Django Test Client All Along?\n*******************************************************************************\nYou may be asking yourself,\n\"Why didn't we just use the Django test client from the very beginning?\"\nIn real life, that's what I would do.\nBut I wanted to show you the \"manual\" way of doing it first, for a couple of reasons.\nFirstly, because it enabled me to introduce concepts one by one,\nand keep the learning curve as shallow as possible.\nSecondly, because you may not always be using Django to build your apps,\nand testing tools may not always be available--but\ncalling functions directly and examining their responses is always possible!\n\nThe Django test client does also have disadvantages;\nlater in the book (in <<chapter_27_hot_lava>>)\nwe'll discuss the difference\nbetween fully isolated unit tests\nand the types of test that the test client pushes us towards\n(people often say these are technically \"integration tests\").\nBut for now, it's very much the pragmatic choice.\n(((\"\", startref=\"testclient04\")))\n(((\"\", startref=\"DJFtestclient04\")))\n*******************************************************************************\n\n[role=\"pagebreak-before\"]\nBut now the moment of truth: will our functional tests pass?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\n[...]\n======================================================================\nFAIL: test_can_start_a_todo_list\n(__main__.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 21, in\ntest_can_start_a_todo_list\n    self.fail(\"Finish the test!\")\nAssertionError: Finish the test!\n----\n\nFailed? What? Oh, it's just our little reminder? Yes? Yes! We have a web page!\n\n\nAhem.  Well, _I_ thought it was a thrilling end to the chapter. You may still\nbe a little baffled, perhaps keen to hear a justification for all these tests\n(and don't worry; all that will come), but I hope you felt just a tinge of\nexcitement near the end there.\n\n\nJust a little commit to calm down, and reflect on what we've covered:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*  # should show our modified test in tests.py, and the new config in urls.py\n$ *git commit -am \"url config, map / to home_page view\"*\n----\n\n\n(((\"\", startref=\"DJFunit03\")))\n(((\"\", startref=\"UTdjango03\")))\nThat was quite a chapter! Why not try typing `git log`, possibly using the\n`--oneline` flag, for a reminder of what we got up to:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git log --oneline*\na6e6cc9 url config, map / to home_page view\n450c0f3 First unit test and view function\nea2b037 Add app for lists, with deliberately failing unit test\n[...]\n----\n\nNot bad--we covered the following:\n\n* Starting a Django app\n* The Django unit test runner\n* The difference between FTs and unit tests\n* Django view functions, and request and response objects\n* Django URL resolving and _urls.py_\n* The Django test client\n* Returning basic HTML from a view\n\n[role=\"pagebreak-before less_space\"]\n.Useful Commands and Concepts\n*******************************************************************************\nRunning the Django dev server::\n    *`python manage.py runserver`*\n    (((\"Django framework\", \"commands and concepts\", \"python manage.py runserver\")))\n\nRunning the functional tests::\n    *`python functional_tests.py`*\n    (((\"Django framework\", \"commands and concepts\", \"python functional_tests.py\")))\n\nRunning the unit tests::\n    *`python manage.py test`*\n    (((\"Django framework\", \"commands and concepts\", \"python manage.py test\")))\n\nThe unit-test/code cycle::\n    1. Run the unit tests in the terminal.\n    2. Make a minimal code change in the editor.\n    3. Repeat!\n    (((\"Django framework\", \"commands and concepts\", \"unit-test/code cycle\")))\n    (((\"unit-test/code cycle\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_04_philosophy_and_refactoring.asciidoc",
    "content": "[[chapter_04_philosophy_and_refactoring]]\n== What Are We Doing with All These Tests? (And, Refactoring)\n\n(((\"Test-Driven Development (TDD)\", \"need for\", id=\"TDDneed04\")))\nNow that we've seen the basics of TDD in action,\nit's time to pause and talk about why we're doing it.\n\nI'm imagining several of you, dear readers, have been holding back\nsome seething frustration--perhaps some of you have done a bit of unit testing before,\nand perhaps some of you are just in a hurry.\nYou've been biting back questions like:\n\n* Aren't all these tests a bit excessive?\n\n* Surely some of them are redundant?\n  There's duplication between the functional tests and the unit tests.\n\n* Those unit tests seemed way too trivial--testing\n  a one-line function that returns a constant!\n  Isn't that just a waste of time?\n  Shouldn't we save our tests for more complex things?\n\n* What about all those tiny changes during the unit-test/code cycle?\n  Couldn't we just skip to the end? I mean, `home_page = None`!?  Really?\n\n* You're not telling me you _actually_ code like this in real life?\n\nAh, young grasshopper. I too was once full of questions like these.\nBut only because they're perfectly good questions.\nIn fact, I still ask myself questions like these—all the time.\nDoes all this stuff really have value? Is this a bit of a cargo cult?\n\n\n\n=== Programming Is Like Pulling a Bucket of Water [.keep-together]#Up from a Well#\n\n(((\"Test-Driven Development (TDD)\", \"philosophy of\", \"bucket of water analogy\")))\nUltimately, programming is hard.  Often, we are smart, so we succeed.\nTDD is there to help us out when we're not so smart.\nKent Beck (who basically invented TDD) uses the metaphor\nof lifting a bucket of water out of a well with a rope:\nwhen the well isn't too deep, and the bucket isn't very full, it's easy.\nAnd even lifting a full bucket is pretty easy at first.\nBut after a while, you're going to get tired.\nTDD is like having a ratchet that lets you save your progress,\nso you can take a break, and make sure you never slip backwards.\n\nThat way, you don't have to be smart _all_ the time (see <<figure4-1>>).\n\n\n[[figure4-1]]\n.Test ALL the things (adapted from https://oreil.ly/n_8R_[Allie Brosh, Hyperbole and a Half])\nimage::images/tdd3_0401.png[\"Test ALL the things\",float=\"right\"]\n\n\nOK, perhaps _in general_, you're prepared to concede that TDD is a good\nidea, but maybe you still think I'm overdoing it?  Testing the tiniest thing,\nand taking ridiculously many small steps?\n\nTDD is a _discipline_, and that means it's not something that comes naturally.\nBecause many of the payoffs aren't immediate but only come in the longer term,\nyou have to force yourself to do it in the moment.\nThat's what the image of the Testing Goat is supposed to represent--you\nneed to be a bit bloody-minded about it.\n\n[role=\"pagebreak-before less_space\"]\n[[trivial_tests_trivial_functions]]\n.On the Merits of Trivial Tests for Trivial Functions\n**********************************************************************\nIn the short term, it may feel a bit silly to write tests for simple\nfunctions and [.keep-together]#constants#.\n\nIt's perfectly possible to imagine still doing ``mostly'' TDD,\nbut following more relaxed rules where you don't unit test _absolutely_ everything.\nBut in this book my aim is to demonstrate full, rigorous TDD.\nLike a kata in a martial art,\nthe idea is to learn the motions in a controlled context,\nwhen there is no adversity,\nso that the techniques are part of your muscle memory.\nIt seems trivial now, because we've started with a very simple example.\nThe problem comes when your application gets complex--that's when you really need your tests.\nAnd the danger is that complexity tends to sneak up on you, gradually.\nYou may not notice it happening, but soon you're a boiled frog.\n\nThere are two other things to say in favour of tiny, simple tests for simple functions.\n\nFirstly, if they're really trivial tests,\nthen they won't take you that long to write.\nSo stop moaning and just write them already.\n\nSecondly, it's always good to have a placeholder.\nHaving a test _there_ for a simple function\nmeans it's that much less of a psychological barrier to overcome\nwhen the simple function gets a tiny bit more complex--perhaps\nit grows an `if`.\nThen a few weeks later, it grows a `for` loop.\nBefore you know it, it's a recursive metaclass-based polymorphic tree parser factory.\nBut because it's had tests from the very beginning,\nadding a new test each time has felt quite natural,\nand it's well tested.\nThe alternative involves trying to decide when a function becomes ``complicated enough'',\nwhich is highly subjective.\nAnd worse, because there's no placeholder,\nit feels like that much more effort to start,\nso you're tempted each time to put it off...and pretty soon--frog soup!\n\nInstead of trying to figure out some hand-wavy subjective rules\nfor when you should write tests,\nand when you can get away with not bothering,\nI suggest following the discipline for now--and as with any discipline,\nyou have to take the time to learn the rules before you can break them.\n\n**********************************************************************\n\nNow, let us return to our muttons.\n(((\"\", startref=\"TDDneed04\")))\n\n[role=\"pagebreak-before less_space\"]\n=== Using Selenium to Test User Interactions\n\n(((\"Selenium\", \"testing user interactions with\", id=\"Suser04\")))\n(((\"user interactions\", \"testing with Selenium\", id=\"UIselenium04\")))\nWhere were we at the end of the last chapter?\nLet's rerun the test and find out:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\nF\n======================================================================\nFAIL: test_can_start_a_todo_list\n(__main__.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 21, in\ntest_can_start_a_todo_list\n    self.fail(\"Finish the test!\")\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\nAssertionError: Finish the test!\n\n ---------------------------------------------------------------------\nRan 1 test in 1.609s\n\nFAILED (failures=1)\n----\n\n\nDid you try it, and get an error saying \"Problem loading page\" or\n\"Unable to connect\"?  So did I. It's because we forgot to spin up the dev\nserver first using `manage.py runserver`.  Do that, and you'll get the failure\nmessage we're after.\n\nNOTE: One of the great things about TDD is that you never have to worry\n    about forgetting what to do next--just rerun your tests\n    and they will tell you what you need to work on.\n\n[role=\"pagebreak-before\"]\n``Finish the test'', it says, so let's do just that!  Open up\n'functional_tests.py' and we'll extend our FT:\n\n[role=\"sourcecode small-code\"]\n.functional_tests.py (ch04l001)\n====\n[source,python]\n----\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\nimport time\nimport unittest\n\n\nclass NewVisitorTest(unittest.TestCase):\n    def setUp(self):\n        self.browser = webdriver.Firefox()\n\n    def tearDown(self):\n        self.browser.quit()\n\n    def test_can_start_a_todo_list(self):\n        # Edith has heard about a cool new online to-do app.\n        # She goes to check out its homepage\n        self.browser.get(\"http://localhost:8000\")\n\n        # She notices the page title and header mention to-do lists\n        self.assertIn(\"To-Do\", self.browser.title)\n        header_text = self.browser.find_element(By.TAG_NAME, \"h1\").text  # <1>\n        self.assertIn(\"To-Do\", header_text)\n\n        # She is invited to enter a to-do item straight away\n        inputbox = self.browser.find_element(By.ID, \"id_new_item\")  # <1>\n        self.assertEqual(inputbox.get_attribute(\"placeholder\"), \"Enter a to-do item\")\n\n        # She types \"Buy peacock feathers\" into a text box\n        # (Edith's hobby is tying fly-fishing lures)\n        inputbox.send_keys(\"Buy peacock feathers\")  # <2>\n\n        # When she hits enter, the page updates, and now the page lists\n        # \"1: Buy peacock feathers\" as an item in a to-do list table\n        inputbox.send_keys(Keys.ENTER)  # <3>\n        time.sleep(1)  # <4>\n\n        table = self.browser.find_element(By.ID, \"id_list_table\")\n        rows = table.find_elements(By.TAG_NAME, \"tr\")  # <1>\n        self.assertTrue(any(row.text == \"1: Buy peacock feathers\" for row in rows))\n\n        # There is still a text box inviting her to add another item.\n        # She enters \"Use peacock feathers to make a fly\"\n        # (Edith is very methodical)\n        self.fail(\"Finish the test!\")\n\n        # The page updates again, and now shows both items on her list\n        [...]\n----\n====\n\n//IDEA: stop using id_new_item, just use name=\n\n[role=\"pagebreak-before\"]\n<1> We're using the two methods that Selenium provides to examine web pages:\n    `find_element` and `find_elements`\n    (notice the extra `s`,\n    which means it will return several elements rather than just one).\n    Each one is parameterised with a `By.SOMETHING`,\n    which lets us search using different HTML properties and attributes.\n\n<2> We also use `send_keys`,\n    which is Selenium's way of typing into input elements.\n\n<3> The `Keys` class (don't forget to import it)\n    lets us send special keys like Enter.footnote:[\n    You could also just use the string +\"\\n\"+, but `Keys`\n    also lets you send special keys like Ctrl, so I thought I'd show it.]\n\n<4> When we hit Enter, the page will refresh.\n    The `time.sleep` is there to make sure the browser has finished loading\n    before we make any assertions about the new page.(((\"explicit and implicit waits\")))\n    This is called an \"explicit wait\"\n    (a very simple one; we'll improve it in <<chapter_06_explicit_waits_1>>).\n\nTIP: Watch out for the difference between the Selenium `find_element()`\n    and `find_elements()` functions.\n    One returns an element and raises an exception if it can't find it,\n    whereas the other returns a list, which may be empty.\n\n\nAlso, just look at that `any()` function.\nIt's a little-known Python built-in.\nI don't even need to explain it, do I?\nPython is such a joy.footnote:[\nPython _is_ most definitely a joy,\nbut if you think I'm being a bit smug here,\nI don't blame you!\nActually, I wish I'd picked up on this feeling of self-satisfaction\nand seen it as a warning sign that I was being a little _too_ clever.\nIn the next chapter, you'll see I get my comeuppance.]\n\n\nNOTE: If you're one of my readers who doesn't know Python,\n    what's happening _inside_ the `any()` may need some explaining.\n    The basic syntax is that of a _list comprehension_,\n    and if you haven't learned about them, you should do so immediately!\n    https://oreil.ly/6bX0h[Trey Hunner's explanation is excellent].\n    In point of fact, because we're omitting the square brackets,\n    we're actually using a _generator expression_ rather than a list comprehension.\n    It's probably less important to understand the difference between those two,\n    but if you're curious, Guido van Rossum, the inventor of Python, has written https://oreil.ly/Om6vK[a blog post explaining the difference].(((\"generator expressions\")))(((\"list comprehensions\")))\n\n[role=\"pagebreak-before\"]\nLet's see how it gets on:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python functional_tests.py*\n[...]\n  File \"...goat-book/functional_tests.py\", line 22, in\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: h1; For documentation on this error, please visit: [...]\n----\n\nDecoding that,\nthe test is saying it can't find an `<h1>` element on the page.\nLet's see what we can do to add that to the HTML of our home page.\n\n(((\"\", startref=\"Suser04\")))\n(((\"\", startref=\"UIselenium04\")))\nBig changes to a functional test are usually a good thing to commit on their own.\nI failed to do so when I was first working out the code for this chapter,\nand I regretted it later when I changed my mind\nand had the change mixed up with a bunch of others.\nThe more atomic your commits, the better:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*  # should show changes to functional_tests.py\n$ *git commit -am \"Functional test now checks we can input a to-do item\"*\n----\n\n\n\n=== The \"Don't Test Constants\" Rule, and Templates [.keep-together]#to the Rescue#\n\n\n(((\"“Don’t Test Constants” rule\", primary-sortas=\"Don’t Test Constants rule\")))\n(((\"unit tests\", \"“Don’t Test Constants” rule\", secondary-sortas=\"Don’t Test Constants rule\")))(((\"constants\", \"“Don’t Test Constants” rule\", secondary-sortas=\"Don&#x27;t\")))\nLet's take a look at our unit tests, _lists/tests.py_.\nCurrently we're looking for specific HTML strings,\nbut that's not a particularly efficient way of testing HTML.\nIn general, one of the rules of unit testing is \"don't test constants\",\nand testing HTML as text is a lot like testing a constant.\n\nIn other words, if you have some code that says:\n\n\n[role=\"skipme\"]\n[source,python]\n----\nwibble = 3\n----\n\nThere's not much point in a test that says:\n\n[role=\"skipme\"]\n[source,python]\n----\nfrom myprogram import wibble\nassert wibble == 3\n----\n\nUnit tests are really about testing logic, flow control, and configuration.\nMaking assertions about exactly what sequence of characters we have in our HTML strings isn't doing that.\n\nIt's not _quite_ that simple, because HTML is code after all,\nand we do want something to check that we've written code that works—but that's our FT's job, not the unit test's.\n\nSo maybe \"don't test constants\" isn't the online guideline at play here,\nbut in any case, mangling raw strings in Python\nreally isn't a great way of dealing with HTML.\nThere's a much better solution, which is to use templates.\nQuite apart from anything else,\nif we can keep HTML to one side in a file whose name ends in '.html',\nwe'll get better syntax highlighting!\n\nThere are lots of Python templating frameworks out there,\nand Django has its own which works very well.\nLet's use that.\n\n\n==== Refactoring to Use a Template\n\n(((\"unit tests\", \"refactoring in\", id=\"UTrefactor04\")))\n(((\"refactoring\", id=\"refactor04\")))(((\"templates\", \"refactoring unit tests to use\", id=\"ix_tmplref\")))\nWhat we want to do now is make our view function return exactly the same HTML,\nbut just using a different process.\nThat's a refactor--when we try to improve the code\n_without changing its functionality_.\n\nThat last bit is really important.\nIf you try to add new functionality at the same time as refactoring,\nyou're much more likely to run into trouble.\nRefactoring is actually a whole discipline in itself,\nand it even has a reference book:\nMartin Fowler's http://refactoring.com[_Refactoring_].\n\nThe first rule is that you can't refactor without tests.\nThankfully, we're doing TDD, so we're way ahead of the game.\nLet's check that our tests pass;\nthey will be what makes sure that our refactoring is behaviour-preserving:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py test*\n[...]\nOK\n----\n\nGreat! We'll start by taking our HTML string and putting it into its own file.\nCreate a directory called _lists/templates_ to keep templates in,\nand then open a file at _lists/templates/home.html_,\nto which we'll transfer our HTML:footnote:[\nSome people like to use another subfolder named after the app\n(i.e., _lists/templates/lists_) and then refer to the template as _lists/home.html_.\nThis is called \"template namespacing\".\nI figured it was overcomplicated for this small project, but it may be worth it on larger projects.\nThere's more in the\nhttps://docs.djangoproject.com/en/5.2/intro/tutorial03/#write-views-that-actually-do-something[Django tutorial].]\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch04l002)\n====\n[source,html]\n----\n<html>\n  <title>To-Do lists</title>\n</html>\n----\n====\n\n\nMmm, syntax-highlighted...much nicer! Now to change our view function:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch04l003)\n====\n[source,python]\n----\nfrom django.shortcuts import render\n\n\ndef home_page(request):\n    return render(request, \"home.html\")\n----\n====\n\nInstead of building our own `HttpResponse`, we now use the Django `render()`\nfunction.  It takes the request as its first parameter (for reasons we'll go\ninto later) and the name of the template to render.  Django will automatically\nsearch folders called _templates_ inside any of your apps' directories.  Then\nit builds an `HttpResponse` for you, based on the content of the template.\n\n\nNOTE: Templates are a very powerful feature of Django's,\n    and their main strength consists of substituting Python variables into HTML text.\n    We're not using this feature yet, but we will in future chapters.\n    That's why we use `render()` rather than, say,\n    manually reading the file from disk with the built-in `open()`.\n\n\nLet's see if it works:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros,callouts\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\n======================================================================\nERROR: test_home_page_returns_correct_html\n(lists.tests.HomePageTest.test_home_page_returns_correct_html)  <2>\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/lists/tests.py\", line 7, in test_home_page_returns_correct_html\n    response = self.client.get(\"/\")  <3>\n               ^^^^^^^^^^^^^^^^^^^^\n[...]\n  File \"...goat-book/lists/views.py\", line 4, in home_page\n    return render(request, \"home.html\")  <4>\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \".../django/shortcuts.py\", line 24, in render\n    content = loader.render_to_string(template_name, context, request, using=using)\n              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \".../django/template/loader.py\", line 61, in render_to_string\n    template = get_template(template_name, using=using)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \".../django/template/loader.py\", line 19, in get_template\n    raise TemplateDoesNotExist(template_name, chain=chain)\ndjango.template.exceptions.TemplateDoesNotExist: home.html  <1>\n\n----------------------------------------------------------------------\nRan 1 test in 0.074s\n----\n\nAnother chance to analyse a traceback:\n\n<1> We start with the error: it can't find the template.\n\n<2> Then we double-check what test is failing: sure enough, it's our test\n    of the view HTML.\n\n<3> Then we find the line in our tests that caused the failure: it's when\n    we request the root URL (\"/\").\n\n<4> Finally, we look for the part of our own application code that caused the\n    failure: it's when we try to call `render`.\n\nSo why can't Django find the template?\nIt's right where it's supposed to be, in the _lists/templates_ folder.\n\nThe thing is that we haven't yet _officially_ registered our lists app with Django.\nUnfortunately, just running the `startapp` command\nand having what is obviously an app in your project folder\nisn't quite enough.\nYou have to tell Django that you _really_ mean it,\nand add it to 'settings.py' as well—belt and braces.\nOpen it up and look for a variable called `INSTALLED_APPS`,\nto which we'll add `lists`:\n\n\n[role=\"sourcecode\"]\n.superlists/settings.py (ch04l004)\n====\n[source,python]\n----\n# Application definition\n\nINSTALLED_APPS = [\n    \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"lists\",\n]\n----\n====\n\nYou can see there's lots of apps already in there by default.\nWe just need to add ours to the bottom of the list.\nDon't forget the trailing comma--it may not be required,\nbut one day you'll be really annoyed when you forget it\nand Python concatenates two strings on different lines...\n\nNow we can try running the tests again:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python manage.py test*\n[...]\nOK\n----\n\nAnd we can double-check with the FTs:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python functional_tests.py*\n[...]\n  File \"...goat-book/functional_tests.py\", line 22, in\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: h1; For documentation on this error, please visit: [...]\n----\n\n\nGood, they still get to the same place they did before.\nOur refactor of the code is now complete,\nand the tests mean we're happy that behaviour is preserved.\nNow we can change the tests so that they're no longer testing constants;\ninstead, they should just check that we're rendering the right template.(((\"templates\", \"refactoring unit tests to use\", startref=\"ix_tmplref\")))\n(((\"\", startref=\"refactor04\")))\n(((\"\", startref=\"UTrefactor04\")))\n\n\n\n=== Revisiting Our Unit Tests\n\nOur unit tests are currently essentially checking HTML by hand—certainly that's very close to \"testing constants\".\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests.py\n====\n[source,python]\n----\ndef test_home_page_returns_correct_html(self):\n    response = self.client.get(\"/\")\n    self.assertContains(response, \"<title>To-Do lists</title>\")  # <1>\n    self.assertContains(response, \"<html>\")\n    self.assertContains(response, \"</html>\")\n----\n====\n\nWe don't want to be duplicating the full content of our HTML\ntemplate in our tests, or even last sections of it.\nWhat could we do instead?\n\nRather than testing the full template,\nwe could just check that we're using the _right_ template.\nThe Django test client has a method, `assertTemplateUsed`,\nwhich will let us do just that.(((\"templates\", \"checking that right template is used\")))\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch04l005)\n====\n[source,python]\n----\ndef test_home_page_returns_correct_html(self):\n    response = self.client.get(\"/\")\n    self.assertContains(response, \"<title>To-Do lists</title>\")  # <1>\n    self.assertContains(response, \"<html>\")\n    self.assertContains(response, \"</html>\")\n    self.assertTemplateUsed(response, \"home.html\")  # <2>\n----\n====\n\n<1> We'll leave the old tests there for now,\n    just to make sure everything is working the way we think it is.\n\n<2> `.assertTemplateUsed` lets us check what template was used to render a response.\n    (NB: It will only work for responses that were retrieved by the test client.)\n\nAnd that test will still pass:\n\n----\nRan 1 tests in 0.016s\n\nOK\n----\n\nJust because I'm always suspicious of a test I haven't seen fail, let's\ndeliberately break it:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch04l006)\n====\n[source,python]\n----\nself.assertTemplateUsed(response, \"wrong.html\")\n----\n====\n\n[role=\"pagebreak-before\"]\nThat way, we'll also learn what its error messages look like:\n\n----\nAssertionError: False is not true : Template 'wrong.html' was not a template\nused to render the response. Actual template(s) used: home.html\n----\n\nThat's very helpful!\n\nLet's change the assert back to the right thing.\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch04l007)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\n\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        response = self.client.get(\"/\")\n        self.assertTemplateUsed(response, \"home.html\")\n----\n====\n\n\nNow, instead of testing constants we're testing at a higher level of abstraction.\nGreat!\n\n\n==== Test Behaviour, Not Implementation\n\nAs so often in the world of programming though,\nthings are not black and white.(((\"behaviour\", \"testing behaviour, not implementation\")))\n\nYes, on the plus side, our tests no longer care about the specific content of our HTML\nso they are no longer brittle with respect to minor changes of the copy in our template.\n\nBut on the other hand, they depend on some Django implementation details,\nso they _are_ brittle with respect to changing the template rendering library,\nor even just renaming templates.\n\nIn a way, testing for the template name\n(and implicitly, even checking that we used a template at all)\nis a lot like testing implementation.\nSo what is the _behaviour_ that we want?\n\nYes, in a sense, the \"behaviour\" we want from the view is \"render the template\".\nBut from the point of view of the user,\nit's \"show me the home page\".\n\nWe're also vulnerable to accidentally breaking the template.\nLet's try it now,\nby just deleting all the contents of the template file:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *mv lists/templates/home.html lists/templates/home.html.bak*\n$ *touch lists/templates/home.html*\n$ *python manage.py test*\n[...]\nOK\n----\n\n[role=\"pagebreak-before\"]\nYes, our FTs will pick up on this, so ultimately we're OK:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\n[...]\n    self.assertIn(\"To-Do\", self.browser.title)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'To-Do' not found in ''\n----\n\nBut it would be nice to have our unit tests pick up on this too:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *mv lists/templates/home.html.bak lists/templates/home.html*\n----\n\n\nDeciding exactly what to test with FTs and what to test with unit tests\nis a fine line, and the objective is not to double-test everything.\nBut in general, the more we can test with unit tests the better.\nThey run faster, and they give more specific feedback.(((\"smoke tests\")))\n\nSo, let's bring back a minimal \"smoke test\"footnote:[\nA smoke test is a minimal test that can quickly tell you if something is wrong,\nwithout exhaustively testing every aspect that you might care about.\nWikipedia has some fun speculation on the\nhttps://oreil.ly/1_isl[etymology].]\nto check that what we're rendering is actually the home page:\n\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch04l008)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        response = self.client.get(\"/\")\n        self.assertTemplateUsed(response, \"home.html\")  # <1>\n\n    def test_renders_homepage_content(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, \"To-Do\")  # <2>\n----\n====\n\n<1> We'll keep this first test, which asserts on whether we're rendering the\n    right \"constant\".\n\n<2> And this gives us a minimal smoke test that we have got the right\n    content in the template.\n\n[role=\"pagebreak-before\"]\nAs our home page template gains more functionality over the next couple of chapters,\nwe'll come back to talking about what to test here in the unit tests\nand what to leave to the FTs.\n\nTIP: Unit tests give you faster and more specific feedback than FTs.\n  Bear this in mind when deciding what to test where.\n\n\nWe'll visit the trade-offs between different types of tests\nat several points in the book, and particularly in <<chapter_27_hot_lava>>.\n\n\n=== On Refactoring\n\n(((\"unit tests\", \"refactoring in\")))\n(((\"refactoring\")))\nThat was an absolutely trivial example of refactoring.\nBut, as Kent Beck puts it in _Test-Driven Development: By Example_,\n\"Am I recommending that you actually work this way? No.\nI'm recommending that you be _able_ to work this way\".\n\nIn fact, as I was writing this my first instinct was to dive in\nand change the test first--make it\nuse the `assertTemplateUsed()` function straight away;\nagainst the expected render;\nand then go ahead and make the code change.\nBut notice how that actually would have left space\nfor me to break things:\nI could have defined the template as containing 'any' arbitrary string,\ninstead of the string with the right `<html>` and `<title>` tags.\n\nTIP: When refactoring, work on either the code or the tests,\n    but not both at once.\n\n// SEBASTIAN: I'd put in other words, perhaps as an additional paragraph.\n//     Change one thing at a time - either code or the tests.\n//     After introducing changes to one of them, run the tests.\n//     If they pass, carry on with changing the other. If they don't - fix the tests first.\n//     *It's virtually impossible (also for experienced software developer) to precisely*\n//     *pinpoint source of error if you ended up with failing tests after changing both*\n//     *the tests and code.*\n\nThere's always a tendency to skip ahead a couple of steps,\nto make a few tweaks to the behaviour while you're refactoring.\nBut pretty soon you've got changes to half a dozen different files,\nyou've totally lost track of where you are, and nothing works anymore.\nIf you don't want to end up like https://oreil.ly/F_Hqf[Refactoring Cat] (<<RefactoringCat>>),\nstick to small steps; keep refactoring and functionality changes entirely separate.\n\n[[RefactoringCat]]\n.Refactoring Cat--be sure to look up the full animated GIF (source: 4GIFs.com)\nimage::images/tdd3_0402.png[\"An adventurous cat, trying to refactor its way out of a slippery bathtub\"]\n\n\nNOTE: We'll come across Refactoring Cat again during this book,\n    as an example of what happens when we get carried away\n    and change too many things at once.\n    Think of it as the little cartoon demon counterpart to the Testing Goat,\n    popping up over your other shoulder and giving you bad advice.\n\nIt's a good idea to do a commit after any refactoring:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # see tests.py, views.py, settings.py, + new templates folder\n$ *git add .*  # will also add the untracked templates folder\n$ *git diff --staged* # review the changes we're about to commit\n$ *git commit -m \"Refactor homepage view to use a template\"*\n----\n\n[role=\"pagebreak-before less_space\"]\n=== A Little More of Our Front Page\n\nIn the meantime, our FT is still failing.(((\"functional  tests (FTs)\", \"passing test on home page\")))\nLet's now make an actual code change to get it passing.\nBecause our HTML is now in a template,\nwe can feel free to make changes to it,\nwithout needing to write any extra unit tests.\n\nNOTE: This is another distinction between FTs and unit tests;\n    because the FTs use a real web browser,\n    we use them as the primary tool for testing our UI,\n    and the HTML that implements it.\n\nSo, we wanted an `<h1>`:\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch04l009)\n====\n[source,html]\n----\n<html>\n  <head>\n    <title>To-Do lists</title>\n  </head>\n  <body>\n    <h1>Your To-Do list</h1>\n  </body>\n</html>\n----\n====\n\nLet's see if our FT likes it a little better:\n\n----\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_new_item\"]; For documentation on this error, [...]\n----\n\nOK, let's add an input with that ID:\n\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch04l010)\n====\n[source,html]\n----\n  [...]\n  <body>\n    <h1>Your To-Do list</h1>\n    <input id=\"id_new_item\" />\n  </body>\n</html>\n----\n====\n\nAnd now what does the FT say?\n\n----\nAssertionError: '' != 'Enter a to-do item'\n----\n\nWe add our placeholder text...\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch04l011)\n====\n[source,html]\n----\n    <input id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n----\n====\n\nWhich gives:\n\n----\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; [...]\n----\n\nSo we can go ahead and put the table onto the page. At this stage it'll just be empty:\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch04l012)\n====\n[source,html]\n----\n    <input id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n    <table id=\"id_list_table\">\n    </table>\n  </body>\n----\n====\n\nWhat does the FT think?\n\n----\n[...]\n  File \"...goat-book/functional_tests.py\", line 40, in\ntest_can_start_a_todo_list\n    self.assertTrue(any(row.text == \"1: Buy peacock feathers\" for row in rows))\n    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: False is not true\n----\n\n\n\nSlightly cryptic!\nWe can use the line number to track it down,\nand it turns out it's that `any()` function I was so smug about earlier--or,\nmore precisely, the `assertTrue`, which doesn't have a very explicit failure message.(((\"error messages\", \"passing custom error message to assertX methods in unittest\")))(((\"unittest module\", \"passing custom error message to assertX methods in\")))\nWe can pass a custom error message as an argument to most `assertX` methods in `unittest`:\n\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch04l013)\n====\n[source,python]\n----\n    self.assertTrue(\n        any(row.text == \"1: Buy peacock feathers\" for row in rows),\n        \"New to-do item did not appear in table\",\n    )\n----\n====\n\nIf you run the FT again, you should see our helpful message:\n\n----\nAssertionError: False is not true : New to-do item did not appear in table\n----\n\n\nBut now, to get this to pass, we will need to actually process the user's form submission.\nAnd that's a topic for the next chapter.\n\nFor now let's do a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n$ *git commit -am \"Front page HTML now generated from a template\"*\n----\n\n\nThanks to a bit of refactoring,\nwe've got our view set up to render a template,\nwe've stopped testing constants,\nand we're now well placed to start processing user input.\n\n\n=== Recap: The TDD Process\n\n\n(((\"Test-Driven Development (TDD)\", \"concepts\", \"Red/Green/Refactor\")))\n(((\"Red/Green/Refactor\")))\n(((\"unit-test/code cycle\")))\n(((\"Test-Driven Development (TDD)\", \"overall process of\", id=\"TDDprocess04\")))\nWe've now seen all the main aspects of the TDD process, in practice:\n\n* Functional tests\n* Unit tests\n* The unit-test/code cycle\n* Refactoring\n\nIt's time for a little recap, and perhaps even some flowcharts.\n(Forgive me, my years misspent as a management consultant have ruined me.\nOn the plus side, said flowcharts will feature recursion!)\n\nWhat does the overall TDD process look like?\n\n* We write a test.\n* We run the test and see it fail.\n* We write some minimal code to get it a little further.\n* We rerun the test and repeat until it passes (the unit-test/code cycle)\n* Then, we look for opportunities to refactor our code,\n  using our tests to make sure we don't break anything.\n* Then, we look for opportunities to refactor our tests too,\n  while attempting to stick to rules like\n  \"test behaviour, not implementation\" and \"don't test constants\".\n* And start again from the top!\n\nSee <<simple-tdd-diagram>>.\n\n[[simple-tdd-diagram]]\n.TDD process as a flowchart, including the unit-test/code cycle\nimage::images/tdd3_0403.png[\"A flowchart with boxes for tests, coding and refactoring, with yes/no labels showing when we move forwards or backwards\"]\n\nIt's very common to talk about this process using the three words:\n_red/green/refactor_. See <<red-green-refactor>>.\n\n[[red-green-refactor]]\n.Red/green/refactor\nimage::images/tdd3_0404.png[\"Red, green, and refactor as three nodes in a circle, with arrows flowing around.\"]\n\n* We write a test, and see it fail (\"red\").\n* We cycle between code and tests until the test passes (\"green\").\n* Then, we look for opportunities to refactor.\n* Repeat as required!\n\n==== Double-loop TDD\n\nBut how does this apply when we have functional tests _and_ unit tests?\nWell, you can think of the FT as driving a higher-level version of the same(((\"double-loop TDD\")))(((\"Red/Green/Refactor\", \"inner and outer loops in\"))) cycle,\nwith an inner red/green/refactor loop being required to get an FT from red to green; see <<double-loop-tdd-diagram>>.\n\n[[double-loop-tdd-diagram]]\n.Double-loop TDD: Inner and outer loops\nimage::images/tdd3_0702.png[\"An inner red/green/refactor loop surrounded by an outer red/green of FTs\"]\n\nWhen a new feature or business requirement comes along,\nwe write a new (failing) FT to capture a high-level view of the requirement.\nIt may not cover every last edge case,\nbut it should be enough to reassure ourselves that things are working.\n\nTo get that FT to green,\nwe then enter into the lower-level unit test cycle,\nwhere we put together all the moving parts required,\nand add tests for all the edge cases.\nAny time we get to green and refactored at the unit test level,\nwe can pop back up to the FT level to guide us towards the\nnext thing we need to work on.\nOnce both levels are green, we can do any extra refactoring\nor work on edge cases.\n\n// SEBASTIAN: It's a great moment to stop and reflect on the things a reader learnt so far.\n//    From my POV, introducing diagrams and explaining them encourages reader to\n//    generalise knowledge they acquired so far.\n//    Therefore, it would be great to recap on how these cycles were in play in this\n//    and previous chapters.\n//    TL;DR: more diagrams pls.\n\nWe'll explore all of the different parts of this workflow in more detail\nover the coming chapters.\n(((\"\", startref=\"TDDprocess04\")))\n\n\n.How to \"Check\" Your Code, or Skip Ahead (If You Must)\n*******************************************************************************\n\n(((\"GitHub\")))\n(((\"code examples, obtaining and using\")))\nAll of the code examples I've used in the book are available\nin https://github.com/hjwp/book-example/[my repo on GitHub].\nSo, if you ever want to compare your code against mine,\nyou can take a look at it there.\n\nEach chapter has its own branch, which is named after its short name.\nThe https://github.com/hjwp/book-example/tree/chapter_04_philosophy_and_refactoring[one for this chapter] is a snapshot of the code as it should be at the _end_ of the chapter.\n\nYou can find a full list of them in <<appendix_github_links>>, as well as\ninstructions on how to download them or use Git to compare your code to\nmine.\n\nObviously I can't possibly condone it,\nbut you can also use my repo to \"skip ahead\"\nand check out the code to let you work on a later chapter\nwithout having worked through all the earlier chapters yourself.\nYou're only cheating yourself you know!\n\n*******************************************************************************\n\n"
  },
  {
    "path": "chapter_05_post_and_database.asciidoc",
    "content": "[[chapter_05_post_and_database]]\n== Saving User Input: Testing the Database\n\n// (((\"user interactions\", \"testing database input\", id=\"UIdatabase05\")))\n// disabled due to pdf rendering issue\nSo far, we've managed to return a static HTML page with an input box in it.\nNext, we want to take the text that the user types into that input box and send it to the server,\nso that we can save it somehow and display it back to them later.\n\nThe first time I started writing code for this chapter,\nI immediately wanted to skip to what I thought was the right design:\nmultiple database tables for lists and list items,\na bunch of different URLs for adding new lists and items,\nthree new view functions,\nand about half a dozen new unit tests for all of the above.\nBut I stopped myself.\nAlthough I was pretty sure I was smart enough\nto handle coding all those problems at once,\nthe point of TDD is to enable you to do one thing at a time,\nwhen you need to.\nSo I decided to be deliberately short-sighted,\nand at any given moment _only_ do what was necessary\nto get the functional tests (FTs) a little further.\n\n(((\"iterative development style\")))\nThis will be a demonstration of how TDD can support an incremental,\niterative style of development--it\nmay not be the quickest route, but you do get there in the end.footnote:[\n\"Geepaw\" Hill, another one of the TDD OGs, has\nhttps://oreil.ly/qTCLk[a series of blog posts]\nadvocating for taking \"Many More Much Smaller Steps (MMMSS)\".\nIn this chapter I'm being unrealistically short-sighted for effect,\nso don't do that!\nBut Geepaw argues that in the real world,\nwhen you slice your work into tiny increments,\nnot only do you get there in the end,\nbut you end up delivering business value _faster_.\n] There's a neat side benefit,\nwhich is that it enables me to introduce new concepts like models,\ndealing with POST requests, Django template tags, and so on,\n_one at a time_ rather than having to dump them on you all at once.\n\nNone of this says that you _shouldn't_ try to think ahead and be clever.\nIn the next chapter, we'll use a bit more design and up-front thinking,\nand show how that fits in with TDD.\nBut for now, let's plough on mindlessly and just do what the tests tell us to.\n\n\n\n=== Wiring Up Our Form to Send a POST Request\n\n(((\"database testing\", \"HTML POST requests\", \"creating\", id=\"DBIpostcreate05\")))(((\"form data validation\", \"wiring up form to send POST request\", id=\"ix_form\")))\n(((\"POST requests\", \"creating\", id=\"POSTcreate05\")))\n(((\"HTML\", \"POST requests\", \"creating\")))\nAt the end of the last chapter,\nthe tests were telling us we weren't able to save the user's input:\n\n----\n  File \"...goat-book/functional_tests.py\", line 40, in\ntest_can_start_a_todo_list\n[...]\nAssertionError: False is not true : New to-do item did not appear in table\n----\n\nTo get it to the server, for now we'll use a standard HTML POST request.\nA little boring, but also nice and easy to deliver--we\ncan use all sorts of sexy HTML5 and JavaScript later in the book.\n\nTo get our browser to send a POST request, we need to do two things:\n\n1. Give the `<input>` element a `name=` attribute.\n2. Wrap it in a `<form>` tagfootnote:[Did you know that\n   you don't need a button to make a form submit?\n   I can't remember when I learned that,\n   but readers have mentioned that it's unusual\n   so I thought I'd draw your attention to it.]\n   with `method=\"POST\"`.\n\n\n=== Testing the Contract Between Frontend and Backend\n\nIf you remember in the last chapter, we said we wanted to come back\nand revisit the smoke test of our home page template content.(((\"frontend, testing contract between backend and\")))(((\"backend, testing contract between frontend and\")))\nLet's have a quick look at our unit tests:\n\n\n[role=\"sourcecode currentcontents\"]\n.lists/tests.py\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        response = self.client.get(\"/\")\n        self.assertTemplateUsed(response, \"home.html\")\n\n    def test_renders_homepage_content(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, \"To-Do\")\n----\n====\n\nWhat's important about our home page content?\nHow can we obey both the \"don't test constants\" rule\nand the \"test behaviour, not implementation\" rule?\n\nThe specific spelling of the word \"To-Do\" is not important.\nAs we've just seen, the most important _behaviour_ that our home page is enabling,\nis the ability to submit a to-do item.(((\"behaviour\", \"testing for To-Do page\")))\nThe way we're going to deliver that is by adding a `<form>` tag with `method=\"POST\"`,\nand inside that, making sure our `<input>` has a `name=\"item_text\"`.\n\nOur FTs are telling us that it's not working at a high level,\nso what unit tests can we write at the lower level?(((\"unit tests\", \"writing for form in To-Do list home page\")))  Let's start with the form:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l001)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n\n    def test_renders_input_form(self):  # <1>\n        response = self.client.get(\"/\")\n        self.assertContains(response, '<form method=\"POST\">')  # <2>\n----\n====\n\n<1> We change the name of the test.\n<2> And we assert on the `<form>` tag specifically.\n\nThat gives us:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\nAssertionError: False is not true : Couldn't find '<form method=\"POST\">' in the\nfollowing response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n<h1>Your To-Do list</h1>\\n    <input id=\"id_new_item\" placeholder=\"Enter a\nto-do item\" />\\n    <table id=\"id_list_table\">\\n    </table>\\n\n</body>\\n</html>\\n'\n----\n\n\nLet's adjust our template at 'lists/templates/home.html':\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch05l002)\n====\n[source,html]\n----\n    <h1>Your To-Do list</h1>\n    <form method=\"POST\">\n      <input id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n    </form>\n----\n====\n\nThat gives us passing unit tests:\n\n----\nOK\n----\n\nAnd next, let's add a test for the `name=` attribute on the `<input>` tag:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l003)\n====\n[source,python]\n----\n    def test_renders_input_form(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, '<form method=\"POST\">')\n        self.assertContains(response, '<input name=\"item_text\"')\n----\n====\n\n[role=\"pagebreak-before\"]\nThat gives us this expected failure:\n\n----\n[...]\nAssertionError: False is not true : Couldn't find '<input name=\"item_text\"' in\nthe following response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n<h1>Your To-Do list</h1>\\n    <form method=\"POST\">\\n      <input\nid=\"id_new_item\" placeholder=\"Enter a to-do item\" />\\n    </form>\\n    <table\nid=\"id_list_table\">\\n    </table>\\n  </body>\\n</html>\\n'\n----\n\n\nAnd we fix it like this:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/home.html (ch05l004)\n====\n[source,html]\n----\n    <h1>Your To-Do list</h1>\n    <form method=\"POST\">\n      <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n    </form>\n    <table id=\"id_list_table\">\n----\n====\n\nThat gives us passing unit tests:\n\n----\nOK\n----\n\nThe lesson here is that we've tried to identify the \"contract\" between\nthe frontend and the backend of our site.  For our HTML form to work,\nit needs the form with the right `method`, and the input with the right `name`.\nEverything else is cosmetic. So that's what we test for in our unit tests.(((\"form data validation\", \"wiring up form to send POST request\", startref=\"ix_form\")))\n\n\n=== Debugging Functional Tests\n\nTime to go back to our FT.(((\"functional  tests (FTs)\", \"debugging for To-Do list home page form\", id=\"ix_FTdbg\"))) It gives us a slightly cryptic, unexpected error:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\n[...]\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests.py\", line 38, in\ntest_can_start_a_todo_list\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; [...]\n----\n\nOh dear, we're now failing two lines _earlier_,\nafter we submit the form, but before we are able to do the assert.\nSelenium seems to be unable to find our list table.\nWhy on earth would that happen?\nLet's take another look at our code:\n\n\n[role=\"sourcecode currentcontents\"]\n.functional_tests.py\n====\n[source,python]\n----\n        # When she hits enter, the page updates, and now the page lists\n        # \"1: Buy peacock feathers\" as an item in a to-do list table\n        inputbox.send_keys(Keys.ENTER)\n        time.sleep(1)\n\n        table = self.browser.find_element(By.ID, \"id_list_table\")  # <1>\n        rows = table.find_elements(By.TAG_NAME, \"tr\")\n        self.assertTrue(\n            any(row.text == \"1: Buy peacock feathers\" for row in rows),\n            \"New to-do item did not appear in table\",\n        )\n----\n====\n\n<1> Our test unexpectedly fails on this line.\n    How do we figure out what's going on?\n\n\n(((\"functional tests (FTs)\", \"debugging techniques\")))\n(((\"time.sleeps\")))\n(((\"error messages\", seealso=\"troubleshooting\")))\n(((\"print\", \"debugging with\")))\n(((\"debugging\", \"of functional tests\")))\nWhen a functional test fails with an unexpected failure, there are several\nthings we can do to debug it:\n\n* Add `print` statements to show, for example, what the current page text is.\n* Improve the _error message_ to show more info about the current state.\n* Manually visit the site yourself.\n* Use `time.sleep` to pause the test during execution so you can inspect what was happening.footnote:[\nAnother common technique for debugging tests is to use `breakpoint()` to drop into a debugger like `pdb`.\nThis is more useful for _unit_ tests rather than FTs though,\nbecause in an FT you usually can't step into actual application code.\nPersonally, I only find debuggers useful for really fiddly algorithms,\nwhich we won't see in this book.]\n\nWe'll look at all of these over the course of this book,\nbut the `time.sleep` option is the one that leaps to mind with this kind of error in an FT.\nLet's try it now.(((\"sleep\", see=\"time.sleeps\")))\n\n\n==== Debugging with time.sleep\n\nConveniently, we've already got a (((\"time.sleeps\", \"debugging with\")))sleep just before the error occurs;\nlet's just extend it a little:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l005)\n====\n[source,python]\n----\n    # When she hits enter, the page updates, and now the page lists\n    # \"1: Buy peacock feathers\" as an item in a to-do list table\n    inputbox.send_keys(Keys.ENTER)\n    time.sleep(10)\n\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n----\n====\n\n(((\"debugging\", \"Django debug page\")))\nDepending on how fast Selenium runs on your PC,\nyou may have caught a glimpse of this already,\nbut when we run the FTs again,\nwe've got time to see what's going on:\nyou should see a page that looks like\n<<csrf_error_screenshot>>, with lots of Django debug information.\n\n\n[[csrf_error_screenshot]]\n.Django debug page showing CSRF error\nimage::images/tdd3_0501.png[\"Django debug page showing CSRF error\"]\n\n\n.Security: Surprisingly Fun!\n*******************************************************************************\n(((\"cross-site request forgery (CSRF)\")))\n(((\"security issues and settings\", \"cross-site request forgery\")))\nIf you've never heard of a _cross-site request forgery_ (CSRF) exploit, why not look it up now?\nLike all security exploits, it's entertaining to read about,\nbeing an ingenious use of a system in unexpected ways.\n\nWhen I went to university to get my computer science degree,\nI signed up for the \"security\" module out of a sense of duty:\n_Oh well, it'll probably be very dry and boring,\nbut I suppose I'd better take it.\nEat your vegetables, and so forth_.\nIt turned out to be one of the most fascinating modules of the whole course!\nAbsolutely full of the joy of hacking, of the particular mindset it takes\nto think about how systems can be used in unintended ways.\n\nI want to recommend the textbook from that course,\nRoss Anderson's https://oreil.ly/TKmYQ[_Security Engineering_].\nIt's quite light on pure crypto,\nbut it's absolutely full of interesting discussions of unexpected topics like lock picking,\nforging bank notes, inkjet printer cartridge [keep-together]#economics#,\nand spoofing South African Air Force jets with replay attacks.\nIt's a huge tome, about three inches thick,\nand I promise you it's an absolute page-turner.\n*******************************************************************************\n\n\n(((\"templates\", \"tags\", \"{% csrf_token %}\")))\n(((\"{% csrf_token %}\")))\nDjango's CSRF protection involves placing a little autogenerated unique token into each generated form,\nto be able to verify that POST requests have definitely come from the form generated by the server.\nSo far, our template has been pure HTML,\nand in this step we make the first use of Django's template magic.\nTo add the CSRF token, we use a 'template tag',\nwhich has the curly-bracket/percent syntax,\n`{% ... %}`&mdash;famous for being the world's most annoying two-key touch-typing\ncombination:\n\n\n// IDEA: unit test this?  can use Client(enforce_csrf_checks=True)\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch05l006)\n====\n[source,html]\n----\n  <form method=\"POST\">\n    <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n    {% csrf_token %}\n  </form>\n----\n====\n\nDjango will substitute the template tag during rendering with an `<input type=\"hidden\">`\ncontaining the CSRF token.\nRerunning the functional test will now bring us back to our previous (expected) failure:\n\n----\n  File \"...goat-book/functional_tests.py\", line 40, in\ntest_can_start_a_todo_list\n[...]\nAssertionError: False is not true : New to-do item did not appear in table\n----\n\nBecause our long `time.sleep` is still there, the test will pause on the final\nscreen, showing us that the new item text disappears after the form is\nsubmitted, and the page refreshes to show an empty form again.  That's because\nwe haven't wired up our server to deal with the POST request yet--it just\nignores it and displays the normal home page.\n\n\n(((\"\", startref=\"DBIpostcreate05\")))\n(((\"\", startref=\"POSTcreate05\")))\nWe can put our normal short `time.sleep` back now though:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l007)\n====\n[source,python]\n----\n    # \"1: Buy peacock feathers\" as an item in a to-do list table\n    inputbox.send_keys(Keys.ENTER)\n    time.sleep(1)\n\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n----\n====\n\n\n\n=== Processing a POST Request on the Server\n\n(((\"functional  tests (FTs)\", \"debugging for To-Do list home page form\", startref=\"ix_FTdbg\")))(((\"database testing\", \"HTML POST requests\", \"processing\")))\n(((\"POST requests\", \"processing\")))\n(((\"HTML\", \"POST requests\", \"processing\")))\nBecause we haven't specified an `action=` attribute in the form,\nit is submitting back to the same URL it was rendered from by default (i.e., `/`),\nwhich is dealt with by our `home_page` function.\nThat's fine for now; let's adapt the view to be able to deal with a POST request.\n\nThat means a new unit test for the `home_page` view.\nOpen up 'lists/tests.py', and add a new method to `HomePageTest`:\n\n[role=\"sourcecode small-code\"]\n.lists/tests.py (ch05l008)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n    def test_renders_input_form(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, '<form method=\"POST\">')\n        self.assertContains(response, '<input name=\"item_text\"')  # <2>\n\n    def test_can_save_a_POST_request(self):\n        response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})  # <1><2>\n        self.assertContains(response, \"A new list item\")  # <3>\n----\n====\n\n<1> To do a POST, we call `self.client.post` and, as you can see, it takes\n  a `data` argument that contains the form data we want to send.\n\n<2> Notice the echo of the `item_text` name from earlier.footnote:[\n    You could even define a constant for this, to make the link more explicit.]\n\n<3> Then we check that the text from our POST request ends up in the rendered HTML.\n\nThat gives us our expected fail:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\nAssertionError: False is not true : Couldn't find 'A new list item' in the\nfollowing response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n<h1>Your To-Do list</h1>\\n    <form method=\"POST\">\\n      <input\nname=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\\n\n<input type=\"hidden\" name=\"csrfmiddlewaretoken\"\nvalue=\"[...]\n</form>\\n    <table id=\"id_list_table\">\\n    </table>\\n  </body>\\n</html>\\n'\n----\n\n\nIn (slightly exaggerated) TDD style,\nwe can single-mindedly do \"the simplest thing that could possibly work\"\nto address this test failure,\nwhich is to add an `if` and a new code path for POST requests,\nwith a deliberately silly return value:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l009)\n====\n[source,python]\n----\nfrom django.http import HttpResponse\nfrom django.shortcuts import render\n\n\ndef home_page(request):\n    if request.method == \"POST\":  # <1>\n        return HttpResponse(\"You submitted: \" + request.POST[\"item_text\"])  # <2>\n    return render(request, \"home.html\")\n----\n====\n\n<1> `request.method` lets us check whether we got a POST or a GET request.\n\n<2> `request.POST` is a dictionary-like object containing the form data\n    (in this case, the `item_text` value we expect from the form `input` tag).\n\nFine, that gets our unit tests passing:\n\n----\nOK\n----\n\n...but it's not really what we want.footnote:[\nBut we _did_ learn about `request.method` and `request.POST`, right?\nI know it might seem that I'm overdoing it,\nbut doing things in tiny little steps really does have a lot of advantages,\nand one of them is that you can really think about (or in this case, learn)\none thing at a time.]\n\nAnd even if we were genuinely hoping this was the right solution,\nour FTs are here to remind us that this isn't how things are supposed to work:\n\n\n----\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; [...]\n----\n\n\nThe list table disappears after the form submission.\nIf you didn't see it in the FT run, try it manually with `runserver`;\nyou'll see something like <<table_gone_screenshot>>.\n\n[[table_gone_screenshot]]\n.I see my item text but no table...\nimage::images/tdd3_0502.png[\"A screenshot of the page after submission, which just has the raw text You submitted: Buy asparagus\"]\n\nWhat we really want to do is add the POST submission\nto the to-do items table in the home page template.\nWe need some sort of way to pass data from our view,\nto be shown in the template.\n\n\n=== Passing Python Variables to Be Rendered in the Template\n\nWe've already had a hint of it,\nand now it's time to start to get to know the real power of the Django template syntax,\nwhich is to pass variables from our Python view code into HTML templates.(((\"database testing\", \"template syntax\", id=\"DTtemplate05\")))(((\"templates\", \"syntax\")))(((\"templates\", \"passing variables to\")))\n\nLet's start by seeing how the template syntax lets us include a Python object in our template.\nThe notation is `{{ ... }}`, which displays the object as a string:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/home.html (ch05l010)\n====\n[source,html]\n----\n<body>\n  <h1>Your To-Do list</h1>\n  <form method=\"POST\">\n    <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n    {% csrf_token %}\n  </form>\n  <table id=\"id_list_table\">\n    <tr><td>{{ new_item_text }}</td></tr>  <1>\n  </table>\n</body>\n----\n====\n\n<1> Here's our template variable.\n    `new_item_text` will be the variable name for the user input we display in the template.\n\nLet's adjust our unit test so that it checks whether we are still using the template:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l011)\n====\n[source,python]\n----\n    def test_can_save_a_POST_request(self):\n        response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n        self.assertContains(response, \"A new list item\")\n        self.assertTemplateUsed(response, \"home.html\")\n----\n====\n\nAnd that will fail as expected:\n\n----\nAssertionError: No templates used to render the response\n----\n\nGood; our deliberately silly return value is now no longer fooling our tests,\nso we are allowed to rewrite our view, and tell it to pass the POST parameter to the template.\nThe `render` function takes, as its third argument, a dictionary,\nwhich maps template variable names to their values.\n\nIn theory, we can use it for the POST case as well as the default GET case,\nso let's remove the `if request.method == \"POST\"` and simplify our view right down to:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l012)\n====\n[source,python]\n----\ndef home_page(request):\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST[\"item_text\"]},\n    )\n----\n====\n\nWhat do the tests think?\n\n----\nERROR: test_uses_home_template\n(lists.tests.HomePageTest.test_uses_home_template)\n\n[...]\n    {\"new_item_text\": request.POST[\"item_text\"]},\n                      ~~~~~~~~~~~~^^^^^^^^^^^^^\n[...]\ndjango.utils.datastructures.MultiValueDictKeyError: 'item_text'\n\n----\n\n\n==== An Unexpected Failure\n\n(((\"unexpected failures\")))\n(((\"Test-Driven Development (TDD)\", \"concepts\", \"unexpected failures\")))\nOops, an _unexpected failure_.\n\nIf you remember the rules for reading tracebacks,\nyou'll spot that it's actually a failure in a _different_ test.\nWe got the actual test we were working on to pass,\nbut the unit tests have picked up an unexpected consequence, a regression:\nwe broke the code path where there is no POST request.\n\nThis is the whole point of having tests.\nYes, perhaps we could have predicted this would happen,\nbut imagine if we'd been having a bad day or weren't paying attention:\nour tests have just saved us from accidentally breaking our application\nand, because we're using TDD, we found out immediately.\nWe didn't have to wait for a QA team,\nor switch to a web browser and click through our site manually,\nso we can get on with fixing it straight away.\nHere's how:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l013)\n====\n[source,python]\n----\ndef home_page(request):\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},\n    )\n----\n====\n\nWe use http://docs.python.org/3/library/stdtypes.html#dict.get[`dict.get`] to\nsupply a default value, for the case where we are doing a normal GET request,\nwhen the POST dictionary is empty.\n\n[role=\"pagebreak-before\"]\nThe unit tests should now pass.  Let's see what the FTs say:\n\n----\nAssertionError: False is not true : New to-do item did not appear in table\n----\n\n\nTIP: If your functional tests show you a different error at this point,\n    or at any point in this chapter, complaining about a\n    +S&#x2060;t&#x2060;a&#x2060;l&#x2060;e&#x2060;E&#x2060;l&#x2060;e&#x2060;m&#x2060;e&#x2060;n&#x2060;t&#x200b;R&#x2060;e&#x2060;f&#x2060;e&#x2060;r&#x2060;e&#x2060;n&#x2060;c&#x2060;e&#x2060;Exception+, you may need to increase the\n    `time.sleep` explicit wait--try two or three seconds instead of one;\n    then read on to the next chapter for a more robust solution.\n\n\n==== Improving Error Messages in Tests\n\n(((\"debugging\", \"improving error messages\")))(((\"error messages\", \"improving in tests\")))\nHmm, not a wonderfully helpful error.\nLet's use another of our FT debugging techniques: improving the error message.\nThis is probably the most constructive technique,\nbecause those improved error messages stay around to help debug any future errors:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l014)\n====\n[source,python]\n----\nself.assertTrue(\n    any(row.text == \"1: Buy peacock feathers\" for row in rows),\n    f\"New to-do item did not appear in table. Contents were:\\n{table.text}\",\n)\n----\n====\n\nThat gives us a more helpful message:\n\n----\nAssertionError: False is not true : New to-do item did not appear in table.\nContents were:\nBuy peacock feathers\n----\n\nActually, you know what would be even better?\nMaking that assertion a bit less clever!\nAs you may remember from <<chapter_04_philosophy_and_refactoring>>,\nI was very pleased with myself for using the `any()` function,\nbut one of my early release readers (thanks, Jason!) suggested a much simpler implementation.\nWe can replace all four lines of the `assertTrue` with a single `assertIn`:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l015)\n====\n[source,python]\n----\n    self.assertIn(\"1: Buy peacock feathers\", [row.text for row in rows])\n----\n====\n\nMuch better.\nYou should always be very worried whenever you think you're being clever,\nbecause what you're probably being is _overcomplicated_.\n\nNow we get the error message for free:\n\n----\n    self.assertIn(\"1: Buy peacock feathers\", [row.text for row in rows])\nAssertionError: '1: Buy peacock feathers' not found in ['Buy peacock feathers']\n----\n\n\nConsider me suitably chastened.\n\nTIP: If, instead, your FT seems to be saying the table is empty (\"not found in\n    ['']\"), check your `<input>` tag--does it have the correct\n    `name=\"item_text\"` attribute?  And does it have `method=\"POST\"`?  Without\n    them, the user's input won't be in the right place in `request.POST`.\n\nThe point is that the FT wants us to enumerate list items with a \"1:\" at the\nbeginning of the first list item.\n\nThe fastest way to get that to pass is with another quick \"cheating\" change to the template:\n\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch05l016)\n====\n[source,html]\n----\n    <tr><td>1: {{ new_item_text }}</td></tr>\n----\n====\n\n\n.When Should You Stop Cheating? DRY Versus Triangulation\n*******************************************************************************\nPeople often ask about when it's OK to \"stop cheating\",\nand change from an implementation we know to be wrong,\nto one we're happy with.(((\"Test-Driven Development (TDD)\", \"concepts\", \"triangulation\")))\n(((\"triangulation\")))\n(((\"Don't Repeat Yourself (DRY)\")))\n(((\"Test-Driven Development (TDD)\", \"concepts\", \"DRY\")))\n(((\"duplication, eliminating\")))\n\nOne justification is _eliminate duplication_—aka DRY (don’t repeat yourself)—which (with some caveats) is a good guideline for any kind of code.\n\nIf your test uses a magic constant (like the \"1:\" in front of our list item),\nand your application code also uses it,\nsome people say _that_ counts as duplication, so it justifies refactoring.\nRemoving the magic constant from the application code usually means you have to stop cheating.\n\nIt's a judgement call,\nbut I feel that this is stretching the definition of \"repetition\" a little,\nso I often like to use a second technique, which is called _triangulation_:\nif your tests let you get away with writing \"cheating\" code that you're not happy with\n(like returning a magic constant),\nthen _write another test_ that forces you to write some better code.\nThat's what we're doing when we extend the FT\nto check that we get a \"2:\" when inputting a second list item.\n\nSee also <<three_strikes_and_refactor>> for a further note of caution\non applying DRY too quickly.\n\n*******************************************************************************\n\n[role=\"pagebreak-before\"]\nNow we get to the `self.fail('Finish the test!')`.\nIf we get rid of that and finish writing our FT,\nto add the check for adding a second item to the table\n(copy and paste is our friend),\nwe begin to see that our first cut solution really isn't going to, um, [.keep-together]#cut it#:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l017)\n====\n[source,python]\n----\n    # There is still a text box inviting her to add another item.\n    # She enters \"Use peacock feathers to make a fly\"\n    # (Edith is very methodical)\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    inputbox.send_keys(\"Use peacock feathers to make a fly\")\n    inputbox.send_keys(Keys.ENTER)\n    time.sleep(1)\n\n    # The page updates again, and now shows both items on her list\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n    rows = table.find_elements(By.TAG_NAME, \"tr\")\n    self.assertIn(\n        \"2: Use peacock feathers to make a fly\",\n        [row.text for row in rows],\n    )\n    self.assertIn(\n        \"1: Buy peacock feathers\",\n        [row.text for row in rows],\n    )\n\n    # Satisfied, she goes back to sleep\n----\n====\n\n(((\"\", startref=\"DTtemplate05\")))\nSure enough, the FTs return an error:\n\n----\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use\npeacock feathers to make a fly']\n----\n\n[role=\"pagebreak-before less_space\"]\n[[three_strikes_and_refactor]]\n=== Three Strikes and Refactor\n\n(((\"code smell\")))\n(((\"three strikes and refactor rule\", id=\"threestrikes05\")))\n(((\"refactoring\", id=\"refactor05\")))\nBut before we go further--we've got a bad __code smell__footnote:[\nIf you've not come across the concept, a \"code smell\" is\nsomething about a piece of code that makes you want to rewrite it. Jeff Atwood\nhas https://oreil.ly/GFrNp[a compilation on\nhis blog, _Coding Horror_]. The more experience you gain as a programmer, the more\nfine-tuned your nose becomes to code smells...]\nin this FT.\nWe have three almost identical code blocks checking for new items in the list table.\n(((\"Don’t Repeat Yourself (DRY)\")))\nWhen we want to apply the DRY principle,\nI like to follow the motto _three strikes and refactor_.\nYou can copy and paste code once,\nand it may be premature to try to remove the duplication it causes,\nbut once you get three occurrences, it's time to tidy up.\n\nLet's start by committing what we have so far. Even though we know our site\nhas a major flaw--it can only handle one list item--it's still further ahead than it was.\nWe may have to rewrite it all, and we may not, but the rule\nis that before you do any refactoring, always do a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n# should show changes to functional_tests.py, home.html,\n# tests.py and views.py\n$ *git commit -a*\n----\n\n\nTIP:  Always do a commit before embarking on a refactor.\n\n// TODO: also, make sure the tests are passing?\n\nOnto our functional test refactor. Let's use a helper method--remember,\nonly methods that begin with `test_` will be run as tests,\nso you can use other methods for your own purposes:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l018)\n====\n[source,python]\n----\n    def tearDown(self):\n        self.browser.quit()\n\n    def check_for_row_in_list_table(self, row_text):\n        table = self.browser.find_element(By.ID, \"id_list_table\")\n        rows = table.find_elements(By.TAG_NAME, \"tr\")\n        self.assertIn(row_text, [row.text for row in rows])\n\n    def test_can_start_a_todo_list(self):\n        [...]\n----\n====\n\n[role=\"pagebreak-before\"]\nI like to put helper methods near the top of the class, between the `tearDown`\nand the first test. Let's use it in the FT:\n\n[role=\"sourcecode\"]\n.functional_tests.py (ch05l019)\n====\n[source,python]\n----\n    # When she hits enter, the page updates, and now the page lists\n    # \"1: Buy peacock feathers\" as an item in a to-do list table\n    inputbox.send_keys(Keys.ENTER)\n    time.sleep(1)\n    self.check_for_row_in_list_table(\"1: Buy peacock feathers\")\n\n    # There is still a text box inviting her to add another item.\n    # She enters \"Use peacock feathers to make a fly\"\n    # (Edith is very methodical)\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    inputbox.send_keys(\"Use peacock feathers to make a fly\")\n    inputbox.send_keys(Keys.ENTER)\n    time.sleep(1)\n\n    # The page updates again, and now shows both items on her list\n    self.check_for_row_in_list_table(\"2: Use peacock feathers to make a fly\")\n    self.check_for_row_in_list_table(\"1: Buy peacock feathers\")\n\n    # Satisfied, she goes back to sleep\n----\n====\n\nWe run the FT again to check that it still behaves in the same way:\n\n----\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use\npeacock feathers to make a fly']\n----\n\nGood. Now we can commit the FT refactor as its own small, atomic change:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff* # check the changes to functional_tests.py\n$ *git commit -a*\n----\n\n\nThere are a couple more bits of duplication in the FTs,\nlike the repetition of finding the `inputbox`,\nbut they're not as egregious yet, so we'll deal with them later.\n\n// SEBASTIAN: One could mention there's still an option to cheat and keep items in a list in memory.\n//    I think there's no need to demonstrate it, though.\n\nInstead, back to work.\nIf we're ever going to handle more than one list item,\nwe're going to need some kind of persistence,\nand databases are a stalwart solution in this area.\n(((\"\", startref=\"threestrikes05\")))\n(((\"\", startref=\"refactor05\")))\n\n\n[role=\"pagebreak-before less_space\"]\n[[django_ORM_first_model]]\n=== The Django ORM and Our First Model\n\n(((\"object-relational mapper (ORM)\", id=\"orm05\")))\n(((\"Django framework\", \"object-relational mapper (ORM)\", id=\"DJForm05\")))\n(((\"database testing\", \"object-relational mapper (ORM)\", id=\"DBTorm05\")))\nAn object-relational mapper (ORM) is a layer of abstraction for data stored in a database\nwith tables, rows, and columns.\nIt lets us work with databases using familiar object-oriented metaphors that work well with code.\nClasses map to database tables, attributes map to columns,\nand an individual instance of the class represents a row of data in the database.\n\nDjango comes with an excellent ORM,\nand writing a unit test that uses it is actually an excellent way of learning it,\nbecause it exercises code by specifying how we want it to work.\n\n// SEBASTIAN: This reminds me of (https://github.com/gregmalcolm/python_koans)[Python Koans].\n//    Perhaps one could link it here as an example of learning with tests\n\nLet's create a new class in _lists/tests.py_:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l020)\n====\n[source,python]\n----\nfrom django.test import TestCase\nfrom lists.models import Item\n\n\nclass HomePageTest(TestCase):\n    [...]\n\n\nclass ItemModelTest(TestCase):\n    def test_saving_and_retrieving_items(self):\n        first_item = Item()\n        first_item.text = \"The first (ever) list item\"\n        first_item.save()\n\n        second_item = Item()\n        second_item.text = \"Item the second\"\n        second_item.save()\n\n        saved_items = Item.objects.all()\n        self.assertEqual(saved_items.count(), 2)\n\n        first_saved_item = saved_items[0]\n        second_saved_item = saved_items[1]\n        self.assertEqual(first_saved_item.text, \"The first (ever) list item\")\n        self.assertEqual(second_saved_item.text, \"Item the second\")\n----\n====\n\nYou can see that creating a new record in the database\nis a relatively simple matter of creating an object,\nassigning some attributes, and calling a `.save()` function.\nDjango also gives us an API for querying the database\nvia a class attribute, `.objects`,\nand we use the simplest possible query, `.all()`,\nwhich retrieves all the records for that table.\nThe results are returned as a list-like object called a `QuerySet`,\nfrom which we can extract individual objects,\nand also call further functions, like `.count()`.\nWe then check the objects as saved to the database,\nto check whether the right information was saved.\n\n\n(((\"Django framework\", \"tutorials\")))\nDjango's ORM has many other helpful and intuitive features;\nthis might be a good time to skim through the\nhttps://docs.djangoproject.com/en/5.2/intro/tutorial01[Django tutorial],\nwhich has an excellent intro to them.\n\nNOTE: I've written this unit test in a very verbose style,\n    as a way of introducing the Django ORM.\n    I wouldn't recommend writing your model tests like this \"in real life\",\n    because it's testing the framework, rather than testing our own code.\n    We'll actually rewrite this test to be much more concise\n    in <<chapter_16_advanced_forms>>\n    (specifically, at <<rewrite-model-test>>).\n\n\n.Unit Tests Versus Integration Tests, and the Database\n*******************************************************************************\n(((\"unit tests\", \"versus integration tests\", secondary-sortas=\"integration\")))\n(((\"integration tests\", \"versus unit tests\", secondary-sortas=\"unit\")))\nSome people will tell you that a \"real\" unit test should never touch the database,\nand that the test I've just written should be more properly called an \"integration\" test,\nbecause it doesn't _only_ test our code,\nbut also relies on an external system--that is, a database.\n\nIt's OK to ignore this distinction for now--we have two types of test:\nthe high-level FTs, which test the application from the user's point of view,\nand these lower-level tests, which test it from the programmer's point of view.\n\nWe'll come back to this topic\nand talk about the differences between unit tests, integration tests, and more\nin <<chapter_27_hot_lava>>, at the end of the book.\n*******************************************************************************\n\nLet's try running the unit test. Here comes another unit-test/code cycle:\n\n[subs=\"specialcharacters,macros\"]\n----\nImportError: cannot import name 'Item' from 'lists.models'\n----\n\nVery well, let's give it something to import from 'lists/models.py'.  We're\nfeeling confident so we'll skip the `Item = None` step, and go straight to\ncreating a class:\n\n[[first-django-model]]\n[role=\"sourcecode\"]\n.lists/models.py (ch05l021)\n====\n[source,python]\n----\nfrom django.db import models\n\n# Create your models here.\nclass Item:\n    pass\n----\n====\n\n[role=\"pagebreak-before\"]\nThat gets our test as far as:\n\n----\n[...]\n  File \"...goat-book/lists/tests.py\", line 25, in\ntest_saving_and_retrieving_items\n    first_item.save()\n    ^^^^^^^^^^^^^^^\nAttributeError: 'Item' object has no attribute 'save'\n----\n\nTo give our `Item` class a `save` method, and to make it into a real Django\nmodel, we make it inherit from the `Model` class:\n\n\n[role=\"sourcecode\"]\n.lists/models.py (ch05l022)\n====\n[source,python]\n----\nfrom django.db import models\n\n\nclass Item(models.Model):\n    pass\n----\n====\n\n\n==== Our First Database Migration\n\n(((\"database migrations\")))\nThe next thing that happens is a huuuuge traceback,\nthe long and short of which is that there's a problem with the database:\n\n----\ndjango.db.utils.OperationalError: no such table: lists_item\n----\n\nIn Django, the ORM's job is to model and read and write from database tables,\nbut there's a second system that's in charge of actually _creating_\nthe tables in the database called \"migrations\".\nIts job is to let you add, remove, and modify tables and columns,\nbased on changes you make to your _models.py_ files.\n\nOne way to think of it is as a version control system (VCS) for your database.\nAs we'll see later, it proves particularly useful\nwhen we need to upgrade a database that's deployed on a live server.\n\nFor now all we need to know is how to build our first database migration,\nwhich we do using the `makemigrations` command:footnote:[\nIf you've done a bit of Django before,\nyou may be wondering about when we're going to run \"migrate\" as well as \"makemigrations\"?\nRead on; that's coming up later in the chapter.]\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  lists/migrations/0001_initial.py\n    + Create model Item\n$ pass:quotes[*ls lists/migrations*]\n0001_initial.py  __init__.py  __pycache__\n----\n\nIf you're curious, you can go and take a look in the migrations file,\nand you'll see it's a representation of our additions to 'models.py'.\n\nIn the meantime, we should find that our tests get a little further.\n\n\n==== The Test Gets Surprisingly Far\n\nThe test actually gets surprisingly far:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\n    self.assertEqual(first_saved_item.text, \"The first (ever) list item\")\n                     ^^^^^^^^^^^^^^^^^^^^^\nAttributeError: 'Item' object has no attribute 'text'\n----\n\nThat's a full eight lines later than the last failure--we've\nbeen all the way through saving the two ++Item++s,\nand we've checked that they're saved in the database,\nbut Django just doesn't seem to have \"remembered\" the `.text` attribute.\n\nIf you're new to Python, you might have been surprised\nthat we were allowed to assign the `.text` attribute at all.\nIn a language like Java, you would probably get a compilation error.\nPython is more relaxed.\n\nClasses that inherit from `models.Model` will map to tables in the database.\nBy default, they get an autogenerated `id` attribute,\nwhich will be a primary key columnfootnote:[\nDatabase tables usually have a special column called a \"primary key\",\nwhich is the unique identifier for each row in the table.(((\"primary key\")))\nIt's worth brushing up on a _tiny_ bit of relational database theory,\nif you're not familiar with the concept or why it's useful.(((\"relational database theory\")))\nThe top three articles I found when searching for \"introduction to databases\"\nall seemed pretty good, at the time of writing.]\nin the database,\nbut you have to define any other columns and attributes you want explicitly.\nHere's how we set up a text column:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch05l024)\n====\n[source,python]\n----\nclass Item(models.Model):\n    text = models.TextField()\n----\n====\n\nDjango has many other field types, like `IntegerField`, `CharField`,\n`DateField`, and so on.  I've chosen `TextField` rather than `CharField` because\nthe latter requires a length restriction, which seems arbitrary at this point.\nYou can read more on field types in the Django\nhttps://docs.djangoproject.com/en/5.2/intro/tutorial02/#creating-models[tutorial]\nand in the\nhttps://docs.djangoproject.com/en/5.2/ref/models/fields[documentation].\n\n\n[role=\"pagebreak-before less_space\"]\n==== A New Field Means a New Migration\n\nRunning the tests gives us another database error:\n\n----\ndjango.db.utils.OperationalError: table lists_item has no column named text\n----\n\nIt's because we've added another new field to our database, which means we need\nto create another migration.(((\"database migrations\", \"new field requiring new migration\")))  Nice of our tests to let us know!\n\nLet's try it:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py makemigrations*]\nIt is impossible to add a non-nullable field 'text' to item without specifying\na default. This is because the database needs something to populate existing\nrows.\nPlease select a fix:\n 1) Provide a one-off default now (will be set on all existing rows with a null\nvalue for this column)\n 2) Quit and manually define a default value in models.py.\nSelect an option:pass:quotes[*2*]\n----\n\n\nAh.  It won't let us add the column without a default value.  Let's pick option\n2 and set a default in 'models.py'.  I think you'll find the syntax reasonably\nself-explanatory:\n\n\n[role=\"sourcecode\"]\n.lists/models.py (ch05l025)\n====\n[source,python]\n----\nclass Item(models.Model):\n    text = models.TextField(default=\"\")\n----\n====\n\n\n//IDEA: default could get another unit test, which could actually replace the\n// overly verbose one.\n\nAnd now the migration should complete:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  lists/migrations/0002_item_text.py\n    + Add field text to item\n----\n\nSo, two new lines in 'models.py', two database migrations, and as a result,\nthe `.text` attribute on our model objects is now recognised as a special attribute,\nso it does get saved to the database, and the tests pass:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\n\nRan 4 tests in 0.010s\nOK\n----\n\n\n(((\"\", startref=\"orm05\")))\n(((\"\", startref=\"DBTorm05\")))\n(((\"\", startref=\"DJForm05\")))\nSo let's do a commit for our first ever model!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # see tests.py, models.py, and 2 untracked migrations\n$ *git diff* # review changes to tests.py and models.py\n$ *git add lists*\n$ *git commit -m \"Model for list Items and associated migration\"*\n----\n\n\n=== Saving the POST to the Database\n\nSo, we have a model; now we need to use it!\n\n(((\"database testing\", \"HTML POST requests\", \"saving\", id=\"DTpostsave05\")))\n(((\"HTML\", \"POST requests\", \"saving\", id=\"HTMLpostsave05\")))\n(((\"POST requests\", \"saving\", id=\"POSTsave05\")))\nLet's adjust the test for our home page POST request,\nand say we want the view to save a new item to the database\ninstead of just passing it through to its response.\nWe can do that by adding three new lines to the existing test called\n+test_can_save_a_POST_request+:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l027)\n====\n[source,python]\n----\ndef test_can_save_a_POST_request(self):\n    response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n\n    self.assertEqual(Item.objects.count(), 1)  # <1>\n    new_item = Item.objects.first()  # <2>\n    self.assertEqual(new_item.text, \"A new list item\")  # <3>\n\n    self.assertContains(response, \"A new list item\")\n    self.assertTemplateUsed(response, \"home.html\")\n----\n====\n\n<1> We check that one new `Item` has been saved to the database.\n    `objects.count()` is a shorthand for `objects.all().count()`.\n\n<2> `objects.first()` is the same as doing `objects.all()[0]`,\n    except it will return `None` if there are no objects.footnote:[\n    You can also use `objects.get()`, which will immediately raise an exception\n    if there are no objects in the database, or if there are more than one.\n    On the plus side you get a more immediate failure,\n    and you get warned if there are too many objects.\n    The downside is that I find it slightly less readable.\n    As so often, it's a trade-off.]\n\n<3> We check that the item's text is correct.\n\n\n[role=\"pagebreak-before\"]\n(((\"unit tests\", \"length of\")))\nThis test is getting a little long-winded.\nIt seems to be testing lots of different things.\nThat's another _code smell_&mdash;a\nlong unit test either needs to be broken into two,\nor it may be an indication that the thing you're testing is too complicated.\nLet's add that to a little to-do list of our own,\nperhaps on a piece of scrap paper:\n\n\n[role=\"scratchpad\"]\n*****\n* 'Code smell: POST test is too long?'\n*****\n\n\n.An Alternative Testing Strategy: Staying at the HTTP Level\n*******************************************************************************\n\nIt's a very common pattern in Django to test POST views\nby asserting on the side effects, as seen in the database.\nSandi Metz, a TDD legend from the Ruby world, puts it like this:\n\"test commands via public side effects\".footnote:[\nThis advice is in her talk\nhttps://oreil.ly/Gqxgg[The Magic Tricks of Testing],\nwhich I highly recommend watching.]\n\nBut is the database really a public API?  That's arguable.\nCertainly it's at a different level of abstraction,\nor a different conceptual \"layer\" in the application,\nto the HTTP requests we're working with in our current unit tests.\n\nIf you wanted to write our tests in a way that stays at the HTTP level—that treats the application as more of an \"opaque box\"—you can prove to yourself that to-do items are persisted,\nby sending more than one:\n\n[role=\"sourcecode skipme\"]\n.lists/tests/tests.py\n====\n[source,python]\n----\ndef test_can_save_multiple_items(self):\n    self.client.post(\"/\", data={\"item_text\": \"first item\"})\n    response = self.client.post(\"/\", data={\"item_text\": \"second item\"})\n    self.assertContains(response, \"first item\")\n    self.assertContains(response, \"second item\")\n----\n====\n\nIf you feel like going off road, why not give it a try?\n\n\n////\nHARRY NOTES 2023-07-07\n\nhad a quick go at a new flow for the chapter based on this idea.\n\nhttps://github.com/hjwp/book-example/tree/chapter_05_post_and_database_possible_alternative\n\n    def test_post_saves_items(self):\n        self.client.post(\"/\", data={\"item_text\": \"onions\"})\n        response1 = self.client.get(\"/\")\n        self.assertContains(response1, \"onions\")\n\n    def test_multiple_posts_save_all_items(self):\n        self.client.post(\"/\", data={\"item_text\": \"onions\"})\n        self.client.post(\"/\", data={\"item_text\": \"carrots\"})\n        response = self.client.get(\"/\")\n        self.assertContains(response, \"onions\")\n        self.assertContains(response, \"carrots\")\n\n    def test_no_items_by_default(self):\n        response = self.client.get(\"/\")\n        empty_table = '<table id=\"id_list_table\"></table>',\n        self.assertContains(response, empty_table, html=True)\n\nnotes\n\n* you can start with just the first test\n* you can cheat to get this to pass by hardcoding 'onions' in the template obvs\n* then maybe we add the last test, no items by default\n* separate calls to get and post eliminates the weird return-things-from-a-post dance, may or may not be a good thing\n  - totally possible to imagine keeping that dance mind you.\n* then can move on to multiple items\n* if you want to cheat, you can just use a global variable, but that will never pass the 'no items by default' test\n* it does end up being a less obvious segue into why use a database tho.\n  because global vars are weirdly less persistent than a db,\n  because the test runner resets the database between each test?\n  that's a lot to explain.\n\noverally, definitely intrigued but haven't quite figured out\nthe perfect way to rewrite this chapter.\n////\n*******************************************************************************\n\n[role=\"pagebreak-before\"]\nWriting things down on a scratchpad like this reassures us that we won't forget them,\nso we are comfortable getting back to what we were working on.\nWe rerun the tests and see an expected failure:\n\n----\n    self.assertEqual(Item.objects.count(), 1)\nAssertionError: 0 != 1\n----\n\n\nLet's adjust our view:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l028)\n====\n[source,python]\n----\nfrom django.shortcuts import render\nfrom lists.models import Item\n\n\ndef home_page(request):\n    item = Item()\n    item.text = request.POST.get(\"item_text\", \"\")\n    item.save()\n\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},\n    )\n----\n====\n\nI've coded a very naive solution and you can probably spot a very obvious problem,\nwhich is that we're going to be saving empty items with every request to the home page.\nLet's add that to our list of things to fix later.\nYou know, along with the painfully obvious fact\nthat we currently have no way at all of having different lists for different people.\nThat we'll keep ignoring for now.\n\nRemember, I'm not saying you should always ignore glaring problems like this in \"real life\".\nWhenever we spot problems in advance, there's a judgement call to make\nover whether to stop what you're doing and start again, or leave them until later.\nSometimes finishing off what you're doing is still worth it,\nand sometimes the problem may be so major as to warrant a stop and rethink.\n\nLet's see how the unit tests get on...\n----\nRan 4 tests in 0.010s\n\nOK\n----\n\n[role=\"pagebreak-before\"]\nThey pass!  Good. Let's have a little look at our scratchpad.\nI've added a couple of the other things that are on our mind:\n\n[role=\"scratchpad\"]\n*****\n* 'Don't save blank items for every request.'\n* 'Code smell: POST test is too long?'\n* 'Display multiple items in the table.'\n* 'Support more than one list!'\n*****\n\n\nLet's start with the first scratchpad item:\n\"Don't save blank items for every request\".\nWe could tack on an assertion to an existing test,\nbut it's best to keep unit tests to testing one thing at a time,\nso let's add a new one:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l029)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n\n    def test_can_save_a_POST_request(self):\n        [...]\n\n    def test_only_saves_items_when_necessary(self):\n        self.client.get(\"/\")\n        self.assertEqual(Item.objects.count(), 0)\n----\n====\n\n// TODO: consider Item.objects.all() == [] instead\n// and explain why it gives you a nicer error message\n\n\nThat gives us a `1 != 0` failure.  Let's fix it by bringing the\n`if request.method` check back and putting the `Item` creation in there:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l030)\n====\n[source,python]\n----\ndef home_page(request):\n    if request.method == \"POST\":  # <1>\n        item = Item()\n        item.text = request.POST[\"item_text\"]  # <2>\n        item.save()\n\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},\n    )\n----\n====\n\n<1> We bring back the `request.method` check.\n<2> And we can switch from using `request.POST.get()` to `request.POST[]`\n    with square brackets,\n    because we know for sure that the `item_text` key should be in there,\n    and it's better to fail hard if it isn't.\n\n\n(((\"\", startref=\"DTpostsave05\")))\n(((\"\", startref=\"HTMLpostsave05\")))\n(((\"\", startref=\"POSTsave05\")))\nAnd that gets the test passing:\n\n----\nRan 5 tests in 0.010s\n\nOK\n----\n\n\n=== Redirect After a POST\n\n(((\"database testing\", \"HTML POST requests\", \"redirect following\", id=\"DThtmlredirect05\")))\n(((\"HTML\", \"POST requests\", \"redirect following\", id=\"HTMLpostredirect05\")))\n(((\"POST requests\", \"redirect following\", id=\"POSTredirect05\")))\nBut, yuck—those duplicated `request.POST` accesses are making me pretty unhappy.\nThankfully we are about to have the opportunity to fix it.\nA view function has two jobs: processing user input and returning an appropriate response.\nWe've taken care of the first part, which is saving the user's input to the database,\nso now let's work on the second part.\n\nhttps://oreil.ly/yGSl0[Always redirect after a POST],\nthey say, so let's do that.\nOnce again we change our unit test for saving a POST request:\ninstead of expecting a response with the item in it,\nwe want it to expect a redirect back to the home page.\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l031)\n====\n[source,python]\n----\n    def test_can_save_a_POST_request(self):\n        response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.first()\n        self.assertEqual(new_item.text, \"A new list item\")\n\n        self.assertRedirects(response, \"/\")  # <1>\n\n    def test_only_saves_items_when_necessary(self):\n        [...]\n----\n====\n\n<1> We no longer expect a response with HTML content rendered by a template,\n    so we lose the `assertContains` calls that looked at that.\n    Instead, we use Django's `assertRedirects` helper,\n    which checks that we return an HTTP 302 redirect, back to the home URL.\n\n[role=\"pagebreak-before\"]\nThat gives us this expected failure:\n\n----\nAssertionError: 200 != 302 : Response didn't redirect as expected: Response\ncode was 200 (expected 302)\n----\n\nWe can now tidy up our view substantially:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l032)\n====\n[source,python]\n----\nfrom django.shortcuts import redirect, render\nfrom lists.models import Item\n\n\ndef home_page(request):\n    if request.method == \"POST\":\n        item = Item()\n        item.text = request.POST[\"item_text\"]\n        item.save()\n        return redirect(\"/\")\n\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},\n    )\n----\n====\n\n\nAnd the tests should now pass:\n\n----\nRan 5 tests in 0.010s\n\nOK\n----\n\n\nWe're at green; time for a little refactor!\n\nLet's have a look at _views.py_\nand see what opportunities for improvement there might be:\n\n[role=\"sourcecode currentcontents\"]\n.lists/views.py\n====\n[source,python]\n----\ndef home_page(request):\n    if request.method == \"POST\":\n        item = Item()  # <1>\n        item.text = request.POST[\"item_text\"]  # <1>\n        item.save()  # <1>\n        return redirect(\"/\")\n\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},  # <2>\n    )\n----\n====\n\n<1> There's a quicker way to do these three lines with `.objects.create()`.\n\n<2> This line doesn't seem quite right now; in fact, it won't work at all.\n    Let's make a note on our scratchpad to sort out passing list items to the template.\n    It's actually closely related to \"Display multiple items\",\n    so we'll put it just before that one:\n\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Don't save blank items for every request.#'\n* 'Code smell: POST test is too long?'\n* 'Pass existing list items to the template somehow.'\n* 'Display multiple items in the table.'\n* 'Support more than one list!'\n*****\n\n\nAnd here's the refactored version of _views.py_ using the `.objects.create()`\nhelper method that Django provides, for one-line creation of objects:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l033)\n====\n[source,python]\n----\ndef home_page(request):\n    if request.method == \"POST\":\n        Item.objects.create(text=request.POST[\"item_text\"])\n        return redirect(\"/\")\n\n    return render(\n        request,\n        \"home.html\",\n        {\"new_item_text\": request.POST.get(\"item_text\", \"\")},\n    )\n\n----\n====\n\n[role=\"pagebreak-before less_space\"]\n=== Better Unit Testing Practice: Each Test Should Test [.keep-together]#One Thing#\n\n(((\"unit tests\", \"testing only one thing\")))\n(((\"testing best practices\")))(((\"POST requests\", \"POST test is too long code smell, addressing\")))\nLet's address the \"POST test is too long\" code smell.\n\nGood unit testing practice says that each test should only test one thing. The\nreason is that it makes it easier to track down bugs.  Having multiple\nassertions in a test means that, if the test fails on an early assertion, you\ndon't know what the statuses of the later assertions are. As we'll see in the next\nchapter, if we ever break this view accidentally, we want to know whether it's\nthe saving of objects that's broken, or the type of response.\n\nYou may not always write perfect unit tests with single assertions on your\nfirst go, but now feels like a good time to separate out our concerns:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l034)\n====\n[source,python]\n----\n    def test_can_save_a_POST_request(self):\n        self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.first()\n        self.assertEqual(new_item.text, \"A new list item\")\n\n    def test_redirects_after_POST(self):\n        response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n        self.assertRedirects(response, \"/\")\n----\n====\n\n(((\"\", startref=\"HTMLpostredirect05\")))\n(((\"\", startref=\"DThtmlredirect05\")))\n(((\"\", startref=\"POSTredirect05\")))\nAnd we should now see six tests pass instead of five:\n\n----\nRan 6 tests in 0.010s\n\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\n=== Rendering Items in the Template\n\n(((\"database testing\", \"rendering items in the template\", id=\"DTrender05\")))\nMuch better!  Back to our to-do list:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Don't save blank items for every request.#'\n* '[strikethrough line-through]#Code smell: POST test is too long?#'\n* 'Pass existing list items to the template somehow.'\n* 'Display multiple items in the table.'\n* 'Support more than one list!'\n*****\n\n\nCrossing things off the list is almost as satisfying as seeing tests pass!\n\nThe third and fourth items are the last of the \"easy\" ones.\nOur view now does the right thing for POST requests;\nit saves new list items to the database.\nNow we want GET requests to load all currently existing list items,\nand pass them to the template for rendering.\nLet's have a new unit test for that:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch05l035)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n    def test_renders_input_form(self):\n        [...]\n\n    def test_displays_all_list_items(self):\n        Item.objects.create(text=\"itemey 1\")\n        Item.objects.create(text=\"itemey 2\")\n\n        response = self.client.get(\"/\")\n\n        self.assertContains(response, \"itemey 1\")\n        self.assertContains(response, \"itemey 2\")\n\n    def test_can_save_a_POST_request(self):\n        [...]\n----\n====\n\n[role=\"pagebreak-before less_space\"]\n.Arrange-Act-Assert or Given-When-Then\n*******************************************************************************\n\nDid you notice the use of whitespace in this test?\nI'm visually separating out the code into three blocks:\n(((\"Arrange, Act, Assert\")))\n(((\"Given / When / Then\")))\n\n[role=\"sourcecode currentcontentss\"]\n.lists/tests.py\n====\n[source,python]\n----\n    def test_displays_all_list_items(self):\n        Item.objects.create(text=\"itemey 1\")  # <1>\n        Item.objects.create(text=\"itemey 2\")  # <1>\n\n        response = self.client.get(\"/\")  # <2>\n\n        self.assertContains(response, \"itemey 1\")  # <3>\n        self.assertContains(response, \"itemey 2\")  # <3>\n----\n====\n\n<1> Arrange: where we set up the data we need for the test.\n<2> Act: where we call the code under test\n<3> Assert: where we check on the results\n\nThis isn't obligatory, but it's a common convention,\nand it does help see the structure of the test.\n\nAnother popular way to talk about this structure is _given-when-then_:\n\n* _Given_ the database contains our list with two items,\n* _When_ I make a GET request for our list,\n* _Then_ I see the both items in our list.\n\nThis latter phrasing comes from the world of behaviour-driven development (BDD),\nand I actually prefer it somewhat.(((\"BDD (behaviour-driven development)\")))\nYou can see that it encourages phrasing things in a more natural way,\nand we're gently nudged to think of things in terms of behaviour\nand the perspective of the user.\n\n\n*******************************************************************************\n\nThat fails as expected:\n\n----\nAssertionError: False is not true : Couldn't find 'itemey 1' in the following\nresponse\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n[...]\n----\n\n[role=\"pagebreak-before\"]\n(((\"templates\", \"tags\", \"{% for ... endfor %}\")))\n(((\"{% for ... endfor %}\")))\nThe Django template syntax has a tag for iterating through lists,\n`{% for .. in .. %}`; we can use it like this:\n\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch05l036)\n====\n[source,html]\n----\n<table id=\"id_list_table\">\n  {% for item in items %}\n    <tr><td>1: {{ item.text }}</td></tr>\n  {% endfor %}\n</table>\n----\n====\n\nThis is one of the major strengths of the templating system. Now the template\nwill render with multiple `<tr>` rows, one for each item in the variable\n`items`.  Pretty neat!  I'll introduce a few more bits of Django template\nmagic as we go, but at some point you'll want to go and read up on the rest of\nthem in the\nhttps://docs.djangoproject.com/en/5.2/topics/templates[Django docs].\n\nJust changing the template doesn't get our tests to green; we need to actually\npass the items to it from our home page view:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch05l037)\n====\n[source,python]\n----\ndef home_page(request):\n    if request.method == \"POST\":\n        Item.objects.create(text=request.POST[\"item_text\"])\n        return redirect(\"/\")\n\n    items = Item.objects.all()\n    return render(request, \"home.html\", {\"items\": items})\n----\n====\n\nThat does get the unit tests to pass. Moment of truth...will the functional\ntest pass?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\n[...]\nAssertionError: 'To-Do' not found in 'OperationalError at /'\n----\n\n[role=\"pagebreak-before\"]\n(((\"\", startref=\"DTrender05\")))\n(((\"debugging\", \"using manual visits to the site\")))\nOops, apparently not.  Let's use another FT debugging technique,\nand it's one of the most straightforward: manually visiting the site!\nOpen up pass:[<em>http://localhost:8000</em>] in your web browser,\nand you'll see a Django debug page saying \"no such table: lists_item\",\nas in <<operationalerror>>.\n\n[[operationalerror]]\n[role=\"width-75\"]\n.Another helpful debug message\nimage::images/tdd3_0503.png[\"Screenshot of Django debug page, saying OperationalError at / no such table: lists_item\"]\n\n\n[role=\"pagebreak-before less_space\"]\n=== Creating Our Production Database with migrate\n\n(((\"database testing\", \"production database creation\", id=\"DTproduction05\")))\n(((\"database migrations\")))\nSo, we've got another helpful error message from Django,\nwhich is basically complaining that we haven't set up the database properly.\nHow come everything worked fine in the unit tests, I hear you ask?\nBecause Django creates a special 'test database' for unit tests;\nit's one of the magical things that Django's `TestCase` does.\n\nTo set up our \"real\" database, we need to explicitly create it.\nSQLite databases are just a file on disk,\nand you'll see in 'settings.py' that Django, by default, will just put it in a file\ncalled 'db.sqlite3' in the base project directory:\n\n[role=\"sourcecode currentcontents\"]\n.superlists/settings.py\n====\n[source,python]\n----\n[...]\n# Database\n# https://docs.djangoproject.com/en/5.2/ref/settings/#databases\n\nDATABASES = {\n    \"default\": {\n        \"ENGINE\": \"django.db.backends.sqlite3\",\n        \"NAME\": BASE_DIR / \"db.sqlite3\",\n    }\n}\n----\n====\n\n\nWe've told Django everything it needs to create the database,\nfirst via 'models.py' and then when we created the migrations file.\nTo actually apply it to creating a real database,\nwe use another Django Swiss Army knife 'manage.py' command, `migrate`:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py migrate*]\nOperations to perform:\n  Apply all migrations: admin, auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying contenttypes.0001_initial... OK\n  Applying auth.0001_initial... OK\n  Applying admin.0001_initial... OK\n  Applying admin.0002_logentry_remove_auto_add... OK\n  Applying admin.0003_logentry_add_action_flag_choices... OK\n  Applying contenttypes.0002_remove_content_type_name... OK\n  Applying auth.0002_alter_permission_name_max_length... OK\n  Applying auth.0003_alter_user_email_max_length... OK\n  Applying auth.0004_alter_user_username_opts... OK\n  Applying auth.0005_alter_user_last_login_null... OK\n  Applying auth.0006_require_contenttypes_0002... OK\n  Applying auth.0007_alter_validators_add_error_messages... OK\n  Applying auth.0008_alter_user_username_max_length... OK\n  Applying auth.0009_alter_user_last_name_max_length... OK\n  Applying auth.0010_alter_group_name_max_length... OK\n  Applying auth.0011_update_proxy_permissions... OK\n  Applying auth.0012_alter_user_first_name_max_length... OK\n  Applying lists.0001_initial... OK\n  Applying lists.0002_item_text... OK\n  Applying sessions.0001_initial... OK\n----\n\nIt seems to be doing quite a lot of work!\nThat's because it's the first ever migration,\nand Django is creating tables for all its built-in \"batteries included\"\napps, like the admin site and the built-in auth modules.\nWe don't need to pay attention to them for now.\nBut you can see our `lists.0001_initial` and `lists.0002_item_text` in there!\n\nAt this point, you can refresh the page on _localhost_ and see that the error is gone.\nLet's try running the functional tests again:footnote:[\nIf you get a different error at this point,\ntry restarting your dev server--it may have gotten confused\nby the changes to the database happening under its feet.]\n\n// DAVID: FWIW I'm not sure how this might happen - interested to know\n// if you have a real example of someone running into this problem.\n\n----\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers', '1: Use peacock feathers to make a fly']\n----\n\n\nSo close!(((\"templates\", \"tags\", \"{{ forloop.counter }}\")))  We just need to get our list numbering right.\nAnother awesome Django template tag, `forloop.counter`, will help here:\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch05l038)\n====\n[source,html]\n----\n  {% for item in items %}\n    <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n  {% endfor %}\n----\n====\n\n[role=\"pagebreak-before\"]\nIf you try it again, you should now see the FT gets to the end:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python functional_tests.py*]\n.\n ---------------------------------------------------------------------\nRan 1 test in 5.036s\n\nOK\n----\n\nHooray! But, as it's running, you may notice something is amiss, like in\n<<items_left_over_from_previous_run>>.\n\n\n[[items_left_over_from_previous_run]]\n.There are list items left over from the last run of the test\nimage::images/tdd3_0504.png[\"There are list items left over from the last run of the test\"]\n\n\nOh dear. It looks like previous runs of the test are leaving stuff lying around\nin our database.  In fact, if you run the tests again, you'll see it gets\nworse:\n\n[role=\"skipme\"]\n----\n1: Buy peacock feathers\n2: Use peacock feathers to make a fly\n3: Buy peacock feathers\n4: Use peacock feathers to make a fly\n5: Buy peacock feathers\n6: Use peacock feathers to make a fly\n----\n\nGrrr.  We're so close! We're going to need some kind of automated way of\ntidying up after ourselves. For now, if you feel like it, you can do it\nmanually by deleting the database and re-creating it fresh with `migrate`\n(you'll need to shut down your Django server first):\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *rm db.sqlite3*\n$ *python manage.py migrate --noinput*\n----\n\nAnd then (after restarting your server!) reassure yourself that the FT still\npasses.\n\nApart from that little bug in our functional testing, we've got some code\nthat's more or less working.  Let's do a commit.\n(((\"\", startref=\"DTproduction05\")))\n\n\nStart by doing a `git status` and a `git diff`, and you should see changes\nto 'home.html', 'tests.py', and 'views.py'. Let's add them:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add lists*\n$ *git commit -m \"Redirect after POST, and show all items in template\"*\n----\n\nTIP: You might find it useful to add markers for the end of each chapter, like\n    *`git tag end-of-chapter-05`*.\n\n\n=== Recap\n\nWhere are we?  How is progress on our app, and what have we learned?\n\n* We've got a form set up to add new items to the list using POST.\n\n* We've set up a simple model in the database to save list items.\n\n* We've learned about creating database migrations, both for the\n  test database (where they're applied automatically) and for the real\n  database (where we have to apply them manually).\n\n* We've used our first couple of Django template tags:  `{% csrf_token %}`\n  and the `{% for ... endfor %}` loop.\n\n* And we've used two different FT debugging techniques:\n  ++time.sleep++s, and improving the error messages.\n\n\nBut we've got a couple of items on our own to-do list,\nnamely getting the FT to clean up after itself,\nand perhaps more critically,\nadding support for more than one list:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Don't save blank items for every request.#'\n* '[strikethrough line-through]#Code smell: POST test is too long?#'\n* '[strikethrough line-through]#Pass existing list items to the template somehow.#'\n* '[strikethrough line-through]#Display multiple items in the table.#'\n* 'Clean up after FT runs.'\n* 'Support more than one list!'\n*****\n\n\nI mean, we _could_ ship the site as it is, but people might find it strange\nthat the entire human population has to share a single to-do list.\nI suppose it might get people to stop and think about\nhow connected we all are to one another,\nhow we all share a common destiny here on Spaceship Earth,\nand how we must all work together to solve the global problems that we face.\n\nBut in practical terms, the site wouldn't be very useful.\n\nAh well.\n// ((\"\", startref=\"UIdatabase05\"))\n\n\n.Useful TDD Concepts\n*******************************************************************************\n\nRegression::\n    When a change unexpectedly breaks some aspect of the application that used to work.\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"regression\")))\n    (((\"regression\")))\n\nUnexpected failure::\n    When a test fails in a way we weren't expecting.\n    This either means that we've made a mistake in our tests,\n    or that the tests have helped us find a regression,\n    and we need to fix something in our code.\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"unexpected failures\")))\n    (((\"unexpected failures\")))\n\nTriangulation::\n    Adding a test case with a new specific example for some existing code,\n    to justify generalising the implementation\n    (which may be a \"cheat\" until that point).\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"triangulation\")))\n    (((\"triangulation\")))\n\nThree strikes and refactor::\n    A rule of thumb for when to remove duplication from code.\n    When two pieces of code look very similar,\n    it often pays to wait until you see a third use case,\n    so that you're more sure about what part of the code really is the common,\n    reusable part to refactor out.\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"three strikes and refactor\")))\n    (((\"three strikes and refactor rule\")))\n\nThe scratchpad to-do list::\n    A place to write down things that occur to us as we're coding,\n    so that we can finish up what we're doing and come back to them later.\n    Love a good old-fashioned piece of paper now and again!\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"scratchpad to-do list\")))\n    (((\"scratchpad to-do list\")))\n\n// SEBASTIAN: (idea) alternative to maintaining a scratchpad could be to write empty unit tests without implementation.\n//    Such \"tests prototypes\" could be skipped initially until we work on them.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_06_explicit_waits_1.asciidoc",
    "content": "[[chapter_06_explicit_waits_1]]\n== Improving Functional Tests: Ensuring Isolation and Removing Magic Sleeps\n\nBefore we dive in and fix our single-global-list problem,\nlet's take care of a couple of housekeeping items.\nAt the end of the last chapter, we made a note\nthat different test runs were interfering with each other, so we'll fix that.\nI'm also not happy with all these ++time.sleep++s peppered through the code;\nthey seem a bit unscientific, so we'll replace them with something more reliable:\n\n[role=\"scratchpad\"]\n*****\n* _Clean up after FT runs._\n* _Remove time.sleeps._\n*****\n\n\nBoth of these changes will be moving us towards testing \"best practices\",\nmaking our tests more deterministic and more reliable.\n\n[role=\"pagebreak-before less_space\"]\n=== Ensuring Test Isolation in Functional Tests\n\n\n(((\"functional tests (FTs)\", \"ensuring isolation\", id=\"FTisolation06\")))\n(((\"isolation of tests\", \"ensuring in functional tests\", id=\"ix_isoFTs\")))\nWe ended the last chapter with a classic testing problem:\nhow to ensure _isolation_ between tests.\nEach run of our functional tests (FTs) left list items lying around in the database,\nand that interfered with the test results when next running the tests.\n\n(((\"unit tests\", \"in Django\", \"test databases\", secondary-sortas=\"Django\")))\nWhen we run _unit_ tests,\nthe Django test runner automatically creates a brand new test database\n(separate from the real one),\nwhich it can safely reset before each individual test is run,\nand then thrown away at the end.\nBut our FTs currently run against the \"real\" database, _db.sqlite3_.\n\nOne way to tackle this would be to \"roll our own\" solution,\nand add some code to _functional_tests.py_, which would do the cleaning up.\nThe `setUp` and `tearDown` methods are perfect for this sort of thing.\n\n\n(((\"LiveServerTestCase\")))\nBut as this is a common problem, Django supplies a test class called `LiveServerTestCase`\nthat addresses this issue.\nIt will automatically create a test database (just like in a unit test run)\nand start up a development server for the FTs to run against.(((\"database testing\", \"creating test database automatically\")))\nAlthough as a tool it has some limitations, which we'll need to work around later,\nit's dead useful at this stage, so let's check it out.\n\n`LiveServerTestCase` expects to be run by the Django test runner using\n_manage.py_, which will run tests from any files whose name begins with _test__.\nTo keep things neat and tidy, let's make a folder for our FTs,\nso that it looks a bit like an app.\nAll Django needs is for it to be a valid Python package directory\n(i.e., one with a +++<i>___init___.py</i>+++ [.keep-together]#in it#):\n\n[subs=\"\"]\n----\n$ <strong>mkdir functional_tests</strong>\n$ <strong>touch functional_tests/__init__.py</strong>\n----\n\n(((\"Git\", \"moving files\")))\nNow we want to 'move' our functional tests,\nfrom being a standalone file called 'functional_tests.py',\nto being the 'tests.py' of the `functional_tests` app.\nWe use `git mv` so that Git keeps track of the fact that this\nis the same file and should have a single history.\n\n\n[subs=\"\"]\n----\n$ <strong>git mv functional_tests.py functional_tests/tests.py</strong>\n$ <strong>git status</strong> # shows the rename to functional_tests/tests.py and __init__.py\n----\n\n[role=\"pagebreak-before\"]\nAt this point, your directory tree should look like this:\n\n----\n.\n├── db.sqlite3\n├── functional_tests\n│   ├── __init__.py\n│   └── tests.py\n├── lists\n│   ├── __init__.py\n│   ├── admin.py\n│   ├── apps.py\n│   ├── migrations\n│   │   ├── 0001_initial.py\n│   │   ├── 0002_item_text.py\n│   │   └── __init__.py\n│   ├── models.py\n│   ├── templates\n│   │   └── home.html\n│   ├── tests.py\n│   └── views.py\n├── manage.py\n└── superlists\n    ├── __init__.py\n    ├── asgi.py\n    ├── settings.py\n    ├── urls.py\n    └── wsgi.py\n----\n\n'functional_tests.py' is gone, and has turned into 'functional_tests/tests.py'.\nNow, whenever we want to run our FTs, instead of running `python\nfunctional_tests.py`, we will use `python manage.py test functional_tests`.\n\nNOTE: You could mix your functional tests into the tests for the `lists` app.\n    I tend to prefer keeping them separate, because FTs usually\n    have cross-cutting concerns that run across different apps.  FTs are meant\n    to see things from the point of view of your users, and your users don't\n    care about how you've split work between different apps!\n\n[role=\"pagebreak-before\"]\nNow, let's edit 'functional_tests/tests.py' and change our `NewVisitorTest`\nclass to make it use `LiveServerTestCase`:\n\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch06l001)\n====\n[source,python]\n----\nfrom django.test import LiveServerTestCase\nfrom selenium import webdriver\n[...]\n\n\nclass NewVisitorTest(LiveServerTestCase):\n    def setUp(self):\n        [...]\n----\n====\n\nNext, instead of hardcoding the visit to localhost port `8000`,\n`LiveServerTestCase` gives us an attribute called `live_server_url`:\n\n\n[role=\"dofirst-ch06l003 sourcecode\"]\n.functional_tests/tests.py (ch06l002)\n====\n[source,python]\n----\n    def test_can_start_a_todo_list(self):\n        # Edith has heard about a cool new online to-do app.\n        # She goes to check out its homepage\n        self.browser.get(self.live_server_url)\n----\n====\n\nWe can also remove the `if __name__ == '__main__'` from the end if we want,\nas we'll be using the Django test runner to launch the FT.\n\n\nNow we are able to run our functional tests using the Django test runner,\nby telling it to run just the tests for our new `functional_tests` app:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\nCreating test database for alias 'default'...\nFound 1 test(s).\nSystem check identified no issues (0 silenced).\n.\n ---------------------------------------------------------------------\nRan 1 test in 10.519s\n\nOK\nDestroying test database for alias 'default'...\n----\n\nNOTE: When I ran this test today, I ran into the (((\"Firefox\", \"upgrading\")))Firefox upgrade pop-up.\n  Just a little reminder, in case you happen to see it too,\n  we talked about it in <<chapter_01>> in a little <<firefox_upgrade_popup_aside,sidebar>>.\n\n[role=\"pagebreak-before\"]\nThe FT still passes, reassuring us that our refactor didn't break anything.\nYou'll also notice that if you run the tests a second time,\nthere aren't any old list items lying around from the previous test--it\nhas cleaned up after itself.\nSuccess! We should commit it as an atomic change:\n\n[subs=\"\"]\n----\n$ <strong>git status</strong> # functional_tests.py renamed + modified, new __init__.py\n$ <strong>git add functional_tests</strong>\n$ <strong>git diff --staged</strong>\n$ <strong>git commit</strong>  # msg eg \"make functional_tests an app, use LiveServerTestCase\"\n----\n\n\n==== Running Just the Unit Tests\n\n(((\"Django framework\", \"running functional and/or unit tests\")))\nNow if we run `manage.py test`,\nDjango will run both the functional and the unit tests:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nCreating test database for alias 'default'...\nFound 8 test(s).\nSystem check identified no issues (0 silenced).\n........\n ---------------------------------------------------------------------\nRan 8 tests in 10.859s\n\nOK\nDestroying test database for alias 'default'...\n----\n\n(((\"\", startref=\"FTisolation06\")))\n(((\"isolation of tests\", \"ensuring in functional tests\", startref=\"ix_isoFTs\")))\nTo run just the unit tests, we can specify that we want to\nonly run the tests for the `lists` app:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\nCreating test database for alias 'default'...\nFound 7 test(s).\nSystem check identified no issues (0 silenced).\n.......\n ---------------------------------------------------------------------\nRan 7 tests in 0.009s\n\nOK\nDestroying test database for alias 'default'...\n----\n\n\n[role=\"pagebreak-before less_space\"]\n.Useful Commands Updated\n*******************************************************************************\n\n(((\"Django framework\", \"commands and concepts\", \"python manage.py test functional_tests\")))To run the functional tests::\n    *`python manage.py test functional_tests`*\n\n(((\"Django framework\", \"commands and concepts\", \"python manage.py test lists\")))To run the unit tests::\n    *`python manage.py test lists`*\n\nWhat to do if I say \"run the tests\", and you're not sure which ones I mean?\nHave another look at the flowchart at the end of <<chapter_04_philosophy_and_refactoring>>,\nand try to figure out where we are.\nAs a rule of thumb, we usually only run the FTs once all the unit tests are passing,\nso if in doubt, try both!\n\n*******************************************************************************\n\n\n\n=== On Implicit and Explicit Waits, and Magic time.sleeps\n\n(((\"functional tests (FTs)\", \"implicit/explicit waits and time.sleeps\", id=\"FTimplicit06\")))(((\"waits\", \"explicit and implicit and time.sleeps\", id=\"ix_wait\")))\n(((\"implicit and explicit waits\", id=\"implicit06\")))\n(((\"explicit and implicit waits\", id=\"explicit06\")))\n(((\"time.sleeps\", \"removing magic sleeps\", id=\"ix_tmslprmv\")))\nLet's talk about the `time.sleep` in our FT:\n\n[role=\"sourcecode currentcontents\"]\n.functional_tests/tests.py\n====\n[source,python]\n----\n        # When she hits enter, the page updates, and now the page lists\n        # \"1: Buy peacock feathers\" as an item in a to-do list table\n        inputbox.send_keys(Keys.ENTER)\n        time.sleep(1)\n\n        self.check_for_row_in_list_table(\"1: Buy peacock feathers\")\n----\n====\n\n\nThis is what's called an \"explicit wait\".\nThat's in contrast with \"implicit waits\":\nin certain cases, Selenium tries to wait \"automatically\" for you when it thinks the page is loading.\nIt even provides a method called `implicitly_wait`\nthat lets you control how long it will wait\nif you ask it for an element that doesn't seem to be on the page yet.\n\nIn fact, in the first edition of this book, I was able to rely entirely on implicit waits.\nThe problem is that implicit waits are always a little flakey, and with the\nrelease of Selenium 4, implicit waits were disabled by default.\nAt the same time, the general opinion from the Selenium team is that implicit\nwaits are just a bad idea,\nand https://www.selenium.dev/documentation/webdriver/waits[should be avoided].\n\n\nSo this edition has explicit waits from the very beginning.\nBut the problem is that those ++time.sleep++s have their own issues.\n\nCurrently we're waiting for one second, but who's to say that's the right amount of time?\nFor most tests we run against our own machine, one second is way too long,\nand it's going to really slow down our FT runs. 0.1s would be fine.\nBut the problem is that if you set it that low,\nevery so often you're going to get a spurious failure\nbecause, for whatever reason, the laptop was being a bit slow just then.\nAnd even at one second, there’s still a chance of random failures that don’t indicate a real problem—and false positives in tests are a real annoyance.footnote:[There's lots more on this in\nhttps://oreil.ly/YdRx-[an article by Martin Fowler].]\n\n\n\n[TIP]\n====\nUnexpected `NoSuchElementException` and `StaleElementException` errors are often a sign that you need an explicit wait.(((\"NoSuchElementException\")))(((\"StaleElementException\")))\n====\n\nSo let's replace our sleeps with a tool that will wait for just as long as is needed,\nup to a nice long timeout to catch any glitches.\nWe'll rename `check_for_row_in_list_table` to `wait_for_row_in_list_table`,\nand add some polling/retry logic to it:\n\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch06l004)\n====\n[source,python]\n----\n[...]\nfrom selenium.common.exceptions import WebDriverException\nimport time\n\nMAX_WAIT = 5  # <1>\n\n\nclass NewVisitorTest(LiveServerTestCase):\n    def setUp(self):\n        [...]\n    def tearDown(self):\n        [...]\n\n    def wait_for_row_in_list_table(self, row_text):\n        start_time = time.time()\n        while True:  # <2>\n            try:\n                table = self.browser.find_element(By.ID, \"id_list_table\")  # <3>\n                rows = table.find_elements(By.TAG_NAME, \"tr\")\n                self.assertIn(row_text, [row.text for row in rows])\n                return  # <4>\n            except (AssertionError, WebDriverException):  # <5>\n                if time.time() - start_time > MAX_WAIT:  # <6>\n                    raise  # <6>\n                time.sleep(0.5)  # <5>\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> We'll use a constant called `MAX_WAIT`\n    to set the maximum amount of time we're prepared to wait.\n    Five seconds should be enough to catch any glitches or random slowness.\n\n<2> Here's the loop, which will keep going forever,\n    unless we get to one of two possible exit routes.\n\n<3> Here are our three lines of assertions\n    from the old version of the method.\n\n<4> If we get through them, and our assertion passes,\n    we return from the function and escape the loop.\n\n<5> But if we catch an exception,\n    we wait a short amount of time and loop around to retry.\n    There are two types of exceptions we want to catch:\n    `WebDriverException` for when the page hasn't loaded\n    and Selenium can't find the table element on the page;\n    and `AssertionError` for when the table is there,\n    but it's perhaps a table from before the page reloads,\n    so it doesn't have our row in yet.(((\"WebDriverException\")))(((\"AssertionError\")))\n\n<6> Here's our second escape route.\n    If we get to this point, that means our code kept raising exceptions\n    every time we tried it until we exceeded our timeout.\n    So this time, we reraise the exception\n    and let it bubble up to our test,\n    and most likely end up in our traceback,\n    telling us why the test failed.\n\nAre you thinking this code is a little ugly,\nand makes it a bit harder to see exactly what we're doing?\nI agree. Later on (<<self.wait-for>>),\nwe'll refactor out a general `wait_for` helper,\nto separate the timing and reraising logic from the test assertions.\nBut we'll wait until we need it in multiple places.\n\nNOTE: If you've used Selenium before, you may know that it has a few\n    https://www.selenium.dev/documentation/webdriver/waits/#explicit-waits[helper functions to conduct waits].\n    I'm not a big fan of them, though not for any objective reason really.\n    Over the course of the book, we'll build a couple of wait helper tools,\n    which I think will make for nice and readable code.(((\"Selenium\", \"helper functions to conduct waits\"))) But of course you should check out the homegrown Selenium waits in your own time,\n    and see if you prefer them.\n\n[role=\"pagebreak-before\"]\nNow we can rename our method calls, and remove the magic ++time.sleep++s:\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch06l005)\n====\n[source,python]\n----\n    [...]\n    # When she hits enter, the page updates, and now the page lists\n    # \"1: Buy peacock feathers\" as an item in a to-do list table\n    inputbox.send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n\n    # There is still a text box inviting her to add another item.\n    # She enters \"Use peacock feathers to make a fly\"\n    # (Edith is very methodical)\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    inputbox.send_keys(\"Use peacock feathers to make a fly\")\n    inputbox.send_keys(Keys.ENTER)\n\n    # The page updates again, and now shows both items on her list\n    self.wait_for_row_in_list_table(\"2: Use peacock feathers to make a fly\")\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n    [...]\n----\n====\n\n\nAnd rerun the tests:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\nCreating test database for alias 'default'...\nFound 8 test(s).\nSystem check identified no issues (0 silenced).\n........\n ---------------------------------------------------------------------\nRan 8 tests in 4.552s\n\nOK\nDestroying test database for alias 'default'...\n----\n\nHooray we're back to passing,\nand notice we've shaved a few of seconds off the execution time too.\nThat might not seem like a lot right now, but it all adds up.\n\nJust to check we've done the right thing,\nlet's deliberately break the test\nin a couple of ways and see some errors.\nFirst, let’s try searching for some text that we know isn’t there, and check that we get the expected error:\n\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch06l006)\n====\n[source,python]\n----\ndef wait_for_row_in_list_table(self, row_text):\n    [...]\n        rows = table.find_elements(By.TAG_NAME, \"tr\")\n        self.assertIn(\"foo\", [row.text for row in rows])\n        return\n----\n====\n\n[role=\"pagebreak-before\"]\nWe see we still get a nice self-explanatory test failure message:\n\n[subs=\"specialcharacters,macros\"]\n----\n    self.assertIn(\"foo\", [row.text for row in rows])\nAssertionError: 'foo' not found in ['1: Buy peacock feathers']\n----\n\nNOTE: Did you get a bit bored waiting five seconds for the test to fail?\n    That's one of the downsides of explicit waits.\n    There's a tricky trade-off between waiting long enough\n    that little glitches don't throw you,\n    versus waiting so long that expected failures are painfully slow to watch.\n    Making `MAX_WAIT` configurable so that it's fast in local dev,\n    but more conservative on continuous integration (CI) servers\n    can be a good idea.\n    See <<chapter_25_CI>> for an introduction to CI.\n\nLet's put that back the way it was and break something else:\n\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch06l007)\n====\n[source,python]\n----\n    try:\n        table = self.browser.find_element(By.ID, \"id_nothing\")\n        rows = table.find_elements(By.TAG_NAME, \"tr\")\n        self.assertIn(row_text, [row.text for row in rows])\n        return\n    [...]\n----\n====\n\n\nSure enough, we get the errors for when the page doesn't contain the element\nwe're looking for too:\n\n----\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_nothing\"]; For documentation on this error, [...]\n----\n\n\nEverything seems to be in order.  Let's put our code back to the way it should be,\nand do one final test run:\n\n[role=\"dofirst-ch06l008\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test*]\n[...]\nOK\n----\n\n\nGreat. With that little interlude over,\nlet's crack on with getting our application actually working\nfor multiple lists.  Don't forget to commit first!\n(((\"waits\", \"explicit and implicit and time.sleeps\", startref=\"ix_wait\")))(((\"\", startref=\"FTimplicit06\")))\n(((\"\", startref=\"implicit06\")))\n(((\"\", startref=\"explicit06\")))\n(((\"time.sleeps\", \"removing magic sleeps\", startref=\"ix_tmslprmv\")))\n\n\n[role=\"pagebreak-before less_space\"]\n.Testing \"Best Practices\" Applied in this Chapter\n*******************************************************************************\n\nEnsuring test isolation and managing global state::\n    Different tests shouldn't affect one another.\n    This means we need to reset any permanent state\n    at the end of each test. Django's test runner helps us do this\n    by creating a test database,\n    which it wipes clean in between each test.\n    (((\"testing best practices\")))\n\nAvoid \"magic\" sleeps::\n    Whenever we need to wait for something to load,\n    it's always tempting to throw in a quick-and-dirty `time.sleep`.\n    But the problem is that the length of time we wait\n    is always a bit of a shot in the dark,\n    either too short and too vulnerable to spurious failures,\n    or too long and it'll slow down our test runs.\n    Prefer a retry loop that polls our app\n    and moves on as soon as possible.\n\n\nDon't rely on Selenium's implicit waits::\n    Selenium does theoretically do some \"implicit\" waits,\n    but the implementation varies between browsers,\n    and is not always reliable.(((\"Selenium\", \"implicit waits, avoiding\")))\n    \"Explicit is better than implicit\", as the Zen of Python says,footnote:[`python -c \"import this\"`]\n    so prefer explicit waits.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_07_working_incrementally.asciidoc",
    "content": "[[chapter_07_working_incrementally]]\n== Working Incrementally\n\n(((\"Test-Driven Development (TDD)\", \"adapting existing code incrementally\", id=\"TDDadapt07\")))\n(((\"Testing Goat\", \"working state to working state\")))\nNow let's address our real problem,\nwhich is that our design only allows for one global list.\nIn this chapter I'll demonstrate a critical TDD technique:\nhow to adapt existing code using an incremental, step-by-step process\nthat takes you from working state to working state.\nTesting Goat, not Refactoring Cat!\n\n\n\n=== Small Design When Necessary\n\n(((\"small vs. big design\", id=\"small07\")))\n(((\"multiple lists testing\", \"small vs. big design\", id=\"MLTsmall07\")))\nLet's have a think about how we want support for multiple lists to work.\n\nAt the moment, the only URL for our site is the home page,\nand that's why there's only one global list.\nThe most obvious way to support multiple lists is to say that each list gets its own URL,\nso that people can start multiple lists,\nor so that different people can have different lists.\nHow might that work?\n\n\n\n==== Not Big Design Up Front\n\n(((\"agile movement\")))\n(((\"Big Design Up Front\")))\n(((\"minimum viable applications\")))\nTDD is closely associated with the Agile movement in software development,\nwhich includes a reaction against “_big design up front_”—the traditional software engineering practice whereby,\nafter a lengthy requirements-gathering exercise,\nthere is an equally lengthy design stage where the software is planned out on paper.\nThe Agile philosophy is that you learn more from solving problems in practice than in theory,\nespecially when you confront your application with real users as soon as possible.\nInstead of a long up-front design phase,\nwe try to put a _minimum viable product_ out there early,\nand let the design evolve gradually based on feedback from real-world usage.\n\n[role=\"pagebreak-before\"]\nBut that doesn't mean that thinking about design is outright banned!\nIn <<chapter_05_post_and_database>>, we saw how just blundering ahead without thinking\ncan _eventually_ get us to the right answer,\nbut often a little thinking about design can help us get there faster.\nSo, let's think about our minimum viable lists app,\nand what kind of design we'll need to deliver it:\n\n* We want each user to be able to store their own list--at least one, for now.\n* A list is made up of several items, whose primary attribute is a bit of descriptive text.\n* We need to save lists from one visit to the next.\n  For now, we can give each user a unique URL for their list.\n  Later on, we may want some way of automatically recognising users and showing them their lists.\n\nTo deliver the \"for now\" items,\nwe're going to have to store lists and their items in a database.\nEach list will have a unique URL,\nand each list item will be a bit of descriptive text, associated with a particular list—something like <<multiple-lists-users-and-urls>>.\n\n[[multiple-lists-users-and-urls]]\n.Multiple users with multiple lists at multiple URLs\nimage::images/tdd3_0701.png[\"An illustration showing two users looking at two different lists with different items, at different URLs.\"]\n\n\n==== YAGNI!\n\n(((\"Test-Driven Development (TDD)\", \"philosophy of\", \"YAGNI\")))\n(((\"YAGNI (You ain't gonna need it!)\")))\nOnce you start thinking about design, it can be hard to stop.\nAll sorts of other thoughts are occurring to us--we might want to give each list a name or title,\nwe might want to recognise users using usernames and passwords,\nwe might want to add a longer notes field as well as short descriptions to our list,\nwe might want to store some kind of ordering, and so on.\nBut we should obey another tenet of the Agile gospel: YAGNI (pronounced yag-knee),\nwhich stands for \"You ain't gonna need it!\"\nAs software developers, we have fun creating,\nand sometimes it's hard to resist the urge to build things\njust because an idea occurred to us and we _might_ need it.\nThe trouble is that more often than not, no matter how cool the idea was,\nyou _won't_ end up using it.\nInstead you just end up with a load of unused code, adding to the complexity of your application.\n\nYAGNI is the motto we use to resist our overenthusiastic creative urges.\nWe avoid writing any code that's not strictly required.\n\nTIP: Don't write any code unless you absolutely have to.footnote:[\nThis is a much more widely applicable rule for programming in business, actually.\nIf you can solve a problem without any coding at all, that's a big win.]\n\n\n==== REST-ish\n\n(((\"Representational State Transfer (REST)\", \"inspiration gained from\")))\n(((\"model-view-controller (MVC) pattern\")))(((\"MVC (model-view-controller) pattern\")))\nWe have an idea of the data structure we want--the \"model\" part of\nmodel-view-controller (we talked about MVC in <<django-mvc>>).\nWhat about the \"view\" and \"controller\" parts?\nHow should the user interact with ++List++s and their ++Item++s using a web browser?\n\nRepresentational state transfer (REST) is an approach to web design\nthat's usually used to guide the design of web-based APIs.\nWhen designing a user-facing site,\nit's not possible to stick _strictly_ to the REST rules,\nbut they still provide some useful inspiration\n(take a look at\nhttps://www.obeythetestinggoat.com/book/appendix_rest_api.html[Online Appendix: Building a REST API]\nif you want to see a real REST API).\nREST suggests that we have a URL structure that matches our data structure—in this case, lists and list items.\nEach list can have its own URL:\n\n[role=\"skipme\"]\n----\n    /lists/<list identifier>/\n----\n\nTo view a list, we use a GET request (a normal browser visit to the page).\n\nTo create a brand new list, we'll have a special URL that accepts POST requests:\n\n[role=\"skipme\"]\n----\n    /lists/new\n----\n\n// DAVID: for consistency, personally I would add trailing slashes to all the URLs.\n// SEBASTIAN: Why not just POST /lists/ ?\n//      Unless it's a URL for a view with a form!\n\nTo add a new item to an existing list,\nwe'll have a separate URL, to which we can send POST requests:\n\n[role=\"skipme\"]\n----\n    /lists/<list identifier>/add_item\n----\n\n// DAVID: I would use kebab case for URLs -> /add-item/\n// SEBASTIAN: Why not just POST /lists/<list identifier>/item ?\n//      Unless it's a URL for a view with a form!\n\n(Again, we're not trying to perfectly follow the rules of REST,\nwhich would use a PUT request here--we're just\nusing REST for inspiration.\nApart from anything else, you can't use PUT in a standard HTML form.)\n\n[role=\"pagebreak-before\"]\n(((\"\", startref=\"small07\")))\n(((\"\", startref=\"MLTsmall07\")))\nIn summary, our scratchpad for this chapter looks something like this:\n\n\n[role=\"scratchpad\"]\n*****\n* _Adjust model so that items are associated with different lists._\n* _Add unique URLs for each list._\n* _Add a URL for creating a new list via POST._\n* _Add URLs for adding a new item to an existing list via POST._\n*****\n\n\n=== Implementing the New Design Incrementally Using TDD\n\n(((\"Test-Driven Development (TDD)\", \"overall process of\")))\n(((\"multiple lists testing\", \"incremental design implementation\")))\nHow do we use TDD to implement the new design?\nLet's take another look at the flowchart for the TDD process, duplicated in <<double-loop-tdd-diagram-2>> for your convenience.\n\nAt the top level, we're going to use a combination of adding new functionality\n(by adding a new FT and writing new application code)\nand refactoring our application--that is,\nrewriting some of the existing implementation\nso that it delivers the same functionality to the user\nbut using aspects of our new design.\nWe'll be able to use the existing FT\nto verify that we don't break what already works,\nand the new FT to drive the new features.\n\nAt the unit test level,\nwe'll be adding new tests or modifying existing ones\nto test for the changes we want,\nand we'll be able to similarly use the unit tests\nwe _don't_ touch to help make sure we don't break anything in the process.\n\n[[double-loop-tdd-diagram-2]]\n.The TDD process with both functional and unit tests\nimage::images/tdd3_1708.png[\"An inner red/green/refactor loop surrounded by an outer red/green of FTs\"]\n\n\n=== Ensuring We Have a Regression Test\n\n(((\"regression\", id=\"regression07\")))\n(((\"multiple lists testing\", \"regression test\", id=\"MLTregression07\")))\nOur existing FT, `test_can_start_a_todo_list()`,\nis going to act as our regression test.\n\nLet's translate our scratchpad into a new FT method,\nwhich introduces a second user and checks that their to-do list is separate from\nEdith's.\n\nWe'll start out very similarly to the first. Edith adds a first item to\ncreate a to-do list, but we introduce our first new assertion—Edith's\nlist should live at its own, unique URL:\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch07l005)\n====\n[source,python]\n----\ndef test_can_start_a_todo_list(self):\n    # Edith has heard about a cool new online to-do app.\n    [...]\n    # Satisfied, she goes back to sleep\n\ndef test_multiple_users_can_start_lists_at_different_urls(self):\n    # Edith starts a new to-do list\n    self.browser.get(self.live_server_url)\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    inputbox.send_keys(\"Buy peacock feathers\")\n    inputbox.send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n\n    # She notices that her list has a unique URL\n    edith_list_url = self.browser.current_url\n    self.assertRegex(edith_list_url, \"/lists/.+\")  # <1>\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> `assertRegex` is a helper function from `unittest`\n    that checks whether a string matches a regular expression.\n    We use it to check that our new REST-ish design has been implemented.\n    Find out more in the https://docs.python.org/3/library/unittest.html[`unittest` documentation].\n    (((\"assertRegex\")))\n    (((\"unittest module\", \"documentation\")))\n\n\nNext, we imagine a new user coming along.\nWe want to check that they don't see any of Edith's items\nwhen they visit the home page,\nand that they get their own unique URL for their list:\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch07l006)\n====\n[source,python]\n----\n    [...]\n    self.assertRegex(edith_list_url, \"/lists/.+\")\n\n    # Now a new user, Francis, comes along to the site.\n\n    ## We delete all the browser's cookies\n    ## as a way of simulating a brand new user session  # <1>\n    self.browser.delete_all_cookies()\n\n    # Francis visits the home page.  There is no sign of Edith's\n    # list\n    self.browser.get(self.live_server_url)\n    page_text = self.browser.find_element(By.TAG_NAME, \"body\").text\n    self.assertNotIn(\"Buy peacock feathers\", page_text)\n\n    # Francis starts a new list by entering a new item. He\n    # is less interesting than Edith...\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    inputbox.send_keys(\"Buy milk\")\n    inputbox.send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Buy milk\")\n\n    # Francis gets his own unique URL\n    francis_list_url = self.browser.current_url\n    self.assertRegex(francis_list_url, \"/lists/.+\")\n    self.assertNotEqual(francis_list_url, edith_list_url)\n\n    # Again, there is no trace of Edith's list\n    page_text = self.browser.find_element(By.TAG_NAME, \"body\").text\n    self.assertNotIn(\"Buy peacock feathers\", page_text)\n    self.assertIn(\"Buy milk\", page_text)\n\n    # Satisfied, they both go back to sleep\n----\n====\n\n<1> I'm using the convention of double-hashes (`##`)\n    to indicate \"meta-comments\"&mdash;comments\n    about _how_ the test is working and why--so that\n    we can distinguish them from regular comments in FTs,\n    which explain the user story.\n    They're a message to our future selves,\n    which might otherwise be wondering why we're\n    faffing about deleting cookies...\n    (((\"double-hashes (&#x23;&#x23;)\")))\n    (((\"&#x23;&#x23; (double-hashes)\")))\n    (((\"meta-comments\")))\n\n\nOther than that, the new test is fairly self-explanatory.\nLet's see how we do when we run our FTs:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\n.F\n======================================================================\nFAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t\nests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)\n\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 77, in\ntest_multiple_users_can_start_lists_at_different_urls\n    self.assertRegex(edith_list_url, \"/lists/.+\")\nAssertionError: Regex didn't match: '/lists/.+' not found in\n'http://localhost:8081/'\n\n ---------------------------------------------------------------------\nRan 2 tests in 5.786s\n\nFAILED (failures=1)\n----\n\n(((\"\", startref=\"regression07\")))\n(((\"\", startref=\"MLTregression07\")))\nGood, our first test still passes,\nand the second one fails where we might expect.\nLet's do a commit, and then go and build some new models and views:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -a*\n----\n\n\n=== Iterating Towards the New Design\n\n(((\"multiple lists testing\", \"iterative development style\")))\n(((\"iterative development style\")))\nBeing all excited about our new design,\nI had an overwhelming urge to dive in at this point\nand start changing _models.py_,\nwhich would have broken half the unit tests,\nand then pile in and change almost every single line of code,\nall in one go.\nThat's a natural urge,\nand TDD, as a discipline, is a constant fight against it.\nObey the Testing Goat, not Refactoring Cat!\nWe don't need to implement our new, shiny design in a single big bang.\nLet's make small changes\nthat take us from a working state to a working state,\nwith our design guiding us gently at each stage.\n\nThere are four items on our to-do list.\nThe FT, with its `Regex didn't match` error,\nis suggesting to us that the second item--giving lists their own URL\nand identifier--is the one we should work on next.\nLet's have a go at fixing that, and only that.\n\n[role=\"pagebreak-before\"]\nThe URL comes from the redirect after POST.\nIn _lists/tests.py_, let's find `test_redirects_after_POST`\nand change the expected redirect location:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l007)\n====\n[source,python]\n----\ndef test_redirects_after_POST(self):\n    response = self.client.post(\"/\", data={\"item_text\": \"A new list item\"})\n    self.assertRedirects(response, \"/lists/the-only-list-in-the-world/\")\n----\n====\n\nDoes that seem slightly strange?\nClearly, _/lists/the-only-list-in-the-world_ isn't a URL\nthat's going to feature in the final design of our application.\nBut we're committed to changing one thing at a time.\nWhile our application only supports one list,\nthis is the only URL that makes sense.\nWe're still moving forwards,\nin that we'll have a different URL for our list and our home page,\nwhich is a step along the way to a more REST-ful design.\nLater, when we have multiple lists, it will be easy to change.\n\n// SEBASTIAN: Yet another mantra that also fits TDD and here is \"fake it till you make it\"\n//      Perhaps worth mentioning here to explain in advance how this helps making small steps\n//      to eventually detect the trick using simply more / other tests\n\nNOTE: Another way of thinking about it\n    is as a problem-solving [keep-together]#technique#:\n    our new URL design is currently not implemented,\n    so it works for zero items.\n    Ultimately, we want to solve for _n_ items,\n    but solving for one item is a good step along the way.\n\nRunning the unit tests gives us an expected fail:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nAssertionError: '/' != '/lists/the-only-list-in-the-world/'\n- /\n+ /lists/the-only-list-in-the-world/\n : Response redirected to '/', expected '/lists/the-only-list-in-the-world/':\nExpected '/' to equal '/lists/the-only-list-in-the-world/'.\n----\n\nWe can go adjust our `home_page` view in 'lists/views.py':\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l008)\n====\n[source,python]\n----\ndef home_page(request):\n    if request.method == \"POST\":\n        Item.objects.create(text=request.POST[\"item_text\"])\n        return redirect(\"/lists/the-only-list-in-the-world/\")\n\n    items = Item.objects.all()\n    return render(request, \"home.html\", {\"items\": items})\n----\n====\n\n\nDjango's unit test runner picks up on the fact that this\nis not a real URL yet:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nAssertionError: 404 != 200 : Couldn't retrieve redirection page\n'/lists/the-only-list-in-the-world/': response code was 404 (expected 200)\n----\n\n\n\n=== Taking a First, Self-Contained Step: One New URL\n\n(((\"URL mappings\", id=\"url07\")))\nOur singleton list URL doesn't exist yet.\nWe fix that in _superlists/urls.py_:\n\n\n[role=\"sourcecode small-code\"]\n.superlists/urls.py (ch07l009)\n====\n[source,python]\n----\nfrom django.urls import path\nfrom lists import views\n\nurlpatterns = [\n    path(\"\", views.home_page, name=\"home\"),\n    path(\"lists/the-only-list-in-the-world/\", views.home_page, name=\"view_list\"),  # <1>\n]\n----\n====\n\n<1> We'll just point our new URL at the existing home page view.\n    This is the minimal change.\n\nTIP: Watch out for trailing slashes in URLs,\n    both here in _urls.py_ and in the tests.\n    They're a common source of confusion:\n    Django will return a 301 redirect rather than a 404\n    if you try to access a URL that's missing its trailing slash.footnote:[\n    The setting that controls this is called https://docs.djangoproject.com/en/5.2/ref/settings/#append-slash[`APPEND_SLASH`].\n    ]\n    (((\"troubleshooting\", \"URL mappings\")))\n\n\nThat gets our unit tests passing:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nOK\n----\n\nWhat do the FTs think?\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\nAssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do\nlist\\n1: Buy peacock feathers'\n----\n\nGood, they get a little further along. We now confirm that we have a new URL,\nbut the actual page content is still the same;\nit shows the old list.\n\n\n==== Separating Out Our Home Page and List View Functionality\n\nWe now have two URLs,\nbut they're actually doing the exact same thing.\n(((\"home page and list view functionality, separating\")))\n(((\"list view functionality, separating from home page\")))\nUnder the hood, they're just pointing at the same function.\nContinuing to work incrementally,\nwe can start to break apart the responsibilities\nfor these two different URLs:\n\n* The home page only needs to display a static form,\n  and support creating a brand new list based on its first item.\n* The list view page needs to be able to display existing list items\n  and add new items to the list.\n\nLet's split out some tests for our new URL.\n\nOpen up 'lists/tests.py', and add a new test class called `ListViewTest`.\nThen:\n\n1. Copy across the `test_renders_input_form()` test from `HomePageTest`\n  into our new class.\n\n2. Move the method called `test_displays_all_list_items()`.\n\n3. In both, change just the URL that is invoked by `self.client.get()`.\n\n4. We _won't_ copy across the `test_uses_home_template()` yet,\n    as we're not quite sure what template we want to use.\n    We'll stick to the tests that check behaviour, rather than implementation.\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l010)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n    def test_renders_input_form(self):\n        [...]\n    def test_can_save_a_POST_request(self):\n        [...]\n    def test_redirects_after_POST(self):\n        [...]\n\n\nclass ListViewTest(TestCase):\n    def test_renders_input_form(self):\n        response = self.client.get(\"/lists/the-only-list-in-the-world/\")\n        self.assertContains(response, '<form method=\"POST\">')\n        self.assertContains(response, '<input name=\"item_text\"')\n\n    def test_displays_all_list_items(self):\n        Item.objects.create(text=\"itemey 1\")\n        Item.objects.create(text=\"itemey 2\")\n\n        response = self.client.get(\"/lists/the-only-list-in-the-world/\")\n\n        self.assertContains(response, \"itemey 1\")\n        self.assertContains(response, \"itemey 2\")\n----\n====\n\n[role=\"pagebreak-before\"]\nLet's try running these tests now:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\nOK\n----\n\nIt passes, because the URL is still pointing at the `home_page` view.\n\nLet's make it point at a new view:\n\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch07l011)\n====\n[source,python]\n----\nfrom django.urls import path\nfrom lists import views\n\nurlpatterns = [\n    path(\"\", views.home_page, name=\"home\"),\n    path(\"lists/the-only-list-in-the-world/\", views.view_list, name=\"view_list\"),\n]\n----\n====\n\nThat predictably fails because there is no such view function yet:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n    path(\"lists/the-only-list-in-the-world/\", views.view_list,\nname=\"view_list\"),\n                                              ^^^^^^^^^^^^^^^\nAttributeError: module 'lists.views' has no attribute 'view_list'\n----\n\n\n===== A new view function\n\nFair enough. Let's create a placeholder view function in _lists/views.py_:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l012-0)\n====\n[source,python]\n----\ndef view_list(request):\n    pass\n----\n====\n\nNot quite good enough:\n\n----\nValueError: The view lists.views.view_list didn't return an HttpResponse\nobject. It returned None instead.\n[...]\nFAILED (errors=3)\n----\n\nLooking for the minimal code change,\nlet's just make the view return our existing _home.html_ template,\nbut with nothing in it:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l012-1)\n====\n\n[source,python]\n----\ndef view_list(request):\n    return render(request, \"home.html\")\n----\n====\n\n[role=\"pagebreak-before\"]\nNow the tests guide us to making sure that our list view\nshows existing list items:\n\n[subs=\"specialcharacters\"]\n----\nFAIL: test_displays_all_list_items\n(lists.tests.ListViewTest.test_displays_all_list_items)\n[...]\nAssertionError: False is not true : Couldn't find 'itemey 1' in the following\nresponse\n----\n\nSo let's copy the last two lines from `home_page`  more directly:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l012)\n====\n[source,python]\n----\ndef view_list(request):\n    items = Item.objects.all()\n    return render(request, \"home.html\", {\"items\": items})\n----\n====\n\nThat gets us to passing unit tests!\n\n----\nRan 8 tests in 0.035s\n\nOK\n----\n\n\n==== The FTs Detect a Regression\n\nAs always when we get to passing unit tests,\nwe run the FTs to check how things are doing\n\"in real life\":\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\nFF\n======================================================================\nFAIL: test_can_start_a_todo_list\n(functional_tests.tests.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 62, in\ntest_can_start_a_todo_list\n[...]\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers']\n\n======================================================================\nFAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t\nests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 89, in\ntest_multiple_users_can_start_lists_at_different_urls\n    self.assertNotIn(\"Buy peacock feathers\", page_text)\nAssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do\nlist\\n1: Buy peacock feathers'\n----\n\n\n.Another Race Condition Example\n*******************************************************************************\nYou may have noticed that the assertions(((\"race conditions\"))) around line 63 are in a slightly\nunexpected order:\n\n[role=\"sourcecode currentcontents\"]\n.functional_tests/tests.py\n====\n[source,python]\n----\n    # The page updates again, and now shows both items on her list\n    self.wait_for_row_in_list_table(\"2: Use peacock feathers to make a fly\")\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n----\n====\n\nTry putting them the other way around, 1 then 2, and run the FTs a few times.\nThere's a good chance you'll notice an inconsistency in the results.(((\"assertions\", \"race conditions and\")))\nSometimes you see:\n\n[role=\"skipme\"]\n----\nAssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock\nfeathers to make a fly']\n----\n\nAnd sometimes you'll see:\n\n\n[role=\"skipme\"]\n----\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers']\n----\n\nThat's because of a race condition between the Selenium assertions in the FT,\nand the server returning our new page.\nJust before we tap Enter,\nthe page is still showing `1: Buy peacock feathers`.\nOur next assertion is then checking for `1: Buy peacock feathers`,\nwhich is already on the page.\nBut, at the same time, the server is busy returning a new page that also says\n`1: Use peacock feathers to make a fly`.\n\nSo, depending on who gets there first,\nthe first assert may pass or fail,\nmeaning that you may get an error on the first assert or on the second.\n\nThat's why I put the assertions \"backwards\",\nso we check for `2: Use peacock feathers` _first_,\nbecause it should _never_ be present on the old page. This means that as soon as we detect it, we must be on the new page.\n\nSubtle, right?  Selenium tests are fiddly like that.\n\n*******************************************************************************\n\n\nNot only is our new FT failing, but the old one is too.\nThat tells us we've introduced a _regression_.  But what?\n\n(((\"debugging\", \"of functional tests\", secondary-sortas=\"functional\")))\n(((\"functional tests (FTs)\", \"debugging techniques\")))\n(((\"POST requests\", \"debugging\")))\n(((\"HTML\", \"POST requests\", \"debugging\")))\nBoth tests are failing when we try to add the second item.\nWe have to put our debugging hats on here.\nWe know the home page is working, because the test has got all\nthe way down to line 62 in the first FT,\nso we've at least added a first item.\nAnd our unit tests are all passing,\nso we're pretty sure the URLs and views that we _do_ have are doing what they should.\nLet's have a quick look at those unit tests to see what they [.keep-together]#tell us#:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *grep -E \"class|def\" lists/tests.py*\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n    def test_renders_input_form(self):\n    def test_can_save_a_POST_request(self):\n    def test_redirects_after_POST(self):\n    def test_only_saves_items_when_necessary(self):\nclass ListViewTest(TestCase):\n    def test_renders_input_form(self):\n    def test_displays_all_list_items(self):\nclass ItemModelTest(TestCase):\n    def test_saving_and_retrieving_items(self):\n----\n\nThe home page displays the right form and template, and can handle POST requests,\nand the _/only-list-in-the-world/_ view knows how to display all items...but it doesn't know how to handle POST requests.\nAh, that gives us a clue.\n\nA second clue is the rule of thumb that,\nwhen all the unit tests are passing but the FTs aren't,\nit's often pointing at a problem in code that's not covered by the unit tests—and in a Django app, that's often a template problem.\n\nNOTE: Have you figured out what the problem is?\n    Why not spend a moment trying to figure it out?\n    Maybe open up the site in your browser,\n    and see where the bug manifests.\n    Perhaps open up the \"view source\" or browser DevTools\n    and look at the underlying HTML?\n\n\nThe answer is that our _home.html_ input form\ncurrently doesn't specify an explicit URL to POST to:\n\n[role=\"sourcecode currentcontents\"]\n.lists/templates/home.html\n====\n[source,html]\n----\n        <form method=\"POST\">\n----\n====\n\nBy default, the browser sends the POST data back to the same URL it's currently\non. When we're on the home page that works fine,\nbut when we're on our _/only-list-in-the-world/_ page, it doesn't.\n\n\n==== Getting Back to a Working State as Quickly as Possible\n\nNow, we could dive in and add POST request handling to our new view,\nbut that would involve writing a bunch more tests and code,\nand at this point we'd like to get back to a working state as quickly as possible.\nActually the _quickest_ thing we can do to get things fixed\nis to just use the existing home page view, which already works,\nfor all POST requests.\n\nIn other words, we've identified a new important part of the\nbehaviour we want from our two views and their templates,\nwhich is the URL that the form points to.\nLet's add a check for that URL explicitly,\nin our two tests for each view\n(I'll use a diff to show the changes,\nhopefully that makes it nice and clear):\n\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l013-1)\n====\n[source,diff]\n----\n@@ -10,7 +10,7 @@ class HomePageTest(TestCase):\n\n     def test_renders_input_form(self):\n         response = self.client.get(\"/\")\n-        self.assertContains(response, '<form method=\"POST\">')\n+        self.assertContains(response, '<form method=\"POST\" action=\"/\">')\n         self.assertContains(response, '<input name=\"item_text\"')\n\n     def test_can_save_a_POST_request(self):\n@@ -31,7 +31,7 @@ class HomePageTest(TestCase):\n class ListViewTest(TestCase):\n     def test_renders_input_form(self):\n         response = self.client.get(\"/lists/the-only-list-in-the-world/\")\n-        self.assertContains(response, '<form method=\"POST\">')\n+        self.assertContains(response, '<form method=\"POST\" action=\"/\">')\n         self.assertContains(response, '<input name=\"item_text\"')\n\n     def test_displays_all_list_items(self):\n----\n====\n\n\nThat gives us two expected failures:\n\n\n----\n======================================================================\nFAIL: test_renders_input_form\n(lists.tests.HomePageTest.test_renders_input_form)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/lists/tests.py\", line 13, in test_renders_input_form\n    self.assertContains(response, '<form method=\"POST\" action=\"/\">')\n    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/\">' in the following response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n<h1>Your To-Do list</h1>\\n    <form method=\"POST\">\\n      <input\nname=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\\n\n<input type=\"hidden\" name=\"csrfmiddlewaretoken\"\nvalue=[...]\n</form>\\n    <table id=\"id_list_table\">\\n      \\n    </table>\\n\n</body>\\n</html>\\n'\n\n======================================================================\nFAIL: test_renders_input_form\n(lists.tests.ListViewTest.test_renders_input_form)\n[...]\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/\">' in the following response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n[...]\n----\n\nAnd so we can fix it like this—the input form, for now, will always point at the home URL:\n\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch07l013-2)\n====\n[source,html]\n----\n    <form method=\"POST\" action=\"/\">\n----\n====\n\nUnit test pass:\n\n----\nOK\n----\n\nAnd we should see our FTs get back to a happier place:\n\n[subs=\"specialcharacters,macros\"]\n----\nFAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t\nests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)\n[...]\nAssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do\nlist\\n1: Buy peacock feathers'\n\nRan 2 tests in 8.541s\nFAILED (failures=1)\n----\n\nOur old FT (the one we're using as a regression test) passes once again,\nso we know we're back to a working state.\nThe new functionality may not be working yet,\nbut at least the old stuff works as well as it used to.\n(((\"\", startref=\"url07\")))\n\n\n==== Green? Refactor\n\n(((\"multiple lists testing\", \"refactoring\")))\n(((\"refactoring\")))\n(((\"Red/Green/Refactor\")))\nTime for a little tidying up.\n\nIn the red/green/refactor dance, our unit tests pass\nand all our old FTs pass, so we've arrived at green.\nThat means it's time to see if anything needs a refactor.\n\nWe now have two views: one for the home page,\nand one for an individual list.\nBoth are currently using the same template,\nand passing it all the list items currently in the database.\nPost requests are only handled by the home page though.\n\nIt feels like the responsibilities of our two views are a little tangled up.\nLet's try and disentangle them.\n\n[role=\"pagebreak-before less_space\"]\n=== Another Small Step: A Separate Template [.keep-together]#for Viewing Lists#\n\n(((\"multiple lists testing\", \"separate list viewing templates\", id=\"MLTseparate07\")))\n(((\"templates\", \"separate list viewing templates\", id=\"TMPseparate07\")))\nAs the home page and the list view are now quite distinct pages,\nthey should be using different HTML templates; _home.html_ can have the\nsingle input box, whereas a new template, _list.html_, can take care\nof showing the table of existing items.\n\nWe held off on copying across `test_uses_home_template()` until now,\nbecause we weren't quite sure what we wanted.\nNow let's add an explicit test to say\nthat this view uses a different template:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l014)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        response = self.client.get(\"/lists/the-only-list-in-the-world/\")\n        self.assertTemplateUsed(response, \"list.html\")\n\n    def test_renders_input_form(self):\n        [...]\n\n    def test_displays_all_list_items(self):\n        [...]\n----\n====\n\n// DAVID: FWIW I don't think this test adds value, it's an internal detail. We're refactoring anyway\n// so we would expect not to have to change tests - because we don't want tests to be overly coupled\n// to the way our code is factored anyway.\n\nLet's see what it says:\n\n----\nAssertionError: False is not true : Template 'list.html' was not a template\nused to render the response. Actual template(s) used: home.html\n----\n\nLooks about right, let's change the view:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l015)\n====\n[source,python]\n----\ndef view_list(request):\n    items = Item.objects.all()\n    return render(request, \"list.html\", {\"items\": items})\n----\n====\n\nBut, obviously, that template doesn't exist yet. If we run the unit tests, we\nget:\n\n----\ndjango.template.exceptions.TemplateDoesNotExist: list.html\n[...]\nFAILED (errors=4)\n----\n\nLet's create a new file at 'lists/templates/list.html':\n\n//16\n[subs=\"specialcharacters,quotes\"]\n----\n$ *touch lists/templates/list.html*\n----\n\n\nA blank template, which gives us two errors--good to know the tests are\nthere to make sure we fill it in:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n======================================================================\nFAIL: test_displays_all_list_items\n(lists.tests.ListViewTest.test_displays_all_list_items)\n ---------------------------------------------------------------------\n[...]\nAssertionError: False is not true : Couldn't find 'itemey 1' in the following\nresponse\nb''\n\n======================================================================\nFAIL: test_renders_input_form\n(lists.tests.ListViewTest.test_renders_input_form)\n----------------------------------------------------------------------\n[...]\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/\">' in the following response\n[...]\n----\n\nThe template for an individual list will reuse quite a lot of the stuff\nwe currently have in _home.html_, so we can start by just copying that:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cp lists/templates/home.html lists/templates/list.html*\n----\n//17\n\nThat gets the tests back to passing (green).\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nOK\n----\n\n\nNow let's do a little more tidying up (refactoring).\nWe said the home page doesn't need to list items;\nit only needs the new list input field.\nSo we can remove some lines from _lists/templates/home.html_,\nand maybe slightly tweak the `h1` to say \"Start a new To-Do list\".\n\n\nI'll present the code change as a diff again,\nas I think that shows nice and clearly what we need to modify:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/home.html (ch07l018)\n====\n[source,diff]\n----\n   <body>\n-    <h1>Your To-Do list</h1>\n+    <h1>Start a new To-Do list</h1>\n     <form method=\"POST\" action=\"/\">\n       <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n       {% csrf_token %}\n     </form>\n-    <table id=\"id_list_table\">\n-      {% for item in items %}\n-        <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n-      {% endfor %}\n-    </table>\n   </body>\n----\n====\n\nWe rerun the unit tests to check that hasn't broken anything...\n\n----\nOK\n----\n\nGood.\n\nNow there's actually no need to pass all the items to the _home.html_ template\nin our `home_page` view, so we can simplify that and delete a few lines:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l019)\n====\n[source,diff]\n----\n     if request.method == \"POST\":\n         Item.objects.create(text=request.POST[\"item_text\"])\n         return redirect(\"/lists/the-only-list-in-the-world/\")\n-\n-    items = Item.objects.all()\n-    return render(request, \"home.html\", {\"items\": items})\n+    return render(request, \"home.html\")\n----\n====\n\nRerun the unit tests once more; they still pass:\n\n----\nOK\n----\n\nTime to run the FTs:\n\n----\n  File \"...goat-book/functional_tests/tests.py\", line 96, in\ntest_multiple_users_can_start_lists_at_different_urls\n    self.wait_for_row_in_list_table(\"1: Buy milk\")\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\n[...]\nAssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy\nmilk']\n\n----------------------------------------------------------------------\nRan 2 tests in 10.606s\n\nFAILED (failures=1)\n----\n\nGreat!  Only one failure, so we know our regression test (the first FT)\nis passing.\nLet's see where we're getting to with the new FT.\n\n[role=\"pagebreak-before\"]\nLet's take a look at it again:\n\n\n[role=\"sourcecode currentcontents\"]\n.functional_tests/tests.py\n====\n[source,python]\n----\n    def test_multiple_users_can_start_lists_at_different_urls(self):\n        # Edith starts a new to-do list\n        self.browser.get(self.live_server_url)\n        inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n        inputbox.send_keys(\"Buy peacock feathers\")\n        inputbox.send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")  # <1>\n        [...]\n\n        # Now a new user, Francis, comes along to the site.\n        [...]\n\n        # Francis visits the home page.  There is no sign of Edith's\n        # list\n        self.browser.get(self.live_server_url)\n        page_text = self.browser.find_element(By.TAG_NAME, \"body\").text\n        self.assertNotIn(\"Buy peacock feathers\", page_text)  # <2>\n\n        # Francis starts a new list by entering a new item. He\n        # is less interesting than Edith...\n        inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n        inputbox.send_keys(\"Buy milk\")\n        inputbox.send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(\"1: Buy milk\")   # <3>\n        [...]\n----\n====\n\n<1> Edith's list says \"Buy peacock feathers\".\n<2> When Francis loads the home page, there's no sign of Edith's list.\n<3> (This is the line where our test fails.) When Francis adds a new item,\n    he sees Edith's item as number 1, and his appears as number 2.\n\nStill, that's progress!  The new FT _is_ getting a little further along.\n\n(((\"\", startref=\"MLTseparate07\")))\n(((\"\", startref=\"TMPseparate07\")))\nIt may feel like we haven't made much headway because,\nfunctionally, the site still behaves almost exactly like it did\nwhen we started the chapter.\nBut this really _is_ progress.\nWe've started on the road to our new design,\nand we've implemented a number of stepping stones\n_without making anything worse than it was before_.\n\n[role=\"pagebreak-before\"]\nLet's commit our work so far:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # should show 4 changed files and 1 new file, list.html\n$ *git add lists/templates/list.html*\n$ *git diff* # should show we've simplified home.html,\n           # moved one test to a new class in lists/tests.py,\n           # changed the redirect in homepageTest & the home_page() view\n           # added a new view view_list() in views.py,\n           # and and added a line to urls.py.\n$ *git commit -a* # add a message summarising the above, maybe something like\n                # \"new URL, view and template to display lists\"\n----\n\nNOTE: If this is all feeling a little abstract,\n  now might be a good time to load up the site with `manage.py runserver`\n  and try adding a couple of different lists yourself,\n  and get a feel for how the site is currently behaving.\n\n\n=== A Third Small Step: A New URL for Adding List Items\n\n(((\"multiple lists testing\", \"list item URLs\", id=\"MLTlist07\")))\n(((\"URL mappings\", id=\"urlmap07a\")))\nWhere are we with our own to-do list?\n\n\n[role=\"scratchpad\"]\n*****\n* 'Adjust model so that items are associated with different lists.'\n* 'Add unique URLs for each list.'\n* 'Add a URL for creating a new list via POST.'\n* 'Add URLs for adding a new item to an existing list via POST.'\n*****\n\n\nWe've _sort of_ made progress on the second item,\neven if there's still only one list in the world.\nThe first item is a bit scary.\nCan we do something about items 3 or 4?\n\nLet's have a new URL for adding new list items at _/lists/new_:\nIf nothing else, it'll simplify the home page view.\n\n[role=\"pagebreak-before less_space\"]\n==== A Test Class for New List Creation\n\nOpen up _lists/tests.py_, and _move_ the\n`test_can_save_a_POST_request()` and `test_redirects_after_POST()` methods\ninto a new class called `NewListTest`. (((\"lists\", \"creating, test class for\")))Then, change the URL they POST to:\n\n// TODO: handwave about test_only_saves_items_when_necessary()\n\n[role=\"sourcecode small-code\"]\n.lists/tests.py (ch07l020)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n    def test_renders_input_form(self):\n        [...]\n    def test_only_saves_items_when_necessary(self):\n        [...]\n\n\nclass NewListTest(TestCase):\n    def test_can_save_a_POST_request(self):\n        self.client.post(\"/lists/new\", data={\"item_text\": \"A new list item\"})\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.get()\n        self.assertEqual(new_item.text, \"A new list item\")\n\n    def test_redirects_after_POST(self):\n        response = self.client.post(\"/lists/new\", data={\"item_text\": \"A new list item\"})\n        self.assertRedirects(response, \"/lists/the-only-list-in-the-world/\")\n\n\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n----\n====\n\n\n// TODO: sneaky change from .first() to .get() here,\n// should grandfather in to chap 5.\n\nTIP: This is another place to pay attention to trailing slashes, incidentally.\n    It's `/lists/new`, with no trailing slash.\n    The convention I'm using is that\n    URLs without a trailing slash are \"action\" URLs, which modify the database.footnote:[\nI don't think this is a very common convention anymore these days,\nbut I quite like it.\nBy all means, cast around for a URL naming scheme that makes sense to you\nin your own projects!]\n\nTry running that:\n\n----\n    self.assertEqual(Item.objects.count(), 1)\nAssertionError: 0 != 1\n[...]\n    self.assertRedirects(response, \"/lists/the-only-list-in-the-world/\")\n[...]\nAssertionError: 404 != 302 : Response didn't redirect as expected: Response\ncode was 404 (expected 302)\n----\n\nThe first failure tells us we're not saving a new item to the database,\nand the second says that, instead of returning a 302 redirect,\nour view is returning a 404.\nThat's because we haven't built a URL for _/lists/new_,\nso the `client.post` is just getting a \"not found\" response.\n\nNOTE: Do you remember how we split this out into two tests earlier?\n    If we only had one test that checked both the saving and the redirect,\n    it would have failed on the `0 != 1` failure,\n    which would have been much harder to debug.\n    Ask me how I know this.\n\n\n==== A URL and View for New List Creation\n\n\nLet's build our new (((\"URL mappings\", \"URL for new list creation\")))(((\"lists\", \"URL and view for new list creation\")))URL now:\n\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch07l021)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"\", views.home_page, name=\"home\"),\n    path(\"lists/new\", views.new_list, name=\"new_list\"),\n    path(\"lists/the-only-list-in-the-world/\", views.view_list, name=\"view_list\"),\n]\n----\n====\n\nNext we get a `no attribute 'new_list'`, so let's fix that, in\n'lists/views.py':\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l022)\n====\n[source,python]\n----\ndef new_list(request):\n    pass\n----\n====\n\nThen we get \"The view lists.views.new_list didn't return an HttpResponse\nobject\".  (This is getting rather familiar!)  We could return a raw\n`HttpResponse`, but because we know we'll need a redirect, let's borrow a line\nfrom `home_page`:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l023)\n====\n[source,python]\n----\ndef new_list(request):\n    return redirect(\"/lists/the-only-list-in-the-world/\")\n----\n====\n\nThat gives:\n\n----\n    self.assertEqual(Item.objects.count(), 1)\nAssertionError: 0 != 1\n----\n\n[role=\"pagebreak-before\"]\nSeems reasonably straightforward.\nWe borrow another line from `home_page`:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l024)\n====\n[source,python]\n----\ndef new_list(request):\n    Item.objects.create(text=request.POST[\"item_text\"])\n    return redirect(\"/lists/the-only-list-in-the-world/\")\n----\n====\n\nAnd everything now passes:\n\n----\nRan 9 tests in 0.030s\n\nOK\n----\n\n\nAnd we can run the FTs to check that we're still in the same place:\n\n\n----\n[...]\nAssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy\nmilk']\nRan 2 tests in 8.972s\nFAILED (failures=1)\n----\n\nOur regression test passes, and the new FT gets to the same point.\n\n==== Removing Now-Redundant Code and Tests\n\n\nWe're looking good.\nAs our new views are now doing most of the work that `home_page` used to do,\nwe should be able to massively simplify it.\nCan we remove the whole `if request.method == 'POST'` section,\nfor example?\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l025)\n====\n[source,python]\n----\ndef home_page(request):\n    return render(request, \"home.html\")\n----\n====\n//24\n\nYep! The unit tests pass:\n\n----\nOK\n----\n\nAnd while we're at it, we can remove the now-redundant\npass:[<code>test_only_saves_&#x200b;items_when_necessary</code>] test too!\n\nDoesn't that feel good?  The view functions are looking much simpler. We rerun\nthe tests to make sure...\n\n[role=\"dofirst-ch07l025-2\"]\n----\nRan 8 tests in 0.016s\nOK\n----\n\nAnd the FTs?\n\n[role=\"pagebreak-before less_space\"]\n==== A Regression! Pointing Our Forms at the New URL\n\nOops. When we run the FTs:\n\n----\n======================================================================\nERROR: test_can_start_a_todo_list\n(functional_tests.tests.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\n[...]\n  File \"...goat-book/functional_tests/tests.py\", line 52, in\ntest_can_start_a_todo_list\n[...]\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^\n[...]\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; For documentation [...]\n\n\n======================================================================\nERROR: test_multiple_users_can_start_lists_at_different_urls (functional_tests.\ntests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)\n ---------------------------------------------------------------------\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; For documentation [...]\n[...]\n\nRan 2 tests in 11.592s\nFAILED (errors=2)\n----\n\n\nOnce again, the FTs pick up a tricky little bug,\nsomething that our unit tests alone would find it hard to catch.\n\n[role=\"pagebreak-before less_space\"]\n==== Debugging in DevTools\n\nThis is another good time to spin up the dev server,\nand have a look around with a browser.(((\"debugging\", \"in DevTools\", secondary-sortas=\"DevTools\")))(((\"DevTools (developer tools)\", \"debugging in\")))\n\nLet's also open up https://firefox-source-docs.mozilla.org/devtools-user[DevTools],footnote:[\nIf you've not seen it before, DevTools is short for \"developer tools\".\nThey're tools that Firefox (and other browsers) give you to be able to look\n\"under the hood\" and see what's going on with web pages,\nincluding the source code, what network requests are being made,\nand what JavaScript is doing.\nYou can open up DevTools with Ctrl+Shift+I or Cmd-Opt-I.]\nand click around to see what's going on:\n\n* First I tried submitting a new list item,\n  and saw we get sent back to the home page.\n\n* Then I did the same with the browser DevTools open,\n  and in the \"network\" tab I saw a POST request to \"/\".\n  See <<post-in-dev-tools>>.\n\n* Finally, I had a look at the HTML source of the home page,\n  and saw that the main form is still pointing at \"/\".\n\n\n[[post-in-dev-tools]]\n.DevTools shows a POST request to /\nimage::images/tdd3_0703.png[\"screenshot of browser dev tools with a POST request in the network tab, with the File column showing /\"]\n\n\nActually, _both_ our forms are still pointing to the old URL.\nWe have tests for this!\nLet's amend them:\n\n\n[role=\"sourcecode small-code\"]\n.lists/tests.py (ch07l026)\n====\n[source,diff]\n----\n@@ -10,7 +10,7 @@ class HomePageTest(TestCase):\n\n     def test_renders_input_form(self):\n         response = self.client.get(\"/\")\n-        self.assertContains(response, '<form method=\"POST\" action=\"/\">')\n+        self.assertContains(response, '<form method=\"POST\" action=\"/lists/new\">')\n         self.assertContains(response, '<input name=\"item_text\"')\n\n\n@@ -33,7 +33,7 @@ class ListViewTest(TestCase):\n\n     def test_renders_input_form(self):\n         response = self.client.get(\"/lists/the-only-list-in-the-world/\")\n-        self.assertContains(response, '<form method=\"POST\" action=\"/\">')\n+        self.assertContains(response, '<form method=\"POST\" action=\"/lists/new\">')\n         self.assertContains(response, '<input name=\"item_text\"')\n----\n====\n\nThat gets us two failures:\n\n----\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/lists/new\">' in the following response\n[...]\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/lists/new\">' in the following response\n[...]\n----\n\nIn _both_ _home.html_ and _list.html_, let's change them:\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch07l027)\n====\n[source,html]\n----\n    <form method=\"POST\" action=\"/lists/new\">\n----\n====\n\nAnd:\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch07l028)\n====\n[source,html]\n----\n    <form method=\"POST\" action=\"/lists/new\">\n----\n====\n\nAnd that should get us back to working again:\n\n----\nRan 8 tests in 0.006s\n\nOK\n----\n\n[role=\"pagebreak-before\"]\nAnd our FTs are still in the familiar \"Francis sees Edith's items\" place:\n\n----\nAssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy\nmilk']\n[...]\nFAILED (failures=1)\n----\n\nPerhaps this all seems quite pernickety,\nbut that's another nicely self-contained commit,\nin that we've made a bunch of changes to our URLs,\nour _views.py_ is looking much neater and tidier\nwith three very short view functions,\nand we're sure the application is still working as well as it was before.\nWe're getting good at this working-state-to-working-state malarkey!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # 5 changed files\n$ *git diff* # URLs for forms x2, new test + view with code moves, and new URL\n$ *git commit -a*\n----\n\n(((\"\", startref=\"MLTlist07\")))\n(((\"\", startref=\"urlmap07a\")))\nAnd we can cross out an item on the to-do list:\n\n[role=\"scratchpad\"]\n*****\n* 'Adjust model so that items are associated with different lists.'\n* 'Add unique URLs for each list.'\n* '[strikethrough line-through]#Add a URL for creating a new list via POST.#'\n* 'Add URLs for adding a new item to an existing list via POST.'\n*****\n\n[role=\"pagebreak-before less_space\"]\n=== Biting the Bullet: Adjusting Our Models\n\nEnough housekeeping with our URLs.\nIt's time to bite the bullet and change our models.\nLet's adjust the model unit test.\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l029)\n====\n[source,diff]\n----\n@@ -1,5 +1,5 @@\n from django.test import TestCase\n-from lists.models import Item\n+from lists.models import Item, List\n\n\n class HomePageTest(TestCase):\n@@ -35,20 +35,30 @@ class ListViewTest(TestCase):\n         self.assertContains(response, \"itemey 2\")\n\n\n-class ItemModelTest(TestCase):\n+class ListAndItemModelsTest(TestCase):\n     def test_saving_and_retrieving_items(self):\n+        mylist = List()\n+        mylist.save()\n+\n         first_item = Item()\n         first_item.text = \"The first (ever) list item\"\n+        first_item.list = mylist\n         first_item.save()\n\n         second_item = Item()\n         second_item.text = \"Item the second\"\n+        second_item.list = mylist\n         second_item.save()\n\n+        saved_list = List.objects.get()\n+        self.assertEqual(saved_list, mylist)\n+\n         saved_items = Item.objects.all()\n         self.assertEqual(saved_items.count(), 2)\n\n         first_saved_item = saved_items[0]\n         second_saved_item = saved_items[1]\n         self.assertEqual(first_saved_item.text, \"The first (ever) list item\")\n+        self.assertEqual(first_saved_item.list, mylist)\n         self.assertEqual(second_saved_item.text, \"Item the second\")\n+        self.assertEqual(second_saved_item.list, mylist)\n----\n====\n\nOnce again, this is a very verbose test,\nbecause I'm using it more as a demonstration of how the ORM works.\nWe'll shorten it later,footnote:[In <<rewrite-model-test>>, if you're curious.]\nbut for now, let's work through and see how things work.\n\nWe create a new `List` object\nand then we assign each item to it by setting it as its `.list` property.\nWe check that the list is properly saved,\nand we check that the two items have also saved their relationship to the list.\nYou'll also notice that we can compare list objects with each other directly\n(`saved_list` and `mylist`)&mdash;behind the scenes,\nthese will compare themselves by checking\nthat their primary key (the `.id` attribute) is the same.\n\nTime for another unit-test/code cycle.\n\nFor the first few iterations,\nrather than explicitly showing you what code to enter in between every test run,\nI'm only going to show you the expected error messages from running the tests.\nI'll let you figure out what each minimal code change should be, on your own.\n\nTIP: Need a hint?\n    Go back and take a look at the steps we took\n    to introduce the `Item` model in <<django_ORM_first_model>>.\n\nYour first error should be:\n\n[subs=\"specialcharacters,macros\"]\n----\nImportError: cannot import name 'List' from 'lists.models'\n----\n\nFix that, and then you should see:\n\n[role=\"dofirst-ch07l030\"]\n----\nAttributeError: 'List' object has no attribute 'save'\n----\n\nNext you should see:\n\n[role=\"dofirst-ch07l031\"]\n----\ndjango.db.utils.OperationalError: no such table: lists_list\n----\n\nSo, we run a `makemigrations`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  lists/migrations/0003_list.py\n    + Create model List\n----\n\nAnd then you should see:\n\n----\n    self.assertEqual(first_saved_item.list, mylist)\nAttributeError: 'Item' object has no attribute 'list'\n----\n\n\n[role=\"pagebreak-before less_space\"]\n==== A Foreign Key Relationship\n\nHow do we give our `Item` a list attribute?(((\"foreign keys\")))\nLet's just try naively making it like the `text` attribute\n(and here's your chance\nto see whether your solution so far looks like mine, by the way):\n\n\n[role=\"sourcecode\"]\n.lists/models.py (ch07l033)\n====\n[source,python]\n----\nfrom django.db import models\n\n\nclass List(models.Model):\n    pass\n\n\nclass Item(models.Model):\n    text = models.TextField(default=\"\")\n    list = models.TextField(default=\"\")\n----\n====\n\n\nAs usual, the tests tell us we need a migration:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\ndjango.db.utils.OperationalError: no such column: lists_item.list\n\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  lists/migrations/0004_item_list.py\n    + Add field list to item\n----\n\n\nLet's see what that gives us:\n\n----\nAssertionError: 'List object (1)' != <List: List object (1)>\n----\n\n\nWe're not quite there. Look closely at each side of the `!=`.\nDo you see the quotes (`'`)?\nDjango has only saved the string representation of the `List` object.\nTo save the relationship to the object itself,\nwe tell Django about the relationship between the two classes using a `ForeignKey`:\n\n[role=\"sourcecode\"]\n.lists/models.py (ch07l035)\n====\n[source,python]\n----\nclass Item(models.Model):\n    text = models.TextField(default=\"\")\n    list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)\n----\n====\n\n// DAVID: this provides None as a default, but the field is non-nullable. Consider adding\n// null=True too? Or else (and I would actually prefer this), don't provide a default\n// and get them to delete their database and remigrate. We don't really want Items\n// in the database that have no list.\n\n[role=\"pagebreak-before\"]\nThat'll need a migration too.  As the last one was a red herring, let's\ndelete it and replace it with a new one:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*rm lists/migrations/0004_item_list.py*]\n$ pass:quotes[*python manage.py makemigrations*]\nMigrations for 'lists':\n  lists/migrations/0004_item_list.py\n    + Add field list to item\n----\n//31\n\n\nWARNING: Deleting migrations is dangerous.\n    Now and again it's nice to do it to keep things tidy,\n    because we don't always get our models' code right on the first go!\n    But if you delete a migration that's already been applied to a database somewhere,\n    Django will be confused about what state it's in,\n    and won't be able to apply future migrations.\n    You should only do it when you're sure the migration hasn't been used.\n    A good rule of thumb is that you should never delete or modify\n    a migration that's already been committed to Git.\n\n\n==== Adjusting the Rest of the World to Our New Models\n\nBack in our tests, now what happens?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\nERROR: test_displays_all_list_items\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n[...]\nERROR: test_redirects_after_POST\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n[...]\nERROR: test_can_save_a_POST_request\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n\nRan 8 tests in 0.021s\n\nFAILED (errors=3)\n----\n\nOh dear!\n\nThere is some good news.\nAlthough it's hard to see, our model tests are passing.\nBut three of our view tests are failing nastily.\n\nThe cause is the new relationship we've introduced between ++Item++s and ++List++s,\nwhich requires each item to have a parent list,\nand which our old tests and code aren't prepared for.\n\n[role=\"pagebreak-before\"]\nStill, this is exactly why we have tests!\nLet's get them working again.\nThe easiest is the `ListViewTest`;\nwe just create a parent list for our two test items:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l038)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    [...]\n    def test_displays_all_list_items(self):\n        mylist = List.objects.create()\n        Item.objects.create(text=\"itemey 1\", list=mylist)\n        Item.objects.create(text=\"itemey 2\", list=mylist)\n----\n====\n\nThat gets us down to two failing tests,\nboth on tests that try to POST to our `new_list` view.\nDecode the tracebacks using our usual technique,\nworking back from error to line of test code to—buried in there somewhere—the line of our own code that caused the failure:\n\n[subs=\"specialcharacters,macros\"]\n----\n  File \"...goat-book/lists/tests.py\", line 25, in test_redirects_after_POST\n    response = self.client.post(\"/lists/new\", data={\"item_text\": \"A new list\nitem\"})\n[...]\n  File \"...goat-book/lists/views.py\", line 11, in new_list\n    Item.objects.create(text=request.POST[\"item_text\"])\n    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n[...]\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n----\n\nIt's when we try to create an item without a parent list.\nSo we make a similar change in the view:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l039)\n====\n[source,python]\n----\nfrom lists.models import Item, List\n[...]\n\ndef new_list(request):\n    nulist = List.objects.create()\n    Item.objects.create(text=request.POST[\"item_text\"], list=nulist)\n    return redirect(\"/lists/the-only-list-in-the-world/\")\n----\n====\n\nAnd that gets our tests passing again:footnote:[Are you wondering about the strange spelling of the \"nulist\" variable?\nOther options are \"list\", which would shadow the built-in `list()` function,\nand `new_list`, which would shadow the name of the function that contains it.\nOr `list_` with the trailing underscore, which I find a bit ugly,\nor `list1` or `listey` or `mylist`, but none are particularly satisfactory.]\n\n----\nRan 8 tests in 0.030s\n\nOK\n----\n\n(((\"Test-Driven Development (TDD)\", \"philosophy of\", \"working state to working state\")))\n(((\"working state to working state\")))\n(((\"Testing Goat\", \"working state to working state\")))\nAre you cringing internally at this point?\n_Arg! This feels so wrong;\nwe create a new list for every single new item submission,\nand we're still just displaying all items as if they belong to the same list!_\nI know; I feel the same.\nThe step-by-step approach,\nin which you go from working code to working code, is counterintuitive.\nI always feel like just diving in\nand trying to fix everything all in one go,\ninstead of going from one weird half-finished state to another.\nBut remember the Testing Goat!\nWhen you're up a mountain,\nyou want to think very carefully about where you put each foot,\nand take one step at a time, checking at each stage\nthat the place you've put it hasn't caused you to fall off a cliff.\n\nSo, just to reassure ourselves that things have worked, we rerun the FT:\n\n----\nAssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy\nmilk']\n[...]\n----\n\n\nSure enough, it gets all the way through to where we were before.\nWe haven't broken anything, and we've made a big change to the database.\nThat's something to be pleased with!\nLet's commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # 3 changed files, plus 2 migrations\n$ *git add lists*\n$ *git diff --staged*\n$ *git commit*\n----\n\nAnd we can cross out another item on the to-do list:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Adjust model so that items are associated with different lists.#'\n* 'Add unique URLs for each list.'\n* '[strikethrough line-through]#Add a URL for creating a new list via POST.#'\n* 'Add URLs for adding a new item to an existing list via POST.'\n*****\n\n[role=\"pagebreak-before less_space\"]\n=== Each List Should Have Its Own URL\n\nWe can get rid of the silly `the-only-list-in-the-world` URL,\nbut what shall we use as the unique identifier for our lists?\nProbably the simplest thing, for now,\nis just to use the autogenerated `id` field from the database.\nLet's change `ListViewTest` so that the two tests point at new URLs.\n\nWe'll also change the old `test_displays_all_list_items` test\nand call it `test_displays_only_items_for_that_list` instead,\nmaking it check that only the items for a specific list are displayed:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l040)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")  # <1>\n        self.assertTemplateUsed(response, \"list.html\")\n\n    def test_renders_input_form(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")  # <1>\n        self.assertContains(response, '<form method=\"POST\" action=\"/lists/new\">')\n        self.assertContains(response, '<input name=\"item_text\"')\n\n    def test_displays_only_items_for_that_list(self):\n        correct_list = List.objects.create()  # <2>\n        Item.objects.create(text=\"itemey 1\", list=correct_list)\n        Item.objects.create(text=\"itemey 2\", list=correct_list)\n        other_list = List.objects.create()  # <2>\n        Item.objects.create(text=\"other list item\", list=other_list)\n\n        response = self.client.get(f\"/lists/{correct_list.id}/\")  # <3>\n\n        self.assertContains(response, \"itemey 1\")\n        self.assertContains(response, \"itemey 2\")\n        self.assertNotContains(response, \"other list item\")  # <4>\n----\n====\n\n<1> Here's where we incorporate the ID of our new list into the GET URL.\n\n<2> In the \"Given\" phase of the test, we now set up two lists:\n    the one we're interested in and an extraneous one.\n\n<3> We change this URL too, to point at the 'correct' list.\n\n<4> And now, our \"Then\" section can check that the irrelevant list's\n    items are definitely not present.\n\n[role=\"pagebreak-before\"]\nRunning the unit tests gives the expected 404s and another related error:\n\n----\nFAIL: test_displays_only_items_for_that_list\nAssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404\n(expected 200)\n[...]\nFAIL: test_renders_input_form\nAssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404\n(expected 200)\n[...]\nFAIL: test_uses_list_template\nAssertionError: No templates used to render the response\n----\n\n\n==== Capturing Parameters from URLs\n\n\nIt's time to learn (((\"views\", \"passing URL parameters to\")))(((\"URLs\", \"parameters from, passing to views\")))how we can pass parameters from URLs to views:\n\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch07l041-0)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"\", views.home_page, name=\"home\"),\n    path(\"lists/new\", views.new_list, name=\"new_list\"),\n    path(\"lists/<int:list_id>/\", views.view_list, name=\"view_list\"),\n]\n----\n====\n\nWe adjust the path string for our URL to include a 'capture group',\n`<int:list_id>`, which will match any numerical characters, up to the following `/`.\nThe captured `id` will be passed to the view as an argument.\n\n\nIn other words, if we go to the URL '/lists/1/', `view_list` will get a second\nargument after the normal `request` argument, namely the integer `1`.\n\nBut our view doesn't expect an argument yet!\nSure enough, this causes problems:\n\n----\nERROR: test_displays_only_items_for_that_list\n[...]\nTypeError: view_list() got an unexpected keyword argument 'list_id'\n[...]\nERROR: test_renders_input_form\n[...]\nTypeError: view_list() got an unexpected keyword argument 'list_id'\n[...]\nERROR: test_uses_list_template\n[...]\nTypeError: view_list() got an unexpected keyword argument 'list_id'\n[...]\nFAIL: test_redirects_after_POST\n[...]\nAssertionError: 404 != 200 : Couldn't retrieve redirection page\n'/lists/the-only-list-in-the-world/': response code was 404 (expected 200)\n[...]\nFAILED (failures=1, errors=3)\n----\n\nWe can fix that easily with an unused parameter in _views.py_:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l041)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    [...]\n----\n====\n\nThat takes us down to our expected failure,\nplus something to do with an _/only-list-in-the-world/_\nthat's still hanging around somewhere,\nwhich I'm sure we can fix later.\n\n----\nFAIL: test_displays_only_items_for_that_list\n[...]\nAssertionError: 1 != 0 : 'other list item' unexpectedly found in the following\nresponse\n[...]\nFAIL: test_redirects_after_POST\nAssertionError: 404 != 200 : Couldn't retrieve redirection page\n'/lists/the-only-list-in-the-world/': response code was 404 (expected 200)\n----\n\nLet's make our list view discriminate\nover which items it sends to the template:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l042)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    items = Item.objects.filter(list=our_list)\n    return render(request, \"list.html\", {\"items\": items})\n----\n====\n\n\n==== Adjusting new_list to the New World\n\nIt's time to address the _/only-list-in-the-world/_ failure:\n\n----\nFAIL: test_redirects_after_POST\n[...]\nAssertionError: 404 != 200 : Couldn't retrieve redirection page\n'/lists/the-only-list-in-the-world/': response code was 404 (expected 200)\n----\n\nLet's have a little look and find the test that's moaning:\n\n\n[role=\"sourcecode currentcontents small-code\"]\n.lists/tests.py\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n\n    def test_redirects_after_POST(self):\n        response = self.client.post(\"/lists/new\", data={\"item_text\": \"A new list item\"})\n        self.assertRedirects(response, \"/lists/the-only-list-in-the-world/\")\n----\n====\n\nIt looks like it hasn't been adjusted to the new world of ++List++s and ++Item++s.\nThe test should be saying that this view redirects\nto the URL of the specific new list it just created.\n\n[role=\"sourcecode small-code\"]\n.lists/tests.py (ch07l043)\n====\n[source,python]\n----\n    def test_redirects_after_POST(self):\n        response = self.client.post(\"/lists/new\", data={\"item_text\": \"A new list item\"})\n        new_list = List.objects.get()\n        self.assertRedirects(response, f\"/lists/{new_list.id}/\")\n----\n====\n\nThe test still fails, but we can now take a look at the view itself,\nand change it so it redirects to the right place:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l044)\n====\n[source,python]\n----\ndef new_list(request):\n    nulist = List.objects.create()\n    Item.objects.create(text=request.POST[\"item_text\"], list=nulist)\n    return redirect(f\"/lists/{nulist.id}/\")\n----\n====\n\nThat gets us back to passing unit tests, phew!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test lists*]\n[...]\n........\n ---------------------------------------------------------------------\nRan 8 tests in 0.033s\n\nOK\n----\n\n\nWhat about the FTs?\n\n\n=== The Functional Tests Detect Another Regression\n\nIt feels like we're done with migrating to the new URL structure;\nwe must be almost there?\n\nWell, almost. When we run the FTs, we get:\n\n\n[subs=\"specialcharacters,macros\"]\n----\nF.\n======================================================================\nFAIL: test_can_start_a_todo_list\n(functional_tests.tests.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 62, in\ntest_can_start_a_todo_list\n    self.wait_for_row_in_list_table(\"2: Use peacock feathers to make a fly\")\n[...]\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use\npeacock feathers to make a fly']\n\n ---------------------------------------------------------------------\nRan 2 tests in 8.617s\n\nFAILED (failures=1)\n----\n\nOur _new_ FT is actually passing: different users can get different lists.\nBut the old test is warning us of a regression.\nIt looks like you can't add a second item to a list any more.\n\nIt's because of our quick-and-dirty hack\nwhere we create a new list for every single POST submission.\nThis is exactly what we have FTs for!\n\nAnd it correlates nicely with the last item on our to-do list:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Adjust model so that items are associated with different lists.#'\n* '[strikethrough line-through]#Add unique URLs for each list.#'\n* '[strikethrough line-through]#Add a URL for creating a new list via POST.#'\n* 'Add URLs for adding a new item to an existing list via POST.'\n*****\n\n\n=== One More URL to Handle Adding Items to an Existing List\n\nWe need a URL and view to handle adding a new item to an existing list\n(_/lists/<list_id>/add_item_).(((\"URLs\", \"URL to handle adding items to existing list\")))\nWe're starting to get used to these now,\nso we know we'll need:\n\n1. A new test for the new URL\n2. A new entry in _urls.py_\n3. A new view function\n\n[role=\"pagebreak-before\"]\nSo, let's see if we can knock all that together quickly:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l045)\n====\n[source,python]\n----\nclass NewItemTest(TestCase):\n    def test_can_save_a_POST_request_to_an_existing_list(self):\n        other_list = List.objects.create()\n        correct_list = List.objects.create()\n\n        self.client.post(\n            f\"/lists/{correct_list.id}/add_item\",\n            data={\"item_text\": \"A new item for an existing list\"},\n        )\n\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.get()\n        self.assertEqual(new_item.text, \"A new item for an existing list\")\n        self.assertEqual(new_item.list, correct_list)\n\n    def test_redirects_to_list_view(self):\n        other_list = List.objects.create()\n        correct_list = List.objects.create()\n\n        response = self.client.post(\n            f\"/lists/{correct_list.id}/add_item\",\n            data={\"item_text\": \"A new item for an existing list\"},\n        )\n\n        self.assertRedirects(response, f\"/lists/{correct_list.id}/\")\n----\n====\n\nNOTE: Are you wondering about `other_list`?\n    A bit like in the tests for viewing a specific list,\n    it's important that we add items to a specific list.\n    Adding this second object to the database prevents me from using a hack\n    like `List.objects.first()` in the view.\n    Yes, that would be a silly thing to do,\n    and you can go too far down the road of testing\n    for all the silly things you must not do\n    (there are an infinite number of those, after all).\n    It's a judgement call, but this one feels worth it.\n    There's some more discussion of this in <<testing-for-silliness>>.\n    Oh, and yes it's an unused variable, and your IDE might nag you about it,\n    but I find it helps me to remember what it's for.\n\n\nSo that fails as expected, the list item is not saved,\nand the new URL currently returns a 404:\n\n----\nAssertionError: 0 != 1\n[...]\nAssertionError: 404 != 302 : Response didn't redirect as expected: Response\ncode was 404 (expected 302)\n----\n\n\n\n==== The Last New urls.py Entry\n\nNow we've got our expected 404,\nlet's add a new URL for adding new items to existing lists:\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch07l046)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"\", views.home_page, name=\"home\"),\n    path(\"lists/new\", views.new_list, name=\"new_list\"),\n    path(\"lists/<int:list_id>/\", views.view_list, name=\"view_list\"),\n    path(\"lists/<int:list_id>/add_item\", views.add_item, name=\"add_item\"),\n]\n----\n====\n\nWe've got three very similar-looking URLs there.\nLet's make a note on our to-do list;\nthey look like good candidates for a refactoring:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Adjust model so that items are associated with different lists.#'\n* '[strikethrough line-through]#Add unique URLs for each list.#'\n* '[strikethrough line-through]#Add a URL for creating a new list via POST.#'\n* 'Add URLs for adding a new item to an existing list via POST.'\n* 'Refactor away some duplication in urls.py.'\n*****\n\n\n==== The Last New View\n\nBack to the tests, we get the usual missing module view objects:\n\n----\nAttributeError: module 'lists.views' has no attribute 'add_item'\n----\n\nLet's try:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l047)\n====\n[source,python]\n----\ndef add_item(request):\n    pass\n----\n====\n\n[role=\"pagebreak-before\"]\nAha:\n\n----\nTypeError: add_item() got an unexpected keyword argument 'list_id'\n----\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l048)\n====\n[source,python]\n----\ndef add_item(request, list_id):\n    pass\n----\n====\n\nAnd then:\n\n----\nValueError: The view lists.views.add_item didn't return an HttpResponse object.\nIt returned None instead.\n----\n\nWe can copy the `redirect()` from `new_list`\nand the `List.objects.get()` from `view_list`:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l049)\n====\n[source,python]\n----\ndef add_item(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    return redirect(f\"/lists/{our_list.id}/\")\n----\n====\n\nThat takes us to:\n\n----\n    self.assertEqual(Item.objects.count(), 1)\nAssertionError: 0 != 1\n----\n\nFinally, we make it save our new list item:\n\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l050)\n====\n[source,python]\n----\ndef add_item(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    Item.objects.create(text=request.POST[\"item_text\"], list=our_list)\n    return redirect(f\"/lists/{our_list.id}/\")\n----\n====\n\nAnd we're back to passing tests:\n\n\n\n----\nRan 10 tests in 0.050s\n\nOK\n----\n\n\nHooray!  Did that feel like quite a nice, fluid, unit-test/code cycle?\n\n\n[role=\"pagebreak-before less_space\"]\n==== Testing Template Context Directly\n\n(((\"templates\", \"testing template context directly\")))\nWe've got our new view and URL for adding items to existing lists;\nnow we just need to actually use it in our _list.html_ template.\nWe have a unit test for the form's action; let's amend it:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch07l051)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")\n        self.assertContains(\n            response,\n            f'<form method=\"POST\" action=\"/lists/{mylist.id}/add_item\">',\n        )\n        self.assertContains(response, '<input name=\"item_text\"')\n\n    def test_displays_only_items_for_that_list(self):\n        [...]\n----\n====\n\nThat fails as expected:\n\n----\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/lists/1/add_item\">' in the following response\n[...]\n----\n\nSo, we open it up to adjust the form tag...\n\n[role=\"sourcecode skipme\"]\n.lists/templates/list.html\n====\n[source,html]\n----\n    <form method=\"POST\" action=\"but what should we put here?\">\n----\n====\n\n...oh.\n\nTo get the URL to add to the current list,\nthe template needs to know what list it's rendering,\nas well as what the items are.\n\n[role=\"pagebreak-before\"]\n(((\"programming by wishful thinking\")))\nWell, \"programming by wishful thinking\",footnote:[\nTDD is a bit like programming by wishful thinking,\nin that, when we write the tests before the implementation,\nwe express a wish: we wish we had some code that worked!\nThe phrase \"programming by wishful thinking\" actually has a wider meaning,\nof writing your code in a top-down kind of way.\nWe'll come back and talk about it more in <<chapter_24_outside_in>>.]\nlet's just pretend we had access to everything we need,\nlike a `list` variable in the template:\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch07l052)\n====\n[source,html]\n----\n    <form method=\"POST\" action=\"/lists/{{ list.id }}/add_item\">\n----\n====\n\nThat changes our error slightly:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros,callouts\"]\n----\nAssertionError: False is not true : Couldn't find '<form method=\"POST\"\naction=\"/lists/1/add_item\">' in the following response\nb'<html>\\n  <head>\\n    <title>To-Do lists</title>\\n  </head>\\n  <body>\\n\n<h1>Your To-Do list</h1>\\n    <form method=\"POST\" action=\"/lists//add_item\">\\n  <1>\n----\n\n<1> Do you see it says `/lists//add_item`?\n    It's because Django templates will just silently\n    ignore any undefined variables, and substitute empty strings for them.\n\nLet's see if we can make our wish come true\nand  pass our list to the template then:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l053)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    items = Item.objects.filter(list=our_list)\n    return render(request, \"list.html\", {\"items\": items, \"list\": our_list})\n----\n====\n\nThat gets us to passing tests:\n\n----\nOK\n----\n\n\nAnd we now have an opportunity to refactor,\nas passing both the list and its items together is redundant.\nHere's the change in the template:\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch07l054)\n====\n[source,html]\n----\n      {% for item in list.item_set.all %}  <1>\n        <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n      {% endfor %}\n----\n====\n\n<1> `.item_set` is called a\n    https://docs.djangoproject.com/en/5.2/topics/db/queries/#following-relationships-backward[reverse lookup].\n    It's one of Django's incredibly useful bits of ORM that lets you look up an\n    object's related items from a different table.\n    (((\"reverse lookups\")))\n\n\n// DAVID: instead of using item_set, might want to consider defining a related name 'items' when we first\n// define the foreign key. It's more explicit and I think people new to Django might understand it better.\n\n\nThe tests still pass...\n\n----\nOK\n----\n\nAnd we can now simplify the view down a little:\n\n[role=\"sourcecode\"]\n.lists/views.py (ch07l055)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    return render(request, \"list.html\", {\"list\": our_list})\n----\n====\n\n\nAnd our unit tests still pass:\n\n----\nRan 10 tests in 0.040s\n\nOK\n----\n\nHow about the FTs?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\n..\n ---------------------------------------------------------------------\nRan 2 tests in 9.771s\n\nOK\n----\n\nHOORAY!  Oh, and a quick check on our to-do list:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Adjust model so that items are associated with different lists.#'\n* '[strikethrough line-through]#Add unique URLs for each list.#'\n* '[strikethrough line-through]#Add a URL for creating a new list via POST.#'\n* '[strikethrough line-through]#Add URLs for adding a new item to an existing list via POST.#'\n* 'Refactor away some duplication in urls.py.'\n*****\n\n\nIrritatingly, the Testing Goat is a stickler for tying up loose ends too, so we've got to do one final thing. Before we start, we'll do a commit--always make sure you've got a commit\nof a working state before embarking on a refactor:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n$ *git commit -am \"new URL + view for adding to existing lists. FT passes :-)\"*\n----\n\n[role=\"pagebreak-before less_space\"]\n=== A Final Refactor Using URL includes\n\n_superlists/urls.py_ is really meant for URLs that apply to your entire site.\nFor URLs that only apply to the `lists` app,\nDjango encourages us to use a separate _lists/urls.py_,\nto make the app more self-contained.(((\"URLs\", \"final list view refactor using URL includes\")))(((\"includes, URL, final refactor using\")))\nThe simplest way to make one is to use a copy of the existing _urls.py_:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cp superlists/urls.py lists/*\n----\n//56\n\nThen we replace the three list-specific lines in _superlists/urls.py_ with an `include()`:\n\n[role=\"sourcecode\"]\n.superlists/urls.py (ch07l057)\n====\n[source,python]\n----\nfrom django.urls import include, path\nfrom lists import views as list_views  # <1>\n\nurlpatterns = [\n    path(\"\", list_views.home_page, name=\"home\"),\n    path(\"lists/\", include(\"lists.urls\")),  # <2>\n]\n----\n====\n\n\n<1> While we're at it, we use the `import x as y` syntax to alias `views`.\n    This is good practice in your top-level _urls.py_,\n    because it will let us import views from multiple apps if we want--and\n    indeed we will need to later on in the book.(((\"views\", \"import syntax aliasing\")))\n\n<2> Here's the `include`.\n    Notice that it can take a part of a URL as a prefix,\n    which will be applied to all the included URLs\n    (this is the bit where we reduce duplication,\n    as well as giving our code a better structure).\n\n\nBack in _lists/urls.py_, we can trim down to only include the latter part\nof our three URLs, and none of the other stuff from the parent _urls.py_:\n\n\n[role=\"sourcecode\"]\n.lists/urls.py (ch07l058)\n====\n[source,python]\n----\nfrom django.urls import path\nfrom lists import views\n\nurlpatterns = [\n    path(\"new\", views.new_list, name=\"new_list\"),\n    path(\"<int:list_id>/\", views.view_list, name=\"view_list\"),\n    path(\"<int:list_id>/add_item\", views.add_item, name=\"add_item\"),\n]\n----\n====\n\n\nRerun the unit tests to check that everything worked.\n\n\n----\nRan 10 tests in 0.040s\n\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\n==== Can You Believe It?\n\nWhen I saw this test pass,\nI couldn't quite believe I did it correctly on the first go.\nIt always pays to be skeptical of your own abilities,\nso I deliberately changed one of the URLs slightly,\njust to check if it broke a test.\nIt did. We're covered.\n\nFeel free to try it yourself!\nRemember to change it back,\ncheck that the tests all pass again (including the FTs),\nand then do a final commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n$ *git add lists/urls.py*\n$ *git add superlists/urls.py*\n$ *git diff --staged*\n$ *git commit*\n----\n\nPhew. This was a marathon chapter.\nBut we covered a number of important topics,\nstarting with some thinking about design.\nWe covered rules of thumb like \"YAGNI\" and \"three strikes and refactor\".\nBut, most importantly, we saw how to adapt an existing codebase\nstep by step, going from working state to working state,\nto iterate towards a new design.\n// CSANAD:  \"three strikes and refactor\", even though it is what we were doing,\n//          was actually not mentioned in this chapter explicitly until this\n// paragraph, but only earlier, in chapter Post and Database.\n\nI'd say we're pretty close to being able to ship this site,\nas the very first beta of the superlists website\nthat's going to take over the world.\nMaybe it needs a little prettification first...let's look at\nwhat we need to do to deploy it in the next couple of chapters.\n(((\"\", startref=\"TDDadapt07\")))\n\n\n.Some More TDD Philosophy\n*******************************************************************************\n\nWorking state to working state (aka the Testing Goat versus Refactoring Cat)::\n    Our natural urge is often to dive in\n    and fix everything at once...but if we're not careful,\n    we'll end up like Refactoring Cat,\n    in a situation with loads of changes to our code\n    and nothing working.\n    The Testing Goat encourages us to take one step at a time,\n    and go from working state to working state.\n    (((\"Test-Driven Development (TDD)\", \"philosophy of\", \"working state to working state\")))\n    (((\"working state to working state\")))\n\n\nSplit work out into small, achievable tasks::\n    Sometimes this means starting with \"boring\" work\n    rather than diving straight in with the fun stuff,\n    but you'll have to trust that YOLO-you in the parallel universe\n    is probably having a bad time, having broken everything\n    and struggling to get the app working again.\n    (((\"Test-Driven Development (TDD)\", \"philosophy of\", \"split work into smaller tasks\")))\n    (((\"small vs. big design\")))\n\n\nYAGNI::\n    You ain't gonna need it!\n    Avoid the temptation to write code that you think 'might' be useful,\n    just because it suggests itself at the time.\n    Chances are, you won't use it,\n    or you won't have anticipated your future requirements correctly.\n     See <<chapter_24_outside_in>> for one methodology that helps us avoid this trap.\n    (((\"Test-Driven Development (TDD)\", \"philosophy of\", \"YAGNI\")))\n    (((\"YAGNI (You ain&#x2019;t gonna need it!)\")))\n\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_08_prettification.asciidoc",
    "content": "[[chapter_08_prettification]]\n== Prettification: Layout and Styling, [.keep-together]#and What to Test About It#\n\n(((\"layout\", see=\"CSS; design and layout testing\")))\n(((\"style\", see=\"CSS; design and layout testing\")))\nWe're starting to think about releasing the first version of our site,\nbut we're a bit embarrassed by how unfinished it looks at the moment.\nIn this chapter, we'll cover some of the basics of styling,\nincluding integrating an HTML/CSS framework called Bootstrap.\nWe'll learn how static files work in Django,\nand what we need to do about testing them.\n\n\n\n=== Testing Layout and Style\n\n(((\"design and layout testing\", \"selecting test targets\", id=\"DLTtargets08\")))\nOur site is undeniably a bit unattractive at the moment\n(<<homepage-looking-ugly>>).\n\nNOTE: If you spin up your dev server with `manage.py runserver`,\n    you may run into a database error, something like this:\n    \"OperationalError: no such table: lists_list\".\n    You need to update your local database\n    to reflect the changes we made in 'models.py'.(((\"manage.py file\", \"migrate\")))(((\"databases\", \"local dev database out of sync with migrations\")))(((\"IntegrityErrors\")))\n    Use `manage.py migrate`.\n    If it gives you any grief about `IntegrityErrors`,\n    just delete the database file.footnote:[\n    What? Delete the database?  Have you taken leave of your senses?  Not completely.\n    The local dev database often gets out of sync with its migrations\n    as we go back and forth in our development,\n    and it doesn't have any important data in it,\n    so it's OK to blow it away now and again.\n    We'll be much more careful once we have a \"production\" database on the server.]\n\n[role=\"pagebreak-before\"]\nWe can't be going back to\nhttps://oreil.ly/ruIZz[Python's historical reputation for being ugly],\nso let's do a tiny bit of polishing.\nHere are a few things we might want:\n\n* A large input field for adding to new and existing lists\n* A large, attention-grabbing, centered box to put it in\n\n(((\"aesthetics, testing\", seealso=\"design and layout testing\")))\nHow do we apply TDD to these things?(((\"Test-Driven Development (TDD)\", \"testing behaviour of aesthetics\")))\nMost people will tell you that you shouldn't test aesthetics, and they're right.\nIt's a bit like testing a constant, in that tests usually wouldn't add any value.\n\n\n[[homepage-looking-ugly]]\n.Our home page, looking a little ugly...\nimage::images/tdd3_0801.png[\"Our home page, looking a little ugly.\"]\n\n\n(((\"static files\", \"challenges of\")))\n(((\"CSS (Cascading Style Sheets)\", \"challenges of static files\")))\nBut we can test the essential _behaviour_ of our aesthetics\n(i.e., that we have any at all).\nAll we want to do is reassure ourselves that things are working.\nFor example, we're going to use Cascading Style Sheets (CSS) for our styling,\nand they are loaded as static files.\nStatic files can be a bit tricky to configure\n(especially, as we'll see later, when you move off your own computer and onto a server),\nso we'll want some kind of simple \"smoke test\" that the CSS has loaded.\nWe don't have to test fonts and colours and every single pixel,\nbut we can do a quick check that the main input box is aligned the way we want it on each page,\nand that will give us confidence that the rest of the styling for that page is probably loaded too.\n\n[role=\"pagebreak-before\"]\nLet's add a new test method inside our functional test (FT):\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch08l001)\n====\n[source,python]\n----\nclass NewVisitorTest(LiveServerTestCase):\n    [...]\n\n\n    def test_layout_and_styling(self):\n        # Edith goes to the home page,\n        self.browser.get(self.live_server_url)\n\n        # Her browser window is set to a very specific size\n        self.browser.set_window_size(1024, 768)\n\n        # She notices the input box is nicely centered\n        inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n        self.assertAlmostEqual(\n            inputbox.location[\"x\"] + inputbox.size[\"width\"] / 2,\n            512,\n            delta=10,\n        )\n----\n====\n\nA few new things here.\nWe start by setting the window size to a fixed size.\nWe then find the input element,\nlook at its size and location,\nand do a little maths\nto check whether it seems to be positioned in the middle of the page.\n`assertAlmostEqual` helps us to deal with rounding errors\nand the occasional weirdness due to scrollbars and the like,\nby letting us specify that we want our arithmetic to work\nto within 10 pixels, plus or minus.\n\nIf we run the FTs, we get:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\n.F.\n======================================================================\nFAIL: test_layout_and_styling\n(functional_tests.tests.NewVisitorTest.test_layout_and_styling)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 119, in\ntest_layout_and_styling\n    self.assertAlmostEqual(\n[...]\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\n\n ---------------------------------------------------------------------\nRan 3 tests in 9.188s\n\nFAILED (failures=1)\n----\n\nThat's the expected failure.\nStill, this kind of FT is easy to get wrong,\nso let's use a quick-and-dirty \"cheat\" solution,\nto check that the FT definitely passes when the input box is centered.\nWe'll delete this code again almost as soon as we've used it\nto check the FT:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/home.html (ch08l002)\n====\n[source,html]\n----\n<form method=\"POST\" action=\"/lists/new\">\n  <p style=\"text-align: center;\">\n    <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n  </p>\n  {% csrf_token %}\n</form>\n----\n====\n\nThat passes, which means the FT works.\nLet's extend it to make sure that the input box is also\ncenter-aligned on the page for a new list:\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch08l003)\n====\n[source,python]\n----\n    # She starts a new list and sees the input is nicely\n    # centered there too\n    inputbox.send_keys(\"testing\")\n    inputbox.send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: testing\")\n    inputbox = self.browser.find_element(By.ID, \"id_new_item\")\n    self.assertAlmostEqual(\n        inputbox.location[\"x\"] + inputbox.size[\"width\"] / 2,\n        512,\n        delta=10,\n    )\n----\n====\n\nThat gives us another test failure:\n\n----\n  File \"...goat-book/functional_tests/tests.py\", line 131, in\ntest_layout_and_styling\n    self.assertAlmostEqual(\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\n----\n\nLet's commit just the FT:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add functional_tests/tests.py*\n$ *git commit -m \"first steps of FT for layout + styling\"*\n----\n\n[role=\"pagebreak-before\"]\nNow it feels like we're justified in finding a \"proper\" solution\nto improve the styling for our site.\nWe can back out our hacky `text-align: center`:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git reset --hard*\n----\n\n\nWARNING: `git reset --hard`\n    is the \"take off and nuke the site from orbit\"\n    Git command, so be careful with it--it\n    blows away all your un-committed changes.\n    Unlike almost everything else you can do with Git,\n    there's no way of going back after this one.(((\"Git\", \"reset --hard\")))(((\"\", startref=\"DLTtargets08\")))\n\n\n\n=== Prettification: Using a CSS Framework\n\n(((\"design and layout testing\", \"CSS frameworks\", id=\"DLTcssframe08\")))\n(((\"CSS (Cascading Style Sheets)\", \"CSS frameworks\", id=\"CSSframe08\")))\n(((\"Bootstrap\", \"downloading\")))\nUI design is hard,\nand doubly so now that we have to deal with mobile, tablets, and so forth.\nThat's why many programmers, particularly lazy ones like me,\nturn to CSS frameworks to solve some of those problems for them.\nThere are lots of frameworks out there,\nbut one of the earliest and most popular still, is Bootstrap.\nLet's use that.\n\nYou can find Bootstrap at https://getbootstrap.com[getbootstrap.com].\n\nWe'll download it and put it in a new folder called _static_ inside the `lists`\napp:footnote:[On Windows, you may not have `wget` and `unzip`,\nbut I'm sure you can figure out how to download Bootstrap,\nunzip it, and put the contents of the _dist_ folder\ninto the _lists/static/bootstrap_ folder.]\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *wget -O bootstrap.zip https://github.com/twbs/bootstrap/releases/download/\\\nv5.3.5/bootstrap-5.3.5-dist.zip*\n$ *unzip bootstrap.zip*\n$ *mkdir lists/static*\n$ *mv bootstrap-5.3.5-dist lists/static/bootstrap*\n$ *rm bootstrap.zip*\n----\n\nBootstrap comes with a plain, uncustomised installation in the 'dist' folder.\nWe're going to use that for now,\nbut you should really never do this for a real site--vanilla\nBootstrap is instantly recognisable,\nand a big signal to anyone in the know\nthat you couldn't be bothered to style your site.\nLearn how to use Sass and change the font, if nothing else!\nThere is info in Bootstrap's docs, or read an\nhttps://www.freecodecamp.org/news/how-to-customize-bootstrap-with-sass[introductory guide].\n\n[role=\"pagebreak-before\"]\nOur 'lists' folder will end up looking like this:\n\n[subs=\"specialcharacters,macros\"]\n----\n[...]\n├── lists\n│   ├── __init__.py\n│   ├── admin.py\n│   ├── apps.py\n│   ├── migrations\n│   │   ├── [...]\n│   ├── models.py\n│   ├── static\n│   │   └── bootstrap\n│   │       ├── css\n│   │       │   ├── bootstrap-grid.css\n│   │       │   ├── bootstrap-grid.css.map\n│   │       │   ├── [...]\n│   │       │   └── bootstrap.rtl.min.css.map\n│   │       └── js\n│   │           ├── bootstrap.bundle.js\n│   │           ├── bootstrap.bundle.js.map\n│   │           ├── [...]\n│   │           └── bootstrap.min.js.map\n│   ├── templates\n│   │   ├── home.html\n│   │   └── list.html\n│   ├── tests.py\n│   ├── urls.py\n│   └── views.py\n[...]\n----\n\n\n(((\"Bootstrap\", \"documentation\")))\nLook at the \"Getting started\" section of the\nhttps://getbootstrap.com/docs/5.3/getting-started/introduction[Bootstrap documentation];\nyou'll see it wants our HTML template to include something like this:\n\n\n[role=\"skipme\"]\n[source,html]\n----\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Bootstrap demo</title>\n  </head>\n  <body>\n    <h1>Hello, world!</h1>\n  </body>\n</html>\n\n----\n\nWe already have two HTML templates.\nWe don't want to be adding a whole load of boilerplate code to each,\nso now feels like the right time to apply\nthe \"Don't repeat yourself\" rule,\nand bring all the common parts together.\nThankfully, the Django template language makes that easy using something\ncalled template inheritance.\n(((\"\", startref=\"DLTcssframe08\")))\n(((\"\", startref=\"CSSframe08\")))\n\n\n\n\n\n=== Django Template Inheritance\n\n(((\"design and layout testing\", \"Django template inheritance\")))\n(((\"templates\", \"Django template inheritance\")))\n(((\"Django framework\", \"template inheritance\")))\nLet's have a little review of what the differences are between 'home.html' and\n'list.html':\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*diff lists/templates/home.html lists/templates/list.html*]\n<     <h1>Start a new To-Do list</h1>\n<     <form method=\"POST\" action=\"/lists/new\">\n---\n>     <h1>Your To-Do list</h1>\n>     <form method=\"POST\" action=\"/lists/{{ list.id }}/add_item\">\n[...]\n>     <table id=\"id_list_table\">\n>       {% for item in list.item_set.all %}\n>         <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n>       {% endfor %}\n>     </table>\n----\n\nThey have different header texts, and their forms use different URLs. On top\nof that, 'list.html' has the additional `<table>` element.\n\n//IDEA add a note re downsides of inheritance?\nNow that we're clear on what's in common and what's not, we can make the two\ntemplates inherit from a common \"superclass\" template.  We'll start by\nmaking a copy of 'list.html':\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cp lists/templates/list.html lists/templates/base.html*\n----\n//006\n\nWe make this into a base template, which just contains the common boilerplate,\nand mark out the \"blocks\", places where child templates can customise it:\n\n[role=\"sourcecode small-code\"]\n.lists/templates/base.html (ch08l007)\n====\n[source,html]\n----\n<html>\n  <head>\n    <title>To-Do lists</title>\n  </head>\n\n  <body>\n    <h1>{% block header_text %}{% endblock %}</h1>\n\n    <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n      <input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />\n      {% csrf_token %}\n    </form>\n\n    {% block table %}\n    {% endblock %}\n  </body>\n\n</html>\n----\n====\n\n[role=\"pagebreak-before\"]\nLet's see how these blocks are used in practice,\nby changing 'home.html' so that it \"inherits\" from 'base.html':\n\n[role=\"sourcecode\"]\n.lists/templates/home.html (ch08l008)\n====\n[source,html]\n----\n{% extends 'base.html' %}\n\n{% block header_text %}Start a new To-Do list{% endblock %}\n\n{% block form_action %}/lists/new{% endblock %}\n----\n====\n\nYou can see that lots of the boilerplate HTML disappears,\nand we just concentrate on the bits we want to customise.\nWe do the same for 'list.html':\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch08l009)\n====\n[source,html]\n----\n{% extends 'base.html' %}\n\n{% block header_text %}Your To-Do list{% endblock %}\n\n{% block form_action %}/lists/{{ list.id }}/add_item{% endblock %}\n\n{% block table %}\n  <table id=\"id_list_table\">\n    {% for item in list.item_set.all %}\n      <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n    {% endfor %}\n  </table>\n{% endblock %}\n----\n====\n\n\nThat's a refactor of the way our templates work.\nWe rerun the FTs to make sure we haven't broken anything:\n\n----\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\n----\n\nSure enough, they're still getting to exactly where they were before.\n\n\nThat's worthy of a commit:\n(((\"Git\", \"diff -w\")))\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff -w*\n# the -w means ignore whitespace, useful since we've changed some html indenting\n$ *git status*\n$ *git add lists/templates* # leave static, for now\n$ *git commit -m \"refactor templates to use a base template\"*\n----\n\n\n=== Integrating Bootstrap\n\n(((\"design and layout testing\", \"Bootstrap integration\")))\n(((\"Bootstrap\", \"integrating\")))\nNow it's much easier to integrate the boilerplate code that Bootstrap wants--we\nwon't add the JavaScript yet, just the CSS:\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l010)\n====\n[source,html]\n----\n<!doctype html>\n<html lang=\"en\">\n\n  <head>\n    <title>To-Do lists</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\">\n  </head>\n[...]\n----\n====\n\n\n==== Rows and Columns\n\nFinally, let's actually use some of the Bootstrap magic!\nYou'll have to read the documentation yourself,\nbut we should be able to use a combination\nof the grid system and the `justify-content-center` class to get what we want:\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l011)\n====\n[source,html]\n----\n  <body>\n    <div class=\"container\">\n\n      <div class=\"row justify-content-center\">\n        <div class=\"col-lg-6 text-center\">\n          <h1>{% block header_text %}{% endblock %}</h1>\n\n          <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n            <input\n              name=\"item_text\"\n              id=\"id_new_item\"\n              placeholder=\"Enter a to-do item\"\n            />\n            {% csrf_token %}\n          </form>\n        </div>\n      </div>\n\n      <div class=\"row justify-content-center\">\n        <div class=\"col-lg-6\">\n          {% block table %}\n          {% endblock %}\n        </div>\n      </div>\n\n    </div>\n  </body>\n----\n====\n\n(If you've never seen an HTML tag broken up over several lines,\nthat `<input>` may be a little shocking.\nIt is definitely valid,\nbut you don't have to use it if you find it offensive.)\n\nTIP: Take the time to browse through the\n    https://getbootstrap.com/docs/5.3/getting-started/introduction/[Bootstrap documentation],\n    if you've never seen it before.\n    It's a shopping trolley brimming full of useful tools\n    to use in your site.\n\nDoes that work?  Whoops—no, we have an error in our unit tests:\n\n----\nFAIL: test_renders_input_form\n(lists.tests.ListViewTest.test_renders_input_form)\n[...]\nAssertionError: False is not true : Couldn't find '<input name=\"item_text\"' in\nthe following response\n[...]\n----\n\nAh, it's because our unit tests are currently a little\nbrittle with respect to whitespace changes in our `<input>` tag,\nwhich actually don't matter semantically.\n\nDjango does provide the `html=True` argument to `assertContains()`,\nwhich does help a bit, but it requires exhaustively\nspecifying every attribute of the element we want to check on,\nlike this:\n\n[role=\"sourcecode small-code\"]\n.lists/tests.py (ch08l011-1)\n====\n[source,python]\n----\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        response = self.client.get(\"/\")\n        self.assertContains(response, '<form method=\"POST\" action=\"/lists/new\">')\n        self.assertContains(\n            response,\n            '<input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />',\n            html=True,\n        )\n[...]\n\n\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")\n        self.assertContains(\n            response,\n            f'<form method=\"POST\" action=\"/lists/{mylist.id}/add_item\">',\n        )\n        self.assertContains(\n            response,\n            '<input name=\"item_text\" id=\"id_new_item\" placeholder=\"Enter a to-do item\" />',\n            html=True,\n        )\n----\n====\n\nThat's not entirely satisfactory,\nbecause all those extra attributes like `id` and `placeholder`\naren't really things we want to nail down in unit tests;\nwe'd rather have the freedom to change them in the template\nwithout needing to change the tests as well.\nThey're more of a presentation concern than a true part of the contract\nbetween backend and frontend.\n\nBut it does get the tests to pass:\n\n----\nOK\n----\n\n[role=\"pagebreak-before\"]\nSo, for now, let's make a note to come back to it:\n\n[role=\"scratchpad\"]\n*****\n* _Find a better way to unit test form &amp; input elements._\n*****\n\n\nSo, the unit tests are happy. What about the FTs?\n\n----\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\n----\n\nHmm. No.  Why isn't our CSS loading?\nIf you try it manually with `runserver` and look around in DevTools,\nyou'll see the browser 404ing when it tries to fetch _bootstrap.min.css_.\nIf you watch the `runserver` terminal session, you'll also see the 404s there,\nas in <<bootstrap_css_404_devtools>>.\n\n[[bootstrap_css_404_devtools]]\n.That's a nope on bootstrap.css\nimage::images/tdd3_0802.png[\"Browser DevTools showing a 404 for css/bootstrap.min.css, but also at the bottom of the screenshot, the terminal window showing the same URL returing a 404 in the runserver session.\"]\n\nTo figure out what's happening,\nlet's talk a bit about how Django deals with static files.\n\n[role=\"pagebreak-before less_space\"]\n=== Static Files in Django\n\n(((\"Django framework\", \"static files in\", id=\"DJFstatic08\")))\nDjango, and indeed any web server,\nneeds to know two things to deal with static files:\n\n1. How to tell when a URL request is for a static file,\n   as opposed to for some HTML\n   that's going to be served via a view function\n2. Where to find the static file that the user wants\n\nIn other words, static files (((\"URL mappings\", \"for static files\", secondary-sortas=\"static\")))are a mapping from URLs to files on disk.\n\n(((\"static files\", \"URL requests for\")))\nFor item 1, Django lets us define a URL \"prefix\"\nto say that any URLs that start with that prefix\nshould be treated as requests for static files.\nBy default, the prefix is [keep-together]#'/static/'#.\nIt's already defined in _settings.py_:\n\n[role=\"sourcecode currentcontents\"]\n.superlists/settings.py\n====\n[source,python]\n----\n[...]\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/5.2/howto/static-files/\n\nSTATIC_URL = \"static/\"\n----\n====\n\n(((\"static files\", \"finding\")))\nThe rest of the settings that we will add to this section\nall have to do with item 2:\nfinding the actual static files on disk.\n\nWhile we're using the Django development server (`manage.py runserver`),\nwe can rely on Django to magically find static files for us--it'll\njust look in any subfolder of one of our apps called _static_.\n\nYou now see why we put all the Bootstrap static files into _lists/static_.\nSo, why are they not working at the moment?\nIt's because we're not using the `/static/` URL prefix.\nHave another look at the link to the CSS in _base.html_:\n\n[role=\"sourcecode currentcontents\"]\n.lists/templates/base.html\n[source,html]\n----\n    <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\">\n----\n\nThat `href` is just what happened to be in the Bootstrap docs.\nTo get it to work, we need to change it to:\n\n\n[role=\"sourcecode small-code\"]\n.lists/templates/base.html (ch08l012)\n====\n[source,html]\n----\n    <link href=\"/static/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n----\n====\n\n// DAVID: Django best practice would be to use the static tag instead.\n// https://docs.djangoproject.com/en/5.2/howto/static-files/#configuring-static-files\n\nNow when `runserver` sees the request,\nit knows that it's for a static file because it begins with `/static/`.\nIt then tries to find a file called _bootstrap/css/bootstrap.min.css_,\nlooking in each of our app folders for subfolders called _static_,\nand it should find it at _lists/static/bootstrap/css/bootstrap.min.css_.\n\nSo if you take a look manually, you should see it works,\nas in <<list-page-centered>>.\n\n\n[[list-page-centered]]\n.Our site starts to look a little better...\nimage::images/tdd3_0803.png[\"The list page with centered header.\"]\n\n\n\n==== Switching to StaticLiveServerTestCase\n\n\n(((\"StaticLiveServerTestCase\")))\nIf you run the FT though, annoyingly, it still won't pass:\n\n----\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\n----\n\nThat's because, although `runserver` automagically finds static files,\n+Live&#x2060;S&#x2060;e&#x2060;r&#x2060;v&#x2060;e&#x2060;r&#x200b;T&#x2060;e&#x2060;s&#x2060;t&#x2060;Case+ doesn't.\nNever fear, though:\nthe Django developers have made an even [.keep-together]#more magical# test class\ncalled `StaticLiveServerTestCase`\n(see https://oreil.ly/mh-iO[the docs]).\n\n// JAN: Maybe you could mention that StaticLiveServerTestCase inherits from LiveServerTestCase - so all previous should work + static files. After reading the name, I imagined StaticLiveServerTestCase as some special test class for testing only static-related stuff\n\nLet's switch to that:\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch08l013)\n====\n[source,diff]\n----\n@@ -1,14 +1,14 @@\n-from django.test import LiveServerTestCase\n+from django.contrib.staticfiles.testing import StaticLiveServerTestCase\n from selenium import webdriver\n from selenium.common.exceptions import WebDriverException\n from selenium.webdriver.common.keys import Keys\n import time\n\n MAX_WAIT = 10\n\n\n-class NewVisitorTest(LiveServerTestCase):\n+class NewVisitorTest(StaticLiveServerTestCase):\n\n     def setUp(self):\n----\n====\n//008\n\n[role=\"pagebreak-before\"]\nAnd now it will find the new CSS, which will get our test to pass:\n(((\"\", startref=\"DJFstatic08\")))\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\nCreating test database for alias 'default'...\n...\n ---------------------------------------------------------------------\nRan 3 tests in 9.764s\n----\n\n// (David): Incidentally, when I ran this the first time I got this error\n// on the second test case. selenium.common.exceptions.NoSuchElementException:\n// Message: Unable to locate element: [id=\"id_new_item\"];\n// I ran it again and it worked.\n\nHooray!\n\n\n=== Using Bootstrap Components to Improve the Look of the Site\n\n(((\"design and layout testing\", \"Bootstrap tools\")))Let's\nsee if we can do even better, using some of the other tools in\nBootstrap's panoply.(((\"Bootstrap\", \"using components of to improve looks of site\", id=\"ix_Bootuse\")))\n\n\n==== Jumbotron!\n\nThe first version of Bootstrap used to ship with a class called `jumbotron`\nfor things that are meant to be particularly prominent on the page.\nIt doesn't exist anymore, but old-timers like me still pine for it,\nso they have a specific page in the docs that tells you how to re-create it.(((\"jumbotron (Bootstrap)\")))\n\nEssentially, we massively embiggen the main page header and the input form,\nputting it into a grey box with nice rounded corners:\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l014)\n====\n[source,html]\n----\n  <body>\n    <div class=\"container\">\n\n      <div class=\"row justify-content-center p-5 bg-body-tertiary rounded-3\">\n        <div class=\"col-lg-6 text-center\">\n          <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n          [...]\n----\n====\n\nThat ends up looking something like <<jumbotron-header>>.\n\n[[jumbotron-header]]\n.A big grey box at the top of the page\nimage::images/tdd3_0804.png[\"The home page with a big grey box surrounding the title and input\"]\n\n\nTIP: When hacking about with design and layout,\n    it's best to have a window open that we can refresh frequently.\n    Use `python manage.py runserver` to spin up the dev server,\n    and then browse to __http://localhost:8000__\n    to see your work as we go.\n\n// JAN: You could mention force refresh here (Cmd + Shift + R; Ctrl + F5, ...). It comes handy many times when working with CSS etc.\n\n\n==== Large Inputs\n\n\n(((\"Bootstrap\", \"large inputs\")))\n(((\"form control classes (Bootstrap)\")))\nThe `jumbotron` is a good start,\nbut now the input box has tiny text compared to everything else.\nThankfully, Bootstrap's form control classes offer an option\nto set an input to \"large\":\n\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l015)\n====\n[source,html]\n----\n    <input\n      class=\"form-control form-control-lg\"\n      name=\"item_text\"\n      id=\"id_new_item\"\n      placeholder=\"Enter a to-do item\"\n    />\n----\n====\n\n\n==== Table Styling\n\n\n(((\"Bootstrap\", \"table styling\")))\n(((\"table styling (Bootstrap)\")))\nThe table text also looks too small compared to the rest of the page now.\nAdding the Bootstrap `table` class improves things, over in _list.html_:\n\n\n[role=\"sourcecode\"]\n.lists/templates/list.html (ch08l016)\n====\n[source,html]\n----\n  <table class=\"table\" id=\"id_list_table\">\n----\n====\n\n==== Optional: Dark Mode\n\nIn contrast to my greybeard nostalgia for `jumbotron`,\nhere's something relatively new to Bootstrap: dark mode!(((\"dark mode (Bootstrap)\")))(((\"Bootstrap\", \"dark mode\")))\n\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l017)\n====\n[source,html]\n----\n<!doctype html>\n<html lang=\"en\" data-bs-theme=\"dark\">\n----\n====\n\nTake a look at <<dark-modeee>>.\nI think that looks great!\n\n[[dark-modeee]]\n.Dark modeeeeeeeeee\nimage::images/tdd3_0805.png[\"Screenshot of lists page in dark mode. Cool.\"]\n\n\nBut it's very much a matter of personal preference,\nand my editor will have kittens\nif I make all the rest of my screenshots use so much ink,\nso I'm going to revert it for now.\nYou're free to keep dark mode on if you like!\n\n[role=\"pagebreak-before less_space\"]\n==== A Semi-Decent Page\n\nGetting it into shape took me a few goes, but I'm reasonably happy with it now\n(<<homepage-looking-better>>).\n\n[[homepage-looking-better]]\n.The lists page, looking...good enough for now\nimage::images/tdd3_0806.png[\"Screenshot of lists page in light mode with decent styling.\"]\n\nIf you want to go further with customising Bootstrap,\nyou need to get into compiling Sass.(((\"Sass/SCSS\")))\nI've said it already, but I _definitely_ recommend\ntaking the time to do that someday.\nSass/SCSS is a great improvement on plain old CSS,\nand a useful tool even if you don't use Bootstrap.(((\"CSS (Cascading Style Sheets)\", \"Sass/SCSS improvement on\")))\n\n\nA last run of the FTs, to see if everything still works OK:\n\n[role=\"dofirst-ch08l018\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\n...\n ---------------------------------------------------------------------\nRan 3 tests in 10.084s\n\nOK\n----\n\n\nThat's it! Definitely time for a commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # changes tests.py, base.html, list.html, settings.py,\n             # and untracked lists/static\n$ *git add .*\n$ *git status* # will now show all the bootstrap additions\n$ *git commit -m \"Use Bootstrap to improve layout\"*\n----\n\n\n=== Parsing HTML for Less Brittle Tests of Key HTML Content\n\nOh whoops, we nearly forgot our scratchpad:\n\n[role=\"scratchpad\"]\n*****\n* _Find a better way to unit test form &amp; input elements._\n*****\n\n\nWhen working on layout and styling, you expect to spend most of your time\nin the browser, in a cycle of tweaking your HTML and refreshing to see\nthe effects, with occasional runs of your layout FT, if you have one.(((\"HTML\", \"parsing for less brittle tests of content\")))\n\nYou wouldn't expect to test-drive design with unit tests.\nAnd sure enough, we haven't run them in a while.\nBecause if we had done, we'd have noticed that they're failing:\n\n----\nFAIL: test_renders_input_form\n(lists.tests.HomePageTest.test_renders_input_form)\n[...]\nAssertionError: False is not true : Couldn't find '<input name=\"item_text\"\nid=\"id_new_item\" placeholder=\"Enter a to-do item\" />' in the following response\nb'<!doctype html>\\n<html lang=\"en\">\\n\\n  <head>\\n    <title>To-Do\n[...]\n<input\\n              class=\"form-control form-control-lg\"\\n\nname=\"item_text\"\\n              id=\"id_new_item\"\\n\nplaceholder=\"Enter a to-do item\"\\n            />\\n            <input\n[...]\nFAIL: test_renders_input_form\n(lists.tests.ListViewTest.test_renders_input_form)\n[...]\n----\n\nIt's also annoyingly hard to see from the tests output,\nbut it happened when we introduced the `class=form-control form-control-lg`.\n\nWe really don't want this sort of thing breaking our unit tests.\nUsing string matching, even whitespace-aware string matching,\nis just the wrong tool for the job.footnote:[\nAs famously explained in a\nhttps://oreil.ly/N-cIc[classic Stack Overflow post].]\nLet's switch to using a proper HTML parser, the venerable\nhttps://lxml.de[lxml].\n\n\n[subs=\"\"]\n----\n$ <strong>pip install 'lxml[cssselect]'</strong>\nCollecting lxml[cssselect]\n  [...]\nCollecting cssselect>=0.7 (from lxml[cssselect])\n  [...]\nInstalling collected packages: lxml, cssselect\nSuccessfully installed [...]\n----\n\n(We need the `cssselect` add-on for the nice CSS selectors.)(((\"lxml parser\")))\n\nAnd here's how we use it to write a more focused version of our test\nthat only cares about the two HTML attributes that actually matter\nto the integration of frontend and backend:\n\n1. The `<form>` tag's `method` and `action`\n2. The `<input>` tag's `name`\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch08l019)\n====\n[source,python]\n----\nimport lxml.html\n[...]\n\n\nclass HomePageTest(TestCase):\n    def test_uses_home_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        response = self.client.get(\"/\")\n        parsed = lxml.html.fromstring(response.content)  # <1>\n        [form] = parsed.cssselect(\"form[method=POST]\")  # <2><3>\n        self.assertEqual(form.get(\"action\"), \"/lists/new\")\n        [input] = form.cssselect(\"input[name=item_text]\")  # <4>\n----\n====\n\n<1> Here's where we parse the HTML into a structured object\n    to represent the DOM (document object model).\n\n<2> Here's where we use a CSS selector to find our form,\n    implicitly also checking that it has `method=\"POST\"`.\n    The `cssselect()` method returns a list of matching elements.\n\n<3> The `[form] =` is worth a mention.\n    What we're using here is a special assignment syntax called \"unpacking\",\n    where the lefthand side is a list of variable names\n    and the righthand side is a list of values.(((\"unpacking\")))(((\"tuple unpacking and multiple assignment\")))\n    It's a bit like saying `form = parsed.cssselect(\"form[method=POST]\")[0]`,\n    but a bit nicer to read, and a bit more strict too.\n    By only putting one element on the left,\n    we're effectively asserting that there is exactly one element on the right;\n    if there isn't, we'll get an error.footnote:[\n    Read more about tuple unpacking and multiple assignment\n    https://oreil.ly/LMfuB[on Trey Hunner's excellent blog].]\n\n<4> We use the same kind of assignment to assert that the form contains\n    exactly one input element with the name `item_text`.\n    \n\n\nHere's the same thing in `ListViewTest`:\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch08l020)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")\n        parsed = lxml.html.fromstring(response.content)\n        [form] = parsed.cssselect(\"form[method=POST]\")\n        self.assertEqual(form.get(\"action\"), f\"/lists/{mylist.id}/add_item\")\n        [input] = form.cssselect(\"input[name=item_text]\")\n----\n====\n\nThat works!\n\n\n----\nRan 10 tests in 0.017s\n\nOK\n----\n\nAnd as always, for any test you've only ever seen green,\nit's nice to introduce a deliberate failure:\n\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l021)\n====\n[source,python]\n----\n@@ -18,7 +18,7 @@\n           <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n             <input\n               class=\"form-control form-control-lg\"\n-              name=\"item_text\"\n+              name=\"geoff\"\n               id=\"id_new_item\"\n               placeholder=\"Enter a to-do item\"\n             />\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd let's see the error message:\n\n----\n    [input] = form.cssselect(\"input[name=item_text]\")\n    ^^^^^^^\nValueError: not enough values to unpack (expected 1, got 0)\n----\n\n\nHmm you know what?  I'm actually not happy with that.\nThe `[input] =` syntax is probably another example of\nme being too clever for my own good.\n\nLet's try something else that will give us a clearer message about\nwhat _is_ on the page and what isn't:\n\n\n[role=\"sourcecode\"]\n.lists/tests.py (ch08l022)\n====\n[source,python]\n----\n        inputs = form.cssselect(\"input\")  # <1>\n        self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])  # <2>\n----\n====\n\n<1> We'll get a list of all the inputs in the form.\n<2> And then we'll assert that at least one of them has the right `name=`.\n\nThat gives us a more self-explanatory message:\n\n----\n    self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'item_text' not found in ['geoff', 'csrfmiddlewaretoken']\n----\n\nNow I feel good about changing our HTML back:\n\n\n[role=\"sourcecode\"]\n.lists/templates/base.html (ch08l023)\n====\n[source,diff]\n----\n@@ -18,7 +18,7 @@\n           <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n             <input\n               class=\"form-control form-control-lg\"\n-              name=\"geoff\"\n+              name=\"item_text\"\n               id=\"id_new_item\"\n               placeholder=\"Enter a to-do item\"\n             />\n----\n====\n\nMuch better!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff* # tests.py\n$ *git commit -am \"use lxml for more specific unit test asserts on html content\"*\n----\n\n\n\n[role=\"pagebreak-before less_space\"]\n=== What We Glossed Over: collectstatic and Other Static Directories\n\n(((\"design and layout testing\", \"collecting static files for deployment\", id=\"DLTcollect08\")))\n(((\"static files\", \"collecting for deployment\", id=\"SFcollect08\")))\n(((\"collectstatic command\", id=\"collect08\")))\nWe saw earlier that the Django dev server will magically find all your static files\ninside app folders, and serve them for you.\nThat's fine during development,\nbut when you're running on a real web server,\nyou don't want Django serving your static content--using Python\nto serve raw files is slow and inefficient,\nand a web server like Apache or nginx can do this all for you.\n\nFor these reasons, you want to be able to gather all your static files\nfrom inside their various app folders\nand copy them into a single location, ready for deployment.\nThis is what the `collectstatic` command is for.\n\nThe destination, the place where the collected static files go,\nneeds to be defined in _settings.py_ as `STATIC_ROOT`.\nIn the next chapter, we'll be doing some deployment,\nso let's actually experiment with that now.\nA common and straightforward place to put it\nis in a folder called \"static\" in the root of our repo:\n\n[role=\"skipme\"]\n----\n.\n├── db.sqlite3\n├── functional_tests/\n├── lists/\n├── manage.py\n├── static/\n└── superlists/\n----\n\nHere's a neat way of specifying that folder,\nmaking it relative to the location of the project base directory:\n\n[role=\"sourcecode\"]\n.superlists/settings.py (ch08l024)\n====\n[source,python]\n----\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/5.2/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"static\"\n----\n====\n\n\nTake a look at the top of the settings file,\nand you'll see how that `BASE_DIR` variable is helpfully defined for us,\nusing `pathlib.Path` and `__file__`\n(both really nice Python built-ins).footnote:[\nNotice in the `Pathlib` wrangling of `__file__`\nthat the `.resolve()` happens before anything else.\nAlways follow this pattern when working with `__file__`,\notherwise you can see unpredictable behaviours\ndepending on how the file is imported.\nThanks to https://github.com/CleanCut/green[Green Nathan]\nfor that tip!]\n\n[role=\"pagebreak-before\"]\nAnyway, let's try running `collectstatic`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py collectstatic*]\n\n171 static files copied to '...goat-book/static'.\n----\n\nAnd if we look in './static', we'll find all our CSS files:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *tree static/*\nstatic/\n├── admin\n│   ├── css\n│   │   ├── autocomplete.css\n│   │   ├── [...]\n[...]\n│               └── xregexp.min.js\n└── bootstrap\n    ├── css\n    │   ├── bootstrap-grid.css\n    │   ├── [...]\n    │   └── bootstrap.rtl.min.css.map\n    └── js\n        ├── bootstrap.bundle.js\n        ├── [...]\n        └── bootstrap.min.js.map\n\n17 directories, 171 files\n----\n\n`collectstatic` has also picked up all the CSS for the admin site.\nThe admin site is one of Django's powerful features,\nbut we don't need it for our simple site, so let's disable it for now:\n\n[role=\"sourcecode\"]\n.superlists/settings.py (ch08l025)\n====\n[source,python]\n----\nINSTALLED_APPS = [\n    # \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"lists\",\n]\n----\n====\n\nAnd we try again:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*rm -rf static/*]\n$ pass:quotes[*python manage.py collectstatic*]\n\n44 static files copied to '...goat-book/static'.\n----\n\n\nMuch better.\n\n\nNow we know how to collect all the static files into a single folder,\nwhere it's easy for a web server to find them.\nWe'll find out all about that, including how to test it, in the next chapter!\n\n\n(((\"\", startref=\"DLTcollect08\")))\n(((\"\", startref=\"SFcollect08\")))\n(((\"\", startref=\"collect08\")))\nFor now, let's save our changes to _settings.py_.\nWe'll also add the top-level static folder to our `gitignore`,\nbecause it will only contain copies of files\nwe actually keep in individual apps' static folders:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff* # should show changes in settings.py\n$ *echo /static >> .gitignore*\n$ *git commit -am \"set STATIC_ROOT in settings and disable admin\"*\n----\n\n\n=== A Few Things That Didn't Make It\n\nInevitably this was only a whirlwind tour of styling and CSS,\nand there were several topics that I'd considered covering that didn't make it.\nHere are a few candidates for further study:\n\n* The `{% static %}` template tag, for more DRY and fewer hardcoded URLs\n* Client-side packaging tools, like `npm` and `bower`\n* Customising Bootstrap with Sass\n\n//RITA: Would you want to point readers to any resources, such as a website or another book for example? You don't have to.\n\n\n.Recap: On Testing Design and Layout\n*******************************************************************************\n\n(((\"design and layout testing\", \"best practices for\")))\nThe tl;dr is: you shouldn't write tests for design and layout per se.\nIt's too much like testing a constant,\nand the tests you write are often brittle.\n\nWith that said,\nthe _implementation_ of design and layout involves something quite tricky:\nCSS and static files.\nAs a result, it is valuable to have some kind of minimal \"smoke test\"\nthat checks that your static files and CSS are working.\nAs we'll see in the next chapter, it can help pick up problems\nwhen you deploy your code to production.\n\nSimilarly, if a particular piece of styling required a lot of client-side JavaScript code\nto get it to work\n(dynamic resizing is one I've spent a bit of time on),\nyou'll definitely want some tests for that\n(see <<chapter_17_javascript>>).\n\nTry to write the minimal tests that will give you the confidence\nthat your design and layout is working,\nwithout testing _what_ it actually is.\nThat includes unit tests!\nAvoid asserting on the cosmetic aspects of your HTML in your unit tests.\n\nAim to leave yourself in a position\nwhere you can freely make changes to the design and layout,\nwithout having to go back and adjust tests all the time.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_09_docker.asciidoc",
    "content": "[[chapter_09_docker]]\n== Containerization aka Docker\n\n[quote, Malvina Reynolds]\n______________________________________________________________\nLittle boxes, all the same\n______________________________________________________________\n\nIn this chapter, we'll start by adapting our FTs so that they can run against a container.\nAnd then we'll set about containerising our app,\nand getting those tests passing our code running inside Docker:\n\n* We'll build a minimal Dockerfile with everything we need to run our site.\n\n* We'll learn how to build and run a container on our machine.\n\n* We'll make a few changes to our source code layout, like using a _src_ folder.\n\n* We'll start flushing out a few issues around networking and the database.\n\n=== Docker, Containers, and Virtualization\n\nDocker is a commercial product that wraps several free\nand open source technologies from the world of Linux,\nsometimes referred to as \"containerization\".(((\"Docker\")))(((\"containerization\")))\n\nNOTE: Feel free to skip this section if you already know all about Docker.\n\nYou may have already heard of the idea of \"virtualization\",\nwhich enables a single physical computer to pretend to be several machines.(((\"virtualization\")))\nPioneered by IBM (amongst others) on mainframes in the 1960s,\nit rose to mainstream adoption in the '90s,\nwhere it was sold as a way to optimise resource usage in datacentres.\nAWS, for example, an offshoot of Amazon,\nwas using virtualization already,\nand realised it could sell some spare capacity on its servers\nto customers outside the business.(((\"Amazon Web Services (AWS)\")))(((\"AWS (Amazon Web Services)\")))\n\nSo, when you come to deploy your code to a real server in a datacentre,\nit will be using virtualization.\nAnd, actually, you can use virtualization on your own machine,\nwith software like VirtualBox or KVM.\nYou can run Windows \"inside\" a Mac or Linux laptop, for example.\n\nBut it can be fiddly to set up!(((\"virtualization\", \"containerization and\")))\nAnd nowadays, thanks to containerization, we can do better\nbecause containerization is a kind of even-more-virtual virtualization.(((\"VMs (virtual machines)\")))\n\nConceptually, \"regular\" virtualization works at the hardware level:\nit gives you multiple virtual machines (VMs)\nthat pretend to be different physical computers, on a single real machine.\nSo you can run multiple operating systems using separate VMs\non the same physical box, as in <<virtualization-diagram>>.\n\n[[virtualization-diagram]]\n.Physical versus virtual machines\nimage::images/tdd3_0901.png[\"A diagram showing a physical machine, with an operating system and a Python virtualenv running inside it, vs multiple virtual machines running different operating systems on a single real machine\"]\n\n\n// TODO; remove virtualenvs from this diagram, they just confuse things.\n// add another diagram later to contrast venvs with dockers.\n\nContainerization works at the operating system (OS) level:\nit gives you multiple virtual operating systems that\nall run on a single real OS.footnote:[\nIt's more accurate to say that containers share the same kernel as the host OS.(((\"operating system (OS), containerization at OS level\")))\nAn operating system is made up of a kernel,\nand a bunch of utility programs that run on top of it.\nThe kernel is the core of the OS;\nit's the program that runs all the other programs.\nWhenever your program needs to interact with the outside world,\nread a file, or talk to the internet, or start another program,\nit actually asks the kernel to do it.\nStarting about 15 years ago, the Linux kernel grew the ability\nto show different filesystems to different programs,\nas well as isolate them into different network and process namespaces;\nthese are the capabilities that underpin Docker and containerization.]\n\nContainers let us pack the source code(((\"containers\"))) and the system dependencies\n(like Python or system libraries) together,\nand our programs run inside separate virtual systems,\nusing a single real host OS and kernel.footnote:[\nBecause containers all share the same kernel,\nwhile virtualization can let you run Windows and Linux on the same machine,\ncontainers on Linux hosts all run Linux, and ones on Windows hosts all run Windows.\nIf you're running Linux containers on a Mac or a PC,\nit's because you're actually running them on a Linux VM under the hood.]\nSee <<containers-diagram>> for an illustration.\n\nThe upshot of this is that containers are much \"cheaper\".\nYou can start one up in milliseconds,\nand you can run hundreds on the same machine.\n\nNOTE: If you're new to all this, I know it's a lot to wrap your head around!\n  It takes a while to build a good mental model of what's happening.(((\"Docker\", \"resources on containers\")))(((\"containers\", \"Docker resources on\")))\n  Have a look at\n  https://www.docker.com/resources/what-container[Docker's resources on containers]\n  for more explanation.\n  Hopefully, following along with these chapters and seeing them working in practice\n  will help you to better understand the theory.\n\n[[containers-diagram]]\n.Containers share a kernel in the host operating system\nimage::images/tdd3_0902.png[\"Diagram showing one or more containers running on a single host operating system, showing that each container uses the kernel from the host OS, but is able to have its own filesystem, based on an image, but also possibly mounting directories from the host filesystem\"]\n\n\n==== Why Not Just Use a Virtualenv?\n\nYou might be thinking that this sounds a lot like a virtualenv—and you'd be right!\nVirtualenvs already let us run different versions of Python,\nwith different Python packages, on the same machine.(((\"virtualenv (virtual environment)\", \"capabilities of Docker and containers versus\")))(((\"Docker\", \"capabilities of containers versus virtualenvs\")))(((\"containers\", \"capabilities of versus virtualenvs\")))\n\nWhat Docker containers give us over and above virtualenvs,\nis the ability to have different _system_ dependencies too;\nthings you can't `pip install`, in other words.\nIn the Python world, this could be C libraries,\nlike `libpq` for PostgreSQL, or `libxml2` for parsing XML.\nBut you could also run totally different programming languages\nin different containers, or even different Linux distributions.\nSo, server administrators or platform people like them\nbecause it's one system for running any kind of software,\nand they don't need to understand the intricacies of any particular\nlanguage's packaging systems.\n\n\n\n\n==== Docker and Your CV\n\nThat's all well and good for the _theoretical_ justification,\nbut let's get to the _real_ reason for using this technology,\nwhich, as always, is:\n\"it's fashionable so it's going to look good on my CV\".(((\"Docker\", \"use of, asset on your CV\")))(((\"CV (curriculum vitae), Docker and\")))\n\nFor the purposes of this book,\nthat's not such a bad justification really!\n\nYes, it's going to be a nice way to have a \"pretend\"\ndeployment on our own machine, before we try the real one--but\nalso, containers are so popular nowadays,\nthat it's very likely that you're going to encounter them at work\n(if you haven't already).\nFor many working developers, a container image is the final artifact of their work;\nit's what they \"deliver\",\nand often the rest of the deployment process is something they rarely have to think about.\n\nIn any case, without further ado, let's get into it!\n\n\n\n=== As Always, Start with a Test\n\n(((\"environment variables\")))(((\"LiveServerTestCase\")))\nLet's adapt our functional tests (FTs)\nso that they can run against a standalone server,\ninstead of the one that `LiveServerTestCase` creates for us.\n\nDo you remember I said that `LiveServerTestCase` had certain limitations?\nWell, one is that it always assumes you want to use its own test server,\nwhich it makes available at `self.live_server_url`.\nI still want to be able to do that _sometimes_,\nbut I also want to be able to selectively tell it not to bother,\nand to use a real server instead.(((\"TEST_SERVER environment variable\")))\n\n[role=\"pagebreak-before\"]\nWe'll do it by checking for an environment variable\ncalled `TEST_SERVER`:\n\n//IDEA; the word \"server\" is overloaded.\n// here we mean docker containers, later we mean a real server.  TEST_HOST??\n\n\n[role=\"sourcecode\"]\n.functional_tests/tests.py (ch09l001)\n====\n[source,python]\n----\nimport os\n[...]\n\nclass NewVisitorTest(StaticLiveServerTestCase):\n    def setUp(self):\n        self.browser = webdriver.Firefox()\n        if test_server := os.environ.get(\"TEST_SERVER\"):  # <1><2>\n            self.live_server_url = \"http://\" + test_server  # <3>\n----\n====\n\n\n<1> Here's where we check for the env var.(((\"walrus operator (:&#x3D;)\")))(((\":&#x3D; (walrus) operator\")))\n\n<2> If you haven't seen this before, the `:=` is known as the \"walrus operator\"\n    (more formally, it's the operator for an \"assignment expression\"),\n    which was a controversial new feature from Python 3.8footnote:[\n    The feature was a favourite of Guido van Rossum's,\n    but the discussion around it was so toxic that Guido\n    stepped down from his role as Python's BDFL, or \"Benevolent Dictator for Life\".]\n    and it's not often useful, but it is quite neat for cases like this,\n    where you have a variable and want to do a conditional on it straight away.\n    See https://oreil.ly/oDyYs[this article]\n    for more explanation.\n\n<3> Here's the hack: we replace `self.live_server_url` with the address of\n    our \"real\" server.\n\n\nNOTE: A clarification: when we say we run tests _against_ our Docker container,\n  or _against_ our staging server,\n  that doesn't mean we run the tests _from_ Docker or _from_ our staging server.(((\"Test-Driven Development (TDD)\", \"concepts\", \"running tests against\")))\n  We still run the tests from our own laptop,\n  but they target the place that's running our code.\n\n\nWe test that said hack hasn't broken anything by running the FTs [keep-together]#\"normally\"#:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python manage.py test functional_tests*]\n[...]\nRan 3 tests in 8.544s\n\nOK\n----\n\nAnd now we can try them against our Docker server URL—which, once we've done the right Docker magic,\nwill be at _http&#58;//localhost:8888_.\n\nTIP: I'm deliberately choosing a different port to run Dockerised Django on (8888)\n    from the default port that a local `manage.py runserver` would choose (8080).\n    This is to avoid getting in the situation where I (or the tests) _think_\n    we're looking at Docker, when we're actually looking at a local `runserver`\n    that I've left running in some terminal somewhere.(((\"Django framework\", \"running Dockerized Django\")))\n\n.Ports\n*******************************************************************************\nPorts are what let you have multiple connections open at the same time on a single machine;\nthe reason you can load two different websites at the same time, for example.(((\"ports\")))(((\"network adapters, range of ports\")))\n\nEach network adapter has a range of ports, numbered from 0 to 65535.\nIn a client/server connection, the client knows the port of the server,\nand the client OS chooses a random local port for its side of the connection.\n\nWhen a server is \"listening\" on a port,\nno other service can bind to that port at the same time.\nThat's why you can't run `manage.py runserver` in two different terminals\nat the same time, because both want to use port `8080` by default.\n*******************************************************************************\n\nWe'll use the `--failfast` option to exit as soon as(((\"--failfast option\", primary-sortas=\"failfast\"))) a single test fails:\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 ./manage.py test functional_tests --failfast*]\n[...]\nE\n======================================================================\nERROR: test_can_start_a_todo_list\n(functional_tests.tests.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/tests.py\", line 38, in\ntest_can_start_a_todo_list\n    self.browser.get(self.live_server_url)\n[...]\n\nselenium.common.exceptions.WebDriverException: Message: Reached error page: abo\nut:neterror?e=connectionFailure&u=http%3A//localhost%3A8888/[...]\n\n\nRan 1 tests in 5.518s\n\nFAILED (errors=1)\n----\n\nNOTE: If, on Windows, you see an error saying something like\n    \"TEST_SERVER is not recognized as a command\",\n  it's probably because you're not using Git Bash.(((\"Git Bash\")))(((\"Bash shell (Git Bash)\")))\n  Take another look at the &#x201c;<<pre-requisites>>&#x201d; section.\n\nYou can see that our tests are failing, as expected, because we're not running Docker yet.\nSelenium reports that Firefox is seeing an error and \"cannot establish connection to the server\",\nand you can see _localhost:8888_ in there too.\n\n\nThe FT seems to be testing the right things, so let's commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff* # should show changes to functional_tests.py\n$ *git commit -am \"Hack FT runner to be able to test docker\"*\n----\n\n\nTIP: Don't use `export` to set the `TEST_SERVER`` environment variable;\n    otherwise, all your subsequent test runs in that terminal will be against staging,\n    and that can be very confusing if you're not expecting it.\n    Setting it explicitly inline each time you run the FTs is best.\n\n\n==== Making a src Folder\n\nWhen preparing a codebase for deployment,\nit's often convenient to separate out the actual source code of our production app\nfrom the rest of the files that you need in the project.\nA folder called _src_ is a common convention.(((\"src folder\")))\n\nCurrently, all our code is source code really, so we move everything into _src_\n(we'll be seeing some new files appearing outside _src_ shortly):footnote:[\nA common thing to find outside of the _src_ folder is a folder called _tests_.\nWe won't be doing that while we're relying on the standard Django test framework,\nbut it can be a good thing to do if you're using pytest, for example.]\n\n\n\n//002\n[subs=\"specialcharacters,quotes\"]\n----\n$ *mkdir src*\n$ *git mv functional_tests lists superlists manage.py src*\n$ *git commit -m \"Move all our code into a src folder\"*\n----\n\n\n=== Installing Docker\n\nThe https://docs.docker.com/get-docker[Docker documentation] is pretty good,\nand you'll find detailed installation instructions for Windows, Mac, and Linux.(((\"Docker\", \"installing\")))\n\nTIP: Choose WSL (Windows Subsystem for Linux) as your backend on Windows,\n    as we'll need it in the next chapter.(((\"WSL (Windows Subsystem for Linux)\")))(((\"Windows Subsystem for Linux (WSL)\")))\n    You can find installation instructions\n    https://learn.microsoft.com/en-us/windows/wsl/install[on the Microsoft website].\n    This doesn't mean you have to switch your development environment\n    to being \"inside\" WSL; Docker just uses WSL as a virtualization engine\n    in the background.\n    You should be able to run all the `docker` CLI commands from the\n    same Git Bash console you've been using so far.\n\n\n// TODO: appendix or link to more detailed instructions for WSL use?\n\n\n\n[[docker-alternatives]]\n.Docker Alternatives: Podman, nerdctl, etc\n*****************************************************************************************\nImpartiality commands me to also mention\nhttps://podman.io[Podman] and\nhttps://github.com/containerd/nerdctl[nerdctl],\nboth like-for-like replacements for Docker.\n\nThey are both(((\"Docker\", \"alternatives to\")))(((\"Podman\")))(((\"nerdctl\"))) pretty much exactly the same as Docker,\narguably with a few advantages even.footnote:[\nDocker uses a central \"daemon\" to manage containers,\nwhich Podman and nerdctl don't.]\n\nI actually tried Podman out on early drafts of this chapter (on Linux)\nand it worked perfectly well.\nBut they are both a little less well established and documented;\nthe Windows installation instructions are a little more DIY, for example.\nSo in the end, although I'm always a fan of a plucky noncommercial upstart,\nI decided to stick with Docker for now.  After all,\nthe core of it is still open source, to its credit!\nBut you could definitely check out one of the alternatives if you feel like it.\n\nYou can follow along all the instructions in the book\nby just substituting the `docker` binary for `podman` or `nerdctl`\nin all the CLI instructions:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker run busybox echo hello*\n# becomes\n$ *podman run busybox echo hello*\n# or\n$ *nerdctl run busybox echo hello*\n# similarly with podman build, nerdtcl build, podman ps, etc.\n----\n\n\n*****************************************************************************************\n\n.Colima: An Alternative Docker Runtime for macOS\n*****************************************************************************************\nIf you're on macOS,\nyou might find the Docker Dekstop licensing terms don't work for you.(((\"Colima, alternative container runtime for MacOS\")))\nIn that case, you can try https://github.com/abiosoft/colima[Colima],\nwhich is a \"container runtime\", essentially the backend for Docker.\nYou still use the Docker CLI tools,\nbut Colima provides the server to run the containers:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker run busybox echo hello*\ndocker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock.\nIs the docker daemon running?.\nSee 'docker run --help'.\n$ *colima start*\nINFO[0001] starting colima\nINFO[0001] runtime: docker\nINFO[0001] starting ...                                  context=vm\nINFO[0014] provisioning ...                              context=docker\nINFO[0016] starting ...                                  context=docker\nINFO[0017] done\n$ *docker run busybox echo hello*\nhello\n----\n\nI used Colima for most of the writing of this book,\nand it worked fine for me.(((\"DOCKER_HOST environment variable\")))\nThe only thing I needed to do was set the `DOCKER_HOST` environment variable,\nand that only came up in <<chapter_12_ansible>>:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *export DOCKER_HOST=unix:///$HOME/.colima/default/docker.sock\n----\n\nNOTE: On macOS, you can use Colima as a backend for nerdctl.\n  Podman ships with its own runtime, for both Mac and Windows\n  (there is no need for a runtime on Linux).\n\nAt the time of writing, Apple had just announced its own container runner,\nhttps://github.com/apple/container[_container_],\nbut it was in beta and I didn't have time to try it out.\n*****************************************************************************************\n\nTest your installation by running:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*docker run busybox echo hello world*]\nUnable to find image 'busybox:latest' locally\n[...]\nlatest: Pulling from library/busybox\n[...]: Pull complete\nDigest: sha256:[...]\nStatus: Downloaded newer image for busybox:latest\nhello world\n----\n\nWhat's happened there is that Docker has:\n\n* Searched for a local copy of the \"busybox\" image and not found it\n* Downloaded the image from Docker Hub\n* Created a container based on that image\n* Started up that container, telling it to run `echo hello world`\n* And we can see it worked!\n\nCool! We'll find out more about all of these steps as the chapter progresses.\n\n\nNOTE: On macOS, if you get errors saying `command not found: docker`,\n  obviously the first thing you should do is Google for \"macOS command not found Docker\",\n  but at least one reader has reported that the solution was\n  Docker Desktop > Settings > Advanced > Change from User to System.\n\n\n=== Building a Docker Image and Running a Docker Container\n\nDocker has the concepts of _images_ as well as containers.\nAn image is essentially a pre-prepared root filesystem,\nincluding the OS, dependencies, and any code you want to run.(((\"images (container)\")))(((\"Docker\", \"building image and running a container\", id=\"ix_Dckimg\")))\n\nOnce you have an image, you can run one or more containers that use the same image.\nIt's a bit like saying, once you've installed your OS and software,\nyou can start up your computer and run that software any number of times,\nwithout needing to change anything else.\n\nAnother way of thinking about it is: images are like classes,\nand containers are like instances.\n\n\n==== A First Cut of a Dockerfile\n\nThink of a Dockerfile as instructions for setting up a brand new computer\nthat we're going to use to run our Django server on.(((\"Docker\", \"building image and running a container\", \"first draft of Dockerfile\")))(((\"Dockerfiles\")))\nWhat do we need to do?  Something like this, right?\n\n1. Install an operating system.\n2. Make sure it has Python on it.\n3. Get our source code onto it.\n4. Run `python manage.py runserver`.\n\n\nWe create a new file called _Dockerfile_ in the base folder of our repo,\nnext to the _src/_ directory we made earlier:\n\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l003)\n====\n[source,dockerfile]\n----\nFROM python:3.14-slim  # <1>\n\nCOPY src /src  # <2>\n\nWORKDIR /src  # <3>\n\nCMD [\"python\", \"manage.py\", \"runserver\"]  # <4>\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> The `FROM` line is usually the first thing in a Dockerfile,\n    and it says which _base image_ we are starting from.\n    Docker images are built from other Docker images!\n    It's not quite turtles all the way down, but almost.\n    So this is the equivalent of choosing a base OS,\n    but images can actually have lots of software preinstalled too.\n    You can browse various base images on Docker Hub.\n    We're using https://hub.docker.com/_/python[one that's published by the Python Software Foundation],\n    called \"slim\" because it's as small as possible.\n    It's based on a popular version of Linux called Debian,\n    and of course it comes with Python already installed on it.\n\n<2> The `COPY` instruction (the uppercase words are called \"instructions\")\n    lets you copy files from your own computer into the container image.\n    We use it to copy all our source code from the newly created _src_ folder,\n    into a similarly named folder at the root of the container image.\n\n<3> `WORKDIR` sets the current working directory for all subsequent commands.\n     It's a bit like doing `cd /src`.\n\n<4> Finally, the `CMD` instruction tells Docker which command you want it to run\n    by default, when you start a container based on that image.\n    The syntax is a bit like a Python list\n    (although it's actually parsed as a JSON array, so you _have_ to use double quotes).\n\n\nIt's probably worth just showing a directory tree,\nto make sure everything is in the right place, right?\nAll our source code is in a folder called _src_,\nnext to our `Dockerfile`:\n\n[[tree-with-src-and-dockerfile]]\n[subs=\"specialcharacters,macros\"]\n----\n.\n├── Dockerfile\n├── db.sqlite3\n├── src\n│   ├── functional_tests\n│   │   ├── [...]\n│   ├── lists\n│   │   ├── [...]\n│   ├── manage.py\n│   └── superlists\n│       ├── [...]\n└── static\n    └── [...]\n----\n\n// TODO: figure out what to do with the /static folder\n\n[role=\"pagebreak-before less_space\"]\n==== Docker Build\n\nYou build an image with `docker build <path-containing-dockerfile>`\nand we'll use the `-t <tagname>` argument to \"tag\" our image\nwith a memorable name.(((\"Docker\", \"building image and running a container\", \"docker build command\")))\n\nIt's typical to invoke `docker build` from the folder that contains your Dockerfile,\nso the last argument is usually `.`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*docker build -t superlists .*]\n[+] Building 1.2s (8/8) FINISHED                            docker:default\n => [internal] load build definition from Dockerfile                  0.0s\n => => transferring dockerfile: 115B                                  0.0s\n => [internal] load .dockerignore                                     0.1s\n => => transferring context: 2B                                       0.0s\n => [internal] load metadata for docker.io/library/python:slim        3.4s\n => [internal] load build context                                     0.2s\n => => transferring context: 68.54kB                                  0.1s\n => [1/3] FROM docker.io/library/python:3.14-slim@sha256:858[...]     4.4s\n => => resolve docker.io/library/python:3.14-slim@sha256:858[...]     0.0s\n => => sha256:72ba3400286b233f3cce28e35841ed58c9e775d69cf11f[...]     0.0s\n => => sha256:3a72e7f66e827fbb943c494df71d2ae024d0b1db543bf6[...]     0.0s\n => => sha256:a7d9a0ac6293889b2e134861072f9099a06d78ca983d71[...]     0.5s\n => => sha256:426290db15737ca92fe1ee6ff4f450dd43dfc093e92804[...]     4.0s\n => => sha256:e8b685ab0b21e0c114aa94b28237721d66087c2bb53932[...]     0.5s\n => => sha256:85824326bc4ae27a1abb5bc0dd9e08847aa5fe73d8afb5[...]     0.0s\n => => extracting sha256:a7d9a0ac6293889b2e134861072f9099a06[...]     0.1s\n => => extracting sha256:426290db15737ca92fe1ee6ff4f450dd43d[...]     0.4s\n => => extracting sha256:e8b685ab0b21e0c114aa94b28237721d660[...]     0.0s\n => [internal] load build context                                     0.0s\n => => transferring context: 7.56kB                                   0.0s\n => [2/3] COPY src /src                                               0.2\n => [3/3] WORKDIR /src                                                0.1s\n => exporting to image                                                0.0s\n => => exporting layers                                               0.0s\n => => writing image sha256:7b8e1c9fa68e7bad7994fa41e2aca852ca79f01a  0.0s\n => => naming to docker.io/library/superlists                         0.0s\n----\n\n[role=\"pagebreak-before\"]\nNow we can see our image in the list of Docker images on the system:\n\n// IDEA, this listing was hard to test due to column widths but there must be a way\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker images*\nREPOSITORY   TAG       IMAGE ID       CREATED          SIZE\nsuperlists   latest    522824a399de   2 minutes ago    164MB\n[...]\n----\n\n\n\nNOTE: If you see an error about `failed to solve / compute cache key` and `src: not found`,\n  it may be because you saved the Dockerfile in the wrong place.\n  Have another look at the directory tree from earlier.\n\n\n\n==== Docker Run\n\nOnce you've built an image,\nyou can run one or more containers based on that image, using `docker run`.\nWhat happens when we run ours?(((\"Docker\", \"building image and running a container\", \"docker run command\")))\n\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*docker run superlists*]\nTraceback (most recent call last):\n  File \"/src/manage.py\", line 11, in main\n    from django.core.management import execute_from_command_line\nModuleNotFoundError: No module named 'django'\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File \"/src/manage.py\", line 22, in <module>\n    main()\n    ~~~~^^\n  File \"/src/manage.py\", line 13, in main\n    raise ImportError(\n    ...<3 lines>...\n    ) from exc\nImportError: Couldn't import Django. Are you sure it's installed and available\non your PYTHONPATH environment variable? Did you forget to activate a virtual\nenvironment?\n----\n\n\nAh, we forgot that we need to install Django.(((\"Docker\", \"building image and running a container\", startref=\"ix_Dckimg\")))\n\n[role=\"pagebreak-before less_space\"]\n=== Installing Django in a Virtualenv in Our Container Image\n\nJust like on our own machine,\na virtualenv(((\"Django framework\", \"installing in virtualenv in container image\", id=\"ix_Djainctnrimg\")))(((\"containers\", \"installing Django in virtualenv in container image\", id=\"ix_cntnrDja\")))(((\"virtualenv (virtual environment)\", \"installing Django in virtualenv in container image\"))) is useful in a deployed environment to make\nsure we have full control over the packages installed\nfor a particular project.footnote:[\nEven a completely fresh Linux install might have odd things installed\nin its system site packages.\nA virtualenv is a guaranteed clean slate.]\n\nWe can create a virtualenv in our Dockerfile\njust like we did on our own machine with `python -m venv`,\nand then we can use `pip install` to get Django:\n\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l004)\n====\n[source,dockerfile]\n----\nFROM python:3.14-slim\n\nRUN python -m venv /venv  <1>\nENV PATH=\"/venv/bin:$PATH\"  <2>\n\nRUN pip install \"django<6\" <3>\n\nCOPY src /src\n\nWORKDIR /src\n\nCMD [\"python\", \"manage.py\", \"runserver\"]\n----\n====\n\n<1> Here's where we create our virtualenv.\n    We use the `RUN` Dockerfile directive,\n    which is how you run arbitrary shell commands as part of\n    building your Docker image.\n\n<2> You can't really \"activate\" a virtualenv inside a Dockerfile,\n    so instead we change the system path so that the venv versions\n    of `pip` and `python` become the default ones\n    (this is actually one of the things that `activate` does, under the hood).\n\n<3> We install Django with `pip install`, just like we do locally.\n\n\n[role=\"pagebreak-before less_space\"]\n==== Successful Run\n\nLet's do the `build` and `run` in a single line.\nThis is a pattern I used quite often when developing a Dockerfile,\nto be able to quickly rebuild and see the effect of a change:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run -it superlists*\n[+] Building 0.2s (11/11) FINISHED                                  docker:default\n[...]\n => [internal] load .dockerignore                                   0.1s\n => => transferring context: 2B                                     0.0s\n => [internal] load build definition from Dockerfile                0.0s\n => => transferring dockerfile: 246B                                0.0s\n => [internal] load metadata for docker.io/library/python:slim      0.0s\n => CACHED [1/5] FROM docker.io/library/python:slim                 0.0s\n => [internal] load build context                                   0.0s\n => => transferring context: 4.75kB                                 0.0s\n => [2/5] RUN python -m venv /venv                                  0.0s\n => [3/5] pip install \"django<6\"                                    0.0s\n => [4/5] COPY src /src                                             0.0s\n => [5/5] WORKDIR /src                                              0.0s\n => exporting to image                                              0.0s\n => => exporting layers                                             0.0s\n => => writing image sha256:[...]                                   0.0s\n => => naming to docker.io/library/superlists                       0.0s\nWatching for file changes with StatReloader\nPerforming system checks...\n\nSystem check identified no issues (0 silenced).\n\nYou have 19 unapplied migration(s). Your project may not [...]\n[...]\nDjango version 5.2, using settings 'superlists.settings'\nStarting development server at http://127.0.0.1:8000/\nQuit the server with CONTROL-C.\n----\n\n\nOK, scanning through that, it looks like the server is running!\n\n\nWARNING: Make sure you use the `-it` flags to the Docker `run`\n    command when running `runserver`, or any other tool that expects\n    to be run in an interactive terminal session,\n    otherwise you'll get strange behaviour, including not being able\n    to interrupt the Docker process with Ctrl+C.\n    See the following sidebar for an escape hatch.\n\n[role=\"pagebreak-before less_space\"]\n[[how-to-stop-a-docker-container]]\n.How to Stop a Docker Container\n*******************************************************************************\nIf you've got a container that's \"hanging\" in a terminal window,\nyou can stop it from another terminal.\n\nThe Docker daemon lets you list all the currently running containers\nwith `docker ps`:\n\n[role=\"skipme small-code\"]\n[subs=\"quotes\"]\n----\n$ *docker ps*\nCONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS\nPORTS     NAMES\n0818e1b8e9bf   superlists   \"/bin/sh -c 'python …\"   4 seconds ago   Up 4\nseconds             hardcore_moore\n----\n\nThis tells us a bit about each container, including a unique ID\nand a randomly-generated name (you can override that if you want to).\n\nWe can use the ID or the name to terminate the container with `docker stop`:footnote:[\nThere is also a `docker kill` if you're in a hurry.\nBut `docker stop` will send a `SIGKILL` if its initial `SIGTERM`\ndoesn't work within a certain timeout (more info in\nhttps://docs.docker.com/reference/cli/docker/container/stop[the Docker docs]).]\n\n[role=\"skipme\"]\n[subs=\"quotes\"]\n----\n$ *docker stop 0818e1b8e9bf*\n0818e1b8e9bf\n----\n\nAnd if you go back to your other terminal window,\nyou should find that the Docker process has been terminated.(((\"containers\", \"installing Django in virtualenv in container image\", startref=\"ix_cntnrDja\")))(((\"Django framework\", \"installing in virtualenv in container image\", startref=\"ix_Djainctnrimg\")))\n\n*******************************************************************************\n\n\n\n=== Using the FT to Check That Our Container Works\n\nLet's see what our FTs think about (((\"containers\", \"checking that Docker container works\")))this Docker version of our site:\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 ./src/manage.py test src/functional_tests --failfast*]\n[...]\nselenium.common.exceptions.WebDriverException: Message: Reached error page: abo\nut:neterror?e=connectionFailure&u=http%3A//localhost%3A8888/[...]\n----\n\nWhat's going on here?  Time for a little debugging.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Debugging Container Networking Problems\n\n(((\"debugging\", \"of container networking problems\", secondary-sortas=\"container\")))\n(((\"containers\", \"debugging networking problems for\")))\nFirst, let's try and take a look ourselves, in our browser,\nby\ngoing to http://localhost:8888/, as in <<firefox-unable-to-connect-screenshot>>.\n\n[[firefox-unable-to-connect-screenshot]]\n.Cannot connect on that port\nimage::images/tdd3_0903.png[\"Firefox showing the 'Unable to connect' error\"]\n\nNow, let's take another look at the output from our `docker run`.\nHere's what appeared right at the end:\n\n\n[role=\"skipme\"]\n----\nStarting development server at http://127.0.0.1:8000/\nQuit the server with CONTROL-C.\n----\n\nAha!  We notice that we're using the wrong port, the default `8000` instead of the `8888`\nthat we specified in the `TEST_SERVER` environment variable (or, \"env var\").\n\nLet's fix that by amending the `CMD` instruction in the Dockerfile:\n\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l005)\n====\n[source,dockerfile]\n----\n[...]\nWORKDIR /src\n\nCMD [\"python\", \"manage.py\", \"runserver\", \"8888\"]\n----\n====\n\nCtrl+C the current Dockerized container process if it's still running in your terminal,\nthen give it another `build && run`:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run -it superlists*\n[...]\nStarting development server at http://127.0.0.1:8888/\n----\n\n[role=\"pagebreak-before less_space\"]\n==== Debugging Web Server Connectivity with curl\n\nA quick run of the FT or check in our browser will show us that nope, that doesn't work either.(((\"debugging\", \"of web server connectivity using curl\", secondary-sortas=\"web\")))(((\"curl utility\")))\nLet's try an even lower-level smoke test, the traditional Unix utility `curl`.\nIt's a command-line tool for making HTTP requests.footnote:[\n`curl` can do FTP (File Transfer Protocol) and many other types of network requests too! Check out the https://man7.org/linux/man-pages/man1/curl.1.html[`curl` manual].]\nTry it on your own computer first:\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*curl -iv localhost:8888*]\n*   Trying 127.0.0.1:8888...\n* connect to 127.0.0.1 port 8888 [...]\n*   Trying [::1]:8888...\n* connect to ::1 port 8888 [...]\n* Failed to connect to localhost port 8888 after 0 ms: [...]\n* Closing connection\n[...]\ncurl: (7) Failed to connect to localhost port 8888 after 0 ms: [...]\n----\n\nTIP: The `-iv` flag to `curl` is useful for debugging.\n    It prints verbose output, as well as full HTTP headers.\n\n\n\n=== Running Code \"Inside\" the Container with docker exec\n\nSo, we can't see Django running on port `8888` when we're _outside_ the container.\nWhat do we see if we run things from _inside_ the container?(((\"containers\", \"running code inside with docker exec\", id=\"ix_cntnrrun\")))(((\"Docker\", \"running code inside container with docker exec\", id=\"ix_Dckexec\")))\n\nWe can use `docker exec` to run commands inside a running container.\nFirst, we need to get the name or ID of the container:\n\n// TODO use --name arg to docker run??\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker ps*\nCONTAINER ID   IMAGE        COMMAND                  CREATED          STATUS\nPORTS     NAMES\n5ed84681fdf8   superlists   \"/bin/sh -c 'python …\"   12 minutes ago   Up 12\nminutes             trusting_wu\n----\n\nYour values for `CONTAINER_ID` and `NAMES` will be different from mine,\nbecause they're randomly generated.\nBut make a note of one or the other, and then run `docker exec -it <container-id> bash`.\nOn most platforms, you can use tab completion for the container ID or name.\n\nLet's try it now.  Notice that the shell prompt will change from your default Bash prompt\nto `root@container-id`.  Watch out for those in future listings,\nso that you can be sure of what's being run inside versus outside containers.\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*docker exec -it container-id-or-name bash*]\nroot@5ed84681fdf8:/src# pass:specialcharacters,quotes[*apt-get update && apt-get install -y curl*]\nGet:1 pass:[http://deb.debian.org/debian] bookworm InRelease [151 kB]\nGet:2 pass:[http://deb.debian.org/debian] bookworm-updates InRelease [52.1 kB]\n[...]\nReading package lists... Done\nBuilding dependency tree... Done\nReading state information... Done\nThe following additional packages will be installed:\n  libbrotli1 libcurl4 libldap-2.5-0 libldap-common libnghttp2-14 libpsl5\n[...]\nroot@5ed84681fdf8:/src# pass:quotes[*curl -iv http://localhost:8888*]\n*   Trying [...]\n* Connected to localhost [...]\n> GET / HTTP/1.1\n> Host: localhost:8888\n> User-Agent: curl/8.6.0\n> Accept: */*\n>\n< HTTP/1.1 200 OK\nHTTP/1.1 200 OK\n[...]\n<!doctype html>\n<html lang=\"en\">\n\n  <head>\n    <title>To-Do lists</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"/static/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n  </head>\n\n  <body>\n    [...]\n  </body>\n\n</html>\n----\n\nTIP:  Use Ctrl+D to exit from the `docker exec` bash shell inside the container.\n\nThat's definitely some HTML! And the `<title>To-Do lists</title>` looks like it's our HTML, too.\n\nSo, we can see Django is serving our site _inside_ the container. Why can't we see it _outside_?\n\n==== Docker Port Mapping\n\nThe (highly, highly recommend) PythonSpeed guide to Docker's very first section is called\nhttps://oreil.ly/e3gYQ[Connection refused?],\nso I'll refer you there once again for an _excellent_, detailed explanation.(((\"ports\", \"Docker port mapping\")))\n\nBut in short: Docker runs in its own little world;\nspecifically, it has its own little network,\nso the ports _inside_ the container are different\nfrom the ports _outside_ the container, the ones we can see on our host machine.\n\nSo, we need to tell Docker to connect the internal ports to the outside ones—to \"publish\" or \"map\" them, in Docker terminology.\n\n`docker run` takes a `-p` argument, with the syntax `OUTSIDE:INSIDE`.\nSo, you can actually map a different port number on the inside and outside.\nBut we're just mapping `8888` to `8888`, and that will look like this:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run -p 8888:8888 -it superlists*\n----\n\nNow that will _change_ the error we see, but only quite subtly (see <<firefox-connection-reset>>).footnote:[\nTip: If you use Chrome as your web browser,\nits error is something like \"localhost didn’t send any data. ERR_EMPTY_RESPONSE\".]\nThings clearly aren't working yet.\n\n\n[[firefox-connection-reset]]\n.Cannot connect on that port\nimage::images/tdd3_0904.png[\"Firefox showing the 'Connection reset' error\"]\n\n// FT would show this\n// selenium.common.exceptions.WebDriverException: Message: Reached error page: about:neterror?e=netReset&u=http%3A//localhost%3A8888/&c=UTF-8&d=The%20connection%20to%20the%20server%20was%20reset%20while%20the%20page%20was%20loading.\n\n[role=\"pagebreak-before\"]\nSimilarly, if you try our `curl -iv` (outside the container) once again,\nyou'll see the error has changed from \"Failed to connect\",\nto \"Empty reply\":\n\n// CI consistently says \"connection reset by peer\",\n// locally it's empty reply, no matter what curl version\n\n[role=\"ignore-errors skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*curl -iv localhost:8888*]\n*   Trying [...]\n* Connected to localhost (127.0.0.1) port 8888\n> GET / HTTP/1.1\n> Host: localhost:8888\n> User-Agent: curl/8.6.0\n> Accept: */*\n[...]\n* Empty reply from server\n* Closing connection\ncurl: (52) Empty reply from server\n----\n\nNOTE: Depending on your system, instead of `(52) Empty reply from server`,\n  you might see `(56) Recv failure: Connection reset by peer`.\n  They mean the same thing: we can connect but we don't get a response.\n\n\n==== Essential Googling the Error Message\n\nThe need to map ports and the `-p` argument to `docker run` are something you just pick up,\nfairly early on in learning Docker.(((\"error messages\", \"Django runserver inside Docker, access problem\")))(((\"&quot;Googling the error message&quot; technique\", primary-sortas=\"Googling\")))\nBut the next debugging step is quite a bit more obscure—although admittedly Itamar does address it in his\nhttps://oreil.ly/VAQhF[Docker networking article] (did I already mention how excellent it is?).\n\n\nBut if we haven't read that, we can always resort to the tried and tested\n\"Googling the error message\" technique instead (<<googling-the-error>>).\n\n\n[[googling-the-error]]\n.An indispensable publication (source: https://oreil.ly/2WptY[Hacker News])\nimage::images/tdd3_0905.png[\"Cover of a fake O'Reilly book called Essential Googling the Error Message\",400]\n\n[role=\"pagebreak-before\"]\nEveryone's search results are a little different,\nand mine are perhaps shaped by years of working with Docker and Django,\nbut I found the answer in my very first result\n(see <<google-results-screenshot>>),\nwhen I searched for \"cannot access Django runserver inside Docker\".\nThe result was was a https://oreil.ly/E_4ed[Stack Overflow post],\nsaying something about needing to specify `0.0.0.0` as the IP address.footnote:[\nKids these days will probably ask an AI right?\nI have to say, I tried it out, with the prompt being\n\"I'm trying to run Django inside a Docker container,\nand I've mapped port 8888, but I still can't connect.\nCan you suggest what the problem might be?\",\nand it come up with a pretty good answer.]\n\n\n[[google-results-screenshot]]\n.Google can still deliver results\nimage::images/tdd3_0906.png[\"Google results with a useful stackoverflow post in first position\",1000]\n\n\nWe're nearing the edges of my understanding of Docker now,\nbut as I understand it, `runserver` binds to `127.0.0.1` by default.\nHowever, that IP address doesn't correspond to a network adapter _inside_\nthe container, which is actually connected to the outside world\nvia the port mapping we defined earlier.\n\n[role=\"pagebreak-before\"]\nThe long and short of it is that\nwe need use the long-form `ipaddr:port` version of the `runserver` command,\nusing the magic \"wildcard\" IP address, `0.0.0.0`:\n\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l007)\n====\n[source,dockerfile]\n----\n[...]\nWORKDIR /src\n\nCMD [\"python\", \"manage.py\", \"runserver\", \"0.0.0.0:8888\"]\n----\n====\n\n\nRebuild and rerun your server, and if you have eagle eyes,\nyou'll spot it's binding to `0.0.0.0` instead of `127.0.0.1`:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run -p 8888:8888 -it superlists*\n[...]\nStarting development server at http://0.0.0.0:8888/\n----\n\n\nWe can verify it's working with `curl`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*curl -iv localhost:8888*]\n*   Trying [...]\n* Connected to localhost [...]\n[...]\n\n  </body>\n\n</html>\n----\n\nLooking good!(((\"Docker\", \"running code inside container with docker exec\", startref=\"ix_Dckexec\")))(((\"containers\", \"running code inside with docker exec\", startref=\"ix_cntnrrun\")))\n\n\n.On Debugging\n*******************************************************************************\nLet me let you in on a little secret: I'm actually not that good at debugging.\nWe all have our psychological strengths and weaknesses,\nand one of my weaknesses is that\nwhen I run into a problem that I can't see an obvious solution to,\nI want to throw up my hands way too soon\nand say \"well, this is hopeless; it can't be fixed\",\nand give up.(((\"debugging\", \"patience and tenacity in\")))\n\nThankfully I have had some good role models over the years\nwho are much better at it than me (hi, Glenn!).\nDebugging needs the patience and tenacity of a bloodhound.\nIf at first you don't succeed,\nyou need to systematically rule out options,\ncheck your assumptions,\neliminate various aspects of the problem, simplify things down, and\nfind the parts that do and don't work,\nuntil you eventually find the cause.\n\nIt might seems hopeless at first! But you usually get there eventually.\n\n*******************************************************************************\n\n[role=\"pagebreak-before less_space\"]\n=== Database Migrations\n\n(((\"database migrations\", \"into Docker container\", secondary-sortas=\"Docker\", id=\"ix_DBmigDck\")))(((\"Docker\", \"testing database migrations in\", id=\"ix_Dcktstdb\")))\nA quick visual inspection confirms--the site is up (<<site-in-docker-is-up>>)!\n\n[[site-in-docker-is-up]]\n.The site in Docker is up!\nimage::images/tdd3_0907.png[\"The front page of the site, at least, is up\"]\n\n\nLet's see what our functional tests say:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 ./src/manage.py test src/functional_tests --failfast*]\n[...]\nE\n======================================================================\nERROR: test_can_start_a_todo_list\n(functional_tests.tests.NewVisitorTest.test_can_start_a_todo_list)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/tests.py\", line 56, in\ntest_can_start_a_todo_list\n    self.wait_for_row_in_list_table(\"1: Buy peacock feathers\")\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"...goat-book/src/functional_tests/tests.py\", line 26, in\nwait_for_row_in_list_table\n    table = self.browser.find_element(By.ID, \"id_list_table\")\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; For documentation [...]\n----\n\nAlthough the FTs can connect happily and interact with our site,\nthey are failing as soon as they try to submit a new item.\n\nYou might have spotted the yellow Django debug page (<<django-debug-screen>>)\ntelling us why.\nIt's because we haven't set up the database\n(which, as you may remember, we highlighted as one of the \"danger areas\" of deployment).\n\n[[django-debug-screen]]\n.But the database isn't\nimage::images/tdd3_0908.png[\"Django DEBUG page showing database error\"]\n\nNOTE: The tests saved us from potential embarrassment there.\n    The site _looked_ fine when we loaded its front page.\n    If we'd been a little hasty and only tested manually,\n    we might have thought we were done,\n    and it would have been the first users that discovered that nasty Django debug page.\n    Okay, slight exaggeration for effect—maybe we _would_ have checked,\n    but what happens as the site gets bigger and more complex?\n    You can't check everything. The tests can.\n\n\n\nTo be fair, if you look back through the `runserver` command output\neach time we've been starting our container,\nyou'll see it's been warning us about this issue:\n\n\n[role=\"skipme\"]\n----\nYou have 19 unapplied migration(s). Your project may not work properly until\nyou apply the migrations for app(s): auth, contenttypes, lists, sessions.\nRun 'python manage.py migrate' to apply them.\n----\n\n\n\nNOTE: If you don't see this error,\n    it's because your _src_ folder had the database file in it, unlike mine.\n    For the sake of argument,\n    run `rm src/db.sqlite3` and rerun the build and run commands,\n    and you should be able to reproduce the error.  I promise it's instructive!\n\n\n==== Should We Run migrate Inside the Dockerfile? No.\n\nSo, should we include `manage.py migrate` in our Dockerfile?\n\nIf you try it, you'll find it certainly _seems_ to fix the(((\"Dockerfiles\", \"database migrations and\"))) problem:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l008)\n====\n[source,dockerfile]\n----\n[...]\nWORKDIR /src\n\nRUN python manage.py migrate --noinput  <1>\n\nCMD [\"python\", \"manage.py\", \"runserver\", \"0.0.0.0:8888\"]\n----\n====\n\n<1> We run `migrate` using the `--noinput` argument to suppress any little \"are you sure\" prompts.\n\n\nIf we rebuild the image...\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run -p 8888:8888 -it superlists*\n[...]\nStarting development server at http://0.0.0.0:8888/\n----\n\n...and try our FTs again, they all pass!\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 ./src/manage.py test src/functional_tests --failfast*]\n[...]\n...\n ---------------------------------------------------------------------\nRan 3 tests in 26.965s\n\nOK\n----\n\nThe problem is that this saves our database file into our system image,\nwhich is not what we want,\nbecause the system image is meant to be something fixed and stateless (whereas the database is living, stateful data that should change over time).\n\n[role=\"pagebreak-before less_space\"]\n.What Would Happen if We Kept the Database File in the Image\n*******************************************************************************\nYou can try this as a little experiment.\nAssuming you've got the `manage.py migrate` line in your Dockerfile:\n\n1. Create a new to-do list and keep a note of its URL (e.g., at _http&#58;//localhost:8888/lists/1_).\n2. Now, `docker stop` your container, and rebuild a new one with the same\n  `build && run` command we used earlier.\n\n3. Go back and try to retrieve your old list.  It's gone!\n\nThis is because rebuilding the image\nwill give us a brand new database each time.\n\nWhat we actually want is for our database storage to be \"outside\" the container somehow,\nso it can persist between different versions of our Docker image.(((\"database migrations\", \"into Docker container\", secondary-sortas=\"Docker\", startref=\"ix_DBmigDck\")))(((\"Docker\", \"testing database migrations in\", startref=\"ix_Dcktstdb\")))\n\n*******************************************************************************\n\n=== Mounting Files Inside the Container\n\nWe want the database on the server to be totally separate data from the data in\nthe system image. (((\"Docker\", \"mounting files in\")))(((\"containers\", \"mounting files in Docker\")))In most deployments, you'd probably be talking to a separate database server,\nlike PostgreSQL. For the purposes of this book,\nthe easiest analogy for a database that's \"outside\" our container is to access the database from the filesystem outside the container.\n\nThat also gives us a convenient excuse to talk about mounting files in Docker,\nwhich is a very Useful Thing to be Able to Do (TM).\n\n\nFirst, let's revert our change:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch09l009)\n====\n[source,dockerfile]\n----\n[...]\nCOPY src /src\n\nWORKDIR /src\n\nCMD [\"python\", \"manage.py\", \"runserver\", \"0.0.0.0:8888\"]\n----\n====\n\n\nThen, let's make sure we _do_ have the database on our local filesystem,\nby running `migrate` (when we moved everything into _./src_, we left the database file behind):\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *./src/manage.py migrate --noinput*\nOperations to perform:\n  Apply all migrations: auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying contenttypes.0001_initial... OK\n[...]\n  Applying sessions.0001_initial... OK\n----\n\nLet's make sure to _.gitignore_ the new location of the database file,\nand we'll also use a file called https://docs.docker.com/reference/dockerfile/#dockerignore-file[_.dockerignore_]\nto make sure we can't copy our local dev database into our Docker image\nduring Docker builds:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo src/db.sqlite3 >> .gitignore*\n$ *echo src/db.sqlite3 >> .dockerignore*\n----\n//ch09l010, ch09l011\n\nNow we rebuild, and try mounting our database file.\nThe extra flag to add to the Docker run command is `--mount`,\nwhere we specify `type=bind`, the `source` path on our machine,footnote:[\nIf you're wondering about the `$PWD` in the listing,\nit's a special environment variable that represents the current directory.\nThe initials echo the `pwd` command, which stands for \"print working directory\".\nDocker requires mount paths to be absolute paths.]\nand the `target` path _inside_ the container:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -it superlists*\n----\n\nTIP: You're likely to come across the old syntax for mounts, which was `-v`.\n    One of the advantages of the new `--mount` version is that\n    it will fail hard if the path you're trying to mount does not exist—it says something like `bind source path does not exist`. This avoids a lot of pain (ask me how I know this).\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 ./src/manage.py test src/functional_tests --failfast*]\n[...]\n...\n ---------------------------------------------------------------------\nRan 3 tests in 26.965s\n\nOK\n----\n\nAMAZING, IT ACTUALLY WORKSSSSSSSS.\n\nAhem, that's definitely good enough for now!  Let's commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add -A .*  # add Dockerfile, .dockerignore, .gitignore\n$ *git commit -am\"First cut of a Dockerfile\"*\n----\n\nPhew.  Well, it took a bit of hacking about,\nbut now we can be reassured that the basic Docker plumbing works.\nNotice that the FT was able to guide us incrementally towards a working config,\nand spot problems early on (like the missing database).\n\nBut we really can't be using the Django dev server in production,\nor running on port `8888` forever.\nIn the next chapter, we'll make our hacky image more production-ready.\n\nBut first, time for a well-earned tea break I think, and perhaps a\nhttps://oreil.ly/GtL7w[chocolate biscuit].\n\n\n.Docker Recap\n*******************************************************************************\n\nDocker lets us reproduce a server environment on our own machine::\n    For developers, ops and infra work is always \"fun\",\n    by which I mean a process full of fear, uncertainty, and surprises—and painfully slow too.\n    Docker helps to minimise this pain by giving us a mini server on our own machine,\n    which we can try things out with and get feedback quickly,\n    as well as enable us to work in small steps.\n\n`docker build && docker run`::\n    We've learned the core tools for working with Docker.\n    The Dockerfile specifies our image, `docker build` builds it,\n    and `docker run` runs it.\n    `build && run` together give us a \"start again from scratch\" cycle,\n    which we use every time we make a code change in _src_,\n    or a change in the Dockerfile.footnote:[\n    There's a common pattern of mounting the whole _src_ folder into\n    your Docker containers in local dev.\n    It means you don't need to rebuild for every source code change.\n    I didn't wan't to introduce that here because it also leads to\n    subtle behaviours that can be hard to wrap your head around,\n    like the _db.sqlite3_ file being shared with the container.\n    For this book, the `build && run` cycle is fast enough,\n    but by all means try out mounting _src_ in your own projects.]\n\nDebugging network issues::\n    We've seen how to use `curl` both outside and inside the container\n    with `docker exec`.\n    We've also seen the `-p` argument to bind ports inside and outside,\n    and the idea of needing to bind to `0.0.0.0`.\n\nMounting files::\n    We've also had a brief intro to mounting files from outside\n    the container, into the inside.\n    It's an insight into the difference between the \"stateless\"\n    system image, and the stateful world outside of Docker.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_10_production_readiness.asciidoc",
    "content": "[[chapter_10_production_readiness]]\n== Making Our App Production-Ready\n\nOur container is working fine but it's not production-ready.\nLet's try to get it there, using the tests to keep us safe.(((\"containers\", \"making production-ready\", id=\"ix_cntnrprd\")))\n\nIn a way we're applying the red/green/refactor cycle to our productionisation process.\nOur hacky container config got us to green, and now we're going to refactor,\nworking incrementally (just as we would while coding),\ntrying to move from working state to working state,\nand using the FTs to detect any regressions.\n\n=== What We Need to Do\n\nWhat's wrong with our hacky container image?\nA few things: first, we need to host our app on the \"normal\" port `80`\nso that people can access it using a regular URL.\n\nPerhaps more importantly, we shouldn't use the Django dev server for production;\nit's not designed for real-life workloads.\nInstead, we'll use the popular Gunicorn Python WSGI HTTP server.\n\nNOTE: Django's `runserver` is built and optimised for local development and debugging.\n  It's designed to handle one user at(((\"Django framework\", \"runserver, limitations of\"))) a time;\n  it handles automatic reloading upon saving of the source code,\n  but it isn't optimised for performance,\n  nor has it been hardened against security vulnerabilities.\n\n(((\"DEBUG settings\")))\nIn addition, several options in _settings.py_ are currently unacceptable.\n`DEBUG=True` is strongly discouraged for production,\nwe'll want to set a unique `SECRET_KEY`\nand, as we'll see, other things will come up.\n\nWARNING: `DEBUG=True` is considered a security risk,\n  because the Django debug page will display sensitive information like\n  the values of variables, and most of the settings in _settings.py_.\n\n\nLet's go through and see if we can fix things one by one.\n\n=== Switching to Gunicorn\n\n(((\"production-ready deployment\", \"using Gunicorn\", secondary-sortas=\"Gunicorn\")))\n(((\"Gunicorn\", \"switching to\")))\nDo you know why the Django mascot is a pony?\nThe story is that Django comes with so many things you want:\nan ORM, all sorts of middleware, the admin site...“What else do you want, a pony?” Well, Gunicorn stands for \"Green Unicorn\",\nwhich I guess is what you'd want next if you already had a pony...\n\nWe'll need to first install Gunicorn into our container,\nand then use it instead of `runserver`:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python -m pip install gunicorn*\nCollecting gunicorn\n[...]\nSuccessfully installed gunicorn-2[...]\n----\n\n\nGunicorn will need to know a path to a \"WSGI server\"footnote:[\nWSGI stands for Web Server Gateway Interface and it's the protocol\nfor communication(((\"Web Server Gateway Interface (WSGI)\")))(((\"WSGI (Web Server Gateway Interface)\"))) between a web server and a Python web application.\nGunicorn is a web server that uses WSGI to interact with Django,\nand so is the web server you get from `runserver`.]\nwhich is usually a function called `application`.\nDjango provides one in 'superlists/wsgi.py'.\nLet's change the command that our image runs:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch10l001)\n====\n[source,dockerfile]\n----\n[...]\nRUN pip install \"django<6\" gunicorn  # <1>\n\nCOPY src /src\n\nWORKDIR /src\n\nCMD [\"gunicorn\", \"--bind\", \":8888\", \"superlists.wsgi:application\"]  # <2>\n----\n====\n\n<1> Installation is a standard `pip install`.\n\n<2> Gunicorn has its own command line, `gunicorn`. Here's where we invoke it, including telling it which port to use, and supplying the dot-notation path to the WSGI server provided by Django.\n\nAs in the previous chapter, we can use the `docker build && docker run`\npattern to try out our changes by rebuilding and rerunning our container:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -it superlists*\n----\n\nTIP: If you see an error saying\n  `Bind for 0.0.0.0:8888 failed: port is already allocated.`,\n  it'll be because you still have a container running from the previous chapter.\n  Do you remember how to use `docker ps` and `docker stop`?\n  If not, have another look at <<how-to-stop-a-docker-container>>.\n\n==== The FTs Catch a Problem with Static Files\n\nAs we run the FTs, you'll see them warning us of a problem, once again.\nThe test for adding list items passes happily,\nbut the test for layout and styling fails.(((\"static files\", \"Gunicorn&#x27;s problem with\")))(((\"Gunicorn\", \"static files, problem with\")))(((\"CSS (Cascading Style Sheets)\", \"challenges of static files\"))) Good job, tests!\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\nFAILED (failures=1)\n----\n\nAnd indeed, if you take a look at the site, you'll find the CSS is all broken,\nas in <<site-with-broken-css>>.\n\nThe reason that we have no CSS is that although the Django dev server will\nserve static files magically for you, Gunicorn doesn't.\n\n\n[[site-with-broken-css]]\n.Broken CSS\nimage::images/tdd3_1001.png[\"The site is up, but CSS is broken\"]\n\n\nOne step forwards, one step backwards,\nbut once again we've identified the problem nice and early.\nMoving on!\n\n\n=== Serving Static Files with WhiteNoise\n\nServing static files is very different from serving\ndynamically rendered content from Python and Django.(((\"static files\", \"serving with WhiteNoise\")))(((\"WhiteNoise library, serving static files with\")))\nThere are many ways to serve them in production:\nyou can use a web server like nginx, or a content delivery network (CDN) like Amazon S3.\nBut in our case, the most straightforward thing to do\nis to use https://whitenoise.readthedocs.io[WhiteNoise],\na Python library expressly designed for serving staticfootnote:[\nBelieve it or not, this pun didn't actually hit me until I was rewriting this chapter.\nFor 10 years, it was right under my nose. I think that makes it funnier actually.]\nfiles from Python.\n\n// DAVID: It might be worth pointing out what Whitenoise is actually doing.\n// From what I understand, we're still using Django to serve static files.\n\nFirst, we install WhiteNoise into our local environment:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n*pip install whitenoise*\n----\n\nThen we tell Django to enable it, in __settings.py__footnote:[\nFind out more about Django middleware\nin https://docs.djangoproject.com/en/5.2/topics/http/middleware[the docs].\n]:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch10l002)\n====\n[source,python]\n----\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"whitenoise.middleware.WhiteNoiseMiddleware\",\n    \"django.contrib.sessions.middleware.SessionMiddleware\",\n    [...]\n\n----\n====\n\nAnd then (((\"Django framework\", \"middleware\")))we need to add it to our ++pip install++s in the Dockerfile:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch10l003)\n====\n[source,dockerfile]\n----\nRUN pip install \"django<6\" gunicorn whitenoise\n----\n====\n\nThis manual list of ++pip install++s is getting a little fiddly!\nWe'll come back to that in a moment.\nFirst let's rebuild and try rerunning our FTs:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -it superlists*\n----\n\nAnd if you take another manual look at your site, things should look much healthier.\n\n[role=\"pagebreak-before\"]\nLet's rerun our FTs to confirm:\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\n\n...\n ---------------------------------------------------------------------\nRan 3 tests in 10.718s\n\nOK\n----\n\n\nPhew.  Let's commit that:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am\"Switch to Gunicorn and Whitenoise\"*\n----\n\n\n\n=== Using requirements.txt\n\nLet's deal with that fiddly list of ++pip install++s.(((\"dependency management tools\")))(((\"requirements.txt\", id=\"ix_reqr\")))\n\nTo reproduce our local virtualenv,\nrather than just manually ++pip install++ing things\none by one and having to remember to sync things\nbetween local dev and Docker,\nwe can \"save\" the list of packages we're using\nby creating a _requirements.txt_ file.footnote:[\nThere are many other dependency management tools these days\nso _requirements.txt_ is not the only way to do it,\nalthough it is one of the oldest and best established.\nAs you continue your Python adventures,\nI'm sure you'll come across many others.]\n\n\nThe `pip freeze` command will show us everything that's installed(((\"virtualenv (virtual environment)\", \"pip freeze command showing all contents\"))) in our virtualenv at the moment:\n\n\n// version numbers change too much\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip freeze*\nasgiref==3.8.1\nattrs==25.3.0\ncertifi==2025.4.26\nDjango==5.2.3\ngunicorn==23.0.0\nh11==0.16.0\nidna==3.10\noutcome==1.3.0.post0\npackaging==25.0\nPySocks==1.7.1\nselenium==4.31.0\nsniffio==1.3.1\nsortedcontainers==2.4.0\nsqlparse==0.5.3\ntrio==0.30.0\ntrio-websocket==0.12.2\ntyping_extensions==4.13.2\nurllib3==2.4.0\nwebsocket-client==1.8.0\nwhitenoise==6.11.0\nwsproto==1.2.0\n----\n\nThat shows _all_ the packages in our virtualenv,\nalong with their version numbers.\nLet's pull out just the \"top-level\" dependencies—Django, Gunicorn, and WhiteNoise:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip freeze | grep -i django*\nDjango==5.2[...]\n\n$ *pip freeze | grep -i django >> requirements.txt*\n$ *pip freeze | grep -i gunicorn >> requirements.txt*\n$ *pip freeze | grep -i whitenoise >> requirements.txt*\n----\n\nThat should give us a _requirements.txt_ file that looks like this:\n\n\n[role=\"sourcecode skipme\"]\n.requirements.txt (ch10l004)\n====\n[source,python]\n----\ndjango==5.2.3\ngunicorn==23.0.0\nwhitenoise==6.11.0\n----\n====\n\nLet's try it out!  To install things from a _requirements.txt_ file,\nyou use the `-r` flag, like this:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip install -r requirements.txt*\nRequirement already satisfied: Django==5.2.[...]\n./.venv/lib/python3.14/site-packages (from -r requirements.txt (line 1))\n(5.2.[...]\nRequirement already satisfied: gunicorn==23.0.0 in\n./.venv/lib/python3.14/site-packages (from -r requirements.txt (line 2))\n(23.0.0)\nRequirement already satisfied: whitenoise==6.11.0 in\n./.venv/lib/python3.14/site-packages (from -r requirements.txt (line 3))\n(6.11.0)\nRequirement already satisfied: asgiref[...]\nRequirement already satisfied: sqlparse[...]\n[...]\n----\n\nAs you can see, it's a no-op because we already have everything installed.\nThat's expected!\n\n\nTIP: Forgetting the `-r` and running `pip install requirements.txt`\n    is such a common error, that I recommend you do it _right now_\n    and get familiar with the error message\n    (which is thankfully much more helpful than it used to be).\n    It's a mistake I still make, _all the time_.\n\n\nAnyway, that's a good first version of a requirements file. Let's commit it:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add requirements.txt*\n$ *git commit -m \"Add a requirements.txt with Django, gunicorn and whitenoise\"*\n----\n\n\n.Dev Dependencies, Transitive Dependencies, and Lockfiles\n*******************************************************************************\nYou may be wondering why we didn't add our other key dependency,\nSelenium, to our requirements.(((\"dependencies\", \"dev and transitive\")))\nOr you might be wondering why we didn't just add _all_ the dependencies,\nincluding the \"transitive\" ones\n(e.g., Django has its own dependencies like `asgiref` and `sqlparse`, etc.).\n\nAs always, I have to gloss over some nuance and trade-offs,\nbut the short answer is: Selenium is only a dependency for the tests, not the application code;\n  we're never going to run the tests directly on our production servers.footnote:[\nSome people like to separate out test or \"dev\" dependencies\ninto a separate requirements file called _requirements.dev.txt_, for example.\nFor the record, I think this is a good idea,\nI just didn't want to add yet another concept to the book.] As for transitive dependencies,\n  they're fiddly to manage without bringing in more tools,\n  and I didn't want to do that for this book.\n// TODO: revisit this decision\n\n\n\nWhen you have a moment, you should probably to do some further reading\non \"lockfiles\", _pyproject.toml_, hard pinning versus soft pinning,\nand immediate versus transitive dependencies.(((\"lockfiles\")))(((\"pip-tools, dependency management\")))\n\nIf I absolutely _had_ to recommend a Python dependency management tool,\nit would be https://github.com/jazzband/pip-tools[pip-tools],\nwhich is a fairly minimal one.\n*******************************************************************************\n\n\nNow let's see how we use that requirements file in our Dockerfile:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch10l005)\n====\n[source,dockerfile]\n----\nFROM python:3.14-slim\n\nRUN python -m venv /venv\nENV PATH=\"/venv/bin:$PATH\"\n\nCOPY requirements.txt /tmp/requirements.txt  # <1>\nRUN pip install -r /tmp/requirements.txt  # <2>\n\nCOPY src /src\n\nWORKDIR /src\n\nCMD [\"gunicorn\", \"--bind\", \":8888\", \"superlists.wsgi:application\"]\n----\n====\n\n<1> We copy our requirements file in, just like the _src_ folder.\n\n<2> Now instead of just installing Django,\n  we install all our dependencies using `pip install -r`.\n\n\nLet's build and run:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -it superlists*\n----\n\nAnd then test to check everything still works:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\n\nOK\n----\n\nHooray.  That's a commit!(((\"requirements.txt\", startref=\"ix_reqr\")))\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Use requirements.txt in Dockerfile\"*\n----\n\n\n\n=== Using Environment Variables to Adjust Settings for Production\n\n(((\"DEBUG settings\")))(((\"environment variables\", \"using to adjust production settings\", id=\"ix_envvar\")))(((\"configurations\", \"dev settings, changing for production\")))\nWe know there are several things in\n_settings.py_ that we want to change for production:\n\n\n* `DEBUG` mode is all very well for hacking about on your own server,\n  but it https://docs.djangoproject.com/en/5.2/ref/settings/#debug[isn't secure].\n  For example, exposing raw tracebacks to the world is a bad idea.\n\n* `SECRET_KEY` is used by Django for some of its crypto--things\n  like cookies and CSRF protection.\n  It's good practice(((\"SECRET_KEY setting\"))) to make sure the secret key in production is different\n  from the one in your source code repo,\n  because that code might be visible to strangers.\n  We'll want to generate a new, random one\n  but then keep it the same for the foreseeable future\n  (find out more in the https://docs.djangoproject.com/en/5.2/topics/signing[Django docs]).\n\nDevelopment, staging, and production sites always have some differences in their configuration. Environment variables are a good place to store those different settings.footnote:[The approach of using environment variables for configuration was originally published by https://oreil.ly/ZdVhR[“The 12-Factor App”] manifesto. Another common way of handling this is to have different versions of _settings.py_ for dev and prod. That can work fine too, but it can get confusing to manage. Environment variables also have the advantage of working for non-Django stuff too.]\n\n[role=\"pagebreak-before less_space\"]\n==== Setting DEBUG=True and SECRET_KEY\n\nThere are lots of ways you might set these settings.(((\"DEBUG settings\", \"setting DEBUG&#x3D;True\")))\n\nWhat I propose may seem a little fiddly,\nbut I'll provide a little justification for each choice.\nLet them be an inspiration (but not a template) for your own choices!\n\nNote that this `if` statement replaces the `DEBUG` and `SECRET_KEY` lines\nthat are included by default in the _settings.py_ file:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch10l006)\n====\n[source,python]\n----\nimport os\n[...]\n\n# SECURITY WARNING: don't run with debug turned on in production!\nif \"DJANGO_DEBUG_FALSE\" in os.environ:  #<1>\n    DEBUG = False\n    SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]  #<2>\nelse:\n    DEBUG = True  #<3>\n    SECRET_KEY = \"insecure-key-for-dev\"\n----\n====\n// CSANAD: I think variable names like \"something_false\" are confusing, since\n//         we need to set something to true so that they mean false.\n// How about `DJANGO_ENV_PRODUCTION` or something similar?\n\n<1> We say we'll use an environment variable called `DJANGO_DEBUG_FALSE`\n    to switch debug mode off and, in effect, require production settings\n    (it doesn't matter what we set it to, just that it's there).\n\n<2> And now we say that, if debug mode is off,\n    we _require_ the `SECRET_KEY` to be set by a second environment variable.\n\n<3> Otherwise we fall back to the insecure, debug mode settings that\n    are useful for dev.\n\nThe end result is that you don't need to set any env vars for dev,\nbut production needs both to be set explicitly,\nand it will error if any are missing.\nI think this gives us a little bit of protection\nagainst accidentally forgetting to set one.\n\nTIP: Better to fail hard than allow a typo in an environment variable name to\n    leave you running with insecure settings.\n\n// CSANAD: I think it would worth pointing out the development environment\n// does not use Docker, launching the dev server should be done from\n// the reader's host system. I think this isn't immediately obvious, e.g. I\n// thought all along that from now on we would only run the server from Docker.\n// If we end up making a TIP or similar about it, I think we should also mention\n// in a development environment relying on containerization, programmers usually\n// mount the whole /src minimizing the time-consuming rebuilding of their images.\n\n[role=\"pagebreak-before less_space\"]\n==== Setting Environment Variables Inside the Dockerfile\n\nNow let's set (((\"Dockerfiles\", \"setting environment variables in\")))(((\"ENV directive (Dockerfiles)\")))that environment variable in our Dockerfile using the `ENV` directive:\n\n[role=\"sourcecode\"]\n.Dockerfile (ch10l007)\n====\n[source,dockerfile]\n----\nWORKDIR /src\n\nENV DJANGO_DEBUG_FALSE=1\n\nCMD [\"gunicorn\", \"--bind\", \":8888\", \"superlists.wsgi:application\"]\n----\n====\n\nAnd try it out...\n\n\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:specialcharacters,quotes[*docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -it superlists*]\n\n[...]\n  File \"/src/superlists/settings.py\", line 23, in <module>\n    SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]\n                 ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^\n[...]\nKeyError: 'DJANGO_SECRET_KEY'\n----\n\nOops. I forgot to set said secret key env var,\nmere seconds after having dreamt it up!\n\n==== Setting Environment Variables at the Docker Command Line\n\nWe've said we can't keep the secret key in our source code,\nso the Dockerfile isn't an option; where else can we put it?(((\"Docker\", \"setting environment variables at command line\")))\n\nFor now, we can set it at the command line using the `-e` flag for `docker run`:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n  -p 8888:8888 \\\n  --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n  -e DJANGO_SECRET_KEY=sekrit \\\n  -it superlists*\n----\n\nWith that running, we can use our FT again to see if we're back to a working state.\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\nAssertionError: 'To-Do' not found in 'Bad Request (400)'\n----\n\n[role=\"pagebreak-before\"]\nNOTE: The eagle-eyed might spot a message saying\n    `UserWarning: No directory at: /src/static/`.\n    That's a little clue about a problem with static files,\n    which we're going to deal with shortly.\n    Let's deal with this 400 issue first.\n\n\n==== ALLOWED_HOSTS Is Required When Debug Mode Is Turned Off\n\nIt's not quite working yet (see <<django-400-error>>)! Let's take a look manually.(((\"ALLOWED_HOSTS setting\")))\n\n[[django-400-error]]\n.An unfriendly 400 error\nimage::images/tdd3_1002.png[\"Web page showing wth the text 400 Bad Request in default font\"]\n\nWe've set our two environment variables, but doing so seems to have broken things.\nHowever, once again, by running our FTs frequently,\nwe're able to identify the problem early,\nbefore we've changed too many things at the same time.\nWe've only changed two settings—which one might be at fault?\n\nLet's use the \"Googling the error message\" technique again,\nwith the search terms \"Django debug false\" and \"400 bad request\".\n\nWell, the very first link in my https://oreil.ly/gVcLz[search results]\nwas Stack Overflow suggesting that a 400 error is usually to do with `ALLOWED_HOSTS`.\nAnd the second was the official Django docs,\nwhich takes a bit more scrolling, but confirms it\n(see <<search-results-400-bad-request>>).\n\n[[search-results-400-bad-request]]\n.Search results for \"django debug false 400 bad request\"\nimage::images/tdd3_1003.png[\"Duckduckgo search results with stackoverflow and django docs\"]\n\n\n`ALLOWED_HOSTS` is a security setting\ndesigned to reject requests that are likely to be forged, broken, or malicious\nbecause they don't appear to be asking for your site.footnote:[HTTP requests contain the address they were intended for in a header called \"host\".]\n\nWhen `DEBUG=True`, `ALLOWED_HOSTS` effectively allows _localhost_ (our own machine) by default, so that's why it was working OK until now.\n\nThere's more information in the\nhttps://docs.djangoproject.com/en/5.2/ref/settings/#allowed-hosts[Django docs].\n\n[role=\"pagebreak-before\"]\nThe upshot is that we need to adjust `ALLOWED_HOSTS` in _settings.py_.\nLet's use another environment variable for that:\n\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch10l008)\n====\n[source,python]\n----\nif \"DJANGO_DEBUG_FALSE\" in os.environ:\n    DEBUG = False\n    SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]\n    ALLOWED_HOSTS = [os.environ[\"DJANGO_ALLOWED_HOST\"]]\nelse:\n    DEBUG = True\n    SECRET_KEY = \"insecure-key-for-dev\"\n    ALLOWED_HOSTS = []\n----\n====\n\nThis is a setting that we want to change,\ndepending on whether our Docker image is running locally\nor on a server, so we'll use the `-e` flag again:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -it superlists*\n----\n\n\n==== Collectstatic Is Required when Debug Is Turned Off\n\nAn FT run (or just looking at the site) reveals that we've had (((\"DEBUG settings\", \"collectstatic required when DEBUG turned off\")))(((\"static files\", \"collectstatic required when debug turned off\")))(((\"collectstatic command\")))a regression\nin our static files:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\nAssertionError: 102.5 != 512 within 10 delta (409.5 difference)\nFAILED (failures=1)\n----\n\nAnd you might have seen this warning message in the `docker run` output:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters\"]\n----\n/venv/lib/python3.14/site-packages/django/core/handlers/base.py:61:\nUserWarning: No directory at: /src/static/\n  mw_instance = middleware(adapted_handler)\n----\n\n[role=\"pagebreak-before\"]\nWe saw this at the beginning of the chapter,\nwhen switching from the Django dev server to Gunicorn,\nand that was why we introduced WhiteNoise.\nSimilarly, when we switch `DEBUG` off,\nWhiteNoise stops automagically finding static files in our code,\nand instead we need to run `collectstatic`:\n\n\n[role=\"sourcecode\"]\n.Dockerfile (ch10l009)\n====\n[source,dockerfile]\n----\nWORKDIR /src\n\nRUN python manage.py collectstatic\n\nENV DJANGO_DEBUG_FALSE=1\n\nCMD [\"gunicorn\", \"--bind\", \":8888\", \"superlists.wsgi:application\"]\n----\n====\n\n\n// DAVID: Interestingly when I did this I put the RUN directive after the ENV\n// directive, which led to a KeyError: 'DJANGO_SECRET_KEY' which foxed me for a bit.\n// Might be worth calling out that we're running collectstatic in debug mode.\n\n\n\nWell, it was fiddly, but that should get us to passing tests\nafter we build and run the Docker container!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -it superlists*\n----\n\nAnd...\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\nOK\n----\n\nWe're nearly ready to ship to production!\n\nLet's quickly adjust our `gitignore`, as the static folder is in a new place,\nand do another commit to mark this bit of incremental progress:\n\n//0010\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n# should show dockerfile and untracked src/static folder\n$ *echo src/static >> .gitignore*\n$ *git status*\n# should now be clean\n$ *git commit -am \"Add collectstatic to dockerfile, and new location to gitignore\"*\n----\n\n\n[role=\"pagebreak-before less_space\"]\n=== Switching to a Nonroot User\n\nLet's do one more!(((\"environment variables\", \"using to adjust production settings\", startref=\"ix_envvar\"))) By default, Docker containers run as root.\nAlthough container security is a very well-tested ground by now,\nexperts agree it's still good practice to use an unprivileged user\ninside your container.(((\"SQLite\", \"dealing with permissions for db.sqlite3 file\", id=\"ix_SQLperm\")))\n\nThe main fiddly thing, for us, will be dealing with permissions\nfor the _db.sqlite3_ file.  It will need to be:\n\n. Writable by the nonroot user\n. In a _directory_ that's writable by the nonroot userfootnote:[\nThis is surprising.  It's due to https://sqlite.org/tempfiles.html[SQLite wanting to write various additional\ntemporary files during operation].]\n\n==== Making the Database Filepath Configurable\n\nFirst, let's make the path to the database file configurable\nusing an environment variable:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch10l011)\n====\n[source,python]\n----\n# SECURITY WARNING: don't run with debug turned on in production!\nif \"DJANGO_DEBUG_FALSE\" in os.environ:\n    DEBUG = False\n    SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]\n    ALLOWED_HOSTS = [os.environ[\"DJANGO_ALLOWED_HOST\"]]\n    db_path = os.environ[\"DJANGO_DB_PATH\"]  # <1>\nelse:\n    DEBUG = True\n    SECRET_KEY = \"insecure-key-for-dev\"\n    ALLOWED_HOSTS = []\n    db_path = BASE_DIR / \"db.sqlite3\"  # <2>\n[...]\n\n# Database\n# https://docs.djangoproject.com/en/5.2/ref/settings/#databases\n\nDATABASES = {\n    \"default\": {\n        \"ENGINE\": \"django.db.backends.sqlite3\",\n        \"NAME\": db_path  # <3>\n    }\n}\n----\n====\n\n<1> Inside Docker, we'll assume that an environment variable called\n    `DJANGO_DB_PATH` has been set.\n    We save it to a local variable called `db_path`.\n\n<2> Outside Docker, we'll use the default path to the database file.\n\n<3> And we modify the `DATABASES` entry to use our `db_path` variable.\n\nNow let's change the (((\"Dockerfiles\", \"changing to set DJANGO_DB_PATH and to nonroot user\")))Dockerfile to set that env var,\nand to create and switch to our nonroot user,\nwhich we may as well call \"nonroot\" (although it could be anything!):\n\n[role=\"sourcecode \"]\n.Dockerfile (ch10l012)\n====\n[source,dockerfile]\n----\nWORKDIR /src\n\nRUN python manage.py collectstatic\n\nENV DJANGO_DEBUG_FALSE=1\n\nRUN adduser --uid 1234 nonroot  # <1>\nUSER nonroot  # <2>\n\nCMD [\"gunicorn\", \"--bind\", \":8888\", \"superlists.wsgi:application\"]\n----\n====\n\n<1> We use the `adduser` command to create our user,\n    explicitly setting its UID to `1234`.footnote:[\n    A more or less arbitrary number,\n    the first non-system user on a system is usually 1000,\n    so it's nice that this won't be the same as the `elspeth` user outside the container.\n    But other than that it could be any number greater than 1000 really.]\n\n<2> The `USER` directive in the Dockerfile tells Docker to run\n    everything as that user by default.\n\n\n==== Using UIDs to Set Permissions Across Host/Container Mounts\n\nOur user will now have a writable home directory at `/home/nonroot`,\nso we'll put the database file in there.\nThat takes care of the \"writable directory\" requirement.\n\nBecause we're mounting the file from outside though,\nthat's not quite enough to make the file itself writable.(((\"host/container mounts, using UIDs to set permissions\")))(((\"user IDs (UIDs)\", \"using to set permissions across host/container mounts\")))\nWe'll need to set the _owner_ of the file to be `nonroot` as well.\nBecause of the way Linux permissions work,\nwe're going to use integer user IDs (UIDs).\nThis might seem a bit magical if you're not used to Linux permissions,\nso you'll have to trust me, I'm afraid.footnote:[\nLinux permissions aren't actually implemented using the string names of users;\ninstead they use integer user IDs (called UIDs).\nThe way we map from the UIDs to strings is using a special file called _/etc/passwd_.\nBecause _/etc/passwd_ is not the same inside and outside the container,\nthe UIDs to username mappings inside and outside are not necessarily the same.\nHowever, the permission UIDs are just numbers, and they actually are stored inside\nindividual files, so they don't change when you mount files.\nThere's more info here on https://oreil.ly/ceIfE[this Stack Overflow post].]\n\nFirst, let's create a file with the right permissions, outside the container:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *touch container.db.sqlite3*\n\n# Change the owner to uid 1234\n$ *sudo chown 1234 container.db.sqlite3*\n\n# This next step is needed on non-Linux dev environments,\n# to make sure that the container host VM can write to the file.\n# Change the file to be group-writeable as well as owner-writeable:\n$ *sudo chmod g+rw container.db.sqlite3*\n----\n\nNow let's rebuild and run our container,\nchanging the `--mount` path to our new file,\nand setting the `DJANGO_DB_PATH` environment variable to match:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/container.db.sqlite3\",target=/home/nonroot/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -it superlists*\n----\n\n\nAs a first check that we can write to the database from inside the container,\nlet's use `docker exec` to populate the database tables using `manage.py migrate`:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker ps*  # note container id\n$ *docker exec container-id-or-name python manage.py migrate*\nOperations to perform:\n  Apply all migrations: auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying contenttypes.0001_initial... OK\n  [...]\n  Applying lists.0001_initial... OK\n  Applying lists.0002_item_text... OK\n  Applying lists.0003_list... OK\n  Applying lists.0004_item_list... OK\n  Applying sessions.0001_initial... OK\n----\n\n[role=\"pagebreak-before\"]\nAnd, as after every incremental change,\nwe rerun our FT suite to make sure everything works:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\nOK\n----\n\nGreat!  We wrap up with a bit of housekeeping;\nwe'll add this new database file to our `.gitignore`,\nand commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo container.db.sqlite3 >> .gitignore*\n$ *git commit -am\"Switch to nonroot user\"*\n----\n// ch10l014\n\n\n\n=== Configuring Logging\n\nOne last thing we'll want to do is make sure that we can get logs out of our server.(((\"SQLite\", \"dealing with permissions for db.sqlite3 file\", startref=\"ix_SQLperm\")))(((\"logging\", \"configuring for production-ready container app\", id=\"ix_logcfg\")))\nIf things go wrong, we want to be able to get to the tracebacks. And as we'll soon see,\nswitching `DEBUG` off means that Django's default logging configuration changes.\n\n\n==== Provoking a Deliberate Error\n\nTo test this, we'll provoke a deliberate error by corrupting the database file:\n\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo 'bla' > container.db.sqlite3*\n----\n\nNow if you run the tests, you'll see they fail:\n\n// TODO: for some reason this wont repro in CI\n\n[role=\"small-code pause-first skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast*]\n[...]\n\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; [...]\n----\n\n// DAVID: Got me thinking, I'm not always clear when I need to rebuild the image.\n// I would have thought I might need to do it here, but I didn't. Might be worth\n// explaining in the previous chapter when we do.\n\nAnd you might spot in the browser that we just see a minimal error page,\nwith no debug info, as in <<minimal-error-page>> (try it manually if you like).\n\n[[minimal-error-page]]\n.Minimal default server error 500\nimage::images/tdd3_1004.png[\"A minimal error page saying just Server error (500)\"]\n\n[role=\"pagebreak-before\"]\nBut if you look in your Docker terminal, you'll see there is no traceback:\n\n[role=\"skipme\"]\n----\n[2024-02-28 10:41:53 +0000] [7] [INFO] Starting gunicorn 21.2.0\n[2024-02-28 10:41:53 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7)\n[2024-02-28 10:41:53 +0000] [7] [INFO] Using worker: sync\n[2024-02-28 10:41:53 +0000] [8] [INFO] Booting worker with pid: 8\n----\n\n\nWhere have the tracebacks gone?\nYou might have been expecting that the Django debug page and its tracebacks\nwould disappear from our web browser,\nbut it's more of shock to see that they are no longer appearing in the terminal either!\nIf you're like me, you might find yourself wondering if we really _did_ see them earlier\nand starting to doubt your own sanity.\nBut the explanation is that Django's\nhttps://docs.djangoproject.com/en/5.2/ref/logging/#default-logging-configuration[default logging configuration]\nchanges when `DEBUG` is turned off.\n\nThis means we need to interact with the standard library's `logging` module,\nunfortunately one of the most fiddly parts of the Python standard library.footnote:[\nIt's not necessarily for bad reasons, but it is all very Java-ey and enterprise-y.\nI mean, yes, separating the concepts of handlers and loggers and filters,\nand making it all configurable in a nested hierarchy, is all well and good\nand covers every possible use case,\nbut sometimes you just wanna say \"just print stuff to stdout pls\",\nand you wish that configuring the simplest thing was a little easier.]\n\nHere's pretty much the simplest possible logging config,\nwhich just prints everything to the console (i.e., standard out);\nI've added this code to the very end of the _settings.py_ file:\n\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch10l013)\n====\n[source,python]\n----\nLOGGING = {\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"handlers\": {\n        \"console\": {\"class\": \"logging.StreamHandler\"},\n    },\n    \"loggers\": {\n        \"root\": {\"handlers\": [\"console\"], \"level\": \"INFO\"},\n    },\n}\n----\n====\n\nRebuild and restart our container...\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -it superlists*\n----\n\nThen try the FT again (or submitting a new list item manually)\nand we now should see a clear error message:\n\n// TODO: test get from docker logs\n[role=\"skipme\"]\n----\nInternal Server Error: /lists/new\nTraceback (most recent call last):\n[...]\n  File \"/src/lists/views.py\", line 10, in new_list\n    nulist = List.objects.create()\n             ^^^^^^^^^^^^^^^^^^^^^\n[...]\n  File \"/venv/lib/python3.14/site-packages/django/db/backends/sqlite3/base.py\",\n  line 328, in execute\n    return super().execute(query, params)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\ndjango.db.utils.DatabaseError: file is not a database\n----\n\nWe can fix and re-create the database by doing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo > container.db.sqlite3*\n$ *docker exec -it <container_id> python manage.py migrate*\n----\n\nAnd rerun the FTs to check we're back to a working state.\n\nLet's do a final commit for this change:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Add logging config to settings.py\"*\n----\n\n=== Exercise for the Reader: Using the Django check Command\n\nI don't have time in this book to cover every last aspect of\nproduction-readiness.(((\"logging\", \"configuring for production-ready container app\", startref=\"ix_logcfg\")))\nApart from anything else, this is a fast-changing area,\nand security updates to Django and its best practice recommandations\nchange frequently, so things I write now might be incomplete\nby the time you read the book.\n\nI _have_ given a decent overview of the various different axes\nalong which you'll need to make production-readiness changes,\nso hopefully you have a toolkit for how to do this sort of work.(((\"Django framework\", \"deployment checklist and check --deploy command\")))\n\nIf you'd like to dig into this a little bit more,\nor if you're preparing a real project for release into the wild,\nthe next step is to read up on Django's \nhttps://docs.djangoproject.com/en/5.2/howto/deployment/checklist[deployment checklist].\n\n[role=\"pagebreak-before\"]\nThe first suggestion is to use Django's \"self-check\" command,\n`manage.py check --deploy`.\nHere's what it reported as outstanding when I ran it in April 2025:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker exec <container-id> python manage.py check --deploy*\nSystem check identified some issues:\n\nWARNINGS:\n?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS\nsetting. If your entire site is served only over SSL, you may want to consider\nsetting a value and enabling HTTP Strict Transport Security. Be sure to read\nthe documentation first; enabling HSTS carelessly can cause serious,\nirreversible problems.\n?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless\nyour site should be available over both SSL and non-SSL connections, you may\nwant to either set this setting True or configure a load balancer or\nreverse-proxy server to redirect all connections to HTTPS.\n?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5\nunique characters, or it's prefixed with 'django-insecure-' indicating that it\nwas generated automatically by Django. Please generate a long and random value,\notherwise many of Django's security-critical features will be vulnerable to\nattack.\n?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a\nsecure-only session cookie makes it more difficult for network traffic sniffers\nto hijack user sessions.\n?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your\nMIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a\nsecure-only CSRF cookie makes it more difficult for network traffic sniffers to\nsteal the CSRF token.\n----\n\nWhy not pick one of these and have a go at fixing it?\n\n\n=== Wrap-Up\n\nWe might not have addressed every last issue that `check --deploy` raised,\nbut we've at least touched on many or most of the things you might need to think about\nwhen considering production-readiness. We've worked in small steps and used our tests all the way along,\nand we're now ready to deploy our container to a real server!\n\nFind out how, in our next exciting installment...\n\nTIP: One more recommendation for PythonSpeed and its\n    https://pythonspeed.com/docker[Docker Packaging for Python Developers]\n    article—again, I cannot recommend it highly enough.\n    Read it before you're too much older!\n\n\n[role=\"pagebreak-before less_space\"]\n.Production-Readiness Config\n*******************************************************************************\n\n(((\"production-ready deployment\", \"configuration, preparing\")))(((\"configurations\", \"production-ready, issues to consider\")))\nA few things to think about when trying to prepare a production-ready configuration:\n\nDon't use the Django dev server in production::\n  Something like Gunicorn or uWSGI is a better tool for running Django;\n  it will let you run multiple workers, for example.\n  (((\"Gunicorn\", \"benefits of\")))\n\nDecide how to serve your static files::\n  Static files aren't the same kind of things as the dynamic content\n  that comes from Django and your web app, so they need to be treated differently.\n  WhiteNoise is just one example of how you might do that.\n\nCheck your settings.py for dev-only config::\n  `DEBUG=True`, `ALLOWED_HOSTS`, and `SECRET_KEY` are the ones we came across,\n  but you will probably have others\n  (and we'll see more when we start to send emails from the server).\n\nChange things one at a time and rerun your tests frequently::\n  Whenever we make a change to our server configuration,\n  we can rerun the test suite,\n  and either be confident that everything works as well as it did before,\n  or find out immediately if we did something wrong.\n\nThink about logging and observability::\n  When things go wrong, you need to be able to find out what happened.\n  At a minimum, you need a way of getting logs and tracebacks out of your server,\n  and in more advanced environments you'll want to think about metrics and tracing too.\n  But we can't cover all that in this book!(((\"containers\", \"making production-ready\", startref=\"ix_cntnrprd\")))\n\nUse the Django \"check\" command::\n  `python manage.py check --deploy` can give you a list of additional settings\n  to check for production-readiness.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_11_server_prep.asciidoc",
    "content": "[[chapter_11_server_prep]]\n== Getting a Server Ready for Deployment\n\n\n(((\"infrastructure as code (IaC)\")))\nThis chapter is all about getting ready for our deployment.\nWe're going to spin up an actual server,\nmake it accessible on the internet with a real domain name,\nand set up the authentication and credentials we need\nto be able to control it remotely with SSH and Ansible.\n\n=== Manually Provisioning a Server to Host Our Site\n\n(((\"staging sites\", \"manual server provisioning\", id=\"SSserver09\")))\n(((\"server provisioning\", id=\"seerver09\")))\nWe can separate our \"deployment\" into two tasks:\n\n. _Provisioning_ a new server to be able to host the code,\n  which includes choosing an operating system,\n  getting basic credentials to log in,\n  and configuring DNS\n. _Deploying_ our application to an existing server,\n  which includes getting our Docker image onto the server,\n  starting a container, and configuring it to talk to the database\n  and the outside world\n\nInfrastructure-as-code tools can let you automate both of these,\nbut the provisioning parts tend to be quite vendor-specific,\nso for the purposes of this book, we can live with manual provisioning.\n\nNOTE: I should probably stress once more that deployment is something that varies a lot\n  and, as a result, there are few universal best practices for how to do it.\n  So, rather than trying to remember the specifics of what I'm doing here,\n  you should be trying to understand the rationale,\n  so that you can apply the same kind of thinking in the specific future circumstances you encounter.\n\n\n==== Choosing Where to Host Our Site\n\n(((\"hosting services\")))\nThere are loads of different solutions out there these days,\nbut they broadly fall into two camps:\n\n. Running your own (probably virtual) server—aka VPS (virtual private server)\n. Using a platform as a service (PaaS)\n  offering like Heroku or my old employers, PythonAnywhere\n  (((\"platform-as-a-service (PaaS)\")))(((\"VPS (virtual private server)\")))\n  (((\"PythonAnywhere\")))\n\nWith a PaaS, you don't get your own server;\ninstead, you're renting a \"service\" at a higher level of abstraction.\nParticularly for small sites,\na PaaS offers many advantages over running your own server,\nand I would definitely recommend looking into them.(((\"PaaS\", see=\"platform-as-a-service\")))\nWe're not going to use a PaaS in this book, however, for several reasons.\nThe main reason is that I want to avoid endorsing specific commercial providers.\nSecondly, all the PaaS offerings are quite different,\nand the procedures to deploy to each vary a lot--learning about one\ndoesn't necessarily tell you about the others.\nAny one of them might radically change their process or business model by the time you get to read this book.\n\nInstead, we'll learn just a tiny bit of good old-fashioned server admin,\nincluding SSH and manual debugging.\nThey're unlikely to ever go away,\nand knowing a bit about them will get you some respect\nfrom all the grizzled dinosaurs out there.\n\n\n==== Spinning Up Our Own Server\n\nI'm not going to dictate how you spin up a server--whether\nyou choose Amazon AWS, Rackspace, DigitalOcean, your own server in a datacentre,\nor a Raspberry Pi in a cupboard under the stairs,\nany solution should(((\"server provisioning\", \"creating a server\", id=\"ix_serprvcr\")))(((\"Ubuntu, server running Ubuntu 22.04\"))) be fine, as long as:\n\n* Your server is running Ubuntu 22.04 (aka \"Jammy/LTS\").\n\n* You have root access to it.(((\"root user\")))\n\n* It's on the public internet (i.e., it has a public IP address).\n\n* You can SSH into it (I recommend using a nonroot user account,\n  with `sudo` access, and public/private key authentication).\n\nI'm recommending Ubuntu as a distro because it's popular and I'm used to it.footnote:[Linux as an operating system comes in lots of different flavours,\ncalled \"distros\" or \"distributions\".(((\"Linux\", \"different flavors or distributions\")))\nThe differences between them and their relative pros and cons are,\nlike any seemingly minor detail, of tremendous interest to the right kind of nerd.\nWe don't need to care about them for this book. As I say, Ubuntu is fine.]\nIf you know what you're doing, you can probably get away with using\nsomething else, but I won't be able to help you as much if you get stuck.\n\n\n[[step-by-step-guide]]\n.Step-by-Step Instructions for Spinning Up a Server\n*******************************************************************************\n(((\"server provisioning\", \"guide to\")))(((\"Linux\", \"server, creating\")))\nI appreciate that, if you've never started a Linux server before\nand you have absolutely no idea where to start,\nthis is a big ask, especially when I'm refusing to \"dictate\"\nexactly how to do it.\n\nWith that in mind, I wrote a\nhttps://github.com/hjwp/Book-TDD-Web-Dev-Python/blob/main/server-quickstart.md[very brief guide on GitHub]. I didn't want to include it in the book itself because,\ninevitably, I do end up specifying a specific commercial provider in there.\n\n\n*******************************************************************************\n\nNOTE: Some people get to this chapter, and are tempted to skip the domain bit\n    and the \"getting a real server\" bit, and just use a VM on their own PC.\n    Don't do this.\n    It's _not_ the same, and you'll have more difficulty following the instructions,\n    which are complicated enough as it is.\n    If you're worried about cost, have a look at the guide I wrote for free options.\n    (((\"getting help\")))\n\n\n\n\n.General Tip for Working with Infrastructure\n*******************************************************************************\n\nThe most important lesson to remember over the next few chapters is,\nas always but more than ever, to work incrementally,\nmake one change at a time, and run your tests frequently.(((\"infrastructure, working with\")))\n\nWhen things (inevitably) go wrong, resist the temptation to flail about\nand make other unrelated changes in the hope that things will start working again;\ninstead, stop, go backwards if necessary to get to a working state,\nand figure out what went wrong before moving forwards again.\n\nIt's just as easy to fall into the Refactoring Cat trap when working with infrastructure!(((\"server provisioning\", \"creating a server\", startref=\"ix_serprvcr\")))\n\n*******************************************************************************\n\n\n=== Getting a Domain Name\n\n(((\"domains\", \"getting a domain name\")))\nWe're going to need a couple of domain names at this point in the book--they\ncan both be subdomains of a single domain.(((\"server provisioning\", \"getting a domain name\")))\nI'm going to use _superlists.ottg.co.uk_ and _staging.ottg.co.uk_.\nIf you don't already own a domain, this is the time to register one!\nAgain, this is something I really want you to _actually_ do.\nIf you've never registered a domain before,\njust pick any old registrar and buy a cheap one--it\nshould only cost you $5 or so,\nand I promise seeing your site on a \"real\" website will be a thrill.\n\n// DAVID: just wondering if it's worth giving them the option to cheat and\n// specify a domain name in a hosts file?\n\n\n\n=== Configuring DNS for Staging and Live Domains\n\nWe don't want to be messing about with IP addresses all the time,\nso we should point our staging and live domains to the server.(((\"domains\", \"configuring DNS for staging and live domains\")))\nAt my registrar, the control screens looked a bit like <<registrar-control-screens>>.\n\n[[registrar-control-screens]]\n.Domain setup\nimage::images/tdd3_1101.png[\"Registrar control screen for adding a DNS record\"]\n\n(((\"A-records\")))(((\"AAAA-records (IPv6)\")))\nIn the DNS system, pointing a domain at a specific IP address is referred to as an \"A-record\".footnote:[\nStrictly speaking, A-records are for IPv4,\nand you can also use AAAA-records for IPv6.\nSome cheap providers only support IPv6,\nand there's nothing wrong with that.]\nAll registrars are slightly different,\nbut a bit of clicking around should get you to the right screen in yours.\nYou'll need two A-records:\none for the staging address and one for the live one.\nNo need to worry about any other type of record.\n\nDNS records take some time to \"propagate\" around the world\n(it's controlled by a setting called \"TTL\", time to live),\nso once you've set up your A-record,\nyou can check its progress on a \"propagation checking\" service like this one:\nhttps://www.whatsmydns.net/#A/staging.ottg.co.uk.\n\nI'm planning to host my staging server at _staging.ottg.co.uk_.\n\n\n=== Ansible\n\nInfrastructure-as-code tools, also called \"configuration management\" tools,\ncome in lots of shapes and sizes.(((\"Ansible\")))(((\"deployment\", \"automating with Ansible\", id=\"Dfarbric11\")))(((\"infrastructure as code (IaC)\", \"tools for\")))(((\"configuration management tools\")))\nChef and Puppet were two of the original ones,\nand you'll probably come across Terraform,\nwhich is particularly strong on managing cloud services like AWS.\n\n// SEBASTIAN: mentioning of too many technologies (e.g. Puppet/Chef - IMHO not necessary in 2024).\n\nWe're going to use the infrastructure automation tool Ansible—because it's relatively popular,\nbecause it can do everything we need it to,\nbecause I'm biased that it happens to be written in Python,\nand because it's probably the one I'm personally most familiar with.\n\nAnother tool could probably have worked just as well!\nThe main thing to remember is the _concept_, which is that,\nas much as possible we want to manage our server configuration _declaratively_,\nby expressing the desired state of the server in a particular configuration syntax,\nrather than specifying a procedural series of steps to be followed one by one.\n\n==== Ansible Versus SSH: How We'll Talk to Our Server\n\n<<ansible-and-ssh>> shows how we’ll interact with our server using SSH, Ansible, and our FTs.\n\n[[ansible-and-ssh]]\n.Ansible and SSH\nimage::images/tdd3_1102.png[\"Diagram \"]\n\nOur objective is to use Ansible(((\"Ansible\", \"using with SSH for server interactions\", id=\"ix_AnsSSH\")))(((\"SSH\", \"using with Ansible to interact with server\"))) to automate the process of deploying to our server:\nmaking sure that the server has everything it needs to run our app\n(mostly, Docker and our container image),\nand then telling it to start or restart our container.\n\nNow and again, we'll want to \"log on\" to the server and have a look around manually;\nfor that, we'll use the `ssh` command line on our computer,\nwhich can let us open up an interactive console on the server.\n\nFinally, we'll run our FTs against the server, once it's running our app,\nto make sure it's all working correctly.\n\n\n=== Start by Making Sure We Can SSH In\n\nAt this point and for the rest of the book,\nI'm assuming that you have a nonroot user account set up,\nand that it has `sudo` privileges,\nso whenever we need to do something that requires root access, we use `sudo`,\n(or \"become\" in Ansible terminology);\nI'll be explicit about that in the various instructions that follow.(((\"SSH\", \"making sure you can SSH to the server\")))\n\nMy user is called \"elspeth\", but you can call yours whatever you like!\nJust remember to substitute it in all the places I've hardcoded it.\nSee the guide I wrote (<<step-by-step-guide>>)\nif you need tips on creating a `sudo` user.\n\n\nAnsible uses SSH under the hood to talk to the server,\nso checking we can log in \"manually\" is a good first step:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ssh elspeth@staging.ottg.co.uk*\nelspeth@server$: *echo \"hello world\"*\nhello world\n----\n\n\nTIP: Look out for that `elspeth@server`\n    in the command-line listings in this chapter.\n    It indicates commands that must be run on the server,\n    as opposed to commands you run on your own PC.\n\n\n.Use WSL on Windows\n*******************************************************************************\nAnsible will not run natively on Windows (see the\nhttps://docs.ansible.com/ansible/latest/os_guide/intro_windows.html#using-windows-as-the-control-node[docs])\nbut you can use the Windows Subsystem for Linux (WSL),\na sort of mini-Linux that Microsoft has made to run inside Windows.(((\"Ansible\", \"using WSL on Windows with\")))(((\"Windows Subsystem for Linux (WSL)\")))\n\nFollow Microsoft's https://learn.microsoft.com/en-us/windows/wsl/setup/environment[instructions for setting up WSL].\n\nOnce inside your WSL environment, you can navigate to your project directory\non the host Windows filesystem at _/mnt/c/Users/yourusername/Projects/superlists_, for example.\n\nYou'll need to use a different virtualenv for WSL:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\nyourusername@wsl: *cd /mnt/c/Users/yourusername/Projects/superlists*\nyourusername@wsl: *python -m venv .venv-wsl*\nyourusername@wsl: *source .venv-wsl/bin/activate*\n----\n\nIf you are using public key authentication,\nit's probably simplest to generate a new SSH keypair,\nand add it to __home/elspeth/.ssh/authorized_keys__ on the server:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\nyourusername@wsl: *ssh-keygen*\n[..]\nyourusername@wsl: *cat ~/.ssh/*.pub*\n# copy the public key to your clipboard,\n----\n\nI'd suggest you _only_ use WSL when you need to use Ansible.\n\nThe alternative is to switch your whole dev environment to WSL,\nand move your source code in there,\nbut you might need to overcome a few hurdles around things like networking.\n\n*******************************************************************************\n\n\n==== Debugging Issues with SSH\n\nHere's a few things to try if you (((\"SSH\", \"debugging issues with\", id=\"ix_SSHdbg\")))can't SSH in.\n\n===== Debugging network connectivity\n\nFirst, check network connectivity:  can we even reach the server?(((\"network connectivity, debugging\")))\n\n[role=\"skipme\"]\n[subs=\"quotes\"]\n----\n$ *ping staging.ottg.co.uk*\n\n# if that doesn't work, try the IP address\n$ *ping 193.184.215.14*  # or whatever your IP is\n\n# also see if the domain name resolves\n$ *nslookup staging.ottg.co.uk*\n----\n\nIf the IP works and the domain name doesn't,\nand/or if the `nslookup` doesn't work,\nyou should go check your DNS config at your registrar.\nYou may just need to wait!(((\"nslookup\")))(((\"domains\", \"checking DNS using propagation checker\")))\nTry a DNS propagation checker like https://www.whatsmydns.net/#A/staging.ottg.co.uk.\n\n[role=\"pagebreak-before less_space\"]\n===== Debugging SSH auth issues\n\nNext, let's try and debug any possible issues with authentication.(((\"authentication\", \"SSH, debugging issues with\")))\n\nFirst, your hosting provider might have the option to open\na console directly from within their web UI.\nThat's worth trying, and if there are any problems there,\nthen you probably need to restart your server,\nor perhaps stop it and create a new one.\n\nTIP: It's worth double-checking your IP address at this point,\n    in your provider's server control panel pages.\n\nNext, we can try debugging our SSH connection:\n\n[role=\"skipme small-code\"]\n[subs=\"quotes\"]\n----\n# try the -v flag which turn on verbose/debug output\n$ *ssh -v elspeth@staging.ottg.uk*\nOpenSSH_9.7p1, LibreSSL 3.3.6\ndebug1: Reading configuration data ~/.ssh/config\ndebug1: Reading configuration data ~/.colima/ssh_config\ndebug1: Reading configuration data /etc/ssh/ssh_config\ndebug1: /etc/ssh/ssh_config line 21: include /etc/ssh/ssh_config.d/* matched no files\ndebug1: /etc/ssh/ssh_config line 54: Applying options for *\ndebug1: Authenticator provider $SSH_SK_PROVIDER did not resolve; disabling\ndebug1: Connecting to staging.ottg.uk port 22.\nssh: Could not resolve hostname staging.ottg.uk: nodename nor servname provided, or not \nknown\n# oops I made a typo!  it should be ottg.co.uk not ottg.uk\n----\n\nIf that doesn't help, try switching to(((\"root user\", \"switching to in SSH debugging\"))) the root user instead:\n\n[role=\"skipme\"]\n[subs=\"quotes\"]\n----\n$ *ssh -v root@staging.ottg.co.uk*\n[...]\ndebug1: Authentications that can continue: publickey\ndebug1: Next authentication method: publickey\ndebug1: get_agent_identities: bound agent to hostkey\ndebug1: get_agent_identities: agent returned 1 keys\ndebug1: Will attempt key: ~/.ssh/id_ed25519 ED25519 SHA256:gZLxb9zCuGVT1Dm8 [...]\ndebug1: Will attempt key: ~/.ssh/id_rsa\ndebug1: Will attempt key: ~/.ssh/id_ecdsa\ndebug1: Will attempt key: ~/.ssh/id_ecdsa_sk\ndebug1: Will attempt key: ~/.ssh/id_ed25519_sk\ndebug1: Will attempt key: ~/.ssh/id_xmss\ndebug1: Will attempt key: ~/.ssh/id_dsa\ndebug1: Offering public key: ~/.ssh/id_ed25519 [...]\ndebug1: Server accepts key: ~/.ssh/id_ed25519 [...]\nAuthenticated to staging.ottg.co.uk ([165.232.110.81]:22) using \"publickey\".\n----\n\nThat one actually worked! But in the verbose output,\nyou can watch to make sure it finds the right SSH keys,\nfor example.(((\"public/private key pairs\", \"SSH keys\")))\n\nTIP: If root works but your nonroot user doesn't,\n    you may need to add your public key to\n    `/home/yournonrootuser/.ssh/authorized_keys`.\n\n\nIf root doesn't work either,\nyou may need to add your public SSH key to your account settings page,\nvia your provider's web UI.\nThat may or may not take effect immediately;\nyou might need to delete your old server and create a new one.\n\nRemember, that probably means a new IP address!\n\n\n.Security\n*******************************************************************************\nA serious discussion of server security is beyond the scope of this book,\nand I'd warn against running your own servers\nwithout learning a good bit more about it.(((\"server provisioning\", \"learning more about server security\")))\n(One reason people choose to use a PaaS to host their code\nis that it means slightly fewer security issues to worry about.)\nIf you'd like a place to start, here's as good a place as any:\nhttps://blog.codelitt.com/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu.\n\nI can definitely recommend the eye-opening experience of installing\nFail2Ban and watching its logfiles to see just how quickly it picks up on\nrandom drive-by attempts to brute force your SSH login.  The internet is a\nwild place!\n(((\"Ansible\", \"using with SSH for server interactions\", startref=\"ix_AnsSSH\")))(((\"SSH\", \"debugging issues with\", startref=\"ix_SSHdbg\")))(((\"security issues and settings\", \"server security\")))\n(((\"platform-as-a-service (PaaS)\")))\n*******************************************************************************\n\n\n\n==== Installing Ansible\n\nAssuming we can reliably SSH into the server,\nit's time to install Ansible and make sure it can talk to our server as well.(((\"Ansible\", \"installing\")))\n\nTake a look at the\nhttps://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html[Ansible installation guide]\nfor all the various options,\nbut probably the simplest thing to do is to install Ansible into the virtualenv\non our local machine (Ansible doesn't need to be installed on the server):\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *pip install ansible*\n# we also need the Docker SDK for the ansible/docker integration to work:\n$ *pip install docker*\n----\n\n// TODO: consider introducing an explicit requirements.dev.txt here,\n// with -r requirements.txt and put ansible, docker, and selenium in there.\n// or, maybe get that in place in the previous chapter, keep this one shorter.\n\n[role=\"pagebreak-before less_space\"]\n==== Checking Ansible Can Talk to Our Server\n\nThis is the last step in ensuring we're ready:\nmaking sure Ansible can talk to our server.(((\"Ansible\", \"checking interactions with server\", id=\"ix_Ansserint\")))\n\nAt the core of Ansible is what's called a \"playbook\",\nwhich describes what we want to happen on our server. Let's create one now.\nIt's probably a good idea to keep it in a folder of its own:\n\n[subs=\"quotes\"]\n----\n*mkdir infra*\n----\n\nAnd here's a minimal playbook whose job is just to \"ping\"\nthe server, to check we can talk to it.(((\"YAML (yet another markup language)\")))\nIt's in a format called YAML (yet another markup language)\nwhich, if you've never come across before,\nyou will soon develop a love-hate relationship for:footnote:[\nThe \"love\" part is that YAML is very easy to _read_ and scan through at a glance.\nThe \"hate\" part is that the actual syntax is surprisingly fiddly to get right:\nthe difference between lists and key/value maps is subtle\nand I can never quite remember it, honestly.]\n\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch11l001)\n====\n[source,yaml]\n----\n- hosts: all\n  tasks:\n    - name: Ping to make sure we can talk to our server\n      ansible.builtin.ping:\n----\n====\n\n\nWe won't worry too much about the syntax or how it works at the moment;\nlet's just use it to make sure everything works.\n\nTo invoke Ansible, we use the command `ansible-playbook`,\nwhich will have been installed into your virutalenv when we did\nthe `pip install ansible` earlier.\n\nHere's the full command we'll use, with an explanation of each part:\n\n[role=\"small-code skipme\"]\n----\nansible-playbook \\\n  --user=elspeth \\ <1>\n  -i staging.ottg.co.uk, \\ <2><3>\n  infra/deploy-playbook.yaml \\ <4>\n  -vv <5>\n----\n\n<1> The `--user=` flag lets us specify the user to use to authenticate\n    with the server.  This should be the same user you can SSH with.\n\n<2> The `-i` flag specifies what server to run against.\n\n<3> Note the trailing comma after the server hostname.\n    Without this, it won't work\n    (it's there because Ansible is designed to work against multiple servers\n    at the same time).footnote:[\n    The \"i\" in the `-i` flag stands for \"inventory\".\n    Using the `-i` flag is actually a little unconventional.\n    If you read the Ansible docs, you'll find they usually\n    recommend having an \"inventory file\", which lists all your servers,\n    along with various bits of qualifying metadata.\n    That's overkill for our use case though!]\n\n<4> Next comes the path to our playbook, as a positional argument.\n\n<5> Finally the `-v` or `-vv` flags control how verbose the output will be—useful for debugging!\n\n\nHere's some example output when I run it:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\nansible-playbook [core 2.17.5]\n  config file = None\n  configured module search path = ['~/.ansible/plugins/modules',\n'/usr/share/ansible/plugins/modules']\n  ansible python module location =\n...goat-book/.venv/lib/python3.14/site-packages/ansible\n  ansible collection location =\n~/.ansible/collections:/usr/share/ansible/collections\n  executable location = ...goat-book/.venv/bin/ansible-playbook\n  python version = 3.14.0 (main, Oct 11 2024, 22:59:05) [Clang 15.0.0\n(clang-1500.3.9.4)] (...goat-book/.venv/bin/python)\n  jinja version = 3.1.4\n  libyaml = True\nNo config file found; using defaults\nSkipping callback 'default', as we already have a stdout callback.\nSkipping callback 'minimal', as we already have a stdout callback.\nSkipping callback 'oneline', as we already have a stdout callback.\n\nPLAYBOOK: deploy-playbook.yaml ************************************************\n1 plays in infra/deploy-playbook.yaml\n\nPLAY [all] ********************************************************************\n\nTASK [Gathering Facts] ********************************************************\ntask path: ...goat-book/infra/deploy-playbook.yaml:1\n[WARNING]: Platform linux on host staging.ottg.co.uk is using the discovered\nPython interpreter at /usr/bin/python3.10, but future\ninstallation of another Python interpreter could change the meaning of that\npath. See https://docs.ansible.com/ansible-\ncore/2.17/reference_appendices/interpreter_discovery.html for more information.\nok: [staging.ottg.co.uk]\n\nTASK [Ping to make sure we can talk to our server] ****************************\ntask path: ...goat-book/infra/deploy-playbook.yaml:3\nok: [staging.ottg.co.uk] => {\"changed\": false, \"ping\": \"pong\"}\n\nPLAY RECAP ********************************************************************\nstaging.ottg.co.uk         : ok=2    changed=0    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n\n\nLooking good!\nIn the next chapter, we'll use Ansible to get our app up and running\non our server.  It'll be a thrill, I promise!(((\"Ansible\", \"checking interactions with server\", startref=\"ix_Ansserint\")))(((\"server provisioning\", \"recap of\")))\n\n[role=\"pagebreak-before less_space\"]\n.Server Prep Recap\n*******************************************************************************\n\nVPS versus PaaS::\n  We discussed the trade-offs of running your own server versus opting for a PaaS.\n  A VPS is great for learning, but you might find the lower admin overhead\n  of a PaaS makes sense for real projects.(((\"platform-as-a-service (PaaS)\", \"VPS versus\")))(((\"VPS (virtual private server)\", \"versus PaaS\", secondary-sortas=\"PaaS\")))\n\nDomain name registration and DNS::\n  This tends to be something you(((\"domains\", \"domain name registration and DNS\"))) only do once,\n  but buying a domain name and pointing it at your server\n  is an unavoidable part of hosting a web app.\n  Now you know your TTLs from your A-records!\n\nSSH::\n  SSH is the Swiss Army knife of server admin.(((\"SSH\")))\n  The dream is that everything is automated,\n  but now and again you just gotta open up a shell on that box!\n\nAnsible::\n  Ansible will be our deployment automation tool.(((\"Ansible\")))\n  We've had the barest of teasers,\n  but we have it installed and we're ready to learn how to use it.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_12_ansible.asciidoc",
    "content": "[[chapter_12_ansible]]\n== Infrastructure as Code: Automated Deployments with Ansible\n\n[quote, 'Cay S. Horstmann']\n______________________________________________________________\nAutomate, automate, automate.\n______________________________________________________________\n\n(((\"deployment\", \"automating with Ansible\", id=\"ix_dplyautAns\")))\n(((\"infrastructure as code (IaC)\")))(((\"IaC\", see=\"infrastructure as code\")))(((\"Ansible\", \"automated deployments with\", id=\"ix_Ansautd\")))\nNow that our server is up and running,\nwe want to install our app on it, using our Docker image and container.(((\"Docker\", \"installing app on server\")))\n\nWe _could_ do this manually,\nbut a key insight of modern software engineering\nis that small, frequent deployments are a must.\n\nNOTE: This insight about the importance of frequent deployments\n  we owe to https://nicolefv.com/writing[Nicole Forsgren] and the _State of DevOps_ reports.\n  They are some of the only really firm science we have\n  in the field of software engineering.\n\nFrequent deployments rely on automation,footnote:[\nSome readers mentioned a worry that using automation tools would leave them\nwith less understanding of the underlying infrastructure.\nBut in fact, using automation requires deep understanding of the things you're automating. So, don't worry; we'll be taking the time to look under the hood\nand make sure we know how things work.]\nso we'll use Ansible.\n\n[role=\"pagebreak-before\"]\nAutomation is also key to making sure our tests give us true confidence over our deployments.(((\"automation of tests, giving confidence in deployments\")))(((\"development server\", \"deploying\")))(((\"staging server\", \"deploying\")))\nIf we go to the trouble of building a staging server,footnote:[\nDepending on where you work, what I'm calling a \"staging\" server,\nsome people would call a \"development\" server,\nand some others would also like to distinguish \"preproduction\" servers.\nWhatever we call it, the point is to have somewhere we can try our code out\nin an environment that's as similar as possible to the real production server.\nAs we'll see, Docker isn't _quite_ enough!]\nwe want to make sure that it's as similar as possible to the production environment.\nBy automating the way we deploy, and using the same automation for staging and prod,\nwe give ourselves much more confidence.\n\nThe buzzword for automating your deployments these days is \"infrastructure as code\" (IaC).(((\"infrastructure as code (IaC)\")))\n\nNOTE: Why not ping me a note once your site is live on the web,\n    and send me the URL?\n    It always gives me a warm and fuzzy feeling...Email me at obeythetestinggoat@gmail.com.\n\n////\nDAVID overall notes\n\nI also think we're missing some stuff at the end about how all this might look\nas a development workflow. Maybe talk about setting up scripts (so we don't\nhave to remember the ansible command?) And what about releasing to production?\nIt doesn't need much, it just feels unfinished to me.\n////\n\n\n=== A First Cut of an Ansible Playbook for Deployment\n\nLet's start using Ansible a little more seriously.(((\"Ansible\", \"automated deployments with\", \"first draft of playbook for deployment\", id=\"ix_Ansautdplybk\")))(((\"Docker\", \"Ansible running simple container on our server\", id=\"ix_DckAns\")))\nWe're not going to jump all the way to the end though!\nBaby steps, as always.\nLet's see if we can get it to run a simple \"hello world\" Docker container on our server.\n\n[role=\"pagebreak-before\"]\nLet's delete the old content, which had the \"ping\",\nand replace it with something like this:\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l001)\n====\n[source,yaml]\n----\n---\n- hosts: all\n\n  tasks:\n\n    - name: Install docker  #<1>\n      ansible.builtin.apt:  #<2>\n        name: docker.io  #<3>\n        state: latest\n        update_cache: true\n      become: true\n\n    - name: Run test container\n      community.docker.docker_container:\n        name: testcontainer\n        state: started\n        image: busybox\n        command: echo hello world\n      become: true\n----\n====\n\n<1> An Ansible playbook is a series of \"tasks\"; we now have more than one.(((\"playbooks\", seealso=\"Ansible\")))\n    In that sense, it's still quite sequential and procedural,\n    but the individual tasks themselves are quite declarative.\n    Each one usually has a human-readable `name` attribute.\n\n<2> Each task uses an Ansible \"module\" to do its work.\n    This one uses the +b&#x2060;u&#x2060;i&#x2060;l&#x2060;t&#x200b;i&#x2060;n&#x2060;.&#x2060;a&#x2060;p&#x2060;t+ module, which provides a wrapper\n    around the `apt` Debian and Ubuntu package management tool.(((\"modules (Ansible)\")))\n\n<3> Each module then provides a bunch of parameters that control how it works.\n    Here, we specify the `name` of the package we want to install (\"docker.io\"footnote:[\n    In the official Docker installation instructions,\n    you'll see a recommendation to install Docker via a private package repository.\n    I wanted to avoid that complexity for the book,\n    but you should probably follow those instructions in a real-world scenario,\n    to make sure your version of Docker has all the latest security patches.])\n    and tell it to update its cache first, which is required on a fresh server.\n\nMost Ansible modules have pretty good documentation—check out the `builtin.apt` one for example;\nI often skip to the\nhttps://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html#examples[\"Examples\" section].\n\n[role=\"pagebreak-before\"]\nLet's rerun our deployment command, `ansible-playbook`,\nwith the same flags we used in the last chapter:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\nansible-playbook [core 2.16.3]\n  config file = None\n  [...]\nNo config file found; using defaults\nBECOME password:\nSkipping callback 'default', as we already have a stdout callback.\nSkipping callback 'minimal', as we already have a stdout callback.\nSkipping callback 'oneline', as we already have a stdout callback.\n\nPLAYBOOK: deploy-playbook.yaml ************************************************\n1 plays in infra/deploy-playbook.yaml\n\nPLAY [all] ********************************************************************\n\nTASK [Gathering Facts] ********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:2\nok: [staging.ottg.co.uk]\nPLAYBOOK: deploy-playbook.yaml ************************************************\n1 plays in infra/deploy-playbook.yaml\n\nTASK [Install docker] *********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:6\nok: [staging.ottg.co.uk] => {\"cache_update_time\": 1708981325, \"cache_updated\":\ntrue, \"changed\": false}\n\n\nTASK [Install docker] *********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:6\nchanged: [staging.ottg.co.uk] => {\"cache_update_time\": [...]\n\"cache_updated\": true, \"changed\": true, \"stderr\": \"\", \"stderr_lines\": [],\n\"stdout\": \"Reading package lists...\\nBuilding dependency tree...\\nReading [...]\ninformation...\\nThe following additional packages will be installed:\\n\nwmdocker\\nThe following NEW packages will be installed:\\n  docker wmdocker\\n0\n\nTASK [Run test container] *****************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:13\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"container\":\n{\"AppArmorProfile\": \"docker-default\", \"Args\": [\"hello\", \"world\"], \"Config\":\n[...]\n\nPLAY RECAP ********************************************************************\nstaging.ottg.co.uk         : ok=3    changed=2    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n// DAVID: rather than having to edit the username and domains each time,\n// what about getting the reader to set them as environment variables at the beginning of the chapter?\n\nI don't know about you, but whenever I make a terminal spew out a stream\nof output, I like to make little _brrp brrp brrp_ noises—a bit like the\ncomputer, Mother, in _Alien_.\nAnsible scripts are particularly satisfying in this regard.\n\n\nTIP: You may need to use the `--ask-become-pass` argument to `ansible-playbook`\n    if you get an error, \"Missing sudo password\".footnote:[\n    You can also look into \"passwordless sudo\" if it's all just too annoying,\n    but that does have security implications.]\n\n\n.Idempotence and Declarative Configuration\n*******************************************************************************\n\nIaC tools like Ansible aim to be \"declarative\",\nmeaning that, as much as possible, you specify the desired state that you want,\nrather than specifying a series of steps to get there.(((\"declarative IaC tools\")))(((\"infrastructure as code (IaC)\", \"declarative tools for\")))\n\nThis concept goes along with the idea of \"idempotence\",\nwhich is when you want a thing that has the same effect,\nwhether it is run just once or multiple times.(((\"idempotence\")))\n\nAn example is the `apt` module that we used to install Docker.\nIt doesn't crash if Docker is already installed and, in fact,\nAnsible is smart enough to check first before trying to install anything.\nIt makes no difference whether you run it once or many times.(((\"Ansible\", \"automated deployments with\", \"first draft of playbook for deployment\", startref=\"ix_Ansautdplybk\")))(((\"Docker\", \"Ansible running simple container on our server\", startref=\"ix_DckAns\")))\n\nIn contrast, adding an item to our to-do list is not currently idempotent.\nIf I add \"Buy milk\" and then I add \"Buy milk\" again, I end up with\ntwo items that both say \"Buy milk\". (We might fix that later, mind you.)\n\n*******************************************************************************\n\n\n=== SSHing Into the Server and Viewing Container Logs\n\nAnsible _looks_ like it's doing its job,\nbut let's practice our SSH skills,\nand do some good old-fashioned system admin.(((\"SSH\", \"SSHing into server and viewing container logs\", id=\"ix_SSHser\")))(((\"Docker\", \"viewing container logs on\")))\nLet's log in to our server and see if we can see any actual evidence\nthat our container has run.\n\nAfter we `ssh` in, we can use `docker ps`, just like we do on our own machine.\nWe pass the `-a` flag to view _all_ containers, including old/stopped ones.\nThen we can use `docker logs` to view the output from one of them:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ssh elspeth@staging.superlists.ottg.co.uk*\nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-67-generic x86_64)\n [...]\n\nelspeth@server$ *sudo docker ps -a*\nCONTAINER ID   IMAGE     COMMAND              CREATED      STATUS\nPORTS     NAMES\n3a2e600fbe77   busybox   \"echo hello world\"   2 days ago   Exited (0) 10\nminutes ago             testcontainer\n\nelspeth@server:$ *sudo docker logs testcontainer*\nhello world\n----\n\nTIP: Look out for that `elspeth@server`\n    in the command-line listings in this chapter.\n    It indicates commands that must be run on the server,\n    as opposed to commands you run on your own PC.\n\n\nSSHing in to check things worked is a key server debugging skill!\nIt's something we want to practice on our staging server,\nbecause ideally we'll want to avoid doing it on production machines.\n\n\n\n.Docker Debugging\n*******************************************************************************\n\n(((\"debugging\", \"Docker\")))(((\"Docker\", \"debugging\")))\nHere's a rundown of some of the debugging tools—some we've already seen\nand some new ones we'll use in this chapter.\nWhen things don't go to plan, they can help shed some light.\nAll of them should be run on the server, inside an SSH session:\n\n- You can check the Container logs using\n  `docker logs superlists`.\n\n- You can run things \"inside\" the container with\n  `docker exec <container-id-or-name> <cmd>`.\n  A couple of useful examples include `docker exec superlists env`,\n  to print environment variables, and just\n  `docker exec -it superlists bash` to open an interactive Bash shell,\n  inside the container.\n\n- You can get lots of detailed info on the _container_ using\n  `docker inspect superlists`.\n  This is a good place to go check on environment variables,\n  port mappings, and exactly which image was running, for example.\n\n\n- You can get detailed info on the _image_ with\n  `docker image inspect superlists`.\n  You might need this to check the exact image hash,\n  to make sure it's the same one you built locally.\n\n\n*******************************************************************************\n\n\n\n=== Allowing Rootless Docker Access\n\nHaving to use `sudo` or `become=True` to run Docker commands is a bit of a pain.(((\"SSH\", \"SSHing into server and viewing container logs\", startref=\"ix_SSHser\")))(((\"root user\", \"allowing rootless Docker access\", id=\"ix_rootls\")))(((\"Docker\", \"rootless access, allowing\", id=\"ix_Dckrtl\")))\nIf we add our user to the `docker` group, we can run Docker commands without `sudo`:\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l001-1)\n====\n[source,yaml]\n----\n  - name: Install docker\n        [...]\n\n  - name: Add our user to the docker group, so we don't need sudo/become\n    ansible.builtin.user:  # <1>\n      name: '{{ ansible_user }}'  # <2>\n      groups: docker\n      append: true  # don't remove any existing groups.\n    become: true\n\n  - name: Reset ssh connection to allow the user/group change to take effect\n    ansible.builtin.meta: reset_connection  # <3>\n\n  - name: Run test container  # <4>\n        [...]\n----\n====\n\n<1> We use the `builtin.user` module to add our user to the `docker` group.\n\n<2> The `{{ ... }}` syntax enables us to interpolate some variables into\n    our config file, much like in a Django template.\n    `ansible_user` will be the user we're using to connect to the server—i.e., \"elspeth\", in my case.\n\n<3> As per the task name, we need this for the user/group change to take effect.\n    Strictly speaking, this is only needed the first time we run the script;\n    if you've got some time, you can read up on how to\n    make tasks https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_conditionals.html[conditional]\n    and configure it to only run if the `builtin.user` tasks has actually made a change.\n\n<4> We can remove the `become: true` from this task and it should still work.\n\n[role=\"pagebreak-before\"]\nLet's run that:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\nPLAYBOOK: deploy-playbook.yaml ************************************************\n1 plays in infra/deploy-playbook.yaml\n\nPLAY [all] ********************************************************************\n\nTASK [Gathering Facts] ********************************************************\n[...]\nok: [staging.ottg.co.uk]\n\nTASK [Install docker] *********************************************************\n[...]\nok: [staging.ottg.co.uk] => {\"cache_update_time\": 1738767216, \"cache_updated\":\ntrue, \"changed\": false}\n\nTASK [Add our user to the docker group, so we don't need sudo/become] *********\n[...]\nchanged: [staging.ottg.co.uk] => {\"append\": false, \"changed\": true, [...]\n\"\", \"group\": 1000, \"groups\": \"docker\", [...]\n\nTASK [Reset ssh connection to allow the user/group change to take effect] *****\n[...]\nMETA: reset connection\n\nTASK [Run test container] *****************************************************\n[...]\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"container\": [...]\n\nPLAY RECAP ********************************************************************\nstaging.ottg.co.uk         : ok=4    changed=2    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\nAnd check that it worked:\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server$ *docker ps -a*  # no sudo yay!\nCONTAINER ID   IMAGE        COMMAND                  CREATED          STATUS\nPORTS     NAMES\nbd3114e43f55   busybox      \"echo hello world\"       12 minutes ago   Exited (0)\n6 seconds ago               testcontainer\n\nelsepth@server$ *docker logs testcontainer*\nhello world\nhello world\n----\n\n[role=\"pagebreak-before\"]\nSure enough, we no longer need `sudo`,\nand we can see that a new version of the container just ran.\n\nYou know, that's worthy of a commit!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add infra/deploy-playbook.yaml*\n$ *git commit -m \"Made a start on an ansible playbook for deployment\"*\n----\n\n\nLet's move on to trying to get our actual Docker container running on the server.\nAs we go through, you'll see that we're going to work through very similar issues\nto the ones we've already figured our way through in the last couple of chapters:\n\n* Configuration\n* Networking\n* The database(((\"root user\", \"allowing rootless Docker access\", startref=\"ix_rootls\")))(((\"Docker\", \"rootless access, allowing\", startref=\"ix_Dckrtl\")))\n\n\n=== Getting Our Image Onto the Server\n\nTypically, you can \"push\" and \"pull\" container images\nto a \"container registry\"—Docker offers a public one called Docker Hub,\nand organisations will often run private ones,\nhosted by cloud providers like AWS.(((\"Ansible\", \"automated deployments with\", \"getting container image onto server\", id=\"ix_Ansautdcntnr\")))(((\"Docker\", \"getting container image onto our server\", id=\"ix_Dckcntimg\")))(((\"containers\", \"getting container image onto server\", id=\"ix_cntnrser\")))\n\nSo your process of getting an image onto a server is usually:\n\n1. Push the image from your machine to the registry.\n\n2. Pull the image from the registry onto the server.\n  Usually this step is implicit,\n  in that you just specify the image name in the format `registry-url/image-name:tag`,\n  and then `docker run` takes care of pulling down the image for you.\n\nBut I don't want to ask you to create a Docker Hub account,\nnor implicitly endorse any particular provider,\nso we're going to \"simulate\" this process by doing it manually.\n\n[role=\"pagebreak-before\"]\nIt turns out you can \"export\" a container image to an archive format,\nmanually copy that to the server, and then reimport it.\nIn Ansible config, it looks like this:\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l002)\n====\n[source,yaml]\n----\n  - name: Install docker\n        [...]\n  - name: Add our user to the docker group, so we don't need sudo/become\n        [...]\n  - name: Reset ssh connection to allow the user/group change to take effect\n        [...]\n\n  - name: Export container image locally  # <1>\n    community.docker.docker_image:\n      name: superlists\n      archive_path: /tmp/superlists-img.tar\n      source: local\n    delegate_to: 127.0.0.1\n\n  - name: Upload image to server  # <2>\n    ansible.builtin.copy:\n      src: /tmp/superlists-img.tar\n      dest: /tmp/superlists-img.tar\n\n  - name: Import container image on server  # <3>\n    community.docker.docker_image:\n      name: superlists\n      load_path: /tmp/superlists-img.tar\n      source: load\n      force_source: true  # <4>\n      state: present\n\n  - name: Run container\n    community.docker.docker_container:\n      name: superlists\n      image: superlists  # <5>\n      state: started\n      recreate: true  # <6>\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> We export the Docker image to a _.tar_ file by using the `docker_image` module\n  with the `archive_path` set to a tempfile, and setting the `delegate_to` attribute\n  to say we're running that command on our local machine rather than the server.\n\n<2> We then use the `copy` module to upload the _.tar_ file to the server.\n\n<3> And we use `docker_image` again, but this time with `load_path` and `source: load`\n  to import the image back on the server.\n\n<4> The `force_source` flag tells the server to attempt the import,\n    even if an image of that name already exists.\n\n<5> We change our \"run container\" task to use the `superlists` image,\n    and we'll use that as the container name too.\n\n<6> Similarly to `source: load`, the `recreate` argument tells Ansible\n    to re-create the container even if there's already one running\n    whose name and image match \"superlists\".\n\n// TODO: consider using commit id as image tag to avoid the force_source.\n\nNOTE: If you see an error saying \"Error connecting: Error while fetching server API version\",\n    it may be because the Python Docker software development kit (SDK) can't find your Docker daemon.\n    Try restarting Docker Desktop if you're on Windows or a Mac.(((\"DOCKER_HOST environment variable\")))\n    If you're not using the standard Docker engine—with Colima or Podman, for example—you may need to set the `DOCKER_HOST` environment variable\n    (e.g., `DOCKER_HOST=unix:///$HOME/.colima/default/docker.sock`)\n    or use a symlink to point to the right place.\n    See the\n    https://oreil.ly/gPJmq[Colima FAQ]\n    or https://oreil.ly/Hqoma[Podman docs].\n\n[role=\"pagebreak-before\"]\nLet's run the new version of our playbook,\nand see if we can upload a Docker image to our server and get it running:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\n[...]\n\nPLAYBOOK: deploy-playbook.yaml **********************************************\n1 plays in infra/deploy-playbook.yaml\n\nPLAY [all] ********************************************************************\n\nTASK [Gathering Facts] ********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:2\nok: [staging.ottg.co.uk]\n\nTASK [Install docker] *********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:5\nok: [staging.ottg.co.uk] => {\"cache_update_time\": 1708982855, \"cache_updated\":\nfalse, \"changed\": false}\nTASK [Add our user to the docker group, so we don't need sudo/become] *********\ntask path: ...goat-book/infra/deploy-playbook.yaml:11\nok: [staging.ottg.co.uk] => {\"append\": false, \"changed\": false, [...]\n\nTASK [Reset ssh connection to allow the user/group change to take effect] *****\ntask path: ...goat-book/infra/deploy-playbook.yaml:17\nMETA: reset connection\n\nTASK [Export container image locally] *****************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:20\nchanged: [staging.ottg.co.uk -> 127.0.0.1] => {\"actions\": [\"Archived image\nsuperlists:latest to /tmp/superlists-img.tar, overwriting archive with image\n11ff3b83873f0fea93f8ed01bb4bf8b3a02afa15637ce45d71eca1fe98beab34 named\nsuperlists:latest\"], \"changed\": true, \"image\": {\"Architecture\": \"amd64\",\n[...]\n\nTASK [Upload image to server] *************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:27\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"checksum\":\n\"313602fc0c056c9255eec52e38283522745b612c\", \"dest\": \"/tmp/superlists-img.tar\",\n[...]\n\nTASK [Import container image on server] ***************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:32\nchanged: [staging.ottg.co.uk] => {\"actions\": [\"Loaded image superlists:latest\nfrom /tmp/superlists-img.tar\"], \"changed\": true, \"image\": {\"Architecture\":\n\"amd64\", \"Author\": \"\", \"Comment\": \"buildkit.dockerfile.v0\", \"Config\":\n[...]\n\nTASK [Run container] **********************************************************\ntask path: ...goat-book/superlists/infra/deploy-playbook.yaml:40\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"container\":\n{\"AppArmorProfile\": \"docker-default\", \"Args\": [\"--bind\", \":8888\",\n\"superlists.wsgi:application\"], \"Config\": {\"AttachStderr\": true, \"AttachStdin\":\nfalse, \"AttachStdout\": true, \"Cmd\": [\"gunicorn\", \"--bind\", \":8888\",\n\"superlists.wsgi:application\"], \"Domainname\": \"\", \"Entrypoint\": null, \"Env\":\n[...]\nstaging.ottg.co.uk         : ok=7    changed=4    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n\nThat looks good!\n\nFor completeness, let's also add a step to explicitly build the image locally\n(this means we aren't dependent on having run `docker build` locally):\n\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l003)\n====\n[source,yaml]\n----\n    - name: Reset ssh connection to allow the user/group change to take effect\n      [...]\n\n    - name: Build container image locally\n      community.docker.docker_image:\n        name: superlists\n        source: build\n        state: present\n        build:\n          path: ..\n          platform: linux/amd64  # <1>\n        force_source: true\n      delegate_to: 127.0.0.1\n\n    - name: Export container image locally\n      [...]\n----\n====\n\n<1> I needed this `platform` attribute to work around an issue\n  with compatibility between Apple's new ARM-based chips and our server's\n  x86/AMD64 architecture.\n  You could also use this `platform:` to cross-build Docker images\n  for a Raspberry Pi from a regular PC, or vice versa.\n  It does no harm in any case.(((\"Ansible\", \"automated deployments with\", \"getting container image onto server\", startref=\"ix_Ansautdcntnr\")))(((\"Docker\", \"getting container image onto our server\", startref=\"ix_Dckcntimg\")))(((\"containers\", \"getting container image onto server\", startref=\"ix_cntnrser\")))\n\n\n\n==== Taking a Look Around Manually\n\n\nTime to take another proverbial look under the hood,\nto check whether it really worked.(((\"Ansible\", \"automated deployments with\", \"checking if container deployment worked\")))\nHopefully we'll see a container that looks like ours:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ssh elspeth@staging.superlists.ottg.co.uk*\nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-67-generic x86_64)\n [...]\n\nelspeth@server$ *docker ps -a*\nCONTAINER ID   IMAGE     COMMAND              CREATED      STATUS\nPORTS     NAMES\n3a2e600fbe77   busybox   \"echo hello world\"   2 days ago   Exited (0) 10\nminutes ago             testcontainer\n129e36a42190   superlists   \"/bin/sh -c \\'gunicor…\"   About a minute ago\nExited (3) About a minute ago             superlists\n----\n\n\nOK!  We can see our \"superlists\" container is there now,\nboth named \"superlists\" and based on an image called \"superlists\".\n\nThe `Status: Exited` is a bit more worrying though.\n\n[role=\"pagebreak-before\"]\nStill, that's a good bit of progress, so let's do a commit\n(back on your own machine):\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am\"Build our image, use export/import to get it on the server, try and run it\"*\n----\n\n\n===== Docker logs\n\nNow, back on the server, let's take a look at the logs of our new container\nto see if we can figure (((\"Ansible\", \"automated deployments with\", \"checking Docker logs on container deployment\")))(((\"Docker\", \"checking logs of container deployed to server\")))out what's happened:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:$ *docker logs superlists*\n[2024-02-26 22:19:15 +0000] [1] [INFO] Starting gunicorn 21.2.0\n[2024-02-26 22:19:15 +0000] [1] [INFO] Listening at: http://0.0.0.0:8888 (1)\n[2024-02-26 22:19:15 +0000] [1] [INFO] Using worker: sync\n[...]\n  File \"/src/superlists/settings.py\", line 22, in <module>\n    SECRET_KEY = os.environ[\"DJANGO_SECRET_KEY\"]\n                 ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^\n  File \"<frozen os>\", line 685, in __getitem__\nKeyError: 'DJANGO_SECRET_KEY'\n[2024-02-26 22:19:15 +0000] [7] [INFO] Worker exiting (pid: 7)\n[2024-02-26 22:19:15 +0000] [1] [ERROR] Worker (pid:7) exited with code 3\n[2024-02-26 22:19:15 +0000] [1] [ERROR] Shutting down: Master\n[2024-02-26 22:19:15 +0000] [1] [ERROR] Reason: Worker failed to boot.\n----\n\nOh, whoops; it can't find the `DJANGO_SECRET_KEY` environment variable.\nWe need to set those environment variables on the server too.(((\"DJANGO_SECRET_KEY environment variable\")))\n\n\n=== Setting Environment Variables and Secrets\n\nWhen we run our container manually locally with `docker run`,\nwe can pass in environment variables with the `-e` flag.(((\"secrets\", \"setting and checking on deployed Docker container\", id=\"ix_scrtcntn\")))(((\"environment variables\", \"setting and checking on deployed Docker container\", id=\"ix_envvarDckcnt\")))(((\"Ansible\", \"automated deployments with\", \"setting environment variables and secrets on Docker container\", id=\"ix_Ansautdenv\")))(((\"Docker\", \"setting environment variables and secrets\", id=\"ix_Dckenvsec\")))\nAs we'll see, it's fairly straightforward to replicate that with Ansible,\nusing the `env` parameter for the `docker.docker_container` module\nthat we're already using.\n\nBut there is at least one \"secret\" value that we don't want to hardcode\ninto our Ansible YAML file: the Django `SECRET_KEY` setting.\n\nThere are many different ways of dealing with secrets;\ndifferent cloud providers have their own tools. There's also HashiCorp Vault—it has varying levels of complexity and security.\n\nWe don't have time to go into detail on those in this book.\nInstead, we'll generate a one-off secret key value from a random string,\nand we'll store it to a file on disk on the server.\nThat's a reasonable amount of security for our purposes.\n\n[role=\"pagebreak-before\"]\nSo, here's the plan:\n\n1. We generate a random, one-off secret key the first time we deploy to a new server, and we store it in a file on disk.\n\n2. We read the secret key value back from that file to put it into the container's environment variables.\n\n3. We set the rest of the env vars we need as well.\n\nHere's what it looks like:\n\n\n[role=\"sourcecode small-code\"]\n.infra/deploy-playbook.yaml (ch12l005)\n====\n[source,yaml]\n----\n    - name: Import container image on server\n      [...]\n\n    - name: Ensure .secret-key file exists\n      # the intention is that this only happens once per server\n      ansible.builtin.copy:  # <1>\n        dest: ~/.secret-key\n        content: \"{{ lookup('password', '/dev/null length=32 chars=ascii_letters') }}\"  # <2>\n        mode: 0600\n        force: false  # do not recreate file if it already exists.\n\n    - name: Read secret key back from file\n      ansible.builtin.slurp:  # <3>\n        src: ~/.secret-key\n      register: secret_key\n\n    - name: Run container\n      community.docker.docker_container:\n        name: superlists\n        image: superlists\n        state: started\n        recreate: true\n        env:  # <4>\n          DJANGO_DEBUG_FALSE: \"1\"\n          DJANGO_SECRET_KEY: \"{{ secret_key.content | b64decode }}\"  # <5>\n          DJANGO_ALLOWED_HOST: \"{{ inventory_hostname }}\"  # <6>\n          DJANGO_DB_PATH: \"/home/nonroot/db.sqlite3\"\n----\n====\n\n<1> The `builtin.copy` module can be used to copy local files up to the server,\n    and also, as we're demonstrating here, to populate a file\n    with an arbitrary string `content`.\n\n<2> This `lookup('password')` thing is how we'll get a random string of characters.\n    I copy-pasted it from Stack Overflow. Come on; there's no shame in that.\n    The rest of the `builtin.copy` directive is designed to save the value to disk,\n    but only if the file doesn't already exist.\n    The `0600` permission will ensure that only the \"elspeth\" user can read it.\n\n<3> The `slurp` command reads the contents of a file on the server,\n    and we can `register` its contents into a variable.\n    Slightly annoyingly, it uses base64 encoding\n    (it's so you can also use it to read binary files).\n    Anyway, the idea is, even though we don't _rewrite_ the file on every deploy,\n    we do _reread_ the value on every deploy.\n\n<4> Here's the `env` parameter for our container.\n\n<5> Here's how we get our original value for the secret key,\n    using the `| b64decode` to decode it back to a regular string.\n\n<6> `inventory_hostname` represents the hostname of the current server\n    we're deploying to, so _staging.ottg.co.uk_ in our case.\n\n\nLet's run this latest version of our playbook now:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -v*]\n[...]\nPLAYBOOK: deploy-playbook.yaml **********************************************\n1 plays in infra/deploy-playbook.yaml\n\nPLAY [all] ********************************************************************\n\nTASK [Gathering Facts] ********************************************************\nok: [staging.ottg.co.uk]\n\nTASK [Install docker] *********************************************************\nok: [staging.ottg.co.uk] => {\"cache_update_time\": 1709136057, \"cache_updated\":\nfalse, \"changed\": false}\n\nTASK [Build container image locally] ******************************************\nchanged: [staging.ottg.co.uk -> 127.0.0.1] => {\"actions\": [\"Built image [...]\n\nTASK [Export container image locally] *****************************************\nchanged: [staging.ottg.co.uk -> 127.0.0.1] => {\"actions\": [\"Archived image [...]\n\nTASK [Upload image to server] *************************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, [...]\n\nTASK [Import container image on server] ***************************************\nchanged: [staging.ottg.co.uk] => {\"actions\": [\"Loaded image [...]\n\nTASK [Ensure .env file exists] ************************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, [...]\n\nTASK [Run container] **********************************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"container\": [...]\n\nPLAY RECAP ********************************************************************\nstaging.ottg.co.uk         : ok=8    changed=6    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n[role=\"pagebreak-before less_space\"]\n==== Manually Checking Environment Variables for Running Containers\n\nWe'll do one more manual check with SSH, to see if those env vars were set correctly.(((\"secrets\", \"setting and checking on deployed Docker container\", startref=\"ix_scrtcntn\")))(((\"Docker\", \"setting environment variables and secrets\", \"checking environment variables with docker ps\")))\nThere's a couple of ways we can do this.\n\nLet's start with a `docker ps` to check whether our container is running:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:$ *docker ps*\nCONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS\nPORTS     NAMES\n96d867b42a31   superlists   \"gunicorn --bind :88…\"   6 seconds ago   Up 5\nseconds             superlists\n----\n\nLooking good!  The `STATUS: Up 5 Seconds` is better than the `Exited` we had before;\nthat means the container is up and running.\n\nLet's take a look at the `docker logs` too:\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:~$ *docker logs superlists*\n[2025-05-02 17:55:18 +0000] [1] [INFO] Starting gunicorn 23.0.0\n[2025-05-02 17:55:18 +0000] [1] [INFO] Listening at: http://0.0.0.0:8888 (1)\n[2025-05-02 17:55:18 +0000] [1] [INFO] Using worker: sync\n[2025-05-02 17:55:18 +0000] [7] [INFO] Booting worker with pid: 7\n----\n\nAlso looking good; no sign of an error. Now let's check on those environment variables.\nThere are two ways we can do this: `docker exec env` and `docker inspect`.\n\n===== docker exec env\n\nOne way is to run the standard shell `env` command,\nwhich prints out all environment variables.(((\"Docker\", \"setting environment variables and secrets\", \"checking settings with docker exec env\")))\nWe run it \"inside\" the container with `docker exec`:\n\n\n[role=\"server-commands small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:~$ *docker exec superlists env*\nPATH=/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nHOSTNAME=96d867b42a31\nDJANGO_DEBUG_FALSE=1\nDJANGO_SECRET_KEY=cXACJZTvoPfWFSBSTdixJTlXCWYTnJlC\nDJANGO_ALLOWED_HOST=staging.ottg.co.uk\nDJANGO_DB_PATH=/home/nonroot/db.sqlite3\nGPG_KEY=7169605F62C751356D054A26A821E680E5FA6305\nPYTHON_VERSION=3.14.3\nPYTHON_SHA256=40f868bcbdeb8149a3149580bb9bfd407b3321cd48f0be631af955ac92c0e041\nHOME=/home/nonroot\n----\n\n[role=\"pagebreak-before less_space\"]\n===== docker inspect\n\nAnother option--useful (((\"Docker\", \"setting environment variables and secrets\", \"checking settings with docker inspect\")))for debugging other things too,\nlike image IDs and mounts--is to use `docker inspect`:\n\n[role=\"server-commands small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:~$ *docker inspect superlists*\n[\n    {\n        [...]\n        \"Config\": {\n            [...]\n            \"Env\": [\n                \"DJANGO_DEBUG_FALSE=1\",\n                \"DJANGO_SECRET_KEY=cXACJZTvoPfWFSBSTdixJTlXCWYTnJlC\",\n                \"DJANGO_ALLOWED_HOST=staging.ottg.co.uk\",\n                \"DJANGO_DB_PATH=/home/nonroot/db.sqlite3\",\n                \"PATH=/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/[...]\n                \"GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305\",\n                \"PYTHON_VERSION=3.14.3\",\n                \"PYTHON_SHA256=40f868bcbdeb8149a3149580bb9bfd407b332[...]\n            ],\n            \"Cmd\": [\n                \"gunicorn\",\n                \"--bind\",\n                \":8888\",\n                \"superlists.wsgi:application\"\n            ],\n            \"Image\": \"superlists\",\n            \"Volumes\": null,\n            \"WorkingDir\": \"/src\",\n            \"Entrypoint\": null,\n            \"OnBuild\": null,\n            \"Labels\": {}\n        },\n        \"NetworkSettings\": {\n          [...]\n        }\n    }\n]\n----\n\nThere's a lot of output!\nIt's more or less everything that Docker knows about the container.\nBut if you scroll around, you can usually get some useful info for debugging\nand diagnostics—like, in this case,\nthe `Env` parameter which tells us what environment variables were set for the container.\n\n\nTIP: `docker inspect` is also useful\n    for checking exactly which image ID a container is using,\n    and which filesystem mounts are configured.\n\nLooking good!(((\"environment variables\", \"setting and checking on deployed Docker container\", startref=\"ix_envvarDckcnt\")))(((\"Ansible\", \"automated deployments with\", \"setting environment variables and secrets on Docker container\", startref=\"ix_Ansautdenv\")))(((\"Docker\", \"setting environment variables and secrets\", startref=\"ix_Dckenvsec\")))\n\n[role=\"pagebreak-before less_space\"]\n=== Running FTs to Check on Our Deploy\n\nEnough manual checking via SSH; let's see what our tests think.(((\"deployment\", \"running functional tests to check server deployment\", id=\"ix_dplytstser\")))\nThe `TEST_SERVER` adaptation we made in <<chapter_09_docker>>\ncan also be used to check against our staging server.\n\n// DAVID: I originally just pasted this as-is, which contacted YOUR server. Another\n// reason to get them to set environment variables at the start of the chapter.\n\nLet's see what they think:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests*]\n[...]\nselenium.common.exceptions.WebDriverException: Message: Reached error page:\nabout:neterror?e=connectionFailure&u=http%3A//staging.ottg.co.uk/[...]\n[...]\nRan 3 tests in 5.014s\n\nFAILED (errors=3)\n----\n\nNone of them passed. Hmm.\nThat `neterror` makes me think it's another networking problem.\n\nNOTE: If your domain provider puts up a temporary holding page,\n    you may get a 404 rather than a connection error at this point,\n    and the traceback might have \"NoSuchElementException\" instead.\n\n\n==== Manual Debugging with curl Against the Staging Server\n\nLet's try our standard debugging technique of using `curl`\nboth locally and then from inside the container on the server.\nFirst, on(((\"debugging\", \"of staging server deployment\", \"manually, using curl\", secondary-sortas=\"staging\")))(((\"curl utility\", \"debugging against staging staging server with \"))) our own machine:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*curl -iv staging.ottg.co.uk*]\n[...]\ncurl: (7) Failed to connect to staging.ottg.co.uk port 80 after 25 ms: Couldn't\nconnect to server\n----\n\n\nNOTE: Similarly, depending on your domain/hosting provider,\n    you may see \"Host not found\" here instead.\n    Or, if your version of `curl` is different, you might see\n    \"Connection refused\".\n\n\nNow let's SSH in to our server and take a look at the Docker logs:\n\n// TODO: rework server-commands book parser to detect \"elsepth@server\" instead of manual skips (or role=)\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server$ *docker logs superlists*\n[2024-02-28 22:14:43 +0000] [7] [INFO] Starting gunicorn 21.2.0\n[2024-02-28 22:14:43 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7)\n[2024-02-28 22:14:43 +0000] [7] [INFO] Using worker: sync\n[2024-02-28 22:14:43 +0000] [8] [INFO] Booting worker with pid: 8\n----\n\nNo errors there.  Let's try our `curl`:\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server$ *curl -iv localhost*\n*   Trying 127.0.0.1:80...\n* connect to 127.0.0.1 port 80 failed: Connection refused\n*   Trying ::1:80...\n* connect to ::1 port 80 failed: Connection refused\n* Failed to connect to localhost port 80 after 0 ms: Connection refused\n* Closing connection 0\ncurl: (7) Failed to connect to localhost port 80 after 0 ms: Connection refused\n----\n\nHmm, `curl` fails on the server too.\nBut all this talk of port `80`, both locally and on the server, might be giving us a clue.\nLet's check `docker ps`:\n\n// CSANAD: Ackchually I'm not sure if it's supposed to work, since we set\n//         `inventory_hostname` for DJANGO_ALLOWED_HOSTS, so `localhost`\n// would not get through.\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\nelspeth@server:$ *docker ps*\nCONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS\nPORTS     NAMES\n1dd87cbfa874   superlists   \"/bin/sh -c 'gunicor…\"   9 minutes ago   Up 9\nminutes             superlists\n----\n\nThis might be ringing a bell now--we forgot the ports.(((\"ports\", \"mapping between container and deployed server\")))\n\nWe want to map port `8888` inside the container as port `80` (the default web/HTTP port)\non the server:\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l006)\n====\n[source,yaml]\n----\n    - name: Run container\n      community.docker.docker_container:\n        name: superlists\n        image: superlists\n        state: started\n        recreate: true\n        env:\n          DJANGO_DEBUG_FALSE: \"1\"\n          DJANGO_SECRET_KEY: \"{{ secret_key.content | b64decode }}\"\n          DJANGO_ALLOWED_HOST: \"{{ inventory_hostname }}\"\n          DJANGO_DB_PATH: \"/home/nonroot/db.sqlite3\"\n        ports: 80:8888\n----\n====\n\nNOTE: You can map a different port on the outside\n    to the one that's \"inside\" the Docker container.\n    In this case, we can map the public-facing standard HTTP port `80` on the host\n    to the arbitrarily chosen port `8888` on the inside.\n\n\nLet's push that up with `ansible-playbook`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, \\\n  infra/deploy-playbook.yaml -v*]\n[...]\n----\n\n[role=\"pagebreak-before\"]\nAnd now give the FTs another go:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_list_table\"]; [...]\n[...]\nRan 3 tests in 21.047s\n\nFAILED (errors=3)\n----\n\nSo, 3/3 failed again, but the FTs _did_ get a little further along.\nIf you saw what was happening,\nor if you go and visit the site manually in your browser,\nyou'll see that the home page loads fine,\nbut as soon as we try and create a new list item,\nit crashes with a 500 error.(((\"deployment\", \"running functional tests to check server deployment\", startref=\"ix_dplytstser\")))\n\n\n=== Mounting the Database on the Server and Running Migrations\n\nLet's do another bit of (((\"databases\", \"mounting on deployed server and running migrations\", id=\"ix_DBmntcntr\")))manual debugging,\nand take a look at the logs from our container with `docker logs`.\nYou'll see an `OperationalError`:\n\n\n[role=\"server-commands\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *ssh elspeth@server docker logs superlists*\n[...]\ndjango.db.utils.OperationalError: no such table: lists_list\n----\n\n\nIt looks like our database isn't initialised.\nAha! Another of those deployment \"danger areas\".\n\nJust like we did on our own machine,\nwe need to mount the `db.sqlite3` file from the filesystem outside the container.\nWe'll also want to run migrations to create the database\nand, in fact, each time we deploy,\nso that any updates to the database schema\nget applied to the database on the server.\n\nHere's the plan:\n\n1. On the host machine, we'll store the database in elspeth's home folder;\n  it's as good a place as any.\n\n2. We'll set its UID to `1234`,\n  just like we did in <<chapter_10_production_readiness>>,\n  to match the UID of the `nonroot` user inside the container.\n\n3. Inside the container, we'll use the path `/home/nonroot/db.sqlite3`—again, just like in the last chapter.\n\n4. We'll run the migrations with a `docker exec`,\n  or the Ansible equivalent thereof.\n\n[role=\"pagebreak-before\"]\nHere's what that looks like:\n\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch12l007)\n====\n[source,python]\n----\n    - name: Ensure db.sqlite3 file exists outside container\n      ansible.builtin.file:\n        path: \"{{ ansible_env.HOME }}/db.sqlite3\"  # <1>\n        state: touch  # <2>\n        owner: 1234  # so nonroot user can access it in container\n      become: true  # needed for ownership change\n\n    - name: Run container\n      community.docker.docker_container:\n        name: superlists\n        image: superlists\n        state: started\n        recreate: true\n        env:\n          DJANGO_DEBUG_FALSE: \"1\"\n          DJANGO_SECRET_KEY: \"{{ secret_key.content | b64decode }}\"\n          DJANGO_ALLOWED_HOST: \"{{ inventory_hostname }}\"\n          DJANGO_DB_PATH: \"/home/nonroot/db.sqlite3\"\n        mounts:  # <3>\n          - type: bind\n            source: \"{{ ansible_env.HOME }}/db.sqlite3\"  # <1>\n            target: /home/nonroot/db.sqlite3\n        ports: 80:8888\n\n    - name: Run migration inside container\n      community.docker.docker_container_exec:  # <4>\n        container: superlists\n        command: ./manage.py migrate\n\n----\n====\n\n<1> `ansible_env` gives us access to the environment variables on the server,\n    including `HOME`, which is the path to the home folder (_/home/elspeth/_ in my case).\n\n<2> We use `file` with `state=touch` to make sure a placeholder file exists\n    before we try and mount it in.\n\n<3> Here is the `mounts` config, which works a lot like the `--mount` flag to\n    `docker run`.\n\n<4> And we use the `docker.container_exec` module\n    to give us the functionality of `docker exec`,\n    to run the migration command inside the container.\n\n[role=\"pagebreak-before\"]\nLet's give that playbook a run and...\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -v*]\n[...]\nTASK [Run migration inside container] *****************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"rc\": 0, \"stderr\": \"\",\n\"stderr_lines\": [], \"stdout\": \"Operations to perform:\\n  Apply all migrations:\nauth, contenttypes, lists, sessions\\nRunning migrations:\\n  Applying\ncontenttypes.0001_initial... OK\\n  Applying\ncontenttypes.0002_remove_content_type_name... OK\\n  Applying\nauth.0001_initial... OK\\n  Applying\nauth.0002_alter_permission_name_max_length... OK\\n  Applying\n[...]\nPLAY RECAP ********************************************************************\nstaging.ottg.co.uk         : ok=9    changed=2    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n=== It Workssss\n\nTry the tests...\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests*]\nFound 3 test(s).\n[...]\n\n...\n ---------------------------------------------------------------------\nRan 3 tests in 13.537s\nOK\n----\n\nHooray!(((\"databases\", \"mounting on deployed server and running migrations\", startref=\"ix_DBmntcntr\")))\n\nAll the tests pass!\nThat gives us confidence that our automated deploy script can reproduce a fully working app,\non a server, hosted on the public internet.\n\nThat's worthy of a commit:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n# should show our changes in deploy-playbook yaml\n$ *git commit -am\"Save secret key, set env vars, mount db, run migrations. It works :)\"*\n----\n\n\n\n\n////\nold content follows\n\n==== Use Vagrant to Spin Up a Local VM\n\nRunning tests against the staging site gives us the ultimate confidence that\nthings are going to work when we go live, but we can also use a VM on our\nlocal machine.\n\nDownload Vagrant and Virtualbox, and see if you can get Vagrant to build a\ndev server on your own PC, using our Ansible playbook to deploy code to it.\nRewire the FT runner to be able to test against the local VM.\n\nHaving a Vagrant config file is particularly helpful when working\nin a team--it helps new developers to spin up servers that look exactly\nlike yours.(((\"\", startref=\"ansible29\")))\n////\n\n\n\n\n=== Deploying to Prod\n\n\nNow that we are confident in our deploy script,\nlet's try using it for our live site!(((\"deployment\", \"deploying to production\")))(((\"domains\", \"passing production domain name to Ansible playbook\")))\n\nThe main change is to the `-i` flag, where we pass in the production\ndomain name, instead of the staging one:\n\n[role=\"small-code against-server\"]\n[subs=\"\"]\n----\n$ <strong>ansible-playbook --user=elspeth -i www.ottg.co.uk, infra/deploy-playbook.yaml -vv</strong>\n[...]\n\nDone.\nDisconnecting from elspeth@www.ottg.co.uk... done.\n----\n\n\n_Brrp brrp brpp_.  Looking good?  Go take a click around your live site!\n\n\n\n=== Git Tag the Release\n\n\n(((\"Git\", \"tagging releases\")))\nOne final bit of admin.\nTo preserve a historical marker,\nwe'll use Git tags to mark the state of the codebase\nthat reflects what's currently live on the server:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git tag LIVE*\n$ *export TAG=$(date +DEPLOYED-%F/%H%M)*  # this generates a timestamp\n$ *echo $TAG* # should show \"DEPLOYED-\" and then the timestamp\n$ *git tag $TAG*\n$ *git push origin LIVE $TAG* # pushes the tags up to GitHub\n----\n\nNow it's easy, at any time, to check what the difference is\nbetween our current codebase and what's live on the servers.\nThis will come in handy in a few chapters,\nwhen we look at database migrations.\nHave a look at the tag in the history:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git log --graph --oneline --decorate*\n* 1d4d814 (HEAD -> main) Save secret key, set env vars, mount db, run\nmigrations. It works :)\n* 95e0fe0 Build our image, use export/import to get it on the server, try and\nrun it\n* 5a36957 Made a start on an ansible playbook for deployment\n[...]\n----\n\nNOTE: Once again, this use of Git tags isn't meant to be the _one true way_.\n  We just need some sort of way to keep track of what was deployed when.\n\n\n=== Tell Everyone!\n\n\nYou now have a live website!  Tell all your friends!\nTell your mum, if no one else is interested!\nOr, tell me!  I'm always delighted to see a new reader's site:\nobeythetestinggoat@gmail.com!\n\nCongratulations again for getting through this block of deployment chapters;\nI know they can be challenging.  I hope you got something out of them—seeing a practical example of how to take these kinds of complex changes and break them down into small, incremental steps, getting frequent feedback\nfrom our tests and manual investigations along the way.\n\nNOTE: Our next deploy won't be until <<chapter_18_second_deploy>>,\n    so you can switch off your servers until then if you want to.\n    If you're using a platform where you only get one month of free hosting,\n    it might run out by then.  You might have to shell out a few bucks,\n    or see if there's some way of getting another free month.\n\n\nIn the next chapter, it's back to coding again.\n(((\"\", startref=\"Fstage11\")))\n\n\n=== Further Reading\n\n(((\"automated deployment\", \"additional resources\")))\nThere's no such thing as the _one true way_ in deployment;\nI've tried to set you off on a reasonably sane path,\nbut there are plenty of things you could do differently—and lots, _lots_ more to learn besides.\nHere are some resources I used for inspiration,\n(including a couple I've already mentioned):\n\n\n* The original https://12factor.net[Twelve-Factor App] manifesto from the Heroku team\n\n* The official Django docs'\n  https://docs.djangoproject.com/en/5.2/howto/deployment/checklist[Deployment Checklist]\n\n* https://oreil.ly/SPDMv[\"How to Write Deployment-friendly Applications\"] by Hynek Schlawack\n\n* The deployment chapter of\n  https://oreil.ly/x7PoY[_Two Scoops of Django_]\n  by Daniel and Audrey Roy Greenfield\n\n* The PythonSpeed\n  https://pythonspeed.com/docker[\"Docker packaging for Python developers\"] guide\n\n\n.Automated Deployment and IaC Recap\n*******************************************************************************\n\nHere's a brief recap of (((\"infrastructure as code (IaC)\", \"recap of IaC and automated deployment\")))what we've been through,\nwhich are a fairly typical set of steps for deployment in general:\n\nProvisioning a server:: This tends to be vendor-specific,\n  so we didn't automate it, but you absolutely can!\n\nInstalling system dependencies:: In our case, it was mainly Docker.\n  But inside the Docker image, we also had some system dependencies too,\n  like Python itself.  The installation of both types of dependencies\n  is now automated, and now defined \"in code\", whether it's the Dockerfile\n  or the Ansible YAML.\n\nGetting our application code (or \"artifacts\") onto the server::\n  In our case, because we're using Docker,\n  the thing we needed to transfer was a Docker image.\n  Typically, you would do this by pushing and pulling from an image repository—although in our automation, we used a more direct process,\n  purely to avoid endorsing any particular vendor.\n\nSetting environment variables and secrets::\n  Depending on how you need to vary them,\n  you can set environment variables on your local PC,\n  in a Dockerfile, in your Ansible scripts, or on the server itself.\n  Figuring out which to use in which case is a big part of deployment.\n\nAttaching to the database:: In our case, we mount a file from the local filesystem.\n  More typically, you'd be supplying some environment variables and secrets to define\n  a host, port, username, and password to use for accessing a database server.\n\nConfiguring networking and port mapping:: This includes DNS config,\n  as well as Docker configuration. Web apps need to be able to talk to the outside world!\n\nRunning database migrations:: We'll revisit this later in the book,\n  but migrations are one of the most risky parts of a deployment,\n  and automating them is a key part of reducing that risk.\n\nGoing live with the new version of our application::\n  In our case, we stop the old container and start a new one.\n  In more advanced setups, you might be trying to achieve zero downtime deploys,\n  and looking into techniques like blue/green deployments,\n  but those are topics for different books.\n\nEvery single aspect of deployment can and probably should be automated.\nHere are a couple of general principles to think about\nwhen implementing IaC:\n\nIdempotence::\n  If your deployment script is deploying to existing servers,\n  you need to design them so that they work against a fresh installation _and_ against\n  a server that's already configured.\n  (((\"idempotence\")))\n\nDeclarative::\n  As much as possible, we want to try and specify _what_ we want the state to be on the server,\n  rather than _how_ we should get there.\n  This goes hand in hand with the idea of idempotence.(((\"deployment\", \"automating with Ansible\", startref=\"ix_dplyautAns\")))(((\"Ansible\", \"automated deployments with\", startref=\"ix_Ansautd\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_13_organising_test_files.asciidoc",
    "content": "[[chapter_13_organising_test_files]]\n== Splitting Our Tests into Multiple Files, [.keep-together]#and a Generic Wait Helper#\n\nBack to local development!\nThe next feature we might like to implement is a little input validation.\nBut as we start writing new tests, we'll notice that\nit's getting hard to find our way around a single _functional_tests.py_, and _tests.py_,\nso we'll reorganise them into multiple files--a little refactor of our tests, if you will.\n\nWe'll also build a generic explicit wait helper.\n\n\n\n=== Start on a Validation FT: Preventing Blank Items\n\n(((\"list items\", id=\"list12\")))\n(((\"user interactions\", \"preventing blank items\", id=\"UIblank12\")))\n(((\"blank items, preventing\", id=\"blank12\")))\n(((\"form data validation\", \"preventing blank items\", id=\"FDVblank12\")))\n(((\"validation\", see=\"form data validation; model-level validation\")))\n(((\"functional tests (FTs)\", \"for validation\", secondary-sortas=\"validation\", id=\"FTvalidat12\")))\nAs our first few users start using the site,\nwe've noticed they sometimes make mistakes that mess up their lists,\nlike accidentally submitting blank list items,\nor inputting two identical items to a list.\nComputers are meant to help stop us from making silly mistakes,\nso let's see if we can get our site to help.\n\n[role=\"pagebreak-before\"]\nHere's the outline of the new FT method, which we will add to\n`NewVisitorTestCase`:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/tests.py (ch13l001)\n====\n[source,python]\n----\ndef test_cannot_add_empty_list_items(self):\n    # Edith goes to the home page and accidentally tries to submit\n    # an empty list item. She hits Enter on the empty input box\n\n    # The home page refreshes, and there is an error message saying\n    # that list items cannot be blank\n\n    # She tries again with some text for the item, which now works\n\n    # Perversely, she now decides to submit a second blank list item\n\n    # She receives a similar warning on the list page\n\n    # And she can correct it by filling some text in\n    self.fail(\"write me!\")\n----\n====\n\n\nThat's all very well, but before we go any further--our\nfunctional tests (FTs) file is beginning to get a little crowded.\nLet's split it out into several files, in which each has a single test method.\n\n\nRemember that FTs are closely linked to \"user stories\" and features.\nOne way of organising your FTs might be to have one per high-level feature.\n\nWe'll also have one base test class, which they can all inherit from.  Here's\nhow to get there step by step.\n\n\n==== Skipping a Test\n\nNOTE: We're back to local development now.\n    Make sure that the `TEST_SERVER` environment variable is unset by executing\n    the command `unset TEST_SERVER` from the terminal.\n\n(((\"unittest module\", \"skip test decorator\")))\n(((\"refactoring\")))\n(((\"decorators\", \"skip test decorator\")))\nIt's always nice, when refactoring, to have a fully passing test suite.\nWe've just written a test with a deliberate failure.(((\"skip test decorator\")))\nLet's temporarily switch it off, using a decorator called \"skip\" from `unittest`:\n\n[role=\"sourcecode\"]\n.src/functional_tests/tests.py (ch13l001-1)\n====\n[source,python]\n----\nfrom unittest import skip\n[...]\n\n    @skip\n    def test_cannot_add_empty_list_items(self):\n----\n====\n\nThis tells the test runner to ignore this test.\nYou can see it works--if we rerun the tests,\nyou'll see it's a pass, but it explicitly mentions the skipped test:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests*\n[...]\nRan 4 tests in 11.577s\nOK (skipped=1)\n----\n\nWARNING: Skips are dangerous--you need to remember\n    to remove them before you commit your changes back to the repo.\n    This is why line-by-line reviews of each of your diffs are a good idea!\n\n\n.Don't Forget the \"Refactor\" in \"Red/Green/Refactor\"\n**********************************************************************\n\n(((\"Test-Driven Development (TDD)\", \"concepts\", \"Red/Green/Refactor\")))\n(((\"Red/Green/Refactor\")))\nA criticism that's sometimes levelled at TDD is that\nit leads to badly architected code,\nas the developer just focuses on getting tests to pass\nrather than stopping to think about how the whole system should be designed.\nI think it's slightly unfair.\n\n_TDD is no silver bullet_.\nYou still have to spend time thinking about good design.\nBut what often happens is that people forget the \"refactor\" in \"red/green/refactor\".(((\"refactoring\", \"red/green/refactor\")))\nThe methodology allows you to throw together any old code to get your tests to pass,\nbut it _also_ asks you to then spend some time refactoring it to improve its design.\nOtherwise, it's too easy to allow\nhttps://oreil.ly/57WKw[\"technical debt\"]\nto build up.\n\nOften, however, the best ideas for how to refactor code don't occur to you straight away.\nThey may occur to you days, weeks, even months after you wrote a piece of code,\nwhen you're working on something totally unrelated\nand you happen to see some old code again with fresh eyes.\nBut if you're halfway through something else,\nshould you stop to refactor the old code?\n\nThe answer is that it depends.\nIn the case at the beginning of the chapter,\nwe haven't even started writing our new code.\nWe know we are in a working state,\nso we can justify putting a skip on our new FT\n(to get back to fully passing tests)\nand do a bit of refactoring straight away.\n\nLater in the chapter, we'll spot other bits of code we want to alter.\nIn those cases, rather than taking the risk\nof refactoring an application that's not in a working state,\nwe'll make a note of the thing we want to change on our scratchpad\nand wait until we're back to a fully passing test suite before refactoring.\n\nKent Beck has a book-length exploration of the trade-offs\nof refactor-now versus refactor-later, called pass:[<a class=\"orm:hideurl\" href=\"https://www.oreilly.com/library/view/tidy-first/9781098151232\"><em>Tidy First?</em></a>].\n**********************************************************************\n\n\n\n=== Splitting Functional Tests Out into Many Files\n\n\n(((\"functional tests (FTs)\", \"splitting into many files\", id=\"FTsplit12\")))\n(((\"test files\", \"splitting FTs into many\", id=\"ix_tstfispl\")))\nWe start putting each test into its own class, still in the same file:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/tests.py (ch13l002)\n====\n[source,python]\n----\nclass FunctionalTest(StaticLiveServerTestCase):\n    def setUp(self):\n        [...]\n    def tearDown(self):\n        [...]\n    def wait_for_row_in_list_table(self, row_text):\n        [...]\n\n\nclass NewVisitorTest(FunctionalTest):\n    def test_can_start_a_todo_list(self):\n        [...]\n    def test_multiple_users_can_start_lists_at_different_urls(self):\n        [...]\n\n\nclass LayoutAndStylingTest(FunctionalTest):\n    def test_layout_and_styling(self):\n        [...]\n\n\nclass ItemValidationTest(FunctionalTest):\n    @skip\n    def test_cannot_add_empty_list_items(self):\n        [...]\n----\n====\n\n\nAt this point, we can rerun the FTs and see they all still work:\n\n----\nRan 4 tests in 11.577s\n\nOK (skipped=1)\n----\n\nThat's labouring it a little bit,\nand we could probably get away with doing this stuff in fewer steps,\nbut—as I keep saying—practising the step-by-step method on the easy cases\nmakes it that much easier when we have a complex case.\n\nNow we switch from a single tests file to using one for each class, and one\n\"base\" file to contain the base class that all the tests will inherit from.  We'll\nmake four copies of 'tests.py', naming them appropriately, and then delete the\nparts we don't need from each:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git mv src/functional_tests/tests.py src/functional_tests/base.py*\n$ *cp src/functional_tests/base.py src/functional_tests/test_simple_list_creation.py*\n$ *cp src/functional_tests/base.py src/functional_tests/test_layout_and_styling.py*\n$ *cp src/functional_tests/base.py src/functional_tests/test_list_item_validation.py*\n----\n\n_base.py_ can be cut down to just the `FunctionalTest` class.\nWe leave the helper method on the base class,\nbecause we suspect we're about to reuse it in our new FT:\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch13l003)\n====\n[source,python]\n----\nimport os\nimport time\n\nfrom django.contrib.staticfiles.testing import StaticLiveServerTestCase\nfrom selenium import webdriver\nfrom selenium.common.exceptions import WebDriverException\nfrom selenium.webdriver.common.by import By\n\nMAX_WAIT = 5\n\n\nclass FunctionalTest(StaticLiveServerTestCase):\n    def setUp(self):\n        [...]\n    def tearDown(self):\n        [...]\n    def wait_for_row_in_list_table(self, row_text):\n        [...]\n----\n====\n\nNOTE: Keeping helper methods in a base `FunctionalTest` class\n    is one useful way of preventing duplication in FTs.\n    Later in the book (in <<chapter_26_page_pattern>>), we'll use the \"page pattern\",\n    which is related, but prefers composition over inheritance--always a good thing.\n\nOur first FT is now in its own file,\nand should be just one class and one test method:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_simple_list_creation.py (ch13l004)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\nfrom .base import FunctionalTest\n\n\nclass NewVisitorTest(FunctionalTest):\n    def test_can_start_a_todo_list(self):\n        [...]\n    def test_multiple_users_can_start_lists_at_different_urls(self):\n        [...]\n----\n====\n\n\nI used a relative import (`from .base`).\nSome people like to use them a lot in Django code\n(e.g., your views might import models using `from .models import List`,\ninstead of `from list.models`).\nUltimately, this is a matter of personal preference.(((\"imports, relative, in Django\")))(((\"relative imports\")))\nI prefer to use relative imports only when I'm super, super confident\nthat the relative position of the thing I'm importing won't change.\nThat applies in this case because I know for sure that\nall the tests will sit next to _base.py_, which they inherit from.\n\n\n\nThe layout and styling FT should now be one file and one class:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_layout_and_styling.py (ch13l005)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\nfrom .base import FunctionalTest\n\n\nclass LayoutAndStylingTest(FunctionalTest):\n        [...]\n----\n====\n\n\nLastly, our new validation test is in a file of its own too:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch13l006)\n====\n[source,python]\n----\nfrom unittest import skip\n\nfrom selenium.webdriver.common.by import By  # <1>\nfrom selenium.webdriver.common.keys import Keys  # <1>\n\nfrom .base import FunctionalTest\n\n\nclass ItemValidationTest(FunctionalTest):\n    @skip\n    def test_cannot_add_empty_list_items(self):\n        [...]\n----\n====\n\n<1> These two will be marked as \"unused imports\" for now but\n    that's OK; we'll use them shortly.\n\nAnd we can test that everything worked\nby rerunning `manage.py test functional_tests`,\nand checking once again that all four tests are run:\n\n----\nRan 4 tests in 11.577s\n\nOK (skipped=1)\n----\n\n(((\"test files\", \"splitting FTs into many\", startref=\"ix_tstfispl\")))(((\"\", startref=\"FTsplit12\")))Now\nwe can remove our skip:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch13l007)\n====\n[source,python]\n----\nclass ItemValidationTest(FunctionalTest):\n    def test_cannot_add_empty_list_items(self):\n        [...]\n----\n====\n\n\n=== Running a Single Test File\n\n(((\"functional tests (FTs)\", \"running single test files\")))\n(((\"test files\", \"running single\")))\nAs a side bonus, we're now able to run an individual test file, like this:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests.test_list_item_validation*\n[...]\nAssertionError: write me!\n----\n\nBrilliant--no need to sit around waiting for all the FTs\nwhen we're only interested in a single one.\nAlthough, we need to remember to run all of them now and again to check for regressions.\nLater in the book, we'll set up a continuous integration (CI) server to run all the tests automatically—for example, every time we push to the main branch.\nFor now, a good prompt for running all the tests is \"just before you do a commit\",\nso let's get into that habit now:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n$ *git add src/functional_tests*\n$ *git commit -m \"Moved FTs into their own individual files\"*\n----\n\nGreat.  We've split our FTs nicely out into different files.\nNext, we'll start writing our FT. But before long, as you may be guessing,\nwe'll do something similar to our unit test files.\n(((\"\", startref=\"list12\")))\n(((\"\", startref=\"blank12\")))\n(((\"\", startref=\"UIblank12\")))\n(((\"\", startref=\"FDVblank12\")))\n(((\"\", startref=\"FTvalidat12\")))\n\n\n\n=== A New FT Tool: A Generic Explicit Wait Helper\n\n(((\"waits\", \"generic explicit wait helper\", id=\"ix_waithlp\")))(((\"implicit and explicit waits\")))\n(((\"explicit and implicit waits\")))\n(((\"functional tests (FTs)\", \"implicit/explicit waits and time.sleeps\")))\n(((\"generic explicit wait helper\", id=\"gewhelper12\")))\nFirst, let's start implementing the test—or at least the beginning of it:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch13l008)\n====\n[source,python]\n----\ndef test_cannot_add_empty_list_items(self):\n    # Edith goes to the home page and accidentally tries to submit\n    # an empty list item. She hits Enter on the empty input box\n    self.browser.get(self.live_server_url)\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n\n    # The home page refreshes, and there is an error message saying\n    # that list items cannot be blank\n    self.assertEqual(\n        self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,  #<1>\n        \"You can't have an empty list item\",  #<2>\n    )\n\n    # She tries again with some text for the item, which now works\n    self.fail(\"finish this test!\")\n    [...]\n----\n====\n\n[role=\"pagebreak-before\"]\nThis is how we might write the test naively:\n\n<1> We specify we're going to use a CSS class called `.invalid-feedback` to mark our\n    error text.  We'll see that Bootstrap has some useful styling for those.\n\n<2> And we can check that our error displays the message we want.\n\nBut can you guess what the potential problem is with the test as it's written\nnow?\n\nOK, I gave it away in the section header, but whenever we do something\nthat causes a page refresh, we need an explicit wait; otherwise, Selenium\nmight go looking for the `.invalid-feedback` element before the page has had a\nchance to load.\n\nTIP: Whenever you submit a form with `Keys.ENTER`\n    or click something that is going to cause a page to load,\n    you probably want an explicit wait for your next assertion.\n\n\nOur first explicit wait was built into a helper method.  For this one, we\nmight decide that building a specific helper method is overkill at this stage,\nbut it might be nice to have some generic way of saying in our tests, \"wait\nuntil this assertion passes\".  (((\"assertions\", \"wrapping in lambda function and passing to wait helper\")))(((\"lambda functions\", \"wrapping assertion in and passing to wait helper\")))Something like this:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch13l009)\n====\n[source,python]\n----\n[...]\n    # The home page refreshes, and there is an error message saying\n    # that list items cannot be blank\n    self.wait_for(\n        lambda: self.assertEqual(  #<1>\n            self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,\n            \"You can't have an empty list item\",\n        )\n    )\n----\n====\n\n<1> Rather than calling the assertion directly,\n    we wrap it in a `lambda` function,\n    and we pass it to a new helper method we imagine called `wait_for`.\n\nNOTE: If you've never seen `lambda` functions in Python before,\n    see <<lamdbafunct>>.\n\n[role=\"pagebreak-before\"]\nSo, how would this magical `wait_for` method work?\nLet's head over to _base.py_, make a copy of our existing `wait_for_row_in_list_table` method,\nand we'll adapt it slightly:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch13l010)\n====\n[source,python]\n----\n    def wait_for(self, fn):  #<1>\n        start_time = time.time()\n        while True:\n            try:\n                table = self.browser.find_element(By.ID, \"id_list_table\")  #<2>\n                rows = table.find_element(By.TAG_NAME, \"tr\")\n                self.assertIn(row_text, [row.text for row in rows])\n                return\n            except (AssertionError, WebDriverException):\n                if time.time() - start_time > MAX_WAIT:\n                    raise\n                time.sleep(0.5)\n----\n====\n\n<1> We make a copy of the method, but we name it `wait_for`,\n    and we change its argument.  It is expecting to be passed a function.\n\n<2> For now, we've still got the old code that's checking table rows.\n    Now, how do we transform this into something that works\n    for any generic `fn` that's been passed in?\n\nLike this:\n\n[[self.wait-for]]\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch13l011)\n====\n[source,python]\n----\n    def wait_for(self, fn):\n        start_time = time.time()\n        while True:\n            try:\n                return fn()  #<1>\n            except (AssertionError, WebDriverException):\n                if time.time() - start_time > MAX_WAIT:\n                    raise\n                time.sleep(0.5)\n----\n====\n\n<1> The body of our `try/except`,\n    instead of being the specific code for examining table rows,\n    just becomes a call to the function we passed in.\n    We also `return` its result,\n    to be able to exit the loop immediately if no exception is raised.\n\n[role=\"pagebreak-before less_space\"]\n[[lamdbafunct]]\n.Lambda Functions\n*******************************************************************************\n\n(((\"lambda functions\")))\n(((\"Python 3\", \"lambda functions\")))\n`lambda` in Python is the syntax for making a one-line, throwaway function. It\nsaves you from having to use `def...():` and an indented block:\n\n[role=\"skipme\"]\n[source,python]\n----\n>>> myfn = lambda x: x+1\n>>> myfn(2)\n3\n>>> myfn(5)\n6\n>>> adder = lambda x, y: x + y\n>>> adder(3, 2)\n5\n----\n\nIn our case, we're using it to transform a bit of code,\nthat would otherwise be executed immediately,\ninto a function that we can pass as an argument, and that can be executed later,\nand multiple times:\n\n[role=\"skipme\"]\n[source,python]\n----\n>>> def addthree(x):\n...     return x + 3\n...\n>>> addthree(2)\n5\n>>> myfn = lambda: addthree(2)  # note addthree isn't called immediately here\n>>> myfn\n<function <lambda> at 0x7f3b140339d8>\n>>> myfn()\n5\n>>> myfn()\n5\n----\n\n*******************************************************************************\n\n\nLet's see our funky `wait_for` helper in action:\n\n\n[subs=\"macros,verbatim\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\n\n======================================================================\nERROR: test_cannot_add_empty_list_items (functional_tests.test_list_item_valida\ntion.ItemValidationTest.test_cannot_add_empty_list_items)\n ---------------------------------------------------------------------\n[...]\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n16, in test_cannot_add_empty_list_items\n    self.wait_for(<1>\n  File \"...goat-book/src/functional_tests/base.py\", line 25, in wait_for\n    return fn()<2>\n           ^^^^\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n18, in <lambda><3>\n    self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,<3>\n    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n\n ---------------------------------------------------------------------\nRan 1 test in 10.575s\n\nFAILED (errors=1)\n----\n\nThe order of the traceback is a little confusing, but we can more or less follow\nthrough what happened:\n\n<1> In our FT, we call our `self.wait_for` helper, where we pass\n    the `lambda`-ified version of `assertEqual`.\n\n<2> We go into `self.wait_for` in _base.py_,\n    where we're calling (and returning) `fn()`, which refers to the passed\n    `lambda` function encapsulating our test assertion.\n\n<3> To explain where the exception has actually come from,\n    the traceback takes us back into _test_list_item_validation.py_\n    and inside the body of the `lambda` function,\n    and tells us that it was attempting to find the `.invalid-feedback` element\n    that failed.\n\n\n(((\"functional programming\")))\nWe're into the realm of functional programming now,\npassing functions as arguments to other functions,\nand it can be a little mind-bending.\nI know it took me a little while to get used to!\nHave a couple of read-throughs of this code,\nand the code back in the FT, to let it sink in;\nand if you're still confused, don't worry about it too much,\nand let your confidence grow from working with it.\nWe'll use it a few more times in this book\nand make it even more functionally fun; you'll see.\n(((\"\", startref=\"gewhelper12\")))\n\n\n\n=== Finishing Off the FT\n\nWe'll finish off the FT like this:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch13l012)\n====\n[source,python]\n----\n    # The home page refreshes, and there is an error message saying\n    # that list items cannot be blank\n    self.wait_for(\n        lambda: self.assertEqual(\n            self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,\n            \"You can't have an empty list item\",\n        )\n    )\n\n    # She tries again with some text for the item, which now works\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(\"Purchase milk\")\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Purchase milk\")\n\n    # Perversely, she now decides to submit a second blank list item\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n\n    # She receives a similar warning on the list page\n    self.wait_for(\n        lambda: self.assertEqual(\n            self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,\n            \"You can't have an empty list item\",\n        )\n    )\n\n    # And she can correct it by filling some text in\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(\"Make tea\")\n    self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"2: Make tea\")\n----\n====\n\n\n\n.Helper Methods in FTs\n*******************************************************************************\n\n(((\"functional tests (FTs)\", \"helper methods in\")))\n(((\"helper methods\")))\n(((\"self.wait_for helper method\")))\n(((\"wait_for_row_in_list_table helper method\")))\nWe've got two helper methods now: our generic `self.wait_for` helper, and `wait_for_row_in_list_table`.\nThe former is a general utility--any of our FTs might need to do a wait.\n\nThe latter also helps prevent duplication across your FT code.\nThe day we decide to change the implementation of how our list table works,\nwe want to make sure we only have to change our FT code in one place,\nnot in dozens of places across loads of FTs...(((\"waits\", \"generic explicit wait helper\", startref=\"ix_waithlp\")))\n\nSee also <<chapter_26_page_pattern>> and\nhttps://www.obeythetestinggoat.com/book/appendix_bdd.html[Online Appendix: BDD]\nfor more on structuring\nyour FT code.\n*******************************************************************************\n\n\nI'll let you do your own \"first-cut FT\" commit.\n\n\n=== Refactoring Unit Tests into Several Files\n\n\n(((\"unit tests\", \"refactoring into several files\")))\n(((\"refactoring\", \"of unit tests into several files\", secondary-sortas=\"unit\")))\n(((\"test files\", \"splitting unit tests into several\")))\nWhen we (finally!) start coding our solution,\nwe're going to want to add another test for our _models.py_.\nBefore we do so, it's time to tidy up our unit tests\nin a similar way to the functional tests.\n\nA difference will be that, because the `lists` app contains real application code\nas well as tests, we'll separate out the tests into their own folder:\n\n[subs=\"\"]\n----\n$ <strong>mkdir src/lists/tests</strong>\n$ <strong>touch src/lists/tests/__init__.py</strong>\n$ <strong>git mv src/lists/tests.py src/lists/tests/test_all.py</strong>\n$ <strong>git status</strong>\n$ <strong>git add src/lists/tests</strong>\n$ <strong>python src/manage.py test lists</strong>\n[...]\nRan 10 tests in 0.034s\n\nOK\n$ <strong>git commit -m \"Move unit tests into a folder with single file\"</strong>\n----\n\nIf you get(((\"dunderinit\")))(((\"&#x5f;&#x5f;init&#x5f;&#x5f;\", primary-sortas=\"init\"))) a message saying \"Ran 0 tests\",\nyou probably forgot to add the dunderinit.footnote:[\n\"Dunder\" is shorthand for double-underscore,\nso \"dunderinit\" means +++<i>__init__.py</i>+++.]\nIt needs to be there for the tests folder to be recognised as a regular Python package,footnote:[\nWithout the dunderinit, a folder with Python files in it is called a\nhttps://oreil.ly/V-w3A[namespace package].\nUsually, they are exactly the same as regular packages (which _do_ have a +++<i>__init__.py</i>+++),\nbut the Django test runner does not recognise them.]\nand thus discovered by the test runner.\n\nNow we turn _test_all.py_ into two files—one called _test_views.py_, which will only contain view tests,\nand one called _test_models.py_.\nI'll start by making two copies:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git mv src/lists/tests/test_all.py src/lists/tests/test_views.py*\n$ *cp src/lists/tests/test_views.py src/lists/tests/test_models.py*\n----\n\n[role=\"pagebreak-before\"]\nAnd strip _test_models.py_ down\nto being just the one test:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch13l016)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\nfrom lists.models import Item, List\n\n\nclass ListAndItemModelsTest(TestCase):\n        [...]\n----\n====\n\nWhereas _test_views.py_ just loses one class:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch13l017)\n====\n[source,diff]\n----\n--- a/src/lists/tests/test_views.py\n+++ b/src/lists/tests/test_views.py\n33 +74,3 @@ class NewItemTest(TestCase):\n         )\n\n         self.assertRedirects(response, f\"/lists/{correct_list.id}/\")\n-\n-\n-class ListAndItemModelsTest(TestCase):\n-    def test_saving_and_retrieving_items(self):\n[...]\n----\n====\n\nWe rerun the tests to check that everything is still there:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test lists*\n[...]\nRan 10 tests in 0.040s\n\nOK\n----\n\nGreat!   That's another small, working step:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/lists/tests*\n$ *git commit -m \"Split out unit tests into two files\"*\n----\n\n\nNOTE: Some people like to make their unit tests into a tests folder\n    straight away, as soon as they start a project. That's a perfectly good idea;\n    I just thought I'd wait until it became necessary,\n    to avoid doing too much housekeeping all in the first chapter!\n\n\nWell, that's our FTs and unit tests nicely reorganised.  In the next chapter,\nwe'll get down to some validation proper.\n\n[role=\"pagebreak-before less_space\"]\n.Tips on Organising Tests and Refactoring\n*******************************************************************************\n\nUse a tests folder::\n    Just as you use multiple files to hold your application code, you should\n    split your tests out into multiple files:\n    * For functional tests, group them into tests for a particular feature or\n      user story.\n    * For unit tests, use a folder called 'tests', with a +++<i>__init__.py</i>+++.\n    * You probably want a separate test file for each tested source code\n      file. For Django, that's typically 'test_models.py', 'test_views.py', and\n      'test_forms.py'.(((\"&#x5f;&#x5f;init&#x5f;&#x5f;\", primary-sortas=\"init\")))(((\"dunderinit\")))\n    * Have at least a placeholder test for 'every' function and class.\n    (((\"test files\", \"organizing and refactoring\")))\n\nDon't forget the \"refactor\" in \"red/green/refactor\"::\n    The whole point of having tests is to allow you to refactor your code!\n    Use them, and make your code (including your tests) as clean as you can.\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"Red/Green/Refactor\")))\n    (((\"Red/Green/Refactor\")))\n\nDon't refactor against failing tests::\n    * The general rule is that you shouldn't mix refactoring and behaviour\n      change. Having green tests is our best guarantee that we aren't changing\n      behaviour. If you start refactoring against failing tests, it becomes much\n      harder to spot when you're accidentally introducing a regression.\n    * This applies strongly to unit tests. With FTs, because we\n      often develop against red FTs anyway, it's sometimes more tempting to\n      refactor against failing tests. My suggestion is to avoid that temptation\n      and use an early return, so that it's 100% clear if, during a refactor,\n      you accidentally introduce a regression that's picked up in your FTs.\n    * You can occasionally put a skip on a test that is testing something you\n      haven't written yet.\n    * More commonly, make a note of the refactor you want to do, finish what\n      you're working on, and do the refactor a little later when you're back\n      to a working state.\n    * Don't forget to remove any skips before you commit your code! You should\n      always review your diffs line by line to catch things like this.\n      (((\"refactoring\")))\n\nTry a generic wait_for helper::\n    Having specific helper methods that do explicit waits is great, and it\n    helps to make your tests readable.  But you'll also often need an ad-hoc,\n    one-line assertion or Selenium interaction that you'll want to add a wait\n    to.  `self.wait_for` does the job well for me, but you might find a slightly\n    different pattern works for you.\n    (((\"generic explicit wait helper\")))\n    (((\"wait_for helper method\")))\n    (((\"self.wait_for helper method\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_14_database_layer_validation.asciidoc",
    "content": "[[chapter_14_database_layer_validation]]\n== Validation at the Database Layer\n\n(((\"user interactions\", \"validating inputs at database layer\", id=\"UIdblayer13\")))\n(((\"database testing\", \"database-layer validation\", id=\"DBTdblayer13\")))\nOver the next few chapters, we'll talk about testing\nand implementing validation of user inputs.(((\"validation\", \"database layer\", id=\"ix_valDB\")))\n\nIn terms of content, there's going to be quite a lot of material here\nthat's more about the specifics of Django, and less discussion of TDD philosophy.\nThat doesn't mean you won't be learning anything about testing--there are\nplenty of little testing tidbits in here, but perhaps it's more about\nreally getting into the swing of things, the rhythm of TDD, and how we get work done.\n\nOnce we get through these three short chapters,\nI've saved a bit of fun with JavaScript (!) for the end of <<part2>>.\nThen it's on to <<part3>>,\nwhere I promise we'll get right back into some of the real nitty-gritty discussions\non TDD methodology--unit tests versus integration tests, mocking, and more.\nStay tuned!\n\n[role=\"pagebreak-before\"]\nBut for now, a little validation.\nLet's just remind ourselves where our FT is pointing us:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python3 src/manage.py test functional_tests.test_list_item_validation*]\n[...]\n\n======================================================================\nERROR: test_cannot_add_empty_list_items (functional_tests.test_list_item_valida\ntion.ItemValidationTest.test_cannot_add_empty_list_items)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n16, in test_cannot_add_empty_list_items\n    self.wait_for(\n    ~~~~~~~~~~~~~^\n        lambda: self.assertEqual(\n        ^^^^^^^^^^^^^^^^^^^^^^^^^\n    ...<2 lines>...\n        )\n        ^\n    )\n    ^\n[...]\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n18, in <lambda>\n    self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,\n    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; For documentation [...]\n----\n\n\nIt's expecting to see an error message if the user tries to input an empty\nitem.\n\n\n=== Model-Layer Validation\n\n(((\"model-layer validation\", \"benefits and drawbacks of\")))\nIn a web app, there are two places you can do validation:\non the client side (using JavaScript or HTML5 properties, as we'll see later),\nand on the server side.\nThe server side is \"safer\" because someone can always bypass the client side,\nwhether it's maliciously or due to some bug.\n\nSimilarly on the server side, in Django, there are two levels at which you can\ndo validation.(((\"Django framework\", \"validation, layers of\"))) One is at the model level, and the other is higher up\nat the forms level.  I like to use the lower level whenever possible, partially\nbecause I'm a bit too fond of databases and database integrity rules, and\npartially because, again, it's safer--you can sometimes forget which form you\nuse to validate input, but you're always going to use the same database.\n\n\n[role=\"pagebreak-before less_space\"]\n==== The self.assertRaises Context Manager\n\n\n(((\"model-layer validation\", \"self.assertRaises context manager\")))\n(((\"self.assertRaises context manager\")))\nLet's go down and write a unit test at the models layer.\nAdd a new test method to [.keep-together]#+ListAndItemModelsTest+#, which tries to create a blank list item.\nThis test is interesting\nbecause it's testing that the code under test should raise an exception:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch14l001)\n====\n[source,python]\n----\nfrom django.db.utils import IntegrityError\n[...]\n\nclass ListAndItemModelsTest(TestCase):\n    def test_saving_and_retrieving_items(self):\n        [...]\n\n    def test_cannot_save_empty_list_items(self):\n        mylist = List.objects.create()\n        item = Item(list=mylist, text=\"\")\n        with self.assertRaises(IntegrityError):\n            item.save()\n----\n====\n\n\nThis is a new unit testing technique:\nwhen we want to check that doing something will raise an error,\nwe can use the `self.assertRaises` context manager.\n\nWe could have used something like this instead:\n\n[role=\"skipme\"]\n[source,python]\n----\ntry:\n    item.save()\n    self.fail('The save should have raised an exception')\nexcept IntegrityError:\n    pass\n----\n\nBut the `with` formulation is neater.\n\nTIP: If you're new to Python, you may never have seen the `with` statement.\n    It's the special keyword to use with what are called \"context managers\".\n    Together, they wrap a block of code,\n    usually with some kind of setup, cleanup, or error-handling code.\n    There's a good write-up on\n    https://oreil.ly/z6Eh8[Python Morsels].\n    (((\"with statements\")))\n    (((\"Python 3\", \"with statements\")))\n\n\n==== Django Model Constraints and Their Interaction with Databases\n\nWhen we run this new unit test, we see(((\"model-layer validation\", \"Django model constraints and database interactions\")))(((\"IntegrityErrors\")))(((\"Django framework\", \"model constraints and interaction with databases\"))) the failure we expected:\n\n----\n    with self.assertRaises(IntegrityError):\nAssertionError: IntegrityError not raised\n----\n\nBut all is not quite as it seems,\nbecause _this test should already pass_.\n\nIf you take a look at the\nhttps://docs.djangoproject.com/en/5.2/ref/models/fields/#blank[docs for the Django model fields],\nyou'll see under \"Field choices\" that the default setting for _all_ fields is\n`blank=False`.\nBecause \"text field\" is a type of field, it _should_ already disallow empty values.\n\n\n(((\"data integrity errors\")))\nSo, why is the test still failing?\nWhy is our database not raising an `IntegrityError` when we try to save an empty string\ninto the `text` column?\n\nThe answer is a combination of Django's design and the database we're using.\n\n\n==== Inspecting Our Constraints at the Database Level\n\nLet's have a look directly(((\"model-layer validation\", \"inspecting constraints at database layer\"))) at the database using the `dbshell` command:\n\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *./src/manage.py dbshell*  # (this is equivalent to running sqlite3 src/db.sqlite3)\nSQLite version 3.[...]\nEnter \".help\" for usage hints.\nsqlite> *.schema lists_item*\nCREATE TABLE IF NOT EXISTS \"lists_item\" (\"id\" integer NOT NULL PRIMARY KEY\nAUTOINCREMENT, \"text\" text NOT NULL, \"list_id\" bigint NOT NULL REFERENCES\n\"lists_list\" (\"id\") DEFERRABLE INITIALLY DEFERRED);\n----\n\nThe `text` column only has the `NOT NULL` constraint.\nThis means that the database would not allow `None` as a value,\nbut it will actually allow the empty string.\n\n\nWhilst it is\nhttps://oreil.ly/kzu65[technically possible]\nto implement a \"not empty string\" constraint on a text column in SQLite,\nthe Django developers have chosen not to do this. This is because Django distinguishes between what they call \"database-related\"\nand \"validation-related\" constraints.(((\"constraints\", \"database-related and validation-related\")))\nAs well as `empty=False`, all fields get a `null=False` setting,\nwhich translates into the database-level `NOT NULL` constraint we saw earlier.\n\nLet's see if we can verify that using our test, instead.\nWe'll pass in `text=None` instead of `text=\"\"`\n(and change the test name):\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch14l002)\n====\n[source,python]\n----\n    def test_cannot_save_null_list_items(self):\n        mylist = List.objects.create()\n        item = Item(list=mylist, text=None)\n        with self.assertRaises(IntegrityError):\n            item.save()\n----\n====\n\nYou'll see that _this_ test now passes:\n\n----\nRan 11 tests in 0.030s\n\nOK\n----\n\n\n==== Testing Django Model Validation\n\nThat's all vaguely interesting, but it's not actually what we set out to do.(((\"model-layer validation\", \"testing Django model validation\")))\nHow do we make sure that the \"validation-related\" constraint is being enforced?(((\"ValidationErrors\")))\nThe answer is that, while `IntegrityError` comes from the database,\nDjango uses `ValidationError` to signal errors that come from its own validation.\n\nLet's write a second test that checks on that:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch14l003)\n====\n[source,python]\n----\nfrom django.core.exceptions import ValidationError\nfrom django.db.utils import IntegrityError\n[...]\n\nclass ListAndItemModelsTest(TestCase):\n    def test_saving_and_retrieving_items(self):\n        [...]\n\n    def test_cannot_save_null_list_items(self):\n        mylist = List.objects.create()\n        item = Item(list=mylist, text=None)\n        with self.assertRaises(IntegrityError):\n            item.save()\n\n    def test_cannot_save_empty_list_items(self):\n        mylist = List.objects.create()\n        item = Item(list=mylist, text=\"\")  # <1>\n        with self.assertRaises(ValidationError):  # <2>\n            item.save()\n----\n====\n\n<1> This time we pass `text=\"\"`.\n<2> And we're expecting a `ValidationError` instead of an `IntegrityError`.\n\n\n\n==== A Django Quirk: Model Save Doesn't Run Validation\n\nWe can try running this new unit test,\nand we'll see its expected failure...\n\n----\n    with self.assertRaises(ValidationError):\nAssertionError: ValidationError not raised\n----\n\nWait a minute!  We expected this to _pass_ actually!\nWe just got through learning that Django should be enforcing the `blank=False`\nconstraint by default.(((\"Django framework\", \"models not running full validation on save\")))  Why doesn't this work?\n\n(((\"model-layer validation\", \"running full validation\")))\nWe've discovered one of Django's little quirks.\nFor\nhttps://oreil.ly/u3N_2[slightly\ncounterintuitive historical reasons],\nDjango models don't run full validation on save.\n\n(((\"full_clean method\")))\nDjango does have a method to manually run full validation, however,\ncalled `full_clean` (more info in\nhttps://docs.djangoproject.com/en/5.2/ref/models/instances/#django.db.models.Model.full_clean[the docs]).\nLet's swap that for the `.save()` and see if it works:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch14l004)\n====\n[source,python]\n----\n    with self.assertRaises(ValidationError):\n        item.full_clean()\n----\n====\n\n\nThat gets the unit test to pass:\n\n\n----\nRan 12 tests in 0.030s\n\nOK\n----\n\nGood. That taught us a little about Django validation,\nand the test is there to warn us if we ever forget our requirement\nand set `blank=True` on the `text` field (try it!).\n\n\n.Recap: Database-level and Model-level Validation in Django\n**********************************************************************\nDjango distinguishes two types of validation for models:\n\n1. Database-level constraints like `null=False` or `unique=True`\n  (as we'll see an example of in <<chapter_16_advanced_forms>>), which are enforced by the database itself,\n  using things like `NOT NULL` or `UNIQUE` constraints and bubble up as ++IntegrityError++s if you try to save an invalid object\n\n2. Model-level validations like `blank=False`,\n  which are only enforced by Django, when you call `full_clean()`,\n  and they raise a `ValidationError`\n\nThe subtlety is that Django also enforces database-level constraints\nwhen you call `full_clean()`.\nSo, you'll only see `IntegrityError` if you forget to call `full_clean()`\nbefore doing a `.save()`.\n\n**********************************************************************\n\nThe FTs are still failing,\nbecause we're not actually forcing these errors to appear in our actual app,\noutside of this one unit test.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Surfacing Model Validation Errors in the View\n\n(((\"model-layer validation\", \"surfacing errors in the view\", id=\"MLVsurfac13\")))\nLet's try to enforce our model validation in the views layer\nand bring it up into our templates so the user can see them.\nTo optionally display an error in our HTML, we check\nwhether the template has been passed an error variable\nand, if so, we do this:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch14l005)\n====\n[source,html]\n----\n  <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\">\n    <input\n      class=\"form-control form-control-lg {% if error %}is-invalid{% endif %}\" <1>\n      name=\"item_text\"\n      id=\"id_new_item\"\n      placeholder=\"Enter a to-do item\"\n    />\n    {% csrf_token %}\n    {% if error %}\n      <div class=\"invalid-feedback\">{{ error }}</div> <2>\n    {% endif %}\n  </form>\n----\n====\n\n<1> We add the `.is-invalid` class to any form inputs that have validation errors.\n<2> We use a `div.invalid-feedback` to display any error messages from the server.\n\n(((\"Bootstrap\", \"documentation\")))\n(((\"form control classes (Bootstrap)\")))\nTake a look at the https://getbootstrap.com/docs/5.3/forms/validation/#server-side[Bootstrap docs] for more\ninfo on form controls.\n\nTIP: However, ignore the Bootstrap docs' advice to prefer client-side\n    validation.(((\"client-side validation\")))(((\"server-side validation\")))\n    Ideally, having both server- and client-side validation is the best.\n    If you can't do both, then server-side validation is the one you really\n    can't do without.\n    Check the\n    https://oreil.ly/pkFo8[OWASP checklist],\n    if you are not convinced yet.\n    Client-side validation will provide faster feedback on the UI, but\n    https://oreil.ly/xAUt8[it is not a security measure.]\n    Server-side validation is indispensable for handling any input\n    that gets processed by the server--and it will also provide (albeit slower)\n    feedback for the client side.\n\n\nPassing this error to the template is the view function's job. Let's take\na look at the unit tests in the `NewListTest` class.  I'm going to use two\nslightly different error-handling patterns here.\n\n[role=\"pagebreak-before\"]\nIn the first case, our URL and view for new lists will optionally render the\nsame template as the home page, but with the addition of an error message.\nHere's a unit test for that:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l006)\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n\n    def test_validation_errors_are_sent_back_to_home_page_template(self):\n        response = self.client.post(\"/lists/new\", data={\"item_text\": \"\"})\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"home.html\")\n        expected_error = \"You can't have an empty list item\"\n        self.assertContains(response, expected_error)\n----\n====\n\nAs we're writing this test, we might get slightly offended by the '/lists/new'\nURL, which we're manually entering as a string. We've got a lot of URLs\nhardcoded in our tests, in our views, and in our templates, which violates the\nDRY (don't repeat yourself) principle.  I don't mind a bit of duplication in tests, but we should\ndefinitely be on the lookout for hardcoded URLs in our views and templates,\nand make a note to refactor them out.  But we won't do that straight away,\nbecause right now our application is in a broken state. We want to get back\nto a working state first.\n\nBack to our test, which is failing because the view is currently returning a\n302 redirect, rather than a \"normal\" 200 response:\n\n----\nAssertionError: 302 != 200\n----\n\nLet's try calling `full_clean()` in the view:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l007)\n====\n[source,python]\n----\ndef new_list(request):\n    nulist = List.objects.create()\n    item = Item.objects.create(text=request.POST[\"item_text\"], list=nulist)\n    item.full_clean()\n    return redirect(f\"/lists/{nulist.id}/\")\n----\n====\n//22\n\nAs we're looking at the view code, we find a good candidate for a hardcoded\nURL to get rid of.  Let's add that to our scratchpad:\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n*****\n\n\nNow the model validation raises an exception, which comes up through our view:\n\n----\n[...]\n  File \"...goat-book/src/lists/views.py\", line 13, in new_list\n    item.full_clean()\n[...]\ndjango.core.exceptions.ValidationError: {'text': ['This field cannot be\nblank.']}\n----\n\nSo we try our first approach:  using a `try/except` to detect errors. Obeying\nthe Testing Goat, we start with just the `try/except` and nothing else.  The\ntests should tell us what to code next.\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l010)\n====\n[source,python]\n----\nfrom django.core.exceptions import ValidationError\n[...]\n\ndef new_list(request):\n    nulist = List.objects.create()\n    item = Item.objects.create(text=request.POST[\"item_text\"], list=nulist)\n    try:\n        item.full_clean()\n    except ValidationError:\n        pass\n    return redirect(f\"/lists/{nulist.id}/\")\n----\n====\n\nThat gets us back to the `302 != 200`:\n\n----\nAssertionError: 302 != 200\n----\n\nLet's return a rendered template then, which should take care of the template\ncheck as well:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l011)\n====\n[source,python]\n----\n    except ValidationError:\n        return render(request, \"home.html\")\n----\n====\n\nAnd the tests now tell us to put the error message into the template:\n\n----\nAssertionError: False is not true : Couldn't find 'You can't have an empty list\nitem' in the following response\n----\n\n\nWe do that by passing a new template variable in:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l012)\n====\n[source,python]\n----\n    except ValidationError:\n        error = \"You can't have an empty list item\"\n        return render(request, \"home.html\", {\"error\": error})\n----\n====\n\n\nHmm, it looks like that didn't quite work:\n\n----\nAssertionError: False is not true : Couldn't find 'You can't have an empty list\nitem' in the following response\n----\n\n\n(((\"print\", \"debugging with\")))\n(((\"debugging\", \"print-based\")))\nA little print-based debug...\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l013)\n====\n[source,python]\n----\nexpected_error = \"You can't have an empty list item\"\nprint(response.content.decode())\nself.assertContains(response, expected_error)\n----\n====\n\n...will show us the cause—Django has\nhttps://docs.djangoproject.com/en/5.2/ref/templates/builtins/#autoescape[HTML-escaped]\nthe apostrophe:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\n              <div class=\"invalid-feedback\">You can&#x27;t have an empty list\nitem</div>\n----\n\nWe could hack something like this into our test:\n\n[role=\"skipme\"]\n[source,python]\n----\n    expected_error = \"You can&#39;t have an empty list item\"\n----\n\nBut using Django's helper function `html.escape()` is probably a better idea:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l014)\n====\n[source,python]\n----\nfrom django.utils import html\n[...]\n\n        expected_error = html.escape(\"You can't have an empty list item\")\n        self.assertContains(response, expected_error)\n----\n====\n\nThat passes!\n\n----\nRan 13 tests in 0.047s\n\nOK\n----\n\n\n==== Checking That Invalid Input Isn't Saved to the Database\n\n(((\"invalid input\", seealso=\"model-layer validation\")))\n(((\"database testing\", \"invalid input\")))\nBefore we go further though,\ndid you notice a little logic error we've allowed to creep into our implementation?\nWe're currently creating an object, even if validation fails:\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/views.py\n====\n[source,python]\n----\n    item = Item.objects.create(text=request.POST[\"item_text\"], list=nulist)\n    try:\n        item.full_clean()\n    except ValidationError:\n        [...]\n----\n====\n\nLet's add a new unit test\nto make sure that empty list items don't get saved:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l015)\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n\n    def test_validation_errors_are_sent_back_to_home_page_template(self):\n        [...]\n\n    def test_invalid_list_items_arent_saved(self):\n        self.client.post(\"/lists/new\", data={\"item_text\": \"\"})\n        self.assertEqual(List.objects.count(), 0)\n        self.assertEqual(Item.objects.count(), 0)\n----\n====\n\n// HARRY: consider assertEqual(Item.objects.all(), [])?  dave and csanners tend to agree.\n\nThat gives:\n\n\n----\n[...]\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_views.py\", line 43, in\ntest_invalid_list_items_arent_saved\n    self.assertEqual(List.objects.count(), 0)\nAssertionError: 1 != 0\n----\n\nWe fix it like this:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l016)\n====\n[source,python]\n----\ndef new_list(request):\n    nulist = List.objects.create()\n    item = Item(text=request.POST[\"item_text\"], list=nulist)\n    try:\n        item.full_clean()\n        item.save()\n    except ValidationError:\n        nulist.delete()\n        error = \"You can't have an empty list item\"\n        return render(request, \"home.html\", {\"error\": error})\n    return redirect(f\"/lists/{nulist.id}/\")\n----\n====\n\n\nDo the FTs pass?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\nFile \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n32, in test_cannot_add_empty_list_items\n    self.wait_for(\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n----\n\n[role=\"pagebreak-before\"]\nNot quite, but they did get a little further.\nChecking the line in which the error occurred (line 31 in my case) we\ncan see that we've got past the first part of the test,\nand are now onto the second check--that\nsubmitting a second empty item also shows an error.\n\n(((\"\", startref=\"MLVsurfac13\")))\nWe've got some working code though, so let's have a commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Adjust new list view to do model validation\"*\n----\n\n==== Adding an Early Return to Our FT to Let Us Refactor Against Green\n\n(((\"early return\")))(((\"refactoring\", \"early return in FT to refactor against green\")))\nLet's put an early return in the FT to separate\nwhat we got working from those that still need to be dealt with:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch14l017)\n====\n[source,python]\n----\nclass ItemValidationTest(FunctionalTest):\n    def test_cannot_add_empty_list_items(self):\n        [...]\n        self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(\"1: Purchase milk\")\n\n        return  # TODO re-enable the rest of this test.\n\n        # Perversely, she now decides to submit a second blank list item\n        self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n        [...]\n----\n====\n\n\nWe should also remind ourselves not to forget to remove this early return:\n\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n* 'Remove the early return from the FT.'\n*****\n\n\nAnd now, we can focus on making our code a little neater.\n\nTIP: When working on a new feature, it's common to realise partway through\n    that a refactor of the application is needed.\n    Adding an early return to the FT you're currently working on\n    enables you to perform this refactor against passing FTs,\n    even while the feature is still in progress.\n\n\n\n=== Django Pattern: Processing POST Requests in the Same View That Renders the Form\n\n\n(((\"model-layer validation\", \"POST requests processing\", id=\"MLVpost13\")))\n(((\"POST requests\", \"Django pattern for processing\", id=\"POSTdjango13\")))\n(((\"HTML\", \"POST requests\", \"Django pattern for processing\", id=\"HTMLpostdjango13\")))\nThis time we'll use a slightly different approach—one that's actually a very common pattern in Django—which uses the same view to both process POST requests\nand render the form that they come from.\nWhilst this doesn't fit the REST-ful URL model quite as well,\nit has the important advantage that the same URL can display a form,\nand display any errors encountered in processing the user's input;\nsee <<single-endpoint-for-forms>>.\n\n[[single-endpoint-for-forms]]\n.Existing list, viewing and adding items in the same end point\nimage::images/tdd3_1401.png[\"A diagram, showing the 3 different user requests for our end point at /lists/list-id/. (1) a GET request, which receives an HTML response containing the list items and the add-item Form. (2) a valid POST request, wich receives a 301 Redirect response to reload /lists/list-id/. (3) an invalid POST request, whose response is HTML including the list and the form, this time with errors\"]\n\nThe current situation is that we have one view and URL for displaying a list,\nand one view and URL for processing additions to that list.\nWe're going to combine them into one.\n\nNOTE: In this section, we're performing a refactor at the application level.\n    We execute our application-level refactor by changing or adding unit tests,\n    and then adjusting our code.\n    We use the functional tests to warn us if we ever go backwards and introduce a regression,\n    and when they're back to green we'll know our refactor is done.\n    Have another look at the diagram from the end of <<chapter_04_philosophy_and_refactoring>>\n    if you need to get your bearings.\n\n==== Refactor: Transferring the new_item Functionality into view_list\n\nLet's take the two old tests from `NewItemTest`—the ones that are about saving POST requests to existing lists—and move them into `ListViewTest`.\nAs we do so, we also make them point at the base list URL, instead of '.../add_item':\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l030)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n\n    def test_renders_input_form(self):\n        mylist = List.objects.create()\n        response = self.client.get(f\"/lists/{mylist.id}/\")\n        parsed = lxml.html.fromstring(response.content)\n        [form] = parsed.cssselect(\"form[method=POST]\")\n        self.assertEqual(form.get(\"action\"), f\"/lists/{mylist.id}/\")  # <1>\n        inputs = form.cssselect(\"input\")\n        self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])\n\n    def test_displays_only_items_for_that_list(self):\n        [...]\n\n    def test_can_save_a_POST_request_to_an_existing_list(self):\n        other_list = List.objects.create()\n        correct_list = List.objects.create()\n\n        self.client.post(\n            f\"/lists/{correct_list.id}/\",  # <2>\n            data={\"item_text\": \"A new item for an existing list\"},\n        )\n\n        self.assertEqual(Item.objects.count(), 1)\n        new_item = Item.objects.get()\n        self.assertEqual(new_item.text, \"A new item for an existing list\")\n        self.assertEqual(new_item.list, correct_list)\n\n    def test_POST_redirects_to_list_view(self):\n        other_list = List.objects.create()\n        correct_list = List.objects.create()\n\n        response = self.client.post(\n            f\"/lists/{correct_list.id}/\",  # <2>\n            data={\"item_text\": \"A new item for an existing list\"},\n        )\n\n        self.assertRedirects(response, f\"/lists/{correct_list.id}/\")\n----\n====\n\n<1> We want our form to point at the base URL.\n<2> And the two tests we've merged in need to target the base URL too.\n\nNote that the `NewItemTest` class disappears completely.\nI've also changed the name of the redirect test\nto make it explicit that it only applies to POST requests.\n\n[role=\"pagebreak-before\"]\nThat gives:\n\n----\nFAIL: test_POST_redirects_to_list_view\n(lists.tests.test_views.ListViewTest.test_POST_redirects_to_list_view)\n[...]\nAssertionError: 200 != 302 : Response didn't redirect as expected: Response\ncode was 200 (expected 302)\n[...]\nFAIL: test_can_save_a_POST_request_to_an_existing_list (lists.tests.test_views.\nListViewTest.test_can_save_a_POST_request_to_an_existing_list)\n[...]\nAssertionError: 0 != 1\n[...]\nFAIL: test_renders_input_form\n(lists.tests.test_views.ListViewTest.test_renders_input_form)\n[...]\nAssertionError: '/lists/1/add_item' != '/lists/1/'\n[...]\nRan 14 tests in 0.025s\n\nFAILED (failures=3)\n----\n\nThat last one is something we can fix in the template.\nLet's go to _list.html_, and change the `action` attribute on our form\nso that it points at the existing list URL:\n\n\n[role=\"sourcecode\"]\n.src/lists/templates/list.html (ch14l031)\n====\n[source,html]\n----\n{% block form_action %}/lists/{{ list.id }}/{% endblock %}\n----\n====\n\nIncidentally, that's another hardcoded URL.  Let's add it to our to-do list\nand, while we're thinking about it, there's one in _home.html_ too:\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n* 'Remove the early return from the FT.'\n* 'Remove hardcoded URL from forms in list.html and home.html.'\n*****\n\n////\nThis will immediately break our original functional test,\nbecause the `view_list` page doesn't know how to process POST requests yet:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers']\n----\n\nThe FTs are warning us that our attempted refactor has introduced a regression.\nLet's try and finish the refactor as soon as we can, and get back to green.\n\n\n////\n\n[role=\"pagebreak-before\"]\nWe're now down to two failing tests:\n\n----\nFAIL: test_POST_redirects_to_list_view\n(lists.tests.test_views.ListViewTest.test_POST_redirects_to_list_view)\n[...]\nAssertionError: 200 != 302 : Response didn't redirect as expected: Response\ncode was 200 (expected 302)\n[...]\nFAIL: test_can_save_a_POST_request_to_an_existing_list (lists.tests.test_views.\nListViewTest.test_can_save_a_POST_request_to_an_existing_list)\n[...]\nAssertionError: 0 != 1\n[...]\nRan 14 tests in 0.025s\n\nFAILED (failures=2)\n----\n\n\nThose are both about getting the list view to handle POST requests.\nLet's copy some code across from `add_item` view to do just that:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l032)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    if request.method == \"POST\":  # <1>\n        Item.objects.create(text=request.POST[\"item_text\"], list=our_list)  # <2>\n        return redirect(f\"/lists/{our_list.id}/\")  # <2>\n    return render(request, \"list.html\", {\"list\": our_list})\n----\n====\n\n<1> We add a branch for when the method is POST.\n\n<2> And we copy the `Item.objects.create()` and\n    `redirect()` lines from the `add_item` view.\n\n\nThat gets us passing unit tests!\n\n----\nRan 14 tests in 0.047s\n\nOK\n----\n\nNow we can delete the `add_item` view, as it's no longer needed...oops, an\nunexpected failure:\n\n[role=\"dofirst-ch14l033\"]\n----\n[...]\nAttributeError: module 'lists.views' has no attribute 'add_item'\n----\n\n[role=\"pagebreak-before\"]\nIt's because we've deleted the view, but it's still being referred to in\n_urls.py_.  We remove it from there:\n\n[role=\"sourcecode\"]\n.src/lists/urls.py (ch14l034)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"new\", views.new_list, name=\"new_list\"),\n    path(\"<int:list_id>/\", views.view_list, name=\"view_list\"),\n]\n----\n====\n\n\nOK, we're back to the green on the unit tests.\n\n----\nOK\n----\n\n\nLet's try a full FT run: they're all passing!\n\n----\nRan 4 tests in 9.951s\n\nOK\n----\n\n\nOur refactor of the `add_item` functionality is complete.\nWe should commit there:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Refactor list view to handle new item POSTs\"*\n----\n\n\nWe can remove the(((\"early return\"))) early return now:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch14l035)\n====\n[source,diff]\n----\n@@ -24,8 +24,6 @@ class ItemValidationTest(FunctionalTest):\n         self.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\n         self.wait_for_row_in_list_table(\"1: Purchase milk\")\n\n-        return  # TODO re-enable the rest of this test.\n-\n         # Perversely, she now decides to submit a second blank list item\n----\n====\n\nAnd, let's cross that off our scratchpad too:\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n* '[strikethrough line-through]#Remove the early return from the FT.#'\n* 'Remove hardcoded URL from forms in list.html and home.html.'\n*****\n\n[role=\"pagebreak-before\"]\nRun the FTs again to see what's still there that needs to be fixed:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests*\n[...]\nERROR: test_cannot_add_empty_list_items (functional_tests.test_list_item_valida\ntion.ItemValidationTest.test_cannot_add_empty_list_items)\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n\nRan 4 tests in 15.276s\nFAILED (errors=1)\n----\n\nWe're back to working on this one failure in our new FT.\n\n\n==== Enforcing Model Validation in view_list\n\nWe still want the addition of items to existing lists to be subject to our model validation rules.\nLet's write a new unit test for that;\nit's very similar to the one for the home page, with just a couple of tweaks:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch14l036)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    [...]\n\n    def test_validation_errors_end_up_on_lists_page(self):\n        list_ = List.objects.create()\n        response = self.client.post(\n            f\"/lists/{list_.id}/\",\n            data={\"item_text\": \"\"},\n        )\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"list.html\")\n        expected_error = html.escape(\"You can't have an empty list item\")\n        self.assertContains(response, expected_error)\n----\n====\n\nBecause our view currently does not do any validation, this should fail and\njust redirect for all POSTs:\n\n\n----\n    self.assertEqual(response.status_code, 200)\nAssertionError: 302 != 200\n----\n\n[role=\"pagebreak-before\"]\nHere's an implementation:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l037)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    error = None\n\n    if request.method == \"POST\":\n        try:\n            item = Item(text=request.POST[\"item_text\"], list=our_list)  # <1>\n            item.full_clean()  # <2>\n            item.save()  # <2>\n            return redirect(f\"/lists/{our_list.id}/\")\n        except ValidationError:\n            error = \"You can't have an empty list item\"\n\n    return render(request, \"list.html\", {\"list\": our_list, \"error\": error})\n----\n====\n\n<1> Notice we do `Item()` instead of `Item.objects.create()`.\n<2> Then we call `full_clean()` before we call `save()`.\n\n\nIt works:\n\n----\nRan 15 tests in 0.047s\n\nOK\n----\n\nBut it's not deeply satisfying, is it?\nThere's definitely some duplication of code here;\nthat `try/except` occurs twice in _views.py_,\nand in general things are feeling clunky.\n\nLet's wait a bit before we do more refactoring though,\nbecause we know we're about to do\nsome slightly different validation coding for duplicate items.\nWe'll just add it to our scratchpad for now:\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n* '[strikethrough line-through]#Remove the early return from the FT.#'\n* 'Remove hardcoded URL from forms in list.html and home.html.'\n* 'Remove duplication of validation logic in views.'\n*****\n\n\nNOTE: One of the reasons that the \"three strikes and refactor\" rule exists is that,\n    if you wait until you have three use cases, each might be slightly different,\n    and it gives you a better view for what the common functionality is.\n    If you refactor too early,\n    you may find that the third use case doesn't quite fit with your refactored code.\n    (((\"database testing\", \"three strikes and refactor rule\")))\n    (((\"Test-Driven Development (TDD)\", \"concepts\", \"three strikes and refactor\")))(((\"refactoring\", \"&quot;three strikes and refactor&quot; rule\", secondary-sortas=\"three\")))\n    (((\"three strikes and refactor rule\")))\n\n\nAt least our FTs are back to passing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests*\n[...]\nOK\n----\n\nWe're back to a working state,\nso we can take a look at some of the items on our scratchpad.\nThis would be a good time for a commit (and possibly a tea break):\n(((\"\", startref=\"MLVpost13\")))(((\"\", startref=\"HTMLpostdjango13\")))(((\"\", startref=\"POSTdjango13\")))\n\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"enforce model validation in list view\"*\n----\n\n\n=== Refactor: Removing Hardcoded URLs\n\n\n(((\"{% url %}\")))\n(((\"templates\", \"tags\", \"{% url %}\")))\n(((\"model-layer validation\", \"removing hardcoded URLs\", id=\"MLVhard13\")))\n(((\"URL mappings\", id=\"url13\")))\nDo you remember those `name=` parameters in _urls.py_?\nWe just copied them across from the default example that Django gave us,\nand I've been giving them some reasonably descriptive names.\nNow we find out what they're for:\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/urls.py\n====\n[source,python]\n----\n    path(\"new\", views.new_list, name=\"new_list\"),\n    path(\"<int:list_id>/\", views.view_list, name=\"view_list\"),\n----\n====\n\n\n==== The {% url %} Template Tag\n\nWe can replace the hardcoded URL in _home.html_ with a Django template tag\nthat refers to the URL's \"name\":\n\n[role=\"sourcecode\"]\n.src/lists/templates/home.html (ch14l038)\n====\n[source,html]\n----\n{% block form_action %}{% url 'new_list' %}{% endblock %}\n----\n====\n\nWe check that this doesn't break the unit tests:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\nOK\n----\n\nLet's do the other template.  This one is more interesting, because we pass it\na [keep-together]#parameter#:\n\n\n[role=\"sourcecode\"]\n.src/lists/templates/list.html (ch14l039)\n====\n[source,html]\n----\n{% block form_action %}{% url 'view_list' list.id %}{% endblock %}\n----\n====\n\nSee the\nhttps://docs.djangoproject.com/en/5.2/topics/http/urls/#reverse-resolution-of-urls[Django\ndocs on reverse URL resolution] for more info. We run the tests again, and check that they all pass:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\nOK\n$ pass:quotes[*python src/manage.py test functional_tests*]\nOK\n----\n\nExcellent! Let's commit our progress:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Refactor hard-coded URLs out of templates\"*\n----\n\nAnd don't forget to cross off the \"Remove hardcoded URL...\" task as well:\n\n[role=\"scratchpad\"]\n*****\n* 'Remove hardcoded URLs from views.py.'\n* '[strikethrough line-through]#Remove the early return from the FT.#'\n* '[strikethrough line-through]#Remove hardcoded URL from forms in list.html and home.html.#'\n* 'Remove duplication of validation logic in views.'\n*****\n\n\n==== Using get_absolute_url for Redirects\n\n(((\"get_absolute_url\")))\nNow let's tackle _views.py_.\nOne way of doing it is just like in the template,\npassing in the name of the URL and a positional argument:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l040)\n====\n[source,python]\n----\ndef new_list(request):\n    [...]\n    return redirect(\"view_list\", nulist.id)\n----\n====\n\nThat would get the unit and functional tests passing, but the `redirect`\nfunction can do even better magic than that!  In Django, because model objects\nare often associated with a particular URL, you can define a special function\ncalled `get_absolute_url` which tells you what page displays the item.  It's useful\nin this case, but it's also useful in the Django admin (which I don't cover in\nthe book, but you'll soon discover for yourself) because it will let you jump from\nlooking at an object in the admin view to looking at the object on the live\nsite. I'd always recommend defining a `get_absolute_url` for a model whenever\nthere is one that makes sense; it takes no time at all.\n\nAll it takes is a super simple unit test in 'test_models.py':\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch14l041)\n====\n[source,python]\n----\n    def test_get_absolute_url(self):\n        mylist = List.objects.create()\n        self.assertEqual(mylist.get_absolute_url(), f\"/lists/{mylist.id}/\")\n----\n====\n\nThat gives:\n\n----\nAttributeError: 'List' object has no attribute 'get_absolute_url'\n----\n\nThe implementation is to use Django's `reverse` function, which\nessentially does the reverse of what Django normally does with _urls.py_:\n\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch14l042)\n====\n[source,python]\n----\nfrom django.urls import reverse\n\n\nclass List(models.Model):\n    def get_absolute_url(self):\n        return reverse(\"view_list\", args=[self.id])\n----\n====\n\nAnd now we can use it in the view--the `redirect` function just takes the\nobject we want to redirect to, and it uses `get_absolute_url` under the\nhood automagically!\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l043)\n====\n[source,python]\n----\ndef new_list(request):\n    [...]\n    return redirect(nulist)\n----\n====\n\nThere's more info in the\nhttps://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#redirect[Django docs].\nQuick check that the unit tests still pass:\n\n[subs=\"specialcharacters,macros\"]\n----\nOK\n----\n\nThen we do the same to `view_list`:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch14l044)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    [...]\n\n            item.save()\n            return redirect(our_list)\n        except ValidationError:\n            error = \"You can't have an empty list item\"\n----\n====\n\nAnd a full unit test and FT run\nto assure ourselves that everything still works:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\nOK\n$ pass:quotes[*python src/manage.py test functional_tests*]\nOK\n----\n\nTime to cross off our to-dos...\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Remove hardcoded URLs from views.py.#'\n* '[strikethrough line-through]#Remove the early return from the FT.#'\n* '[strikethrough line-through]#Remove hardcoded URL from forms in list.html and home.html.#'\n* 'Remove duplication of validation logic in views.'\n*****\n\nAnd commit...\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"Use get_absolute_url on List model to DRY urls in views\"*\n----\n\nAnd we're done with that bit!\nWe have working model-layer validation,\nand we've taken the opportunity to do a few refactors along the way.\n(((\"\", startref=\"MLVhard13\")))(((\"\", startref=\"url13\")))\n\n\nThat final scratchpad item will be the subject of the next chapter.\n\n[role=\"pagebreak-before less_space\"]\n.On Database-layer Validation\n*******************************************************************************\n\nAs we saw, the specific \"not empty\" constraint we're trying to apply here\nisn't enforceable by SQLite, and so it was actually Django that ended up enforcing it for us. However,\nI always like to push my validation logic down as low as possible:\n(((\"model-layer validation\", \"benefits and drawbacks of\")))\n\n\nValidation at the database layer is the ultimate guarantee of data integrity::\n    It can ensure that, no matter how complex your code gets at the layers\n    above, you have guarantees at the lowest level that your data is\n    valid and consistent.\n    (((\"data integrity errors\")))\n\nBut it comes at the expense of flexibility::\n    This benefit doesn't come for free! It's now impossible, even temporarily,\n    to have inconsistent data.  Sometimes you might have a good reason for temporarily\n    storing data that breaks the rules rather than storing nothing at all.  Perhaps\n    you're importing data from an external source in several stages, for\n    example.\n\nAnd it's not designed for user-friendliness::\n    Trying to store invalid data will cause a nasty `IntegrityError` to come\n    back from your database, and possibly the user will see a confusing 500\n    error page.\n    As we'll see in later chapters, forms-layer validation is designed with the\n    user in mind, anticipating the kinds of helpful error messages we want to\n    send them.(((\"validation\", \"database layer\", startref=\"ix_valDB\")))\n    (((\"\", startref=\"UIdblayer13\")))(((\"\", startref=\"DBTdblayer13\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_15_simple_form.asciidoc",
    "content": "[[chapter_15_simple_form]]\n== A Simple Form\n\nAt the end of the last chapter,\nwe were left with the thought that there was too much duplication\nin the validation handling bits of our views.\nDjango encourages you to use form classes to do the work of validating user input,\nand choosing what error messages to display.\n\nWe'll use tests to explore the way Django forms work,\nand then we'll refactor our views to use them.\nAs we go along, we'll see our unit tests and functional tests,\nin combination, will protect us from regressions.\n\n\n\n=== Moving Validation Logic Into a Form\n\nTIP: In Django, a complex view is a code smell.\n    Could some of that logic be pushed out to a form?\n    Or to some custom methods on the model class?\n    Or (perhaps best of all) to a non-Django module that represents your business logic?\n\n\n(((\"form data validation\", \"benefits of\")))\n(((\"form data validation\", \"moving validation logic to forms\", id=\"FDVmoving14\")))\n(((\"user interactions\", \"form data validation\", id=\"UIform14\")))\nForms have several superpowers in Django:\n\n* They can process user input and validate it for errors.\n\n* They can be used in templates to render HTML input elements, and error\n  messages too.\n\n* And, as we'll see later, some of them can even save data to the database\n  for you.\n\nYou don't have to use all three superpowers in every form.  You may prefer\nto roll your own HTML or do your own saving. But they are an excellent place\nto keep validation logic.\n\n\n==== Exploring the Forms API with a Unit Test\n\n\n(((\"Forms API\", seealso=\"form data validation\")))(((\"unit tests\", \"Forms API\")))Let's\ndo a little experimenting with forms by using a unit test.  My plan is to\niterate towards a complete solution, and hopefully introduce forms gradually\nenough that they'll make sense if you've never seen them before.\n\nFirst we add a new file for our form unit tests, and we start with a test that\njust looks at the form HTML:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l001)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\nfrom lists.forms import ItemForm\n\n\nclass ItemFormTest(TestCase):\n    def test_form_renders_item_text_input(self):\n        form = ItemForm()\n        self.fail(form.as_p())\n----\n====\n\n`form.as_p()` renders the form as HTML.  This unit test uses a `self.fail`\nfor some exploratory coding.  You could just as easily use a `manage.py shell`\nsession, although you'd need to keep reloading your code for each change.\n\nLet's make a minimal form.  It inherits from the base `Form` class, and has\na single field called `item_text`:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l002)\n====\n[source,python]\n----\nfrom django import forms\n\n\nclass ItemForm(forms.Form):\n    item_text = forms.CharField()\n----\n====\n\nWe now see a failure message that tells us what the autogenerated form\nHTML will look like:\n\n----\nAssertionError: <p>\n    <label for=\"id_item_text\">Item text:</label>\n    <input type=\"text\" name=\"item_text\" required id=\"id_item_text\">\n[...]\n  </p>\n----\n\n[role=\"pagebreak-before\"]\nIt's already pretty close to what we have in _base.html_.  We're missing\nthe placeholder attribute and the Bootstrap CSS classes.  Let's make our\nunit test into a test for that:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l003)\n====\n[source,python]\n----\nclass ItemFormTest(TestCase):\n    def test_form_item_input_has_placeholder_and_css_classes(self):\n        form = ItemForm()\n\n        rendered = form.as_p()\n\n        self.assertIn('placeholder=\"Enter a to-do item\"', rendered)\n        self.assertIn('class=\"form-control form-control-lg\"', rendered)\n----\n====\n\n\nThat gives us a fail, which justifies some real coding:\n\n[subs=\"specialcharacters\"]\n----\n    self.assertIn('placeholder=\"Enter a to-do item\"', rendered)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'placeholder=\"Enter a to-do item\"' not found in [...]\n----\n\nHow can we customise the input for a form field? We use a \"widget\".\nHere it is with just the placeholder:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l004)\n====\n[source,python]\n----\nclass ItemForm(forms.Form):\n    item_text = forms.CharField(\n        widget=forms.widgets.TextInput(\n            attrs={\n                \"placeholder\": \"Enter a to-do item\",\n            }\n        ),\n    )\n----\n====\n\nThat gives:\n\n----\nAssertionError: 'class=\"form-control form-control-lg\"' not found in '<p>\\n\n<label for=\"id_item_text\">Item text:</label>\\n    <input type=\"text\"\nname=\"item_text\" placeholder=\"Enter a to-do item\" required id=\"id_item_text\">\\n\n\\n    \\n      \\n    \\n  </p>'\n----\n\nAnd then:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l005)\n====\n[source,python]\n----\n        widget=forms.widgets.TextInput(\n            attrs={\n                \"placeholder\": \"Enter a to-do item\",\n                \"class\": \"form-control form-control-lg\",\n            }\n        ),\n----\n====\n\nNOTE: Doing this sort of `widget` customisation would get tedious\n    if we had a much larger, more complex form.\n    Check out\n    https://django-crispy-forms.readthedocs.org[django-crispy-forms]\n    for some help.\n    (((\"django-crispy-forms\")))\n\n\n\n.Development-Driven Tests: Using Unit Tests for Exploratory Coding\n*******************************************************************************\n\n(((\"development-driven tests\")))(((\"unit tests\", \"using for exploratory coding\")))\n(((\"exploratory coding\")))\nDoes this feel a bit like development-driven tests?\nThat's OK, now and again.\n\nWhen you're exploring a new API,\nyou're absolutely allowed to mess about with it for a while\nbefore you get back to rigorous TDD.\nYou might use the interactive console, or write some exploratory code\n(but you have to promise the Testing Goat that you'll throw it away\nand rewrite it properly later).\n\nHere, we're actually using a unit test as a way of experimenting with the forms API.\nIt can be a surprisingly good way of learning how something works.\n\n\n*******************************************************************************\n\n// SEBASTIAN: Small suggestion - I'd appreciate mentioning breakpoint() for use in test\n//      to be able to play with a form instance even more\n\n\n==== Switching to a Django ModelForm\n\n(((\"ModelForms\")))\nWhat's next?\nWe want our form to reuse the validation code that we've already defined on our model.\nDjango provides a special class that can autogenerate a form for a model, called `ModelForm`.\nAs you'll see, it's configured using a special inner class called `Meta`:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l006)\n====\n[source,python]\n----\nfrom django import forms\n\nfrom lists.models import Item\n\n\nclass ItemForm(forms.models.ModelForm):\n    class Meta:  # <1>\n        model = Item\n        fields = (\"text\",)\n\n    # item_text = forms.CharField(  #<2>\n    #     widget=forms.widgets.TextInput(\n    #         attrs={\n    #             \"placeholder\": \"Enter a to-do item\",\n    #             \"class\": \"form-control form-control-lg\",\n    #         }\n    #     ),\n    # )\n----\n====\n\n<1> In `Meta`, we specify which model the form is for\n    and which fields we want it to use.\n\n<2> We'll comment out our manually created field for now.\n\n\n++ModelForm++ does all sorts of smart stuff,\nlike assigning sensible HTML form input types to different types of field,\nand applying default validation.\nCheck out the\nhttps://docs.djangoproject.com/en/5.2/topics/forms/modelforms[docs]\nfor more info.\n\nWe now have some different-looking form HTML:\n\n----\nAssertionError: 'placeholder=\"Enter a to-do item\"' not found in '<p>\\n\n<label for=\"id_text\">Text:</label>\\n    <textarea name=\"text\" cols=\"40\"\nrows=\"10\" required id=\"id_text\">\\n</textarea>\\n    \\n    \\n      \\n    \\n\n</p>'\n----\n\n\nIt's lost our placeholder and CSS class.\nAnd you can also see that it's using\n`name=\"text\"` instead of `name=\"item_text\"`.\nWe can probably live with that.\nBut it's using a `textarea` instead of a normal input,\nand that's not the UI we want for our app.\nThankfully, you can override `widgets` for `ModelForm` fields,\nsimilarly to the way we did it with the normal form:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l007)\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    class Meta:\n        model = Item\n        fields = (\"text\",)\n        widgets = {  # <1>\n            \"text\": forms.widgets.TextInput(\n                attrs={\n                    \"placeholder\": \"Enter a to-do item\",\n                    \"class\": \"form-control form-control-lg\",\n                }\n            ),\n        }\n----\n====\n\n<1> We restore some of our commented-out code here,\n    but modified slightly, from being an attribute declaration\n    to a key in a dict.\n\nThat gets the test passing.\n\n[role=\"pagebreak-before less_space\"]\n==== Testing and Customising Form Validation\n\nNow let's see if the `ModelForm` has picked up the same validation rules\nthat we defined on the model.(((\"form data validation\", \"testing and customizing validation\")))\nWe'll also learn how to pass data into the form, as if it came from the user:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l008)\n====\n[source,python]\n----\n    def test_form_item_input_has_placeholder_and_css_classes(self):\n        [...]\n\n    def test_form_validation_for_blank_items(self):\n        form = ItemForm(data={\"text\": \"\"})\n        form.save()\n----\n====\n\nThat gives us:\n\n----\nValueError: The Item could not be created because the data didn't validate.\n----\n\nGood: the form won't allow you to save if you give it an empty item text. Now let's see if we can get it to use the specific error message that we\nwant.  The API for checking form validation 'before' we try to save any\ndata is a function called `is_valid`:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l009)\n====\n[source,python]\n----\ndef test_form_item_input_has_placeholder_and_css_classes(self):\n    [...]\n\ndef test_form_validation_for_blank_items(self):\n    [...]\n\ndef test_form_validation_for_blank_items(self):\n    form = ItemForm(data={\"text\": \"\"})\n    self.assertFalse(form.is_valid())\n    self.assertEqual(form.errors[\"text\"], [\"You can't have an empty list item\"])\n----\n====\n\nCalling `form.is_valid()` returns `True` or `False`,\nbut it also has the side effect of validating the input data\nand populating the `errors` attribute.\nIt's a dictionary mapping the names of fields to lists of errors for those fields\n(it's possible for a field to have more than one error).\n\nThat gives us:\n\n----\nAssertionError: ['This field is required.'] != [\"You can't have an empty list\nitem\"]\n----\n\n[role=\"pagebreak-before\"]\nDjango already has a default error message\nthat we could present to the user--you might use it\nif you were in a hurry to build your web app,\nbut we care enough to make our message special.\nCustomising it means changing `error_messages`—another `Meta` variable:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/forms.py (ch15l010)\n====\n[source,python]\n----\n    class Meta:\n        model = Item\n        fields = (\"text\",)\n        widgets = {\n            \"text\": forms.widgets.TextInput(\n                attrs={\n                    \"placeholder\": \"Enter a to-do item\",\n                    \"class\": \"form-control form-control-lg\",\n                }\n            ),\n        }\n        error_messages = {\"text\": {\"required\": \"You can't have an empty list item\"}}\n----\n====\n\n----\nOK\n----\n\nYou know what would be even better than messing about with all these error strings?\nHaving a constant:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l011)\n====\n[source,python]\n----\nEMPTY_ITEM_ERROR = \"You can't have an empty list item\"\n[...]\n        error_messages = {\"text\": {\"required\": EMPTY_ITEM_ERROR}}\n----\n====\n\nRerun the tests to see that they pass...OK.\nNow we can change the tests too.\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l012)\n====\n[source,python]\n----\nfrom lists.forms import EMPTY_ITEM_ERROR, ItemForm\n[...]\n\n    def test_form_validation_for_blank_items(self):\n        form = ItemForm(data={\"text\": \"\"})\n        self.assertFalse(form.is_valid())\n        self.assertEqual(form.errors[\"text\"], [EMPTY_ITEM_ERROR])\n----\n====\n\nTIP: This is a good example of reusing constants in tests.\n    It makes it easier to change the error message later.\n\n\nAnd the tests still pass:\n\n\n----\nOK\n----\n\n(((\"\", startref=\"FDVmoving14\")))Great.  Totes committable:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status* # should show forms.py and test_forms.py\n$ *git add src/lists*\n$ *git commit -m \"new form for list items\"*\n----\n\n\n=== Attempting to Use the Form in Our Views\n\n(((\"form data validation\", \"using forms in views\", id=\"FDVviews14\")))\nAt this point, we may be tempted to carry on—perhaps extend the form to capture uniqueness validation\nand empty-item validation.\n\nBut there's a sort of corollary to the \"deploy as early as possible\" Lean methodology,\nwhich is \"merge code as early as possible\".\nIn other words: while building this bit of forms code,\nit would be easy to go on for ages,\nadding more and more functionality to the form--I should know,\nbecause that's exactly what I did during the drafting of this chapter,\nand I ended up doing all sorts of work\nmaking an all-singing, all-dancing form class\nbefore I realised it wouldn't _actually_ work for our most basic use case.\n\nSo, instead, try to use your new bit of code as soon as possible.\nThis makes sure you never have unused bits of code lying around,\nand that you start checking your code against \"the real world\" as soon as possible.\n\nWe have a form class that can render some HTML\nand do validation of at least one kind of error--let's start using it!\nWe should be able to use it in our _base.html_ template—so, also, in all of our views.\n\n\n==== Using the Form in a View with a GET Request\n\n\n(((\"GET requests\")))\n(((\"HTML\", \"GET requests\")))\n\nSo, let's start using our form in our home page view:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l013)\n====\n[source,python]\n----\n[...]\nfrom lists.forms import ItemForm\nfrom lists.models import Item, List\n\n\ndef home_page(request):\n    return render(request, \"home.html\", {\"form\": ItemForm()})\n----\n====\n\nOK, now let's try using it in the template--we\nreplace the old `<input ..>` with `{{ form.text }}`:\n\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch15l014)\n====\n[source,html]\n----\n  <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n    {{ form.text }}  <1>\n    {% csrf_token %}\n    {% if error %}\n      <div class=\"invalid-feedback\">{{ error }}</div>\n    {% endif %}\n  </form>\n----\n====\n\n<1> `{{ form.text }}` renders just the HTML input for the `text` field of the form.\n\nThat causes our two unit tests that check on the form input to fail:\n\n[subs=\"specialcharacters,callouts\"]\n----\n[...]\n======================================================================\nFAIL: test_renders_input_form\n(lists.tests.test_views.HomePageTest.test_renders_input_form)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_views.py\", line 19, in\ntest_renders_input_form\n    self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'item_text' not found in ['text', 'csrfmiddlewaretoken']  <1>\n\n======================================================================\nFAIL: test_renders_input_form\n(lists.tests.test_views.ListViewTest.test_renders_input_form)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_views.py\", line 60, in\ntest_renders_input_form\n    self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'item_text' not found in ['csrfmiddlewaretoken']  <2>\n\nRan 18 tests in 0.022s\n\nFAILED (failures=2)\n----\n\n<1> The test for the home page is failing because the `name` attribute\n    of the input box is now `text`, not `item_text`.\n\n<2> The test for the list view is failing because\n    because we're not instantiating a form in that view,\n    so there's no `form` variable in the template.\n    The input box isn't even being rendered.\n\n[role=\"pagebreak-before\"]\nLet's fix things one at a time.\nFirst, let's back out our change and restore the hand-crafted HTML input\nin cases where `{{ form }}` is not defined:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch15l015)\n====\n[source,html]\n----\n          <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n            {% if form %}\n              {{ form.text }}\n            {% else %}\n              <input\n                class=\"form-control form-control-lg {% if error %}is-invalid{% endif %}\"\n                name=\"item_text\"\n                id=\"id_new_item\"\n                placeholder=\"Enter a to-do item\"\n              />\n            {% endif %}\n            {% csrf_token %}\n            {% if error %}\n              <div class=\"invalid-feedback\">{{ error }}</div>\n            {% endif %}\n          </form>\n----\n====\n\nThat takes us down to one failure:\n\n----\nAssertionError: 'item_text' not found in ['text', 'csrfmiddlewaretoken']\n----\n\nLet's make a note to come back and tidy this up,\nand then we'll talk about what's happened and how to deal with it:\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n*****\n\n\n==== The Trade-offs of Django ModelForms: The Frontend Is Coupled to the Database\n\nThis highlights one of the trade-offs of using `ModelForm`:\nby auto-generating the form from the model,\nwe tie the `name=` attribute of our form's HTML `<input>`\nto the name of the model field in the database.(((\"ModelForms\", \"tradeoffs of\")))\n\nIn a simple CRUD (create, read, update, and delete) app like ours, that's probably a good deal.\nBut it does mean we need to go back and change our assumptions about\nwhat the `name=` attribute of the input box is going to be.\n\nWhile we're at it, it's worth doing an FT run too:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: [id=\"id_new_item\"]; [...]\n[...]\n\nFAILED (errors=4)\n----\n\nLooks like something else has changed.\n\nIf you pause the FTs or inspect the HTML manually in a browser,\nyou'll see that the `ModelForm` also changes the `id` attribute\nto being `id_text`.footnote:[It's actually possible to customise this attribute via the `widgets`\nattribute we used earlier, even on a `ModelForm`,\nbut because you cannot change the `name` one, we may as well just accept this too.]\n\n\n\n=== A Big Find-and-Replace\n\n\n(((\"find and replace\")))\n(((\"grep command\")))\nIf we want to change our assumption about these two attributes,\nwe'll need to embark on a couple of big find-and-replaces basically:\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n* _Change input name attribute from item_text to just text._\n* _Change input id from id_new_item to id_text._\n*****\n\nBut before we do that,\nlet's back out the rest of our changes and get back to a working state.\n\n[role=\"pagebreak-before less_space\"]\n==== Backing Out Our Changes and Getting to a Working State\n\nThe simplest way to back out changes is with `git`.\nBut in this case, leaving a couple of placeholders does no harm,\nand they'll be helpful to come back to later.(((\"commented-out code\")))(((\"Git\", \"commented-out code and if branches, caution with\")))\n\nSo we can leave the `{{ form.text }}` in the HTML\nbut, by backing out the change in the view, we'll make sure that branch is never actually exercised.\nAgain, to leave ourselves a little placeholder,\nwe'll comment out our code rather than deleting it:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l016)\n====\n[source,python]\n----\ndef home_page(request):\n    # return render(request, \"home.html\", {\"form\": ItemForm()})\n    return render(request, \"home.html\")\n----\n====\n\nWARNING: Be very cautious about leaving commented-out code\n    and unused `if` branches lying around.\n    Do so only if you're sure you're coming back to them very soon,\n    otherwise your codebase will soon get messy!\n\nNow we can do a full unit test and FT run\nto confirm we're back to a working state:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test lists*\nFound 18 test(s).\n[...]\nOK\n\n$ *python src/manage.py test functional_tests*\nFound 4 test(s).\n[...]\n\nOK\n----\n\nAnd let's do a commit to be able to separate out the \nrename from anything else:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff* # changes in base.html + views.py\n$ *git commit -am \"Placeholders for using form in view+template, not in use yet\"*\n----\n\n[role=\"pagebreak-before\"]\nAnd pop an item on the to-do list:\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n* _Change input name attribute from item_text to just text._\n* _Change input id from id_new_item to id_text._\n* _Uncomment use of form in home_page() view_item to id_text._\n* _Use form in other views._\n*****\n\n==== Renaming the name Attribute\n\nSo, let's have a look for `item_text` in the codebase:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*grep -Ir item_text src*]\nsrc/lists/migrations/0003_list.py:        (\"lists\", \"0002_item_text\"),\nsrc/lists/tests/test_views.py:        self.assertIn(\"item_text\",\n[input.get(\"name\") for input in inputs])\n[...]\nsrc/lists/templates/base.html:                name=\"item_text\"\nsrc/lists/views.py:    item = Item(text=request.POST[\"item_text\"], list=nulist)\nsrc/lists/views.py:            item = Item(text=request.POST[\"item_text\"],\nlist=our_list)\n----\n\nWe can ignore the migration, which is just using `item_text` as metadata.\nSo the changes we need to make are in three places:\n\n1. _views.py_ \n2. _test_views.py_\n3. _base.html_\n\n[role=\"pagebreak-before\"]\nLet's go ahead and make those.\nI'm sure you can manage your own find-and-replace!\nThey should look something like this:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch15l017)\n====\n[source,diff]\n----\n@@ -16,12 +16,12 @@ class HomePageTest(TestCase):\n         [form] = parsed.cssselect(\"form[method=POST]\")\n         self.assertEqual(form.get(\"action\"), \"/lists/new\")\n         inputs = form.cssselect(\"input\")\n-        self.assertIn(\"item_text\", [input.get(\"name\") for input in inputs])\n+        self.assertIn(\"text\", [input.get(\"name\") for input in inputs])\n \n \n class NewListTest(TestCase):\n     def test_can_save_a_POST_request(self):\n-        self.client.post(\"/lists/new\", data={\"item_text\": \"A new list item\"})\n+        self.client.post(\"/lists/new\", data={\"text\": \"A new list item\"})\n         self.assertEqual(Item.objects.count(), 1)\n         new_item = Item.objects.get()\n         self.assertEqual(new_item.text, \"A new list item\")\n[...]\n----\n====\n\nOr, in _views.py_:\n\n[role=\"sourcecode dofirst-ch15l018\"]\n.src/lists/views.py (ch15l019)\n====\n[source,diff]\n----\n@@ -12,7 +12,7 @@ def home_page(request):\n \n def new_list(request):\n     nulist = List.objects.create()\n-    item = Item(text=request.POST[\"item_text\"], list=nulist)\n+    item = Item(text=request.POST[\"text\"], list=nulist)\n     try:\n         item.full_clean()\n         item.save()\n@@ -29,7 +29,7 @@ def view_list(request, list_id):\n \n     if request.method == \"POST\":\n         try:\n-            item = Item(text=request.POST[\"item_text\"], list=our_list)\n+            item = Item(text=request.POST[\"text\"], list=our_list)\n             item.full_clean()\n             item.save()\n             return redirect(our_list)\n----\n====\n\n[role=\"pagebreak-before\"]\nFinally, in _base.html_:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch15l020)\n====\n[source,diff]\n----\n@@ -21,7 +21,7 @@\n             {% else %}\n               <input\n                 class=\"form-control form-control-lg {% if error %}is-invalid{% endif %}\"\n-                name=\"item_text\"\n+                name=\"text\"\n                 id=\"id_new_item\"\n                 placeholder=\"Enter a to-do item\"\n               />\n----\n====\n\nOnce you're done, rerun the unit tests to confirm that the application is self-consistent:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\nRan 18 tests in 0.126s\n\nOK\n----\n\n\nAnd rerun the FTs too:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nRan 4 tests in 12.154s\n\nOK\n----\n\nGood! One down:\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _Change input id from id_new_item to id_text._\n* _Uncomment use of form in home_page() view_item to id_text._\n* _Use form in other views._\n*****\n\n==== Renaming the id Attribute\n\nNow for the `id=` attribute.\nA quick `grep` shows us that `id_new_item` appears in the template,\nand in all three FT files:\n\n[subs=\"\"]\n----\n$ <strong>grep -r id_new_item</strong>\nsrc/lists/templates/base.html:                id=\"id_new_item\"\nsrc/functional_tests/test_list_item_validation.py:\nself.browser.find_element(By.ID, \"id_new_item\").send_keys(Keys.ENTER)\nsrc/functional_tests/test_list_item_validation.py:\nself.browser.find_element(By.ID, \"id_new_item\").send_keys(\"Purchase milk\")\n[...]\n----\n\nThat's a good call for a refactor within the FTs too.\nLet's make a new helper method in _base.py_:\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch15l021)\n====\n[source,python]\n----\nclass FunctionalTest(StaticLiveServerTestCase):\n    [...]\n    def get_item_input_box(self):\n        return self.browser.find_element(By.ID, \"id_new_item\")  # <1>\n----\n====\n\n<1> We'll keep the old `id` for now. Working state to working state!\n\nAnd then we use it throughout--I had to make four changes in\n_test_simple_list_creation.py_, two in _test_layout_and_styling.py_, and six\nin _test_list_item_validation.py_, for example:\n\n\n[role=\"sourcecode dofirst-ch15l022 currentcontents\"]\n.src/functional_tests/test_simple_list_creation.py\n====\n[source,python]\n----\n    # She is invited to enter a to-do item straight away\n    inputbox = self.get_item_input_box()\n----\n====\n\nOr:\n\n[role=\"sourcecode currentcontents\"]\n.src/functional_tests/test_list_item_validation.py\n====\n[source,python]\n----\n    # an empty list item. She hits Enter on the empty input box\n    self.browser.get(self.live_server_url)\n    self.get_item_input_box().send_keys(Keys.ENTER)\n----\n====\n\nI won't show you every single one; I'm sure you can manage this for yourself!\nYou can redo the `grep` to check that you've caught them all:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *grep -r id_new_item*\nsrc/lists/templates/base.html:                id=\"id_new_item\"\nsrc/functional_tests/base.py:        return self.browser.find_element(By.ID,\n\"id_new_item\")\n----\n\n[role=\"pagebreak-before\"]\nAnd we can do an FT run too, to make sure we haven't broken anything:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nRan 4 tests in 12.154s\n\nOK\n----\n\nGood! FT refactor complete—now hopefully we can make\nthe application-level refactor of the `id` attribute in just two places,\nand we've been in a working state the whole way through.\n\nIn the FT helper method:\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch15l023)\n====\n[source,diff]\n----\n@@ -43,4 +43,4 @@ class FunctionalTest(StaticLiveServerTestCase):\n                 time.sleep(0.5)\n\n     def get_item_input_box(self):\n-        return self.browser.find_element(By.ID, \"id_new_item\")\n+        return self.browser.find_element(By.ID, \"id_text\")\n----\n====\n\nAnd in the template:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch15l024)\n====\n[source,diff]\n----\n@@ -22,7 +22,7 @@\n               <input\n                 class=\"form-control form-control-lg {% if error %}is-invalid{% endif %}\"\n                 name=\"text\"\n-                id=\"id_new_item\"\n+                id=\"id_text\"\n                 placeholder=\"Enter a to-do item\"\n               />\n             {% endif %}\n----\n====\n\nAnd an FT run to confirm:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nRan 4 tests in 12.154s\n\nOK\n----\n\nHooray!\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text.#_\n* _Uncomment use of form in home_page() view_item to id_text._\n* _Use form in other views._\n*****\n\n\n=== A Second Attempt at Using the Form in Our Views\n\nNow that we've done the groundwork,\nhopefully we can drop in our form in the `home_page()` once again:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l025)\n====\n[source,python]\n----\ndef home_page(request):\n    return render(request, \"home.html\", {\"form\": ItemForm()})\n----\n====\n\nLooking good!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test lists*\nFound 18 test(s).\n[...]\nOK\n----\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _Remove if branch and hardcoded input tag from base.html._\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text.#_\n* _[strikethrough line-through]#Uncomment use of form in home_page() view_item to id_text.#_\n* _Use form in other views._\n*****\n\n\n// TODO at this point the FTs actually start failing,\n// due to the required=true issue.\n// could address that here, but it does make all the use-form-for-validation stuff\n// seem a bit pointless\n\n\nLet's see what happens if we remove that `if` from the template:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch15l026)\n====\n[source,diff]\n----\n@@ -16,16 +16,7 @@\n           <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n\n           <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n-            {% if form %}\n-              {{ form.text }}\n-            {% else %}\n-              <input\n-                class=\"form-control form-control-lg {% if error %}is-invalid{% endif %}\"\n-                name=\"text\"\n-                id=\"id_text\"\n-                placeholder=\"Enter a to-do item\"\n-              />\n-            {% endif %}\n+            {{ form.text }}\n             {% csrf_token %}\n             {% if error %}\n               <div class=\"invalid-feedback\">{{ error }}</div>\n----\n====\n\nAha—the unit tests are there to tell us\nthat we need to use the form in `view_list()` too:\n\n\n----\nAssertionError: 'text' not found in ['csrfmiddlewaretoken']\n----\n\n\n[role=\"pagebreak-before\"]\nHere's the minimal use of the form--we won't use it for validation yet,\njust for getting the form into the template:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l027)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    error = None\n    form = ItemForm()\n\n    if request.method == \"POST\":\n        try:\n            item = Item(text=request.POST[\"text\"], list=our_list)\n            item.full_clean()\n            item.save()\n            return redirect(our_list)\n        except ValidationError:\n            error = \"You can't have an empty list item\"\n\n    return render(\n        request, \"list.html\", {\"list\": our_list, \"form\": form, \"error\": error}\n    )\n----\n====\n\nAnd the tests are happy with that too:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test lists*\nFound 18 test(s).\n[...]\nOK\n----\n\nWe're done with the template; what's next?\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _[strikethrough line-through]#Remove if branch and hardcoded input tag from base.html.#_\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text.#_\n* _[strikethrough line-through]#Uncomment use of form in home_page() view_item to id_text.#_\n* _Use form in other views._\n*****\n\n\nRight, let's move on to the next view that doesn't use our form yet—`new_list()`.\nAnd actually, that'll help us with the first item,\nwhich was the whole point of this adventure, really: to\nsee if the forms can help us better handle validation.\n\nLet's see how that works now.\n\n\n=== Using the Form in a View That Takes POST Requests\n\n(((\"form data validation\", \"processing POST requests\")))\nHere's how we can use the form in the `new_list()` view,\navoiding all the manual manipulation of `request.POST` and the error message:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l028)\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)  #<1>\n    if form.is_valid():  #<2>\n        nulist = List.objects.create()\n        Item.objects.create(text=request.POST[\"text\"], list=nulist)\n        return redirect(nulist)\n    else:\n        return render(request, \"home.html\", {\"form\": form})  #<3>\n----\n====\n\n<1> We pass the `request.POST` data into the form's constructor.\n\n<2> We use `form.is_valid()` to determine whether this is a good\n    or a bad submission.\n\n<3> In the invalid case, we pass the form down to the template,\n    instead of our hardcoded error string.\n\nThat view is now looking much nicer!\n\n\nBut, we have a regression in the unit tests:\n\n\n----\n======================================================================\nFAIL: test_validation_errors_are_sent_back_to_home_page_template (lists.tests.t\nest_views.NewListTest.test_validation_errors_are_sent_back_to_home_page_templat\ne)\n ---------------------------------------------------------------------\n[...]\n    self.assertContains(response, expected_error)\n    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: False is not true : Couldn't find 'You can&#x27;t have an empty\nlist item' in the following response\nb'<!doctype html>\\n<html lang=\"en\">\\n\\n  <head>\\n    <title>To-Do\n[...]\n----\n\n\n==== Using the Form to Display Errors in the Template\n\nWe're failing because we're not yet _using_ the form\nto display errors in the template.(((\"templates\", \"using form to display errors in\")))\nHere's how to do that:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch15l029)\n====\n[source,html]\n----\n  <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n    {{ form.text }}\n    {% csrf_token %}\n    {% if form.errors %}  <1>\n      <div class=\"invalid-feedback\">{{ form.errors.text }}</div>  <2>\n    {% endif %}\n  </form>\n----\n====\n\n<1> We change the `if` to look at `form.errors`:\n    it contains a list of all the errors for the form.\n\n<2> `form.errors.text` is magical Django template syntax\n    for `form.errors[\"text\"]`—i.e., the list of errors for the text field in particular.\n\nWhat does that do to our unit tests?\n\n----\n======================================================================\nFAIL: test_validation_errors_end_up_on_lists_page (lists.tests.test_views.ListV\niewTest.test_validation_errors_end_up_on_lists_page)\n ---------------------------------------------------------------------\n[...]\nAssertionError: False is not true : Couldn't find 'You can&#x27;t have an empty\nlist item' in the following response\n----\n\n[role=\"pagebreak-before\"]\nAn unexpected failure--it's actually in the tests for our final view, `view_list()`. Once again, because we've changed the base template, which is used\nby _all_ views, we've made a change that impacts more places than we intended. Let's follow our standard pattern, get back to a working state,\nand see if we can dig into this a bit.\n\n==== Get Back to a Working State\n\nLet's restore the old `[% if %}` in the template,\nso we display errors in both old and new cases:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch15l029-1)\n====\n[source,html]\n----\n          <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n            {{ form.text }}\n            {% csrf_token %}\n            {% if error %}\n              <div class=\"invalid-feedback\">{{ error }}</div>\n            {% endif %}\n            {% if form.errors %}\n              <div class=\"invalid-feedback\">{{ form.errors.text }}</div>\n            {% endif %}\n          </form>\n----\n====\n\nAnd add an item to our stack:\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views_\n* _[strikethrough line-through]#Remove if branch and hardcoded input tag from base.html#_\n* _[strikethrough line-through]#Change input name attribute from item_text to just text#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text#_\n* _[strikethrough line-through]#Uncomment use of form in home_page() view_item to id_text#_\n* _Use form in other views_\n* _Remove if error branch from template_\n*****\n\n[role=\"pagebreak-before less_space\"]\n==== A Helper Method for Several Short Tests\n\nLet's take a look at (((\"helper methods\", \"for short form validation tests\", secondary-sortas=\"short\")))our tests for both views,\nparticularly the ones that check for invalid inputs:\n\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/tests/test_views.py\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n    def test_validation_errors_are_sent_back_to_home_page_template(self):\n        response = self.client.post(\"/lists/new\", data={\"text\": \"\"})\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"home.html\")\n        expected_error = html.escape(\"You can't have an empty list item\")\n        self.assertContains(response, expected_error)\n\n    def test_invalid_list_items_arent_saved(self):\n        self.client.post(\"/lists/new\", data={\"text\": \"\"})\n        self.assertEqual(List.objects.count(), 0)\n        self.assertEqual(Item.objects.count(), 0)\n\nclass ListViewTest(TestCase):\n    [...]\n    def test_validation_errors_end_up_on_lists_page(self):\n        list_ = List.objects.create()\n        response = self.client.post(\n            f\"/lists/{list_.id}/\",\n            data={\"text\": \"\"},\n        )\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"list.html\")\n        expected_error = html.escape(\"You can't have an empty list item\")\n        self.assertContains(response, expected_error)\n----\n====\n\nI see a few problems here:\n\n1. We’re explicitly checking that validation errors prevent anything from being saved to the database in `NewListTest`, but not in `ListViewTest`.\n\n2. We're mixing up the test for the status code, the template,\n  and finding the error in the result.\n\n\n\nLet's be extra meticulous here, and separate out these concerns.\nIdeally, each test should have one assert.\nIf we used copy-paste, that would start to involve a lot of duplication,\nso using a couple of helper methods is a good idea here.\n\n[role=\"pagebreak-before\"]\nHere's some better tests in `NewListTest`:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch15l029-2)\n====\n[source,python]\n----\nfrom lists.forms import EMPTY_ITEM_ERROR\n[...]\n\nclass NewListTest(TestCase):\n    def test_can_save_a_POST_request(self):\n        [...]\n    def test_redirects_after_POST(self):\n        [...]\n\n    def post_invalid_input(self):\n        return self.client.post(\"/lists/new\", data={\"text\": \"\"})\n\n    def test_for_invalid_input_nothing_saved_to_db(self):\n        self.post_invalid_input()\n        self.assertEqual(Item.objects.count(), 0)\n\n    def test_for_invalid_input_renders_list_template(self):\n        response = self.post_invalid_input()\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"home.html\")\n\n    def test_for_invalid_input_shows_error_on_page(self):\n        response = self.post_invalid_input()\n        self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))\n----\n====\n\nBy making a little helper function, `post_invalid_input()`,\nwe can make three separate tests without duplicating lots of lines of code. We've seen this several times now.\nIt often feels more natural to write view tests as a single,\nmonolithic block of assertions--the view should do this and this and this,\nthen return that with this.\n\nBut breaking things out into multiple tests is often worthwhile;\nas we saw in previous chapters,\nit helps you isolate the exact problem you have\nwhen you later accidentally introduce a bug.\nHelper methods are one of the tools that lower the psychological barrier,\nby reducing boilerplate and keeping the tests readable.\n\n[role=\"pagebreak-before\"]\nLet's do something similar in `ListViewTest`:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch15l029-3)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        [...]\n    def test_renders_input_form(self):\n        [...]\n    def test_displays_only_items_for_that_list(self):\n        [...]\n    def test_can_save_a_POST_request_to_an_existing_list(self):\n        [...]\n    def test_POST_redirects_to_list_view(self):\n        [...]\n\n    def post_invalid_input(self):\n        mylist = List.objects.create()\n        return self.client.post(f\"/lists/{mylist.id}/\", data={\"text\": \"\"})\n\n    def test_for_invalid_input_nothing_saved_to_db(self):\n        self.post_invalid_input()\n        self.assertEqual(Item.objects.count(), 0)\n\n    def test_for_invalid_input_renders_list_template(self):\n        response = self.post_invalid_input()\n        self.assertEqual(response.status_code, 200)\n        self.assertTemplateUsed(response, \"list.html\")\n\n    def test_for_invalid_input_shows_error_on_page(self):\n        response = self.post_invalid_input()\n        self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))\n----\n====\n\n// (See <<single-endpoint-for-forms>> in the previous chapter if a diagram would be helpful).\n\n// TODO - maybe a little aside saying i'm exaggerating here?\n// not sure i would do this IRL.\n// i mean, it's a good idea _in general_,\n// just maybe not for forms???\n\nAnd let's rerun all our tests:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test lists*\nFound 21 test(s).\n[...]\nOK\n----\n\nGreat!  We now feel confident that we have a lot of very specific unit tests,\nwhich can point us to exactly what goes wrong if we ever make a mistake.\n\n[role=\"pagebreak-before\"]\nSo let's have another go at using our form for _all_ views,\nby fully committing to the `{{ form.errors }}` in the template:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch15l029-4)\n====\n[source,diff]\n----\n@@ -18,9 +18,6 @@\n           <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n             {{ form.text }}\n             {% csrf_token %}\n-            {% if error %}\n-              <div class=\"invalid-feedback\">{{ error }}</div>\n-            {% endif %}\n             {% if form.errors %}\n               <div class=\"invalid-feedback\">{{ form.errors.text }}</div>\n             {% endif %}\n----\n====\n\nAnd we'll see that exactly one test is failing:\n\n----\nFAIL: test_for_invalid_input_shows_error_on_page (lists.tests.test_views.ListVi\newTest.test_for_invalid_input_shows_error_on_page)\n[...]\nAssertionError: False is not true : Couldn't find 'You can&#x27;t have an empty\nlist item' in the following response\n----\n\n\n=== Using the Form in the Existing Lists View\n\n(((\"form data validation\", \"processing POST and GET requests\")))\nLet's try and work step by step towards fully using our form in this final view.\n\n\n==== Using the Form to Pass Errors to the Template\n\nAt the moment, one test is failing because the `view_list()` view for existing lists is not populating `form.errors` in the invalid case. (((\"templates\", \"using form to pass errors to\")))Let’s address just that:\n\n[role=\"sourcecode small-code\"]\n.src/lists/views.py (ch15l030-1)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    error = None\n    form = ItemForm()  # <2>\n\n    if request.method == \"POST\":\n        form = ItemForm(data=request.POST)  # <1>\n        try:\n            item = Item(text=request.POST[\"text\"], list=our_list)\n            item.full_clean()\n            item.save()\n            return redirect(our_list)\n        except ValidationError:\n            error = \"You can't have an empty list item\"\n\n    return render(\n        request, \"list.html\", {\"list\": our_list, \"form\": form, \"error\": error}  # <3>\n    )\n----\n====\n\n<1> Let's add this line, in the `method=POST` branch,\n    and instantiate a form using the POST data.\n\n<2> We already had this empty form for the GET case,\n    but our new one will override it.\n\n<3> And it should now drop through to the template here.\n    \n\nThat gets us back to a working state!\n\n[subs=\"specialcharacters,quotes\"]\n----\nFound 21 test(s).\n[...]\nOK\n----\n\n[role=\"scratchpad\"]\n*****\n* _Remove duplication of validation logic in views._\n* _[strikethrough line-through]#Remove if branch and hardcoded input tag from base.html.#_\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text.#_\n* _[strikethrough line-through]#Uncomment use of form in home_page() view_item to id_text.#_\n* _Use form in other views._\n* _[strikethrough line-through]#Remove if error branch from template.#_\n*****\n\n[role=\"pagebreak-before less_space\"]\n==== Refactoring the View to Use the Form Fully\n\n\nNow let's start using the form more fully,\nand remove some of the manual error handling.\n\nWe remove the `try/except` and replace it with an\n`if form.is_valid()` check, like the one in `new_list()`:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l030-2)\n====\n[source,diff]\n----\n@@ -26,13 +26,11 @@ def view_list(request, list_id):\n\n     if request.method == \"POST\":\n         form = ItemForm(data=request.POST)\n-        try:\n+        if form.is_valid():\n             item = Item(text=request.POST[\"text\"], list=our_list)\n             item.full_clean()\n             item.save()\n             return redirect(our_list)\n-        except ValidationError:\n-            error = \"You can't have an empty list item\"\n     return render(\n         request, \"list.html\", {\"list\": our_list, \"form\": form, \"error\": error}\n----\n====\n\nAnd the tests still pass:\n\n----\nOK\n----\n\nNext, we no longer need the `.full_clean()`,\nso we can go back to using `.objects.create()`:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l030-3)\n====\n[source,diff]\n----\n@@ -27,9 +27,7 @@ def view_list(request, list_id):\n     if request.method == \"POST\":\n         form = ItemForm(data=request.POST)\n         if form.is_valid():\n-            item = Item(text=request.POST[\"text\"], list=our_list)\n-            item.full_clean()\n-            item.save()\n+            Item.objects.create(text=request.POST[\"text\"], list=our_list)\n             return redirect(our_list)\n----\n====\n\nThe tests still pass:\n\n----\nOK\n----\n\n[role=\"pagebreak-before\"]\nFinally, the `error` variable is always `None`,\nand is no longer needed in the template anyhow:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l030-4)\n====\n[source,diff]\n----\n@@ -21,7 +21,6 @@ def new_list(request):\n\n def view_list(request, list_id):\n     our_list = List.objects.get(id=list_id)\n-    error = None\n     form = ItemForm()\n\n     if request.method == \"POST\":\n@@ -30,6 +29,4 @@ def view_list(request, list_id):\n             Item.objects.create(text=request.POST[\"text\"], list=our_list)\n             return redirect(our_list)\n\n-    return render(\n-        request, \"list.html\", {\"list\": our_list, \"form\": form, \"error\": error}\n-    )\n+    return render(request, \"list.html\", {\"list\": our_list, \"form\": form})\n----\n====\n\nAnd the tests are happy with that!\n\n----\nOK\n----\n\nI think our view is in a pretty good shape now.\nHere it is in non-diff mode, as a recap:\n\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/views.py\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    form = ItemForm()\n\n    if request.method == \"POST\":\n        form = ItemForm(data=request.POST)\n        if form.is_valid():\n            Item.objects.create(text=request.POST[\"text\"], list=our_list)\n            return redirect(our_list)\n\n    return render(request, \"list.html\", {\"list\": our_list, \"form\": form})\n----\n====\n\n[role=\"pagebreak-before\"]\nI think we can give ourselves the satisfaction of doing some\ncrossing-things-out:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Remove duplication of validation logic in views.#_\n* _[strikethrough line-through]#Remove if branch and hardcoded input tag from base.html.#_\n* _[strikethrough line-through]#Change input name attribute from item_text to just text.#_\n* _[strikethrough line-through]#Change input id from id_new_item to id_text.#_\n* _[strikethrough line-through]#Uncomment use of form in home_page() view_item to id_text.#_\n* _[strikethrough line-through]#Use form in other views.#_\n* _[strikethrough line-through]#Remove if error branch from template.#_\n*****\n\nPhew! \n\nHey, it's been a while, what do our FTs think?\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n[...]\n======================================================================\nERROR: test_cannot_add_empty_list_items (functional_tests.test_list_item_valida\ntion.ItemValidationTest.test_cannot_add_empty_list_items)\n ---------------------------------------------------------------------\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n[...]\n\nRan 4 tests in 14.897s\n\nFAILED (errors=1)\n----\n\nOh.  All the regression tests are OK,\nbut our validation test seems to be failing—and failing early too!\nIt's on the first attempt to submit an empty item.\nWhat happened?\n\n\n[role=\"pagebreak-before\"]\n=== An Unexpected Benefit: Free Client-Side Validation from HTML5\n\n(((\"HTML5\", \"client-side validation from\")))(((\"client-side validation\", \"from HTML5\", secondary-sortas=\"HTML5\")))\nHow shall we find out what's going on here?\nOne option is to add the usual `time.sleep` just before the error in the FTs,\nand take a look at what's happening while they run.\nAlternatively, spin up the site manually with `manage.py runserver` if you prefer.\nEither way, you should see something like <<html5_popup_screenshot>>.\n\n\n[[html5_popup_screenshot]]\n.HTML5 validation says no\nimage::images/tdd3_1501.png[\"The input with a popup saying 'please fill out this field'\"]\n\nIt seems like the browser is preventing the user\nfrom even submitting the input when it's empty.(((\"required attribute (HTML input)\"))) It's because Django has added the `required` attribute to the HTML input\n(take another look at our `as_p()` printouts from earlier if you don't believe me,\nor have a look at the source in DevTools).\n\nThis is a\nhttps://oreil.ly/z2XiU[feature of HTML5];\nbrowsers nowadays will do some validation at the client side if they can,\npreventing users from even submitting invalid input.\nThat's actually good news!\n\n[role=\"pagebreak-before\"]\nBut, we were working based on incorrect assumptions\nabout what the user experience was going to be.\nLet's change our FT to reflect this new expectation:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/test_list_item_validation.py (ch15l031)\n====\n[source,python]\n----\nclass ItemValidationTest(FunctionalTest):\n    def test_cannot_add_empty_list_items(self):\n        # Edith goes to the home page and accidentally tries to submit\n        # an empty list item. She hits Enter on the empty input box\n        self.browser.get(self.live_server_url)\n        self.get_item_input_box().send_keys(Keys.ENTER)\n\n        # The browser intercepts the request, and does not load the list page\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_text:invalid\")  #<1>\n        )\n\n        # She starts typing some text for the new item and the error disappears\n        self.get_item_input_box().send_keys(\"Purchase milk\")\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_text:valid\")  #<2>\n        )\n\n        # And she can submit it successfully\n        self.get_item_input_box().send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(\"1: Purchase milk\")\n\n        # Perversely, she now decides to submit a second blank list item\n        self.get_item_input_box().send_keys(Keys.ENTER)\n\n        # Again, the browser will not comply\n        self.wait_for_row_in_list_table(\"1: Purchase milk\")\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_text:invalid\")\n        )\n\n        # And she can make it happy by filling some text in\n        self.get_item_input_box().send_keys(\"Make tea\")\n        self.wait_for(\n            lambda: self.browser.find_element(\n                By.CSS_SELECTOR,\n                \"#id_text:valid\",\n            )\n        )\n        self.get_item_input_box().send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(\"2: Make tea\")\n----\n====\n\n<1> Instead of checking for our custom error message,\n    we check using the CSS pseudo-selector `:invalid`,\n    which the browser applies to any HTML5 input that has invalid input.(((\"CSS (Cascading Style Sheets)\", \"pseudo-selector :invalid\")))\n\n<2> And we check for its converse in the case of valid inputs.\n\nSee how useful and flexible our `self.wait_for()` function is turning out to be?\n\nOur FT does look quite different from how it started though, doesn't it?\nI'm sure that's raising a lot of questions in your mind right now.\nPut a pin in them for a moment;\nI promise we'll talk. Let's first see if we're back to passing tests:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nRan 4 tests in 12.154s\n\nOK\n----\n\n\n\n=== A Pat on the Back\n\nFirst, let's give ourselves a massive pat on the back:\nwe've just made a major change to our small app--that input field,\nwith its name and ID, is absolutely critical to making everything work.\nWe've touched seven or eight different files,\ndoing a refactor that's quite involved...this\nis the kind of thing that, without tests, would seriously worry me.\nIn fact, I might well have decided\nthat it wasn't worth messing with code that works.\nBut, because we have a full test suite, we can delve around,\ntidying things up, safe in the knowledge\nthat the tests are there to spot any mistakes we make.\nIt just makes it that much more likely that you're going to keep refactoring,\nkeep tidying up, keep gardening, keep tending to your code, and\nkeep everything neat and tidy and clean and smooth\nand precise and concise and functional and good.\n\n\nAnd it's definitely time for a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n$ *git commit -am \"use form in all views, back to working state\"*\n----\n\n==== But Have We Wasted a Lot of Time?\n\n(((\"form data validation\", \"benefits of\")))\nBut what about our custom error message?\nWhat about all that effort rendering the form in our HTML template?\nWe're not even passing those errors from Django to the user\nif the browser is intercepting the requests before the user even makes them!\nAnd our FT isn't even testing that stuff any more!\n\nWell, you're quite right.\nBut there are two or three reasons all our time hasn't been wasted.\nFirstly, client-side validation isn't enough\nto guarantee you're protected from bad inputs,\nso you always need the server side as well\nif you really care about data integrity;\nusing a form is a nice way of encapsulating that logic.\n\n(((\"HTML5\", \"browsers&#x27; support for\")))\nAlso, not all browsers fully implement HTML5,footnote:[\nSafari was a notable laggard in the last decade;\nit's up to date now.]\nso some users might still see our custom error message.\nAnd if or when we come to letting users access our data via an API\n(see https://www.obeythetestinggoat.com/book/appendix_rest_api.html[Online Appendix: Building a REST API]),\nthen our validation messages will come back into use. On top of that, we'll be able to reuse all our validation and forms code\nwhen we do some more advanced validation that can't be done by HTML5 magic.\n\nBut you know, even if all that weren't true, you can’t be too hard on yourself for occasionally barking up the wrong tree while you're coding.\nNone of us can see the future,\nand we should concentrate on finding the right solution\nrather than the time \"wasted\" on the wrong solution.\n\n\n=== Using the ModelForm's Own Save Method\n\n(((\"form data validation\", \"using form&#x2019;s own save method\", id=\"FDVsave14\")))(((\"ModelForms\", \"using save method\", id=\"ix_MdFsave\")))\nThere are a couple more things we can do to make our views even simpler.\nI've mentioned that forms are supposed to be able to save data\nto the database for us.\nOur case won't quite work out of the box,\nbecause the item needs to know what list to save to.\nBut it's not hard to fix that!\n\nWe start, as always, with a test.\nJust to illustrate what the problem is,\nlet's see what happens if we just try to call `form.save()`:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l033)\n====\n[source,python]\n----\n    def test_form_save_handles_saving_to_a_list(self):\n        form = ItemForm(data={\"text\": \"do me\"})\n        new_item = form.save()\n----\n====\n\nDjango isn't happy, because an item needs to belong to a list:\n\n----\ndjango.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\n----\n\nOur solution is to tell the form's save method what list it should save to:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch15l034)\n====\n[source,python]\n----\nfrom lists.models import Item, List\n[...]\n\n    def test_form_save_handles_saving_to_a_list(self):\n        mylist = List.objects.create()\n        form = ItemForm(data={\"text\": \"do me\"})\n        new_item = form.save(for_list=mylist)  # <1>\n        self.assertEqual(new_item, Item.objects.get())  #<2>\n        self.assertEqual(new_item.text, \"do me\")\n        self.assertEqual(new_item.list, mylist)\n----\n====\n\n<1> We'll imagine that the `.save()` method takes a `for_list=` argument.\n\n<2> We then make sure that the item is correctly saved to the database,\n    with the right attributes.\n\n[role=\"pagebreak-before\"]\nThe tests fail as expected, because as usual, it's still only wishful thinking:\n\n----\n    new_item = form.save(for_list=mylist)\nTypeError: BaseModelForm.save() got an unexpected keyword argument 'for_list'\n----\n\nHere's how we can implement a custom save method:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch15l035)\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    class Meta:\n        [...]\n\n    def save(self, for_list):\n        self.instance.list = for_list\n        return super().save()\n----\n====\n\nThe `.instance` attribute on a form represents the database object\nthat is being modified or created.\nAnd I only learned that as I was writing this chapter!\nThere are other ways of getting this to work,\nincluding manually creating the object yourself,\nor using the `commit=False` argument to save,\nbut this way seemed neatest.\nWe'll explore a different way of making a form \"know\" what list it's for\nin the next chapter. A quick test run to prove it works:\n\n----\nRan 22 tests in 0.086s\n\nOK\n----\n\nFinally, we can refactor our views. `new_list()` first:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l036)\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        nulist = List.objects.create()\n        form.save(for_list=nulist)\n        return redirect(nulist)\n    else:\n        return render(request, \"home.html\", {\"form\": form})\n----\n====\n\nRerun the test to check that everything still passes:\n\n----\nRan 22 tests in 0.086s\nOK\n----\n\n[role=\"pagebreak-before\"]\nThen, refactor `view_list()`:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch15l037)\n====\n[source,python]\n----\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    form = ItemForm()\n\n    if request.method == \"POST\":\n        form = ItemForm(data=request.POST)\n        if form.is_valid():\n            form.save(for_list=our_list)\n            return redirect(our_list)\n\n    return render(request, \"list.html\", {\"list\": our_list, \"form\": form})\n----\n====\n\nWe still have full passes:\n\n// remove unused imports\n[role=\"dofirst-ch15l038\"]\n----\nRan 22 tests in 0.111s\nOK\n----\n\nAnd:\n\n\n----\nRan 4 tests in 14.367s\nOK\n----\n\nGreat!  Let's commit our changes:\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"implement custom save method for the form\"*\n----\n\nOur two views are now looking very much like \"normal\" Django views:\nthey take information from a user's request,\ncombine it with some custom logic or information from the URL (`list_id`),\npass it to a form for validation and possible saving,\nand then redirect or render a template.\n\nForms and validation are really important in Django—and in web programming, in general—so let's try to make a slightly more complicated one in the next chapter, to\nlearn how to prevent duplicate items.\n(((\"ModelForms\", \"using save method\", startref=\"ix_MdFsave\")))(((\"\", startref=\"FDVsave14\")))\n\n\n\n[role=\"less_space pagebreak-before\"]\n.Tips\n*******************************************************************************\nThin views:: If you find yourself looking at complex views, and having to write a lot of\n    tests for them, it's time to start thinking about whether that logic could\n    be moved elsewhere: possibly to a form, like we've done here. Another possible place would be a custom method on the model class—and, once the complexity of the app demands it, out of Django-specific files and into your own classes and functions, that capture your core\n    business logic.(((\"form data validation\", \"best practices\")))\n    (((\"views\", \"thin versus complex views\")))(((\"thin views versus complex views\")))\n    (((\"complex views versus thin views\")))\n\n\nEach test should test one thing::\n    The heuristic is to be suspicious if there's more than one assertion\n    in a unit test.(((\"assertions\", \"one assertion per unit test\")))\n    Sometimes two assertions are closely related, so they belong together.\n    But often your first draft of a test ends up testing multiple behaviours.\n    Therefore, it's worth rewriting it as several tests\n    so that each one can pinpoint specific problems more precisely,\n    and so one failure doesn't mask another.\n    Helper functions can keep your tests from getting too bloated.\n    (((\"\", startref=\"UIform14\")))\n    (((\"unit tests\", \"testing only one thing\")))\n    (((\"testing best practices\")))\n\nBe aware of trade-offs when using frameworks::\n    When we switched to using a `ModelForm`,\n    we saw that it forced us to change the `name=` attribute\n    in our frontend HTML.(((\"frameworks\", \"trade-offs of using\")))\n    Django gave us a lot: it autogenerated the form based on the model,\n    and we have a nice API for doing both validation and saving objects.\n    But we lost something too—we'll revisit this trade-off in the next chapter.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_16_advanced_forms.asciidoc",
    "content": "[[chapter_16_advanced_forms]]\n== More Advanced Forms\n\nLet's look at some more advanced forms usage.\nWe’ve successfully helped our users to avoid blank list items, so now let’s help them to avoid duplicate items as well.\n\nOur validation constraint so far has been about preventing blank items,\nand as you may remember, it turned out that we can enforce that very easily in the frontend.\nAvoiding duplicate items, however, is less straightforward to do in the frontend\n(although not impossible, of course),\nso this chapter will lean more heavily on server-side validation,\nand bubbling errors from the backend back up to the UI.\n\nThis chapter goes into the more intricate details of Django's forms framework,\nso you have my official permission to skim through it\nif you already know all about customising Django forms and how to display errors in the UI,\nor if you're reading this book for the TDD rather than for the Django.\n\nIf you're still learning Django, there's good stuff in here!\nIf you want to just skim-read, that's OK too.\nMake sure you take a quick look at\n<<testing-for-silliness>>,\nand <<what-to-test-in-views>> at the end.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Another FT for Duplicate Items\n\n(((\"form data validation\", \"for duplicate items\", id=\"FDVduplicate15\")))\n(((\"functional tests (FTs)\", \"for duplicate items\", secondary-sortas=\"duplicate items\", id=\"FTduplicate15\")))\n(((\"duplicate items testing\", \"functional test for\", id=\"DITfunctional15\")))\n(((\"user interactions\", \"preventing duplicate items\", id=\"UIduplicate15\")))\nWe add a second test method to `ItemValidationTest`,\nand tell a little story about what we want to see happen\nwhen a user tries to enter the same item twice into their to-do list:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch16l001)\n====\n[source,python]\n----\ndef test_cannot_add_duplicate_items(self):\n    # Edith goes to the home page and starts a new list\n    self.browser.get(self.live_server_url)\n    self.get_item_input_box().send_keys(\"Buy wellies\")\n    self.get_item_input_box().send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Buy wellies\")\n\n    # She accidentally tries to enter a duplicate item\n    self.get_item_input_box().send_keys(\"Buy wellies\")\n    self.get_item_input_box().send_keys(Keys.ENTER)\n\n    # She sees a helpful error message\n    self.wait_for(\n        lambda: self.assertEqual(\n            self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\").text,\n            \"You've already got this in your list\",\n        )\n    )\n----\n====\n\nWhy use two test methods instead of extending one,\nor instead of creating a new file and class?\nIt's a judgement call. These two feel closely related;\nthey're both about validation on the same input field,\nso it feels right to keep them in the same file.\nOn the other hand, they're logically separate enough\nthat it's practical to keep them in different methods:\n\n// DAVID: This feels a bit hand-wavy. What are we weighing up here?\n// For example, does 'signal' matter in functional tests?\n// How about speed?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n\nRan 2 tests in 9.613s\n----\n\n// DAVID: Side note: The favicon 404s are getting pretty distracting by this point, I wonder if it would be\n// worth fixing / silencing that somehow earlier in the book?\n// HARRY: could do it like this https://stackoverflow.com/a/38917888\n\nOK, so we know the first of the two tests passes now.\nIs there a way to run just the failing one, I hear you ask?\nWhy, yes indeed:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.\\\ntest_list_item_validation.ItemValidationTest.test_cannot_add_duplicate_items*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n----\n\n[role=\"pagebreak-before\"]\nIn any case, let's commit it:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am\"Ft for duplicate item validation\"*\n----\n\n\n==== Preventing Duplicates at the Model Layer\n\n(((\"model-layer validation\", \"preventing duplicate items\")))\nSo, if we want to start to implement our actual objective for the chapter,\nlet's write a new test that checks that duplicate items in the same list raise an error:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l002)\n====\n[source,python]\n----\ndef test_duplicate_items_are_invalid(self):\n    mylist = List.objects.create()\n    Item.objects.create(list=mylist, text=\"bla\")\n    with self.assertRaises(ValidationError):\n        item = Item(list=mylist, text=\"bla\")\n        item.full_clean()\n----\n====\n\nAnd, while it occurs to us,\nwe add another test to make sure we don't overdo it on our integrity constraints:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l003)\n====\n[source,python]\n----\ndef test_CAN_save_same_item_to_different_lists(self):\n    list1 = List.objects.create()\n    list2 = List.objects.create()\n    Item.objects.create(list=list1, text=\"bla\")\n    item = Item(list=list2, text=\"bla\")\n    item.full_clean()  # should not raise\n----\n====\n\nI always like to put a little comment for tests that are checking\nthat a particular use case should _not_ raise an error; otherwise,\nit can be hard to see what's being tested:\n\n----\nAssertionError: ValidationError not raised\n----\n\nIf we want to get it deliberately wrong, we can do this:\n\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch16l004)\n====\n[source,python]\n----\nclass Item(models.Model):\n    text = models.TextField(default=\"\", unique=True)\n    list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)\n----\n====\n\n[role=\"pagebreak-before\"]\nThat lets us check that our second test really does pick up on this\nproblem:\n\n----\nERROR: test_CAN_save_same_item_to_different_lists (lists.tests.test_models.List\nAndItemModelsTest.test_CAN_save_same_item_to_different_lists)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_models.py\", line 59, in\ntest_CAN_save_same_item_to_different_lists\n    item.full_clean()  # should not raise\n    [...]\ndjango.core.exceptions.ValidationError: {'text': ['Item with this Text already\nexists.']}\n[...]\n----\n\n\n[[testing-for-silliness]]\n.An Aside on When to Test for Developer Silliness\n*******************************************************************************\n\n// TODO: i kinda want to back to \"stupidity\".  talk to Rita about it.\n\nOne of the judgement calls in testing is when you should write tests that sound\nlike \"check that we haven't done something weird\".  (((\"developer silliness, when to test for\")))In general, you should\nbe wary of these.\n\n\nIn this case, we've written a test to check that you can't save duplicate items\nto the same list.  Now, the simplest way to get that test to pass, the way in\nwhich you'd write the fewest lines of code, would be to make it impossible to\nsave 'any' duplicate items.  That justifies writing another test, despite the\nfact that it would be a \"silly\" or \"wrong\" thing for us to code.\n\nBut you can't be writing tests for every possible way we could have coded\nsomething wrong.footnote:[With that said, you can come pretty close.\nOnce you get comfortable writing tests manually, take a look at\nhttps://hypothesis.readthedocs.io[Hypothesis].\nIt lets you automatically generate input for your tests,\ncovering many more test scenarios than you could realistically type manually.\nIt's not always easy to see how to use it,\nbut for the right kind of problem, it can be very powerful;\nthe very first time I used it, it found a bug!]\nIf you have a function that adds two numbers,\nyou can write a couple of tests:\n\n[role=\"skipme\"]\n[source,python]\n----\nassert adder(1, 1) == 2\nassert adder(2, 1) == 3\n----\n\nBut you have the right to assume that the implementation isn't deliberately\nscrewy or perverse:\n\n[role=\"skipme\"]\n[source,python]\n----\ndef adder(a, b):\n    # unlikely code!\n    if a == 3:\n        return 666\n    else:\n        return a + b\n----\n\nOne way of putting it is: trust yourself not to do something _deliberately_ silly, but do protect against things that might be _accidentally_ silly.\n*******************************************************************************\n\n(((\"Meta attributes\")))(((\"constraints\", \"for form input uniqueness, in Meta attributes\")))\nJust like `ModelForm`, models can use an inner class called `Meta`,\nand that's where we can implement a constraint\nthat says an item must be unique for a particular list—or, in other words, that `text` and `list` must be unique together:\n\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch16l005)\n====\n[source,python]\n----\nclass Item(models.Model):\n    text = models.TextField(default=\"\")\n    list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)\n\n    class Meta:\n        unique_together = (\"list\", \"text\")\n----\n====\n\nAnd that passes:\n\n----\nRan 24 tests in 0.024s\n\nOK\n----\n\nYou might want to take a quick peek at the\nhttps://docs.djangoproject.com/en/5.2/ref/models/options[Django docs on model `Meta` attributes]\nat this point.\n\n\n\n[[rewrite-model-test]]\n==== Rewriting the Old Model Test\n\nThat long-winded model test did serendipitously help us find unexpected\nbugs, but now it's time to rewrite it. I wrote it in a very verbose style to\nintroduce the Django ORM, but in fact, we can get the same coverage from a\ncouple of much shorter tests.\nDelete `test_saving_and_retrieving_items` and replace it with this:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l006)\n====\n[source,python]\n----\nclass ListAndItemModelsTest(TestCase):\n    def test_default_text(self):\n        item = Item()\n        self.assertEqual(item.text, \"\")\n\n    def test_item_is_related_to_list(self):\n        mylist = List.objects.create()\n        item = Item()\n        item.list = mylist\n        item.save()\n        self.assertIn(item, mylist.item_set.all())\n\n    [...]\n----\n====\n\nThat's more than enough really--a check of the default values of attributes\non a freshly initialised model object is enough to sense-check that we've\nprobably set some fields up in 'models.py'.  The \"item is related to list\" test\nis a real \"belt and braces\" test to make sure that our foreign key relationship\nworks.\n\nWhile we're at it, we can split this file out into tests for `Item` and tests\nfor `List` (there's only one of the latter, `test_get_absolute_url`):\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l007)\n====\n[source,python]\n----\nclass ItemModelTest(TestCase):\n    def test_default_text(self):\n        [...]\n\n\nclass ListModelTest(TestCase):\n    def test_get_absolute_url(self):\n        [...]\n----\n====\n\nThat's neater and tidier:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\nRan 25 tests in 0.092s\n\nOK\n----\n\n\n==== Integrity Errors That Show Up on Save\n\n(((\"data integrity errors\")))(((\"database migrations\", \"data integrity errors on uniqueness\")))\nA final aside before we move on.\nDo you remember the discussion mentioned in <<chapter_14_database_layer_validation>>\nthat some data integrity errors _are_ picked up on save?\nIt all depends on whether the integrity constraint is actually being enforced by the database.\n\nTry running `makemigrations` and you'll see\nthat Django wants to add the `unique_together` constraint to the database itself,\nrather than just having it as an application-layer constraint:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'lists':\n  src/lists/migrations/0005_alter_item_unique_together.py\n    ~ Alter unique_together for item (1 constraint(s))\n----\n//ch16l005-1\n\nNow let's run the migration:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py migrate*]\n----\n\n[role=\"pagebreak-before less_space\"]\n.What to Do If You See an IntegrityError When Running Migrations\n*******************************************************************************\nWhen you run the migration, you may encounter the following error:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py migrate*]\nOperations to perform:\n  Apply all migrations: auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying lists.0005_alter_item_unique_together...\nTraceback (most recent call last):\n[...]\nsqlite3.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n\n[...]\ndjango.db.utils.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n----\n\nThe problem is that\nwe have at least one database record that _used_ to be valid\nbut, after introducing our new constraint—the `unique_together`—it's no longer compatible.\n\nTo fix this problem locally, we can just delete `src/db.sqlite3` and run the migration again.\nWe can do this because the database on our laptop is only used for dev,\nso the data in it is not important.\n\nIn <<chapter_18_second_deploy>>, we'll deploy our new code to production,\nand discuss what to do if we run into migrations and data integrity issues at that point.\n*******************************************************************************\n\nNow, if we change our duplicate test to do a `.save` instead of a\n`.full_clean`...\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l008)\n====\n[source,python]\n----\n    def test_duplicate_items_are_invalid(self):\n        mylist = List.objects.create()\n        Item.objects.create(list=mylist, text=\"bla\")\n        with self.assertRaises(ValidationError):\n            item = Item(list=mylist, text=\"bla\")\n            # item.full_clean()\n            item.save()\n----\n====\n\n[role=\"pagebreak-before\"]\nIt gives:\n\n----\nERROR: test_duplicate_items_are_invalid\n(lists.tests.test_models.ItemModelTest.test_duplicate_items_are_invalid)\n[...]\nsqlite3.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n[...]\ndjango.db.utils.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n----\n\nYou can see that the error bubbles up from SQLite, and it's a different\nerror from the one we want—an `IntegrityError` instead of a `ValidationError`.\n\nLet's revert our changes to the test, and see them all passing again:\n\n[role=\"dofirst-ch16l008-1\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\nRan 25 tests in 0.092s\nOK\n----\n\n(((\"\", startref=\"FTduplicate15\")))(((\"\", startref=\"DITfunctional15\")))And\nnow it's time to commit our model-layer changes:\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:[<strong>git status</strong>] # should show changes to tests + models and new migration\n$ pass:[<strong>git add src/lists</strong>]\n$ pass:[<strong>git diff --staged</strong>]\n$ pass:[<strong>git commit -m \"Implement duplicate item validation at model layer\"</strong>]\n----\n\n[role=\"pagebreak-before less_space\"]\n=== Experimenting with Duplicate Item Validation at the Views Layer\n\n\n(((\"duplicate items testing\", \"at the views layer\", secondary-sortas=\"views layer\")))\nLet's try running our FT, to see if that's made any difference.\n\n----\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .invalid-feedback; [...]\n----\n\nIn case you didn't see it as it flew past, the site is 500ing,footnote:[500ing, showing a server error, code 500—of course you can use HTTP status codes as verbs!]\nas in <<integrity-error-unique-constraint>> (feel free to try it out manually).\n\n[[integrity-error-unique-constraint]]\n.Well, at least it didn't make it into the database\nimage::images/tdd3_1601.png[\"The Django Debug Page showing an IntegrityError, details 'UNIQUE constraint failed: lists_item.list_id, lists_item.text', and traceback\"]\n\n[role=\"pagebreak-before\"]\nWe need to be clearer on what we want to happen at the views level.\nLet's write a unit test to set out our expectations:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch16l009)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    [...]\n    def test_for_invalid_input_nothing_saved_to_db(self):\n        [...]\n    def test_for_invalid_input_renders_list_template(self):\n        [...]\n    def test_for_invalid_input_shows_error_on_page(self):\n        [...]\n\n    def test_duplicate_item_validation_errors_end_up_on_lists_page(self):\n        list1 = List.objects.create()\n        Item.objects.create(list=list1, text=\"textey\")\n\n        response = self.client.post(\n            f\"/lists/{list1.id}/\",\n            data={\"text\": \"textey\"},\n        )\n\n        expected_error = html.escape(\"You've already got this in your list\")\n        self.assertContains(response, expected_error)  # <1>\n        self.assertTemplateUsed(response, \"list.html\")  # <2>\n        self.assertEqual(Item.objects.all().count(), 1)  # <3>\n----\n====\n\n<1> Here's our main assertion,\n  which is that we want to see a nice error message on the page.\n\n<2> Here's where we check that it's landing on the normal list page.\n\n<3> And we double-check that we haven't saved anything to the database.footnote:[\nHarry, didn't we spend time in the last chapter making sure all the asserts\nwere in different tests?  Absolutely yes.  Feel free to do that!\nIf I had to justify myself,\nI'd say that we already have all the granular asserts for _one_ error type,\nand this really is just a smoke test that an additional error type is also handled. So, arguably, it doesn't need to be so granular.]\n\n\nThat test confirms that the `IntegrityError` is bubbling all the way up:\n\n----\n  File \"...goat-book/src/lists/views.py\", line 28, in view_list\n    form.save(for_list=our_list)\n    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^\n[...]\ndjango.db.utils.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n----\n\nWe want to avoid integrity errors!\nIdeally, we want the call to `is_valid()` to somehow notice\nthe duplication error before we even try to save.\nBut to do that, our form will need to know in advance what list it's being used for.\nLet's put a skip on this test for now:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch16l010)\n====\n[source,python]\n----\nfrom unittest import skip\n[...]\n\n    @skip\n    def test_duplicate_item_validation_errors_end_up_on_lists_page(self):\n----\n====\n\n// IDEA: alternatively, try/except on the validation error,\n// get everything passing, then refactor to use a form.\n// use the forms tests to explore the api (introduce the idea of a spike)\n// maybe get it working, show how the forms-layer tests are annoying\n// and switch to only views-layer tests\n\n\n=== A More Complex Form to Handle Uniqueness Validation\n\n(((\"duplicate items testing\", \"complex form for\")))\n(((\"uniqueness validation\", seealso=\"duplicate items testing\")))\nThe form to create a new list only needs to know one thing: the new item text.\nA form validating that list items are unique will\nneed to know what list they're in as well.\nJust as we overrode the save method on our `ItemForm`,\nthis time we'll override the _constructor_ on our new form class\nso that it knows what list it applies to.\n\nLet's duplicate our tests from the previous form, tweaking them slightly:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch16l011)\n====\n[source,python]\n----\n[...]\nfrom lists.forms import (\n    DUPLICATE_ITEM_ERROR,\n    EMPTY_ITEM_ERROR,\n    ExistingListItemForm,\n    ItemForm,\n)\n[...]\n\nclass ExistingListItemFormTest(TestCase):\n    def test_form_renders_item_text_input(self):\n        list_ = List.objects.create()\n        form = ExistingListItemForm(for_list=list_)  # <1>\n        self.assertIn('placeholder=\"Enter a to-do item\"', form.as_p())\n\n    def test_form_validation_for_blank_items(self):\n        list_ = List.objects.create()\n        form = ExistingListItemForm(for_list=list_, data={\"text\": \"\"})\n        self.assertFalse(form.is_valid())\n        self.assertEqual(form.errors[\"text\"], [EMPTY_ITEM_ERROR])\n\n    def test_form_validation_for_duplicate_items(self):\n        list_ = List.objects.create()\n        Item.objects.create(list=list_, text=\"no twins!\")\n        form = ExistingListItemForm(for_list=list_, data={\"text\": \"no twins!\"})\n        self.assertFalse(form.is_valid())\n        self.assertEqual(form.errors[\"text\"], [DUPLICATE_ITEM_ERROR])\n----\n====\n\n<1> We're specifying that our new `ExistingListItemForm` will take\n    an argument `for_list=` in its constructor,\n    to be able to specify which list the item is for.\n\nNext we iterate through a few TDD cycles until we get a form with a\ncustom constructor, which just ignores its `for_list` argument.\n(I won't show them all, but I'm sure you'll do them, right? Remember, the Goat\nsees all.)\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l012)\n====\n[source,python]\n----\nDUPLICATE_ITEM_ERROR = \"You've already got this in your list\"\n[...]\nclass ExistingListItemForm(forms.models.ModelForm):\n    def __init__(self, for_list, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n----\n====\n\nAt this point, our error should be:\n\n----\nValueError: ModelForm has no model class specified.\n----\n\nThen, let's see if making it inherit from our existing form helps:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l013)\n====\n[source,python]\n----\nclass ExistingListItemForm(ItemForm):\n    def __init__(self, for_list, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n----\n====\n\nYes, that takes us down to just one failure:\n\n----\nFAIL: test_form_validation_for_duplicate_items (lists.tests.test_forms.Existing\nListItemFormTest.test_form_validation_for_duplicate_items)\n[...]\n    self.assertFalse(form.is_valid())\nAssertionError: True is not false\n----\n\nThe next step requires a little knowledge of Django's validation system—you can read up on it in the Django docs on\nhttps://docs.djangoproject.com/en/5.2/ref/models/instances/#validating-objects[model\nvalidation] and\nhttps://docs.djangoproject.com/en/5.2/ref/forms/validation[form validation].\n\n[role=\"pagebreak-before\"]\nWe can customise validation for a field by implementing a `clean_<fieldname>()`\nmethod, and raising a `ValidationError` if the field is invalid:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l013-1)\n====\n[source,python]\n----\nfrom django.core.exceptions import ValidationError\n[...]\n\nclass ExistingListItemForm(ItemForm):\n    def __init__(self, for_list, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.instance.list = for_list\n\n    def clean_text(self):\n        text = self.cleaned_data[\"text\"]\n        if self.instance.list.item_set.filter(text=text).exists():\n            raise forms.ValidationError(DUPLICATE_ITEM_ERROR)\n        return text\n----\n====\n\nThat makes the tests happy:\n\n----\nFound 29 test(s).\n[...]\nOK (skipped=1)\n----\n\n\nWe're there!  A quick commit:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*\n$ *git add src/lists/forms.py src/lists/tests/test_forms.py*\n$ *git commit -m \"implement ExistingListItemForm, add DUPLICATE_ITEM_ERROR message\"*\n----\n\n\n=== Using the Existing List Item Form in the List View\n\n(((\"duplicate items testing\", \"in the list view\", secondary-sortas=\"list view\", id=\"DITlist15\")))\nNow let's see if we can put this form to work in our view. We remove the skip and, while we're at it, we can use our new constant:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch16l014)\n====\n[source,python]\n----\nfrom lists.forms import (\n    DUPLICATE_ITEM_ERROR,\n    EMPTY_ITEM_ERROR,\n)\n[...]\n\n    def test_duplicate_item_validation_errors_end_up_on_lists_page(self):\n        [...]\n        expected_error = html.escape(DUPLICATE_ITEM_ERROR)\n        self.assertContains(response, expected_error)\n        [...]\n----\n====\n\n[role=\"pagebreak-before\"]\nWe see our `IntegrityError` once again:\n\n----\ndjango.db.utils.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\nlists_item.text\n----\n\nOur fix for this is to switch to using the new form class:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch16l016)\n====\n[source,python]\n----\nfrom lists.forms import ExistingListItemForm, ItemForm\n[...]\ndef view_list(request, list_id):\n    our_list = List.objects.get(id=list_id)\n    form = ExistingListItemForm(for_list=our_list)  # <1>\n\n    if request.method == \"POST\":\n        form = ExistingListItemForm(for_list=our_list, data=request.POST)  # <1>\n        if form.is_valid():\n            form.save(for_list=our_list)  # <2>\n            [...]\n    [...]\n----\n====\n\n<1> We swap out `ItemForm` for `ExistingListItemForm`, and pass in the `for_list=`.\n\n<2> This is a bit annoying—we're duplicating the `for_list=` argument.\n    This form should already know this!\n\n==== Customising the Save Method on Our New Form\n\nProgramming by wishful thinking, as always. Let's specify in our _views.py_ that we wish we could call `save()`\nwithout the duplicated argument:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch16l016-1)\n====\n[source,diff]\n----\n@@ -25,6 +25,6 @@ def view_list(request, list_id):\n     if request.method == \"POST\":\n         form = ExistingListItemForm(for_list=our_list, data=request.POST)\n         if form.is_valid():\n-            form.save(for_list=our_list)\n+            form.save()\n             return redirect(our_list)\n     return render(request, \"list.html\", {\"list\": our_list, \"form\": form})\n\n----\n====\n\nThat gives us a failure as expected:\n\n----\n  File \"...goat-book/src/lists/views.py\", line 28, in view_list\n    form.save()\n    ~~~~~~~~~^^\nTypeError: ItemForm.save() missing 1 required positional argument: 'for_list'\n----\n\n[role=\"pagebreak-before\"]\nLet's drop down to the forms level,\nand write another unit test for how we want our save method to work:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch16l017)\n====\n[source,python]\n----\nclass ExistingListItemFormTest(TestCase):\n[...]\n    def test_form_save(self):\n        mylist = List.objects.create()\n        form = ExistingListItemForm(for_list=mylist, data={\"text\": \"hi\"})\n        self.assertTrue(form.is_valid())\n        new_item = form.save()\n        self.assertEqual(new_item, Item.objects.get())\n[...]\n----\n====\n\nWe can make our form call the grandparent save method:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l018)\n====\n[source,python]\n----\nclass ExistingListItemForm(ItemForm):\n    [...]\n    def save(self):\n        return forms.models.ModelForm.save(self)  # <1>\n----\n====\n\n<1> This manually calls the grandparent `save()`.\n    Personal opinion here: I could have used `super()`,\n    but I prefer not to use `super()` when it requires arguments,\n    say, to get a grandparent.\n    I find Python 3's `super()` with no arguments is awesome to get the immediate parent.\n    Anything else is too error-prone—and, besides, I find it ugly. YMMV.\n\n// SEBASTIAN: IMHO it's actually Django's fault that it handles code reuse using inheritance and methods overriding\n//      Wouldn't do the same thing, but it's your book and your opinion so I shall close my mouth :D\n\nOK, how does that look?  Yep, both the forms level and views level tests now pass:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\nRan 30 tests in 0.082s\n\nOK\n----\n\nTime to see what our FTs think!\n\n[role=\"pagebreak-before less_space\"]\n=== The FTs Pick Up an Issue with Bootstrap Classes\n\nUnfortunately, the FTs are telling (((\"Bootstrap\", \"uniqueness constraint, failure on\")))us we're not done:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\nFAIL: test_cannot_add_duplicate_items [...]\n----------------------------------------------------------------------\n[...]\nAssertionError: '' != \"You've already got this in your list\"\n+ You've already got this in your list\n----\n\nLet's spin up the server with `runserver` and try it out manually—with DevTools open—to see what's going on.\nIf you look through the HTML, you'll see our error `div` is there,\nwith the correct error text, but it's greyed out, indicating that it's hidden (as in <<devtools_error_div_hidden>>).\n\n[[devtools_error_div_hidden]]\n.Our error `div` is there but it's hidden\nimage::images/tdd3_1602.png[\"Our page has a duplicate item in the table and in the form, and devtools is open showing us that the error IS actually in the page HTML, but it's greyed out, to indicate that it is hidden.\"]\n\nI had to dig through https://getbootstrap.com/docs/5.2/forms/validation/#server-side[the docs] a little, but it turns out that Bootstrap requires form elements\nwith errors to have _another_ custom class, `is-invalid`. You can actually try this out in DevTools!\nIf you double-click, you can edit the HTML and add the class,\nas in <<devtools_closeup_edit_html>>.\n\n[[devtools_closeup_edit_html]]\n.Hack it in manually—yay\nimage::images/tdd3_1603.png[\"A close-up on the Devtools HTML inspector, showing one of the HTML elements open for editing.  I'm adding the is-invalid class to the main input field.\"]\n\n=== Conditionally Customising CSS Classes for Invalid Forms\n\nSpeaking of hackery, I'm starting to get a bit nervous\nabout the amount of hackery we're doing in our forms now,\nbut let's try getting this to work by doing _even more_\ncustomisation in our forms.\n\nWe want this behaviour for both types of form really,\nso it can go in the tests for the parent `ItemForm` class:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch16l019-1)\n====\n[source,python]\n----\nclass ItemFormTest(TestCase):\n    def test_form_item_input_has_placeholder_and_css_classes(self):\n        [...]\n    def test_form_validation_for_blank_items(self):\n        [...]\n\n    def test_invalid_form_has_bootstrap_is_invalid_css_class(self):\n        form = ItemForm(data={\"text\": \"\"})\n        self.assertFalse(form.is_valid())\n        field = form.fields[\"text\"]\n        self.assertEqual(\n            field.widget.attrs[\"class\"],  # <1>\n            \"form-control form-control-lg is-invalid\",\n        )\n\n    def test_form_save_handles_saving_to_a_list(self):\n        [...]\n----\n====\n\n<1> Here's where you can inspect the `class` attribute on the input field `widget`.\n\n[role=\"pagebreak-before\"]\nAnd here's how we can make it work, by overriding the `is_valid()` method:\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l019-2)\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    class Meta:\n        [...]\n\n    def is_valid(self):\n        result = super().is_valid()  # <1>\n        if not result:\n            self.fields[\"text\"].widget.attrs[\"class\"] += \" is-invalid\"  # <2>\n        return result  # <3>\n\n    def save(self, for_list):\n        [...]\n----\n====\n\n<1> We make sure to call the parent `is_valid()` method first,\n    so we can do all the normal built-in validation.\n\n<2> Here's how we add the extra CSS class to our `widget`.\n\n<3> And we remember to return the result.\n\nIt's not _too_ bad—but, as I say, I'm getting nervous about the amount\nof fiddly code in our forms classes.\nLet's make a note on our scratchpad, and come back to it when our FT is passing perhaps:\n\n[role=\"scratchpad\"]\n*****\n* Review amount of hackery in forms.py.\n*****\n\n[role=\"pagebreak-before\"]\nSpeaking of our FT, let's see how it does now:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\n======================================================================\nFAIL: test_cannot_add_empty_list_items (functional_tests.test_list_item_validat\nion.ItemValidationTest.test_cannot_add_empty_list_items)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n47, in test_cannot_add_empty_list_items\n    self.wait_for_row_in_list_table(\"2: Make tea\")\n  File \"...goat-book/src/functional_tests/base.py\", line 37, in\nwait_for_row_in_list_table\n    self.assertIn(row_text, [row.text for row in rows])\nAssertionError: '2: Make tea' not found in ['1: Make tea', '2: Purchase milk']\n----\n\nOoops; what happened here (<<wrong_order_list>>)?\n\n\n[[wrong_order_list]]\n.The cart is before the horse\nimage::images/tdd3_1604.png[\"A screenshot of the todo list from the FT, with Make Tea appearing above Purchase Milk\"]\n\n==== A Little Digression on Queryset Ordering and String Representations\n\n(((\"queryset ordering\", id=\"queryset15\")))\n(((\"string representations\", id=\"triprep15\")))\nSomething seems to be going wrong with the ordering of our list items.\nTrying to fix this by iterating against an FT is going to be slow,\nso let's work at the unit test level.\n\nWe'll add a test that checks that list items are ordered\nin the sequence they are inserted.\nYou'll have to forgive me if I jump straight to the right answer,\nusing intuition borne of long experience,\nbut I suspect that it might be sorting alphabetically based on list text instead\n(what else would it sort by after all?),\nso I'll pick some text values designed to test that hypothesis:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l020)\n====\n[source,python]\n----\nclass ListModelTest(TestCase):\n    def test_get_absolute_url(self):\n        [...]\n\n    def test_list_items_order(self):\n        list1 = List.objects.create()\n        item1 = Item.objects.create(list=list1, text=\"i1\")\n        item2 = Item.objects.create(list=list1, text=\"item 2\")\n        item3 = Item.objects.create(list=list1, text=\"3\")\n        self.assertEqual(\n            list1.item_set.all(),\n            [item1, item2, item3],\n        )\n----\n====\n\nTIP: FTs are a slow feedback loop.\n    Switch to unit tests when you want to drill down on edge case bugs.\n\n\nThat gives us a new failure, but it's not very readable:\n\n----\nAssertionError: <QuerySet [<Item: Item object (3)>, <Item[40 chars]2)>]> !=\n[<Item: Item object (1)>, <Item: Item obj[29 chars](3)>]\n----\n\nWe need a better string representation for our `Item` model.\nLet's add another unit test:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l021)\n====\n[source,python]\n----\nclass ItemModelTest(TestCase):\n    [...]\n    def test_string_representation(self):\n        item = Item(text=\"some text\")\n        self.assertEqual(str(item), \"some text\")\n----\n====\n\nNOTE: Ordinarily, you would be wary of adding more failing tests\n    when you already have some--it\n    makes reading test output that much more complicated,\n    and just generally makes you nervous.\n    Will we ever get back to a working state?\n    In this case, they're all quite simple tests, so I'm not worried.\n\nThat gives us:\n\n----\nAssertionError: 'Item object (None)' != 'some text'\n----\n\n[role=\"pagebreak-before\"]\nAnd it also gives us the other two failures.  Let's start fixing them all now:\n\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch16l022)\n====\n[source,python]\n----\nclass Item(models.Model):\n    [...]\n\n    def __str__(self):\n        return self.text\n----\n====\n\nNow we're down to one failure, and the ordering test has a more readable\nfailure message:\n\n----\nAssertionError: <QuerySet [<Item: 3>, <Item: i1>, <Item: item 2>]> != [<Item:\ni1>, <Item: item 2>, <Item: 3>]\n----\n\nThat confirms our suspicion that the ordering was alphabetical.\n\nWe can fix that in the `class Meta`:\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch16l023)\n====\n[source,python]\n----\nclass Item(models.Model):\n    [...]\n    class Meta:\n        ordering = (\"id\",)\n        unique_together = (\"list\", \"text\")\n----\n====\n\nDoes that work?\n\n----\nAssertionError: <QuerySet [<Item: i1>, <Item: item 2>, <Item: 3>]> != [<Item:\ni1>, <Item: item 2>, <Item: 3>]\n----\n\nUrp?  It has worked; you can see the items _are_ in the same order,\nbut the tests are confused.\n\nI keep running into this problem actually--Django\nQuerySets don't compare well with lists.\nWe can fix it by converting the QuerySet to a list in our test:footnote:[You could also check out `assertSequenceEqual` from `unittest`, and\n`assertQuerysetEqual` from Django's test tools—although I confess, when I last looked at `assertQuerysetEqual`,\nI was quite baffled...]\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch16l024)\n====\n[source,python]\n----\n    self.assertEqual(\n        list(list1.item_set.all()),\n        [item1, item2, item3],\n    )\n----\n====\n\n// SEBASTIAN: If it's not too much of Django internals, maybe it's worth to mention\n//      how models instances are compared (or at least leave a link for curious readers)\n//      That said, if it wasn't shown before in the book\n//      https://docs.djangoproject.com/en/5.2/topics/db/queries/#comparing-objects\n\n[role=\"pagebreak-before\"]\nThat works; we get a fully passing unit test suite:\n\n----\nRan 33 tests in 0.034s\n\nOK\n----\n\n(((\"\", startref=\"triprep15\")))\n(((\"\", startref=\"queryset15\")))\nWe do need a migration for that ordering change though:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'lists':\n  src/lists/migrations/0006_alter_item_options.py\n    ~ Change Meta options on item\n----\n//ch16l024-1\n\nAnd as a final check, we rerun 'all' the FTs:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\n ---------------------------------------------------------------------\nRan 5 tests in 19.048s\n\nOK\n----\n\nHooray! Time for a final commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n*git status*\n*git add src*\n*git commit -m \"Add is-invalid css class, fix list item ordering\"*\n----\n(((\"\", startref=\"DITlist15\")))\n\n\n=== On the Trade-offs of Django ModelForms, and Frameworks in General\n\nLet's come back to(((\"ModelForms\", \"trade-offs of\")))(((\"frameworks\", \"trade-offs of using\"))) our scratchpad item:\n\n[role=\"scratchpad\"]\n*****\n* Review amount of hackery in forms.py.\n*****\n\n[role=\"pagebreak-before\"]\nLet's take a look at the current state of our forms classes.\nWe've got a real mix of presentation logic,\nvalidation logic, and ORM/storage logic:\n\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/forms.py\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    class Meta:\n        model = Item\n        fields = (\"text\",)\n        widgets = {\n            \"text\": forms.widgets.TextInput(\n                attrs={\n                    \"placeholder\": \"Enter a to-do item\",  # <1>\n                    \"class\": \"form-control form-control-lg\",  # <1>\n                }\n            ),\n        }\n        error_messages = {\"text\": {\"required\": EMPTY_ITEM_ERROR}}\n\n    def is_valid(self):\n        result = super().is_valid()\n        if not result:\n            self.fields[\"text\"].widget.attrs[\"class\"] += \" is-invalid\"  # <1>\n        return result\n\n    def save(self, for_list):  # <3>\n        self.instance.list = for_list\n        return super().save()\n\n\nclass ExistingListItemForm(ItemForm):\n    def __init__(self, for_list, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.instance.list = for_list  # <3>\n\n    def clean_text(self):\n        text = self.cleaned_data[\"text\"]\n        if self.instance.list.item_set.filter(text=text).exists():  <2>\n            raise forms.ValidationError(DUPLICATE_ITEM_ERROR)  <2>\n        return text\n\n    def save(self):\n        return forms.models.ModelForm.save(self)  # <3>\n----\n====\n\n<1> Presentation logic\n<2> Validation logic\n<3> ORM/storage logic\n\n////\nI'm also nervous about the fact that we're overriding parts of the forms\nAPI, like `is_valid()`, and `save()`. Not only that, but;\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/forms.py\n====\n[source,python]\n----\nclass ItemForm(forms.models.ModelForm):\n    def save(self, for_list):  # <1>\n        [...]\n\nclass ExistingListItemForm(ItemForm):\n    def __init__(self, for_list, *args, **kwargs):  # <2>\n        [...]\n    def save(self):  # <3>\n        return forms.models.ModelForm.save(self)\n----\n====\n\n<1> Here we not only override the forms API method,\n    but we actually _change_ the API, meaning that `ItemForm` no longer matches\n    the normal forms API\n\n<2> It's the same here where we override the constructor to add the `for_list` argument.\n\n<3> And in this one, we change the `save()` API _again_,\n    so the API isn't even consistent within our own inheritance hierarchy.\n\nWithout wanting to get all OO-nerdy, this is a violation of the\nLiskov Substitution Principle, which basically says that subclasses should\nlook like their parents.footnote:[\nRead a better write-up here: https://realpython.com/solid-principles-python/]\n////\n\nI think what's happened is that we've reached the limits of the Django forms framework's sweet spot.\n`ModelForms` can be great _because_ they can do presentation, validation, and database storage all in one go, so you can get a lot done without much code.\nBut once you want to customise the default behaviours for each of those things,\nthe code you _do_ end up writing starts to get hard to understand.\n\nLet's see what things would look like if we tried to:\n\n. Move the responsibility for presentation and the rendering of HTML back into the template.\n. Stop using `ModelForm` and do any database logic more explicitly,\n  with less magic.\n// 3. Tried to remove some of the Liskov violations\n\n\n.Another Flip-flop!\n*******************************************************************************\nWe spent most of the last chapter switching from handcrafted HTML\nto having our form autogenerated by Django, and now we're switching back. It's a little frustrating,\nand I could have gone back and changed the book's outline to avoid the back and forth,\nbut I prefer to show software development as it really is.\n\nWe often try things out and end up changing our minds.\nParticularly with frameworks like Django,\nyou can find yourself taking advantage of autogenerated shortcuts for as long as they work. But at some point, you meet the limits of what the framework designers have anticipated,\nand it's time to go back to doing the work yourself.\n\nFrameworks have trade-offs. It doesn't mean you should always reinvent the wheel! It’s OK to cut yourself some slack for “wasting time” on avenues that don’t work out, or revisiting decisions that worked well in the past, but don't work so well now.\n\n*******************************************************************************\n\n==== Moving Presentation Logic Back into the Template\n\nWe're talking about another refactor here;\nwe want to move some functionality out of the form\nand into the template/views layer.(((\"templates\", \"moving presentation logic from form back to\")))(((\"presentation logic, moving from form to template\")))\nHow do we make sure we've got good test coverage?\n\n* We currently have some tests for the CSS classes including `is-invalid`\n  in _test_forms.py_.\n\n* We have some tests of some form attributes in _test_views.py_—e.g., the asserts on the input's `name`.\n\n* And the FTs, ultimately, will tell us if things\n  \"really work\" or not, including testing the interaction between our HTML,\n  Bootstrap, and the browser (e.g., CSS visibility).\n\nWhat we are learning is that the things we're testing in _test_forms.py_\nwill need to move.\n\nTIP: Lower-level tests are good for exploring an API,\n    but they are tightly coupled to it.\n    Higher-level tests can enable more refactoring.\n\n\nHere's one way to write that kind of test:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch16l025-1)\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    [...]\n    def test_for_invalid_input_shows_error_on_page(self):\n        [...]\n\n    def test_for_invalid_input_sets_is_invalid_class(self):\n        response = self.post_invalid_input()\n        parsed = lxml.html.fromstring(response.content)\n        [input] = parsed.cssselect(\"input[name=text]\")\n        self.assertIn(\"is-invalid\", input.get(\"class\"))\n\n    def test_duplicate_item_validation_errors_end_up_on_lists_page(self):\n        [...]\n----\n====\n\nThat's green straight away:\n\n----\nRan 34 tests in 0.040s\n\nOK\n----\n\nAs always, it's nice to deliberately break it,\nto see whether it has a nice failure message, if nothing else.\nLet's do that in _forms.py_:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l025-2)\n====\n[source,diff]\n----\n@@ -24,7 +24,7 @@ class ItemForm(forms.models.ModelForm):\n     def is_valid(self):\n         result = super().is_valid()\n         if not result:\n-            self.fields[\"text\"].widget.attrs[\"class\"] += \" is-invalid\"\n+            self.fields[\"text\"].widget.attrs[\"class\"] += \" boo!\"\n         return result\n\n     def save(self, for_list):\n----\n====\n\n[role=\"pagebreak-before\"]\nReassuringly, both our old test and the new one fail:\n\n----\n[...]\n======================================================================\nFAIL: test_invalid_form_has_bootstrap_is_invalid_css_class (lists.tests.test_fo\nrms.ItemFormTest.test_invalid_form_has_bootstrap_is_invalid_css_class)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_forms.py\", line 30, in\ntest_invalid_form_has_bootstrap_is_invalid_css_class\n    self.assertEqual(\n    ~~~~~~~~~~~~~~~~^\n        field.widget.attrs[\"class\"],\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n        \"form-control form-control-lg is-invalid\",\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\nAssertionError: 'form-control form-control-lg boo!' != 'form-control\nform-control-lg is-invalid'\n- form-control form-control-lg boo!\n?                              ^^^^\n+ form-control form-control-lg is-invalid\n?                              ^^^^^^^^^^\n\n\n======================================================================\nFAIL: test_for_invalid_input_sets_is_invalid_class (lists.tests.test_views.List\nViewTest.test_for_invalid_input_sets_is_invalid_class)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/lists/tests/test_views.py\", line 129, in\ntest_for_invalid_input_sets_is_invalid_class\n    self.assertIn(\"is-invalid\", input.get(\"class\"))\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'is-invalid' not found in 'form-control form-control-lg boo!'\n\n ---------------------------------------------------------------------\nRan 34 tests in 0.039s\n\nFAILED (failures=2)\n----\n\nLet's revert that and get back to passing.\n\nSo, rather than using the `{{ form.text }}` magic in our template,\nlet's bring back our handcrafted HTML.\nIt'll be longer,\nbut at least all of our Bootstrap classes will be in one place,\nwhere we expect them, in the template:\n\n[role=\"sourcecode dofirst-ch16l025-3\"]\n.src/lists/templates/base.html (ch16l025-4)\n====\n[source,diff]\n----\n@@ -16,10 +16,22 @@\n           <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n\n           <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n-            {{ form.text }}\n             {% csrf_token %}\n+            <input  <1>\n+              id=\"id_text\"\n+              name=\"text\"\n+              class=\"form-control  <2>\n+                     form-control-lg\n+                     {% if form.errors %}is-invalid{% endif %}\"\n+              placeholder=\"Enter a to-do item\"\n+              value=\"{{ form.text.value | default:'' }}\"  <3>\n+              aria-describedby=\"id_text_feedback\"  <4>\n+              required\n+            />\n             {% if form.errors %}\n-              <div class=\"invalid-feedback\">{{ form.errors.text }}</div>\n+              <div id=\"id_text_feedback\" class=\"invalid-feedback\">  <4>\n+                {{ form.errors.text.0 }}  <5>\n+              </div>\n             {% endif %}\n           </form>\n         </div>\n----\n====\n\n<1> Here's our artisan `<input>` once again,\n  and the most important custom setting will be its `class` attributes.\n\n<2> As you can see, we can use conditionals even for providing additional ++class++-es.footnote:[\n    We've split the `input` tag across multiple lines so it fits nicely on the screen.\n    If you've not seen that before, it may look a little weird,\n    but I promise it is valid HTML.\n    You don't have to use it if you don't like it though.]\n\n<3> The `| default` \"filter\" is a way to avoid the string \"None\"\n    from showing up as the value in our input field.\n\n<4> We add an `id` to the error message\n    to be able to use `aria-describedby` on the input,\n    as recommended in the Bootstrap docs;\n    it makes the error message more accessible to screen readers.\n\n<5> If you just try to use `form.errors.text`, you'll see\n    that Django injects a `<ul>` list,\n    because the forms framework can report multiple errors for each field.\n    We know we've only got one, so we can use use `form.errors.text.0`.\n\n// TODO: show a screenshot of this bullet point earlier\n\nThat passes:\n\n----\nRan 34 tests in 0.034s\n\nOK\n----\n\nOut of curiosity, let's try a deliberate failure here:\n\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch16l025-5)\n====\n[source,diff]\n----\n@@ -22,7 +22,7 @@\n               name=\"text\"\n               class=\"form-control\n                      form-control-lg\n-                     {% if form.errors %}is-invalid{% endif %}\"\n+                     {% if form.errors %}isnt-invalid{% endif %}\"\n               placeholder=\"Enter a to-do item\"\n               value=\"{{ form.text.value | default:'' }}\"\n               aria-describedby=\"id_text_feedback\"\n\n----\n====\n\nThe failure looks like this:\n\n----\n    self.assertIn(\"is-invalid\", input.get(\"class\"))\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'is-invalid' not found in 'form-control\\n\nform-control-lg\\n                     isnt-invalid'\n----\n\n[role=\"pagebreak-before\"]\nHmm, that's not ideal actually. Let's tweak our assert:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch16l025-6)\n====\n[source,python]\n----\n    def test_for_invalid_input_sets_is_invalid_class(self):\n        response = self.post_invalid_input()\n        parsed = lxml.html.fromstring(response.content)\n        [input] = parsed.cssselect(\"input[name=text]\")\n        self.assertIn(\"is-invalid\", set(input.classes))  # <1>\n----\n====\n\n<1> Rather than using `get(\"class\")`, which returns a raw string,\n    `lxml` can give us the classes as a list\n    (well, actually a special object, but one that we can turn into a set).\n\nThat's more semantically correct, and gives a better error message:\n\n----\n    self.assertIn(\"is-invalid\", set(input.classes))\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'is-invalid' not found in {'form-control', 'isnt-invalid',\n'form-control-lg'}\n----\n\nOK, that's good; we can revert the deliberate mistake in _base.html_.\n\nLet's do a quick FT run to check we've got it right:\n\n[role=\"dofirst-ch16l025-7\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\nFound 2 test(s).\n[...]\nOK\n----\n\nGood!\n\n[role=\"pagebreak-before less_space\"]\n==== Tidying Up the Forms\n\nNow let's start tidying up our forms.(((\"presentation-layer tests, deleting from ItemFormTest\")))\nWe can start by deleting the three presentation-layer tests from `ItemFormTest`:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_forms.py (ch16l026)\n====\n[source,diff]\n----\n@@ -10,28 +10,11 @@ from lists.models import Item, List\n\n\n class ItemFormTest(TestCase):\n-    def test_form_item_input_has_placeholder_and_css_classes(self):\n-        form = ItemForm()\n-\n-        rendered = form.as_p()\n-\n-        self.assertIn('placeholder=\"Enter a to-do item\"', rendered)\n-        self.assertIn('class=\"form-control form-control-lg\"', rendered)\n-\n     def test_form_validation_for_blank_items(self):\n         form = ItemForm(data={\"text\": \"\"})\n         self.assertFalse(form.is_valid())\n         self.assertEqual(form.errors[\"text\"], [EMPTY_ITEM_ERROR])\n\n-    def test_invalid_form_has_bootstrap_is_invalid_css_class(self):\n-        form = ItemForm(data={\"text\": \"\"})\n-        self.assertFalse(form.is_valid())\n-        field = form.fields[\"text\"]\n-        self.assertEqual(\n-            field.widget.attrs[\"class\"],\n-            \"form-control form-control-lg is-invalid\",\n-        )\n-\n     def test_form_save_handles_saving_to_a_list(self):\n         mylist = List.objects.create()\n         form = ItemForm(data={\"text\": \"do me\"})\n@@ -42,11 +25,6 @@ class ItemFormTest(TestCase):\n\n\n class ExistingListItemFormTest(TestCase):\n-    def test_form_renders_item_text_input(self):\n-        list_ = List.objects.create()\n-        form = ExistingListItemForm(for_list=list_)\n-        self.assertIn('placeholder=\"Enter a to-do item\"', form.as_p())\n-\n     def test_form_validation_for_blank_items(self):\n         list_ = List.objects.create()\n         form = ExistingListItemForm(for_list=list_, data={\"text\": \"\"})\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd now(((\"ItemForm class, removing custom logic from\"))) we can remove all that custom logic from the base `ItemForm` class:\n\n\n[role=\"sourcecode dofirst-ch16l027-1\"]\n.src/lists/forms.py (ch16l027)\n====\n[source,diff]\n----\n@@ -11,22 +11,8 @@ class ItemForm(forms.models.ModelForm):\n     class Meta:\n         model = Item\n         fields = (\"text\",)\n-        widgets = {\n-            \"text\": forms.widgets.TextInput(\n-                attrs={\n-                    \"placeholder\": \"Enter a to-do item\",\n-                    \"class\": \"form-control form-control-lg\",\n-                }\n-            ),\n-        }\n         error_messages = {\"text\": {\"required\": EMPTY_ITEM_ERROR}}\n\n-    def is_valid(self):\n-        result = super().is_valid()\n-        if not result:\n-            self.fields[\"text\"].widget.attrs[\"class\"] += \" is-invalid\"\n-        return result\n-\n     def save(self, for_list):\n         self.instance.list = for_list\n         return super().save()\n----\n====\n\n\nDeleting code, yay!\n\nAt this point we should be down to 31 passing tests:\n\n----\nRan 31 tests in 0.024s\n\nOK\n----\n\n==== Switching Back to Simple Forms\n\nNow let's change our forms away from being `ModelForms` and back to regular forms. (((\"ModelForms\", \"switching from to simple forms\")))We'll keep the `save()` methods for now,\nbut we'll switch to using the ORM more explicitly,\nrather than relying on the `ModelForm` magic:\n\n\n[role=\"sourcecode\"]\n.src/lists/forms.py (ch16l028)\n====\n[source,diff]\n----\n@@ -7,27 +7,29 @@ EMPTY_ITEM_ERROR = \"You can't have an empty list item\"\n DUPLICATE_ITEM_ERROR = \"You've already got this in your list\"\n\n\n-class ItemForm(forms.models.ModelForm):\n-    class Meta:\n-        model = Item\n-        fields = (\"text\",)\n-        error_messages = {\"text\": {\"required\": EMPTY_ITEM_ERROR}}\n+class ItemForm(forms.Form):\n+    text = forms.CharField(\n+        error_messages={\"required\": EMPTY_ITEM_ERROR},\n+        required=True,\n+    )\n\n     def save(self, for_list):\n-        self.instance.list = for_list\n-        return super().save()\n+        return Item.objects.create(\n+            list=for_list,\n+            text=self.cleaned_data[\"text\"],\n+        )\n\n\n class ExistingListItemForm(ItemForm):\n     def __init__(self, for_list, *args, **kwargs):\n         super().__init__(*args, **kwargs)\n-        self.instance.list = for_list\n+        self._for_list = for_list\n\n     def clean_text(self):\n         text = self.cleaned_data[\"text\"]\n-        if self.instance.list.item_set.filter(text=text).exists():\n+        if self._for_list.item_set.filter(text=text).exists():\n             raise forms.ValidationError(DUPLICATE_ITEM_ERROR)\n         return text\n\n     def save(self):\n-        return forms.models.ModelForm.save(self)\n+        return super().save(for_list=self._for_list)\n----\n====\n\nWe should still have passing tests at this point:\n\n----\nRan 31 tests in 0.026s\n\nOK\n----\n\nAnd we're in a better place I think!\n\n////\n\nWe still have the Liskov violations on the `__init__()` and `save()`,\nbut perhaps we can live with those for now.\n\nTODO: start by addressing this in 15, ch14l034,\nno need to pass the `for_list=` into the save() method.\n\nThen the custom constructor\n////\n\n\n=== Wrapping Up: What We've Learned About Testing Django\n\n(((\"class-based generic views (CBGVs)\", \"key tests and assertions\")))\n(((\"Django framework\", \"class-based generic views\")))\nWe're now at a point where our app looks a lot more like a \"standard\" Django app,\nand it implements the three common Django layers: models, forms, and views.\nWe no longer have any \"training wheel&#x201d; tests,\nand our code looks pretty much like code we'd be happy to see in a real app.(((\"models, forms, and views  (Django layers)\")))\n\nWe have one unit test file for each of our key source code files.\nHere's a recap of the biggest (and highest-level) one: _test_views_.\n\n\n[[what-to-test-in-views]]\n.Wrap-Up: What to Test in Views\n******************************************************************************\n\nBy way of a recap, let's see an outline of all the test methods and main\nassertions in our `test_views`. (((\"Test-Driven Development (TDD)\", \"testing in views\")))This isn't to say you should copy-paste these\nexactly—it's more like a list of things you should at least consider testing:\n\n[role=\"sourcecode skipme small-code\"]\n.src/lists/tests/test_views.py, selected test methods and asserts\n====\n[source,python]\n----\nclass ListViewTest(TestCase):\n    def test_uses_list_template(self):\n        response = self.client.get(f\"/lists/{mylist.id}/\")  # <1>\n        self.assertTemplateUsed(response, \"list.html\")  # <2>\n    def test_renders_input_form(self):\n        parsed = lxml.html.fromstring(response.content)  # <3>\n        self.assertIn(\"text\", [input.get(\"name\") for input in inputs])  # <3>\n    def test_displays_only_items_for_that_list(self):\n        self.assertContains(response, \"itemey 1\")  # <4>\n        self.assertContains(response, \"itemey 2\")  # <4>\n        self.assertNotContains(response, \"other list item\")  # <4>\n    def test_can_save_a_POST_request_to_an_existing_list(self):\n        self.assertEqual(new_item.text, \"A new item for an existing list\")  # <5>\n    def test_POST_redirects_to_list_view(self):\n        self.assertRedirects(response, f\"/lists/{correct_list.id}/\")  # <5>\n    def test_for_invalid_input_nothing_saved_to_db(self):\n        self.assertEqual(Item.objects.count(), 0)  # <6>\n    def test_for_invalid_input_renders_list_template(self):\n        self.assertEqual(response.status_code, 200)  # <6>\n        self.assertTemplateUsed(response, \"list.html\")  # <6>\n    def test_for_invalid_input_shows_error_on_page(self):\n        self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))  # <6>\n    def test_duplicate_item_validation_errors_end_up_on_lists_page(self):\n        self.assertContains(response, expected_error)  # <7>\n        self.assertTemplateUsed(response, \"list.html\")  # <7>\n        self.assertEqual(Item.objects.all().count(), 1)  # <7>\n----\n====\n\n<1> Use the Django test client.\n\n<2> Optionally (this is a bit of an implementation detail),\n    check the template used.\n\n<3> Check that key parts of your HTML are present.\n    Things that are critical to the integration of frontend and backend\n    are good candidates, like form action and input `name` attributes.\n    Using `lxml` might be overkill, but it does give you less brittle tests.\n\n<4> Think about smoke-testing any other template contents,\n    or any logic in the template:\n    any `{% for %}` or `{% if %}` might deserve a check.\n\n<5> For POST requests, test the valid case via its database side effects,\n    and the redirect response.\n\n<6> For invalid requests, it's worth a basic check that errors\n    make it back to the template.\n\n<7> You don't _always_ have to have ultra-granular tests though.\n\n\n// TODO: link\n// If you'd like to see a worked example of a major refactor,\n// enabled by these tests,\n// check out \n// <<appendix_Django_Class-Based_Views>>\n\n(((\"\", startref=\"FDVduplicate15\")))(((\"\", startref=\"UIduplicate15\")))\n\n******************************************************************************\n\nNext, we'll try to make our data validation more friendly\nby using a bit of client-side code.\nUh-oh, you know what that means...\n"
  },
  {
    "path": "chapter_17_javascript.asciidoc",
    "content": "[[chapter_17_javascript]]\n== A Gentle Excursion into JavaScript\n\n[quote, Geoffrey Willans, English author and journalist]\n______________________________________________________________\nYou can never understand one language until you understand at least two.\n______________________________________________________________\n\nOur new validation logic is good,\nbut wouldn't it be nice if the duplicate-item error messages disappeared\nonce the user started fixing the problem, just like our nice HTML5 validation errors do?\n\nTry it--spin up the site with `./src/manage.py runserver`,\nstart a list, and if you try to submit an empty item,\nyou get the \"Please fill out this field\" pop-up,\nand it disappears as soon as you enter some text.\nBy contrast, enter an item twice,\nyou get the \"You've already got this in your list\" message in red—and even if you edit your submission to something valid,\nthe error stays there until you submit the form (see <<duplicate_item_error>>).\n\n[[duplicate_item_error]]\n.But I've fixed it!\nimage::images/tdd3_1701.png[\"A screenshot of the 'Please fill out this field' error in red, still shown despite the fact that the input value has been modified to be different from the existing item in the list\"]\n\nTo get that error to disappear dynamically, we'd need a teeny-tiny bit of JavaScript.(((\"JavaScript\"))) Python is a delightful language to program in.\nJavaScript wasn't always that.\nBut many of the rough edges have been smoothed off,\nand I think it's fair to say that JavaScript is actually quite nice now.\nAnd in the world of web development, using JavaScript is unavoidable.\nSo let's dip our toes in, and see if we can't have a bit of fun.\n\nNOTE: I'm going to assume you know the basics of JavaScript syntax.\n  If not, the Mozilla guides on \nhttps://oreil.ly/RCAPk[MDN]\n  are always good quality.\n  I've also heard good things about\n  https://eloquentjavascript.net[_Eloquent JavaScript_],\n  if you prefer a real book.\n  (((\"JavaScript testing\", \"additional resources\")))\n\n\n\n=== Starting with an FT\n\n(((\"JavaScript testing\", \"functional test\")))\n(((\"functional tests (FTs)\", \"JavaScript\", id=\"FTjava16\")))\nLet's add a new functional test (FT) to the `ItemValidationTest` class;\nthat asserts that our error message disappears when we start typing:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch17l001)\n====\n[source,python]\n----\ndef test_error_messages_are_cleared_on_input(self):\n    # Edith starts a list and causes a validation error:\n    self.browser.get(self.live_server_url)\n    self.get_item_input_box().send_keys(\"Banter too thick\")\n    self.get_item_input_box().send_keys(Keys.ENTER)\n    self.wait_for_row_in_list_table(\"1: Banter too thick\")\n    self.get_item_input_box().send_keys(\"Banter too thick\")\n    self.get_item_input_box().send_keys(Keys.ENTER)\n    self.wait_for(  # <1>\n        lambda: self.assertTrue(  # <1>\n            self.browser.find_element(\n                By.CSS_SELECTOR, \".invalid-feedback\"\n            ).is_displayed()  # <2>\n        )\n    )\n\n    # She starts typing in the input box to clear the error\n    self.get_item_input_box().send_keys(\"a\")\n\n    # She is pleased to see that the error message disappears\n    self.wait_for(\n        lambda: self.assertFalse(\n            self.browser.find_element(\n                By.CSS_SELECTOR, \".invalid-feedback\"\n            ).is_displayed()  # <2>\n        )\n    )\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> We use another of our `wait_for` invocations, this time with `assertTrue`.\n\n<2> `is_displayed()` tells you whether an element is visible or not.\n    We can't just rely on checking whether the element is _present_ in the DOM,\n    because we're now going to mark elements as hidden,\n    rather than removing them from the document object model (DOM) altogether.\n\n\nThe FT fails appropriately:\n\n\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation.\\\nItemValidationTest.test_error_messages_are_cleared_on_input*]\n\nFAIL: test_error_messages_are_cleared_on_input (functional_tests.test_list_item\n_validation.ItemValidationTest.test_error_messages_are_cleared_on_input)\n[...]\n  File \"...goat-book/src/functional_tests/test_list_item_validation.py\", line\n89, in <lambda>\n    lambda: self.assertFalse(\n            ~~~~~~~~~~~~~~~~^\n        self.browser.find_element(\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^\n            By.CSS_SELECTOR, \".invalid-feedback\"\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n        ).is_displayed()\n        ^^^^^^^^^^^^^^^^\n    )\n    ^\nAssertionError: True is not false\n----\n\nBut, before we move on:  three strikes and refactor!\nWe've got several places where we find the error element using CSS.\nLet's move the logic to a helper function:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch17l002)\n====\n[source,python]\n----\nclass ItemValidationTest(FunctionalTest):\n    def get_error_element(self):\n        return self.browser.find_element(By.CSS_SELECTOR, \".invalid-feedback\")\n\n    [...]\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd we then make three replacements in 'test_list_item_validation', like this:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_list_item_validation.py (ch17l003)\n====\n[source,python]\n----\n    self.wait_for(\n        lambda: self.assertEqual(\n            self.get_error_element().text,\n            \"You've already got this in your list\",\n        )\n    )\n[...]\n    self.wait_for(\n        lambda: self.assertTrue(self.get_error_element().is_displayed()),\n    )\n[...]\n    self.wait_for(\n        lambda: self.assertFalse(self.get_error_element().is_displayed()),\n    )\n----\n====\n\nWe still have our expected failure:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_list_item_validation*]\n[...]\n    lambda: self.assertFalse(self.get_error_element().is_displayed()),\n            ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: True is not false\n----\n\n\nTIP: I like to keep helper methods in the FT class that's using them,\n    and only promote them to the base class when they're actually needed elsewhere.(((\"helper methods\")))\n    It stops the base class from getting too cluttered. You ain’t gonna need it (YAGNI)!\n\n[[js-spike]]\n=== A Quick Spike\n\n(((\"spike\")))\n(((\"exploratory coding\", see=\"also spiking and de-spiking\")))\n(((\"spiking and de-spiking\", \"defined\")))\n(((\"prototyping\", see=\"spiking and de-spiking\")))\nThis will be our first bit of JavaScript.\nWe're also interacting with the Bootstrap CSS framework,\nwhich we maybe don't know very well.\n\nIn <<chapter_15_simple_form>>, we saw that you\ncan use a unit test as a way of exploring a new API or tool.\nSometimes though, you just want to hack something together\nwithout any tests at all, just to see if it works,\nto learn it or get a feel for it.\n\nThat's absolutely fine!\nWhen learning a new tool or exploring a new possible solution,\nit's often appropriate to leave the rigorous TDD process to one side,\nand build a little prototype without tests, or perhaps with very few tests.\nThe Goat doesn't mind looking the other way for a bit.\n\nTIP: It's actually _fine_ to code without tests sometimes,\n    when you want to explore a new tool or build a throwaway proof-of-concept—as long as you geniunely do throw that hacky code away,\n    and start again with TDD for the real thing.\n    The code _always_ comes out much nicer the second time around.\n\nThis kind of prototyping activity is often called a \"spike\",\nfor https://oreil.ly/Ey27H[reasons that aren't entirely clear],\nbut it's a nice memorable name.footnote:[\nThis chapter shows a very small spike.\nWe'll come back and look at the spiking process again,\nwith a weightier Python/Django example,\nin <<chapter_19_spiking_custom_auth>> .]\n\nBefore we start, let's commit our FT.  When embarking on a spike,\nyou want to be able to get back to a clean slate:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git diff*  # new method in src/tests/functional_tests/test_list_item_validation.py\n$ *git commit -am\"FT that validation errors disapper on type\"\n----\n\n\nTIP: Always do a commit before embarking on a spike.\n\n\n==== A Simple Inline Script\n\nI hacked around for a bit,\nand here's more or less the first thing I came up with.(((\"inline scripts (JavaScript)\")))\nI'm adding the JavaScript inline, in a `<script>` tag\nat the bottom of our _base.html_ template:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch17l004)\n====\n[source,html]\n----\n    [...]\n    </div>\n\n    <script>\n      const textInput = document.querySelector(\"#id_text\");  //<1>\n      textInput.oninput = () => {  //<2><3>\n        const errorMsg = document.querySelector(\".invalid-feedback\");\n        errorMsg.style.display = \"none\";  //<4>\n      }\n    </script>\n\n  </body>\n</html>\n----\n====\n\n<1> `document.querySelector` is a way of finding an element in the DOM,\n    using CSS selector syntax, very much like the Selenium\n    `find_element(By.CSS_SELECTOR)` method from our FTs.\n    Grizzled readers may remember having to use jQuery's `$` function for this.\n\n<2> `oninput` is how you attach an event listener \"callback\" function,\n    which will be called whenever the user inputs something into the text box.\n\n<3> Arrow functions, `() => {...}`, are the new way of writing anonymous functions\n    in JavaScript, a bit like Python's `lambda` syntax.\n    I think they're cute!\n    Arguments go in the round brackets and\n    the function body goes in the curly brackets.\n    This is a function that takes no arguments—or I should say, ignores any arguments you try to give it.\n    So, what does it do?\n\n<4> It finds the error message element,\n    and then hides it by setting its `style.display` to \"none\".\n\nThat's actually good enough to get our FT passing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests.test_list_item_validation.\\\nItemValidationTest.test_error_messages_are_cleared_on_input*\nFound 1 test(s).\n[...]\n.\n ---------------------------------------------------------------------\nRan 1 test in 3.284s\n\nOK\n----\n\n\nTIP: It's good practice to put your script loads at the end of your body HTML,\n    as it means the user doesn't have to wait for all your JavaScript to load\n    before they can see something on the page.(((\"HTML\", \"script loads at end of body\")))\n    It also helps to make sure most of the DOM has loaded before any scripts run.\n    See also <<columbo-onload>> later in this chapter.\n\n[role=\"pagebreak-before less_space\"]\n==== Using the Browser DevTools\n\nThe test might be happy, but our solution is a little unsatisfactory.(((\"browsers\", \"editing HTML using DevTools\")))(((\"DevTools (developer tools)\", \"editing HTML in\")))\nIf you actually try it in your browser,\nyou'll see that although the error message is gone,\nthe input is still red and invalid-looking (see <<input-still-red>>).\n\n[[input-still-red]]\n.The error message is gone but the input box is still red\nimage::images/tdd3_1702.png[\"Screenshot of our page where the error `div` is gone but the input is still red.\"]\n\nYou're probably imagining that this has something to do with Bootstrap.\nWe might have been able to hide the error message,\nbut we also need to tell Bootstrap that this input no longer has invalid contents.(((\"Bootstrap\", \"is-invalid CSS class\")))\n\nThis is where I'd normally open up DevTools.\nIf level one of hacking is spiking code directly into an inline `<script>` tag,\nlevel two is hacking things directly in the browser,\nwhere it's not even saved to a file!\n\nIn <<editing-html-in-devtools>>, you can see me directly editing the HTML of the page,\nand finding out that removing the `is-invalid` class from the input element\nseems to do the trick.\nIt not only removes the error message,\nbut also the red border around the input box.\n\n[[editing-html-in-devtools]]\n.Editing the HTML in the browser DevTools\nimage::images/tdd3_1703.png[\"Screenshot of the browser devtools with us editing the classes for the input element\"]\n\nWe have a reasonable solution now; let's write it down:\n\n[role=\"scratchpad\"]\n*****\n* Remove is-invalid Bootstrap CSS class to hide error message and red border.\n*****\n\nTime to de-spike!\n\n[role=\"pagebreak-before less_space\"]\n.Do We Really Need to Write Unit Tests for This?\n*******************************************************************************\n\nDo we really need to write unit tests for this?(((\"unit tests\")))\nBy this point in the book, you probably know I'm going to say \"yes\",\nbut let's talk about it anyway.\n\nOur FT definitely covers the functionality that our JavaScript is delivering,\nand we could extend it if we wanted to,\nto check on the colour of the input box\nor to look at the input element's CSS classes. And if I was really sure that this was the _only_ bit of JavaScript we were ever going to write, I probably would be tempted to leave it at that.\n\nBut I want to press on for two reasons.\nFirstly, because any book on web development has to talk about JavaScript\nand, in a TDD book, I have to show a bit of TDD in JavaScript.\n\nMore importantly though, as always, we have the boiled frog problem.footnote:[For a reminder, read back on this problem in <<trivial_tests_trivial_functions>>.]\nWe might not have enough JavaScript _yet_ to justify a full test suite,\nbut what about when we come along later and add a tiny bit more?\nAnd a tiny bit more again?\n\nIt's always a judgement call. On the one hand YAGNI,\nbut on the other hand, I think it's best to put the scaffolding in place early\nso that going test-first is the easy choice later.\n\nI can already think of several extra things I'd want to do in the frontend!\nWhat about resetting the input to being invalid if someone types in the\nexact duplicate text again?\n\n*******************************************************************************\n\n\n=== Choosing a Basic JavaScript Test Runner\n\n\n(((\"test running libraries\")))\n(((\"JavaScript testing\", \"test running libraries\", id=\"JStestrunner16\")))\n(((\"pytest\")))\nChoosing your testing tools in the Python world is fairly straightforward.\nThe standard library `unittest` package is perfectly adequate,\nand the Django test runner also makes a good default choice.\nMore and more though, people will choose http://pytest.org[pytest]\nfor its `assert`-based assertions, and its fixture management.\nWe don't need to get into the pros and cons now!(((\"assertions\", \"pytest\")))\nThe point is that there's a \"good enough\" default,\nand there's one main popular alternative.\n\nThe JavaScript world has more of a proliferation!\nMocha, Karma, Jester, Chai, AVA, and Tape are just a few of the options\nI came across when researching for the third edition.\n\nI chose Jasmine, because it's still popular despite being around for nearly a decade,\nand because it offers a \"stand-alone\" test runner that you can use\nwithout needing to dive into the whole Node.js/NPM ecosystem.\n(((\"Node.js\")))(((\"Jasmine\")))(((\"unittest module\", \"how testing works with\")))\n\n\n=== An Overview of Jasmine\n\nBy now, we're used to the way that testing works with Python's `unittest` library:\n\n1. We have a tests file, separate from the code we're actually testing.\n2. We have a way of grouping blocks of code into a test:\n  it's a method, whose name starts with `test_`, on a class that inherits\n  from `unittest.TestCase`.\n3. We have a way of making assertions in the test\n  (the special `assert` methods, e.g., `self.assertEqual()`).\n4. We have a way of grouping related tests together\n  (putting them in the same class).\n5. We can specify shared setup and cleanup code\n  that runs before and after all the tests in a given group,\n  the `setUp()` and `tearDown()` methods.\n6. We have some additional helpers that set up our app in a way that simulates\n  what happens “in real life”—whether that's Selenium and the `LiveServerTestCase`,\n  or the Django test client.  This is sometimes called the \"test harness\".\n\nThere are going to be fairly straightforward equivalents for the first five of these concepts(((\"Jasmine\", \"unittest and\"))) in Jasmine:\n\n1. There is a tests file (_Spec.js_).\n2. Tests go into an anonymous function inside an `it()` block.\n3. Assertions use a special function called `expect()`,\n  with a syntax based on method chaining for asserting equality.\n4. Blocks of related tests go into a function in a `describe()` block.\n5. `setUp()` and `tearDown()` are called `beforeEach()` and `afterEach()`, respectively.\n\nThere are some differences for sure, but you'll see over the course of the chapter\nthat they're fundamentally the same. What _is_ substantially different is the \"test harness\" part—the way that Jasmine creates an environment for us to work against.\n\nBecause we're using the browser runner,\nwhat we're actually going to do is define an HTML file\n(_SpecRunner.html_),\nand the engine for running our code is going to be an actual browser\n(with JavaScript running inside it).\n\nThat HTML will be the entry point for our tests, so it will be in charge\nof importing our framework, our tests file, and the code under test.\nIt's essentially a parallel, standalone web page that isn't actually part of our app,\nbut it _does_ import the same JavaScript source code that our app uses.\n\n\n=== Setting Up Our JavaScript Test Environment\n\n// TODO: go all in and use jasmine-browser-runner instead,\n// it will let me use ES6 modules.\n\nLet's download(((\"Jasmine\", \"installing\"))) Jasmine now:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *wget -O jasmine.zip \\\n  https://github.com/jasmine/jasmine/releases/download/v4.6.1/jasmine-standalone-4.6.1.zip*\n$ *unzip jasmine.zip -d src/lists/static/tests*\n$ *rm jasmine.zip*\n# if you're on Windows you may not have wget or unzip,\n# but i'm sure you can manage to manually download and unzip the jasmine release\n\n# move the example tests \"Spec\" file to a more central location\n$ *mv src/lists/static/tests/spec/PlayerSpec.js src/lists/static/tests/Spec.js*\n\n# delete all the other stuff we don't need\n$ *rm -rf src/lists/static/tests/src*\n$ *rm -rf src/lists/static/tests/spec*\n----\n//005-1\n\nThat leaves us with a directory structure like this:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *tree src/lists/static/tests*\nsrc/lists/static/tests\n├── MIT.LICENSE\n├── Spec.js\n├── SpecRunner.html\n└── lib\n    └── jasmine-4.6.1\n        ├── boot0.js\n        ├── boot1.js\n        ├── jasmine-html.js\n        ├── jasmine.css\n        ├── jasmine.js\n        └── jasmine_favicon.png\n\n3 directories, 9 files\n----\n\n_SpecRunner.html_ is the file that ties the proverbial room together. So, we need to go edit it to make sure it's pointing at the right places,\nto take into account the things we've moved around:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/SpecRunner.html (ch17l006)\n[source,diff]\n----\n@@ -14,12 +14,10 @@\n   <script src=\"lib/jasmine-4.6.1/boot1.js\"></script>\n\n   <!-- include source files here... -->\n-  <script src=\"src/Player.js\"></script>\n-  <script src=\"src/Song.js\"></script>\n+  <script src=\"../lists.js\"></script>\n\n   <!-- include spec files here... -->\n-  <script src=\"spec/SpecHelper.js\"></script>\n-  <script src=\"spec/PlayerSpec.js\"></script>\n+  <script src=\"Spec.js\"></script>\n\n </head>\n----\n\nWe change the source files to point at a (for-now imaginary)\n_lists.js_ file that we'll put into the _static_ folder,\nand we change the spec files to point at the single _Spec.js_ file,\nin the _static/tests_ folder.\n\n=== Our First Smoke Test: Describe, It, Expect\n\nNow, let's open up that _Spec.js_ file and strip it down (((\"JavaScript testing\", \"first smoke test, describe, it, and expect\")))to a single minimal smoke test:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l007)\n====\n[source,javascript]\n----\ndescribe(\"Superlists JavaScript\", () => {  //<1>\n\n  it(\"should have working maths\", () => {  //<2>\n    expect(1 + 1).toEqual(2);  //<3>\n  });\n\n});\n----\n====\n\n\n<1> The `describe` block is a way of grouping tests together,\n    a bit like we use classes in our Python tests.\n    It starts with a string name, and then an arrow function for its body.\n\n<2> The `it` block is a single test, a bit like a method in a Python test class.\n    Similarly to the `describe` block,\n    we have a name and then a function to contain the test code.\n    As you can see, the convention is for the descriptive name to complete\n    the sentence started by `it`, in the context of the `describe()` block earlier;\n    so, they often start with \"should\".\n\n<3> Now we have our assertion.\n    This is a little different from assertions in unittest;\n    it's using what's sometimes called \"expect\" style,\n    often also seen in the Ruby world.\n    We wrap our \"actual\" value in the `expect()` function,\n    and then our assertions are methods on the resulting expect object,\n    where `.toEqual` is the equivalent of `assertEqual` in Python.\n\n\n==== Running the Tests via the Browser\n\nLet's see how that looks.(((\"browsers\", \"running Jasmine spec runner test\")))(((\"web browsers\", \"running Jasmine spec runner test\")))\nOpen up _SpecRunner.html_ in your browser;\nyou can do this from the command line with:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *firefox src/lists/static/tests/SpecRunner.html*\n# or, on a mac:\n$ *open src/lists/static/tests/SpecRunner.html*\n----\n\nOr, you can navigate to it in the address bar,\nusing the `file://` protocol—something like this:\n_file&#58;//home/your-username/path/to/superlists/src/lists/static/tests/SpecRunner.html_.\n\nEither way you get there, you should see something like <<jasmine-specrunner-green>>.\n\n[[jasmine-specrunner-green]]\n.The Jasmine spec runner in action\nimage::images/tdd3_1704.png[\"Jasmine browser-based spec runner showing one passing test.\"]\n\n\nLet's try adding a deliberate failure to see what that looks like:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l008)\n====\n[source,javascript]\n----\n  it(\"should have working maths\", () => {\n    expect(1 + 1).toEqual(3);\n  });\n----\n====\n\nNow if we refresh our browser, we'll see red (<<jasmine-specrunner-red>>).\n\n[[jasmine-specrunner-red]]\n.Our Jasmine tests are now red\nimage::images/tdd3_1705.png[\"Jasmine browser-based spec runner showing one failing test, with lots of red.\"]\n\n\n.Is the Jasmine Standalone Browser Test Runner Unconventional?\n*******************************************************************************\n\nIs the Jasmine standalone browser test runner unconventional?\nI think it probably is, to be honest.(((\"Jasmine\", \"standalone browser test runner\")))(((\"browsers\", \"Jasmine standalone browser test runner\")))\nAlthough, the JavaScript world moves so fast, so\nI could be wrong by the time you read this.\n\nWhat I do know is that, along with moving very fast,\nJavaScript things can very quickly become very complicated.\nA lot of people are working with frameworks these days (React being the main one),\nand that comes with TypeScript, transpilers, Node.js,\nNPM, the massive _node_modules_ folder—and a very steep learning curve.(((\"Node.js\")))(((\"frameworks\", \"JavaScript\")))\n\nIn this chapter, my aim is to stick with the basics.\nThe standalone/browser-based test runner lets us write tests without\nneeding to install Node.js or anything else,\nand it lets us test interactions with the DOM. That's enough to give us a basic environment in which to do TDD in JavaScript.\n\nIf you decide to go further in the world of frontend,\nyou probably will eventually get into the complexity of frameworks\nand TypeScript and transpilers,\nbut the basics we work with here will still be a good foundation.\n\nWe will actually take things a small step further in this book,\nincluding dipping our toes into NPM and Node.js in <<chapter_25_CI>>,\nwhere we _will_ get CLI-based JavaScript tests working.\nSo, look out for that!(((\"\", startref=\"JStestrunner16\")))\n(((\"\", startref=\"qunit16\")))\n*******************************************************************************\n\n\n=== Testing with Some DOM Content\n\nWhat do we _actually_ want to test?(((\"JavaScript testing\", \"testing with DOM content\", id=\"ix_JStstDOM\")))\nWe want some JavaScript that will hide the `.invalid-feedback` error div\nwhen the user starts typing into the input box. In other words, our code is going to interact with the `input` element on the page and with the `div.invalid-feedback`.\n\nLet's look at how to set up some copies of these elements in our JavaScript test environment,\nfor our tests and our code to interact with:\n\n\n[role=\"sourcecode small-code dofirstch17l009\"]\n.src/lists/static/tests/Spec.js (ch17l010)\n====\n[source,javascript]\n----\ndescribe(\"Superlists JavaScript\", () => {\n  let testDiv;  //<4>\n\n  beforeEach(() => {  //<1>\n    testDiv = document.createElement(\"div\");  //<2>\n    testDiv.innerHTML = `  //<3>\n      <form>\n        <input\n          id=\"id_text\"\n          name=\"text\"\n          class=\"form-control form-control-lg is-invalid\"\n          placeholder=\"Enter a to-do item\"\n          value=\"Value as submitted\"\n          aria-describedby=\"id_text_feedback\"\n          required\n        />\n        <div id=\"id_text_feedback\" class=\"invalid-feedback\">An error message</div>\n      </form>\n    `;\n    document.body.appendChild(testDiv);\n  });\n\n  afterEach(() => {  //<1>\n    testDiv.remove();\n  });\n----\n====\n\n<1> The `beforeEach` and `afterEach` functions are Jasmine's equivalent of `setUp` and `tearDown`.\n\n<2> The `document` global is a built-in browser variable\n  that represents the current HTML page.\n  So, in our case, it's a reference to the _SpecRunner.html_ page.\n\n<3> We create a new `div` element and populate it with some HTML that matches\n  the elements we care about from our Django template.\n  Notice the use of backticks (+`+) to enable us to write multiline strings.\n  Depending on your text editor, it may even nicely syntax-highlight the HTML for you.\n\n<4> A little quirk of JavaScript here,\n  because we want the same `testDiv` variable to be available inside both the\n  `beforeEach` and `afterEach` functions: we declare the variable with `let`\n  in the containing scope outside of both functions.\n\nIn theory, we could have just added the HTML to the _SpecRunner.html_ file,\nbut by using `beforeEach` and `afterEach`,\nI'm making sure that each test gets a completely fresh copy of the HTML elements involved,\nso that one test can't affect another.\n\nTIP:  To ensure isolation between browser-based JavaScript tests,\n      use `beforeEach()` and `afterEach()` to create and tidy up any DOM elements\n      that your code needs to interact with.\n\nLet's now play with our testing framework\nto see if we can find DOM elements and make assertions on whether they are visible.\nWe'll also try the same `style.display=none` hiding technique\nthat we originally used in our spiked code:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l011)\n====\n[source,javascript]\n----\n  it(\"should have a useful html fixture\", () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    expect(errorMsg.checkVisibility()).toBe(true);  //<1>\n  });\n\n  it(\"can hide things manually and check visibility in tests\", () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    errorMsg.style.display = \"none\";  //<2>\n    expect(errorMsg.checkVisibility()).toBe(false);  //<3>\n  });\n----\n====\n\n<1> We retrieve our error `div` with `querySelector` again,\n    and then use another fairly new API in JavaScript-Land called `checkVisibility()`\n    to check if it's displayed or hidden.footnote:[\nRead up on the `checkVisibility()` method in the https://oreil.ly/hk6qg[MDN documentation].]\n\n<2> We _manually_ hide the element in the test,\n  by setting its `style.display` to \"none\".\n  (Again, our objective here is to smoke-test,\n  both our ability to hide things\n  and our ability to test that they are hidden.)\n\n<3> And we check it worked, with `checkVisibility()` again.\n\n\n\nNotice that I'm being really good about splitting things out into multiple tests,\nwith one assertion each.\nJasmine encourages that by deprecating the ability to pass failure messages into individual `expect/toBe` expressions, for example.\n\n[role=\"pagebreak-before\"]\nIf you refresh the browser, you should see that all passes:\n\n[[first-jasmine-output]]\n====\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n2 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * can hide things manually and check visibility in tests\n  * should have a useful html fixture\n----\n====\n\n(From now on, I'll show the Jasmine outputs as text, like this,\nto avoid filling the chapter with screenshots.)\n\n\n\n=== Building a JavaScript Unit Test for Our Desired Functionality\n\n\n(((\"JavaScript testing\", \"testing with DOM content\", startref=\"ix_JStstDOM\")))(((\"JavaScript testing\", \"unit test\")))\n(((\"unit tests\", \"JavaScript\")))\nNow that we're acquainted with our JavaScript testing tools,\nwe can start to write the real thing:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/static/tests/Spec.js (ch17l012)\n====\n[source,javascript]\n----\n  it(\"should have a useful html fixture\", () => {  // <1>\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    expect(errorMsg.checkVisibility()).toBe(true);\n  });\n\n  it(\"should hide error message on input\", () => {  //<2>\n    const textInput = document.querySelector(\"#id_text\");  //<3>\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n\n    textInput.dispatchEvent(new InputEvent(\"input\"));  //<4>\n\n    expect(errorMsg.checkVisibility()).toBe(false);  //<5>\n  });\n----\n====\n\n<1> As it's not doing any harm, let's keep the first smoke test.\n\n<2> Let's change the second one, and give it a name that describes\n  what we want to happen;\n  our objective is that, when the user starts typing into the input box,\n  we should hide the error message.\n\n<3> We retrieve the `<input>` element from the DOM,\n  in a similar way to how we found the error message `div`.\n\n<4> Here's how we simulate a user typing into the input box.\n\n<5> And here's our real assertion: the error `div` should be hidden after\n  the input box sees an input event.\n\n\nThat gives us our expected failure:\n\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n2 specs, 1 failure, randomized with seed 12345      finished in 0.005s\n\nSpec List | Failures\n\nSuperlists JavaScript > should hide error message on input\nExpected true to be false.\n<Jasmine>\n@file:///...goat-book/src/lists/static/tests/Spec.js:38:40\n<Jasmine>\n----\n\n\nNow let's try reintroducing the code we hacked together in our spike,\ninto _lists.js_:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l014)\n====\n[source,javascript]\n----\nconst textInput = document.querySelector(\"#id_text\");\ntextInput.oninput = () => {\n  const errorMsg = document.querySelector(\".invalid-feedback\");\n  errorMsg.style.display = \"none\";\n};\n----\n====\n\n\nThat doesn't work!  We get an unexpected error:\n\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n2 specs, 2 failures, randomized with seed 12345      finished in 0.005s\nError during loading: TypeError: can't access property \"oninput\", textInput is\nnull in file:///...goat-book/src/lists/static/lists.js line 2\nSpec List | Failures\n\nSuperlists JavaScript > should hide error message on input\nExpected true to be false.\n<Jasmine>\n@file:///...goat-book/src/lists/static/tests/Spec.js:38:40\n<Jasmine>\n----\n\n[role=\"pagebreak-before\"]\nIf your Jasmine output shows `Script error` instead of `textInput is null`,\nopen up the DevTools console, and you'll see the actual error printed in there,\nas in <<typeerror-in-devools>>.footnote:[\nSome users have also reported that Google Chrome will show a different\nerror, to do with the browser preventing loading local files.(((\"web browsers\", \"textInput is null errors and\")))\nIf you really can't use Firefox, you might be able to find some solutions on https://oreil.ly/EkwdH[Stack Overflow].]\n\n[[typeerror-in-devools]]\n.`textInput` is null, one way or another\nimage::images/tdd3_1706.png[\"Screenshot of devtools console showing the textInput is null TypeError\"]\n\n`textInput is null`, it says. Let's see if we can figure out why.\n\n\n=== Fixtures, Execution Order, and Global State: Key Challenges of JavaScript Testing\n\n\n(((\"JavaScript testing\", \"managing global state\")))\n(((\"global state\")))\n(((\"JavaScript testing\", \"key challenges of\", id=\"JSTkey16\")))\n(((\"HTML fixtures\")))\nOne of the difficulties with JavaScript in general, and testing in particular,\nis understanding the order of execution of our code (i.e., what happens when).\nWhen does our code in _lists.js_ run, and when do each of our tests run?\nHow do they all interact with global state—that is, the DOM of our web page\nand the fixtures that we've already seen are supposed to be cleaned up after each test?\n\n[role=\"pagebreak-before less_space\"]\n==== console.log for Debug Printing\n\n(((\"print\", \"debugging with\")))\n(((\"debugging\", \"print-based\")))\n(((\"console.log\")))\nLet's add a couple of debug prints, or \"console.logs\":\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l015)\n====\n[source,javascript]\n----\nconsole.log(\"Spec.js loading\");\n\ndescribe(\"Superlists JavaScript\", () => {\n  let testDiv;\n\n  beforeEach(() => {\n    console.log(\"beforeEach\");\n    testDiv = document.createElement(\"div\");\n\n    [...]\n\n  it(\"should have a useful html fixture\", () => {\n    console.log(\"in test 1\");\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    [...]\n\n  it(\"should hide error message on input\", () => {\n    console.log(\"in test 2\");\n    const textInput = document.querySelector(\"#id_text\");\n    [...]\n----\n====\n\nAnd the same in our actual JavaScript code:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l016)\n====\n[source,javascript]\n----\nconsole.log(\"lists.js loading\");\nconst textInput = document.querySelector(\"#id_text\");\ntextInput.oninput = () => {\n  const errorMsg = document.querySelector(\".invalid-feedback\");\n  errorMsg.style.display = \"none\";\n};\n----\n====\n\n[role=\"pagebreak-before\"]\nRerun the tests, opening up the browser debug console (Ctrl+Shift+I or Cmd+Alt+I)\nand you should see something like <<jasmine-with-js-console>>.\n\n[[jasmine-with-js-console]]\n.Jasmine tests with `console.log` debug outputs\nimage::images/tdd3_1707.png[\"Jasmine tests with console.log debug outputs\"]\n\nWhat do we see?\n\n. First, _lists.js_ loads.\n. Then, we see the error saying `textInput is null`.\n. Next, we see our tests loading in _Spec.js_.\n. Then, we see a `beforeEach`, which is when our test fixture actually gets added to the DOM.\n. Finally, we see the first test run.\n\nThis explains the problem: when _lists.js_ loads,\nthe input node doesn't exist yet.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Using an Initialize Function for More Control Over Execution Time\n\nWe need more control over the order of execution of our JavaScript.(((\"JavaScript testing\", \"using initialize function to control execution time\")))(((\"initialize function in JavaScript testing\")))\nRather than just relying on the code in _lists.js_ running\nwhenever it is loaded by a `<script>` tag,\nwe can use a common pattern: define an \"initialize\" function\nand call that when we want to in our tests (and later in real life).footnote:[Have you been enjoying the British English spelling in the book so far and are shocked to see the _z_ in “initialize”?  By convention, even us Brits often use American spelling in code, because it makes it easier for international colleagues to read, and to make it correspond better with code samples on the internet.]\n\nHere's what that function could look like:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l017)\n====\n[source,javascript]\n----\nconsole.log(\"lists.js loading\");\nconst initialize = () => {\n  console.log(\"initialize called\");\n  const textInput = document.querySelector(\"#id_text\");\n  textInput.oninput = () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    errorMsg.style.display = \"none\";\n  };\n};\n----\n====\n\n\nAnd in our tests file, we call `initialize()` in our key test:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l018)\n====\n[source,javascript]\n----\n  it(\"should have a useful html fixture\", () => {\n    console.log(\"in test 1\");\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    expect(errorMsg.checkVisibility()).toBe(true);\n  });\n\n  it(\"should hide error message on input\", () => {\n    console.log(\"in test 2\");\n    const textInput = document.querySelector(\"#id_text\");\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n\n    initialize();  //<1>\n    textInput.dispatchEvent(new InputEvent(\"input\"));\n\n    expect(errorMsg.checkVisibility()).toBe(false);\n  });\n});\n----\n====\n\n<1> This is where we call `initialize()`. We don't need to call it in our fixture sense-check.\n\n\nAnd that will actually get our tests passing!\n\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n2 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * should hide error message on input\n  * should have a useful html fixture\n----\n\n\nAnd now the `console.log` outputs should be in a more sensible order:\n\n[role=\"skipme\"]\n----\nlists.js loading            lists.js:1:9\nSpec.js loading             Spec.js:1:9\nbeforeEach                  Spec.js:7:13\nin test 1                   Spec.js:31:13\nbeforeEach                  Spec.js:7:13\nin test 2                   Spec.js:37:13\ninitialize called           lists.js:3:11\n----\n\n\n=== Deliberately Breaking Our Code to Force Ourselves to Write More Tests\n\nI'm always nervous when I see green tests.\nWe've copy-pasted five lines of code from our spike with just one test.\nThat was a little too easy,\neven if we did have to go through that little `initialize()` dance.\n\nSo, let's change our `initialize()` function to deliberately break it.\nWhat if we just immediately hide errors?\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l019)\n====\n[source,javascript]\n----\nconst initialize = () => {\n  // const textInput = document.querySelector(\"#id_text\");\n  // textInput.oninput = () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    errorMsg.style.display = \"none\";\n  // };\n};\n----\n====\n\n\nOh dear, as I feared—the tests just pass:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n2 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * should hide error message on input\n  * should have a useful html fixture\n----\n\n\nWe need an extra test, to check that our `initialize()` function\nisn't overzealous:\n\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l020)\n====\n[source,javascript]\n----\n  it(\"should hide error message on input\", () => {\n    [...]\n  });\n\n  it(\"should not hide error message before event is fired\", () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    initialize();\n    expect(errorMsg.checkVisibility()).toBe(true);  //<1>\n  });\n----\n====\n\n<1> In this test, we don't fire the input event with `dispatchEvent`,\n  so we expect the error message to still be visible.\n\n\nThat gives us our expected failure:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 1 failure, randomized with seed 12345      finished in 0.005s\n\nSpec List | Failures\n\nSuperlists JavaScript > should not hide error message before event is fired\nExpected false to be true.\n<Jasmine>\n@file:///...goat-book/src/lists/static/tests/Spec.js:48:40\n<Jasmine>\n----\n\n\nThis justifies us to restore the `textInput.oninput()`:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l021)\n====\n[source,javascript]\n----\n\nconst initialize = () => {\n  const textInput = document.querySelector(\"#id_text\");\n  textInput.oninput = () => {\n    const errorMsg = document.querySelector(\".invalid-feedback\");\n    errorMsg.style.display = \"none\";\n  };\n};\n----\n====\n\n\n=== Red/Green/Refactor: Removing Hardcoded Selectors\n\nThe `#id_text` and `.invalid-feedback` selectors are \"magic constants\" at the moment.\nIt would be better to pass them into `initialize()`,\nboth in the tests and in _base.html_,\nso that they're defined in the same file that actually has the HTML elements.\n\nAnd while we're at it, our tests could do with a bit of refactoring too,\nto remove some duplication.(((\"JavaScript testing\", \"red/green/refactor, removing hardcoded selectors\")))(((\"CSS (Cascading Style Sheets)\", \"removing hardcoded selectors\")))(((\"Red/Green/Refactor\", \"removing hardcoded selectors\")))(((\"selectors (CSS)\", \"removing hardcoded selectors\")))  We'll start with that,\nby defining a few more variables in the top-level scope,\nand populate them in the `beforeEach`:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/static/tests/Spec.js (ch17l022)\n====\n[source,javascript]\n----\ndescribe(\"Superlists JavaScript\", () => {\n  const inputId = \"id_text\";  //<1>\n  const errorClass = \"invalid-feedback\";  //<1>\n  const inputSelector = `#${inputId}`;  //<2>\n  const errorSelector = `.${errorClass}`;  //<2>\n  let testDiv;\n  let textInput;  //<3>\n  let errorMsg;  //<3>\n\n  beforeEach(() => {\n    console.log(\"beforeEach\");\n    testDiv = document.createElement(\"div\");\n    testDiv.innerHTML = `\n      <form>\n        <input\n          id=\"${inputId}\"  //<4>\n          name=\"text\"\n          class=\"form-control form-control-lg is-invalid\"\n          placeholder=\"Enter a to-do item\"\n          value=\"Value as submitted\"\n          aria-describedby=\"id_text_feedback\"\n          required\n        />\n        <div id=\"id_text_feedback\" class=\"${errorClass}\">An error message</div>  //<4>\n      </form>\n    `;\n    document.body.appendChild(testDiv);\n    textInput = document.querySelector(inputSelector);  //<5>\n    errorMsg = document.querySelector(errorSelector);  //<5>\n  });\n----\n====\n\n<1> Let's define some constants to represent the selectors for our input element\n    and our error message `div`.\n\n<2> We can use JavaScript's string interpolation (the equivalent of f-strings)\n    to then define the CSS selectors for the same elements.\n\n<3> We'll also set up some variables to hold the elements we're always referring\n    to in our tests (these can't be constants, as we'll see shortly).\n\n<4> We use a bit more interpolation to reuse the constants in our HTML template.\n    A first bit of de-duplication!\n\n<5> Here's why `textInput` and `errorMsg` can't be constants:\n    we're re-creating the DOM fixture in every `beforeEach`,\n    so we need to re-fetch the elements each time.\n\n\nNow we (((\"Don&#x27;t Repeat Yourself (DRY)\")))can apply some DRY (\"don't repeat yourself\") to strip down our tests:\n\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l023)\n====\n[source,javascript]\n----\n  it(\"should have a useful html fixture\", () => {\n    expect(errorMsg.checkVisibility()).toBe(true);\n  });\n\n  it(\"should hide error message on input\", () => {\n    initialize();\n    textInput.dispatchEvent(new InputEvent(\"input\"));\n\n    expect(errorMsg.checkVisibility()).toBe(false);\n  });\n\n  it(\"should not hide error message before event is fired\", () => {\n    initialize();\n    expect(errorMsg.checkVisibility()).toBe(true);\n  });\n----\n====\n\nYou can definitely overdo DRY in test,\nbut I think this is working out very nicely.\nEach test is between one and three lines long,\nmeaning it's very easy to see what each one is doing,\nand what it's doing differently from the others.\n\nWe've only refactored the tests so far, so let's check that they still pass:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * should hide error message on input\n  * should have a useful html fixture\n  * should not hide error message before event is fired\n----\n\n[role=\"pagebreak-before\"]\nThe next refactor is wanting to pass the selectors to `initialize()`.\nLet's see what happens if we just do that straight away, in the tests:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l024)\n====\n[source,diff]\n----\n@@ -40,14 +40,14 @@ describe(\"Superlists JavaScript\", () => {\n   });\n\n   it(\"should hide error message on input\", () => {\n-    initialize();\n+    initialize(inputSelector, errorSelector);\n     textInput.dispatchEvent(new InputEvent(\"input\"));\n\n     expect(errorMsg.checkVisibility()).toBe(false);\n   });\n\n   it(\"should not hide error message before event is fired\", () => {\n-    initialize();\n+    initialize(inputSelector, errorSelector);\n     expect(errorMsg.checkVisibility()).toBe(true);\n   });\n });\n\n----\n====\n\n\nNow we look at the tests:\n\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * should hide error message on input\n  * should have a useful html fixture\n  * should not hide error message before event is fired\n----\n\nThey still pass!\n\nYou might have been expecting a failure to do with the fact that `initialize()`\nwas defined as taking no arguments—but we passed two!\nThat's because JavaScript is too chill for that.(((\"JavaScript\", \"calling functions with too few or too many arguments\")))\nYou can call a function with too many or too few arguments,\nand JavaScript will just _deal with it_.\n\nLet's fish those arguments out in `initialize()`:\n\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l025)\n====\n[source,javascript]\n----\nconst initialize = (inputSelector, errorSelector) => {\n  const textInput = document.querySelector(inputSelector);\n  textInput.oninput = () => {\n    const errorMsg = document.querySelector(errorSelector);\n    errorMsg.style.display = \"none\";\n  };\n};\n----\n====\n\n\nAnd the tests still pass:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n----\n\n\nLet's deliberately use the arguments the wrong way round,\njust to check we get a failure:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l026)\n====\n[source,javascript]\n----\nconst initialize = (errorSelector, inputSelector) => {\n----\n====\n\nPhew, that does indeed fail:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 1 failure, randomized with seed 12345      finished in 0.005s\n\nSpec List | Failures\n\nSuperlists JavaScript > should hide error message on input\nExpected true to be false.\n<Jasmine>\n@file:///...goat-book/src/lists/static/tests/Spec.js:46:40\n<Jasmine>\n----\n\nOK, back to the right way around:\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l027)\n====\n[source,javascript]\n----\nconst initialize = (inputSelector, errorSelector) => {\n----\n====\n\n\n=== Does it Work?\n\nAnd for the moment of truth, we'll pull in our script\nand invoke our initialize function on our real pages.(((\"JavaScript testing\", \"inline script calling initialize with right selectors\"))) Let's use another `<script>` tag to include our _lists.js_, and strip down the the inline JavaScript to just calling `initialize()` with the right selectors:\n\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch17l028)\n====\n[source,html]\n----\n    </div>\n\n    <script src=\"/static/lists.js\"></script>\n    <script>\n      initialize(\"#id_text\", \".invalid-feedback\");\n    </script>\n\n  </body>\n</html>\n----\n====\n\n[role=\"pagebreak-before\"]\nAaaand we run our FT:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests.test_list_item_validation.\\\nItemValidationTest.test_error_messages_are_cleared_on_input*\n[...]\n\nRan 1 test in 3.023s\n\nOK\n----\n\nHooray!  That's a commit!\n(((\"\", startref=\"JSTkey16\")))\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/lists*\n$ *git commit -m\"Despike our js, add jasmine tests\"*\n----\n\n\nNOTE: We're using a `<script>` tag to import our code,\n  but modern JavaScript lets you use `import` and `export` to explicitly\n  import particular parts of your code.(((\"JavaScript\", \"import and export in to import code\")))\n  However, that involves specifying the scripts as modules,\n  which is fiddly to get working with the single-file test runner we're using.\n  So, I decided to use the \"simple\" old-fashioned way.\n  By all means, investigate modules in your own projects!\n\n\n=== Testing Integration with CSS and Bootstrap\n\nAs the tests flashed past, you may have noticed an unsatisfactory bit of red,\nstill left around our input box. Wait a minute!  We forgot one of the key things we learned in our spike!(((\"JavaScript testing\", \"testing integration with CSS and Bootstrap\")))(((\"Bootstrap\", \"JavaScript test&#x27;s integration with\")))\n\n[role=\"scratchpad\"]\n*****\n* Remove is-invalid Bootstrap CSS class to hide error message and red border.\n*****\n\nWe don't need to manually hack `style.display=none`;\nwe can work _with_ the Bootstrap framework\nand just remove the `.is-invalid` class.\n\n[role=\"pagebreak-before\"]\nOK, let's try it in our implementation:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l029)\n====\n[source,javascript]\n----\nconst initialize = (inputSelector, errorSelector) => {\n  const textInput = document.querySelector(inputSelector);\n  textInput.oninput = () => {\n    textInput.classList.remove(\"is-invalid\");\n  };\n};\n----\n====\n\nOh dear; it seems like that doesn't quite work:\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 1 failure, randomized with seed 12345      finished in 0.005s\n\nSpec List | Failures\n\nSuperlists JavaScript > should hide error message on input\nExpected true to be false.\n<Jasmine>\n@file:///...goat-book/src/lists/static/tests/Spec.js:46:40\n<Jasmine>\n----\n\nWhat's happening here? Well, as hinted in the section title,\nwe're now relying on the integration with Bootstrap's CSS,\nbut our test runner doesn't know about Bootstrap yet.\n\nWe can include it in a reasonably familiar way,\nwhich is by including it in the `<head>` of our _SpecRunner.html_ file:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/SpecRunner.html (ch17l030)\n====\n[source,html]\n----\n  <link rel=\"stylesheet\" href=\"lib/jasmine-4.6.1/jasmine.css\">\n\n  <!-- Bootstrap CSS -->\n  <link href=\"../bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n\n  <script src=\"lib/jasmine-4.6.1/jasmine.js\"></script>\n----\n====\n\n\nThat gets us back to passing tests:\n\n\n[role=\"jasmine-output\"]\n[subs=\"specialcharacters,quotes\"]\n----\n3 specs, 0 failures, randomized with seed 12345      finished in 0.005s\n\n\nSuperlists JavaScript\n  * should hide error message on input\n  * should have a useful html fixture\n  * should not hide error message before event is fired\n----\n\n\nLet's do a little more refactoring.\nIf your editor is set up to do some JavaScript linting,\nyou might have seen a warning saying:\n\n\n[role=\"skipme\"]\n----\n'errorSelector' is declared but its value is never read.\n----\n\n\nGreat!  Looks like we can get away with just one argument to our `initialize()` function:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/lists.js (ch17l031)\n====\n[source,javascript]\n----\nconst initialize = (inputSelector) => {\n  const textInput = document.querySelector(inputSelector);\n  textInput.oninput = () => {\n    textInput.classList.remove(\"is-invalid\");\n  };\n};\n----\n====\n\nAre you enjoying the way the tests keep passing\neven though we're giving the function too many arguments?\nJavaScript is so chill, man.\nLet's strip them down anyway:\n\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/Spec.js (ch17l032)\n====\n[source,diff]\n----\n@@ -40,14 +40,14 @@ describe(\"Superlists JavaScript\", () => {\n   });\n\n   it(\"should hide error message on input\", () => {\n-    initialize(inputSelector, errorSelector);\n+    initialize(inputSelector);\n     textInput.dispatchEvent(new InputEvent(\"input\"));\n\n     expect(errorMsg.checkVisibility()).toBe(false);\n   });\n\n   it(\"should not hide error message before event is fired\", () => {\n-    initialize(inputSelector, errorSelector);\n+    initialize(inputSelector);\n     expect(errorMsg.checkVisibility()).toBe(true);\n   });\n });\n----\n====\n\nAnd the base template, yay.\nNothing more satisfying than _deleting code_:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch17l033)\n====\n[source,html]\n----\n    <script>\n      initialize(\"#id_text\");\n    </script>\n----\n====\n\n\nAnd we can run the FT one more time, just for safety:\n\n\n----\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\n.Trade-offs in JavaScript Unit Testing Versus Selenium\n*******************************************************************************\nSimilarly to the way our Selenium tests and our Django unit tests interact,\nwe have an overlap between the functionality covered by our JavaScript unit tests\nand our Selenium FTs.(((\"unit tests\", \"trade-offs in JavaScript unit testing versus Selenium\")))(((\"Selenium\", \"trade-offs in JavaScript unit testing and\")))(((\"JavaScript testing\", \"trade-offs in unit testing versus Selenium\")))\n\nAs always, the downside of the FTs is that they are slow,\nand they can't always point you towards exactly what went wrong.\nBut they _do_ give us the best reassurance that all our components--in\nthis case, browser, CSS framework, and JavaScript--are all working together.\n\nOn the other hand, by using the jasmine-browser-runner,\nwe are _also_ testing the integration between our browser, our JavaScript, and Bootstrap.\nThis comes at the expense of having a slightly clunky testing setup.\n\nIf you wanted to switch to faster, more focused unit tests,\nyou could try the following:\n\n* Stop using the browser runner.\n* Switch to a node-based CLI test runner.\n* Change from asserting using `checkVisibility()` (which won't work without a real DOM)\n  to asserting what the JavaScript code is actually doing—removing the `.is-invalid` CSS class.\n\nIt might look something like this:\n\n[role=\"sourcecode skipme\"]\n.src/lists/static/tests/Spec.js\n====\n[source,javascript]\n----\n  it(\"should hide error message on input\", () => {\n    initialize(inputSelector);\n    textInput.dispatchEvent(new InputEvent(\"input\"));\n\n    expect(errorMsg.classList).not.toContain(\"is-invalid\");\n  });\n----\n====\n\nThe trade-off here is that you get faster, more focused unit tests,\nbut you need to lean more heavily on Selenium to test the integration with Bootstrap.\nThat could be worth it,\nbut probably only if you start to have a lot more JavaScript code.\n\n*******************************************************************************\n\n\n\n[[columbo-onload]]\n=== Columbo Says: Wait for Onload\n\n[quote, Columbo (fictional trench-coat-wearing American detective known for his persistence)]\n______________________________________________________________\nWait, there's just one more thing...\n______________________________________________________________\n\nAs always, there's one final thing.(((\"JavaScript testing\", \"JavaScript interacting with the DOM, wrapping in onload boilerplate\")))\nWhenever you have some JavaScript that interacts with the DOM,\nit's good to wrap it in some \"onload\" boilerplate\nto make sure that the page has fully loaded before it tries to do anything.\nCurrently it works anyway,\nbecause we've placed the `<script>` tag right at the bottom of the page,\nbut we shouldn't rely on that.\n\nhttps://oreil.ly/buBe8[The MDN documentation] on this is good, as usual.\n\nThe modern JavaScript onload boilerplate is minimal:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch17l034)\n====\n[source,javascript]\n----\n    <script>\n      window.onload = () => {\n        initialize(\"#id_text\");\n      };\n    </script>\n----\n====\n\nThat's a commit folks!\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n$ *git add src/lists/static*  # all our js and tests\n$ *git add src/lists/templates*  # changes to the base template\n$ *git commit -m\"Javascript to hide error messages on input\"*\n----\n\n\n=== JavaScript Testing in the TDD Cycle\n\n\n(((\"JavaScript testing\", \"in the TDD cycle\", secondary-sortas=\"TDD cycle\")))\n(((\"Test-Driven Development (TDD)\", \"JavaScript testing in double loop TDD cycle\")))\nYou may be wondering how these JavaScript tests fit in with our \"double loop\" TDD cycle (see <<double-loop-tdd-ch17>>).\n\n[[double-loop-tdd-ch17]]\n.Double-loop TDD reminder\nimage::images/tdd3_aa01.png[\"Diagram showing an inner loop of red/green/refactor, and an outer loop of red-(inner loop)-green.\"]\n\nThe answer is that the JavaScript unit-test/code cycle\nplays exactly the same role as the Python unit one:\n\n[role=\"pagebreak-before\"]\n1. Write an FT and see it fail.\n2. Figure out what kind of code you need next: Python or JavaScript?\n3. Write a unit test in either language, and see it fail.\n4. Write some code in either language, and make the test pass.\n5. Rinse and repeat.\n\n\nPhew. Well, hopefully some sense of closure there.\nThe next step is to deploy our new code to our servers.\n\nThere is more JavaScript fun in this book too!\nHave a look at the https://www.obeythetestinggoat.com/book/appendix_rest_api.html[Online Appendix: Building a REST API]),\nwhen you're ready for it.\n(((\"\", startref=\"FTjava16\")))\n\n\nNOTE: Want a little more practice with JavaScript?\n    See if you can get our error messages to be hidden\n    when the user clicks inside the input element,\n    as well as just when they type in it.\n    You should be able to FT it too, if you want a bit of extra Selenium practice.\n\n\n.JavaScript Testing Notes\n*******************************************************************************\n\nSelenium as the outer loop::\nOne of the great advantages of Selenium is that it enables you to test that your JavaScript really works, just as it tests your Python code. But, as always, FTs are a very blunt tool, so it's often worth pairing them with some lower-level tests.(((\"Selenium\", \"and JavaScript\", secondary-sortas=\"JavaScript\")))\n\nChoosing your testing framework::\nThere are many JavaScript test-running libraries out there. Jasmine has been around for a while, but the others are also worth investigating. (((\"JavaScript testing\", \"test running libraries\")))\n\nIdiosyncrasies of the browser::\nNo matter which testing library you use, if you're working with Vanilla JavaScript (i.e., not a framework like React), you'll need to work around the key \"gotchas\" of JavaScript:\n+\n* The DOM and HTML fixtures\n* Global state\n* Understanding and controlling execution order(((\"JavaScript testing\", \"managing global state\")))\n(((\"global state\")))\n\nFrontend frameworks::\nAn awful lot of frontend work these days is done in frameworks, React being the 1,000-pound gorilla. There are lots of resources on React testing out there, so I'll let you go out and find them if you need them.\n\n*******************************************************************************\n\n//IDEA: take the opportunity to use {% static %} tag in templates?\n"
  },
  {
    "path": "chapter_18_second_deploy.asciidoc",
    "content": "[[chapter_18_second_deploy]]\n== Deploying Our New Code\n\n(((\"deployment\", \"procedure for\", id=\"Dpro17\")))\nIt's time to deploy our brilliant new validation code to our live servers. This will be a chance to see our automated deploy scripts in action for the second time. Let's take the opportunity to make a little deployment checklist.\n\nNOTE: At this point I always want to say a huge thanks to Andrew Godwin\n    and the whole Django team.\n    In the first edition, I used to have a whole long section,\n    entirely devoted to migrations.\n    Since Django 1.7, migrations now \"just work\", so I was able to drop it altogether.\n    I mean yes this all happened nearly ten years ago,\n    but still--open source software is a gift.\n    We get such amazing things, entirely for free.\n    It's worth taking a moment to be grateful, now and again.\n\n=== The Deployment Checklist\n\nLet's make a little checklist of pre-deployment tasks:\n\n1. We run all our unit tests and functional tests (FTs) in the regular way—just in case!\n2. We rebuild our Docker image and run our tests against Docker on our local machine.\n3. We deploy to staging, and run our FTs against staging.\n4. Now we can deploy to prod.\n\n\nTIP: A deployment checklist like this should be a temporary measure.\n  Once you've worked through it manually a few times,\n  you should be looking to take the next step in automation:\n  continuous deployment straight to production using a CI/CD (continuous integration/continuous development) pipeline. We'll touch on this in <<chapter_25_CI>>.\n\n\n=== A Full Test Run Locally\n\nOf course, under the watchful eye of the Testing Goat,\nwe're running the tests all the time! But, just in case:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cd src && python manage.py test*\n[...]\n\nRan 37 tests in 15.222s\n\nOK\n----\n\n\n=== Quick Test Run Against Docker\n\nThe next step towards production is running things in Docker.(((\"Docker\", \"test run against\")))(((\"containers\", \"rebuilding Docker image and local container\")))\nThis was one of the main reasons we went to the trouble of containerising our app:\nto reproduce the production environment as faithfully as possible on our own machine.\n\n[role=\"pagebreak-before\"]\nSo let's rebuild our Docker image and spin up a local Docker container:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -it superlists\n => [internal] load build definition from Dockerfile                  0.0s\n => => transferring dockerfile: 371B                                  0.0s\n => [internal] load metadata for docker.io/library/python:3.14-slim   1.4s\n [...]\n => => naming to docker.io/library/superlists                         0.0s\n+ docker run -p 8888:8888 --mount\ntype=bind,source=\"$PWD/src/db.sqlite3\",target=/src/db.sqlite3 -e\nDJANGO_SECRET_KEY=sekrit -e DJANGO_ALLOWED_HOST=localhost -e EMAIL_PASSWORD -it\nsuperlists\n[2025-01-27 21:29:37 +0000] [7] [INFO] Starting gunicorn 22.0.0\n[2025-01-27 21:29:37 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7)\n[2025-01-27 21:29:37 +0000] [7] [INFO] Using worker: sync\n[2025-01-27 21:29:37 +0000] [8] [INFO] Booting worker with pid: 8\n----\n\nAnd now, in a separate terminal, we can run our FT suite against the Docker:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *TEST_SERVER=localhost:8888 python src/manage.py test functional_tests*\n[...]\n......\n ---------------------------------------------------------------------\nRan 6 tests in 17.047s\n\nOK\n----\n\nLooking good!  Let's move on to staging.(((\"staging server\", \"deployment to and test run\")))(((\"Ansible\", \"deployment to staging, playbook for\")))\n\n\n[role=\"pagebreak-before less_space\"]\n=== Staging Deploy and Test Run\n\n\nHere's our `ansible-playbook` command to deploy to staging:\n\n[role=\"against-server small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\n[...]\n\nPLAY [all] *********************************************************************\n\nTASK [Gathering Facts] *********************************************************\n[...]\nok: [staging.ottg.co.uk]\n\nTASK [Install docker] **********************************************************\nok: [staging.ottg.co.uk] => {\"cache_update_time\": [...]\n\nTASK [Add our user to the docker group, so we don't need sudo/become] **********\nok: [staging.ottg.co.uk] => {\"append\": true, \"changed\": false, [...]\n\nTASK [Reset ssh connection to allow the user/group change to take effect] ******\n\nTASK [Build container image locally] *******************************************\nchanged: [staging.ottg.co.uk -> 127.0.0.1] => {\"actions\": [\"Built image\n[...]\n\nTASK [Export container image locally] ******************************************\nchanged: [staging.ottg.co.uk -> 127.0.0.1] => {\"actions\": [\"Archived image [...]\n\nTASK [Upload image to server] **************************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"checksum\": [...]\n\nTASK [Import container image on server] ****************************************\nchanged: [staging.ottg.co.uk] => {\"actions\": [\"Loaded image superlists:latest\n[...]\n\nTASK [Ensure .secret-key file exists] ******************************************\nok: [staging.ottg.co.uk] => {\"changed\": false, \"dest\":\n[...]\n\nTASK [Read secret key back from file] ******************************************\nok: [staging.ottg.co.uk] => {\"changed\": false, \"content\": \n[...]\n\nTASK [Ensure db.sqlite3 file exists outside container] *************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"dest\": [...]\n\nTASK [Run container] ***********************************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"container\":\n[...]\n\nTASK [Run migration inside container] ******************************************\nchanged: [staging.ottg.co.uk] => {\"changed\": true, \"rc\": 0, \"stderr\": \"\",\n[...]\n\nPLAY RECAP *********************************************************************\nstaging.ottg.co.uk         : ok=12   changed=7    unreachable=0    failed=0\nskipped=0    rescued=0    ignored=0\n----\n\n\n\nNOTE: If your server is offline because you ran out of free credits with your provider,\n    you'll have to create a new one.  Skip back to <<chapter_11_server_prep>> if you need.\n\n\nAnd now we run the FTs against staging:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests*]\nOK\n----\n\n\n\nHooray!\n\n\n=== Production Deploy\n\nAs all is looking well, we can deploy to prod!(((\"production-ready deployment\")))\n\n\n[role=\"against-server small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i www.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\n----\n\n\n\n=== What to Do If You See a Database Error\n\nBecause our migrations introduce a new integrity constraint, you may find\nthat it fails to apply because some existing data violates that constraint.(((\"databases\", \"deployed database, integrity error\")))\nFor example, here's what you might see if any of the lists on the server\nalready contain duplicate items:\n\n[role=\"skipme\"]\n----\nsqlite3.IntegrityError: columns list_id, text are not unique\n----\n\n\nAt this point, you have two choices:\n\n1. Delete the database on the server and try again—after all, it's only a toy project!\n\n2. Create a data migration. You can find out more in the https://docs.djangoproject.com/en/5.2/topics/migrations/#data-migrations[Django migrations docs].\n\n\n==== How to Delete the Database on the Staging Server\n\nHere's how you(((\"databases\", \"deleting database on staging server\")))(((\"staging server\", \"deleting database on\"))) might do option 1:\n\n[role=\"skipme\"]\n----\nssh elspeth@staging.ottg.co.uk rm db.sqlite3\n----\n\nThe `ssh` command takes an arbitrary shell command to run as its last argument,\nso we pass in `rm db.sqlite3`.\nWe don't need a full path because we keep the SQLite database in our home folder.\n\nWARNING: Try not to accidentally delete your production database.\n\n\n\n=== Wrap-Up: git tag the New Release\n\n\nThe last thing to do is to tag the release(((\"Git\", \"tagging releases\"))) in our version control system (VCS)—it's important that\nwe're always able to keep track of what's live:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git tag -f LIVE*  # needs the -f because we are replacing the old tag\n$ *export TAG=`date +DEPLOYED-%F/%H%M`*\n$ *git tag $TAG*\n$ *git push -f origin LIVE $TAG*\n----\n\nNOTE: Some people don't like to use `push -f` and update an existing tag,\n    and will instead use some kind of version number to tag their releases.\n    Use whatever works for you.\n\nAnd on that note, we can wrap up the last of the concepts we discussed in <<part3>>, and move on to the more exciting topics that comprise <<part4>>. Can't wait!\n\n\n.Deployment Procedure Review\n*******************************************************************************\n\nWe've done a couple of deploys now, so this is a good time for a little recap:\n\n* Deploy to staging first.\n* Run our FTs against staging.\n* Deploy to live.\n* Tag the release.\n\nDeployment procedures evolve and get more complex as projects grow,\nand it's an area that can become hard to maintain—full of manual checks and procedures—if you're not careful to keep things automated.\nThere's lots more to learn about this, but it's out of scope for this book.\nDave Farley's\nhttps://oreil.ly/X2O_T[video on continuous delivery]\nis a good place to start.\n(((\"\", startref=\"Dpro17\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_19_spiking_custom_auth.asciidoc",
    "content": "[[chapter_19_spiking_custom_auth]]\n== User Authentication, Spiking, [keep-together]#and De-Spiking#\n\n(((\"authentication\", id=\"AuthSpike18\")))\nOur beautiful lists site has been live for a few days,\nand our users are starting to come back to us with feedback.\n\"We love the site\", they say, \"but we keep losing our lists.\nManually remembering URLs is hard.\nIt'd be great if it could remember what lists we'd started.\"\n\nRemember Henry Ford and faster horses. Whenever you hear a user requirement,\nit's important to dig a little deeper\nand think--what is the real requirement here?\nAnd how can I make it involve a cool new technology I've been wanting to try out?\n\nClearly the requirement here\nis that people want to have some kind of user account on the site.\nSo, without further ado, let's dive into authentication.\n\n(((\"passwords\")))\nNaturally we're not going to mess about\nwith remembering passwords ourselves--besides being _so_ '90s,\nsecure storage of user passwords is a security nightmare\nwe'd rather leave to someone else.\nWe'll use something fun called \"passwordless authentication\" instead.footnote:[If you _insist_ on storing your own passwords, Django's default authentication module is ready and waiting for you. It's nice and straightforward, and I'll leave it to you to discover on your own.]\n\n=== Passwordless Auth with \"Magic Links\"\n\n(((\"authentication\", \"passwordless\")))\n(((\"magic links\")))\n(((\"OAuth\")))\n(((\"Openid\")))\nWhat authentication system could we use to avoid storing passwords ourselves?\nOAuth?  OpenID?  \"Sign in with Facebook\"? Ugh.(((\"passwords\", \"passwordless authentication with magic links\")))\nFor me, those all have unacceptable creepy overtones;\nwhy should Google or Facebook know what sites you're logging in to and when?\n\nInstead, for the second edition,footnote:[\nIn the first edition, I used an experimental project called \"Persona\",\ncooked up by some of the wonderful techno-hippie-idealists at Mozilla,\nbut sadly that project was abandoned.]\nI found a fun approach to authentication\nthat now goes by the name of \"Magic Links\",\nbut you might call it \"just use email\".\n\nThe system was invented (or at least popularised) back in 2014(((\"emails\", \"using to verify identity\")))\nby someone annoyed at having to create new passwords for so many websites.\nThey found themselves just using random, throwaway passwords,\nnot even trying to remember them, and using the \"forgot my password\" feature\nwhenever they needed to log in again.\nYou can https://oreil.ly/je14i[read\nall about it on Medium].\n\nThe concept is:  just use email to verify someone's identity.\nIf you're going to have a \"forgot my password\" feature,\nthen you're trusting email anyway, so why not just go the whole hog?\nWhenever someone wants to log in,\nwe generate a unique URL for them to use, email it to them,\nand they then click through that to get into the site.\n\nIt's by no means a perfect system,\nand in fact there are lots of subtleties to be thought through\nbefore it would really make a good login solution for a production website,\nbut this is just a fun toy project so let's give it a go.\n\n\n=== A Somewhat Larger Spike\n\nNOTE: Reminder: a spike is a phase of exploratory coding,\n  where we can code without tests,\n  in order to explore a new tool or experiment with a new idea.\n  We will come back and redo the code \"properly\" with TDD later.(((\"spiking and de-spiking\", \"spiking magic links authentication\", id=\"ix_spkauth\")))\n\n(((\"django-allauth\")))\n(((\"python-social-auth\")))\nTo get this Magic Links project set up, the first thing I did was take a look at existing Python and Django authentication\npackages, like https://docs.allauth.org[django-allauth],\nbut both of them looked overcomplicated for this stage\n(and besides, it'll be more fun to code our own!).\n\n\nSo instead, I dived in and hacked about, and after a few dead ends and wrong turns,\nI had something that just about works.\nI'll take you on a tour,\nand then we'll go through and \"de-spike\" the implementation--that is,\nreplace the prototype with tested, production-ready code.\n\nYou should go ahead and add this code to your own site too,\nand then you can have a play with it.\nTry logging in with your own email address,\nand convince yourself that it really does work.\n\n\n\n==== Starting a Branch for the Spike\n\n(((\"spiking and de-spiking\", \"branching your VCS\")))\n(((\"Git\", \"creating branches\")))\nThis spike is going to be a bit more involved than the last one,\nso we'll be a little more rigorous with our version control. Before embarking on a spike it's a good idea to start a new branch,\nso you can still use your VCS without worrying about\nyour spike commits getting mixed up with your production code:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git switch -c passwordless-spike*\n----\n\nLet's keep track of some of the things we're hoping to learn from the\nspike:\n\n[role=\"scratchpad\"]\n*****\n* _How to send emails_\n* _Generating and recognising unique tokens_\n* _How to authenticate someone in Django_\n*****\n\n\n==== Frontend Login UI\n\n\n(((\"authentication\", \"frontend login UI\")))\nLet's start with the frontend by adding in\nan actual form to enter your email address into the navbar,\nalong with a logout link for users who are already authenticated:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch19l001)\n====\n[source,html]\n----\n  <body>\n    <div class=\"container\">\n\n      <div class=\"navbar\">\n        {% if user.is_authenticated %}\n          <p>Logged in as {{ user.email }}</p>\n          <form method=\"POST\" action=\"/accounts/logout\">\n            {% csrf_token %}\n            <button id=\"id_logout\" type=\"submit\">Log out</button>\n          </form>\n        {% else %}\n          <form method=\"POST\" action =\"accounts/send_login_email\">\n            Enter email to log in: <input name=\"email\" type=\"text\" />\n            {% csrf_token %}\n          </form>\n        {% endif %}\n      </div>\n\n      <div class=\"row justify-content-center p-5 bg-body-tertiary rounded-3\">\n      [...]\n----\n====\n\n\n==== Sending Emails from Django\n\n(((\"authentication\", \"sending emails from Django\", id=\"SDemail18\")))\n(((\"Django framework\", \"sending emails\", id=\"DFemail18\")))\n(((\"send_mail function\", id=\"sendmail18\")))\n(((\"emails\", \"sending from Django\", id=\"ix_emlDj\")))\nThe login will be something like <<magic-links-diagram>>.\n\n[[magic-links-diagram]]\n.Overview of the Magic Links login process\nimage::images/tdd3_1901.png[\"A diagram showing the interaction between user and server.  First the user fills in a login form and gives their email.  the server creates a token associated with that email, and then sends them an email containg a magic link, which includes the token.  the user waits for the email.  they click the link in the email, which goes to another endpoint on the server. it then checks if the token is valid, and if so, logs the user in.\"]\n\n1. When someone wants to log in, we generate a unique secret token for them,\n  link it to their email, store it in the database, and send it to them.\n\n2. The user then checks their email,\n  which will have a link for a URL that includes that token.\n\n3. When they click that link, we check whether the token exists in the database\n  and, if so, they are logged in as the associated user.\n\n// https://docs.djangoproject.com/en/5.2/topics/auth/customizing/\n\n\nFirst, let's prep an app for our accounts stuff:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cd src && python manage.py startapp accounts && cd ..*\n$ *ls src/accounts*\n__init__.py admin.py    apps.py     migrations  models.py   tests.py    views.py\n----\n\n// DAVID: Worth discussing why you chose to make this an app?\n\nAnd we'll wire up _urls.py_ with at least one URL.\nIn the top-level _superlists/urls.py_...\n\n[role=\"sourcecode\"]\n.src/superlists/urls.py (ch19l003)\n====\n[source,python]\n----\nfrom django.urls import include, path\nfrom lists import views as list_views\n\nurlpatterns = [\n    path(\"\", list_views.home_page, name=\"home\"),\n    path(\"lists/\", include(\"lists.urls\")),\n    path(\"accounts/\", include(\"accounts.urls\")),\n]\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd we give the accounts module its own _urls.py_:\n\n\n[role=\"sourcecode\"]\n.src/accounts/urls.py (ch19l004)\n====\n[source,python]\n----\nfrom django.urls import path\n\nfrom accounts import views\n\nurlpatterns = [\n    path(\"send_login_email\", views.send_login_email, name=\"send_login_email\"),\n]\n----\n====\n\nHere's the view that's in charge of(((\"tokens\", \"creating, view for\"))) creating a token\nassociated with the email address that the user puts in our login form:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch19l005)\n====\n[source,python]\n----\nimport sys\nimport uuid\n\nfrom django.core.mail import send_mail\nfrom django.shortcuts import render\n\nfrom accounts.models import Token\n\n\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    uid = str(uuid.uuid4())\n    Token.objects.create(email=email, uid=uid)\n    print(\"saving uid\", uid, \"for email\", email, file=sys.stderr)\n    url = request.build_absolute_uri(f\"/accounts/login?uid={uid}\")\n    send_mail(\n        \"Your login link for Superlists\",\n        f\"Use this link to log in:\\n\\n{url}\",\n        \"noreply@superlists\",\n        [email],\n    )\n    return render(request, \"login_email_sent.html\")\n----\n====\n\n\nFor that to work, we'll need(((\"templates\", \"messaging confirming login email sent\"))) a template with a placeholder message confirming the email was\nsent:\n\n[role=\"sourcecode\"]\n.src/accounts/templates/login_email_sent.html (ch19l006)\n====\n[source,html]\n----\n<html>\n<h1>Email sent</h1>\n\n<p>Check your email, you'll find a message with a link that will log you into\nthe site.</p>\n\n</html>\n----\n====\n\n(You can see how hacky this code is--we'd want to integrate this template\nwith our 'base.html' in the real version.)\n\n==== Email Server Config for Django\n\nThe https://docs.djangoproject.com/en/5.2/topics/email[django docs on email]\nexplain how `send_mail()` works, as well as how you configure it\nby telling Django what email server to use,\nand how to authenticate with it.\nHere, I'm just using my Gmailfootnote:[\nDidn't I just spend a whole intro banging on about the privacy implications\nof using Google for login, only to go on and use Gmail?\nYes, it's a contradiction (honest, I will move off Gmail one day!).(((\"SMTP (Simple Mail Transfer Protocol)\")))\nBut in this case I'm just using it for testing,\nand the important thing is that I'm not forcing Google on my users.]\naccount for now—but you can use any email provider you like, as long as they support SMTP (Simple Mail Transfer Protocol):\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch19l007)\n====\n[source,python]\n----\nEMAIL_HOST = \"smtp.gmail.com\"\nEMAIL_HOST_USER = \"obeythetestinggoat@gmail.com\"\nEMAIL_HOST_PASSWORD = os.environ.get(\"EMAIL_PASSWORD\")\nEMAIL_PORT = 587\nEMAIL_USE_TLS = True\n----\n====\n\nTIP: If you want to use Gmail as well,\n    you'll probably have to visit your Google account security settings page.\n    If you're using two-factor authentication, you'll want to set up an\n    https://myaccount.google.com/apppasswords[app-specific password].\n    If you're not, you will probably still need to\n    https://www.google.com/settings/security/lesssecureapps[allow access for less secure apps].\n    You might want to consider creating a new Google account for this purpose,\n    rather than using one containing sensitive data.\n    (((\"Gmail\")))\n(((\"emails\", \"sending from Django\", startref=\"ix_emlDj\")))\n(((\"\", startref=\"sendmail18\")))\n(((\"\", startref=\"DFemail18\")))\n(((\"\", startref=\"SDemail18\")))\n\n\n==== Another Secret, Another Environment Variable\n\n(((\"authentication\", \"avoiding secrets in source code\")))\n(((\"environment variables\")))(((\"secrets\", \"storing in environment variables\")))\nOnce again, we have a \"secret\"\nthat we want to avoid keeping directly in our source code or on GitHub,\nso another environment variable is used in the `os.environ.get`. To get this to work,\nwe need to set it in the shell that's running my dev server:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *export EMAIL_PASSWORD=\"ur-email-server-password-here\"*\n----\n\nLater, we'll see about adding that to the env file\non the staging server as well.\n\n[role=\"pagebreak-before less_space\"]\n==== Storing Tokens in the Database\n\n// CSANAD (transcribed) you should probably hash the tokens\n\n(((\"authentication\", \"storing tokens in databases\")))\n(((\"tokens\", \"storing in the database\")))\nHow are we doing? Let's review where we're at in the process:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#How to send emails#_\n* _Generating and recognising unique tokens_\n* _How to authenticate someone in Django_\n*****\n\n// DAVID: In practice would we really cross something off the list like this before giving it a try?\n// Might be better to gradually build things up, e.g. write a function to send an email (and check it works).\n// Could even use as an excuse to introduce manage.py shell and do it from there?\n// Equally with the user interface stuff, maybe starting up the application and having a look at what it looks like?\n// Or maybe start with the model and then layer things on top of that.\n\nWe'll need a model to store our tokens in the database--they\nlink an email address with a unique ID.\nIt's pretty simple:\n\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l008)\n====\n[source,python]\n----\nfrom django.db import models\n\n\nclass Token(models.Model):\n    email = models.EmailField()\n    uid = models.CharField(max_length=255) <1>\n----\n====\n\n<1> Django does have a specific UID (universally unique identifier) fields type for many databases,\nbut I just want to keep things simple for now.\n\nThe point of this spike is about authentication and emails,\nnot optimising database storage.\nWe've got enough things we need to learn as it is!\n\n\nLet's switch on our new accounts app in _settings.py_:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch19l008-1)\n====\n[source,python]\n----\nINSTALLED_APPS = [\n    # \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"lists\",\n    \"accounts\",\n]\n----\n====\n\n[role=\"pagebreak-before\"]\nWe can then do a quick (((\"database migrations\", \"adding token model to database\")))migrations dance to add the token model to the database:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0001_initial.py\n    + Create model Token\n$ pass:quotes[*python src/manage.py migrate*]\nOperations to perform:\n  Apply all migrations: accounts, auth, contenttypes, lists, sessions\nRunning migrations:\n  Applying accounts.0001_initial... OK\n----\n//ch19l008-2\n\n\nAnd at this point, if you actually try the email form in your browser,\nyou'll see it really does send an actual real email—to your real email address hopefully (best not spam someone else now!). See Figures <<spike-email-sent, 19-2>> and <<spike-email-received, 19-3>>.\n\n\n[[spike-email-sent]]\n.Looks like we might have sent an email\nimage::images/tdd3_1902.png[\"The email sent confirmation page, indicating the server at least thinks it sent an email successfully\"]\n\n[[spike-email-received]]\n.Yep, looks like we received it\nimage::images/tdd3_1903.png[\"Screenshot of my email client showing the email from the server, saying 'your login link for superlist' and including a token url\"]\n\n[role=\"pagebreak-before less_space\"]\n==== Custom Authentication Models\n\n(((\"authentication\", \"custom authentication models\")))\nOK, so we've done the first half of \"Generating and recognising unique tokens\":\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#How to send emails#'\n* '[strikethrough line-through]#Generating# and recognising unique tokens'\n* 'How to authenticate someone in Django'\n*****\n\nBut, before we can move on to recognising them and making the login work end-to-end though,\nwe need to explore Django's authentication system.(((\"Django framework\", \"authentication system\")))(((\"user models\", \"Django authentication user model\"))) The first thing we'll need is a user model.\nI took a dive into the https://docs.djangoproject.com/en/5.2/topics/auth/customizing[Django\nauth documentation] and tried to hack in the simplest possible one:\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l009)\n====\n[source,python]\n----\nfrom django.contrib.auth.models import (\n    AbstractBaseUser,\n    BaseUserManager,\n)\n[...]\n\n\nclass ListUser(AbstractBaseUser):\n    email = models.EmailField(primary_key=True)\n    USERNAME_FIELD = \"email\"\n    # REQUIRED_FIELDS = ['email', 'height']\n\n    objects = ListUserManager()\n\n    @property\n    def is_staff(self):\n        return self.email == \"harry.percival@example.com\"\n\n    @property\n    def is_active(self):\n        return True\n----\n====\n// DAVID: Maybe better include the ListUserManager() here too? Or leave it out until we create it?\n\n\n[role=\"pagebreak-before\"]\nThat's what I call a minimal user model!\nOne field, none of this first name/last name/username nonsense,\nand—pointedly—no password! That's somebody else's problem!\n\nBut, again, you can see that this code isn't ready for production—from the commented-out lines to the hardcoded Harry email address.\nWe'll neaten this up quite a lot when we de-spike.\n\n\nTo get it to work, I needed to add a model manager for the user,\nfor some reason:\n\n[role=\"sourcecode small-code\"]\n.src/accounts/models.py (ch19l010)\n====\n[source,python]\n----\n[...]\nclass ListUserManager(BaseUserManager):\n    def create_user(self, email):\n        ListUser.objects.create(email=email)\n\n    def create_superuser(self, email, password):\n        self.create_user(email)\n----\n====\n\n// CSANAD: ListUserManager has to be defined before ListUser, since its\n// reference to ListUser isn't evaluated until `create_user` is called. This is\n// not the case the other way around, ListUser's reference to ListUserManager\n// is instantiated in the class definition. Maybe we could leave a note about\n// this?\n\n\nNo need to worry about what a model manager is at this stage;\nfor now, we just need it because we need it, and it works.\nWhen we de-spike, we'll examine each bit of code that actually ends up in production\nand make sure we understand it fully.\n\nWe'll need to run `makemigrations` and `migrate` again to make the user model real:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0002_listuser.py\n    + Create model ListUser\n$ pass:quotes[*python src/manage.py migrate*]\n[...]\nRunning migrations:\n  Applying accounts.0002_listuser... OK\n----\n//ch19l009-1\n\n\n==== Finishing the Custom Django Auth\n\nLet's review our scratchpad:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#How to send emails#_\n* _[strikethrough line-through]#Generating# and recognising unique tokens_\n* _How to authenticate someone in Django_\n*****\n\n[role=\"pagebreak-before\"]\n(((\"Django framework\", \"custom authentication system\", id=\"ix_Djcusauth\")))(((\"authentication\", \"custom Django authentication\", id=\"SDcustom18\")))\nHmm, we can't quite cross off anything yet.\nTurns out the steps we _thought_ we'd go through\naren't quite the same as the steps we're _actually_ going through\n(this is not uncommon, as I'm sure you know).\n// CSANAD: I find it vague like this. Maybe it would be helpful to clarify what\n// it is that \"we are actually going through\" and what it was that\n// \"we thought we'd go through\".\n\nStill, we're almost there--our last step will combine recognising the token\nand then actually logging the user in.\nOnce we've done this,\nwe'll be able to pretty much strike off all the items on our scratchpad.\n\n\nSo here's the view that actually handles the click-through from the link in the\nemail:\n\n[role=\"sourcecode small-code\"]\n.src/accounts/views.py (ch19l011)\n====\n[source,python]\n----\nimport sys\nimport uuid\n\nfrom django.contrib.auth import authenticate\nfrom django.contrib.auth import login as auth_login\nfrom django.core.mail import send_mail\nfrom django.shortcuts import redirect, render\n\nfrom accounts.models import Token\n\n\ndef send_login_email(request):\n    [...]\n\n\ndef login(request):\n    print(\"login view\", file=sys.stderr)\n    uid = request.GET.get(\"uid\")\n    user = authenticate(request, uid=uid)\n    if user is not None:\n        auth_login(request, user)\n    return redirect(\"/\")\n----\n====\n\nThe `authenticate()` function invokes Django's authentication framework,\nwhich we configure using a custom \"authentication backend,\"\nwhose job it is to validate the UID (unique identifier) and return a user with the right email.\n\n[role=\"pagebreak-before\"]\nWe could have done this stuff directly in the view,\nbut we may as well structure things the way Django expects.\nIt makes for a reasonably neat separation of concerns:\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch19l012)\n====\n[source,python]\n----\nimport sys\n\nfrom accounts.models import ListUser, Token\n\nfrom django.contrib.auth.backends import BaseBackend\n\n\nclass PasswordlessAuthenticationBackend(BaseBackend):\n    def authenticate(self, request, uid):\n        print(\"uid\", uid, file=sys.stderr)\n        if not Token.objects.filter(uid=uid).exists():\n            print(\"no token found\", file=sys.stderr)\n            return None\n        token = Token.objects.get(uid=uid)\n        print(\"got token\", file=sys.stderr)\n        try:\n            user = ListUser.objects.get(email=token.email)\n            print(\"got user\", file=sys.stderr)\n            return user\n        except ListUser.DoesNotExist:\n            print(\"new user\", file=sys.stderr)\n            return ListUser.objects.create(email=token.email)\n\n    def get_user(self, email):\n        return ListUser.objects.get(email=email)\n----\n====\n\n\nAgain, lots of debug prints in there, and some duplicated code—not something we'd want in production, but it works...as long as we add it to _settings.py_ (it doesn't matter where):\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch19l012-1)\n====\n[source,python]\n----\nAUTH_USER_MODEL = \"accounts.ListUser\"\nAUTHENTICATION_BACKENDS = [\n    \"accounts.authentication.PasswordlessAuthenticationBackend\",\n]\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd finally, a logout view:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch19l013)\n====\n[source,python]\n----\nfrom django.contrib.auth import authenticate\nfrom django.contrib.auth import login as auth_login\nfrom django.contrib.auth import logout as auth_logout\n[...]\n\n\ndef logout(request):\n    auth_logout(request)\n    return redirect(\"/\")\n----\n====\n\n\nAdd login and logout to our _urls.py_...\n\n[role=\"sourcecode\"]\n.src/accounts/urls.py (ch19l014)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"send_login_email\", views.send_login_email, name=\"send_login_email\"),\n    path(\"login\", views.login, name=\"login\"),\n    path(\"logout\", views.logout, name=\"logout\"),\n]\n----\n====\n\n\n\nAnd we should be all done! Spin up a dev server with `runserver` and try it--believe it or not, it _actually_ works (see <<spike-login-worked>>).\n\n[[spike-login-worked]]\n.It works! It works!\nimage::images/tdd3_1904.png[\"screenshot of several windows including gmail and terminals but in the foreground our site showing us as being logged in.\"]\n\nTIP: If you get an `SMTPSenderRefused` error message, don't forget to set\n    the `EMAIL_PASSWORD` environment variable in the shell that's running\n    `runserver`.\n    (((\"SMTPSenderRefused error message\")))Also, if you see a message saying \"Application-specific password required\",\n    that's a Gmail security policy.  Follow the link in the error message.\n\nThat's pretty much it!\nAlong the way, I had to fight pretty hard,\nincluding clicking around the Gmail account security UI for a while,\nstumbling over several missing attributes on my custom user model\n(because I didn't read the docs properly),\nand even at one point switching to the dev version of Django to overcome a bug,\nwhich thankfully turned out to be a red herring.\n(((\"Django framework\", \"custom authentication system\", startref=\"ix_Djcusauth\")))(((\"\", startref=\"SDcustom18\")))\n\n\nBut we now have a working solution!  Let's commit it on our spike branch:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n$ *git add src/accounts*\n$ *git commit -am \"spiked in custom passwordless auth backend\"*\n----\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#How to send emails#_\n* _[strikethrough line-through]#Generating and recognising unique tokens#_\n* _[strikethrough line-through]#_How to authenticate someone in Django#_\n*****\n\n\n\nTime to de-spike!\n\n[role=\"pagebreak-before less_space\"]\n=== De-spiking\n\n(((\"spiking and de-spiking\", \"spiking magic links authentication\", startref=\"ix_spkauth\")))(((\"spiking and de-spiking\", \"de-spiking authentication code\", id=\"SDde18\")))(((\"authentication\", \"de-spiking authentication code\", id=\"ix_authdes\")))\nDe-spiking means rewriting your prototype code using TDD. In this section, we’ll work through how to do that in a safe and methodical way. We’ll take the knowledge we’ve acquired during the spiking process—whether that’s in our heads, in our notes, or in our branch in Git—and apply it as we re-implement gradually in a test-first way. And the hope is that our code will turn out a bit nicer the second time around!\n\n==== Making a Plan\n\nWhile it's fresh in our minds,\nlet's make a few notes based on what we've learned\nabout what we know we're probably going to need to build during our de-spike:\n\n[role=\"scratchpad\"]\n*****\n* _Token model with email and UID_\n* _View to create token and send login email incl. url w/ token UID_\n* _Custom user model with USERNAME_FIELD=email_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\n==== Wring an FT Against the Spiked Code\n\nWe now have enough information to \"do it properly\".\nSo, what's the first step?  An FT, of course! We'll stay on the spike branch for now\nto see our FT pass against our spiked code.\nThen we'll go back to our main branch and commit just the FT.\n\n[role=\"pagebreak-before\"]\nHere's a first, simple version of the FT:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/test_login.py (ch19l018)\n====\n[source,python]\n----\nimport re\n\nfrom django.core import mail\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\nfrom .base import FunctionalTest\n\nTEST_EMAIL = \"edith@example.com\"  # <1>\nSUBJECT = \"Your login link for Superlists\"\n\n\nclass LoginTest(FunctionalTest):\n    def test_login_using_magic_link(self):\n        # Edith goes to the awesome superlists site\n        # and notices a \"Log in\" section in the navbar for the first time\n        # It's telling her to enter her email address, so she does\n        self.browser.get(self.live_server_url)\n        self.browser.find_element(By.CSS_SELECTOR, \"input[name=email]\").send_keys(\n            TEST_EMAIL, Keys.ENTER\n        )\n\n        # A message appears telling her an email has been sent\n        self.wait_for(\n            lambda: self.assertIn(\n                \"Check your email\",\n                self.browser.find_element(By.CSS_SELECTOR, \"body\").text,\n            )\n        )\n\n        # She checks her email and finds a message\n        email = mail.outbox.pop()  # <2>\n        self.assertIn(TEST_EMAIL, email.to)\n        self.assertEqual(email.subject, SUBJECT)\n\n        # It has a URL link in it\n        self.assertIn(\"Use this link to log in\", email.body)\n        url_search = re.search(r\"http://.+/.+$\", email.body)\n        if not url_search:\n            self.fail(f\"Could not find url in email body:\\n{email.body}\")\n        url = url_search.group(0)\n        self.assertIn(self.live_server_url, url)\n\n        # she clicks it\n        self.browser.get(url)\n\n        # she is logged in!\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\"),\n        )\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertIn(TEST_EMAIL, navbar.text)\n----\n====\n\n<1> Whenever you're testing against something that can send real emails,\n    you don't want to use a real address.\n    It's best practice to use a special domain like `@example.com`,\n    which has been reserved for exactly this sort of thing,\n    to avoid accidentally spamming anyone!\n\n<2> Were you worried about how we were going to handle retrieving emails\n    in our tests?\n    Thankfully, we can cheat for now!\n    When running tests, Django gives us access to any emails that\n    the server tries to send via the `mail.outbox` attribute.\n    We'll discuss checking \"real\" emails in <<chapter_23_debugging_prod>>.\n\n\nAnd if we run the FT, it works!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nNot Found: /favicon.ico\nsaving uid [...]\nlogin view\nuid [...]\ngot token\nnew user\n\n.\n ---------------------------------------------------------------------\nRan 1 test in 2.729s\n\nOK\n----\n\nYou can even see some of the debug output I left in my spiked view implementations.\nNow it's time to revert all of our temporary changes,\nand reintroduce them one by one in a test-driven way.\n\n\n==== Reverting Our Spiked Code\n\nWe can revert our spike using our version (((\"Git\", \"reverting spiked code\")))control system:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git switch main* # switch back to main branch\n$ *rm -rf src/accounts* # remove any trace of spiked code\n$ *git add src/functional_tests/test_login.py*\n$ *git commit -m \"FT for login via email\"*\n----\n\nNow we rerun the FT and let it be the main driver of our development,\nreferring back to our scratchpad from time to time when we need to:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: input[name=email]; [...]\n[...]\n----\n\nTIP: If you see an exception saying \"No module named accounts\",\n    you may have missed a step in the de-spiking process—maybe a commit or the change of branch.\n\n\nThe first thing it wants us to do is add an email input element.\nBootstrap has some built-in classes for navigation bars,\nso we'll use them, and include a form for the login email:footnote:[\nWe are now introducing a conceptual dependency from the base template\nto the `accounts` app because its URL is in the form.\nI didn't want to spend time on this in the book,\nbut this might be a good time to consider moving the template\nout of _lists/templates_ and into _superlists/templates_.\nBy convention, that's the place for templates\nwhose scope is wider than a single app.]\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch19l020)\n====\n[source,html]\n----\n<body>\n  <div class=\"container\">\n\n    <nav class=\"navbar\">\n      <div class=\"container-fluid\">\n        <a class=\"navbar-brand\" href=\"/\">Superlists</a>\n        <form method=\"POST\" action=\"/accounts/send_login_email\">\n          <div class=\"input-group\">\n            <label class=\"navbar-text me-2\" for=\"id_email_input\">\n              Enter your email to log in\n            </label>\n            <input\n              id=\"id_email_input\"\n              name=\"email\"\n              class=\"form-control\"\n              placeholder=\"your@email.com\"\n            />\n            {% csrf_token %}\n          </div>\n        </form>\n      </div>\n    </nav>\n\n\n    <div class=\"row justify-content-center p-5 bg-body-tertiary rounded-3\">\n      <div class=\"col-lg-6 text-center\">\n        <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n        [...]\n----\n====\n\n\n[role=\"pagebreak-before\"]\nAt this point, you'll find that the unit tests start to fail:\n\n----\nERROR: test_renders_input_form\n[...]\n    [form] = parsed.cssselect(\"form[method=POST]\")\n    ^^^^^^\nValueError: too many values to unpack (expected 1, got 2)\n\n\nERROR: test_renders_input_form\n    [form] = parsed.cssselect(\"form[method=POST]\")\n    ^^^^^^\nValueError: too many values to unpack (expected 1, got 2)\n----\n\nIt's because these unit tests had a hard assumption\nthat there's only one POST form on the page. Let's change them to be more resilient.\nHere's how you might change the first one:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/tests/test_views.py (ch19l020-1)\n====\n[source,python]\n----\n    def test_renders_input_form(self):\n        response = self.client.get(\"/\")\n        parsed = lxml.html.fromstring(response.content)\n        forms = parsed.cssselect(\"form[method=POST]\")  # <1>\n        self.assertIn(\"/lists/new\", [form.get(\"action\") for form in forms])  # <2>\n        [form] = [form for form in forms if form.get(\"action\") == \"/lists/new\"]  # <3>\n        inputs = form.cssselect(\"input\")  # <4>\n        self.assertIn(\"text\", [input.get(\"name\") for input in inputs])  # <4>\n----\n====\n\n<1> We get all forms, rather than using the clever `[form] =` syntax.\n\n<2> We check that at least _one_ of the forms has the right `action=` URL.\n    I'm using `assertIn()`, so we get a nice error message. If we can't find the right URL,\n    we'll see the list of URLs that _do_ exist on the page.\n\n<3> Now we can feel free to go back to unpacking,\n    and get the right form, based on its `action` attribute.\n\n<4> The rest of the test is as before.\n\n[role=\"pagebreak-before\"]\nHere's a similar set of changes in the second test:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch19l020-2)\n====\n[source,diff]\n----\n@@ -65,10 +65,12 @@ class ListViewTest(TestCase):\n\n     def test_renders_input_form(self):\n         mylist = List.objects.create()\n-        response = self.client.get(f\"/lists/{mylist.id}/\")\n+        url = f\"/lists/{mylist.id}/\"\n+        response = self.client.get(url)\n         parsed = lxml.html.fromstring(response.content)\n-        [form] = parsed.cssselect(\"form[method=POST]\")\n-        self.assertEqual(form.get(\"action\"), f\"/lists/{mylist.id}/\")\n+        forms = parsed.cssselect(\"form[method=POST]\")\n+        self.assertIn(url, [form.get(\"action\") for form in forms])\n+        [form] = [form for form in forms if form.get(\"action\") == url]\n         inputs = form.cssselect(\"input\")\n         self.assertIn(\"text\", [input.get(\"name\") for input in inputs])\n\n----\n====\n\nIt's pretty much the same edit,\nexcept this time I decided to have a `url` variable,\nto remove the duplication of using `/lists/{mylist.id}/` three times. That gets our unit tests passing again:\n\n----\nOK\n----\n\n\nIf we try our FT again, we'll see it fails because the login form\ndoesn't send us to a real URL yet--you'll\nsee the `Not found:` message in the server output,\nas well as the assertion reporting the content of the default 404 page:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nNot Found: /accounts/send_login_email\n[...]\nAssertionError: 'Check your email' not found in 'Not Found\\nThe requested\nresource was not found on this server.'\n----\n\nTime to start writing some Django code.\nWe begin, like in the spike, by creating an app called `accounts`\nto hold all the files related to login:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cd src && python manage.py startapp accounts && cd ..*\n$ *ls src/accounts*\n__init__.py admin.py    apps.py     migrations  models.py   tests.py    views.py\n----\n//ch19l021\n\nYou could even do a commit just for that, to be able to distinguish the\nplaceholder app files from our modifications.\n(((\"authentication\", \"de-spiking authentication code\", startref=\"ix_authdes\")))(((\"spiking and de-spiking\", \"de-spiking authentication code\", startref=\"ix_spikdeauth\")))\n\n\n=== A Minimal Custom User Model\n\n// IDEA: consider starting with a test for the login view instead.\n\n(((\"user models\", \"minimum custom user model for authentication\", id=\"ix_usrmdcus\")))(((\"authentication\", \"minimal custom user model\", id=\"SDminimal18\")))\nLet's turn to the models layer:footnote:[\nIn this chapter, we're building things in a \"bottom-up\" way,\nstarting with the models, and then building the layers on top—the views and templates that depend on them.\nThis is a common approach, but it's not the only one!\nIn <<chapter_24_outside_in>> we'll explore building software from the outside in,\nwhich has all sorts of advantages too.]\n\n[role=\"scratchpad\"]\n*****\n* _Token model with email and UID_\n* _View to create token and send login email incl. url w/ token UID_\n* _Custom user model with USERNAME_FIELD=email_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\nWe know we have to build a token model and a custom user model,\nand the user model was the messiest part in our spike.\nSo, let's have a go at redoing that test-first, to see if it comes out nicer.\n\nDjango's built-in user model makes all sorts of assumptions about\nwhat information you want to track about users—from explicitly requiring a first name and last namefootnote:[\nThis is a decision that even some prominent Django maintainers\nhave said they now regret—not everyone has a first and last name.]\nto forcing you to use a username.\nI'm a great believer in not storing information about users\nunless you absolutely must,\nso a user model that records an email address and nothing else\nsounds good to me!\n\nLet's start straight away with a tests folder instead of _tests.py_\nin this app:\n\n[subs=\"\"]\n----\n$ <strong>rm src/accounts/tests.py</strong>\n$ <strong>mkdir src/accounts/tests</strong>\n$ <strong>touch src/accounts/tests/__init__.py</strong>\n----\n\nAnd now, let's add a _test_models.py_ to say:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_models.py (ch19l023)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\nfrom accounts.models import User\n\n\nclass UserModelTest(TestCase):\n    def test_user_is_valid_with_email_only(self):\n        user = User(email=\"a@b.com\")\n        user.full_clean()  # should not raise\n----\n====\n\n\nThat gives us the expected failure:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nImportError: cannot import name 'User' from 'accounts.models'\n(...goat-book/src/accounts/models.py)\n----\n\n\nOK, let's try the absolute minimum then:\n\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l024)\n====\n[source,python]\n----\nfrom django.db import models\n\n\nclass User(models.Model):\n    email = models.EmailField()\n----\n====\n\nThat gives us an error because Django won't recognise models\nunless they're in `INSTALLED_APPS`:\n\n[subs=\"specialcharacters,macros\"]\n----\nRuntimeError: Model class accounts.models.User doesn't declare an explicit\napp_label and isn't in an application in INSTALLED_APPS.\n----\n\nSo, let's add it to _settings.py_:\n\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch19l025)\n====\n[source,python]\n----\nINSTALLED_APPS = [\n    # \"django.contrib.admin\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.staticfiles\",\n    \"accounts\",\n    \"lists\",\n]\n\n----\n====\n\n\nAnd that gets our tests passing!\n\n\n----\nOK\n----\n\nNow let's see if we've built a user model that Django can actually work with.\nThere's a built-in function in `django.contrib.auth` called `get_user_model()`—which retrieves the currently active user model and, as we'll see, also performs some checks on it.\nLet's use it in our tests:\n\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_models.py (ch19l026-1)\n====\n[source,python]\n----\nfrom django.contrib import auth\nfrom django.test import TestCase\n\nfrom accounts.models import User\n\n\nclass UserModelTest(TestCase):\n    def test_model_is_configured_for_django_auth(self):\n        self.assertEqual(auth.get_user_model(), User)\n\n    def test_user_is_valid_with_email_only(self):\n        [...]\n----\n====\n\nThat gives:\n\n----\nAssertionError: <class 'django.contrib.auth.models.User'> != <class\n'accounts.models.User'>\n----\n\n\nOK, so let's try wiring up our model inside _settings.py_,\nin a variable called `AUTH_USER_MODEL`:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch19l026-2)\n====\n[source,python]\n----\nAUTH_USER_MODEL = \"accounts.User\"\n----\n====\n\n\nNow when we run our tests, Django complains\nthat our custom user model is missing a couple of bits of metadata.\nIn fact, it's so unhappy that it won't even run the tests:\n\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\nTraceback (most recent call last):\n[...]\n  File \".../django/contrib/auth/checks.py\", line 46, in check_user_model\n    if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):\n                      ^^^^^^^^^^^^^^^^^^^\nAttributeError: type object 'User' has no attribute 'REQUIRED_FIELDS'\n----\n\n\nSigh.  Come on, Django; it's only got one field,\nso you should be able to figure out the answers to these questions for yourself.\n\n[role=\"pagebreak-before\"]\nHere you go:\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l027)\n====\n[source,python]\n----\nclass User(models.Model):\n    email = models.EmailField()\n\n    REQUIRED_FIELDS = []\n----\n====\n\nNext silly question?footnote:[\nYou might ask, if I think Django is so silly,\nwhy don't I submit a pull request to fix it?\nIt should be quite a simple fix.\nWell, I promise I will, as soon as I've finished updating the book.\nFor now, snarky comments will have to suffice.]\n\n[subs=\"specialcharacters,macros\"]\n----\nAttributeError: type object 'User' has no attribute 'USERNAME_FIELD'\n----\n\nWe'll go through a few more of these, until we get to:\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l029)\n====\n[source,python]\n----\nclass User(models.Model):\n    email = models.EmailField()\n\n    REQUIRED_FIELDS = []\n    USERNAME_FIELD = \"email\"\n    is_anonymous = False\n    is_authenticated = True\n----\n====\n\n\nAnd now we get a slightly different error:\n\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nSystemCheckError: System check identified some issues:\n\nERRORS:\naccounts.User: (auth.E003) 'User.email' must be unique because it is named as\nthe 'USERNAME_FIELD'.\n----\n\nWell, the simple way to fix that would be like this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l030)\n====\n[source,python]\n----\n    email = models.EmailField(unique=True)\n----\n====\n\nAnd now we get a different error again, slightly more familiar this time!\nDjango is a bit happier with the structure of our custom user model,\nbut it's unhappy about the database:\n\n----\ndjango.db.utils.OperationalError: no such table: accounts_user\n----\n\n[role=\"pagebreak-before\"]\nIn other words, we need to create a migration:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0001_initial.py\n    + Create model User\n----\n//ch19l031\n\n\nAnd our tests pass:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\nRan 2 tests in 0.001s\nOK\n----\n\n\nBut our model isn't quite as simple as it could be.\nIt has the email field, and also an autogenerated \"ID\" field as its primary key.\nWe could make it even simpler!\n// DAVID: Maybe spell this out more clearly to the reader that there are actually two fields,\n// they might not realise this.\n\n\n==== Tests as Documentation\n\n\n(((\"tests\", \"as documentation\", secondary-sortas=\"documentation\")))\n(((\"documentation, tests as\")))\nLet's go all the way and make the email field the primary key,footnote:[\nEmails may not be the perfect primary key in real life.\nOne reader—clearly deeply scarred—wrote me an emotional email about how much they've suffered for over a decade\nfrom trying to deal with the consquences of using email as a primary key,\nparticularly how it makes multiuser account management nearly impossible.\nSo, as ever, YMMV.]\nand thus implicitly remove the autogenerated `id` column. Although we could just _do it_ and our test would still pass,\nand conceivably claim it was \"just a refactor\",\nit would be better to have a specific test:\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_models.py (ch19l032)\n====\n[source,python]\n----\nclass UserModelTest(TestCase):\n    def test_model_is_configured_for_django_auth(self):\n        [...]\n    def test_user_is_valid_with_email_only(self):\n        [...]\n\n    def test_email_is_primary_key(self):\n        user = User(email=\"a@b.com\")\n        self.assertEqual(user.pk, \"a@b.com\")\n----\n====\n\nIt'll help us remember if we ever come back and look at the code again\nin future:\n\n----\n    self.assertEqual(user.pk, \"a@b.com\")\nAssertionError: None != 'a@b.com'\n----\n\nTIP: Your tests can be a form of documentation for your code--they\n    express the requirements for a particular class or function.\n    Sometimes, if you forget why you've done something a particular way,\n    going back and looking at the tests will give you the answer.\n    That's why it's important to make your tests readable,\n    including giving them explicit, verbose method names.\n\nHere's the implementation (`primary_key` makes the `unique=True` obsolete):\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l033)\n====\n[source,python]\n----\n    email = models.EmailField(primary_key=True)\n----\n====\n\n\nAnd we mustn't forget to adjust our migrations:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*rm src/accounts/migrations/0001_initial.py*]\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0001_initial.py\n    + Create model User\n----\n//ch19l034\n\n// DAVID: Deleting migrations can get readers in a pickle if they have already run migrations locally.\n// Might be worth saying we're only doing this because we've just created it, and advise them to delete\n// their database if they happen to have run the migration they've just deleted? (Or you can get them\n// to run `migrate accounts zero` I think.)\n\n(((\"user models\", \"minimum custom user model for authentication\", startref=\"ix_usrmdcus\")))(((\"\", startref=\"SDminimal18\")))\nNow both our tests pass:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nRan 3 tests in 0.001s\nOK\n----\n\nIt's probably a good time for a commit, too:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/accounts*\n$ *git commit -m \"custom user model with email as primary key\"*\n----\n\nAnd we can cross off one item from our de-spiking list.  Hooray!\n\n[role=\"scratchpad\"]\n*****\n* _Token model with email and UID_\n* _View to create token and send login email incl. url w/ token UID_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\n=== A Token Model to Link Emails with a Unique ID\n\n(((\"emails\", \"token model linking with unique ID\", id=\"ix_emltkn\")))(((\"authentication\", \"token model to link emails\", id=\"SDtoken18\")))(((\"tokens\", \"token model linking emails and UID\", id=\"ix_tknmod\")))\nNext let's build a token model.\nHere's a short unit test that captures the essence--you\nshould be able to link an email to a unique ID,\nand that ID shouldn't be the same twice in a row:\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_models.py (ch19l035)\n====\n[source,python]\n----\nfrom accounts.models import Token, User\n[...]\n\n\nclass TokenModelTest(TestCase):\n    def test_links_user_with_auto_generated_uid(self):\n        token1 = Token.objects.create(email=\"a@b.com\")\n        token2 = Token.objects.create(email=\"a@b.com\")\n        self.assertNotEqual(token1.uid, token2.uid)\n----\n====\n\nI won't show every single listing for creating the token class in _models.py_;\nI'll let you do that yourself instead.\nDriving Django models with basic TDD\ninvolves jumping through a few hoops because of the migration,\nso you'll see a few iterations like this--minimal code change,\nmake migrations, get new error, delete migrations,\nre-create new migrations, another code change, and so on...\n\n\n[role=\"dofirst-ch19l036\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nTypeError: Token() got unexpected keyword arguments: 'email'\n----\n\nI'll trust you to go through these conscientiously--remember,\nI may not be able to see you, but the Testing Goat can!\n\nYou might go through a hoop like this one, for example, where you find yourself needing to create and then delete a migration for an incomplete solution:\n\n[role=\"dofirst-ch19l037\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0002_token.py\n    + Create model Token\n$ pass:quotes[*python src/manage.py test accounts*]\nAttributeError: 'Token' object has no attribute 'uid'. Did you mean: 'id'?\n$ pass:quotes[*rm src/accounts/migrations/0002_token.py*]\n----\n\n\nEventually, you should get to this code...\n\n[role=\"sourcecode dofirst-ch19l038-0\"]\n.src/accounts/models.py (ch19l038)\n====\n[source,python]\n----\nclass Token(models.Model):\n    email = models.EmailField()\n    uid = models.CharField(max_length=40)\n----\n====\n// DAVID: could it confuse people that the max_length is 40 here but 255 in the spike?\n\nAnd this error:\n\n[role=\"dofirst-ch19l039\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\n\n    self.assertNotEqual(token1.uid, token2.uid)\nAssertionError: '' == ''\n----\n\nAnd here we have to decide how to generate our random unique ID field.\nWe could use the `random` module, but Python actually comes with another module\nspecifically designed for generating unique IDs called \"UUID\"\n(for \"universally unique ID\").\nWe can use it like this:\n\n// DAVID: It feels like a strange time to introduce it, seeing as we've already used it in the spike earlier.\n\n[role=\"sourcecode\"]\n.src/accounts/models.py (ch19l040)\n====\n[source,python]\n----\nimport uuid\n[...]\n\nclass Token(models.Model):\n    email = models.EmailField()\n    uid = models.CharField(default=uuid.uuid4, max_length=40)  # <1>\n----\n====\n\n<1> The `default=` argument for a field can be either a static value\n    or a callable that returns a value at the time the model is created.\n    In our case, using a callable means\n    we'll get a different unique ID for every model.\n\n[role=\"pagebreak-before\"]\nAnd, perhaps with a bit more wrangling of `makemigrations`...\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*rm src/accounts/migrations/0002_token.py*]\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'accounts':\n  src/accounts/migrations/0002_token.py\n    + Create model Token\n----\n\n...that should get us to passing tests:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\nRan 4 tests in 0.015s\n\nOK\n----\n\n\nSo, we are well on our way!\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _View to create token and send login email incl. url w/ token UID_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\nThe models layer is done, at least.\nIn the next chapter, we'll get into mocking—a key technique for testing external dependencies like email.(((\"tokens\", \"token model linking emails and UID\", startref=\"ix_tknmod\")))(((\"emails\", \"token model linking with unique ID\", startref=\"ix_emltkn\")))(((\"\", startref=\"SDtoken18\")))\n\n\n[role=\"pagebreak-before\"]\n.Exploratory Coding, Spiking, and De-spiking\n*******************************************************************************\nSpiking::\n    Spiking is exploratory coding to find out about a new API,\n    or to explore the feasibility of a new solution.\n    Spiking can be done without tests.\n    It's a good idea to do your spike on a new branch,\n    and go back to your main branch when de-spiking.\n    (((\"spiking and de-spiking\", \"defined\")))\n\n\nDe-spiking::\n    De-spiking means taking the work from a spike and making it part of the production codebase.\n    The idea is to throw away the old spike code altogether,\n    and start again from scratch, using TDD once again.\n    De-spiked code can often come out looking quite different\n    from the original spike, and usually much nicer.\n\n\nWriting your FT against spiked code::\n    Whether or not this is a good idea depends on your circumstances.\n    The reason it can be useful is because it can help you write the FT\n    correctly--figuring out how to test your spike\n    can be just as challenging as the spike itself.\n    On the other hand, it might constrain you to\n    reimplementing a solution very similar to your spiked one;\n    something to watch out for.\n    (((\"functional tests (FTs)\", \"spiked code and\")))\n    (((\"\", startref=\"AuthSpike18\")))\n*******************************************************************************\n"
  },
  {
    "path": "chapter_20_mocking_1.asciidoc",
    "content": "[[chapter_20_mocking_1]]\n== Using Mocks to Test External Dependencies\n\n(((\"Django framework\", \"sending emails\")))\n(((\"emails\", \"sending from Django\")))\n(((\"mail.outbox attribute\")))\nIn this chapter, we'll start testing the parts of our code that send emails—i.e., the second item on our scratchpad:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _View to create token and send login email incl. url w/ token UID_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\nIn the functional test (FT), you saw that Django gives us a way of retrieving\nany emails it sends by using the `mail.outbox` attribute.\nBut in this chapter, I want to demonstrate a widespread testing technique called _mocking_. So, for the purpose of these unit tests, we'll pretend that this nice Django shortcut doesn't exist.\n(((\"mocks\", \"benefits and drawbacks of\")))\n\nAm I telling you _not_ to use Django's `mail.outbox`?\nNo—use it; it's a neat helper.\nBut I want to teach you about mocks because they're a useful general-purpose tool\nfor unit testing external dependencies.\nYou may not always be using Django!\nAnd even if you are, you may not be sending email--any\ninteraction with a third-party API\nis a place you might find yourself wanting to test with mocks.\n(((\"mocks\", \"deciding whether to use\")))(((\"external dependencies\")))\n\n\n.To Mock or Not to Mock?\n*******************************************************************************\n\nI once gave a talk called\nhttps://oreil.ly/XJPbT[\"Stop Using Mocks\"];\nit's entirely possible to find ways to write tests for external dependencies\nwithout using mocks at all.\n\nI'm covering mocking in this book because it's such a common technique,\nbut it does come with some downsides, as we'll see.\nOther techniques—including dependency injection\nand the use of custom fake objects—are well worth exploring,\nbut they're more advanced.\n\nMy second book https://www.cosmicpython.com[_Architecture Patterns with Python_]\ngoes into some detail on these alternatives.\n*******************************************************************************\n\n\n\n=== Before We Start: Getting the Basic Plumbing In\n\n(((\"mocks\", \"preparing for\")))\nLet's just get a basic view and URL set up first.\nWe can do so with a simple test to ensure\nthat our new URL for sending the login email should eventually redirect\nback to the home page:\n\n\n[role=\"sourcecode dofirst-ch20l002\"]\n.src/accounts/tests/test_views.py (ch20l001)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\n\nclass SendLoginEmailViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        response = self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n        self.assertRedirects(response, \"/\")\n----\n====\n\n[role=\"pagebreak-before\"]\nWire up the `include` in _superlists/urls.py_,\nplus the `url` in _accounts/urls.py_,\nand get the test passing with something a bit like this:\n\n[role=\"sourcecode dofirst-ch20l003\"]\n.src/accounts/views.py (ch20l004)\n====\n[source,python]\n----\nfrom django.core.mail import send_mail  # <1>\nfrom django.shortcuts import redirect\n\n\ndef send_login_email(request):\n    return redirect(\"/\")\n----\n====\n\n\n<1> I've added the import of the `send_mail` function as a placeholder for now.\n\nIf you've got the plumbing right, the tests should pass at this point:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\nRan 5 tests in 0.015s\n\nOK\n----\n\nOK, now we have a starting point—so let's get mocking!\n\n\n=== Mocking Manually—aka Monkeypatching\n\n(((\"mocks\", \"manual\", id=\"Mmanual19\")))\n(((\"monkeypatching\", id=\"monkey19\")))(((\"send_mail function\", \"mocking\", id=\"ix_sndml\")))\nWhen we call `send_mail` in real life,\nwe expect Django to be making a connection to our email provider,\nand sending an actual email across the public internet.\nThat's not something we want to happen in our tests.\nIt's a similar problem whenever you have code that has external side effects—calling\nan API, sending out an SMS, integrating with a payment provider, whatever it may be.\n\nWhen running our unit tests,\nwe don't want to be sending out real payments or making API calls across the internet.\nBut we would still like a way of testing that our code is correct.\nMocksfootnote:[I'm using the generic term \"mock\", but testing enthusiasts like\nto distinguish other types of a general class of test tools called \"test\ndoubles\", including spies, fakes, and stubs.  The differences don't really\nmatter for this book, but if you want to get into the nitty-gritty, check out\nthe https://github.com/testdouble/contributing-tests/wiki/Test-Double[amazing\nwiki by Justin Searls]. Warning: absolutely chock full of great testing content.]\ngive us one way to do that.\n\n\nActually, one of the great things about Python is that its dynamic nature\nmakes it very easy to do things like mocking—or what's sometimes called https://oreil.ly/vXHWY[monkeypatching].\nLet's suppose that, as a first step,\nwe want to get to some code that invokes `send_mail`\nwith the right subject line, \"from\" address, and \"to\" address.\nThat would look something like this:\n\n\n[role=\"sourcecode skipme\"]\n.src/accounts/views.py (expected future contents)\n====\n[source,python]\n----\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    # expected future code:\n    send_mail(\n        \"Your login link for Superlists\",\n        \"some kind of body text tbc\",\n        \"noreply@superlists\",\n        [email],\n    )\n    return redirect(\"/\")\n----\n====\n\nHow can we test this without calling the _real_ `send_mail` function?\nThe answer is that our test can ask Python to swap out the `send_mail` function\nfor a fake version, at runtime, just before we invoke the `send_login_email` view.\n\nCheck this out:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l005)\n====\n[source,python]\n----\nfrom django.test import TestCase\n\nimport accounts.views  # <2>\n\n\nclass SendLoginEmailViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        [...]\n\n    def test_sends_mail_to_address_from_post(self):\n        self.send_mail_called = False\n\n        def fake_send_mail(subject, body, from_email, to_list):  # <1>\n            self.send_mail_called = True\n            self.subject = subject\n            self.body = body\n            self.from_email = from_email\n            self.to_list = to_list\n\n        accounts.views.send_mail = fake_send_mail  # <2>\n\n        self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n\n        self.assertTrue(self.send_mail_called)\n        self.assertEqual(self.subject, \"Your login link for Superlists\")\n        self.assertEqual(self.from_email, \"noreply@superlists\")\n        self.assertEqual(self.to_list, [\"edith@example.com\"])\n----\n====\n\n<1> We define a `fake_send_mail` function,\n    which looks like the real `send_mail` function,\n    but all it does is save some information about how it was called,\n    using some variables on `self`.\n\n\n<2> Then, before we execute the code under test by doing the `self.client.post`,\n    we swap out the real `accounts.views.send_mail`\n    with our fake version—it's as simple as just assigning it.\n\n// DAVID: Might be better to get everything else working, and the test passing, without send_mail at all.\n// Then we introduce it, run the test and see it fail because it has some dependencies? Then we can just concentrate on\n// the mock bit.\n\nIt's important to realise that there isn't really anything magical going on here;\nwe're just taking advantage of Python's dynamic nature and scoping rules.\n\nUp until we actually invoke a function, we can modify the variables it has access to,\nas long as we get into the right namespace.\nThat's why we import the top-level `accounts` module:\nto be able to get down to the `accounts.views` module,\nwhich is the scope in which the `accounts.views.send_login_email` function will run.\n\nThis isn't even something that only works inside unit tests—you can do this kind of monkeypatching in any Python code! That may take a little time to sink in.\nSee if you can convince yourself that it's not all totally crazy—and then consider a couple of extra details that are worth knowing:\n\n* Why do we use `self` as a way of passing information around?\n  It's just a convenient variable that's available\n  both inside the scope of the `fake_send_mail` function and outside of it.\n  We could use any mutable object, like a list or a dictionary,\n  as long as we are making in-place changes to an existing variable\n  that exists outside our fake function.(((\"self variable\")))\n  (Feel free to have a play around with different ways of doing this, if\n  you're curious, and see what works and doesn't.)\n\n* The \"before\" is critical! I can't tell you how many times I've sat there,\n  wondering why a mock isn't working,\n  only to realise that I didn't mock _before_ I called the code under test.\n\n\nLet's see if our hand-rolled mock object will let us test-drive some code:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\n    self.assertTrue(self.send_mail_called)\nAssertionError: False is not true\n----\n\n[role=\"pagebreak-before\"]\nSo let's call `send_mail`, naively:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l006-1)\n====\n[source,python]\n----\nfrom django.core.mail import send_mail  # <1>\n[...]\n\ndef send_login_email(request):\n    send_mail()  # <2>\n    return redirect(\"/\")\n----\n====\n\n<1> This import should still be in the file from earlier,\n    but in case an overenthusiastic IDE has removed it,\n    I'm re-listing it for you here.\n<2> Here's our new call to `send_mail()`.\n\n\nThat gives:\n\n[subs=\"specialcharacters,macros\"]\n----\nTypeError: SendLoginEmailViewTest.test_sends_mail_to_address_from_post.<locals>\n.fake_send_mail() missing 4 required positional arguments: 'subject', 'body',\n'from_email', and 'to_list'\n----\n\nIt looks like our monkeypatch is working!\nWe've called `send_mail`, and it's gone into our `fake_send_mail` function,\nwhich wants more arguments.\nLet's try this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l006-2)\n====\n[source,python]\n----\ndef send_login_email(request):\n    send_mail(\"subject\", \"body\", \"from_email\", [\"to email\"])\n    return redirect(\"/\")\n----\n====\n\nThat gives:\n\n----\n    self.assertEqual(self.subject, \"Your login link for Superlists\")\nAssertionError: 'subject' != 'Your login link for Superlists'\n----\n\nThat's working pretty well!\nNow we can work step-by-step, all the way through to something like this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l006)\n====\n[source,python]\n----\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    send_mail(\n        \"Your login link for Superlists\",\n        \"body text tbc\",\n        \"noreply@superlists\",\n        [email],\n    )\n    return redirect(\"/\")\n----\n====\n\nAnd we have passing tests!\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n\nRan 6 tests in 0.016s\n\nOK\n----\n\n\nBrilliant!  We've managed to write tests for some code, which would\nordinarily go out and try to send real emails across the internet,\nand by \"mocking out\" the `send_email` function,\nwe're able to write the tests and code all the same.footnote:[Again,\nwe're acting as if Django's `mail.outbox` didn't exist,\nfor the sake of learning.\nAfter all, what if you were using Flask?\nOr what if this was an API call, not an email?]\n\nBut our hand-rolled mock has a couple of problems:\n\n* It involved a fair bit of boilerplate code,\n  populating all those `self.xyz` variables to let us assert on them.\n\n* More importantly, although we didn't see this,\n  the monkeypatching will persist from one test to the next,\n  breaking isolation between tests.\n  This can cause serious confusion.\n(((\"send_mail function\", \"mocking\", startref=\"ix_sndml\")))(((\"\", startref=\"monkey19\")))(((\"\", startref=\"Mmanual19\")))\n\n// TODO: illustrate this explicitly\n\n\n=== The Python Mock Library\n\n(((\"mocks\", \"Python Mock library\", id=\"Mpythong19\")))\n(((\"Python 3\", \"Mock library\", id=\"Pmock19\")))\nThe `mock` package was added to the standard library as part of Python 3.3.\nIt provides a magical object called a `Mock`; try this out in a Python shell:\n\n\n[role='skipme']\n[source,python]\n----\n>>> from unittest.mock import Mock\n>>> m = Mock()\n>>> m.any_attribute\n<Mock name='mock.any_attribute' id='140716305179152'>\n>>> type(m.any_attribute)\n<class 'unittest.mock.Mock'>\n>>> m.any_method()\n<Mock name='mock.any_method()' id='140716331211856'>\n>>> m.foo()\n<Mock name='mock.foo()' id='140716331251600'>\n>>> m.called\nFalse\n>>> m.foo.called\nTrue\n>>> m.bar.return_value = 1\n>>> m.bar(42, var='thing')\n1\n>>> m.bar.call_args\ncall(42, var='thing')\n----\n\n[role=\"pagebreak-before\"]\nA mock is a magical object for a few reasons:\n\n* It responds to any request for an attribute or method call with other mocks.\n* You can configure it in turn to return specific values when called.\n* It enables you to inspect what it was called with.\n\nSounds like a useful thing to be able to use in our unit tests!\n\n\n==== Using unittest.patch\n\n(((\"unittest module\", \"mock module and\")))(((\"patch function in unittest and mock modules\")))\nAnd as if that weren't enough,\nthe `mock` module also provides a helper function called `patch`,\nwhich we can use to do the monkeypatching we did by hand earlier.\n\nI'll explain how it all works shortly, but let's see it in action first:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l007)\n====\n[source,python]\n----\nfrom unittest import mock\n\nfrom django.test import TestCase\n[...]\n\nclass SendLoginEmailViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        [...]\n\n    @mock.patch(\"accounts.views.send_mail\")  # <1>\n    def test_sends_mail_to_address_from_post(self, mock_send_mail):  # <2>\n        self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n\n        self.assertEqual(mock_send_mail.called, True)\n        (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args\n        self.assertEqual(subject, \"Your login link for Superlists\")\n        self.assertEqual(from_email, \"noreply@superlists\")\n        self.assertEqual(to_list, [\"edith@example.com\"])\n\n----\n====\n\n<1> Here's the decorator--we'll go into detail about how it works shortly.\n\n<2> Here's the extra argument we add to the test method.\n    Again, detailed explanation to come,\n    but as you'll see, it's going to do most of the work that `fake_send_mail`\n    was doing before.\n\n[role=\"pagebreak-before\"]\nIf you rerun the tests, you'll see they still pass.\nAnd because we're always suspicious of any test that still passes after a big change,\nlet's deliberately break it just to see:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l008)\n====\n[source,python]\n----\n        self.assertEqual(to_list, [\"schmedith@example.com\"])\n----\n====\n\nAnd let's add a little debug print to our view as well,\nto see the effects of the `mock.patch`:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l009)\n====\n[source,python]\n----\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    print(type(send_mail))\n    send_mail(\n        [...]\n----\n====\n\nLet's run the tests again:\n\n[subs=\"macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]pass:specialcharacters[\n....<class 'function'>\n.<class 'unittest.mock.MagicMock'>\n][...]pass:[\nAssertionError: Lists differ: ['edith@example.com'\\] !=\n['schmedith@example.com'\\]\n][...]\n\nRan 6 tests in 0.024s\n\nFAILED (failures=1)\n----\n\n\nSure enough, the tests fail.\nAnd we can see, just before the failure message,\nthat when we print the `type` of the `send_mail` function,\nin the first unit test it's a normal function,\nbut in the second unit test we're seeing a mock object.\n\nLet's remove the deliberate mistake and dive into exactly what's going on:\n\n[role=\"sourcecode dofirst-ch20l010\"]\n.src/accounts/tests/test_views.py (ch20l011)\n====\n[source,python]\n----\n@mock.patch(\"accounts.views.send_mail\")  # <1>\ndef test_sends_mail_to_address_from_post(self, mock_send_mail):  # <2>\n    self.client.post(  # <3>\n        \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n    )\n\n    self.assertEqual(mock_send_mail.called, True)  # <4>\n    (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args  # <5>\n    self.assertEqual(subject, \"Your login link for Superlists\")\n    self.assertEqual(from_email, \"noreply@superlists\")\n    self.assertEqual(to_list, [\"edith@example.com\"])\n----\n====\n\n<1> The `mock.patch()` decorator takes a dot-notation name of an object to monkeypatch.\n    That's the equivalent of manually replacing the `send_mail` in `accounts.views`.\n    The advantage of the decorator is that,\n    firstly, it automatically replaces the target with a mock.\n    And secondly, it automatically puts the original object back at the end!\n    (Otherwise, the object stays monkeypatched for the rest of the test run,\n    which might cause problems in other tests.)\n\n\n<2> `patch` then injects the mocked object into the test\n    as an argument to the test method.\n    We can choose whatever name we want for it,\n    but I usually use a convention of `mock_` plus the original name of the object.\n\n\n<3> We call our view under test as usual,\n    but everything inside this test method has our mock applied to it,\n    so the view won't call the real `send_mail` object;\n    it'll be seeing `mock_send_mail` instead.\n\n<4> And we can now make assertions about what happened to that mock object\n    during the test.  We can see it was called...\n\n<5> ...and we can also unpack its various positional and keyword call arguments,\n    to examine what it was called with.\n    (See <<mock-call-args-sidebar>> in the next chapter for a longer\n    explanation of `.call_args`.)\n\n\nAll crystal clear? No? Don't worry; we'll do a couple more tests with mocks\nto see if they start to make more sense as we use them more.\n\n\n\n==== Getting the FT a Little Further Along\n\nFirst let's get back to our FT and see where it's failing:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nAssertionError: 'Check your email' not found in 'Superlists\\nEnter your email\nto log in\\nStart a new To-Do list'\n----\n\nSubmitting the email address currently has no effect. Hmmm. Currently our form is hardcoded to send to _/accounts/send_login_email_. Let's switch to using the `{% url %}` syntax just to make sure it's the right URL:\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch20l012)\n====\n[source,html]\n----\n<form method=\"POST\" action=\"{% url 'send_login_email' %}\">\n----\n====\n\n\nDoes that help?  Nope, same error.  Why? Ah, nothing to do with the URL actually;\nit's because we're not displaying a success message after we send the user an email.\nLet's add a test for that.\n\n\n==== Testing the Django Messages Framework\n\n(((\"messages framework (Django), testing\", id=\"ix_msgfrm\")))(((\"Django framework\", \"messages framework, testing\", id=\"ix_Djmsg\")))\nWe'll use Django's \"messages framework\",\nwhich is often used to display ephemeral \"success\" or \"warning\" messages\nto show the results of an action, something like what's shown in <<success-message>>.\n\n[[success-message]]\n.A green success message\nimage::images/tdd3_2001.png[\"Screenshot of success message saying check your email, as it will look at the end of the de-spike.\"]\n\nHave a look at the https://docs.djangoproject.com/en/5.2/ref/contrib/messages[Django messages docs]\nif you haven't come across it already. Testing Django messages is a bit contorted:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l013)\n====\n[source,python]\n----\n    def test_adds_success_message(self):\n        response = self.client.post(\n            \"/accounts/send_login_email\",\n            data={\"email\": \"edith@example.com\"},\n            follow=True,  # <1>\n        )\n\n        message = list(response.context[\"messages\"])[0]  # <2>\n        self.assertEqual(\n            message.message,\n            \"Check your email, we've sent you a link you can use to log in.\",\n        )\n        self.assertEqual(message.tags, \"success\")\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> We have to pass `follow=True`\n    to the test client to tell it to get the page _after_ the 302-redirect.\n\n<2> Then we examine the response context for a `messages` iterable,\n    which we have to listify before it'll play nicely.\n    (We'll use these later in a template with `{% for message in messages %}`.)\n\n\nThat gives:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\n    message = list(response.context[\"messages\"])[0]\nIndexError: list index out of range\n----\n\nAnd we can get it passing with:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l014)\n====\n[source,python]\n----\nfrom django.contrib import messages\n[...]\n\ndef send_login_email(request):\n    [...]\n    messages.success(\n        request,\n        \"Check your email, we've sent you a link you can use to log in.\",\n    )\n    return redirect(\"/\")\n----\n====\n\n[role=\"pagebreak-before less_space\"]\n[[mocks-tightly-coupled-sidebar]]\n.Mocks Can Leave You Tightly Coupled to the Implementation\n*******************************************************************************\n\nTIP: This sidebar is an intermediate-level testing tip.\n    If it goes over your head the first time around,\n    come back and take another look when you've finished this chapter.\n\nI said testing messages is a bit contorted;\nit took me several goes to get it right.\nIn fact, at a previous employer,\nwe gave up on testing them like this and decided to just use mocks.\nLet's see what that would look like in this case:\n\n[role=\"sourcecode small-code\"]\n.src/accounts/tests/test_views.py (ch20l014-2)\n====\n[source,python]\n----\n    @mock.patch(\"accounts.views.messages\")\n    def test_adds_success_message_with_mocks(self, mock_messages):\n        response = self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n\n        expected = \"Check your email, we've sent you a link you can use to log in.\"\n        self.assertEqual(\n            mock_messages.success.call_args,\n            mock.call(response.wsgi_request, expected),\n        )\n----\n====\n\nWe mock out the `messages` module, and check that `messages.success` was\ncalled with the right arguments: the original request and the message we want.\n\nAnd you could get it passing by using the exact same code as earlier.  Here's\nthe problem though:  the messages framework gives you more than one way\nto achieve the same result.  I could write the code like this:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l014-3)\n====\n[source,python]\n----\n    messages.add_message(\n        request,\n        messages.SUCCESS,\n        \"Check your email, we've sent you a link you can use to log in.\",\n    )\n----\n====\n\nAnd the original, non-mocky test would still pass.\nBut our mocky test will fail,\nbecause we're no longer calling `messages.success`;\nwe're calling `messages.add_message`.\nEven though the end result is the same and our code is \"correct\",\nthe test is broken.(((\"mocks\", \"use of, tight coupling with implementation\")))\n\nThis is what it means to say that using mocks leave you\n\"tightly coupled with the implementation\".\nWe usually say it's better to test behaviour, not implementation details;\ntest what happens, not how you do it.\nMocks often end up erring too much on the side of the \"how\" rather than the \"what\".\n\nTIP: Test should be about behaviour, not implementation.\n    If your tests tie you to specific implementation details,\n    they will prevent you from refactoring as freely.\n\n*******************************************************************************\n\n\n==== Adding Messages to Our HTML\n\nWhat happens next in the functional test?(((\"Django framework\", \"messages framework, testing\", startref=\"ix_Djmsg\")))(((\"messages framework (Django), testing\", startref=\"ix_msgfrm\")))(((\"messages\", \"adding to HTML  for page\")))\nAh.  Still nothing.\nWe need to actually add the messages to the page.\nSomething like this:\n\n\n[role=\"sourcecode dofirst-ch20l014-4\"]\n.src/lists/templates/base.html (ch20l015)\n====\n[source,html]\n----\n      [...]\n      </nav>\n\n      {% if messages %}\n        <div class=\"row\">\n          <div class=\"col-md-12\">\n            {% for message in messages %}\n              {% if message.level_tag == 'success' %}\n                <div class=\"alert alert-success\">{{ message }}</div>\n              {% else %}\n                <div class=\"alert alert-warning\">{{ message }}</div>\n              {% endif %}\n            {% endfor %}\n          </div>\n        </div>\n      {% endif %}\n----\n====\n\n// TODO: feed thru change\n\nNow do we get a little further?  Yes!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nRan 7 tests in 0.023s\n\nOK\n\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nAssertionError: 'Use this link to log in' not found in 'body text tbc'\n----\n\n\nWe need to fill out the body text of the email,\nwith a link that the user can use to log in. Let's just cheat for now though, by changing the value in the view:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l016)\n====\n[source,python]\n----\n    send_mail(\n        \"Your login link for Superlists\",\n        \"Use this link to log in\",\n        \"noreply@superlists\",\n        [email],\n    )\n----\n====\n\n\nThat gets the FT a little further:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nAssertionError: Could not find url in email body:\nUse this link to log in\n----\n\nOK, I think we can call the `send_login_email` view done for now:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\n==== Starting on the Login URL\n\nWe're going to have to build some kind of URL!(((\"URLs\", \"starting login URL\")))(((\"tokens\", \"passing in GET pararameter to login URL\")))\nLet's build the minimal thing, just a placeholder really:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l017)\n====\n[source,python]\n----\nclass LoginViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        response = self.client.get(\"/accounts/login?token=abcd123\")\n        self.assertRedirects(response, \"/\")\n----\n====\n\nWe're imagining we'll pass the token in as a GET parameter, after the `?`.\nIt doesn't need to do anything for now.\n\n\nI'm sure you can find your way through to getting the boilerplate in\nfor a basic URL and view, via errors like these:\n\n[role=\"simplelist\"]\n* No URL:\n+\n[role=\"small-code\"]\n----\nAssertionError: 404 != 302 : Response didn't redirect as expected: Response\ncode was 404 (expected 302)\n----\n* No view:\n+\n[role=\"dofirst-ch20l018 small-code\"]\n----\nAttributeError: module 'accounts.views' has no attribute 'login'\n----\n* Broken view:\n+\n[role=\"dofirst-ch20l019 small-code\"]\n----\nValueError: The view accounts.views.login didn't return an HttpResponse object.\nIt returned None instead.\n----\n* OK!\n+\n[role=\"dofirst-ch20l020 small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\n\nRan 8 tests in 0.029s\nOK\n----\n\n\nAnd now we can give people a link to use.\nIt still won't do much though,\nbecause we still don't have a token to give to the user.\n\n\n\n==== Checking That We Send the User a Link with a Token\n\nBack in our `send_login_email` view,\nwe've tested the email subject, and the \"from\", and \"to\" fields.\nThe body is the part that will have to include a token or URL they can use to log in.(((\"emails\", \"checking sending of link with a token\")))(((\"emails\", \"checking sending of link with token\")))\nLet's spec out two tests for that:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch20l021)\n====\n[source,python]\n----\nfrom accounts.models import Token\n[...]\n\nclass SendLoginEmailViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        [...]\n    def test_adds_success_message(self):\n        [...]\n    @mock.patch(\"accounts.views.send_mail\")\n    def test_sends_mail_to_address_from_post(self, mock_send_mail):\n        [...]\n\n    def test_creates_token_associated_with_email(self):  # <1>\n        self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n        token = Token.objects.get()\n        self.assertEqual(token.email, \"edith@example.com\")\n\n    @mock.patch(\"accounts.views.send_mail\")\n    def test_sends_link_to_login_using_token_uid(self, mock_send_mail):  # <2>\n        self.client.post(\n            \"/accounts/send_login_email\", data={\"email\": \"edith@example.com\"}\n        )\n\n        token = Token.objects.get()\n        expected_url = f\"http://testserver/accounts/login?token={token.uid}\"\n        (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args\n        self.assertIn(expected_url, body)\n----\n====\n\n<1> The first test is fairly straightforward;\n  it checks that the token we create in the database\n  is associated with the email address from the POST request.\n\n<2> The second one is our second test using mocks.\n  We mock out the `send_mail` function again using the `patch` decorator,\n  but this time we're interested in the `body` argument from the call arguments.\n\nRunning them now will fail because we're not creating any kind of token:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\naccounts.models.Token.DoesNotExist: Token matching query does not exist.\n[...]\naccounts.models.Token.DoesNotExist: Token matching query does not exist.\n----\n\nWe can get the first one to pass by creating a token:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l022)\n====\n[source,python]\n----\nfrom accounts.models import Token\n[...]\n\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    token = Token.objects.create(email=email)\n    send_mail(\n        [...]\n----\n====\n\nAnd now the second test prompts us to actually use the token in the body\nof our email:\n\n[subs=\"\"]\n----\n[...]\nAssertionError:\n'http://testserver/accounts/login?token=[...]\nnot found in 'Use this link to log in'\n\nFAILED (failures=1)\n----\n\nSo, we can insert the token into our email like this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch20l023)\n====\n[source,python]\n----\nfrom django.urls import reverse\n[...]\n\ndef send_login_email(request):\n    email = request.POST[\"email\"]\n    token = Token.objects.create(email=email)\n    url = request.build_absolute_uri(  # <1>\n        reverse(\"login\") + \"?token=\" + str(token.uid),\n    )\n    message_body = f\"Use this link to log in:\\n\\n{url}\"\n    send_mail(\n        \"Your login link for Superlists\",\n        message_body,\n        \"noreply@superlists\",\n        [email],\n    )\n    [...]\n----\n====\n\n<1> `request.build_absolute_uri` deserves a mention--it's\n    one way to build a \"full\" URL,\n    including the domain name and the HTTP(S) part, in Django.\n    There are other ways,\n    but they usually involve getting into the \"sites\" framework,\n    which gets complicated pretty quickly.\n    You can find lots more discussion on this if you're curious\n    by doing a bit of googling.\n\n// IDEA: investigate kwargs for reverse() call\n// reverse(\"login\", token=str(token.uid))\n\n\nAnd the tests pass:\n\n----\nOK\n----\n\nI think _that's_ our `send_login_email` view done:\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#_View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _Authentication backend with authenticate() and get_user() functions_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\nThe next piece in the puzzle is the authentication backend,\nwhose job it will be to examine tokens for validity\nand then return the corresponding users. Then, we need to get our login view to actually log users in,\nif they can authenticate.\n(((\"\", startref=\"Mpythong19\")))(((\"\", startref=\"Pmock19\")))\n\n\n=== De-spiking Our Custom Authentication Backend\n\n(((\"mocks\", \"de-spiking custom authentication\", id=\"ix_mckdespCA\")))\n(((\"spiking and de-spiking\", \"de-spiking custom authentication\", id=\"ix_spkdesCA\")))\nHere's how our authentication backend looked in the spike:\n\n\n[[spike-reminder]]\n[role=\"skipme small-code\"]\n[source,python]\n----\nclass PasswordlessAuthenticationBackend(BaseBackend):\n    def authenticate(self, request, uid):\n        print(\"uid\", uid, file=sys.stderr)\n        if not Token.objects.filter(uid=uid).exists():\n            print(\"no token found\", file=sys.stderr)\n            return None\n        token = Token.objects.get(uid=uid)\n        print(\"got token\", file=sys.stderr)\n        try:\n            user = ListUser.objects.get(email=token.email)\n            print(\"got user\", file=sys.stderr)\n            return user\n        except ListUser.DoesNotExist:\n            print(\"new user\", file=sys.stderr)\n            return ListUser.objects.create(email=token.email)\n\n    def get_user(self, email):\n        return ListUser.objects.get(email=email)\n----\n\n[role=\"pagebreak-before\"]\nDecoding this:\n\n* We take a UID and check if it exists in the database.\n* We return `None` if it doesn't.\n* If it does exist, we extract an email address,\n  and either find an existing user with that address or create a new one.\n// CSANAD: shouldn't we use the numbered annotations instead?\n\n\n\n==== One if = One More Test\n\nA rule of thumb for these sorts of tests:\nany `if` means an extra test, and any `try/except` means an extra test. So, this should be about three tests.\nHow about something like this?\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_authentication.py (ch20l024)\n====\n[source,python]\n----\nfrom django.http import HttpRequest\nfrom django.test import TestCase\n\nfrom accounts.authentication import PasswordlessAuthenticationBackend\nfrom accounts.models import Token, User\n\n\nclass AuthenticateTest(TestCase):\n    def test_returns_None_if_no_such_token(self):\n        result = PasswordlessAuthenticationBackend().authenticate(\n            HttpRequest(), \"no-such-token\"\n        )\n        self.assertIsNone(result)\n\n    def test_returns_new_user_with_correct_email_if_token_exists(self):\n        email = \"edith@example.com\"\n        token = Token.objects.create(email=email)\n        user = PasswordlessAuthenticationBackend().authenticate(\n            HttpRequest(), token.uid\n        )\n        new_user = User.objects.get(email=email)\n        self.assertEqual(user, new_user)\n\n    def test_returns_existing_user_with_correct_email_if_token_exists(self):\n        email = \"edith@example.com\"\n        existing_user = User.objects.create(email=email)\n        token = Token.objects.create(email=email)\n        user = PasswordlessAuthenticationBackend().authenticate(\n            HttpRequest(), token.uid\n        )\n        self.assertEqual(user, existing_user)\n----\n====\n\n[role=\"pagebreak-before\"]\nIn _authenticate.py_, we'll just have a little placeholder:\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l025)\n====\n[source,python]\n----\nclass PasswordlessAuthenticationBackend:\n    def authenticate(self, request, uid):\n        pass\n----\n====\n\n\nHow do we get on?\n\n[subs=\"macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n\n.FE..........\n======================================================================\nERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests\n.test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_\nif_token_exists)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/accounts/tests/test_authentication.py\", line 21, in\ntest_returns_new_user_with_correct_email_if_token_exists\n    new_user = User.objects.get(email=email)\n[...]\naccounts.models.User.DoesNotExist: User matching query does not exist.\n\n\n======================================================================\nFAIL: test_returns_existing_user_with_correct_email_if_token_exists (accounts.t\nests.test_authentication.AuthenticateTest.test_returns_existing_user_with_corre\nct_email_if_token_exists)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/accounts/tests/test_authentication.py\", line 31, in\ntest_returns_existing_user_with_correct_email_if_token_exists\n    self.assertEqual(user, existing_user)\n    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^\nAssertionError: None != pass:specialcharacters[<User: User object (edith@example.com)>]\n\n ---------------------------------------------------------------------\nRan 13 tests in 0.038s\n\nFAILED (failures=1, errors=1)\n----\n\n//TODO: do we need that inline pass:specialcharacters?\n\n[role=\"pagebreak-before\"]\nHere's a first cut:\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l026)\n====\n[source,python]\n----\nfrom accounts.models import Token, User\n\n\nclass PasswordlessAuthenticationBackend:\n    def authenticate(self, request, uid):\n        token = Token.objects.get(uid=uid)\n        return User.objects.get(email=token.email)\n----\n====\n\n\nNow, instead of one `FAIL` and one `ERROR`,\nwe get two ++ERROR++s:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n\nERROR: test_returns_None_if_no_such_token (accounts.tests.test_authentication.A\nuthenticateTest.test_returns_None_if_no_such_token)\n[...]\naccounts.models.Token.DoesNotExist: Token matching query does not exist.\n\nERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests\n.test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_\nif_token_exists)\n[...]\naccounts.models.User.DoesNotExist: User matching query does not exist.\n----\n\nNotice that our third test,\n+test_returns_existing_user_with_c&#x2060;o&#x2060;r&#x2060;r&#x2060;e&#x2060;c&#x2060;t&#x2060;_&#x2060;e&#x2060;m&#x2060;a&#x2060;i&#x2060;l&#x2060;_&#x200b;i&#x2060;f&#x2060;_&#x2060;t&#x2060;o&#x2060;k&#x2060;e&#x2060;n&#x2060;_exists+,\nis actually passing.  Our code _does_ currently handle the \"happy path\",\nwhere both the token and the user already exist in the database.\n\nLet's fix each of the remaining ones in turn.\nNotice how the test names are telling us exactly what we need to do.\nFirst, `test_returns_None_if_no_such_token`,\nwhich is telling us what to do if the token doesn't exist:\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l027)\n====\n[source,python]\n----\n    def authenticate(self, request, uid):\n        try:\n            token = Token.objects.get(uid=uid)\n            return User.objects.get(email=token.email)\n        except Token.DoesNotExist:\n            return None\n----\n====\n\nThat gets us down to one failure:\n\n[subs=\"specialcharacters,macros\"]\n----\nERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests\n.test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_\nif_token_exists)\n[...]\naccounts.models.User.DoesNotExist: User matching query does not exist.\n\nFAILED (errors=1)\n----\n\nOK, so we need to return a `new_user_with_correct_email` `if_token_exists`?\nWe can do that!\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l028)\n====\n[source,python]\n----\n    def authenticate(self, request, uid):\n        try:\n            token = Token.objects.get(uid=uid)\n            return User.objects.get(email=token.email)\n        except User.DoesNotExist:\n            return User.objects.create(email=token.email)\n        except Token.DoesNotExist:\n            return None\n----\n====\n\nThat's turned out neater than our <<spike-reminder,spike>>!\n\n\n==== The get_user Method\n\n\n(((\"get_user method\")))\nWe've handled the `authenticate` function, which Django will use to log new users in.\nThe second part of the protocol we have to implement is the `get_user` method,\nwhose job is to retrieve a user based on their unique identifier (the email address),\nor to return `None` if it can't find one.\n(Have another look at <<spike-reminder,the spiked code>> if you need a\nreminder.)\n\nHere are a couple of tests for those two requirements:\n\n\n[role=\"sourcecode small-code\"]\n.src/accounts/tests/test_authentication.py (ch20l030)\n====\n[source,python]\n----\nclass GetUserTest(TestCase):\n    def test_gets_user_by_email(self):\n        User.objects.create(email=\"another@example.com\")\n        desired_user = User.objects.create(email=\"edith@example.com\")\n        found_user = PasswordlessAuthenticationBackend().get_user(\"edith@example.com\")\n        self.assertEqual(found_user, desired_user)\n\n    def test_returns_None_if_no_user_with_that_email(self):\n        self.assertIsNone(\n            PasswordlessAuthenticationBackend().get_user(\"edith@example.com\")\n        )\n----\n====\n\nAnd our first failure:\n\n----\nAttributeError: 'PasswordlessAuthenticationBackend' object has no attribute\n'get_user'\n----\n\n[role=\"pagebreak-before\"]\nLet's create a placeholder one then:\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l031)\n====\n[source,python]\n----\nclass PasswordlessAuthenticationBackend:\n    def authenticate(self, request, uid):\n        [...]\n\n    def get_user(self, email):\n        pass\n----\n====\n\nNow we get:\n\n\n[subs=\"macros\"]\n----\n    self.assertEqual(found_user, desired_user)\nAssertionError: None != pass:specialcharacters[<User: User object (edith@example.com)>]\n----\n\nAnd (step by step, just to see if our test fails the way we think it will):\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l033)\n====\n[source,python]\n----\n    def get_user(self, email):\n        return User.objects.first()\n----\n====\n\nThat gets us past the first assertion, and onto:\n\n[subs=\"macros\"]\n----\n    self.assertEqual(found_user, desired_user)\nAssertionError: pass:specialcharacters[<User: User object (another@example.com)>] != pass:specialcharacters[<User: User object\n(edith@example.com)>]\n----\n\nAnd so, we call `get` with the email as an argument:\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l034)\n====\n[source,python]\n----\n    def get_user(self, email):\n        return User.objects.get(email=email)\n----\n====\n\n\nNow our test for the `None` case fails:\n\n----\nERROR: test_returns_None_if_no_user_with_that_email (accounts.tests.test_authen\ntication.GetUserTest.test_returns_None_if_no_user_with_that_email)\n[...]\naccounts.models.User.DoesNotExist: User matching query does not exist.\n----\n\nThat prompts us to finish the method like this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/authentication.py (ch20l035)\n====\n[source,python]\n----\n    def get_user(self, email):\n        try:\n            return User.objects.get(email=email)\n        except User.DoesNotExist:\n            return None  # <1>\n----\n====\n\n<1> You could just use `pass` here, and the function would return `None` by default.\n    However, because we specifically need the function to return `None`,\n    the \"explicit is better than implicit\" rule applies here.\n\nThat gets us to passing tests:\n\n----\nOK\n----\n\n\nAnd we have a working authentication backend!\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#_View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _[strikethrough line-through]#Authentication backend with authenticate() and get_user() functions#_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\nLet's call that a win and, in the next chapter,\nwe'll work on integrating it into our login view\nand getting our FT passing.(((\"spiking and de-spiking\", \"de-spiking custom authentication\", startref=\"ix_spkdesCA\")))(((\"mocks\", \"de-spiking custom authentication\", startref=\"ix_mckdespCA\")))\n\n[role=\"pagebreak-before less_space\"]\n[[mocking-py-sidebar-1]]\n.On Mocking in Python\n*******************************************************************************\n\nMocking and external dependencies::\n  One place to consider using mocking is when we have an external dependency\n  that we don't want to actually use in our tests.\n  A mock can be used to simulate the third-party API.\n  Whilst it is possible to \"roll your own\" mocks in Python,\n  a mocking framework like the +unittest.mock+ module provides a lot of helpful shortcuts\n  that will make it easier to write (and more importantly, read) your tests.\n  (((\"external dependencies\")))\n\nThe mock library::\n  The `unittest.mock` module from Python's standard library\n  contains most everything you might need for monkeypatching\n  and mocking in Python.footnote:[This library was originally written as a standalone package by Michael Foord while he was working at the company that later spawned PythonAnywhere, a few years before I joined. It became part of the standard library in Python 3.3.\n  Michael was a friend, and sadly passed away in 2025.]\n  (((\"mocks\", \"Python Mock library\")))\n  (((\"Python 3\", \"Mock library\")))\n\n\nMonkeypatching::\n  This is the process of replacing an object in a namespace at runtime.\n  We use it in our unit tests to replace a real function\n  that has undesirable side effects\n  with a mock object, using the `mock.patch` decorator.\n  (((\"monkeypatching\")))\n\n\nThe mock.patch decorator::\n  `unittest.mock` (((\"patch decorator\")))provides a function called `patch`,\n  which can be used to \"mock out\" (monkeypatch)\n  any object from the module you're testing.\n  It's commonly used as a decorator on a test method.\n  Importantly, it \"undoes\" the mocking at the end of the test for you,\n  to avoid contamination between tests.\n\nMocks can leave you tightly coupled to the implementation::\n  As discussed in the earlier sidebar,\n  mocks can leave you tightly coupled to your implementation.\n  For that reason, you shouldn't use them unless you have a good reason.\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_21_mocking_2.asciidoc",
    "content": "[[chapter_21_mocking_2]]\n== Using Mocks for Test Isolation\n\nIn this chapter, we'll finish up our login system.\nWhile doing so, we'll explore an alternative use of mocks:\nto isolate parts of the system from each other. This\nenables more targeted testing, fights combinatorial explosion,\nand reduces duplication between tests.\n\n\nNOTE: In this chapter, we start to drift towards what's called \"London-school TDD\",\n    which is a variant on the \"Classical\" or \"Detroit\" style of TDD\n    that I mostly show in the book.\n    We won't get into the details here,\n    but London-school TDD places more emphasis on mocking and isolating parts of the system.\n    As always, there are pros and cons!\n    Read more at \n    https://www.obeythetestinggoat.com/book/appendix_purist_unit_tests.html[Online Appendix: Test Isolation and \"Listening to Your Tests\"].\n\n\nAlong the way, we'll learn a few more useful features of `unittest.mock`,\nand we'll also have a discussion about how many tests are \"enough\".\n\n\n\n=== Using Our Auth Backend in the Login View\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _[strikethrough line-through]#Authentication backend with authenticate() and get_user() functions#_\n* _Registering auth backend in settings.py_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\nWe got our auth backend ready in the last chapter;\nnow we need use the backend in our login view.\nBut first, as our scratchpad says, we need to add it to _settings.py_:\n\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch21l001)\n====\n[source,python]\n----\nAUTH_USER_MODEL = \"accounts.User\"\nAUTHENTICATION_BACKENDS = [\n    \"accounts.authentication.PasswordlessAuthenticationBackend\",\n]\n\n[...]\n----\n====\n\nThat was easy!\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _[strikethrough line-through]#Authentication backend with authenticate() and get_user() functions#_\n* _[strikethrough line-through]#Registering auth backend in settings.py#_\n* _Login view calls authenticate() and login() from django.contrib.auth_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\nNext, let's write some tests for what should happen in our view.\nLooking back at the spike again:\n\n[role=\"sourcecode skipme\"]\n.src/accounts/views.py\n====\n[source,python]\n----\ndef login(request):\n    print(\"login view\", file=sys.stderr)\n    uid = request.GET.get(\"uid\")\n    user = auth.authenticate(uid=uid)\n    if user is not None:\n        auth.login(request, user)\n    return redirect(\"/\")\n----\n====\n\nTIP: You can view the contents of files from the spike\n    using, for example, `git show passwordless-spike:src/accounts/views.py`.\n\nWe call `django.contrib.auth.authenticate` and then,\nif it returns a user, we call `django.contrib.auth.login`.\n\nTIP: This is a good time to check out the\n    https://docs.djangoproject.com/en/5.2/topics/auth/default/#how-to-log-a-user-in[Django docs on authentication]\n    for a little more context.\n    (((\"Django framework\", \"documentation\")))\n\n\n==== Straightforward Non-Mocky Test for Our View\n\nHere's the most obvious test we might want to write,\nthinking in terms of the _behaviour_ we want:\n\n* If someone has a valid token, they should get logged in.\n* If someone tries to use an invalid token (or does not have one), it should not log them in.\n\n\nHere's how we might add the happy-path test for the user with the valid token:\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch21l002)\n====\n[source,python]\n----\nfrom django.contrib import auth\n[...]\n\nclass LoginViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        [...]\n\n    def test_logs_in_if_given_valid_token(self):\n        anon_user = auth.get_user(self.client)  # <1>\n        self.assertEqual(anon_user.is_authenticated, False)  # <2>\n\n        token = Token.objects.create(email=\"edith@example.com\")\n        self.client.get(f\"/accounts/login?token={token.uid}\")\n\n        user = auth.get_user(self.client)\n        self.assertEqual(user.is_authenticated, True)  # <3>\n        self.assertEqual(user.email, \"edith@example.com\")  # <3>\n----\n====\n\n<1> We use Django's `auth.get_user()` to extract the current user from the test client.\n\n<2> We verify we're not logged in before we start. (This isn't strictly necessary, but it's always nice to know you're on firm ground.)\n\n<3> And here's where we check that we've been logged in,\n    with a user with the right email address.\n\nAnd that will fail as expected:\n\n----\n    self.assertEqual(user.is_authenticated, True)\nAssertionError: False != True\n----\n\n[role=\"pagebreak-before\"]\nWe can get it to pass by \"cheating\", like this:\n\n\n[role=\"sourcecode small-code\"]\n.src/accounts/views.py (ch21l003)\n====\n[source,python]\n----\nfrom django.contrib import auth, messages\n[...]\nfrom accounts.models import Token, User\n\n\ndef send_login_email(request):\n    [...]\n\n\ndef login(request):\n    user = User.objects.create(email=\"edith@example.com\")\n    auth.login(request, user)\n    return redirect(\"/\")\n----\n====\n\n...\n\n----\nOK\n----\n\nThat forces us to write another test:\n\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch21l004)\n====\n[source,python]\n----\ndef test_shows_login_error_if_token_invalid(self):\n    response = self.client.get(\"/accounts/login?token=invalid-token\", follow=True)\n    user = auth.get_user(self.client)\n    self.assertEqual(user.is_authenticated, False)\n    message = list(response.context[\"messages\"])[0]\n    self.assertEqual(\n        message.message,\n        \"Invalid login link, please request a new one\",\n    )\n    self.assertEqual(message.tags, \"error\")\n----\n====\n\nAnd now we get that passing by using the most straightforward implementation...\n\n\n[role=\"sourcecode small-code\"]\n.src/accounts/views.py (ch21l005)\n====\n[source,python]\n----\ndef login(request):\n    if Token.objects.filter(uid=request.GET[\"token\"]).exists():  # <1>\n        user = User.objects.create(email=\"edith@example.com\")  # <2> <3>\n        auth.login(request, user)\n    else:\n        messages.error(request, \"Invalid login link, please request a new one\")  # <4>\n    return redirect(\"/\")\n----\n====\n\n<1> Oh wait; we forgot about our authentication backend\n    and just did the query directly from the token model!\n    Well that's arguably more straightforward,\n    but how do we force ourselves to write the code the way we want to—i.e.,\n    using Django's authentication API?\n\n<2> Oh dear, and the email address is still hardcoded.\n    We might have to think about writing an extra test to force ourselves to fix that.\n\n\n<3> Oh--also, we're hardcoding the creation of a user every time,\n    but actually, we want to have the get-or-create logic\n    that we implemented in our backend.\n\n<4> This bit is OK at least!\n\nIs this starting to feel a bit familiar?\nWe've already written all the tests for the various permutations of our authentication logic,\nand we're considering writing equivalent tests at the views layer.\n\n\n=== Combinatorial Explosion\n\n<<table-21-1>> recaps the tests we might want to write at each layer in our application.(((\"combinatorial explosion\")))\n\n[[table-21-1]]\n.What we want to test in each layer\n|=======\n|Views layer| Authentication backend | Models layer\n\na| * Valid token means user is logged in\n  * Invalid token means user is not logged in\n\na| * Returns correct existing user for a valid token\n  * Creates a new user for a new email address\n  * Returns `none` for an invalid token\n\na| * Token associates email and UID\n  * User can be retrieved from token UID\n|=======\n\nWe already have three tests in the models layer, and five in the authentication layer.\nWe started off writing the tests in the views layer,\nwhere—_conceptually_—we only really want two test cases,\nand we're finding ourselves wondering if we need to write\na whole bunch of tests that essentially duplicate the authentication layer tests. This is an example of the _combinatorial explosion_ problem.\n\n\n==== The Car Factory Example\n\nImagine we're testing a car factory:\n\n* First, we choose the car type: normal, station-wagon, or convertible.\n* Then, we choose the engine type: petrol, diesel, or electric.\n* Finally, we choose the colour: red, white, or hot pink.\n\n[role=\"pagebreak-before\"]\nHere's how it might look in code:\n\n[role=\"skipme\"]\n[source,python]\n----\ndef build_car(car_type, engine_type, colour):\n    engine = _create_engine(engine_type)\n    naked_car = _assemble_car(engine, car_type)\n    finished_car = _paint_car(naked_car, colour)\n    return finished_car\n----\n\nHow many tests do we need?  Well, the upper bound to test every possible combination\nis 3 &times; 3 &times; 3 = 27 tests.  That's a lot!\n\nHow many tests do we _actually_ need to write?\nWell, it depends on how we're testing, how the different parts of the factory are integrated,\nand what we know about the system. Do we need to test every single colour? Maybe!\nOr, maybe, if we're happy that we can do two different colours, then we're happy that we can do any number—whether it's two, three, or hundreds.  Perhaps we need two tests, maybe three.\n\nOK, but do we need to test that painting works for all the different engine types?\nWell, the painting process is probably independent of engine type:\nif we can paint a diesel in red, we can paint it in pink or white too.\n\nBut, perhaps it _is_ affected by the car type:\npainting a convertible with a fabric roof\nmight be a very different technological process to painting a hard-bodied car. So, we'd probably want to test that painting _in general_ works for each car type (three tests),\nbut we don't need to test that painting works for every engine type.\n\nWhat we're analysing here is the level of \"coupling\" between the different parts of the system.\nPainting is tightly coupled to car type, but not to engine type.\nPainting \"needs to know\" about car types, but it does not \"need to know\" about engine types.\n\n\nTIP: The more tightly coupled two parts of the system are,\n    the more tests you'll need to write to cover all the combinations of their behaviour.\n\nAnother way of thinking about it is: what level are we writing tests at?\nYou can choose to write low-level tests that cover only one part of the assembly process,\nor higher-level ones that test several steps together—or perhaps all of them end-to-end.\nSee <<car-factory-illustration>>.\n\n[[car-factory-illustration]]\n.Analysing how many tests are needed at different levels\nimage::images/tdd3_2101.png[\"An illustration of the car factory, with boxes for each step in the process (build engine, assemble, paint), and descriptions of testing each step separately vs testing them in combination.\"]\n// CSANAD: just a tiny thing: in the diagram, below the \"Paint\" box, there is\n// an apostrophe missing in \"engine type doesn't matter\".\n\n// SEBASTIAN: How about splitting this big image into several smaller ones? At the first encounter, I skipped it only to discover I need to jump up and down to have visualizations of paragraphs below.\n//      Not a showstopper, tho.\n\nAnalysing things in these terms,\nwe think about the inputs and outputs that apply to each type of test,\nas well as which attributes of the inputs matter, and which don't.\n\nTesting the first stage of the process—building the engine—is straightforward.  The \"engine type\" input has three possible values, so we need three tests of the output, which is the engine.\nIf we're testing at the end-to-end level, no matter how many tests we have in total,\nwe know we'll need at least three to be the tests\nthat check if we can produce a car with a working engine of each type.\n\nTesting the painting needs a bit more thought.\nIf we test at the low level, the inputs are a naked car and a paint colour.\nThere are theoretically nine types of naked car; do we need to test all of them?\nNo. The engine type doesn't matter; we only need to test one of each body type.\nDoes that mean 3 &times; 3 = 9 tests?  No.  The colour and body type are independent.\nWe can just test that all three colours work, and that all three body types work—so that's six tests.\n\nWhat about at the end-to-end level?\nIt depends if we're being rigorous about \"closed-box\" testing,\nwhere we're not supposed to know anything about how the production process works.\nIn that case, maybe we _do_ need 27 tests.\nBut if we allow that we know about the internals,\nthen we can apply similar reasoning to what we used at the lower level.\nHowever many tests we end up with,\nwe need three of them to be checking each colour,\nand three that check that each body type can be painted.\n\nLet's see if we can apply this sort of analysis to our authentication system.\n\n\n=== Using Mocks to Test Parts of Our System in Isolation\n\nTo recap, so far we have some minimal tests at the models layer,\nand we have comprehensive tests of our authentication backend,\nand we're now wondering how many tests we need at the views layer.\n\n\nHere's the current state of our view:\n\n[role=\"sourcecode currentcontents\"]\n.src/accounts/views.py\n====\n[source,python]\n----\ndef login(request):\n    if Token.objects.filter(uid=request.GET[\"token\"]).exists():\n        user = User.objects.create(email=\"edith@example.com\")\n        auth.login(request, user)\n    else:\n        messages.error(request, \"Invalid login link, please request a new one\")\n    return redirect(\"/\")\n----\n====\n\nWe know we want to transform it to something like this:\n\n\n[role=\"sourcecode skipme small-code\"]\n.src/accounts/views.py\n====\n[source,python]\n----\ndef login(request):\n    if user := auth.authenticate(uid=request.GET.get(\"token\"))  # <1>\n        auth.login(request, user)  # <2>\n    else:\n        messages.error(request, \"Invalid login link, please request a new one\")  # <3>\n\n    return redirect(\"/\")\n----\n====\n\n<1> We want to refactor our logic to use the `authenticate()` function\n    from our backend.  Really good place for a walrus (`:=`) too!\n<2> We have the happy path where the user gets logged in.\n<3> We have the unhappy path where the user gets an error message instead.\n\nBut currently, our tests are letting us \"get away\" with\nthe wrong implementation. Here are three possible options for getting ourselves to the right state:\n\n1. Add more tests for all possible combinations at the view level\n  (token exists but no user, token exists for an existing user, invalid token,\n  etc.), until we end up duplicating all the logic in the auth backend in our view—and then feel justified in refactoring across to just calling the auth backend.\n\n2. Stick with our current two tests, and decide it's OK to refactor already.\n\n3. Test the view in isolation, using mocks to verify that we call the auth backend.\n\n\nEach option has pros and cons!  If I was going for option (1),\nessentially going all in on test coverage at the views layer,\nI'd probably think about deleting all the tests at the auth layer afterwards.\n\nIf you were to ask me what my personal preference or instinctive choice would be,\nI'd say at this point it might be to go with (2),\nand say with one happy-path and one unhappy-path test,\nwe're OK to refactor and switch across already.\n\nBut because this chapter is about mocks, let's investigate option (3) instead.\nBesides, it'll be an excuse to do fun things with them,\nlike playing with `.return_value`.\n\n(((\"mocks\", \"reducing duplication with\", id=\"Mreduce19\")))\n(((\"duplication, eliminating\", id=\"dupel19\")))\nSo far, we've used mocks to test external dependencies,\nlike Django's mail-sending function.\nThe main reason to use a mock we've discussed so far is to isolate ourselves from external side effects—in this case, to avoid sending out actual emails during our tests.\n\nIn this section, we'll look at a different possible use case for mocks: testing parts of our _own_ code in isolation from each other,\nas a way of reducing duplication and avoiding combinatorial explosion in our tests.\n\n\n==== Mocks Can Also Let You Test the Implementation, When It Matters\n\n\nOn top of that, the fact that we're using the Django `auth.authenticate` function\nrather than calling our own code directly is relevant.\nDjango has already introduced an abstraction:\nto decouple the specifics of authentication backends\nfrom the views that use them.\nThis makes it easier for us to add further backends in future.\n\nSo in this case\n(in contrast to the example in  <<mocks-tightly-coupled-sidebar>>)\nthe implementation _does_ matter,\nbecause we've decided to use a particular, specific interface to implement our authentication system. This is something we might want to document and verify in our tests—and mocks are one way to enable that.\n\n// SEBASTIAN: I am missing one crucial sentence here - that this Django-provided abstraction IS STABLE, so it's safe to mock it.\n//      This is part of a public Django API, meaning it's not going anywhere soon or without breaking backwards-compatibility. That would of course be not welcomed by Django users :)\n// HARRY - otoh, \"don't mock what you don't own\".\n// some ppl would say, better to write your own wrapper around any third party api,\n// and then your mock doesn't need to change if the third party api changes.\n\n[role=\"pagebreak-before less_space\"]\n=== Starting Again: Test-Driving Our Implementation with Mocks\n\nLet's see how things would look if we had decided to test-drive our implementation with mocks in the first place.\nWe'll start by reverting all the authentication stuff,\nboth from our test and from our view.\n\nLet's disable the test first (we can re-enable them later to sense-check things):\n\n[role=\"sourcecode small-code\"]\n.src/accounts/tests/test_views.py (ch21l006)\n====\n[source,python]\n----\nclass LoginViewTest(TestCase):\n    def test_redirects_to_home_page(self):  <1>\n        [...]\n    def DONT_test_logs_in_if_given_valid_token(self):  <2>\n        [...]\n    def DONT_test_shows_login_error_if_token_invalid(self):  <2>\n        [...]\n----\n====\n\n<1> We can leave the test for the redirect, as that doesn't involve the auth framework.\n<2> We change the test name so it no longer starts with `test_`,\n    using a highly noticeable set of capital letters\n    so we don't forget to come back and re-enable them later.\n    I call this \"DONTifying\" tests. :)\n\n\nNow let's revert the view, and replace our hacky code with some to-dos:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l007)\n====\n[source,python]\n----\n# from django.contrib import auth, messages  # <1>\nfrom django.contrib import messages\n[...]\n\n\ndef login(request):\n    # TODO: call authenticate(),  # <2>\n    # then auth.login() with the user if we get one,\n    # or messages.error() if we get None.\n    return redirect(\"/\")\n----\n====\n\n<1> In order to demonstrate a common error message shortly,\n    I'm also reverting our import of the `contrib.auth` module.\n\n<2> And here's where we delete our first implementation\n    and replace it with some to-dos.\n\n[role=\"pagebreak-before\"]\nLet's check that all our tests pass:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nRan 15 tests in 0.021s\n\nOK\n----\n\n\nNow let's start again with mock-based tests.\nFirst, we can write a test that makes sure we call `authenticate()` correctly:\n\n[role=\"sourcecode small-code\"]\n.src/accounts/tests/test_views.py (ch21l008)\n====\n[source,python]\n----\nclass LoginViewTest(TestCase):\n    [...]\n\n    @mock.patch(\"accounts.views.auth\")  # <1>\n    def test_calls_authenticate_with_uid_from_get_request(self, mock_auth):  # <2>\n        self.client.get(\"/accounts/login?token=abcd123\")\n        self.assertEqual(\n            mock_auth.authenticate.call_args,  # <3>\n            mock.call(uid=\"abcd123\"),  # <4>\n        )\n----\n====\n\n<1> We expect to be using the `django.contrib.auth` module in _views.py_,\n    and we mock it out here.  Note that this time, we're not mocking out\n    a function; we're mocking out a whole module, and thus implicitly\n    mocking out all the functions (and any other objects) that module contains.\n\n<2> As usual, the mocked object is injected into our test method.\n\n<3> This time, we've mocked out a module rather than a function.\n    So we examine the `call_args`—not of the `mock_auth` module,\n    but of the `mock_auth.authenticate` function.\n    Because all the attributes of a mock are more mocks, that's a mock too.\n    You can start to see why `Mock` objects are so convenient,\n    compared to trying to build your own.\n\n<4> Now, instead of \"unpacking\" the call args, we use the `call` function\n    for a neater way of saying what it should have been called with--that is,\n    the token from the GET request.\n    (See <<mock-call-args-sidebar>>.)\n\n\n[role=\"less_space pagebreak-before\"]\n[[mock-call-args-sidebar]]\n.On Mock call_args\n*******************************************************************************\n\n(((\"call_args property\")))\nThe `.call_args` property on a mock represents the positional and keyword arguments\nthat the mock was called with.\nIt's a special \"call\" object type,\nwhich is essentially a tuple of `(positional_args, keyword_args)`.\n`positional_args` is itself a tuple,\nconsisting of the set of positional arguments.\n`keyword_args` is a dictionary. Here they all are in action:\n\n[role=\"small-code skipme\"]\n[source,python]\n----\n>>> from unittest.mock import Mock, call\n>>> m = Mock()\n>>> m(42, 43, 'positional arg 3', key='val', thing=666)\n<Mock name='mock()' id='139909729163528'>\n\n>>> m.call_args\ncall(42, 43, 'positional arg 3', key='val', thing=666)\n\n>>> m.call_args == ((42, 43, 'positional arg 3'), {'key': 'val', 'thing': 666})\nTrue\n>>> m.call_args == call(42, 43, 'positional arg 3', key='val', thing=666)\nTrue\n----\n\nSo in our test,  we could have done this instead:\n\n[role=\"sourcecode skipme\"]\n.src/accounts/tests/test_views.py\n====\n[source,python]\n----\n    self.assertEqual(\n        mock_auth.authenticate.call_args,\n        ((,), {'uid': 'abcd123'})\n    )\n    # or this\n    args, kwargs = mock_auth.authenticate.call_args\n    self.assertEqual(args, (,))\n    self.assertEqual(kwargs, {'uid': 'abcd123'})\n----\n====\n\nBut you can see how using the `call` helper is nicer.\n\nSee also <<avoid-assert-called-with-sidebar>>,\nfor some discussion of `call_args` versus the magic `assert_called_with` methods.\n\n*******************************************************************************\n\n\nWhat happens when we run the test?   The first error is this:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nAttributeError: <module 'accounts.views' from\n'...goat-book/src/accounts/views.py'> does not have the attribute 'auth'\n----\n\nTIP: `module foo does not have the attribute bar`\n    is a common first failure in a test that uses mocks.\n    It's telling you that you're trying to mock out something\n    that doesn't yet exist (or isn't yet imported)\n    in the target module.\n\n\nOnce we reimport `django.contrib.auth`, the error changes:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l009)\n====\n[source,python]\n----\nfrom django.contrib import auth, messages\n[...]\n----\n====\n\nNow we get:\n\n\n[subs=\"specialcharacters,macros\"]\n----\nFAIL: test_calls_authenticate_with_uid_from_get_request [...]\n[...]\nAssertionError: None != call(uid='abcd123')\n----\n\nIt's telling us that the view doesn't call the `auth.authenticate` function at all.\nLet's fix that, but get it deliberately wrong, just to see:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l010)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    auth.authenticate(\"bang!\")\n    # then auth.login() with the user if we get one,\n    # or messages.error() if we get None.\n    return redirect(\"/\")\n----\n====\n\n\nBang, indeed!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nAssertionError: call('bang!') != call(uid='abcd123')\n[...]\nFAILED (failures=1)\n----\n\nLet's give `authenticate` the arguments it expects then:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l011)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    auth.authenticate(uid=request.GET[\"token\"])\n    # then auth.login() with the user if we get one,\n    # or messages.error() if we get None.\n    return redirect(\"/\")\n----\n====\n\n[role=\"pagebreak-before\"]\nThat gets us to passing tests:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\nRan 16 tests in 0.023s\n\nOK\n----\n\n==== Using mock.return_value\n\n(((\"mocks\", \"mock.return_value\")))\nNext, we want to check that if the authenticate function returns a user,\nwe pass that into `auth.login`.  Let's see how that test looks:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch21l012)\n====\n[source,python]\n----\n@mock.patch(\"accounts.views.auth\")  # <1>\ndef test_calls_auth_login_with_user_if_there_is_one(self, mock_auth):\n    response = self.client.get(\"/accounts/login?token=abcd123\")\n    self.assertEqual(\n        mock_auth.login.call_args,  # <2>\n        mock.call(\n            response.wsgi_request,  # <3>\n            mock_auth.authenticate.return_value,  # <4>\n        ),\n    )\n----\n====\n\n<1> We mock the `contrib.auth` module again.\n\n<2> This time we examine the call args for the `auth.login` function.\n\n<3> We check that it's called with the request object that the view sees...\n\n<4> ...and we check that the second argument was\n    \"whatever the `authenticate()` function returned\".\n    Because `authenticate()` is also mocked out,\n    we can use its special `.return_value` attribute.\n    We know that, in real life, that will be a user object.\n    But in this test, it's all mocks.\n    Can you see what I mean about mocky tests being hard to understand sometimes?\n\nWhen you call a mock, you get another mock.\nBut you can also get a copy of that returned mock from the original mock that you called.\nBoy, it sure is hard to explain this stuff without saying \"mock\" a lot!\nAnother little console illustration might help:\n\n[role=\"skipme\"]\n[source,python]\n----\n>>> m = Mock()\n>>> thing = m()\n>>> thing\n<Mock name='mock()' id='140652722034952'>\n>>> m.return_value\n<Mock name='mock()' id='140652722034952'>\n>>> thing == m.return_value\nTrue\n----\n\n\n[[avoid-assert-called-with-sidebar]]\n.Avoid Mock's Magic assert_called...Methods?\n*******************************************************************************\n\nIf you've used `unittest.mock` before, you may have come across its special\n`assert_called...`\nhttps://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called[methods],\nand you may be wondering why I didn't use them.\n\nFor example, instead of doing:\n\n[role=\"skipme\"]\n[source,python]\n----\nself.assertEqual(a_mock.call_args, call(foo, bar))\n----\n\nYou can just do:\n\n[role=\"skipme\"]\n[source,python]\n----\na_mock.assert_called_with(foo, bar)\n----\n\nAnd the _mock_ library will raise an `AssertionError` for you if there is a\nmismatch.\n\nWhy not use that?  For me, the problem with these magic methods is that\nit's too easy to make a silly typo and end up with a test that always passes:\n\n[role=\"skipme\"]\n[source,python]\n----\na_mock.asssert_called_with(foo, bar)  # will always pass\n----\n\nUnless you get the magic method name exactly right,footnote:[\nThere was actually an attempt to mitigate this problem in Python 3.5,\nwith the addition of an `unsafe` argument that defaults to `False`,\nwhich will cause the mock to raise `AttributeError` for some common\nmisspellings of `assert_`.  Just not, for example, the one I'm using here—so I prefer not to rely on that. More info in the https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock[Python docs].]\nit will just silently return another mock,\nand you may not realise that you've written a test that tests nothing at all. That's why I prefer to always have an explicit `unittest` method in there.footnote:[\nIf you're using Pytest, there's an additional benefit to seeing the `assert` keyword\nrather than a normal method call: it makes the assert pop out.]\n\n*******************************************************************************\n\n\nIn any case, what do we get from running the test?\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\nAssertionError: None != call(<WSGIRequest: GET '/accounts/login?t[...]\n----\n\nSure enough, it's telling us that we're not calling `auth.login()` at all yet.\nLet's first try doing that deliberately wrong as usual!\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l013)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    auth.authenticate(uid=request.GET[\"token\"])\n    # then auth.login() with the user if we get one,\n    auth.login(\"ack!\")\n    # or messages.error() if we get None.\n    return redirect(\"/\")\n----\n====\n\nAck, indeed!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test accounts*]\n[...]\n\nERROR: test_redirects_to_home_page\n[...]\nTypeError: login() missing 1 required positional argument: 'user'\n\nFAIL: test_calls_auth_login_with_user_if_there_is_one [...]\n[...]\nAssertionError: call('ack!') != call(<WSGIRequest: GET\n'/accounts/login?token=[...]\n[...]\n\nRan 17 tests in 0.026s\n\nFAILED (failures=1, errors=1)\n----\n\nThat's one expected failure from our mocky test,\nand one (more) unexpected failure from the non-mocky test.\n\nLet's see if we can fix them:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l014)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    user = auth.authenticate(uid=request.GET[\"token\"])\n    # then auth.login() with the user if we get one,\n    auth.login(request, user)\n    # or messages.error() if we get None.\n    return redirect(\"/\")\n----\n====\n\n\nWell, that does fix our mocky test, but not the other one;\nit now has a slightly different complaint:\n\n[subs=\"specialcharacters,macros\"]\n----\nERROR: test_redirects_to_home_page\n(accounts.tests.test_views.LoginViewTest.test_redirects_to_home_page)\n[...]\n  File \"...goat-book/src/accounts/views.py\", line 33, in login\n    auth.login(request, user)\n[...]\nAttributeError: 'AnonymousUser' object has no attribute '_meta'\n----\n\nIt's because we're still calling `auth.login` indiscriminately on any kind of user,\nand that's causing problems back in our original test for the redirect,\nwhich _isn't_ currently mocking out `auth.login`.\n\n[role=\"pagebreak-before\"]\nWe can get back to passing like this:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l015)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    if user := auth.authenticate(uid=request.GET[\"token\"]):\n        # then auth.login() with the user if we get one,\n        auth.login(request, user)\n----\n====\n\n\nThis gets our unit test passing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\n\nOK\n----\n\n\n==== Using .return_value During Test Setup\n\nI'm a little nervous that we've introduced an `if` without an _explicit_ test for it.\nTesting the unhappy path will reassure me.\nWe can use our existing test for the error case to crib from.\n\nWe want to be able to set up our mocks to say:\n`auth.authenticate()` should return `None`.\nWe can do that by setting the `.return_value` on the mock:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch21l016)\n====\n[source,python]\n----\n    @mock.patch(\"accounts.views.auth\")\n    def test_adds_error_message_if_auth_user_is_None(self, mock_auth):\n        mock_auth.authenticate.return_value = None  # <1>\n\n        response = self.client.get(\"/accounts/login?token=abcd123\", follow=True)\n\n        message = list(response.context[\"messages\"])[0]\n        self.assertEqual(  # <2>\n            message.message,\n            \"Invalid login link, please request a new one\",\n        )\n        self.assertEqual(message.tags, \"error\")\n----\n====\n\n<1> We use `.return_value` on our mock once again.\n    But this time, we assign to it _before_ it's used\n    (in the setup part of the test—aka the \"arrange\" or \"given\" phase),\n    rather than reading from it (in the assert/“when” part),\n    as we did earlier.\n\n<2> Our asserts are copied across from the existing test for the error case,\n    `DONT_test_shows_login_error_if_token_invalid()`.\n\n[role=\"pagebreak-before\"]\nThat gives us this somewhat cryptic, but expected failure:\n\n----\nERROR: test_adds_error_message_if_auth_user_is_None [...]\n[...]\n    message = list(response.context[\"messages\"])[0]\n              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\nIndexError: list index out of range\n----\n\nEssentially, that's saying there are no messages in our response. We can get it passing like this, starting with a deliberate mistake as always:\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l017)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    if user := auth.authenticate(uid=request.GET[\"token\"]):\n        # then auth.login() with the user if we get one,\n        auth.login(request, user)\n    else:\n        # or messages.error() if we get None.\n        messages.error(request, \"boo\")\n    return redirect(\"/\")\n----\n====\n\nWhich gives us:\n\n----\nAssertionError: 'boo' != 'Invalid login link, please request a new one'\n----\n\nAnd so:\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l018)\n====\n[source,python]\n----\ndef login(request):\n    # TODO: call authenticate(),\n    if user := auth.authenticate(uid=request.GET[\"token\"]):\n        # then auth.login() with the user if we get one,\n        auth.login(request, user)\n    else:\n        # or messages.error() if we get None.\n        messages.error(request, \"Invalid login link, please request a new one\")\n    return redirect(\"/\")\n----\n====\n\nNow our tests pass:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\n\nRan 18 tests in 0.025s\n\nOK\n----\n\n[role=\"pagebreak-before\"]\nAnd we can do a final refactor to remove those comments:\n\n\n\n[role=\"sourcecode\"]\n.src/accounts/views.py (ch21l019)\n====\n[source,python]\n----\nfrom accounts.models import Token  # <1>\n[...]\n\n\ndef login(request):  # <2>\n    if user := auth.authenticate(uid=request.GET[\"token\"]):\n        auth.login(request, user)\n    else:\n        messages.error(request, \"Invalid login link, please request a new one\")\n    return redirect(\"/\")\n----\n====\n\n<1> We no longer need to explicitly import the user model\n<2> and our view is down to just five lines.\n\nLovely!  What's next?\n(((\"\", startref=\"Mreduce19\")))(((\"\", startref=\"dupel19\")))\n\n\n==== UnDONTifying\n\nRemember we still have the DONTified, non-mocky tests?\nLet's re-enable now to sense-check that our mocky tests have driven\nus to the right place:\n\n\n[role=\"sourcecode small-code\"]\n.src/accounts/tests/test_views.py (ch21l020)\n====\n[source,diff]\n----\n@@ -63,7 +63,7 @@ class LoginViewTest(TestCase):\n         response = self.client.get(\"/accounts/login?token=abcd123\")\n         self.assertRedirects(response, \"/\")\n\n-    def DONT_test_logs_in_if_given_valid_token(self):\n+    def test_logs_in_if_given_valid_token(self):\n         anon_user = auth.get_user(self.client)\n         self.assertEqual(anon_user.is_authenticated, False)\n\n@@ -74,7 +74,7 @@ class LoginViewTest(TestCase):\n         self.assertEqual(user.is_authenticated, True)\n         self.assertEqual(user.email, \"edith@example.com\")\n\n-    def DONT_test_shows_login_error_if_token_invalid(self):\n+    def test_shows_login_error_if_token_invalid(self):\n         response = self.client.get(\"/accounts/login?token=invalid-token\", follow=True)\n----\n====\n\n\nSure enough, they both pass:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\nRan 20 tests in 0.025s\n\nOK\n----\n\n\n=== Deciding Which Tests to Keep\n\n\nWe now definitely have duplicate tests:\n\n\n[role=\"sourcecode skipme\"]\n.src/accounts/tests/test_views.py\n====\n[source,python]\n----\nclass LoginViewTest(TestCase):\n    def test_redirects_to_home_page(self):\n        [...]\n\n    def test_logs_in_if_given_valid_token(self):\n        [...]\n\n    def test_shows_login_error_if_token_invalid(self):\n        [...]\n\n    @mock.patch(\"accounts.views.auth\")\n    def test_calls_authenticate_with_uid_from_get_request(self, mock_auth):\n        [...]\n\n    @mock.patch(\"accounts.views.auth\")\n    def test_calls_auth_login_with_user_if_there_is_one(self, mock_auth):\n        [...]\n\n    @mock.patch(\"accounts.views.auth\")\n    def test_adds_error_message_if_auth_user_is_None(self, mock_auth):\n        [...]\n----\n====\n\nThe redirect test could stay the same whether we're using mocks or not.\nWe then have two non-mocky tests for the happy and unhappy paths,\nand three mocky tests:\n\n. One checks that we are integrated with our auth backend correctly.\n. One checks that we call the built-in `auth.login` function correctly,\n  which tests the happy path.\n. And one checks we set an error message in the unhappy path.\n\n[role=\"pagebreak-before\"]\nI think there are lots of ways to justify different choices here,\nbut my instinct tends to be to avoid using mocks if you can.\nSo, I propose we delete the two mocky tests for the happy and unhappy paths,\nas they are reasonably covered by the non-mocky ones.\nBut I think we can justify keeping the first mocky test,\nbecause it adds value by checking that we're doing our authentication\nthe \"right\" way—i.e., by calling into Django's `auth.authenticate()` function\n(instead of, for example, instantiating and calling our auth backend ourselves,\nor even just implementing authentication inline in the view).\n\n\nTIP: \"Test behaviour, not implementation\" is a GREAT rule of thumb for tests.\n    But sometimes, the fact that you're using one implementation rather than another\n    really is important.  In these cases, a mocky test can be useful.\n\n\nSo let's delete our last two mocky tests.\nI'm also going to rename the remaining one to make our intention clear;\nwe want to check we are using the Django auth library:\n\n\n[role=\"sourcecode\"]\n.src/accounts/tests/test_views.py (ch21l021)\n====\n[source,python]\n----\n    @mock.patch(\"accounts.views.auth\")\n    def test_calls_django_auth_authenticate(self, mock_auth):\n        [...]\n----\n====\n// CSANAD: I think the `diff` style snippets are better for renaming things.\n\nAnd we're down to 18 tests:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test accounts*\n[...]\nRan 18 tests in 0.015s\n\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\n=== The Moment of Truth:  Will the FT Pass?\n\n(((\"mocks\", \"functional test for\")))\n(((\"functional tests (FTs)\", \"for mocks\", secondary-sortas=\"mocks\")))\nWe're just about ready to try our functional test!\nLet's just make sure our base template shows a different navbar\nfor logged-in and non–logged-in users.\nOur FT relies on being able to see the user's email in the navbar\nin the logged-in state, and it needs a \"Log out\" button too:\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch21l022)\n====\n[source,html]\n----\n<nav class=\"navbar\">\n  <div class=\"container-fluid\">\n    <a class=\"navbar-brand\" href=\"/\">Superlists</a>\n    {% if user.email %}  <1>\n      <span class=\"navbar-text\">Logged in as {{ user.email }}</span>\n      <form method=\"POST\" action=\"TODO\">\n        {% csrf_token %}\n        <button id=\"id_logout\" class=\"btn btn-outline-secondary\" type=\"submit\">\n          Log out\n        </button>\n      </form>\n    {% else %}\n      <form method=\"POST\" action=\"{% url 'send_login_email' %}\">\n        <div class=\"input-group\">\n          <label class=\"navbar-text me-2\" for=\"id_email_input\">\n            Enter your email to log in\n          </label>\n          <input\n            id=\"id_email_input\"\n            name=\"email\"\n            class=\"form-control\"\n            placeholder=\"your@email.com\"\n          />\n          {% csrf_token %}\n        </div>\n      </form>\n    {% endif %}\n  </div>\n</nav>\n----\n====\n\n<1> Here's a new `{% if %}`, and navbar content for logged-in users.\n\n[role=\"pagebreak-before\"]\nOK, there's a to-do in there about the log-out button. We'll get to that, but how does our FT look now?\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\n.\n ---------------------------------------------------------------------\nRan 1 test in 3.282s\n\nOK\n----\n\n\n\n=== It Works in Theory!  Does It Work in Practice?\n\n\n(((\"mocks\", \"practical application of\")))\nWow! Can you believe it?  I scarcely can!\nTime for a manual look around with `runserver`:\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py runserver*]\n[...]\nInternal Server Error: /accounts/send_login_email\nTraceback (most recent call last):\n  File \"...goat-book/accounts/views.py\", line 20, in send_login_email\n\nConnectionRefusedError: [Errno 111] Connection refused\n----\n\n\n==== Using Our New Environment Variable, and Saving It to .env\n\nYou'll probably get an error, like I did, when you try to run things manually.\nIt's because of two things.\n\nFirstly, we need to re-add the email configuration to _settings.py_:\n\n// DAVID: Shouldn't we write a failing test first? If not, why not?\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch21l023)\n====\n[source,python]\n----\nEMAIL_HOST = \"smtp.gmail.com\"\nEMAIL_HOST_USER = \"obeythetestinggoat@gmail.com\"\nEMAIL_HOST_PASSWORD = os.environ.get(\"EMAIL_PASSWORD\")\nEMAIL_PORT = 587\nEMAIL_USE_TLS = True\n----\n====\n\nSecondly, we (probably) need to reset the `EMAIL_PASSWORD` in our shell:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *export EMAIL_PASSWORD=\"yoursekritpasswordhere\"*\n----\n\n[role=\"pagebreak-before less_space\"]\n.Using a Local .env File for Development\n*******************************************************************************\n\nUntil now, we've not needed to \"save\" any of our local environment variables,\nbecause the command-line ones are easy to remember and type,\nand we've made sure all the other ones that affect config settings have sensible defaults for dev.\nBut there's just no way to get a working login system without this one!\n\nRather than having to go look up this password every time you start a new shell,\nit's quite common to save these sorts of settings into a local file\nin your project folder named `.env`.\nIt's a convention that makes it a hidden file, on Unix-like systems at least:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo .env >> .gitignore*  # we don't want to commit our secrets into git!\n$ *echo 'EMAIL_PASSWORD=\"yoursekritpasswordhere\"' >> .env*\n$ *set -a; source .env; set +a;*\n----\n\nIt does mean you have to remember to do that weird `set -a; source...` dance,\nevery time you start working on the project,\nas well as remembering to activate your virtualenv.\n\nIf you search or ask around, you'll find there are some tools and shell plugins\nthat load virtualenvs and _.env_ files automatically, or Django plugins that handle this stuff too. A few options:\n\n* Django-specific:\n  https://django-environ.readthedocs.io[django-environ] or\n  https://github.com/jpadilla/django-dotenv[django-dotenv]\n* More general Python project management: https://docs.pipenv.org[Pipenv]\n* Or even: https://oreil.ly/F9iV3[roll your own]\n\n*******************************************************************************\n\nAnd now...\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py runserver*\n----\n\n...you should see something like <<despiked-success-message>>.\n\n\n[[despiked-success-message]]\n.Check your email...\nimage::images/tdd3_2102.png[\"De-spiked site with success message\"]\n\nWoohoo!\n\nI've been waiting to do a commit up until this moment, just to make sure\neverything works.  At this point, you could make a series of separate\ncommits--one for the login view, one for the auth backend, one for\nthe user model, one for wiring up the template.  Or you could decide that—because they're all interrelated, and none will work without the others—you may as well just have one big commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git status*\n$ *git add .*\n$ *git diff --staged*\n$ *git commit -m \"Custom passwordless auth backend + custom user model\"*\n----\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _[strikethrough line-through]#Authentication backend with authenticate() and get_user() functions#_\n* _[strikethrough line-through]#Registering auth backend in settings.py#_\n* _[strikethrough line-through]#Login view calls authenticate() and login() from django.contrib.auth#_\n* _Logout view calls django.contrib.auth.logout_\n*****\n\n\n=== Finishing Off Our FT: Testing Logout\n\n\n(((\"mocks\", \"logout link\")))\nThe last thing we need to do before we call it a day is to test the logout button.\nWe extend the FT with a couple more steps:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/test_login.py (ch21l024)\n====\n[source,python]\n----\n        [...]\n        # she is logged in!\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\"),\n        )\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertIn(TEST_EMAIL, navbar.text)\n\n        # Now she logs out\n        self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\").click()\n\n        # She is logged out\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"input[name=email]\")\n        )\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertNotIn(TEST_EMAIL, navbar.text)\n----\n====\n\nWith that, we can see that the test is failing because the logout button\ndoesn't have a valid URL to submit to:\n\n[subs=\"\"]\n----\n$ <strong>python src/manage.py test functional_tests.test_login</strong>\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: input[name=email]; [...]\n----\n\n\nSo, let's tell the base template that we want a new URL named \"logout\":\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch21l025)\n====\n[source,html]\n----\n          {% if user.email %}\n            <span class=\"navbar-text\">Logged in as {{ user.email }}</span>\n            <form method=\"POST\" action=\"{% url 'logout' %}\">\n              {% csrf_token %}\n              <button id=\"id_logout\" class=\"btn btn-outline-secondary\" type=\"submit\">\n                Log out\n              </button>\n            </form>\n          {% else %}\n----\n====\n\nIf you try the FTs at this point,\nyou'll see an error saying that the URL doesn't exist yet:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\nInternal Server Error: /\n[...]\ndjango.urls.exceptions.NoReverseMatch: Reverse for 'logout' not found. 'logout'\nis not a valid view function or pattern name.\n\n======================================================================\nERROR: test_login_using_magic_link\n(functional_tests.test_login.LoginTest.test_login_using_magic_link)\n[...]\n\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: #id_logout; [...]\n----\n\n\n\nImplementing a logout URL is actually very simple:\nwe can use Django's\nhttps://docs.djangoproject.com/en/5.2/topics/auth/default/#module-django.contrib.auth.views[built-in logout view],\nwhich clears down the user's session and redirects them to a page of our choice:\n\n[role=\"sourcecode small-code\"]\n.src/accounts/urls.py (ch21l026)\n====\n[source,python]\n----\nfrom django.contrib.auth import views as auth_views\nfrom django.urls import path\n\nfrom . import views\n\nurlpatterns = [\n    path(\"send_login_email\", views.send_login_email, name=\"send_login_email\"),\n    path(\"login\", views.login, name=\"login\"),\n    path(\"logout\", auth_views.LogoutView.as_view(next_page=\"/\"), name=\"logout\"),\n]\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd that gets us a fully passing FT--indeed, a fully passing test suite:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *python src/manage.py test functional_tests.test_login*\n[...]\nOK\n$ *cd src && python manage.py test*\n[...]\nRan 56 tests in 78.124s\n\nOK\n----\n\n\nWARNING: We're nowhere near a truly secure or acceptable login system here.\n    As this is just an example app for a book, we'll leave it at that,\n    but in \"real life\" you'd want to explore a lot more security\n    and usability issues before calling the job done.\n    We're dangerously close to \"rolling our own crypto\" here,\n    and relying on a more established login system would be much safer.\n    Read more at https://security.stackexchange.com/a/18198.\n    (((\"security issues and settings\", \"login systems\")))\n\n// CSANAD: for demonstrating a security issue with our current, custom\n// authentication, we could mention that after logout, we can log in using any\n// of the previous login magic links (there is no token invalidation)\n\n[role=\"scratchpad\"]\n*****\n* _[strikethrough line-through]#Token model with email and UID#_\n* _[strikethrough line-through]#View to create token and send login email incl. url w/ token UID#_\n* _[strikethrough line-through]#Custom user model with USERNAME_FIELD=email#_\n* _[strikethrough line-through]#Authentication backend with authenticate() and get_user() functions#_\n* _[strikethrough line-through]#Registering auth backend in settings.py#_\n* _[strikethrough line-through]#Login view calls authenticate() and login() from django.contrib.auth#_\n* _[strikethrough line-through]#Logout view calls django.contrib.auth.logout#_\n*****\n\nIn the next chapter, we'll start trying to put our login system to good use.\nIn the meantime, do a commit and enjoy this recap.\n\n[role=\"pagebreak-before less_space\"]\n[[mocking-py-sidebar]]\n.On Mocking in Python\n*******************************************************************************\n\nUsing mock.return_value::\n  The `.return_value` attribute on a mock can be used in two ways.\n  You can _read_ it, to access the return value of a mocked-out function,\n  and thus check on how it gets used later in your code;\n  this usually happens in the \"assert\" or \"then\" part of your test.\n  Alternatively, you can _assign_ to it,\n  usually up-front in the \"arrange\" or \"given\" part of your test,\n  as a way of saying\n  \"I want this mocked-out function to return a particular value\".\n\nMocks can ensure test isolation and reduce duplication::\n  You can use mocks to isolate different parts of your code from each other,\n  and thus test them independently.\n  This can help you to avoid duplication,\n  because you're only testing a single layer at a time,\n  rather than having to think about combinations of interactions\n  of different layers.\n  Used extensively, this approach leads to \"London-style\" TDD,\n  but that's quite different from the style I mostly follow and show in this book.\n  (((\"mocks\", \"reducing duplication with\")))\n  (((\"duplication, eliminating\")))\n\nMocks can enable you to verify implementation details::\n  Most tests should test behaviour, not implementation.\n  At some point though, we decided using a particular implementation\n  _was_ important. And so, we used a mock as a way to verify that,\n  and to document it for our future selves.\n\nThere are alternatives to mocks, but they require rethinking how your code is structured::\n  In a way, mocks make it \"too easy\".\n  In programming languages\n  that lack Python's dynamic ability to monkeypatch things at runtime,\n  developers have had to work on alternative ways to test code with dependencies.\n  While these techniques can be more complex,\n  they do force you to think about how your code is structured—to cleanly identify your dependencies and build clean abstractions and interfaces around them.\n  Further discussion is beyond the scope of this book,\n  but check out http://cosmicpython.com[Cosmic Python].\n\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_22_fixtures_and_wait_decorator.asciidoc",
    "content": "[[chapter_22_fixtures_and_wait_decorator]]\n== Test Fixtures and a Decorator [keep-together]#for Explicit Waits#\n\n(((\"authentication\", \"skipping in FTs\")))\nNow that we have a functional authentication system, we want to use it to identify users,\nand to show them all the lists they have created.\n\nTo do that, we're going to have to write FTs that have a logged-in user.\nRather than making each test go through the (time-consuming) login email dance,\nwe want to be able to skip that part.\n\nThis is about separation of concerns.(((\"functional  tests (FTs)\", \"versus unit tests\", secondary-sortas=\"unit\")))(((\"unit tests\", \"versus functional tests\", secondary-sortas=\"functional\")))\nFunctional tests aren't like unit tests,\nin that they don't usually have a single assertion.\nBut, conceptually, they should be testing a single thing.\nThere's no need for every single FT to test the login/logout mechanisms.\nIf we can figure out a way to \"cheat\" and skip that part,\nwe'll spend less time waiting for tests to repeat these duplicated setup steps.\n\nTIP: Don't overdo de-duplication in FTs.\n      One of the benefits of an FT is that\n      it can catch strange and unpredictable interactions\n      between different parts of your application.\n\nIn this short chapter, we'll start writing our new FT,\nand use that as an opportunity to talk about de-duplication using test fixtures for FTs.\nWe'll also refactor out a nice helper for explicit waits,\nusing Python's lovely decorator syntax.\n\n\n=== Skipping the Login Process by Pre-creating a Session\n\n(((\"sessions\", \"pre-creating\", id=\"ix_sesspre\")))\n(((\"login process, skipping\", seealso=\"authentication\")))\n(((\"cookies\")))\nIt's quite common for a user to return to a site and still have a cookie,\nwhich means they are \"pre-authenticated\",\nso this isn't an unrealistic cheat at all.\nHere's how you can set it up:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch22l001)\n====\n[source,python]\n----\nfrom django.conf import settings\nfrom django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model\nfrom django.contrib.sessions.backends.db import SessionStore\n\nfrom .base import FunctionalTest\n\nUser = get_user_model()\n\n\nclass MyListsTest(FunctionalTest):\n    def create_pre_authenticated_session(self, email):\n        user = User.objects.create(email=email)\n        session = SessionStore()\n        session[SESSION_KEY] = user.pk # <1>\n        session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]\n        session.save()\n        ## to set a cookie we need to first visit the domain.\n        ## 404 pages load the quickest!\n        self.browser.get(self.live_server_url + \"/404_no_such_url/\")\n        self.browser.add_cookie(\n            dict(\n                name=settings.SESSION_COOKIE_NAME,\n                value=session.session_key,  # <2>\n                path=\"/\",\n            )\n        )\n----\n====\n\n<1> We create a session object in the database.  The session key is the\n    primary key of the user object (which is actually the user's email address).\n// CSANAD: there is a different suggested way of importing SessionStore, using\n// the SESSION_ENGINE from the `settings`:\n// https://docs.djangoproject.com/en/5.2/topics/http/sessions/#using-sessions-out-of-views\n\n<2> We then add a cookie to the browser that matches the session on the\n    server--on our next visit to the site, the server should recognise\n    us as a logged-in user.\n\n\nNote that, as it is, this will only work because we're using `LiveServerTestCase`,\nso the `User` and `Session` objects we create will end up in the same database\nas the test server.\nAt some point, we'll need to think about how this will work against Docker or staging.(((\"LiveServerTestCase\")))\n\n\n[role=\"pagebreak-before less_space\"]\n.Django Sessions: How a User's Cookies Tell the Server They Are Authenticated\n**********************************************************************\n\nThis is an attempt to explain sessions, cookies, and authentication in Django.\n\n(((\"authentication\", \"cookies and\")))\nHTTP is a \"stateless\" protocol,\nmeaning that the protocol itself doesn't keep track of any state from one\nrequest to the next, and each request is independent of the next.\nThere's no built-in way to tell that a series of requests come from the same client.\n\nFor this reason, servers need a way of recognising different clients with _every single request_.\nThe usual solution is to give each client a unique session ID,\nwhich the browser will store in a text file called a \"cookie\"\nand send with every request.(((\"cookies\", \"session\")))\n\nThe server will store that ID somewhere (by default, in the database),\nand then it can recognise each request that comes in\nas being from a particular client.\n\nIf you log in to the site using the dev server,\nyou can actually take a look at your session ID by hand if you like.\nIt's stored under the key `sessionid` by default.\nSee <<session-cookie-screenshot>>.\n\n[[session-cookie-screenshot]]\n.Examining the session cookie in the DevTools UI\nimage::images/tdd3_2201.png[\"A browser with the devtools open, showing a session cookie called sessionid for localhost:800\"]\n\n\nThese session cookies are set for all visitors to a Django site,\nwhether they're logged in or not.\n\nWhen we want to recognise a client as being a logged-in and authenticated user,\nagain, rather than asking the client to send their username and password\nwith every single request,\nthe server can actually just mark that client's session as authenticated,\nand associate it with a user ID in its database.(((\"user IDs (UIDs)\", \"for Django sessions\", secondary-sortas=\"Django\")))(((\"Django framework\", \"sessions\")))\n\nA Django session is a dictionary-like data structure,\nand the user ID is stored under the key given by `django.contrib.auth.SESSION_KEY`.\nYou can check this out in a [keep-together]#`./manage.py`# `shell` if you like:\n\n++++\n<pre translate=\"no\" data-type=\"programlisting\" class=\"skipme small-code\">$ <strong>python src/manage.py shell</strong>\n[...]\nIn [1]: from django.contrib.sessions.models import Session\n\n# substitute your session id from your browser cookie here\nIn [2]: session = Session.objects.get(\n    session_key=\"8u0pygdy9blo696g3n4o078ygt6l8y0y\"\n)\n\nIn [3]: print(session.get_decoded())\n{&#39;_auth_user_id&#39;: &#39;obeythetestinggoat&#64;gmail.com&#39;, &#39;_auth_user_backend&#39;:\n&#39;accounts.authentication.PasswordlessAuthenticationBackend&#39;}</pre>\n++++\n\n\nYou can also store any other information you like on a user's session,\nas a way of temporarily keeping track of some state.\nThis works for non–logged-in users too.(((\"sessions\", \"pre-creating\", startref=\"ix_sesspre\")))\nJust use `request.session` inside any view, and it works as a dictionary.\nThere's more information in the\nhttps://docs.djangoproject.com/en/5.2/topics/http/sessions[Django docs on sessions].\n\n\n**********************************************************************\n\n\n==== Checking That It Works\n\nTo check that the `create_pre_authenticated_session()`  system works,\nit would be good to reuse some of the code from our previous test.(((\"sessions\", \"testing pre-creation of sessions\")))\nLet's make a couple of functions: `wait_to_be_logged_in` and `wait_to_be_logged_out`.\nTo access them from a different test,\nwe'll need to pull them up into `FunctionalTest`.\nWe'll also tweak them slightly so that they can take an arbitrary email address\nas a parameter:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/base.py (ch22l002)\n====\n[source,python]\n----\nclass FunctionalTest(StaticLiveServerTestCase):\n    [...]\n\n    def wait_to_be_logged_in(self, email):\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\"),\n        )\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertIn(email, navbar.text)\n\n    def wait_to_be_logged_out(self, email):\n        self.wait_for(\n            lambda: self.browser.find_element(By.CSS_SELECTOR, \"input[name=email]\")\n        )\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertNotIn(email, navbar.text)\n----\n====\n\n\nHmm, that's not bad. But I'm not quite happy\nwith the amount of duplication of `wait_for` stuff in here.\nLet's make a note to come back to it and let's first get these helpers working:\n\n[role=\"scratchpad\"]\n*****\n* 'Clean up wait_for stuff in base.py.'\n*****\n\n\nFirst, we use them in 'test_login.py':\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_login.py (ch22l003)\n====\n[source,python]\n----\n    def test_login_using_magic_link(self):\n        [...]\n        # she is logged in!\n        self.wait_to_be_logged_in(email=TEST_EMAIL)\n\n        # Now she logs out\n        self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\").click()\n\n        # She is logged out\n        self.wait_to_be_logged_out(email=TEST_EMAIL)\n----\n====\n\nJust to make sure we haven't broken anything, we rerun the login test:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_login*]\n[...]\nOK\n----\n\nAnd now we can write a placeholder for the \"My lists\" test,\nto see if our pre-authenticated session creator really does work:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch22l004)\n====\n[source,python]\n----\n    def test_logged_in_users_lists_are_saved_as_my_lists(self):\n        email = \"edith@example.com\"\n        self.browser.get(self.live_server_url)\n        self.wait_to_be_logged_out(email)\n\n        # Edith is a logged-in user\n        self.create_pre_authenticated_session(email)\n        self.browser.get(self.live_server_url)\n        self.wait_to_be_logged_in(email)\n----\n====\n\nThat gets us:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\nOK\n----\n\n\nThat's a good place for a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/functional_tests*\n$ *git commit -m \"test_my_lists: precreate sessions, move login checks into base\"*\n----\n\n\n\n\n.JSON Test Fixtures Considered Harmful\n*******************************************************************************\n(((\"JSON fixtures\")))\n(((\"fixtures\", \"JSON fixtures\")))\n(((\"test fixtures\")))(((\"Django framework\", \"fixtures\")))\nWhen we pre-populate the database with test data—as we've done here with the `User` object and its associated `Session` object—what we're doing is setting up what's called a \"test fixture\".\n\nIf you look up \"Django fixtures\",\nyou'll find that Django has a built-in way of saving objects\nfrom your database using JSON (using `manage.py dumpdata`),\nand automatically loading them in your test runs\nusing the `fixtures` class attribute on `TestCase`.\n\nYou'll find people out there saying https://oreil.ly/Nklcr[not to use JSON fixtures],\nand I tend to agree.\nThey're a nightmare to maintain when your model changes.\nPlus, it's difficult for the reader\nto tell which of the many attribute values specified in the JSON\nare critical for the behaviour under test, and which of them are just filler.\n\nFinally, even if tests start out sharing fixtures,\nsooner or later one test will want slightly different versions of the data,\nand you end up copying the whole thing around to keep them isolated. Again, it's hard to tell what's relevant to the test and what is just happenstance.\n\nIt's usually much more straightforward to just load the data directly\nusing the Django ORM.\n\nTIP: Once you have more than a handful of fields on a model,\n    and/or several related models,\n    you'll want to factor out some nice helper methods with descriptive names\n    to build out your data.\n    A lot of people also like\n    https://factoryboy.readthedocs.org[`factory_boy`],\n    but I think the most important thing is the descriptive names.\n\n\n*******************************************************************************\n\n\n=== Our Final Explicit Wait Helper: A Wait Decorator\n\n(((\"decorators\", \"wait decorator\", id=\"Dwait20\")))\n(((\"explicit and implicit waits\", id=\"exp20\")))\n(((\"implicit and explicit waits\", id=\"imp20\")))\n(((\"helper methods\", id=\"help20\")))\n(((\"wait_for_row_in_list_table helper method\")))\n(((\"self.wait_for helper method\")))\n(((\"wait_to_be_logged_in/out\")))(((\"waits\", \"explicit wait helper, wait decorator\", id=\"ix_waitdec\")))\nWe've used decorators a few times in our code so far,\nbut it's time to learn how they actually work by making one of our own. First, let's imagine how we might want our decorator to work.\nIt would be nice to be able to replace all the custom wait/retry/timeout logic\nin `wait_for_row_in_list_table()` and the inline `self.wait_fors()`\nin the `wait_to_be_logged_in/out`.\nSomething like this would look lovely:\n\n// TODO: there's a change to the rows= here, backport.\n// DAVID: I didn't realise that I was meant to paste this in yet -\n// be more explicit?\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch22l005)\n====\n[source,python]\n----\n    @wait\n    def wait_for_row_in_list_table(self, row_text):\n        rows = self.browser.find_elements(By.CSS_SELECTOR, \"#id_list_table tr\")\n        self.assertIn(row_text, [row.text for row in rows])\n\n    @wait\n    def wait_to_be_logged_in(self, email):\n        self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\")\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertIn(email, navbar.text)\n\n    @wait\n    def wait_to_be_logged_out(self, email):\n        self.browser.find_element(By.CSS_SELECTOR, \"input[name=email]\")\n        navbar = self.browser.find_element(By.CSS_SELECTOR, \".navbar\")\n        self.assertNotIn(email, navbar.text)\n----\n====\n\n\nAre you ready to dive in?\nAlthough decorators are quite difficult to wrap your head around,footnote:[I know it took me a long time before I was comfortable with them,\nand I still have to think about them quite carefully whenever I make one.]\nthe nice thing is that we've already dipped our toes into functional programming\nin our `self.wait_for()` helper function.\nThat's a function that takes another function as an argument—and a decorator is the same.\nThe difference is that the decorator doesn't actually execute any code itself; it\nreturns a modified version of the function that it was given.\n\nOur decorator wants to return a new function,\nwhich will keep retrying the function being decorated—catching our usual exceptions\nuntil a timeout occurs. Here's a first cut:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch22l006)\n====\n[source,python]\n----\ndef wait(fn):  #<1>\n    def modified_fn():  #<3>\n        start_time = time.time()\n        while True:  #<4>\n            try:\n                return fn()  #<5>\n            except (AssertionError, WebDriverException) as e:  #<4>\n                if time.time() - start_time > MAX_WAIT:\n                    raise e\n                time.sleep(0.5)\n\n    return modified_fn  #<2>\n----\n====\n// JAN: Why not use functools.wraps here?\n\n<1> A decorator is a way of modifying a function;\n    it takes a function as an [keep-together]#argument...#\n\n<2> ...and returns another function as the modified (or \"decorated\") version.\n\n<3> Here's where we define our modified function.\n\n<4> And here's our familiar loop, which will keep catching those\n    exceptions until the timeout.\n\n<5> And as always, we call our original function and return immediately if there are\n    no [keep-together]#exceptions#.\n\n//IDEA: discuss the fact that multiple calls to fn() may have side-effects?\n\nThat's _almost_ right, but not quite;  try running it?\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\n    self.wait_to_be_logged_out(email)\nTypeError: wait.<locals>.modified_fn() takes 0 positional arguments but 2 were\ngiven\n----\n\n[role=\"pagebreak-before\"]\nUnlike in `self.wait_for`, the decorator is being applied to functions\nthat have [keep-together]#arguments#:\n\n\n\n[role=\"sourcecode currentcontents\"]\n.src/functional_tests/base.py\n====\n[source,python]\n----\n    @wait\n    def wait_to_be_logged_in(self, email):\n        self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\")\n        [...]\n----\n====\n\n`wait_to_be_logged_in` takes `self` and `email` as positional arguments.\nBut when it's decorated, it's replaced with `modified_fn`,\nwhich currently takes no arguments.\nHow do we magically make it so our `modified_fn` can handle the same arguments\nas whatever function the decorator is given?(((\"variadic arguments\")))(((\"kwargs\")))\n\nThe answer is a bit of Python magic,\n+++<code>*args</code>+++ and +++<code>**kwargs</code>+++, more formally known as\nhttps://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists[\"variadic arguments\"]\n(apparently—I only just learned that):\n\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch22l007)\n====\n[source,python]\n----\ndef wait(fn):\n    def modified_fn(*args, **kwargs):  #<1>\n        start_time = time.time()\n        while True:\n            try:\n                return fn(*args, **kwargs)  #<2>\n            except (AssertionError, WebDriverException) as e:\n                if time.time() - start_time > MAX_WAIT:\n                    raise e\n                time.sleep(0.5)\n\n    return modified_fn\n----\n====\n\n<1> Using +++<code>*args</code>+++ and +++<code>**kwargs</code>+++, we specify that `modified_fn()`\n    may take any arbitrary positional and keyword arguments.\n\n<2> As we've captured them in the function definition,\n    we make sure to pass those same arguments to `fn()` when we actually call it.\n\nOne of the fun things this can be used for is to make a decorator\nthat changes the arguments of a function.  But we won't get into that now.\nThe main thing is that our decorator now works!\n\n// SEBASTIAN: that's actually an awful idea, making it harder to leverage type hints. I wouldn't be giving people such ideas :D\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\nOK\n----\n\n\nAnd do you know what's truly satisfying?\nWe can use our `wait` decorator for our `self.wait_for` helper as well!\nLike this:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch22l008)\n====\n[source,python]\n----\n    @wait\n    def wait_for(self, fn):\n        return fn()\n----\n====\n\n\nLovely!  Now all our wait/retry logic is encapsulated in a single place,\nand we have a nice easy way of applying those waits—either inline in our FTs using `self.wait_for()`,\nor on any helper function using the `@wait` decorator.\n\nLet's just check all the FTs still pass of course:\n\n----\nRan 8 tests in 19.379s\n\nOK\n----\n\n\nDo a commit, and we're good to cross off that scratchpad item:\n\n[role=\"scratchpad\"]\n*****\n* '[strikethrough line-through]#Clean up wait_for stuff in base.py.#'\n*****\n\n\nIn the next chapter, we'll try to deploy our code to staging,\nand use the pre-authenticated session fixtures on the server.\nAs we'll see, it'll help us catch a little bug or two!\n(((\"waits\", \"explicit wait helper, wait decorator\", startref=\"ix_waitdec\")))(((\"\", startref=\"Dwait20\")))\n(((\"\", startref=\"exp20\")))\n(((\"\", startref=\"imp20\")))\n\n\n\n[role=\"pagebreak-before less_space\"]\n.Lessons Learned\n*******************************************************************************\n\nDecorators::\n    Decorators can be a great way of abstracting out\n    different levels of concerns.\n    They let us write our test assertions\n    without having to think about waits at the same time.\n    (((\"decorators\", \"benefits of\")))\n\nDe-duplicating your FTs, with caution::\n    Every single FT doesn't need to test every single part of your application.\n    In our case, we wanted to avoid going through the full login process for\n    every FT that needs an authenticated user, so we used a test fixture to\n    \"cheat\" and skip that part. You might find other things you want to skip\n    in your FTs.  A word of caution, however: functional tests are there to\n    catch unpredictable interactions between different parts of your\n    application, so be wary of pushing de-duplication to the extreme.\n    (((\"duplication, eliminating\")))\n\nTest fixtures::\n    Test fixtures refers to test data that needs to be set up as a precondition\n    before a test is run--often this means populating the database with some\n    information, but as we've seen (with browser cookies), it can involve other\n    types of preconditions.\n    (((\"test fixtures\")))\n\nAvoiding JSON fixtures::\n    Django makes it easy to save and restore data from the database\n    in JSON format (and others) using the `dumpdata` and `loaddata` management commands.\n    I would tend to recommend against them,\n    as they are painful to manage when your database schema changes.\n    Use the ORM, with some nicely named helper functions instead.\n    (((\"JSON fixtures\")))\n    (((\"dumpdata command\")))\n    (((\"loaddata command\")))\n    (((\"fixtures\", \"JSON fixtures\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_23_debugging_prod.asciidoc",
    "content": "[[chapter_23_debugging_prod]]\n== Debugging and Testing Server Issues\n\nPopping a few layers off the stack of things we're working on:\nwe have nice wait-for helpers; what were we using them for?\nOh yes, waiting to be logged in. And why was that?\nAh yes, we had just built a way of pre-authenticating a user.\nLet's see how that works against Docker and our staging server.\n\n\n\n=== The Proof Is in the Pudding: Using Docker to Catch Final Bugs\n\n\nRemember the deployment checklist from <<chapter_18_second_deploy>>?\nLet's see if it can't come in handy today!(((\"Docker\", \"using to catch bugs in authentication system\")))\n\nFirst, we rebuild and start our Docker container locally,\non port `8888`:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/container.db.sqlite3\",target=/home/nonroot/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -it superlists*\n[...]\n => => naming to docker.io/library/superlists [...]\n[2025-01-27 22:37:02 +0000] [7] [INFO] Starting gunicorn 22.0.0\n[2025-01-27 22:37:02 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7)\n[2025-01-27 22:37:02 +0000] [7] [INFO] Using worker: sync\n[2025-01-27 22:37:02 +0000] [8] [INFO] Booting worker with pid: 8\n----\n\n// TODO: we really should have shellscripts for this\n\nNOTE: If you see an error saying `bind source path does not exist`,\n    you've lost your container database somehow.\n    Create a new one with  `touch container.db.sqlite3`.\n\n\nNow let's make sure our container database is fully up to date,\nby running `migrate` inside the container:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker exec $(docker ps --filter=ancestor=superlists -q) python manage.py migrate*\nOperations to perform:\n  Apply all migrations: accounts, auth, contenttypes, lists, sessions\nRunning migrations:\n[...]\n----\n\nNOTE: That little `$(docker ps --filter=ancestor=superlists -q)`\n    is a neat way to avoid manually looking up the container ID.\n    An alternative would be to just set the container name explicitly\n    in our `docker run` commands, using `--name`.\n\n\nAnd now, let's do an FT run:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: #id_logout; [...]\n[...]\nAssertionError: 'Check your email' not found in 'Server Error (500)'\n[...]\nFAILED (failures=1, errors=1)\n----\n\nWe can't log in--either with the real email system or with our pre-authenticated session.\nLooks like our nice new authentication system is crashing when we run it in Docker.\n\nLet's practice a bit of production debugging!\n\n\n=== Inspecting the Docker Container Logs\n\n(((\"logging\", \"inspecting Docker container logs\")))\n(((\"Gunicorn\", \"logging setup\")))\nWhen Django fails with a 500 or \"unhandled exception\" and `DEBUG` is off,\nit doesn't print the tracebacks to your web browser.\nBut it will send them to your logs instead.\n\n[role=\"pagebreak-before less_space\"]\n.Check Our Django LOGGING Settings\n*******************************************************************************\n\nIt's worth double-checking at this point that your _settings.py_\nstill contains the `LOGGING` settings that will actually send stuff\nto the console:\n\n[role=\"sourcecode currentcontents\"]\n.src/superlists/settings.py\n====\n[source,python]\n----\nLOGGING = {\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"handlers\": {\n        \"console\": {\"class\": \"logging.StreamHandler\"},\n    },\n    \"loggers\": {\n        \"root\": {\"handlers\": [\"console\"], \"level\": \"INFO\"},\n    },\n}\n----\n====\n\nRebuild and restart the Docker container if necessary,\nand then either rerun the FT, or just try to log in manually.\n*******************************************************************************\n\nIf you switch to the terminal that's running your Docker image, you should see the traceback printed out in there:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros\"]\n----\nInternal Server Error: /accounts/send_login_email\nTraceback (most recent call last):\n[...]\n  File \"/src/accounts/views.py\", line 16, in send_login_email\n    send_mail(\n    ~~~~~~~~~^\n        \"Your login link for Superlists\",\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    ...<2 lines>...\n        [email],\n        ^^^^^^^^\n    )\n    ^\n[...]\n    self.connection.sendmail(\n    ~~~~~~~~~~~~~~~~~~~~~~~~^\n        from_email, recipients, message.as_bytes(linesep=\"\\r\\n\")\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\n  File \"/usr/local/lib/python3.14/smtplib.py\", line 876, in sendmail\n    raise SMTPSenderRefused(code, resp, from_addr)\nsmtplib.SMTPSenderRefused: (530, b'5.7.0 Authentication Required. [...]\n----\n\nSure enough, that looks like a pretty good clue as to what's going on:\nwe're getting a \"sender refused\" error when trying to send our email.\nGood to know our local Docker setup can reproduce the error on the server!\n(((\"\", startref=\"Dockercatch21\")))\n\n\n\n=== Another Environment Variable in Docker\n\nSo, Gmail is refusing to let us send emails, is it?(((\"environment variables\", \"email password in Docker\")))(((\"Docker\", \"adding email password environment variable to local container\")))\nNow why might that be? \"Authentication required\", you say?\nOh, whoops; we haven't told the server what our password is!\n\n\nAs you might remember from earlier chapters,\nour _settings.py_ expects to get the email server password from an environment variable\nnamed `EMAIL_PASSWORD`:\n\n[role=\"sourcecode currentcontents\"]\n.src/superlists/settings.py\n====\n[source,python]\n----\nEMAIL_HOST_PASSWORD = os.environ.get(\"EMAIL_PASSWORD\")\n----\n====\n\n\nLet's add this new environment variable to our local Docker container `run`\ncommand. First, set your email password in your terminal if you need to:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo $EMAIL_PASSWORD*\n# if that's empty, let's set it:\n$ *export EMAIL_PASSWORD=\"yoursekritpasswordhere\"*\n----\n\nNow let's pass that environment variable through to our Docker container using one more `-e` flag—this one fishing the env var out of the shell we're in:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/container.db.sqlite3\",target=/home/nonroot/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -e EMAIL_PASSWORD \\\n    -it superlists*\n----\n\nTIP: If you use `-e` without the `=something` argument,\n    it sets the env var inside Docker to the same value set in the current shell.\n    It's like saying `-e EMAIL_PASSWORD=$EMAIL_PASSWORD`.\n\n[role=\"pagebreak-before\"]\nAnd now we can rerun our FT again.\nWe'll narrow it down to just the `test_login` test, because that's the main one that has a problem:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_login*]\n[...]\nERROR: test_login_using_magic_link\n(functional_tests.test_login.LoginTest.test_login_using_magic_link)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_login.py\", line 32, in\ntest_login_using_magic_link\n    email = mail.outbox.pop()\nIndexError: pop from empty list\n----\n\nWell, not a pass, but the tests do get a little further.\nIt looks like our server _can_ now send emails.(((\"mail.outbox attribute\", \"not working outside of Django\")))\n(If you check the Docker logs, you'll see there are no more errors.)\nBut our FT is saying it can't see any emails appearing in `mail.outbox`.\n\n\n==== mail.outbox Won't Work Outside Django's Test Environment\n\nThe reason is that `mail.outbox` is a local, in-memory variable in Django,\nso that's only going to work when our tests and our server are running in the same process—like they do with unit tests or with `LiveServerTestCase` FTs.\n\nWhen we run against another process, be it Docker or an actual server, we can't access the same `mail.outbox` variable. If we want to actually inspect the emails that the server sends we need another technique in our tests against Docker (or later, against the staging server).\n\n\n[[options-for-testing-real-email]]\n=== Deciding How to Test \"Real\" Email Sending\n\nThis is a point at which we have to explore some trade-offs.(((\"emails\", \"testing real email sending\", id=\"ix_emltstreal\")))\nThere are a few different ways we could test email sending:\n\n1. We could build a \"real\" end-to-end test, and have our tests\n   log in to an email server using the POP3 protocol to retrieve the email from there.\n   That's what I did in the first and second editions of this book.\n\n2. We can use a service like Mailinator or Mailsac,\n   which gives us an email account to send to,\n   along with APIs for checking what mail has been delivered.\n\n3. We can use an alternative, fake email backend\n   whereby Django will save the emails to a\n   https://docs.djangoproject.com/en/5.2/topics/email/#file-backend[file on disk],\n   for example, and we can inspect them there.\n\n4. We could give up on testing email on the server.\n   If we have a minimal smoke test confirming that the server _can_ send emails,\n   then we don't need to test that they are actually delivered.\n\n[role=\"pagebreak-before\"]\n<<testing_strategy_table>> lays out some of the pros and cons.\n\n\n[[testing_strategy_table]]\n.Testing strategy trade-offs\n[options=\"header\"]\n|=======\n| Strategy | Pros | Cons\n| End-to-end with POP3 | Maximally realistic, tests the whole system | Slow, fiddly, unreliable\n| Email testing service e.g., Mailinator/Mailsac| As realistic as real POP3, with better APIs for testing| Slow, possibly expensive (and I don't want to endorse any particular commercial provider)\n| File-based fake email backend | Faster, more reliable, no network calls, tests end-to-end (albeit with fake components) | Still fiddly, requires managing database and filesystem side effects\n| Giving up on testing email on the server/Docker | Fast, simple | Less confidence that things work \"for real\"\n|=======\n\nWe're exploring a common problem in testing integration with external systems;\nhow far should we go?  How realistic should we make our tests?\n\nIn this case, I'm going to suggest we go for the last option,\nwhich is _not_ to test email sending on the server or in Docker.\nEmail itself is a well-understood protocol\n(reader, it's been around since _before I was born_, and that's a while ago now),\nand Django has supported sending email for more than a decade.\nSo, I think we can afford to say, in this case,\nthat the costs of building testing tools for email outweigh the benefits.\n\n// RITA: Although the sentence has a lot of your voice, I don't think mentioning your birthday is necessary. The reader probably has no idea how old you are. It would be enough to say that email has been around for a while now.\n\n\nI'm going to suggest we stick to using `mail.outbox` when we're running local tests,\nand we configure our FTs to just check that Docker (or, later, the staging server)\n_seems_ to be able to send email (in the sense of \"not crashing\").\nWe can skip the bit where we check the email contents in our FT.\nRemember, we also have unit tests for the email content!\n\nNOTE: I explore some of the difficulties involved in getting\n  these kinds of tests to work in\n  https://www.obeythetestinggoat.com/book/appendix_fts_for_external_dependencies.html[Online Appendix: Functional Tests for External Dependencies],\n  so go check that out if this feels like a bit of a cop-out!\n\n[role=\"pagebreak-before\"]\nHere's where we can put an(((\"early return\"))) early return in the FT:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_login.py (ch23l009)\n====\n[source,python]\n----\n    # A message appears telling her an email has been sent\n    self.wait_for(\n        lambda: self.assertIn(\n            \"Check your email\",\n            self.browser.find_element(By.CSS_SELECTOR, \"body\").text,\n        )\n    )\n\n    if self.test_server:\n        # Testing real email sending from the server is not worth it.\n        return\n\n    # She checks her email and finds a message\n    email = mail.outbox.pop()\n----\n====\n\nThis test will still fail if you don't set `EMAIL_PASSWORD` to a valid value\nin Docker or on the server, meaning it would still have warned us of the bug we started the chapter with—so that's good enough for now.\n\nHere's how we populate the `FunctionalTest.test_server` attribute:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch23l010)\n====\n[source,python]\n----\nclass FunctionalTest(StaticLiveServerTestCase):\n    def setUp(self):\n        self.browser = webdriver.Firefox()\n        self.test_server = os.environ.get(\"TEST_SERVER\")  # <1>\n        if self.test_server:\n            self.live_server_url = \"http://\" + self.test_server\n----\n====\n\n<1> We upgrade `test_server` to be an attribute on the test object,\n    so we can access it in various places in our FTs\n    (we'll see several examples later).\n    Sad to see our walrus go, though!\n\n\nAnd you can confirm that the FT fails if you _don't_ set `EMAIL_PASSWORD` in Docker, or passes, if you do:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_login*]\n[...]\n\nOK\n----\n\n\nNow let's see if we can get our FTs to pass against the server.\nFirst, we'll need to figure out how to set that env var on the server.(((\"emails\", \"testing real email sending\", startref=\"ix_emltstreal\")))\n\n\n=== An Alternative Method for Setting Secret Environment Variables on the Server\n\n(((\"environment variables\", \"secret, alternative method for setting on server\", id=\"ix_envvarset\")))(((\"secrets\", \"setting secret environment variables on server\", id=\"ix_secrenvvar\")))\n(((\"secret values\")))\nIn <<chapter_12_ansible>>, we dealt with setting the `SECRET_KEY` by\ngenerating a random value, and then saving it to a file on the server. We could use a similar technique here. But, just to give you an alternative, I'll show how to pass the environment variable directly up to the container, without storing it in a file:\n\n[role=\"sourcecode\"]\n.infra/deploy-playbook.yaml (ch23l012)\n====\n[source,python]\n----\n        env:\n          DJANGO_DEBUG_FALSE: \"1\"\n          DJANGO_SECRET_KEY: \"{{ secret_key.content | b64decode }}\"\n          DJANGO_ALLOWED_HOST: \"{{ inventory_hostname }}\"\n          DJANGO_DB_PATH: \"/home/nonroot/db.sqlite3\"\n          EMAIL_PASSWORD: \"{{ lookup('env', 'EMAIL_PASSWORD') }}\"  # <1>\n----\n====\n\n<1> `lookup()` with `env` as its argument is how you look up _local_ environment variables—i.e., the ones set on the computer you're running Ansible from.\n\nThis means you'll need the `EMAIL_PASSWORD` environment variable\nto be set on your local machine every time you want to run Ansible.\n\nLet's consider some pros and cons of the two approaches:\n\n* Saving the secret to a file on the server means you don't need to \"remember\"\n  or store the secret anywhere on your own machine.\n\n* In contrast, always passing it up from the local environment does\n  mean you can change the value of the secret at any time.\n\n* In terms of security, they are fairly equivalent—in either case, the environment variable is visible via `docker inspect`.\n\n\nIf we rerun our full FT suite against the server,\nyou should see that the login test passes,\nand we're down to just one failure, in\n+test_&#x2060;l&#x2060;o&#x2060;g&#x2060;g&#x2060;e&#x2060;d&#x200b;_&#x2060;i&#x2060;n&#x2060;_&#x2060;u&#x2060;s&#x2060;e&#x2060;r&#x2060;s&#x2060;_lists_are_saved_as_my_lists()+:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests*]\n[...]\nERROR: test_logged_in_users_lists_are_saved_as_my_lists\n(functional_tests.test_my_lists.MyListsTest.test_logged_in_users_lists_are_saved_[...]\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_my_lists.py\", line 36, in\ntest_logged_in_users_lists_are_saved_as_my_lists\n    self.wait_to_be_logged_in(email)\n    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: #id_logout; [...]\n[...]\n ---------------------------------------------------------------------\n\nRan 8 tests in 30.087s\n\nFAILED (errors=1)\n----\n\nLet's look into that next.(((\"secrets\", \"setting secret environment variables on server\", startref=\"ix_secrenvvar\")))(((\"environment variables\", \"secret, alternative method for setting on server\", startref=\"ix_envvarset\")))\n\n\n=== Debugging with SQL\n\nLet's switch back to testing locally against our Docker container:(((\"SQL\", \"debugging creation of pre-authenticated sessions with\", id=\"ix_SQLdbg\")))\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_my_lists*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: #id_logout; [...]\nFAILED (errors=1)\n----\n\nIt looks like the attempt to create pre-authenticated sessions doesn't work,\nso we're not being logged in. Let's do a bit of debugging with SQL.\n\nFirst, try logging in to your local \"runserver\" instance\n(where things definitely work)\nand take a look in the normal local database, _src/db.sqlite3_:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros,callouts\"]\n----\n$ pass:[<strong>sqlite3 src/db.sqlite3</strong>]\nSQLite version 3.43.2 2023-10-10 13:08:14\nEnter \".help\" for usage hints.\n\nsqlite> pass:[<strong>select * from accounts_token;</strong>]  <1>\npass:[1|obeythetestinggoat@gmail.com|11d3e26d-32a3-4434-af71-5e0f62fefc52]\npass:[2|obeythetestinggoat@gmail.com|25a570c8-736f-42e4-931b-ed5c410b5b51]\n\nsqlite> pass:[<strong>select * from django_session;</strong>]  <2>\ntv2m5byccfs05gfpkc1l8k4pep097y3c|.eJxVjEsKg0AMQO-StcwBurI9gTcYYgwzo[...]\n----\n\n<1> We can do a `SELECT *` in our tokens table\n    to see some of the tokens we've been creating for our users.\n\n<2> And we can take a look in the `django_session` table.\n    You should find the first column matches the session ID\n    you'll see in your DevTools.\n\nLet's do a bit of debugging. Take a look in _container.db.sqlite3_:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros,callouts\"]\n----\n$ pass:[<strong>sqlite3 container.db.sqlite3</strong>]\nSQLite version 3.43.2 2023-10-10 13:08:14\nEnter \".help\" for usage hints.\n\nsqlite> pass:[<strong>select * from accounts_token;</strong>]  <1>\n\nsqlite> pass:[<strong>select * from django_session;</strong>]  <2>\n----\n\n<1> The users table is empty.\n    (If you do see `edith@example.com` in here, it's from a previous test run.\n    Delete and re-create the database if you want to be sure.)\n\n<2> And the sessions table is definitely empty.\n\n\nNow, let's try manually. If you visit `localhost:8888` and log in—getting the token from your email—you'll see it works. You can also run +f&#x2060;u&#x2060;n&#x2060;c&#x2060;t&#x2060;i&#x2060;o&#x2060;n&#x2060;a&#x2060;l&#x2060;_&#x200b;t&#x2060;e&#x2060;s&#x2060;t&#x2060;s&#x2060;.test_login+ and you'll see _that_ pass.\n\nIf we look in the database again, we'll see some more data:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,macros,callouts\"]\n----\n$ pass:[<strong>sqlite3 container.db.sqlite3</strong>]\nSQLite version 3.43.2 2023-10-10 13:08:14\nEnter \".help\" for usage hints.\n\nsqlite> pass:[<strong>select * from accounts_token;</strong>]\n3|obeythetestinggoat@gmail.com|115812a3-7d37-485c-9c15-337b12293f69\n4|edith@example.com|a901bee9-88aa-4965-9277-a13723a6bfe1\n\nsqlite> pass:[<strong>select * from django_session;</strong>]\n09df51nmvpi137mpv5bwjoghh2a4y5lh|.eJxVjEsKg0AMQO-[...]\n----\n\nSo, there's nothing _fundamentally_ wrong with the Docker environment.\nIt's seems like it's specifically our test utility function\n`create_pre_authenticated_session()` that isn't working.\n\nAt this point, a little niggle in your head might be growing louder,\nreminding us of a problem we anticipated in the last chapter:\n`LiveServerTestCase` only lets us talk to the in-memory database.\nThat's where our pre-authenticated sessions are ending up!(((\"SQL\", \"debugging creation of pre-authenticated sessions with\", startref=\"ix_SQLdbg\")))\n\n\n=== Managing Fixtures in Real Databases\n\nWe need a way to make changes to the database inside Docker or on the server.\nEssentially, we want to run some code outside the context of the tests\n(and the test database) and in the context of the server and its database.(((\"fixtures\", \"managing in real databases\", id=\"ix_fxtDB\")))\n\n\n==== A Django Management Command to Create Sessions\n\n(((\"scripts, building standalone\")))(((\"sessions\", \"Django management command to create\")))(((\"management command (Django) to create sessions\")))\nWhen trying to build a standalone script that works with Django\n(i.e., can talk to the database and so on),\nthere are some fiddly issues you need to get right,\nlike setting the `DJANGO_SETTINGS_MODULE` environment variable\nand setting `sys.path` correctly.\n\n\nInstead of messing about with all that, Django lets you create your own\n\"management commands\" (commands you can run with `python manage.py`), which\nwill do all that path-mangling for you. They live in a folder called\n_management/commands_ inside your apps:\n\n[subs=\"\"]\n----\n$ <strong>mkdir -p src/functional_tests/management/commands</strong>\n$ <strong>touch src/functional_tests/management/__init__.py</strong>\n$ <strong>touch src/functional_tests/management/commands/__init__.py</strong>\n----\n\nThe boilerplate in a management command is a class that inherits from\n`django.core.management.BaseCommand`, and that defines a method called\n`handle`:\n\n[role=\"sourcecode\"]\n.src/functional_tests/management/commands/create_session.py (ch23l014)\n====\n[source,python]\n----\nfrom django.conf import settings\nfrom django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model\nfrom django.contrib.sessions.backends.db import SessionStore\nfrom django.core.management.base import BaseCommand\n\nUser = get_user_model()\n\n\nclass Command(BaseCommand):\n    def add_arguments(self, parser):\n        parser.add_argument(\"email\")\n\n    def handle(self, *args, **options):\n        session_key = create_pre_authenticated_session(options[\"email\"])\n        self.stdout.write(session_key)\n\n\ndef create_pre_authenticated_session(email):\n    user = User.objects.create(email=email)\n    session = SessionStore()\n    session[SESSION_KEY] = user.pk\n    session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]\n    session.save()\n    return session.session_key\n----\n====\n\nWe've taken the code for `create_pre_authenticated_session` from\n'test_my_lists.py'. `handle` will pick up an email address from the parser,\nand then return the session key that we'll want to add to our browser cookies,\nand the management command prints it out at the command line.\n\n[role=\"pagebreak-before\"]\nTry it out:\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py create_session a@b.com*]\nUnknown command: 'create_session'. Did you mean clearsessions?\n----\n\nOne more step: we need to add `functional_tests` to our 'settings.py'\nso that it's recognised as a real app that might have management commands as\nwell as tests:\n\n[role=\"sourcecode\"]\n.src/superlists/settings.py (ch23l015)\n====\n[source,python]\n----\n+++ b/superlists/settings.py\n@@ -42,6 +42,7 @@ INSTALLED_APPS = [\n     \"accounts\",\n     \"lists\",\n+    \"functional_tests\",\n ]\n----\n====\n\n\nWARNING: Beware of the security implications here.\n    We're now adding some remotely executable code for bypassing authentication\n    to our default configuration.  Yes, someone exploiting this would need to have already\n    gained access to the server, so it was game over anyway,\n    but nonetheless, this is a sensitive area.\n    If you were doing something like this in a real application,\n    you might consider adding an `if environment != prod`, or similar.\n\n\n\nNow it works:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py create_session a@b.com*]\nqnslckvp2aga7tm6xuivyb0ob1akzzwl\n----\n\nNOTE: If you see an error saying the `auth_user` table is missing,\n    you may need to run `manage.py migrate`.\n    In case that doesn't work, delete the _db.sqlite3_ file\n    and run `migrate` again to get a clean slate.\n\n[role=\"pagebreak-before less_space\"]\n==== Getting the FT to Run the Management Command on the Server\n\nNext, we need to adjust `test_my_lists` so that it runs the local function\nwhen we're using the local in-memory test server from `LiveServerTestCase`.\nAnd, if we're running against the Docker container or staging server,\nit should run the management command instead.(((\"management command (Django) to create sessions\", \"getting it to run on server\")))\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch23l016)\n====\n[source,python]\n----\nfrom django.conf import settings\n\nfrom .base import FunctionalTest\nfrom .container_commands import create_session_on_server  # <1>\nfrom .management.commands.create_session import create_pre_authenticated_session\n\n\nclass MyListsTest(FunctionalTest):\n    def create_pre_authenticated_session(self, email):\n        if self.test_server:  # <2>\n            session_key = create_session_on_server(self.test_server, email)\n        else:\n            session_key = create_pre_authenticated_session(email)\n\n        ## to set a cookie we need to first visit the domain.\n        ## 404 pages load the quickest!\n        self.browser.get(self.live_server_url + \"/404_no_such_url/\")\n        self.browser.add_cookie(\n            dict(\n                name=settings.SESSION_COOKIE_NAME,\n                value=session_key,\n                path=\"/\",\n            )\n        )\n\n    [...]\n----\n====\n\n<1> Programming by wishful thinking,\n  let's imagine we'll have a module called `container_commands`\n  with a function called `create_session_on_server()` in it.\n\n<2> Here's the `if` where we decide which of our two session-creation\n    functions to execute.\n\n\n==== Running Commands Using Docker Exec and (Optionally) SSH\n\n\nYou may remember `docker exec` from <<chapter_09_docker>>; it lets us run\ncommands inside a running Docker container.\nThat's fine for when we're running against the local Docker,\nbut when we're against the server, we need to SSH in first.(((\"Docker\", \"running commands using docker exec\")))(((\"SSH\", \"running commands on Docker container running on the server\")))\n\nThere's a bit of plumbing here, but I've tried to break things down into small chunks:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/container_commands.py (ch23l018)\n====\n[source,python]\n----\nimport subprocess\n\nUSER = \"elspeth\"\n\n\ndef create_session_on_server(host, email):\n    return _exec_in_container(\n        host, [\"/venv/bin/python\", \"/src/manage.py\", \"create_session\", email]  # <1>\n    )\n\n\ndef _exec_in_container(host, commands):\n    if \"localhost\" in host:  # <2>\n        return _exec_in_container_locally(commands)\n    else:\n        return _exec_in_container_on_server(host, commands)\n\n\ndef _exec_in_container_locally(commands):\n    print(f\"Running {commands} on inside local docker container\")\n    return _run_commands([\"docker\", \"exec\", _get_container_id()] + commands)  # <3>\n\n\ndef _exec_in_container_on_server(host, commands):\n    print(f\"Running {commands!r} on {host} inside docker container\")\n    return _run_commands(\n        [\"ssh\", f\"{USER}@{host}\", \"docker\", \"exec\", \"superlists\"] + commands  # <4>\n    )\n\n\ndef _get_container_id():\n    return subprocess.check_output(  # <5>\n        [\"docker\", \"ps\", \"-q\", \"--filter\", \"ancestor=superlists\"]  # <3>\n    ).strip()\n\n\ndef _run_commands(commands):\n    process = subprocess.run(  # <5>\n        commands,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        check=False,\n    )\n    result = process.stdout.decode()\n    if process.returncode != 0:\n        raise Exception(result)\n    print(f\"Result: {result!r}\")\n    return result.strip()\n----\n====\n// DAVID: In _run_commands, why not do `check=True`, then we can omit the returncode / exception handling?\n\n<1> We invoke our management command with the path to the virtualenv Python,\n    the `create_session` command name, and pass in the email we want to create a session for.\n\n<2> We dispatch to two slightly different ways of running a command inside a container,\n    with the assumption that a host on \"localhost\" is a local Docker container,\n    and the others are on the staging server.\n\n<3> To run a command on the local Docker container, we're going to use `docker exec`,\n    and we have a little extra hop first to get the correct container ID.\n\n<4> To run a command on the Docker container that's on the staging server,\n    we still use `docker exec`, but we do it inside an SSH session.\n    In this case we don't need the container ID, because the container is always named \"superlists\".\n\n<5> Finally, we use Python's `subprocess` module to actually run a command.\n    You can see a couple of different ways of running it here,\n    which differ based on how we're handing errors and output;\n    the details don't matter too much.\n\n\n==== Recap: Creating Sessions Locally Versus Staging\n\n(((\"staging sites\", \"local versus staged sessions\")))(((\"sessions\", \"creating locally versus staging\")))\nDoes that all make sense?\nPerhaps a little ASCII-art diagram will help:\n\n\n===== Locally:\n\n[role=\"skipme small-code\"]\n----\n+-----------------------------------+       +-------------------------------------+\n| MyListsTest                       |       | .management.commands.create_session |\n| .create_pre_authenticated_session |  -->  |  .create_pre_authenticated_session  |\n|            (locally)              |       |             (locally)               |\n+-----------------------------------+       +-------------------------------------+\n----\n\n\n===== Against Docker locally:\n\n[role=\"skipme small-code\"]\n----\n+-----------------------------------+             +-------------------------------------+\n| MyListsTest                       |             | .management.commands.create_session |\n| .create_pre_authenticated_session |             |  .create_pre_authenticated_session  |\n|            (locally)              |             |            (in Docker)              |\n+-----------------------------------+             +-------------------------------------+\n            |                                                        ^\n            v                                                        |\n+----------------------------+                                       |\n| server_tools               |     +-------------+     +----------------------------+\n| .create_session_on_server  | --> | docker exec | --> | ./manage.py create_session |\n|        (locally)           |     +-------------+     |       (in Docker)          |\n+----------------------------+                         +----------------------------+\n----\n\n\n===== Against Docker on the server:\n\n[role=\"skipme small-code\"]\n----\n+-----------------------------------+             +-------------------------------------+\n| MyListsTest                       |             | .management.commands.create_session |\n| .create_pre_authenticated_session |             |  .create_pre_authenticated_session  |\n|            (locally)              |             |            (on server)              |\n+-----------------------------------+             +-------------------------------------+\n            |                                                           ^\n            v                                                           |\n+----------------------------+                                          |\n| server_tools               |    +-----+    +--------+    +----------------------------+\n| .create_session_on_server  | -> | ssh | -> | docker | -> | ./manage.py create_session |\n|        (locally)           |    |     |    |  exec  |    |        (on server)         |\n+----------------------------+    +-----+    +--------+    +----------------------------+\n----\n\nWe do love a bit of ASCII art now and again!\n\n\n\n.An Alternative for Managing Test Database Content: Talking Directly to the Database\n**********************************************************************\nAn alternative way of managing database content inside Docker,\nor on a server, would be to talk directly to the database.(((\"databases\", \"alternative for managing test database content\")))\n\nBecause we're using SQLite, that involves writing to the file directly.\nThis can be fiddly to get right, because when we're running inside Django's\ntest runner, Django takes over the test database creation,\nso you end up having to write raw SQL and manage your connections to the database directly.\n\nThere are also some tricky interactions with the filesystem mounts and Docker,\nas well as the need to have the `SECRET_KEY` env var set to the same value as on the server.\n\nIf we were using a \"classic\" database server like PostgreSQL or MySQL,\nwe'd be able to talk directly to the database over its port,\nand that's an approach https://oreil.ly/Uk1y_[I've used successfully in the past] but it's still quite tricky, and usually requires writing your own SQL.\n**********************************************************************\n\n\n=== Testing the Management Command\n\nIn any case, let's see if this whole rickety pipeline works.(((\"fixtures\", \"managing in real databases\", startref=\"ix_fxtDB\")))(((\"management command (Django) to create sessions\", \"testing the command\")))\nFirst, locally, to check that we didn't break anything:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\nOK\n----\n\n[role=\"pagebreak-before\"]\nNext, against Docker—rebuild first:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -t superlists . && docker run \\\n    -p 8888:8888 \\\n    --mount type=bind,source=\"$PWD/container.db.sqlite3\",target=/home/nonroot/db.sqlite3 \\\n    -e DJANGO_SECRET_KEY=sekrit \\\n    -e DJANGO_ALLOWED_HOST=localhost \\\n    -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \\\n    -e EMAIL_PASSWORD \\\n    -it superlists*\n----\n\nAnd then we run the FT (that uses our fixture) against Docker:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_my_lists*]\n\n[...]\nOK\n----\n\n\nNext, we run it against the server.  First, we re-deploy to make sure our code on the server is up to date:\n\n[role=\"against-server small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*ansible-playbook --user=elspeth -i staging.ottg.co.uk, infra/deploy-playbook.yaml -vv*]\n----\n\n// CSANAD: at some point I deleted my .venv and reinstalled the pip packages on\n// my dedicated book-development environment. I just noticed I forgot about\n// installing ansible. Just a thought, maybe we could mention in a footnote,\n// perhaps in chapter 11 (after installing ansible), that it's a common practice\n// to create a separate requirements-dev.txt and we could list selenium, ansible\n// and requests in ours.\n\nAnd now we run the test:\n\n\n[role=\"against-server small-code\"]\n[subs=\"\"]\n----\n$ <strong>TEST_SERVER=staging.ottg.co.uk python src/manage.py test \\\n functional_tests.test_my_lists</strong>\nFound 1 test(s).\nCreating test database for alias 'default'...\nSystem check identified no issues (0 silenced).\nRunning '/venv/bin/python /src/manage.py create_session edith@example.com' on\nstaging.ottg.co.uk inside docker container\nResult: '7n032ogf179t2e7z3olv9ct7b3d4dmas\\n'\n.\n ---------------------------------------------------------------------\nRan 1 test in 4.515s\n\nOK\nDestroying test database for alias 'default'...\n----\n\nLooking good!  We can rerun all the tests to make sure...\n\n[role=\"against-server small-code\"]\n[subs=\"\"]\n----\n$ <strong>TEST_SERVER=staging.ottg.co.uk python src/manage.py test functional_tests</strong>\n[...]\n[elspeth@staging.ottg.co.uk] run:\n~/sites/staging.ottg.co.uk/.venv/bin/python\n[...]\nRan 8 tests in 89.494s\n\nOK\n----\n\nHooray!\n\n[role=\"pagebreak-before less_space\"]\n=== Test Database Cleanup\n\nOne more thing to be aware of: now that we're running against a real database,\nwe don't get cleanup for free any more.(((\"database testing\", \"test database cleanup\", id=\"ix_DBtstcln\")))\nIf you try running the tests twice—locally or against Docker—you'll run into this error:\n\n[role=\"small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_my_lists*]\n[...]\ndjango.db.utils.IntegrityError: UNIQUE constraint failed: accounts_user.email\n----\n\nIt's because the user we created the first time we ran the tests is still in the database.\nWhen we're running against Django's test database, Django cleans up for us.\nLet's try and emulate that when we're running against a real database:\n\n\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/container_commands.py (ch23l019)\n====\n[source,python]\n----\ndef reset_database(host):\n    return _exec_in_container(\n        host, [\"/venv/bin/python\", \"/src/manage.py\", \"flush\", \"--noinput\"]\n    )\n----\n====\n\n\nAnd let's add the call to `reset_database()` in our base test `setUp()` method:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch23l020)\n====\n[source,python]\n----\nfrom .container_commands import reset_database\n[...]\n\nclass FunctionalTest(StaticLiveServerTestCase):\n    def setUp(self):\n        self.browser = webdriver.Firefox()\n        self.test_server = os.environ.get(\"TEST_SERVER\")\n        if self.test_server:\n            self.live_server_url = \"http://\" + self.test_server\n            reset_database(self.test_server)\n----\n====\n\n\nIf you try to run your tests again, you'll find they pass happily:\n\n\n[role=\"dofirst-ch23l021 small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*TEST_SERVER=localhost:8888 python src/manage.py test functional_tests.test_my_lists*]\n[...]\n\nOK\n----\n\nProbably a good time for a commit! :)(((\"database testing\", \"test database cleanup\", startref=\"ix_DBtstcln\")))\n\n\n[role=\"pagebreak-before less_space\"]\n.Warning: Be Careful Not to Run Test Code Against the Production Server!\n*******************************************************************************\n\n(((\"database testing\", \"safeguarding production databases\")))\n(((\"production databases\")))\nWe're in dangerous territory now that we have code that can directly affect a database on the server.\nYou want to be very, very careful\nthat you don't accidentally blow away your production database\nby running FTs against the wrong host.\n\nYou might consider putting some safeguards in place at this point.\nYou almost definitely want to put staging and production on different servers, for example,\nand make it so that they use different key pairs for authentication, with different passphrases.\n\nI also mentioned not including the FT management commands in `INSTALLED_APPS`\nfor production environments.\n\nThis is similarly dangerous territory to running tests against clones of production data.\nI could tell you a little story about accidentally sending thousands of\nduplicate invoices to clients, for example. LFMF! And tread carefully.\n\n*******************************************************************************\n\n\n=== Wrap-Up\n\nActually getting your new code up and running on a server\nalways tends to flush out some last-minute bugs and unexpected issues.\nWe had to do a bit of work to get through them,\nbut we've ended up with several useful things as a result.\n\nWe now have a lovely generic `wait` decorator,\nwhich will be a nice Pythonic helper for our FTs from now on.\nWe've got some more robust logging configuration.\nWe have test fixtures that work both locally and on the server,\nand we've come out with a pragmatic approach for testing email integration.\n\nBut before we can deploy our actual production site,\nwe'd better actually give the users what they wanted--the\nnext chapter describes how to give them\nthe ability to save their lists on a \"My lists\" page.(((\"debugging\", \"catching bugs in staging\")))\n\n[role=\"pagebreak-before less_space\"]\n.Lessons Learned Catching Bugs in Staging\n*******************************************************************************\n\nIt's nice to be able to repro things locally.::\n    The effort we put into adapting our app to use Docker is paying off.\n    We discovered an issue in staging, and were able to reproduce it locally.\n    That gives us the ability to experiment and get feedback much quicker\n    than trying to do experiments on the server itself.\n\nFixtures also have to work remotely.::\n    `LiveServerTestCase` makes it easy to interact with the test database\n    using the Django ORM for tests running locally.\n    Interacting with the database inside Docker is not so straightforward.\n    One solution is `docker exec` and Django management commands,\n    as I've shown, but you should explore what works for you--connecting\n    directly to the database over SSH tunnels, for example.\n    (((\"fixtures\", \"staging and\")))\n    (((\"staging sites\", \"fixtures and\")))\n\nBe very careful when resetting data on your servers.::\n    A command that can remotely wipe the entire database on one of your\n    servers is a dangerous weapon, and you want to be really, really sure\n    it's never accidentally going to hit your production data.\n    (((\"database testing\", \"safeguarding production databases\")))\n    (((\"production databases\")))\n\nLogging is critical to debugging issues on the server.::\n    At the very least, you'll want to be able to see any error messages\n    that are being generated by the server.\n    For thornier bugs,\n    you'll also want to be able to do the occasional \"debug print\",\n    and see it end up in a file somewhere.\n    (((\"logging\")))\n    (((\"debugging\", \"server-side\", \"baking in logging code\")))\n\n*******************************************************************************\n"
  },
  {
    "path": "chapter_24_outside_in.asciidoc",
    "content": "[[chapter_24_outside_in]]\n== Finishing \"My Lists\": Outside-In TDD\n\n(((\"Test-Driven Development (TDD)\", \"outside-in technique\", id=\"TTDoutside22\")))\nIn this chapter, I'd like to talk about a technique called outside-in TDD.\nIt's pretty much what we've been doing all along.\nOur \"double-loop\" TDD process,\nin which we write the functional test first and then the unit tests,\nis already a manifestation of outside-in--we\ndesign the system from the outside, and build up our code in layers.\nNow I'll make it explicit, and talk about some of the common issues involved.\n\n\n=== The Alternative: Inside-Out\n\nThe alternative to \"outside-in\" is to work \"inside-out\",\nwhich is the way most people intuitively work before they encounter TDD.(((\"inside-out TDD\")))\nAfter coming up with a design, the natural inclination is sometimes\nto implement it starting with the innermost, lowest-level components first.\n\nFor example, when faced with our current problem,\nproviding users with a \"My lists\" page of saved lists,\nthe temptation is to start at the models layer:\nwe probably want to add an \"owner\" attribute to the `List` model object,\nreasoning that an attribute like this is \"obviously\" going to be required.\nOnce that's in place, we would modify the more peripheral layers of code—such as views and templates—taking advantage of the new attribute,\nand then finally add URL routing to point to the new view.\n\nIt feels comfortable because it means you're never working on a bit of code\nthat is dependent on something that hasn't yet been implemented.\nEach bit of work on the inside is a solid foundation\non which to build the next layer out.\n\nBut working inside-out like this also has some weaknesses.\n\n\n=== Why Prefer \"Outside-In\"?\n\n(((\"outside-in TDD\", \"versus inside-out\", secondary-sortas=\"inside-out\")))\n(((\"inside-out TDD\", \"versus outside-in\")))\nThe most obvious problem with inside-out TDD is that\nit requires us to stray from a TDD workflow.\nOur functional test's first failure might be due to missing URL routing,\nbut we decide to ignore that\nand go off adding attributes to our database model objects instead.\n\nWe might have ideas in our head\nabout the desired behaviour of our inner layers like database models,\nand often these ideas will be pretty good—but they are actually just speculation about what's really required,\nbecause we haven't yet built the outer layers that will use them.\n\nOne problem that can occur is building inner components that are more general\nor more capable than we actually need, which is a waste of time\nand an added source of complexity for your project.\nAnother common problem is that you create inner components\nwith an API that is convenient for their own internal design,\nbut which later turns out to be inappropriate\nfor the calls that your outer layers would like to make...worse still,\nyou might end up with inner components which, you later realise,\ndon't actually solve the problem that your outer layers need solved.\n\nIn contrast, working outside-in enables you to use each layer\nto imagine the most convenient API you could want from the layer beneath it.\nLet's see it in action.\n\n\n=== The FT for \"My Lists\"\n\n(((\"functional  tests (FTs)\", \"outside-in technique\", id=\"ix_FToutin\")))\nAs we work through the following functional test,\nwe start with the most outward-facing (presentation layer),\nthrough to the view functions (or \"controllers\"),\nand lastly the innermost layers, which in this case will be model code.\nSee <<outside-in-layers>>.\n\n\n[[outside-in-layers]]\n.The layer in our application\nimage::images/tdd3_2401.png[\"A diagram of the layers of the application, with the user at the top, then a presentation layer (aka views in mvc) containing some templates, below that the views layer (aka controllers in mvc) containg views and forms, then the models layer with django models, and finally an icon for the database at the bottom.  there is also an arrow from top to bottom, with the top labelled OUTSIDE! and the bottom labelled IN!\"]\n\n\nWhile we're drawing diagrams, would it help to sketch out\nwhat we're imagining?  See <<my-lists-page-sketch>>.\n\n[[my-lists-page-sketch]]\n.A sketch of the \"My lists\" page\nimage::images/tdd3_2402.png[\"A sketch of the My Lists page, showing the users email in the page header, and several lists with a callout saying that we will use the first item text as the hyperlink text for each list.\"]\n\n[role=\"pagebreak-before\"]\nLet's incarnate this idea in FT form.\nWe know our `create_pre_authenticated_session` code works now,\nso we can just fill out the actual body of the test\nto describe how a user might interact with this prospective \"My lists\" page:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch24l001)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.by import By\n[...]\n\n    def test_logged_in_users_lists_are_saved_as_my_lists(self):\n        # Edith is a logged-in user\n        self.create_pre_authenticated_session(\"edith@example.com\")\n\n        # She goes to the home page and starts a list\n        self.browser.get(self.live_server_url)\n        self.add_list_item(\"Reticulate splines\")  # <1>\n        self.add_list_item(\"Immanentize eschaton\")\n        first_list_url = self.browser.current_url\n\n        # She notices a \"My lists\" link, for the first time.\n        self.browser.find_element(By.LINK_TEXT, \"My lists\").click()\n\n        # She sees her email is there in the page heading\n        self.wait_for(\n            lambda: self.assertIn(\n                \"edith@example.com\",\n                self.browser.find_element(By.CSS_SELECTOR, \"h1\").text,\n            )\n        )\n\n        # And she sees that her list is in there,\n        # named according to its first list item\n        self.wait_for(\n            lambda: self.browser.find_element(By.LINK_TEXT, \"Reticulate splines\")\n        )\n        self.browser.find_element(By.LINK_TEXT, \"Reticulate splines\").click()\n        self.wait_for(\n            lambda: self.assertEqual(self.browser.current_url, first_list_url)\n        )\n----\n====\n\n<1> We'll define this `add_list_item()` shortly.\n\nAs you can see, we create a list with a couple of items. Then, we check that this list appears on a new \"My lists\" page,\nand that it's \"named\" after the first item in the list.\n\n[role=\"pagebreak-before\"]\nLet's validate that it really works by creating a second list,\nand seeing that appear on the \"My lists\" page as well.\nThe FT continues, and while we're at it,\nwe check that only logged-in users can see the \"My lists\" page:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/test_my_lists.py (ch24l002)\n====\n[source,python]\n----\n        [...]\n        self.wait_for(\n            lambda: self.assertEqual(self.browser.current_url, first_list_url)\n        )\n\n        # She decides to start another list, just to see\n        self.browser.get(self.live_server_url)\n        self.add_list_item(\"Click cows\")\n        second_list_url = self.browser.current_url\n\n        # Under \"my lists\", her new list appears\n        self.browser.find_element(By.LINK_TEXT, \"My lists\").click()\n        self.wait_for(lambda: self.browser.find_element(By.LINK_TEXT, \"Click cows\"))\n        self.browser.find_element(By.LINK_TEXT, \"Click cows\").click()\n        self.wait_for(\n            lambda: self.assertEqual(self.browser.current_url, second_list_url)\n        )\n\n        # She logs out.  The \"My lists\" option disappears\n        self.browser.find_element(By.CSS_SELECTOR, \"#id_logout\").click()\n        self.wait_for(\n            lambda: self.assertEqual(\n                self.browser.find_elements(By.LINK_TEXT, \"My lists\"),\n                [],\n            )\n        )\n----\n====\n\nOur FT uses a new helper method, `add_list_item()`,\nwhich abstracts away the process of entering text into the right input box.\nWe define it in _base.py_:\n\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/base.py (ch24l003)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.keys import Keys\n[...]\n\n    def add_list_item(self, item_text):\n        num_rows = len(self.browser.find_elements(By.CSS_SELECTOR, \"#id_list_table tr\"))\n        self.get_item_input_box().send_keys(item_text)\n        self.get_item_input_box().send_keys(Keys.ENTER)\n        item_number = num_rows + 1\n        self.wait_for_row_in_list_table(f\"{item_number}: {item_text}\")\n----\n====\n\n\nAnd while we're at it, we can use it in a few of the other FTs—like this, for example:\n\n\n[role=\"sourcecode dofirst-ch24l004-1\"]\n.src/functional_tests/test_layout_and_styling.py (ch24l004-2)\n====\n[source,diff]\n----\n         # She starts a new list and sees the input is nicely\n         # centered there too\n-        inputbox.send_keys(\"testing\")\n-        inputbox.send_keys(Keys.ENTER)\n-        self.wait_for_row_in_list_table(\"1: testing\")\n+        self.add_list_item(\"testing\")\n+\n----\n====\n\nI think it makes the FTs a lot more readable.\nI made a total of six changes--see if you agree with me.\n\nLet's do a quick run of all FTs, a commit, and then back to the FT we're working on.\nThe first error should look like this:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: My lists; [...]\n----\n\n\n=== The Outside Layer: Presentation and Templates\n\n\n(((\"functional  tests (FTs)\", \"outside-in technique\", startref=\"ix_FToutin\")))(((\"outside-in TDD\", \"outside layer\")))\nThe test is currently failing because it can't find a link saying \"My lists\".\nWe can address that at the presentation layer, in _base.html_, in our navigation bar.\nHere's the minimal code change:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch24l005)\n====\n[source,html]\n----\n      <nav class=\"navbar\">\n        <div class=\"container-fluid\">\n          <a class=\"navbar-brand\" href=\"/\">Superlists</a>\n          {% if user.email %}\n            <a class=\"navbar-link\" href=\"#\">My lists</a>\n            <span class=\"navbar-text\">Logged in as {{ user.email }}</span>\n            <form method=\"POST\" action=\"{% url 'logout' %}\">\n              [...]\n----\n====\n\nOf course the `href=\"#\"` means that link doesn't actually go anywhere,\nbut it _does_ get our FT along to the next failure:\n\n\n[subs=\"\"]\n----\n$ <strong>python src/manage.py test functional_tests.test_my_lists</strong>\n[...]\n    lambda: self.assertIn(\n            ~~~~~~~~~~~~~^\n        \"edith@example.com\",\n        ^^^^^^^^^^^^^^^^^^^^\n        self.browser.find_element(By.CSS_SELECTOR, \"h1\").text,\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\nAssertionError: 'edith@example.com' not found in 'Your To-Do list'\n----\n\nThat is telling us that we're going to have to build a page\nthat at least has the user's email in its header.\nLet's start with the basics--a URL and a placeholder template for it. Again, we can go outside-in,\nstarting at the presentation layer with just the URL and nothing else:\n\n[role=\"sourcecode\"]\n.src/lists/templates/base.html (ch24l006)\n====\n[source,html]\n----\n  {% if user.email %}\n    <a class=\"navbar-link\" href=\"{% url 'my_lists' user.email %}\">My lists</a>\n----\n====\n\n// TODO: mention urlencoding emails\n\n=== Moving Down One Layer to View Functions (the Controller)\n\n(((\"controller layer (outside-in TDD)\")))(((\"outside-in TDD\", \"controller layer\")))\nThat will cause a template error in the FT:\n\n[subs=\"\"]\n----\n$ <strong>./src/manage.py test functional_tests.test_my_lists</strong>\n[...]\nInternal Server Error: /\n[...]\n  File \"...goat-book/src/lists/views.py\", line 8, in home_page\n    return render(request, \"home.html\", {\"form\": ItemForm()})\n[...]\ndjango.urls.exceptions.NoReverseMatch: Reverse for 'my_lists' not found.\n'my_lists' is not a valid view function or pattern name.\n[...]\nERROR: test_logged_in_users_lists_are_saved_as_my_lists [...]\n[...]\nselenium.common.exceptions.NoSuchElementException: [...]\n----\n\nTo fix it, we'll need to start moving from working at the presentation layer,\ngradually into the controller layer—Django's URLs and views. As always, we start with a test.\nIn this layer, a unit test is the way to go:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch24l007)\n====\n[source,python]\n----\nclass MyListsTest(TestCase):\n    def test_my_lists_url_renders_my_lists_template(self):\n        response = self.client.get(\"/lists/users/a@b.com/\")\n        self.assertTemplateUsed(response, \"my_lists.html\")\n----\n====\n\nThat gives:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\nAssertionError: No templates used to render the response\n----\n\n[role=\"pagebreak-before\"]\nThat's because the URL doesn't exist yet, and a 404 has no template.\nLet's start our fix in _urls.py_:\n\n\n[role=\"sourcecode\"]\n.src/lists/urls.py (ch24l008)\n====\n[source,python]\n----\nurlpatterns = [\n    path(\"new\", views.new_list, name=\"new_list\"),\n    path(\"<int:list_id>/\", views.view_list, name=\"view_list\"),\n    path(\"users/<str:email>/\", views.my_lists, name=\"my_lists\"),\n]\n----\n====\n\n\nThat gives us a new test failure,\nwhich informs us of what we should do.\nAs you can see, it's pointing us at a _views.py_.\nWe're clearly in the controller layer:\n\n----\n    path(\"users/<str:email>/\", views.my_lists, name=\"my_lists\"),\n                               ^^^^^^^^^^^^^^\nAttributeError: module 'lists.views' has no attribute 'my_lists'\n----\n\n\nLet's create a minimal placeholder then:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch24l009)\n====\n[source,python]\n----\ndef my_lists(request, email):\n    return render(request, \"my_lists.html\")\n----\n====\n\nLet's also create a minimal template, with no real content\nexcept for the header that shows the user's email address:\n\n[role=\"sourcecode\"]\n.src/lists/templates/my_lists.html (ch24l010)\n====\n[source,html]\n----\n{% extends 'base.html' %}\n\n{% block header_text %}{{ user.email }}'s Lists{% endblock %}\n----\n====\n\nThat gets our unit tests passing:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *./src/manage.py test lists*\n[...]\nOK\n----\n\nAnd hopefully it will address the current error in our FT:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: Reticulate splines; [...]\n----\n\nStep by step! Sure enough, the FT gets a little further.\nIt can now find the email in the `<h1>`,\nbut it's now saying that the \"My lists\" page doesn't yet show any lists.\nIt wants them to appear as clickable links, named after the first item.\n\n=== Another Pass, Outside-In\n\n\n(((\"outside-in TDD\", \"FT-driven development\", id=\"OITDDft22\")))\n(((\"functional  tests (FTs)\", \"FT-driven development, outside-in technique\", id=\"ix_FToutin2\")))At each stage, we're still letting the FT drive what development we do.\nStarting again at the outside layer, in the template,\nwe begin to write the template code we'd like to use\nto get the \"My lists\" page to work the way we want it to.\nAs we do so, we start to specify the API we want\nfrom the code at the layers below.\n\n// Programming by wishful thinking, as always!\n\n\n==== A Quick Restructure of Our Template Composition\n\n(((\"templates\", \"composition\")))\nLet's take a look at our base template, _base.html_.\nIt currently has a lot of content that's specific to editing to-do lists,\nwhich our \"My lists\" page doesn't need:\n\n\n[role=\"sourcecode currentcontents small-code\"]\n.src/lists/templates/base.html\n====\n[source,html]\n----\n    <div class=\"container\">\n\n      <nav class=\"navbar\">\n        [...]\n      </nav>\n\n      {% if messages %}\n        [...]\n      {% endif %}\n\n      <div class=\"row justify-content-center p-5 bg-body-tertiary rounded-3\">\n        <div class=\"col-lg-6 text-center\">\n          <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n\n          <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >  <1>\n            [...]\n          </form>\n        </div>\n      </div>\n\n      <div class=\"row justify-content-center\">\n        <div class=\"col-lg-6\">\n          {% block table %}  <2>\n          {% endblock %}\n        </div>\n      </div>\n\n    </div>\n\n    <script src=\"/static/lists.js\"></script>  <3>\n      [...]\n----\n====\n\n[role=\"pagebreak-before\"]\n<1> The `<form>` tag is definitely something we only want on pages where we edit lists.\n    Everything else up to this point is generic enough to be on any page.\n\n<2> Similarly, the `{% block table %}` isn't something we'd need on the \"My lists\" page.\n\n<3> Finally, the `<script>` tag is specific to lists too.\n\nSo, we'll want to change things so that _base.html_ is a bit more generic.\n\nLet's recap. We've got three actual pages we want to render:\n\n1. The home page (where you can enter a first to-do item to create a new list)\n2. The \"List\" page (where you can view an existing list and add to it)\n3. The \"My lists\" page (which is a list of all your existing lists)\n\nAnd the home page and list page both share the same \"form\" elements and the _lists.js_ JavaScript. But the \"List\" page is the only one that needs to show the full table of list items. The \"My lists\" page doesn't need anything related to editing or displaying lists.\n\nSo, we have some things shared between all three, and some only shared between the first and second.\n\nSo far, we've been using inheritance to share the common parts of our templates,\nbut this is a good place to start using composition instead.\nAt the moment, we're saying that \"home\" is a type of \"base\" template,\nbut with the \"table\" section switched off, which is a bit awkward.\nLet's not make it even more awkward by saying that \"list\"\nis a \"base\" template with both the form and the table switched off!\nIt might make more sense to say that \"home\" is a type of base template that includes a list form,\nbut no table, and that \"list\" includes both the list form and the list table.\n\n\nTIP: People often say \"prefer composition over inheritance\",(((\"composition over inheritance principle\")))\n    because inheritance can become hard to reason about as the inheritance hierarchy grows.\n    Composition is more flexible\n    and often makes more sense.\n    For a lengthy discussion of this topic, see\n    https://hynek.me/articles/python-subclassing-redux/[Hynek Schlawack's definitive article on subclassing in Python].\n\n[role=\"pagebreak-before\"]\nSo, let's do the following:\n\n1. Pull out the `<form>` tag and the _lists.js_ `<script>` tag into into some blocks\n   we can \"include\" in our home page and lists page.\n\n2. Move the `<table>` block so it only exists in the list page.\n\n3. Take all the list-specific stuff out of the _base.html_ template,\n   making it into a more generic page with a header and a placeholder for generic content.\n\nWe'll use what's called an \nhttps://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include[`include`]\nto compose reusable template fragments\nwhen we don't want to use inheritance.\n\n\n==== An Early Return So We're Refactoring Against Green\n\n\nBefore we start refactoring, let's put an early return in our FT,\nso we're refactoring against green tests:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch24l010-0)\n====\n[source,python]\n----\n        # She sees her email is there in the page heading\n        self.wait_for(\n            lambda: self.assertIn(\n                \"edith@example.com\",\n                self.browser.find_element(By.CSS_SELECTOR, \"h1\").text,\n            )\n        )\n        return # TODO: resume here after templates refactor\n\n        # And she sees that her list is in there,\n        # named according to its first list item\n        [...]\n----\n====\n\nVerify the FTs are all green:\n\n----\nRan 8 tests in 19.712s\n\nOK\n----\n\n[role=\"pagebreak-before less_space\"]\n==== Factoring Out Two Template includes\n\nFirst let's pull out the form and the script tag from _base.html_:\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/base.html (ch24l010-1)\n====\n[source,diff]\n----\n@@ -58,43 +58,19 @@\n         <div class=\"col-lg-6 text-center\">\n           <h1 class=\"display-1 mb-4\">{% block header_text %}{% endblock %}</h1>\n \n-          <form method=\"POST\" action=\"{% block form_action %}{% endblock %}\" >\n-            {% csrf_token %}\n-            <input\n-              id=\"id_text\"\n-              name=\"text\"\n-              class=\"form-control\n-                     form-control-lg\n-                     {% if form.errors %}is-invalid{% endif %}\"\n-              placeholder=\"Enter a to-do item\"\n-              value=\"{{ form.text.value }}\"\n-              aria-describedby=\"id_text_feedback\"\n-              required\n-            />\n-            {% if form.errors %}\n-              <div id=\"id_text_feedback\" class=\"invalid-feedback\">\n-                {{ form.errors.text.0 }}\n-              </div>\n-            {% endif %}\n-          </form>\n+          {% block extra_header %}\n+          {% endblock %}\n+\n         </div>\n       </div>\n \n-      <div class=\"row justify-content-center\">\n-        <div class=\"col-lg-6\">\n-          {% block table %}\n-          {% endblock %}\n-        </div>\n-      </div>\n+      {% block content %}\n+      {% endblock %}\n \n     </div>\n \n-    <script src=\"/static/lists.js\"></script>\n-    <script>\n-      window.onload = () => {\n-        initialize(\"#id_text\");\n-      };\n-    </script>\n+    {% block scripts %}\n+    {% endblock %}\n \n   </body>\n </html>\n----\n====\n\n[role=\"pagebreak-before\"]\nYou can see we've replaced all the list-specific stuff with three new blocks:\n\n. `extra_header` for anything we want to put in the big header section\n. `content` for the main content of the page\n. `scripts` for any JavaScript we want to include\n\nLet's paste the `<form>` tag into a file at\n_src/lists/templates/includes/form.html_\n(having a subfolder in templates for includes is a common practice):\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/includes/form.html (ch24l010-2)\n====\n[source,html]\n----\n<form method=\"POST\" action=\"{{ form_action }}\" >  <1>\n  {% csrf_token %}\n  <input\n    id=\"id_text\"\n    name=\"text\"\n    class=\"form-control\n           form-control-lg\n           {% if form.errors %}is-invalid{% endif %}\"\n    placeholder=\"Enter a to-do item\"\n    value=\"{{ form.text.value | default:'' }}\"\n    aria-describedby=\"id_text_feedback\"\n    required\n  />\n  {% if form.errors %}\n    <div id=\"id_text_feedback\" class=\"invalid-feedback\">\n      {{ form.errors.text.0 }}\n    </div>\n  {% endif %}\n</form>\n----\n====\n\n<1> This is the only change;\n    we've replaced the `{% block form_action %}` with `{{ form_action }}`.\n\n\nLet's paste the ++script++ tags verbatim\ninto a new file at _includes/scripts.html_:\n\n[role=\"sourcecode\"]\n.src/lists/templates/includes/scripts.html (ch24l010-3)\n====\n[source,html]\n----\n<script src=\"/static/lists.js\"></script>\n\n<script>\n  window.onload = () => {\n    initialize(\"#id_text\");\n  };\n</script>\n----\n====\n\n[role=\"pagebreak-before\"]\nNow let's look at how to use the `include`,\nand how the `form_action` change plays out\nin the changes to _home.html_:\n\n\n[role=\"sourcecode small-code\"]\n.src/lists/templates/home.html (ch24l010-4)\n====\n[source,html]\n----\n{% extends 'base.html' %}\n\n{% block header_text %}Start a new To-Do list{% endblock %}\n\n{% block extra_header %}\n  {% url 'new_list' as form_action %}  <1>\n  {% include \"includes/form.html\" with form=form form_action=form_action %}  <2>\n{% endblock %}\n\n{% block scripts %}  <3>\n  {% include \"includes/scripts.html\" %}\n{% endblock %}\n----\n====\n\n<1> The `{% url ... as %}` syntax lets us define a template variable inline.\n\n<2> Then we use `{% include ... with key=value... %}`\n    to pull in the contents of the `form.html` template,\n    with the appropriate context variables passed in--a bit like\n    calling a function.footnote:[\n    Strictly speaking, you could have omitted the `with=` in this case,\n    as included templates automatically get the context of their parent.\n    But sometimes you want to pass a context variable under a different name,\n    so I like the `with`, for consistency and explicitness.]\n\n<3> The `scripts` block is just a straightforward `include`\n    with no variables.\n\n[role=\"pagebreak-before\"]\nNow let's see it in _list.html_:\n\n[role=\"sourcecode\"]\n.src/lists/templates/list.html (ch24l010-5)\n====\n[source,diff]\n----\n@@ -2,12 +2,24 @@\n\n {% block header_text %}Your To-Do list{% endblock %}\n\n-{% block form_action %}{% url 'view_list' list.id %}{% endblock %}\n\n-{% block table %}\n+{% block extra_header %}  <1>\n+  {% url 'view_list' list.id as form_action %}\n+  {% include \"includes/form.html\" with form=form form_action=form_action %}\n+{% endblock %}\n+\n+{% block content %}  <2>\n+<div class=\"row justify-content-center\">\n+  <div class=\"col-lg-6\">\n     <table class=\"table\" id=\"id_list_table\">\n       {% for item in list.item_set.all %}\n         <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>\n       {% endfor %}\n     </table>\n+  </div>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}  <3>\n+  {% include \"includes/scripts.html\" %}\n {% endblock %}\n\n----\n====\n\n<1> The `block table` becomes an `extra_header` block,\n    and we use the `include` to pull in the form.\n\n<2> The `block table` becomes a `content` block,\n    with all the HTML we need for our table.\n\n<3> And the `scripts` block is the same as the one from _home.html_.\n\n\nNow a little rerun of all our FTs to make sure we haven't broken anything:\n\n----\nRan 8 tests in 19.712s\n\nOK\n----\n\n[role=\"pagebreak-before\"]\nOK, let's remove the early return:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_my_lists.py (ch24l010-6)\n====\n[source,diff]\n----\n@@ -44,7 +44,6 @@ class MyListsTest(FunctionalTest):\n                 self.browser.find_element(By.CSS_SELECTOR, \"h1\").text,\n             )\n         )\n-        return # TODO: resume here after templates refactor\n\n         # And she sees that her list is in there,\n         # named according to its first list item\n----\n====\n\n\n// CSANAD something somewhere broke my styling tests, even though right now I work from the book-example\n// commit-to-commit (no manual changes, just `git checkout` the next commit).\n\n// DAVID: For some reason I got an extra failure in test_layout_and_styling. Running it again, it passed.\n// Is it possible it's flakey? (I appreciate this is a terribly vague bug report so feel free to ignore.)\n\nAnd we'll commit that as a nice refactor:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/lists/templates*\n$ *git commit -m \"refactor templates to use composition/includes\"*\n----\n\n\nNow let's get back to our outside-in process,\nand to working in our template to drive out the requirements\nfor our views layer.\n\n\n==== Designing Our API Using the Template\n\nWith the early return removed,\nour FT is back to telling us that we need to actually show our lists—named after their first items—on the new \"My lists\" page:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *./src/manage.py test functional_tests.test_my_lists*\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: Reticulate splines; [...]\n----\n\n(If you haven't taken a look around the site recently,\nit does look pretty blank—see <<empty-my-lists-page>>.)\n\n[[empty-my-lists-page]]\n.Not much to see here\nimage::images/tdd3_2403.png[\"A screenshot of the My Lists page, showing the user's email in the page header, but no lists.\"]\n\n\n(((\"templates\", \"designing APIs using\")))\nSo, in _my_lists.html_, we can now work in the `content` block:\n\n[role=\"sourcecode\"]\n.src/lists/templates/my_lists.html (ch24l010-7)\n====\n[source,html]\n----\n[...]\n\n{% block content %}\n  <h2>{{ owner.email }}'s lists</h2>  <1>\n  <ul>\n    {% for list in owner.lists.all %}  <2>\n      <li><a href=\"{{ list.get_absolute_url }}\">{{ list.name }}</a></li>  <3>\n    {% endfor %}\n  </ul>\n{% endblock %}\n----\n====\n\n// TODO: look into changing the user.email at the top to owner.email as well\n// the trouble is that changing it at this point introduces a regression\n// in the FT.\n\nWe've made several design decisions in this template\nthat are going to filter their way down through the code:\n\n<1> We want a variable called `owner` to represent the user in our template.\n    This is what will allow one user to view another user's lists.\n\n<2> We want to be able to iterate through the lists created by that user\n    using `owner.lists.all`.\n    (I happen to know how to make this work with the Django ORM.)\n\n<3> We want to use `list.name` to print out the \"name\" of the list,\n    which is currently specified as the text of its first element.\n\n\n\n.Programming by Wishful Thinking Again, Still\n*******************************************************************************\n\nThe phrase \"programming by wishful thinking\" was first popularised by\nthe amazing, mind-expanding textbook\nhttps://oreil.ly/5EZNI[Structure and Interpretation of Computer Programs (SICP)], which I _cannot_ recommend highly enough.\n\nIn it, the authors use it as a way to think about and write code\nat a higher level of abstraction,\nwithout worrying about the details of a lower level\nthat might not even exist yet.\nFor them, it's a key tool for designing programs\nand managing complexity.\n\nWe've been doing a lot of \"programming by wishful thinking\" in this book.\nWe've talked about how TDD itself is a form of wishful thinking;\nour tests express that we _wish_ we had code that worked in such-and-such a way.\n\nOutside-in TDD is very much an extension of this philosophy.\nWe start writing code at the higher levels\nbased on what we _wish_ we had at the lower levels,\neven though it doesn't exist yet...\n\nYAGNI also comes into it.  By driving our development from the outside in,\neach piece of code we write is only there because we know it's actually needed\nby a higher layer and, ultimately, by the user.\n\n*******************************************************************************\n\n\nWe can rerun our FTs to check that we didn't break anything,\nand to see whether we've gotten any further:\n\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: Reticulate splines; [...]\n\n ---------------------------------------------------------------------\nRan 8 tests in 77.613s\n\nFAILED (errors=1)\n----\n\nWell, no further—but at least we didn't break anything. Time for a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/lists*\n$ *git diff --staged*   # urls+views.py, templates\n$ *git commit -m \"url, placeholder view, and first-cut templates for my_lists\"*\n----\n\n[role=\"pagebreak-before less_space\"]\n==== Moving Down to the Next Layer: What the View Passes to the Template\n\n(((\"templates\", \"views layer and\")))\nNow our views layer needs to respond to the requirements we've laid out in the template layer,\nby giving it the objects it needs—in this case, the list owner:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch24l011)\n====\n[source,python]\n----\nfrom accounts.models import User\n[...]\n\n\nclass MyListsTest(TestCase):\n    def test_my_lists_url_renders_my_lists_template(self):\n        [...]\n\n    def test_passes_correct_owner_to_template(self):\n        User.objects.create(email=\"wrong@owner.com\")\n        correct_user = User.objects.create(email=\"a@b.com\")\n        response = self.client.get(\"/lists/users/a@b.com/\")\n        self.assertEqual(response.context[\"owner\"], correct_user)\n----\n====\n\nThat gives:\n\n----\nKeyError: 'owner'\n----\n\n\nSo:\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch24l012)\n====\n[source,python]\n----\nfrom accounts.models import User\n[...]\n\n\ndef my_lists(request, email):\n    owner = User.objects.get(email=email)\n    return render(request, \"my_lists.html\", {\"owner\": owner})\n----\n====\n\n\nThat gets our new test passing, but we'll also see an error from the previous test.\nWe just need to add a user for it as well:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch24l013)\n====\n[source,python]\n----\n    def test_my_lists_url_renders_my_lists_template(self):\n        User.objects.create(email=\"a@b.com\")\n        [...]\n----\n====\n\nAnd we get to an OK:\n(((\"functional  tests (FTs)\", \"FT-driven development, outside-in technique\", startref=\"ix_FToutin2\")))(((\"\", startref=\"OITDDft22\")))\n\n\n----\nOK\n----\n\n\n=== The Next \"Requirement\" from the Views Layer: [.keep-together]#New Lists Should Record Owner#\n\n(((\"outside-in TDD\", \"views layer\")))\nBefore we move down to the model layer,\nthere's another part of the code at the view layer that will need to use our model:\nwe need some way for newly created lists to be assigned to an owner,\nif the current user is logged in to the site.\n\nHere's a first crack at writing the test:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_views.py (ch24l014)\n====\n[source,python]\n----\nclass NewListTest(TestCase):\n    [...]\n\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        user = User.objects.create(email=\"a@b.com\")\n        self.client.force_login(user)  #<1>\n        self.client.post(\"/lists/new\", data={\"text\": \"new item\"})\n        new_list = List.objects.get()\n        self.assertEqual(new_list.owner, user)\n----\n====\n\n<1> `force_login()` is the way you get the test client to make requests\n    with a logged-in user.\n\nThe test fails as follows:\n\n----\nAttributeError: 'List' object has no attribute 'owner'\n----\n\nTo fix it, let's first try writing code like this:\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch24l015)\n====\n[source,python]\n----\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        nulist = List.objects.create()\n        nulist.owner = request.user  <1>\n        nulist.save()  <2>\n        form.save(for_list=nulist)\n        return redirect(nulist)\n    else:\n        return render(request, \"home.html\", {\"form\": form})\n----\n====\n\n<1> We'll set the `.owner` attribute on our new list.\n<2> And we'll try and save it to the database.\n\n[role=\"pagebreak-before\"]\nBut it won't actually work, because we don't know _how_ to save a list owner yet:\n\n\n----\n    self.assertEqual(new_list.owner, user)\n                     ^^^^^^^^^^^^^^\nAttributeError: 'List' object has no attribute 'owner'\n----\n\n\n==== A Decision Point: Whether to Proceed to the Next Layer with a Failing Test\n\n(((\"outside-in TDD\", \"model layer\", id=\"OITDDmodel21\")))\nIn order to get this test passing, as it's written now,\nwe have to move down to the model layer.\nHowever, it means doing more work with a failing test, which is not ideal.(((\"isolation of tests\", \"using mocks for\")))(((\"mocks\", \"isolating tests using\"))) The alternative is to rewrite the test\nto make it more _isolated_ from the level below, using mocks.\n\nOn the one hand, it's a lot more effort to use mocks,\nand it can lead to tests that are harder to read.\nOn the other hand, advocates of London-school TDD\nare very keen on the approach.\nYou can read an exploration of this approach in \nhttps://www.obeythetestinggoat.com/book/appendix_purist_unit_tests.html[Online Appendix: Test Isolation and \"Listening to Your Tests\"].\n\nFor now, we'll accept the trade-off: moving down one layer with failing tests,\nbut avoiding the extra mocks.\n\n[[revisit_this_point_with_isolated_tests]]\nLet's do a commit, and then `tag` the commit as a way of remembering our\nposition if we want to revisit this decision later:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git commit -am \"new_list view tries to assign owner but cant\"*\n$ *git tag revisit_this_point_with_isolated_tests*\n----\n\n\n=== Moving Down to the Model Layer\n\nOur outside-in design has driven out two requirements for the model layer:\nwe want to be able to assign an owner to a list using the attribute `.owner`,\nand we want to be able to access the list's owner with the API `owner.lists.all()`.\n\n\nLet's write a test for that:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch24l018)\n====\n[source,python]\n----\nfrom accounts.models import User\n[...]\n\n\nclass ListModelTest(TestCase):\n    def test_get_absolute_url(self):\n        [...]\n    def test_list_items_order(self):\n        [...]\n\n    def test_lists_can_have_owners(self):\n        user = User.objects.create(email=\"a@b.com\")\n        mylist = List.objects.create(owner=user)\n        self.assertIn(mylist, user.lists.all())\n----\n====\n\nAnd that gives us a new unit test failure:\n\n----\n    mylist = List.objects.create(owner=user)\n    [...]\nTypeError: List() got unexpected keyword arguments: 'owner'\n----\n\nThe naive implementation would be this:\n\n[role=\"skipme\"]\n[source,python]\n----\nfrom django.conf import settings\n[...]\n\nclass List(models.Model):\n    owner = models.ForeignKey(settings.AUTH_USER_MODEL)\n----\n\nBut we want to make sure the list owner is optional.  Explicit\nis better than implicit, and tests are documentation, so let's have a test for\nthat too:\n\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch24l020)\n====\n[source,python]\n----\n    def test_list_owner_is_optional(self):\n        List.objects.create()  # should not raise\n----\n====\n\nThe correct implementation is this:\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch24l021)\n====\n[source,python]\n----\n\nclass List(models.Model):\n    owner = models.ForeignKey(\n        \"accounts.User\",\n        related_name=\"lists\",\n        blank=True,\n        null=True,\n        on_delete=models.CASCADE,\n    )\n\n    def get_absolute_url(self):\n        return reverse(\"view_list\", args=[self.id])\n----\n====\n\nNow running the tests gives the usual database error:\n\n----\n    return super().execute(query, params)\n           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\ndjango.db.utils.OperationalError: table lists_list has no column named owner_id\n----\n\n\nBecause we need to make some migrations:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py makemigrations*]\nMigrations for 'lists':\n  src/lists/migrations/0007_list_owner.py\n    + Add field owner to list\n----\n//22\n\nWe're almost there; a couple more failures in some of our old tests:\n\n----\nERROR: test_can_save_a_POST_request\n[...]\nValueError: Cannot assign \"<SimpleLazyObject:\n<django.contrib.auth.models.AnonymousUser object at 0x1069852e>>\": \"List.owner\"\nmust be a \"User\" instance.\n[...]\n\nERROR: test_redirects_after_POST\n[...]\nValueError: Cannot assign \"<SimpleLazyObject:\n<django.contrib.auth.models.AnonymousUser object at 0x106a1b440>>\": \"List.owner\"\nmust be a \"User\" instance.\n----\n\nWe're moving back up to the views layer now, just doing a little tidying up.\nNotice that these are in the existing test for the `new_list` view,\nwhen we haven't got a logged-in user.\n\nThe tests are reminding us to think of this use case too:\nwe should only save the list owner when the user is actually logged in.\nThe `.is_authenticated` attribute we came across in <<chapter_19_spiking_custom_auth>>\ncomes in useful now:footnote:[When they're not logged in,\nDjango represents users using a class called `AnonymousUser`,\nwhose `.is_authenticated` is always `False`.]\n\n\n[role=\"sourcecode\"]\n.src/lists/views.py (ch24l023)\n====\n[source,python]\n----\n    if form.is_valid():\n        nulist = List.objects.create()\n        if request.user.is_authenticated:\n            nulist.owner = request.user\n            nulist.save()\n        form.save(for_list=nulist)\n        return redirect(nulist)\n        [...]\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd that gets us passing!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test lists*]\n[...]\n\nRan 36 tests in 0.237s\n\nOK\n----\n\nThis is a good time for a commit:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/lists*\n$ *git commit -m \"lists can have owners, which are saved on creation.\"*\n----\n\n\n\n=== Final Step: Feeding Through the .name API from the Template\n\nThe last thing our outside-in design wanted (((\"outside-in TDD\", \"accessing list name through the template\")))came from the templates,\nwhich want to be able to access a list \"name\" based on the text of\nits first item:\n\n[role=\"sourcecode\"]\n.src/lists/tests/test_models.py (ch24l024)\n====\n[source,python]\n----\n    def test_list_name_is_first_item_text(self):\n        list_ = List.objects.create()\n        Item.objects.create(list=list_, text=\"first item\")\n        Item.objects.create(list=list_, text=\"second item\")\n        self.assertEqual(list_.name, \"first item\")\n----\n====\n\n\n[role=\"sourcecode\"]\n.src/lists/models.py (ch24l025)\n====\n[source,python]\n----\n    @property\n    def name(self):\n        return self.item_set.first().text\n----\n====\n\nAnd that, believe it or not, actually gets us a passing test\nand a working \"My lists\" page (see <<my-lists-page>>)!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests*]\n[...]\nRan 8 tests in 93.819s\n\nOK\n----\n\n[[my-lists-page]]\n.The \"My lists\" page, in all its glory (and proof I did test on Windows)\nimage::images/tdd3_2404.png[\"Screenshot of new My Lists page\"]\n\n// DAVID: At the moment it's possible to see other users' list pages. (That's by design, right?)\n// But then - I hate to say it - we probably shouldn't call it the 'My Lists' page, except in\n// the link to the page.\n\n.The @property Decorator in Python\n*******************************************************************************\n\n(((\"@property decorator\")))\n(((\"decorators\", \"property decorator\")))\n(((\"Python 3\", \"@property decorator\")))\nIf you haven't seen it before, the `@property` decorator transforms a method\non a class to make it appear to the outside world like an attribute.\n\n(((\"duck typing\")))\nThis is a powerful feature of the language, because it makes it easy to\nimplement \"duck typing\"—to change the implementation of a property without\nchanging the interface of the class.  In other words, if we decide to change\n`.name` into being a \"real\" attribute on the model, stored as text in\nthe database, then we will be able to do so entirely transparently--as far as\nthe rest of our code is concerned, will still be able to just access\n`.name` and get the list name, without needing to know about the\nimplementation.\n\nRaymond Hettinger gave a https://oreil.ly/WQ2CX[classic, beginner-friendly talk on\nthis topic at PyCon back in 2013], which I enthusiastically recommend (it\ncovers about a million good practices for Pythonic class design besides).\nOf course, in the Django template language, `.name` would still call the method\neven if it didn't have `@property`, but that's a particularity of Django, and\ndoesn't apply to Python in general...\n*******************************************************************************\n\n// SEBASTIAN: While @property indeed is a helpful gimmick, I consider\n// @property doing DB operations or causing other side-effects an anti-pattern.\n// I wonder if readers of the book are also not already knowing that.\n// What I suggest is to consider whether to keep it in this chapter or not.\n// It seems to be a bit off. Might be as \"quick hack\" we're doing constantly to make\n// tests pass but I wouldn't settle on leaving it as it is.\n\n(((\"\", startref=\"OITDDmodel21\")))\nIn the next chapter, it's time to recruit some computers\nto do more of the work for us.  Let's talk about continuous integration (CI).\n\n// RITA: Later, let's check to make sure the end-of-chapter sidebars are consistent throughout the book. Some of them are called \"Lessons Learned,\" others \"Lessons Learned in X,\" and others are called just the topic name.\n\n.Outside-In TDD\n*******************************************************************************\n\nOutside-in TDD::\n    This is methodology for building code, driven by tests,\n    which proceeds by starting from the \"outside\" layers (presentation, GUI),\n    and moving \"inwards\" step by step, via view/controller layers,\n    down towards the model layer.\n    The idea is to drive the design of your code from how it will be used,\n    rather than trying to anticipate requirements from the bottom up.\n    (((\"outside-in TDD\", \"defined\")))\n\n// SEBASTIAN: Might be worth mentioning that outside-in plays nicely with API-first\n// or, at the very least, that it may also mean writing test at the API level\n// if we have a SPA\n\n\nProgramming by wishful thinking::\n    The outside-in process is sometimes called \"programming by wishful thinking\".\n    Actually, any kind of TDD involves some wishful thinking.\n    We're always writing tests for things that don't exist yet.\n    (((\"programming by wishful thinking\")))\n\n\nThe pitfalls of outside-in::\n    Outside-in isn't a silver bullet.\n    It encourages us to focus on things that are immediately visible to the user,\n    but it won't automatically remind us to write other critical tests\n    that are less user-visible--things like security, for example.\n    You'll need to remember them yourself.\n    (((\"\", startref=\"TTDoutside22\")))\n    (((\"outside-in TDD\", \"drawbacks of\")))\n\n\n*******************************************************************************\n\n"
  },
  {
    "path": "chapter_25_CI.asciidoc",
    "content": "[[chapter_25_CI]]\n== CI: Continuous Integration\n\n(((\"continuous integration (CI)\", id=\"CI24\")))\n(((\"continuous integration (CI)\", \"benefits of\")))\nAs our site grows, it takes longer and longer to run all of our functional tests.\nIf this continues, the danger is that we're going to stop bothering.(((\"CI\", see=\"continuous integration\")))\n\nRather than let that happen, we can automate the running of functional tests\nby setting up \"continuous integration\", or CI.\nThat way, in day-to-day development,\nwe can just run the FT that we're working on at that time,\nand rely on CI to run all the other tests automatically\nand let us know if we've broken anything accidentally.\n\n\nThe unit tests should stay fast enough that we can keep running\nthe full suite locally, every few seconds.\n\nNOTE: Continuous integration is another practice that was popularised by\n    Kent Beck's\n    https://martinfowler.com/bliki/ExtremeProgramming.html[extreme programming (XP)]\n    movement in the 1990s.\n\nAs we'll see, one of the great frustrations of configuring CI\nis that the feedback loop is so much slower than working locally.\nAs we go along, we'll look for ways to optimise for that where we can.\n\nWhile debugging, we'll also touch on the theme of _reproducibility_. It's the idea that we want to be able to reproduce behaviours of our CI environment locally—in the same way that we try and make our production and dev environments as similar as possible.\n\n[role=\"pagebreak-before less_space\"]\n=== CI in Modern Development Workflows\n\nWe use CI for a number of reasons:\n\n* As mentioned, it can patiently run the full suite of tests,\n  even if they've grown too large to run locally.\n\n* It can act as a \"gate\" in your deployment/release process,\n  to ensure that you never deploy code that isn't passing all the tests.\n\n* In open source projects that use a \"pull request\" workflow,\n  it's a way to ensure that any code submitted by potentially unknown\n  contributors passes all your tests, before you consider merging it.\n\n* It's (sadly) increasingly common in corporate environments\n  to see this pull request process and its associated CI checks\n  to be used as the default way for teams to merge all code changes.footnote:[\nI say \"sadly\" because you _should_ be able to trust your colleagues,\nnot put them through a process designed for open source projects\nto de-risk code contributions from random strangers on the internet.\nLook up \"trunk-based development\"\nif you want to see more old people shouting at clouds on this topic.]\n\n\n\n=== Choosing a CI Service\n\n(((\"continuous integration (CI)\", \"choosing a service\")))\nIn the early days, CI would be implemented by configuring a server\n(perhaps under a desk in the corner of the office)\nwith software on it that could pull down all the code from the main branch\nat the end of each day, and scripts to compile all the code and run all the tests—a process that became known as a \"build\".\nThen, each morning, developers would take a look at the results,\nand deal with any broken builds.\n\nAs the practice spread, and feedback cycles grew faster,\nCI software matured. CI has become a common cloud-based service,\ndesigned to integrate with code hosting providers like GitHub—or even provided directly by the same providers.\nGitHub has \"GitHub Actions\", and because it's like, _right there_,\nit's probably the most popular choice for open source projects these days.\nIn a corporate environment, you might come across other solutions\nlike CircleCI, Travis CI, and GitLab.\n\nIt is still absolutely possible to download and self-host your own CI server;\nin the first and second editions of this book,\nI demonstrated the use of Jenkins, a popular tool at the time.\nBut the installation and subsequent admin/maintenance burden is not effort-free,\nso for this edition I wanted to pick a service more like the kind of thing you're likely to encounter in your day job—while trying not to endorse the largest commercial provider.\nThere's nothing wrong with GitHub Actions!\nIt just doesn't need any _more_ help dominating the market.\n\n\nSo I've decided to use GitLab in this book.\nIt is absolutely a commercial service,\nbut it retains an open source version, and you can self-host it if you want to. The syntax (it's always YAML...) and core concepts are common across all providers,\nso the things you learn here will be replicable in whichever service\nyou encounter in future.\n\nLike most of the services out there, GitLab has a free tier,\nwhich will work fine for our purposes.\n\n\n=== Getting Our Code into GitLab\n\nGitLab is primarily a code hosting service, like GitHub,\nso the first thing to do is get our code up there.(((\"GitLab\", \"getting code into\", id=\"ix_GitL\")))\n\n\n==== Signing Up\n\nHead over to https://gitlab.com[GitLab.com], and sign up for a free account.\n\nThen, head over to your profile page, and find the SSH Keys section,\nand upload a copy of your public key.\n\n\n\n==== Starting a Project\n\nThen, use the New Project -> Create Blank Project option,\nas in <<gitlab-new-blank-project>>. Feel free to name the project whatever you want;\nyou can see I've fancifully named mine with a \"z\".\nI'm a free spirit, what can I say.\n\n.Creating a new repo on GitLab\n[[gitlab-new-blank-project]]\nimage::images/tdd3_2501.png[\"New Blank Project\"]\n\n\n==== Pushing Our Code Up Using Git Push\n\nFirst, we set up GitLab as a \"remote\" for our project:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n# substitute your username and project name as necessary\n$ *git remote add gitlab git@gitlab.com:yourusername/superlists.git*\n$ *git remote -v*\ngitlab    git@gitlab.com:hjwp/superlistz.git (fetch)\ngitlab    git@gitlab.com:hjwp/superlistz.git (push)\norigin    git@github.com:hjwp/book-example.git (fetch)\norigin    git@github.com:hjwp/book-example.git (push)\n# (as you can see i already had a remote for github called 'origin')\n----\n\n\nNow we can push up our code with `git push`:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git push gitlab*\nEnumerating objects: 706, done.\nCounting objects: 100% (706/706), done.\nDelta compression using up to 11 threads\nCompressing objects: 100% (279/279), done.\nWriting objects: 100% (706/706), 918.72 KiB | 131.25 MiB/s, done.\nTotal 706 (delta 413), reused 682 (delta 408), pack-reused 0 (from 0)\nremote: Resolving deltas: 100% (413/413), done.\nTo gitlab.com:hjwp/superlistz.git\n * [new branch]        main -> main\nbranch 'main' set up to track 'gitlab/main'.\n----\n\nIf you refresh the GitLab UI, you should now see your code,\nas in <<gitlab_files_ui>>.\n\n.CI project files on GitLab\n[[gitlab_files_ui]]\nimage::images/tdd3_2502.png[\"GitLab UI showing project files\"]\n\n\n=== Setting Up a First Cut of a CI Pipeline\n\nThe \"pipeline\" terminology was popularised by Dave Farley and Jez Humble\nin their book _Continuous Delivery_ (Addison-Wesley).(((\"GitLab\", \"getting code into\", startref=\"ix_GitL\")))(((\"pipelines (CI)\")))(((\"continuous integration (CI)\", \"setting up CI pipeline, first cut\", id=\"ix_CIpipe1\")))\nThe name alludes to the fact that a CI build typically has a series,\nwhere the process flows from one to another.\n\n\nGo to Build -> Pipelines, and you'll see a list of example templates.\nWhen getting to know a new configuration language,\nit's nice to be able to start with something that works,\nrather than a blank slate. I chose the Python example template and made a few customisations,\nbut you could just as easily start from a blank slate and paste\nwhat I have here (YAML, once again, folks!):\n\n\n\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml (ch25l001)\n====\n[source,yaml]\n----\n# Use the same image as our Dockerfile\nimage: python:slim\n\n# These two settings let us cache pip-installed packages,\n# it came from the default template\nvariables:\n  PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\ncache:\n  paths:\n    - .cache/pip\n\n# \"setUp\" phase, before the main build\nbefore_script:\n  - python --version ; pip --version  # For debugging\n  - pip install virtualenv\n  - virtualenv .venv\n  - source .venv/bin/activate\n\n# This is the main build\ntest:\n  script:\n    - pip install -r requirements.txt  # <1>\n    # unit tests\n    - python src/manage.py test lists accounts  # <2>\n    # (if those pass) all tests, incl. functional.\n    - pip install selenium  # <3>\n    - cd src && python manage.py test  # <4>\n----\n====\n\n<1> We start by installing our core requirements.\n\n<2> I've decided to run the unit tests first.\n    This gives us an \"early failure\" if  there's any problem at this stage,\n    and saves us from having to run—and more importantly, wait for—the FTs to run.\n\n<3> Then we need Selenium for the functional tests.\n    Again, I'm delaying this `pip install` until it's absolutely necessary,\n    to get feedback as quickly as possible.\n\n<4> And here is a full test run, including the functional tests.\n\n\nTIP: It's a good idea in CI pipelines to try and run the quickest tests first,\n    so that you can get feedback as quickly as possible.\n\n\nYou can use the GitLab web UI to edit your pipeline YAML,\nand then when you save it, you can go check for results straight away.\n\nBut it is also just a file in your repo!\nSo as we go on through the chapter, you can also just edit it locally.\nYou'll need to commit it and then `git push` up to GitLab,\nand then go check the Jobs section\nin the Build UI to see the results(((\"continuous integration (CI)\", \"setting up CI pipeline\", startref=\"ix_CIpipe1\"))) of your changes:\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git push gitlab*\n----\n\n\n=== First Build!  (and First Failure)\n\n// IDEA: consider deliberately forgetting to pip install selenium\n\nWhichever way you click through the UI, you should be able to find your way\nto see the output of the build job, as in <<gitlab_first_build>>.(((\"continuous integration (CI)\", \"building the pipeline\", id=\"ix_CIbld\")))(((\"GitLab\", \"building a CI pipeline in\")))\n\n.First build on GitLab\n[[gitlab_first_build]]\nimage::images/tdd3_2503.png[\"GitLab UI showing the output of the first build\"]\n\n\n[role=\"pagebreak-before\"]\nHere's a selection of what I saw in the output console:\n\n\n[role=\"skipme small-code\"]\n----\nRunning with gitlab-runner 17.7.0~pre.103.g896916a8 (896916a8)\n  on green-1.saas-linux-small-amd64.runners-manager.gitlab.com/default\n  JLgUopmM, system ID: s_deaa2ca09de7\nPreparing the \"docker+machine\" executor 00:20\nUsing Docker executor with image python:latest ...\nPulling docker image python:latest ...\n[...]\n$ python src/manage.py test lists accounts\nCreating test database for alias 'default'...\nFound 55 test(s).\nSystem check identified no issues (0 silenced).\n................../builds/hjwp/book-example/.venv/lib/python3.14/site-packages/django/core\n/handlers/base.py:61: UserWarning: No directory at: /builds/hjwp/book-example/src/static/\n  mw_instance = middleware(adapted_handler)\n.....................................\n ---------------------------------------------------------------------\nRan 53 tests in 0.129s\nOK\nDestroying test database for alias 'default'...\n$ pip install selenium\nCollecting selenium\n  Using cached selenium-4.28.1-py3-none-any.whl.metadata (7.1 kB)\nCollecting urllib3<3,>=1.26 (from urllib3[socks]<3,>=1.26->selenium)\n[...]\nSuccessfully installed attrs-25.1.0 certifi-2025.1.31 h11-0.14.0 idna-3.10 \noutcome-1.3.0.post0 pysocks-1.7.1 selenium-4.28.1 sniffio-1.3.1 sortedcontainers-2.4.0 \ntrio-0.29.0 trio-websocket-0.12.1 typing_extensions-4.12.2 urllib3-2.3.0 \nwebsocket-client-1.8.0 wsproto-1.2.0\n$ cd src && python manage.py test\nCreating test database for alias 'default'...\nFound 63 test(s).\nSystem check identified no issues (0 silenced).\n......../builds/hjwp/book-example/.venv/lib/python3.14/site-packages/django/core/handlers\n/base.py:61: UserWarning: No directory at: /builds/hjwp/book-example/src/static/\n  mw_instance = middleware(adapted_handler)\n...............................................EEEEEEEE\n======================================================================\nERROR: test_layout_and_styling (functional_tests.test_layout_and_styling.\nLayoutAndStylingTest.test_layout_and_styling)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/builds/hjwp/book-example/src/functional_tests/base.py\", line 30, in setUp\n    self.browser = webdriver.Firefox()\n                   ~~~~~~~~~~~~~~~~~^^\n\n[...]\nselenium.common.exceptions.WebDriverException: Message: Process unexpectedly closed with \nstatus 255\n ---------------------------------------------------------------------\nRan 61 tests in 8.658s\nFAILED (errors=8)\n\nselenium.common.exceptions.WebDriverException: Message: Process unexpectedly closed with \nstatus 255\n----\n\nNOTE: If GitLab won't run your build at this point,\n  you may need to go through some sort of identity-verification process.\n  Check your profile page.\n  \nYou can see we got through the unit tests,\nand then in the full test run we have 8 errors out of 63 tests.\nThe FTs are all failing. I'm \"lucky\" because I've done this sort of thing many times before,\nso I know what to expect:  it's failing because Firefox isn't installed\nin the image we're using.(((\"Firefox\", \"installing in container image\")))\n\n\nLet's modify the script, and add an `apt install`.\nAgain we'll do it as late as possible:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml (ch25l002)\n====\n[source,yaml]\n----\n# This is the main build\ntest:\n  script:\n    - pip install -r requirements.txt\n    # unit tests\n    - python src/manage.py test lists accounts\n    # (if those pass) all tests, incl. functional.\n    - apt update -y && apt install -y firefox-esr  # <1>\n    - pip install selenium\n    - cd src && python manage.py test\n----\n====\n\n<1> We use the Debian Linux `apt` package manager to install Firefox.\n    `firefox-esr` is the \"extended support release\",\n    which is a more stable version of Firefox to test against.\n\n[role=\"pagebreak-before\"]\nWhen you save that change (and commit and push if necessary),\nthe pipeline will run again.\nIf you wait a bit, you'll see we get a slightly different failure:\n\n\n[role=\"skipme small-code\"]\n----\n$ apt-get update -y && apt-get install -y firefox-esr\nGet:1 http://deb.debian.org/debian bookworm InRelease [151 kB]\nGet:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]\nGet:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]\n[...]\nThe following NEW packages will be installed:\n  adwaita-icon-theme alsa-topology-conf alsa-ucm-conf at-spi2-common\n  at-spi2-core dbus dbus-bin dbus-daemon dbus-session-bus-common\n  dbus-system-bus-common dbus-user-session dconf-gsettings-backend\n  dconf-service dmsetup firefox-esr fontconfig fontconfig-config\n[...]\nGet:117 http://deb.debian.org/debian-security bookworm-security/main amd64\nfirefox-esr amd64 128.7.0esr-1~deb12u1 [69.8 MB]\n[...]\nSelecting previously unselected package firefox-esr.\nPreparing to unpack .../105-firefox-esr_128.7.0esr-1~deb12u1_amd64.deb ...\nAdding 'diversion of /usr/bin/firefox to /usr/bin/firefox.real by firefox-esr'\nUnpacking firefox-esr (128.7.0esr-1~deb12u1) ...\n[...]\nSetting up firefox-esr (128.7.0esr-1~deb12u1) ...\nupdate-alternatives: using /usr/bin/firefox-esr to provide\n/usr/bin/x-www-browser (x-www-browser) in auto mode\n[...]\n======================================================================\nERROR: test_multiple_users_can_start_lists_at_different_urls\n(functional_tests.test_simple_list_creation.NewVisitorTest.\ntest_multiple_users_can_start_lists_at_different_urls)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/builds/hjwp/book-example/src/functional_tests/base.py\", line 30, in setUp\n    self.browser = webdriver.Firefox()\n                   ~~~~~~~~~~~~~~~~~^^\n[...]\nselenium.common.exceptions.WebDriverException: Message: Process unexpectedly\nclosed with status 1\n ---------------------------------------------------------------------\nRan 61 tests in 3.654s\nFAILED (errors=8)\n----\n\nWe can see Firefox installing OK, but we still get an error.\nThis time, it's exit code 1.\n\n[role=\"pagebreak-before less_space\"]\n==== Trying to Reproduce a CI Error Locally\n\nThe cycle of \"change _.gitlab-ci.yml_, push, wait for a build, check results\"\nis painfully slow. Let's see if we can reproduce this error locally.(((\"errors\", \"reproducing CI error locally\")))\n\nTo reproduce the CI environment locally, I put together a quick Dockerfile,\nby copy-pasting the steps in the `script` section and prefixing them with `RUN` commands:\n\n\n[role=\"sourcecode\"]\n.infra/Dockerfile.ci (ch25l003)\n====\n[source,dockerfile]\n----\nFROM python:slim\n\nRUN pip install virtualenv\nRUN virtualenv .venv\n\n# this won't work\n# RUN source .venv/bin/activate\n# use full path to venv instead.\n\nCOPY requirements.txt requirements.txt\nRUN .venv/bin/pip install -r requirements.txt\nRUN apt update -y && apt install -y firefox-esr\nRUN .venv/bin/pip install selenium\n\nCOPY infra/debug-ci.py debug-ci.py\nCMD .venv/bin/python debug-ci.py\n----\n====\n\nAnd let's add a little debug script at _debug-ci.py_:\n\n\n[role=\"sourcecode small-code\"]\n.infra/debug-ci.py (ch25l004)\n====\n[source,python]\n----\nfrom selenium import webdriver\n\n# just try to open a selenium session\nwebdriver.Firefox().quit()\n----\n====\n\n[role=\"pagebreak-before\"]\nWe build and run it like this:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:specialcharacters,quotes[*docker build -f infra/Dockerfile.ci -t debug-ci . && \\\n  docker run -it debug-ci*]\n[...]\n => [internal] load build definition from infra/Dockerfile.ci         0.0s\n => => transferring dockerfile: [...]\n => [internal] load metadata for docker.io/library/python:slim [...]\n => [1/8] FROM docker.io/library/python:slim@sha256:[...]\n => CACHED [2/8] RUN pip install virtualenv                           0.0s\n => CACHED [3/8] RUN virtualenv .venv                                 0.0s\n => CACHED [4/8] COPY requirements.txt requirements.txt               0.0s\n => CACHED [5/8] RUN .venv/bin/pip install -r requirements.txt        0.0s\n => CACHED [6/8] RUN apt update -y && apt install -y firefox-esr      0.0s\n => CACHED [7/8] RUN .venv/bin/pip install selenium                   0.0s\n => [8/8] COPY infra/debug-ci.py debug-ci.py                          0.0s\n => exporting to image                                                0.0s\n => => exporting layers                                               0.0s\n => => writing image sha256:[...]\n => => naming to docker.io/library/debug-ci                           0.0s\nTraceback (most recent call last):\n  File\n  \"//.venv/lib/python3.14/site-packages/selenium/webdriver/common/driver_finder.py\",\n  line 67, in _binary_paths\n    output = SeleniumManager().binary_paths(self._to_args())\n[...]\nselenium.common.exceptions.WebDriverException: Message: Unsupported\nplatform/architecture combination: linux/aarch64\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File \"//debug-ci.py\", line 4, in <module>\n    webdriver.Firefox().quit()\n    ~~~~~~~~~~~~~~~~~^^\n[...]\nselenium.common.exceptions.NoSuchDriverException: Message: Unable to obtain\ndriver for firefox; For documentation on this error, please visit:\nhttps://www.selenium.dev/documentation/webdriver/troubleshooting/errors/driver_location\n----\n\nYou might not see this--that \"Unsupported platform/architecture combination\" error is spurious;\nit's because I was on a Mac.  Let's try again with:\n\n// SEBASTIAN: Might use extra sentence of explanation why being on Mac requires you to\n// do a cross-build\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:specialcharacters,quotes[*docker build -f infra/Dockerfile.ci -t debug-ci --platform=linux/amd64 . && \\\n  docker run --platform=linux/amd64 -it debug-ci*]\n[...]\nTraceback (most recent call last):\n  File \"//debug-ci.py\", line 4, in <module>\n    webdriver.Firefox().quit()\n[...]\nselenium.common.exceptions.WebDriverException: Message: Process unexpectedly\nclosed with status 1\n----\n\nOK, that's a reproduction of our issue.  But no further clues yet!\n\n\n==== Enabling Debug Logs for Selenium/Firefox/Webdriver\n\nGetting debug information out of Selenium can be a bit fiddly.(((\"logging\", \"enabling debug logs for Firefox/Selenium/Webdriver\")))(((\"Webdriver\", \"enabling debug logs for\")))(((\"Firefox\", \"enabling debug logs for\")))(((\"Selenium\", \"enabling debug logs for\")))\nI tried two avenues: setting `options` and setting the `service`.\nThe former doesn't really work as far as I can tell,\nbut the latter does:\n\n[role=\"sourcecode\"]\n.infra/debug-ci.py (ch25l005)\n====\n[source,python]\n----\nimport subprocess\n\nfrom selenium import webdriver\n\noptions = webdriver.FirefoxOptions()  # <1>\noptions.log.level = \"trace\"\n\nservice = webdriver.FirefoxService(  # <2>\n    log_output=subprocess.STDOUT, service_args=[\"--log\", \"trace\"]\n)\n\n# just try to open a selenium session\nwebdriver.Firefox(options=options, service=service).quit()\n----\n====\n\n<1> This is how I attempted to increase the log level using `options`.\n    I had to reverse-engineer it from the source code,\n    and it doesn't seem to work anyway,\n    but I thought I'd leave it here for future reference. There is some limited info in the\nhttps://www.selenium.dev/documentation/webdriver/browsers/firefox/#log-output[Selenium docs].\n\n<2> This is the `FirefoxService` config class,\n    which _does_ seem to let you print some debug info.\n    I'm configuring it to print to standard output.\n\nSure enough, we can see some output now!\n\n[role=\"ignore-errors small-code\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:specialcharacters,quotes[*docker build -f infra/Dockerfile.ci -t debug-ci --platform=linux/amd64 . && \\\n  docker run --platform=linux/amd64 -it debug-ci*]\n[...]\n1234567890111   geckodriver     INFO    Listening on 127.0.0.1:XXXX\n1234567890112   webdriver::server       DEBUG   -> POST /session\n{\"capabilities\": {\"firstMatch\": [{}], \"alwaysMatch\": {\"browserName\": \"firefox\",\n\"acceptInsecureCerts\": true, ... , \"moz:firefoxOptions\": {\"binary\":\n\"/usr/bin/firefox\", \"prefs\": {\"remote.active-protocols\": 1}, \"log\": {\"level\":\n\"trace\"}}}}}\n1234567890111   geckodriver::capabilities       DEBUG   Trying to read firefox\nversion from ini files\n1234567890111   geckodriver::capabilities       DEBUG   Trying to read firefox\nversion from binary\n1234567890111   geckodriver::capabilities       DEBUG   Found version\n128.10.1esr\n1740029792102   mozrunner::runner       INFO    Running command:\nMOZ_CRASHREPORTER=\"1\" MOZ_CRASHREPORTER_NO_REPORT=\"1\"\nMOZ_CRASHREPORTER_SHUTDOWN=\"1\" [...]\n\"--remote-debugging-port\" [...]\n\"-no-remote\" \"-profile\" \"/tmp/rust_mozprofile[...]\n1234567890111   geckodriver::marionette DEBUG   Waiting 60s to connect to\nbrowser on 127.0.0.1\n1234567890111   geckodriver::browser    TRACE   Failed to open\n/tmp/rust_mozprofile[...]\n1234567890111   geckodriver::marionette TRACE   Retrying in 100ms\nError: no DISPLAY environment variable specified\n1234567890111   geckodriver::browser    DEBUG   Browser process stopped: exit\nstatus: 1\n1234567890112   webdriver::server       DEBUG   <- 500 Internal Server Error\n{\"value\":{\"error\":\"unknown error\",\"message\":\"Process unexpectedly closed with\nstatus 1\",\"stacktrace\":\"\"}}\nTraceback (most recent call last):\n  File \"//debug-ci.py\", line 13, in <module>\n    webdriver.Firefox(options=options, service=service).quit()\n[...]\nselenium.common.exceptions.WebDriverException: Message: Process unexpectedly\nclosed with status 1\n----\n\n// DAVID: Pasting this into an LLM gave some good suggestions.\n\nWell, it wasn't immediately obvious what's going on there,\nbut I did eventually get a clue from the line that says `no DISPLAY environment variable specified`.\n\nOut of curiosity, I thought I'd try running `firefox` directly:footnote:[\nIf you remember from <<chapter_09_docker>>, `docker run`\nby default runs the command specified in `CMD`,\nbut you can override that by specifying a different command to run at the end of the parameter list.]\n\n\n[role=\"ignore-errors\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *docker build -f infra/Dockerfile.ci -t debug-ci --platform=linux/amd64 . && \\\n  docker run --platform=linux/amd64 -it debug-ci firefox*\n[...]\nError: no DISPLAY environment variable specified\n----\n\nSure enough, the same error.\n\n\n==== Enabling Headless Mode for Firefox\n\nIf you search around for this error,\nyou'll eventually find enough pointers to the answer:\nFirefox is crashing because it can't find a display.(((\"headless mode\")))(((\"Firefox\", \"enabling headless mode for\")))\nServers are \"headless\", meaning they don't have a screen.\nThankfully, Firefox has a headless mode,\nwhich we can enable by setting an environment variable,\n`MOZ_HEADLESS`.\n\nLet's confirm that locally. We'll use the `-e` flag for `docker run`:\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:specialcharacters,quotes[*docker build -f infra/Dockerfile.ci -t debug-ci --platform=linux/amd64 . && \\\n  docker run -e MOZ_HEADLESS=1 --platform=linux/amd64 -it debug-ci*]\n1234567890111   geckodriver     INFO    Listening on 127.0.0.1:43137\n[...]\n*** You are running in headless mode.\n[...]\n1234567890112   webdriver::server       DEBUG   Teardown [...]\n1740030525996   Marionette      DEBUG   Closed connection 0\n1234567890111   geckodriver::browser    DEBUG   Browser process stopped: exit\nstatus: 0\n1234567890112   webdriver::server       DEBUG   <- 200 OK [...]\n----\n\nIt takes quite a long time to run,\nand there's lots of debug out, but...it looks OK!\nThat's no longer an error.\n\n\nLet's set that environment variable in our CI script:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml (ch25l006)\n====\n[source,yaml]\n----\nvariables:\n  # Put pip-cache in home folder so we can use gitlab cache\n  PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\n  # Make Firefox run headless.\n  MOZ_HEADLESS: \"1\"\n----\n====\n\nTIP: Using a local Docker image to reproduce the CI environment\n  is a hint that it might be worth investing time in running CI\n  in a custom Docker image that you fully control;\n  this is another way of improving _reproducibility_.\n  We won't have time to go into detail in this book though.\n\n\nAnd we'll see what happens when we do `git push gitlab` again.\n\n[role=\"pagebreak-before less_space\"]\n=== A Common Bugbear: Flaky Tests\n\nDid it work for you?  For me, it _almost_ did.(((\"continuous integration (CI)\", \"building the pipeline\", startref=\"ix_CIbld\")))(((\"flaky tests\")))\nAll but one of the FTs passed,\nbut I did see one unexpected error:\n\n\n[role=\"skipme small-code\"]\n----\n+ python manage.py test functional_tests\n......F.\n======================================================================\nFAIL: test_can_start_a_todo_list\n(functional_tests.test_simple_list_creation.NewVisitorTest)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/test_simple_list_creation.py\", line\n38, in test_can_start_a_todo_list\n    self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')\n  File \"...goat-book/functional_tests/base.py\", line 51, in\nwait_for_row_in_list_table\n    raise e\n  File \"...goat-book/functional_tests/base.py\", line 47, in\nwait_for_row_in_list_table\n    self.assertIn(row_text, [row.text for row in rows])\nAssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy\npeacock feathers']\n ---------------------------------------------------------------------\n----\n\n\nNow, you might not see this error,\nbut it's common for the switch to CI to flush out some \"flaky\" tests—things that will fail intermittently.\nIn CI, a common cause is the \"noisy neighbour\" problem,\nwhere the CI server might be much slower than your own machine,\nthus flushing out some race conditions—or in this case,\njust randomly hanging for a few seconds, taking us past the default timeout.\n\n\nLet's give ourselves some tools to help debug though.\n\n\n=== Taking Screenshots\n\n(((\"continuous integration (CI)\", \"screenshots\", id=\"CIscreen24\")))\n(((\"screenshots\", id=\"screen24\")))\n(((\"debugging\", \"screenshots for\", id=\"DBscreen24\")))\nTo be able to debug unexpected failures that happen on a remote server,\nit would be good to see a picture of the screen at the moment of the failure,\nand maybe also a dump of the page's HTML.\n\nWe can do that using some custom logic in our FT class `tearDown`.\nWe'll need to do a bit of introspection of `unittest` internals\n(a private attribute called `._outcome`)\nbut this will work:footnote:[...or at least until the next Python version.\nUsing private APIs is risky, but I couldn't find a better way.]\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch25l007)\n====\n[source,python]\n----\nimport os\nimport time\nfrom datetime import datetime\nfrom pathlib import Path\n[...]\nMAX_WAIT = 5\n\nSCREEN_DUMP_LOCATION = Path(__file__).absolute().parent / \"screendumps\"\n[...]\nclass FunctionalTest(StaticLiveServerTestCase):\n    def setUp(self):\n        [...]\n\n    def tearDown(self):\n        if self._test_has_failed():\n            if not SCREEN_DUMP_LOCATION.exists():\n                SCREEN_DUMP_LOCATION.mkdir(parents=True)\n            self.take_screenshot()\n            self.dump_html()\n        self.browser.quit()\n        super().tearDown()\n\n    def _test_has_failed(self):\n        # slightly obscure but couldn't find a better way!\n        return self._outcome.result.failures or self._outcome.result.errors\n----\n====\n\nWe first create a directory for our screenshots if necessary,\nand then we take our screenshot and dump the HTML.\nLet's see how those will work:\n\n[role=\"sourcecode\"]\n.src/functional_tests/base.py (ch25l008)\n====\n[source,python]\n----\n    def take_screenshot(self):\n        path = SCREEN_DUMP_LOCATION / self._get_filename(\"png\")\n        print(\"screenshotting to\", path)\n        self.browser.get_screenshot_as_file(str(path))\n\n    def dump_html(self):\n        path = SCREEN_DUMP_LOCATION / self._get_filename(\"html\")\n        print(\"dumping page HTML to\", path)\n        path.write_text(self.browser.page_source)\n----\n====\n\nAnd finally, here's a way of generating a unique filename identifier,\nwhich includes the name of the test and its class, as well as a timestamp:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/base.py (ch25l009)\n====\n[source,python]\n----\n    def _get_filename(self, extension):\n        timestamp = datetime.now().isoformat().replace(\":\", \".\")[:19]\n        return (\n            f\"{self.__class__.__name__}.{self._testMethodName}-{timestamp}.{extension}\"\n        )\n----\n====\n\nYou can test this first locally by deliberately breaking one of the tests—with a `self.fail()` half-way through, for example—and you'll see something like this:\n\n\n[role=\"dofirst-ch25l010\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *./src/manage.py test functional_tests.test_my_lists*\n[...]\nFscreenshotting to ...goat-book/src/functional_tests/screendumps/MyListsTest.te\nst_logged_in_users_lists_are_saved_as_my_lists-[...]\ndumping page HTML to ...goat-book/src/functional_tests/screendumps/MyListsTest.\ntest_logged_in_users_lists_are_saved_as_my_lists-[...]\nFscreenshotting to ...goat-book/src/functional_tests/screendumps/MyListsTest.te\nst_logged_in_users_lists_are_saved_as_my_lists-2025-02-18T11.29.00.png\ndumping page HTML to ...goat-book/src/functional_tests/screendumps/MyListsTest.\ntest_logged_in_users_lists_are_saved_as_my_lists-2025-02-18T11.29.00.html\n----\n\nWhy not try and open one of those files up?  It's kind of satisfying.\n\n\n=== Saving Build Outputs (or Debug Files) as Artifacts\n\nWe also need to tell GitLab to \"save\" these files,\nfor us to be able to actually look at them.(((\"GitLab\", \"saving build outputs as artifacts\")))(((\"artifacts\")))\nThose are called _artifacts_:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml (ch25l012)\n====\n[source,yaml]\n----\ntest:\n  [...]\n\n  script:\n    [...]\n\n  artifacts: # <1>\n    when: always  # <2>\n    paths: # <1>\n      - src/functional_tests/screendumps/\n----\n====\n\n<1> `artifacts` is the name of the key,\n    and the `paths` argument is fairly self-explanatory.\n    You can use wildcards here—more info in the https://docs.gitlab.com/ci/jobs/job_artifacts[GitLab docs].\n\n<2> One thing the docs _didn't_ make obvious is that you need `when: always`,\n    because otherwise it won't save artifacts for failed jobs.\n    That was annoyingly hard to figure out!\n\n\nIn any case, that should work.\nIf you commit the code and then push it back to GitLab,\nwe should be able to see a new build job:\n\n[role=\"dofirst-ch25l010-1\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *echo \"src/functional_tests/screendumps\" >> .gitignore*\n$ *git commit -am \"add screenshot on failure to FT runner\"*\n$ *git push*\n----\n\n[role=\"pagebreak-before\"]\nIn its output, we'll see the screenshots and HTML dumps being saved:\n\n\n[role=\"skipme small-code\"]\n----\nscreendumps/LoginTest.test_can_get_email_link_to_log_in-window0-2014-01-22T17.45.12.html\nFscreenshotting to /builds/hjwp/book-example/src/functional_tests/screendumps/\nNewVisitorTest.test_can_start_a_todo_list-2025-02-17T17.51.01.png\ndumping page HTML to /builds/hjwp/book-example/src/functional_tests/screendumps/\nNewVisitorTest.test_can_start_a_todo_list-2025-02-17T17.51.01.html\nNot Found: /favicon.ico\n.screenshotting to /builds/hjwp/book-example/src/functional_tests/screendumps/\nNewVisitorTest.test_multiple_users_can_start_lists_at_different_urls-2025-02-17T17.\n51.06.png\ndumping page HTML to /builds/hjwp/book-example/src/functional_tests/screendumps/\nNewVisitorTest.test_multiple_users_can_start_lists_at_different_urls-2025-02-17T17.51.\n06.html\n======================================================================\nFAIL: test_can_start_a_todo_list (functional_tests.test_simple_list_creation.NewVisitorTest.\ntest_can_start_a_todo_list)\n[...]\n----\n\n\nAnd to the right, some new UI options appear to Browse the artifacts,\nas in <<gitlab_ui_for_browse_artifacts>>.\n\n.Artifacts appear on the right of the build job\n[[gitlab_ui_for_browse_artifacts]]\nimage::images/tdd3_2504.png[\"GitLab UI tab showing the option to browse artifacts\"]\n\n[role=\"pagebreak-before\"]\nAnd if you navigate through, you'll see something like <<gitlab_ui_show_screenshot>>.\n\n.Our screenshot in the GitLab UI, looking unremarkable\n[[gitlab_ui_show_screenshot]]\nimage::images/tdd3_2505.png[\"GitLab UI showing a normal-looking screenshot of the site\"]\n\n// TODO: this errors if there are no screenshots.\n\n\n=== If in Doubt, Try Bumping the Timeout!\n\n(((\"\", startref=\"CIscreen24\")))\n(((\"\", startref=\"screen24\")))\n(((\"\", startref=\"DBscreen24\")))\n(((\"continuous integration (CI)\", \"timeout bumping\")))\n\nYour build might be clear, but mine was still failing,\nand those screenshots didn't offer any obvious clues.\nHmm. Well, when in doubt, bump the timeout—as the old adage goes:\n\n[role=\"sourcecode skipme\"]\n.src/functional_tests/base.py\n====\n[source,python]\n----\nMAX_WAIT = 10\n----\n====\n\nThen we can rerun the build by pushing, and confirm it now works.\n\n[role=\"pagebreak-before less_space\"]\n=== A Successful Python Test Run\n\nAt this point, we should get a working pipeline (see <<gitlab_pipeline_success>>).\n\n.A successful GitLab pipeline\n[[gitlab_pipeline_success]]\nimage::images/tdd3_2506.png[\"GitLab UI showing a successful pipeline run\"]\n\n\n\n=== Running Our JavaScript Tests in CI\n\n(((\"continuous integration (CI)\", \"setting up CI pipeline\", startref=\"ix_CIpipe1\")))(((\"continuous integration (CI)\", \"QUnit JavaScript tests\", id=\"CIjs5\")))\n(((\"JavaScript testing\", \"in CI\", secondary-sortas=\"CI\", id=\"JSCI\")))\nThere's a set of tests we almost forgot--the JavaScript tests.\nCurrently our \"test runner\" is an actual web browser.\nTo get them running in CI, we need a command-line test runner.\n\nNOTE: Our JavaScript tests currently test the interaction\n    between our code and the Bootstrap framework/CSS,\n    so we still need a real browser to be able to make our\n    visibility checks work.\n\n\nThankfully, the Jasmine docs point us straight towards the kind of tool we need:\nhttps://github.com/jasmine/jasmine-browser-runner[Jasmine browser runner].\n\n\n==== Installing Node.js\n\nIt's time to stop pretending we're not in the JavaScript game.\nWe're doing web development; that means we do JavaScript; that means we're going to end up with Node.js on our computers.(((\"Node.js\", \"installing\")))\nIt's just the way it has to be.\n\nFollow the instructions on the http://nodejs.org[Node.js home page].\nIt should guide you through installing the \"node version manager\" (nvm),\nand then to getting the latest version of node:\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *nvm install --lts*\nInstalling Node v22.17.0 (arm64)\n[...]\n$ *node -v*\nv22.17.0\n----\n\n\n==== Installing and Configuring the Jasmine Browser Runner\n\nThe docs suggest we install it (((\"Jasmine\", \"installing and configuring browser runner\", id=\"ix_Jasbrwsrun\")))(((\"browsers\", \"browser-based test runner (Jasmine)\", id=\"ix_brwststrun\")))like this,\nand then run the `init` command to generate a default config file:\n\n// IDEA: unskip. should be able to do some sort of rule=with-cd thingie\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cd src/lists/static*\n\n$ *npm install --save-dev jasmine-browser-runner jasmine-core*\n[...]\nadded 151 packages in 4s\n\n$ *cat package.json*  # this is the equivalent of requirements.txt\n{\n  \"devDependencies\": {\n    \"jasmine-browser-runner\": \"^3.0.0\",\n    \"jasmine-core\": \"^5.6.0\"\n  }\n}\n\n$ *ls node_modules/*\n# will show several dozen directories\n\n$ *npx jasmine-browser-runner init*\nWrote configuration to spec/support/jasmine-browser.mjs.\n----\n\nWell, we now have about a million files in _node_modules/_\n(which is JavaScript's version of a virtualenv, essentially),\nand we also have a new config file in _spec/support/jasmine-browser.mjs_. That's not the ideal place, because we've said our tests live in a folder called _tests_. So, let's move the config file in there:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *mv spec/support/jasmine-browser.mjs tests/jasmine-browser-runner.config.mjs*\n$ *rm -rf spec*\n----\n\n[role=\"pagebreak-before\"]\nThen let's edit it slightly, to specify a few things correctly:\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/jasmine-browser-runner.config.mjs (ch25l013)\n====\n[source,js]\n----\nexport default {\n  srcDir: \".\",  // <1>\n  srcFiles: [\n    \"*.js\"\n  ],\n  specDir: \"tests\",  // <2>\n  specFiles: [\n    \"**/*[sS]pec.js\"\n  ],\n  helpers: [\n    \"helpers/**/*.js\"\n  ],\n  env: {\n    stopSpecOnExpectationFailure: false,\n    stopOnSpecFailure: false,\n    random: true,\n    forbidDuplicateNames: true\n  },\n  listenAddress: \"localhost\",\n  hostname: \"localhost\",\n  browser: {\n    name: \"headlessFirefox\"  // <3>\n  }\n};\n----\n====\n// DAVID: srcFiles was \"**/*.js\", should it be changed too?\n\n<1> Our source files are in the current directory,\n    _src/lists/static_—i.e., _lists.js_.\n\n<2> Our spec files are in _tests/_.\n\n<3> And here we say we want to use the headless\n    version of Firefox.\n    (We could have done this by setting `MOZ_HEADLESS`\n    at the command line again, but this saves us from having to remember.)\n\n[role=\"pagebreak-before\"]\nLet's try running it now. We use the `--config` option to pass it\nto the now non-standard path to the config file:\n\n[role=\"skipme small-code\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *npx jasmine-browser-runner runSpecs \\\n  --config=tests/jasmine-browser-runner.config.mjs*\nJasmine server is running here: http://localhost:62811\nJasmine tests are here:         ...goat-book/src/lists/static/tests\nSource files are here:          ...goat-book/src/lists/static\nRunning tests in the browser...\nRandomized with seed 17843\nStarted\n.F.\n\nFailures:\n1) Superlists tests error message should be hidden on input\n  Message:\n    Expected true to be false.\n  Stack:\n    <Jasmine>\n    @http://localhost:62811/__spec__/Spec.js:46:40\n    <Jasmine>\n\n3 specs, 1 failure\nFinished in 0.014 seconds\nRandomized with seed 17843 (jasmine-browser-runner runSpecs --seed=17843)\n----\n\nCould be worse! One failure out of three specs. Unfortunately, it's the most important test:\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/static/tests/Spec.js\n====\n[source,python]\n----\n  it(\"should hide error message on input\", () => {\n    initialize(inputSelector);\n    textInput.dispatchEvent(new InputEvent(\"input\"));\n\n    expect(errorMsg.checkVisibility()).toBe(false);\n  });\n----\n====\n\nAh yes, if you remember, I said that the main reason we need to use a browser-based test runner\nis because our visibility checks depend on the Bootstrap CSS framework.\n\nIn the HTML spec runner we've configured so far,\nwe load Bootstrap using a `<link>` tag:\n\n[role=\"sourcecode currentcontents\"]\n.src/lists/static/tests/SpecRunner.html\n====\n[source,html]\n----\n  <!-- Bootstrap CSS -->\n  <link href=\"../bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n----\n====\n\n[role=\"pagebreak-before\"]\nAnd here's how we load it for `jasmine-browser-runner`:\n\n[role=\"sourcecode\"]\n.src/lists/static/tests/jasmine-browser-runner.config.mjs (ch25l014)\n====\n[source,js]\n----\nexport default {\n  srcDir: \".\",\n  srcFiles: [\n    \"*.js\"\n  ],\n  specDir: \"tests\",\n  specFiles: [\n    \"**/*[sS]pec.js\"\n  ],\n  cssFiles: [  // <1>\n    \"bootstrap/css/bootstrap.min.css\"  // <1>\n  ],\n  helpers: [\n    \"helpers/**/*.js\"\n  ],\n----\n====\n\n<1> The `cssFiles` key is how you tell the runner to load, er, some CSS.\n    I found that out in the https://jasmine.github.io/api/browser-runner/edge/Configuration.html[docs].\n\n\nLet's give that a go...\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *npx jasmine-browser-runner runSpecs \\\n  --config=tests/jasmine-browser-runner.config.mjs*\nJasmine server is running here: http://localhost:62901\nJasmine tests are here:         .../goat-book/src/lists/static/tests\nSource files are here:          .../goat-book/src/lists/static\nRunning tests in the browser...\nRandomized with seed 06504\nStarted\n...\n\n\n3 specs, 0 failures\nFinished in 0.016 seconds\nRandomized with seed 06504 (jasmine-browser-runner runSpecs --seed=06504)\n----\n\nHooray!  That works locally—let's get it into CI:\n\n\n[role=\"skipme\"]\n[subs=\"specialcharacters,quotes\"]\n----\n$ *cd -*  # go back to the project root\n# add the package.json, which saves our node depenencies\n$ *git add src/lists/static/package.json src/lists/static/package-lock.json*\n# ignore the node_modules/ directory\n$ *echo \"node_modules/\" >> .gitignore*\n# and our config file\n$ *git add src/lists/static/tests/jasmine-browser-runner.config.mjs*\n$ *git add .gitignore*\n$ *git commit -m \"config for node + jasmine-browser-runner for JS tests\"*\n----\n//015,016,017\n\n\n\n==== Adding a Build Step for JavaScript\n\n(((\"Jasmine\", \"installing and configuring browser runner\", startref=\"ix_Jasbrwsrun\")))(((\"browsers\", \"browser-based test runner (Jasmine)\", startref=\"ix_brwststrun\")))\nWe now want two different build steps,\nso let's rename `test` to `test-python` and move all its\nspecific bits like `variables` and `before_script` inside it,\nand then create a separate step called `test-js`,\nwith a similar structure:\n\n[role=\"sourcecode\"]\n..gitlab-ci.yml (ch25l018)\n====\n[source,yaml]\n----\ntest-python:\n  # Use the same image as our Dockerfile\n  image: python:slim  # <1>\n\n  variables:  # <1>\n    # Put pip-cache in home folder so we can use gitlab cache\n    PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\n    # Make Firefox run headless.\n    MOZ_HEADLESS: \"1\"\n\n  cache:  # <1>\n    paths:\n      - .cache/pip\n\n  # \"setUp\" phase, before the main build\n  before_script:  # <1>\n    - python --version ; pip --version  # For debugging\n    - pip install virtualenv\n    - virtualenv .venv\n    - source .venv/bin/activate\n\n  script:\n    - pip install -r requirements.txt\n    # unit tests\n    - python src/manage.py test lists accounts\n    # (if those pass) all tests, incl. functional.\n    - apt update -y && apt install -y firefox-esr\n    - pip install selenium\n    - cd src && python manage.py test\n\n  artifacts:\n    when: always\n    paths:\n      - src/functional_tests/screendumps/\n\ntest-js:  # <2>\n  image: node:slim\n  script:\n    - apt update -y && apt install -y firefox-esr  # <3>\n    - cd src/lists/static\n    - npm install  # <4>\n    - npx jasmine-browser-runner runSpecs\n      --config=tests/jasmine-browser-runner.config.mjs  # <5>\n----\n====\n\n<1> `image`, `variables`, `cache`, and `before_script` all move\n    out of the top level and into the `test-python` step,\n    as they're all specific to this step only now.\n\n<2> Here's our new step, `test-js`.\n\n<3> We install Firefox into the node image,\n    just like we do for the Python one.\n\n<4> We don't need to specify _what_ to `npm install`,\n    because that's all in the _package-lock.json_ file.\n\n<5> And here's our command to run the tests.\n\n\nAnd slap me over the head with a wet fish if that doesn't pass on the first go!\nSee <<gitlab_pipeline_js_success>> for a successful pipeline run.\n\n\n.Wow, there are those JavaScript tests, passing on the first attempt!\n[[gitlab_pipeline_js_success]]\nimage::images/tdd3_2507.png[\"GitLab UI showing a successful pipeline run with JavaScript tests\"]\n\n(((\"\", startref=\"CIjs5\")))\n(((\"\", startref=\"JSCI\")))\n\n\n[role=\"pagebreak-before less_space\"]\n=== Tests Now Pass\n\nAnd there we are!  A complete CI build featuring all of our tests! See <<gitlab_pipeline_overview_success.png>>.\n\n.Here are both our jobs in all their green glory\n[[gitlab_pipeline_overview_success.png]]\nimage::images/tdd3_2508.png[\"GitLab UI the pipeline overview, with both build jobs green\"]\n\n\nNice to know that, no matter how lazy I get\nabout running the full test suite on my own machine, the CI server will catch me.\nAnother one of the Testing Goat's agents in cyberspace, watching over us...\n\n\n.Alternatives: Woodpecker and Forgejo\n*******************************************************************************\n\nI want to give a shout out to https://woodpecker-ci.org[Woodpecker CI]\nand https://forgejo.org[Forgejo], two of the newer self-hosted CI options.\nAnd while I'm at it, to https://www.jenkins.io[Jenkins],\nwhich did a great job for the first and second editions,\nand still does for many people.(((\"continuous integration (CI)\", \"self-hosted CI options\")))\n\n// CSANAD: I just found framagit.org by Framasoft. Maybe we could mention them? Although\n// it might be important to ask them first, in case they need to handle the\n// expected additional traffic.\n\nIf you want true independence from overly commercial interests,\nthen self-hosted is the way to go.\nYou'll need your own server for both of these.\n\nI tried both, and managed to get them working within an hour or two.\nTheir documentation is good.\n\nIf you do decide to give them a go, I'd say,\nbe a bit cautious about security options. For example, you might decide you don't want any old person from the internet\nto be able to sign up for an account on your server:\n\n\n[role=\"skipme\"]\n----\nDISABLE_REGISTRATION: true\n----\n\nBut more power to you for giving it a go!\n\n*******************************************************************************\n\n\n=== Some Things We Didn't Cover\n\nCI is a big topic and, inevitably, I couldn't cover everything.\nHere's a few pointers to things you might want to learn about.\n\n==== Defining a Docker Image for CI\n\nWe spent quite a bit of time debugging—for example, the unhelpful messages\nwhen Firefox wasn't installed.(((\"continuous integration (CI)\", \"defining Docker image for\")))(((\"Docker\", \"defining container image for CI\")))\nJust as we did when preparing our deployment, it's a big help having an environment that you can run on your local machine\nthat's as close as possible to what you have remotely; that's why we chose to use a Docker image.\n\nIn CI, our tests also run a Docker image (`python:slim` and `node:slim`),\nso one common pattern is to define a Docker image within your repo that you'll use for CI.\nIdeally, it should also be as similar as possible to the one you use in production!\nA typical solution here is to use multistage Docker builds—with a base stage, a prod stage, and a dev/CI stage.\nIn our case, the last stage would have Firefox, Selenium,\nand other test-only dependencies in it, which we don't need for prod.\n\nYou can then run your tests locally inside the same Docker image that's used in CI.(((\"reproducibility\")))\n\n\nTIP: _Reproducibility_ is one of the key attributes we're aiming for.\n    The more your project grows in complexity,\n    the more it's worth investing in minimising the differences\n    between local dev, CI, and prod.\n\n\n==== Caching\n\nWe touched on the use of caches in CI for the `pip` download cache,\nbut as CI pipelines grow in maturity,\nyou'll find you can make more and more use of caching. (((\"caching in CI pipelines\")))For example, it might be a good idea to cache your _node_modules/_\ndirectory.\n\nIt's a topic for another time, but this is yet another way\nof trying to speed up the feedback cycle.\n\n\n==== Automated Deployment, aka Continuous Delivery (CD)\n\nThe natural next step is to finish our journey into automation,\nand set up a pipeline that will deploy our code all the way to production,\neach time we push code...as long as the tests pass!(((\"continuous delivery (CD)\")))(((\"automated deployment\")))(((\"deployment\", \"continuous delivery\")))\n\nI work through an example of how to do that in the https://www.obeythetestinggoat.com/book/appendix_CD.html[Online Appendix: Continuous Deployment (CD)]. If you're feeling inspired, I'd encourage you to take a look.\n\nNow, onto our last chapter of coding, everyone!\n\n\n.Best Practices for CI (Including Selenium Tips)\n*******************************************************************************\n\nSet up CI as soon as possible for your project.::\n    As soon as your functional tests take more than a few seconds to run,\n    you'll find yourself avoiding running them.\n    Give this job to a CI server,\n    to make sure that all your tests are being run somewhere.\n    (((\"Selenium\", \"best CI practices\")))\n    (((\"continuous integration (CI)\", \"tips\")))\n\nOptimise for fast feedback.::\n    CI feedback loops can be frustratingly slow.\n    Optimising things to get results quicker is worth the effort.\n    Run your fastest tests first,\n    and use caches to try to minimise time spent on, for example, dependency installation.\n\nSet up screenshots and HTML dumps for failures.::\n    Debugging test failures is easier if you can see what the page looked\n    like when the failure occurred.  This is particularly useful for debugging\n    CI failures, but it's also very useful for tests that you run locally.\n    (((\"screenshots\")))\n    (((\"debugging\", \"screenshots for\")))\n    (((\"HTML\", \"screenshot dumps\")))\n\nBe prepared to bump your timeouts.::\n    A CI server may not be as speedy as your laptop—especially if it's under load, running multiple tests at the same time.\n    Be prepared to be even more generous with your timeouts,\n    in order to minimise the chance of random failures.\n    (((\"flaky tests\")))\n\nTake the next step, CD (continuous deployment).::\n    Once we're running tests automatically,\n    we can take the next step, which is to automate our deployments\n    (when the tests pass). See the https://www.obeythetestinggoat.com/book/appendix_CD.html[Online Appendix: Continuous Deployment (CD)].\n    (((\"continuous deployment (CD)\")))\n\n*******************************************************************************\n\n"
  },
  {
    "path": "chapter_26_page_pattern.asciidoc",
    "content": "[[chapter_26_page_pattern]]\n== The Token Social Bit, the Page Pattern, [.keep-together]#and an Exercise for the Reader#\n\n////\nDAVID\n\n\nThe format of this chapter works really well!\n\nI wonder if there is a way of introducing some of this earlier in the book in\ntwo or three places, maybe in smaller ways. They could commit beforehand and\nthen try to solve certain problems on their own, then undoing their work\nafterwards and replacing it with how you did it.\n////\n\n\n(((\"functional tests (FTs)\", \"with multiple users\", secondary-sortas=\"multiple users\", id=\"FTmultiple25\")))\n(((\"functional tests (FTs)\", \"structuring test code\", id=\"FTstructure25\")))\nAre jokes about how \"everything has to be social now\" slightly old hat?\nYes, Harry; they were old hat 10 years ago when you started writing this book,\nand they're positively prehistoric now.\n_Irregardless_, let's say lists are often better shared.\nWe should allow our users to collaborate on their lists with other users.\n\nAlong the way, we'll improve our FTs\nby starting to implement something called the \"page object pattern\". Then, rather than showing you explicitly what to do,\nI'm going to let you write your unit tests and application code by yourself.\nDon't worry; you won't be totally on your own!\nI'll give an outline of the steps to take, as well as some hints and tips.\n\nBut still--if you haven't already,\nthis is the chapter where you get a chance to spread your wings.\nEnjoy!\n\n[role=\"pagebreak-before less_space\"]\n=== An FT with Multiple Users, and addCleanup\n\n(((\"Page pattern\", \"FT with multiple user\")))\nLet's get started--we'll need two users for this FT:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/test_sharing.py (ch26l001)\n====\n[source,python]\n----\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\n\nfrom .base import FunctionalTest\n\n\ndef quit_if_possible(browser):\n    try:\n        browser.quit()\n    except:\n        pass\n\n\nclass SharingTest(FunctionalTest):\n    def test_can_share_a_list_with_another_user(self):\n        # Edith is a logged-in user\n        self.create_pre_authenticated_session(\"edith@example.com\")\n        edith_browser = self.browser\n        self.addCleanup(lambda: quit_if_possible(edith_browser))\n\n        # Her friend Onesiphorus is also hanging out on the lists site\n        oni_browser = webdriver.Firefox()\n        self.addCleanup(lambda: quit_if_possible(oni_browser))\n        self.browser = oni_browser\n        self.create_pre_authenticated_session(\"onesiphorus@example.com\")\n\n        # Edith goes to the home page and starts a list\n        self.browser = edith_browser\n        self.browser.get(self.live_server_url)\n        self.add_list_item(\"Get help\")\n\n        # She notices a \"Share this list\" option\n        share_box = self.browser.find_element(By.CSS_SELECTOR, 'input[name=\"sharee\"]')\n        self.assertEqual(\n            share_box.get_attribute(\"placeholder\"),\n            \"your-friend@example.com\",\n        )\n----\n====\n\n\nThe interesting feature to note about this section is the `addCleanup` function,\nwhose documentation you can find\nhttps://docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup[online].\nIt can be used as an alternative to the `tearDown` function\nas a way of cleaning up resources used during the test.\nIt's most useful when the resource is only allocated halfway through a test,\nso you don't have to spend time in `tearDown`\nwith a bunch of conditional logic designed to clean up resources\nthat may or may not have been used by the point the test failed.\n\n`addCleanup` is run after `tearDown`,\nwhich is why we need that `try/except` formulation for `quit_if_possible`.\nBy the time the test ends, the browser assigned to `self.browser`—whether it was `edith_browser` or `oni_browser`—will already have been quit by `tearDown()`.\n\nWe'll also need to move `create_pre_authenticated_session`\nfrom _test_my_lists.py_ into _base.py_, so we can use it in more than one test.\n\nOK, let's see if that all works:\n\n[role=\"dofirst-ch26l002\"]\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_sharing*]\n[...]\nTraceback (most recent call last):\n  File \"...goat-book/src/functional_tests/test_sharing.py\", line 33, in\ntest_can_share_a_list_with_another_user\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: input[name=\"sharee\"]; [...]\n----\n\nGreat! It seems to have made it through creating the two user sessions, and\nit gets onto an expected failure--there is no input for an email address\nof a person to share a list with on the page.\n\nLet's do a commit at this point, because we've got at least a placeholder\nfor our FT, we've got a useful modification of the\n`create_pre_authenticated_session` function, and we're about to embark on\na bit of an FT refactor:\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/functional_tests*\n$ *git commit -m \"New FT for sharing, move session creation stuff to base\"*\n----\n\n\n\n=== The Page Pattern\n\n(((\"Page pattern\", \"reducing duplication with\", id=\"POPduplic25\")))\n(((\"duplication, eliminating\", id=\"dup25\")))\nBefore we go any further,\nI want to show an alternative method for reducing duplication in your FTs,\ncalled https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models[\"page objects\"].\n\nWe've already built several helper methods for our FTs—including `add_list_item`, which we've used here—but if we just keep adding more and more, it's going to get very crowded.\nI've worked on a base FT class that was over 1,500 lines long,\nand that got pretty unwieldy.\n\n[role=\"pagebreak-before\"]\nPage objects are an alternative that encourage us\nto store all the information and helper methods\nabout the different types of pages on our site\nin a single place.\nLet's see how that might look for our site,\nstarting with a class to represent any lists page:\n\n[role=\"sourcecode small-code\"]\n.src/functional_tests/list_page.py\n====\n[source,python]\n----\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\nfrom .base import wait\n\n\nclass ListPage:\n    def __init__(self, test):\n        self.test = test  # <1>\n\n    def get_table_rows(self):  # <3>\n        return self.test.browser.find_elements(By.CSS_SELECTOR, \"#id_list_table tr\")\n\n    @wait\n    def wait_for_row_in_list_table(self, item_text, item_number):  # <2>\n        expected_row_text = f\"{item_number}: {item_text}\"\n        rows = self.get_table_rows()\n        self.test.assertIn(expected_row_text, [row.text for row in rows])\n\n    def get_item_input_box(self):  # <2>\n        return self.test.browser.find_element(By.ID, \"id_text\")\n\n    def add_list_item(self, item_text):  # <2>\n        new_item_no = len(self.get_table_rows()) + 1\n        self.get_item_input_box().send_keys(item_text)\n        self.get_item_input_box().send_keys(Keys.ENTER)\n        self.wait_for_row_in_list_table(item_text, new_item_no)\n        return self  # <4>\n----\n====\n//003\n\n<1> It's initialised with an object that represents the current test.\n    That gives us the ability to make assertions,\n    access the browser instance via `self.test.browser`,\n    and use the `self.test.wait_for` function.\n\n<2> I've copied across some of the existing helper methods from _base.py_,\n    but I've tweaked them slightly...\n\n<3> For example, this new method is used\n    in the new versions of the old helper methods.\n\n<4> Returning `self` is just a convenience. It enables\n    https://oreil.ly/I1Sr7[method chaining],\n    which we'll see in action immediately.\n\n[role=\"pagebreak-before\"]\nLet's see how to use it in our test:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_sharing.py (ch26l004)\n====\n[source,python]\n----\nfrom .list_page import ListPage\n[...]\n\n        # Edith goes to the home page and starts a list\n        self.browser = edith_browser\n        self.browser.get(self.live_server_url)\n        list_page = ListPage(self).add_list_item(\"Get help\")\n----\n====\n\nLet's continue rewriting our test, using the page object whenever\nwe want to access elements from the lists page:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_sharing.py (ch26l008)\n====\n[source,python]\n----\n        # She notices a \"Share this list\" option\n        share_box = list_page.get_share_box()\n        self.assertEqual(\n            share_box.get_attribute(\"placeholder\"),\n            \"your-friend@example.com\",\n        )\n\n        # She shares her list.\n        # The page updates to say that it's shared with Onesiphorus:\n        list_page.share_list_with(\"onesiphorus@example.com\")\n----\n====\n\nWe add the following three functions to our `ListPage`:\n\n\n[role=\"sourcecode\"]\n.src/functional_tests/list_page.py (ch26l009)\n====\n[source,python]\n----\n    def get_share_box(self):\n        return self.test.browser.find_element(\n            By.CSS_SELECTOR,\n            'input[name=\"sharee\"]',\n        )\n\n    def get_shared_with_list(self):\n        return self.test.browser.find_elements(\n            By.CSS_SELECTOR,\n            \".list-sharee\",\n        )\n\n    def share_list_with(self, email):\n        self.get_share_box().send_keys(email)\n        self.get_share_box().send_keys(Keys.ENTER)\n        self.test.wait_for(\n            lambda: self.test.assertIn(\n                email, [item.text for item in self.get_shared_with_list()]\n            )\n        )\n----\n====\n\nThe idea behind the page pattern is that it should capture all the information\nabout a particular page in your site. That way, if you later want to go and\nmake changes to that page--even just simple tweaks to its HTML layout--you'll have a single place to adjust your functional\ntests, rather than having to dig through dozens of FTs.\n\nThe next step would be to pursue the FT refactor through our other tests.\nI'm not going to show that here, but it's something you could do for practice,\nto get a feel for what the trade-offs are like between \"don't repeat yourself\" (DRY) and test readability...\n(((\"\", startref=\"POPduplic25\")))\n(((\"\", startref=\"dup25\")))\n\n\n\n\n\n=== Extend the FT to a Second User, and the \"My Lists\" Page\n\n\n(((\"Page pattern\", \"adding a second Page object\")))\nLet's spec out just a little more detail\nof what we want our sharing user story to be.\nEdith has seen on her list page that the list is now \"shared with\" Onesiphorus,\nand then we can have Onesiphorus log in and see the list on his \"My lists\" page—maybe in a section called \"lists shared with me\":\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_sharing.py (ch26l010)\n====\n[source,python]\n----\nfrom .my_lists_page import MyListsPage\n[...]\n\n        list_page.share_list_with(\"onesiphorus@example.com\")\n\n        # Onesiphorus now goes to the lists page with his browser\n        self.browser = oni_browser\n        MyListsPage(self).go_to_my_lists_page(\"onesiphorus@example.com\")\n\n        # He sees Edith's list in there!\n        self.browser.find_element(By.LINK_TEXT, \"Get help\").click()\n----\n====\n\n[role=\"pagebreak-before\"]\nThat means another function in our `MyListsPage` class:\n\n[role=\"sourcecode\"]\n.src/functional_tests/my_lists_page.py (ch26l011)\n====\n[source,python]\n----\nfrom selenium.webdriver.common.by import By\n\n\nclass MyListsPage:\n    def __init__(self, test):\n        self.test = test\n\n    def go_to_my_lists_page(self, email):\n        self.test.browser.get(self.test.live_server_url)\n        self.test.browser.find_element(By.LINK_TEXT, \"My lists\").click()\n        self.test.wait_for(\n            lambda: self.test.assertIn(\n                email,\n                self.test.browser.find_element(By.TAG_NAME, \"h1\").text,\n            )\n        )\n        return self\n----\n====\n\nOnce again, this is a function that would be good to carry across\ninto _test_my_lists.py_, along with maybe a `MyListsPage` object.\n\nIn the meantime, Onesiphorus can also add things to the list:\n\n[role=\"sourcecode\"]\n.src/functional_tests/test_sharing.py (ch26l012)\n====\n[source,python]\n----\n    # On the list page, Onesiphorus can see says that it's Edith's list\n    self.wait_for(\n        lambda: self.assertEqual(list_page.get_list_owner(), \"edith@example.com\")\n    )\n\n    # He adds an item to the list\n    list_page.add_list_item(\"Hi Edith!\")\n\n    # When Edith refreshes the page, she sees Onesiphorus's addition\n    self.browser = edith_browser\n    self.browser.refresh()\n    list_page.wait_for_row_in_list_table(\"Hi Edith!\", 2)\n----\n====\n\n\nThat's another addition to our `ListPage` object:\n\n[role=\"sourcecode\"]\n.src/functional_tests/list_page.py (ch26l013)\n====\n[source,python]\n----\nclass ListPage:\n    [...]\n\n    def get_list_owner(self):\n        return self.test.browser.find_element(By.ID, \"id_list_owner\").text\n----\n====\n\n[role=\"pagebreak-before\"]\nIt's long past time to run the FT and check if all of this works!\n\n[subs=\"specialcharacters,macros\"]\n----\n$ pass:quotes[*python src/manage.py test functional_tests.test_sharing*]\n[...]\n  File \"...goat-book/src/functional_tests/test_sharing.py\", line 35, in\ntest_can_share_a_list_with_another_user\n    share_box = list_page.get_share_box()\n    [...]\n    return self.test.browser.find_element(\n           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^\n        By.CSS_SELECTOR,\n        ^^^^^^^^^^^^^^^^\n        'input[name=\"sharee\"]',\n        ^^^^^^^^^^^^^^^^^^^^^^^\n    [...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: input[name=\"sharee\"]; [...]\n----\n\nThat's the expected failure;\nwe don't have an input for email addresses of people to share with.\nLet's do a commit:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n$ *git add src/functional_tests*\n$ *git commit -m \"Create Page objects for list pages, use in sharing FT\"*\n----\n\n\n\n=== An Exercise for the Reader\n\n[quote, Iain H. (reader)]\n______________________________________________________________\nI probably didn’t _really_ understand what I was doing\nuntil after having completed the \"exercise for the reader\"\nin the page pattern chapter.\n______________________________________________________________\n\n(((\"Page pattern\", \"practical exercise\")))\nThere's nothing that cements learning like taking the training wheels off,\nand getting something working on your own, so I hope you'll give this a go.\n\nBy this point in the book, you should have all the elements you need\nto test-drive this new feature, from the outside in.\nThe FT is there to guide you, and this feature should take you down\ninto both the views and the models layers.\nSo, give it a go!\n\n\n==== Step-by-step Guide\n\nIf you'd like a bit more help, here's an outline of the steps you could take:\n\n1. You'll need a new section in _list.html_,\n  initially with just a form containing an input box for an email address.\n  That should get the FT one step further.\n\n2. Next, you'll need a view for the form to submit to.\n  Start by defining the URL in the template—maybe something like 'lists/<list_id>/share'.\n\n3. Then, you'll have your first unit test.\n  It can be just enough to get a placeholder view in.\n  You want the view to respond to POST requests\n  and respond with a redirect back to the list page.\n  The test could be called something like\n  `ShareListTest.test_post_redirects_to_lists_page`.\n\n4. You build out your placeholder view, as just a two-liner\n  that finds a list and redirects to it.\n\n5. You can then write a new unit test that creates a user and a list,\n  does a POST with their email address,\n  and checks that the user is added to `mylist.shared_with.all()`\n  (a similar ORM usage to \"My lists\").\n  That `shared_with` attribute won't exist yet; you're going outside-in.\n\n6. So, before you can get this test to pass, you'll have to move down to the model layer.\n  The next test, in _test_models.py_, can check that a list has a `shared_with.add()` method that works with a user’s email address, and that `shared_with.all()` subsequently includes that user.\n\n7. You'll then need a `ManyToManyField`.\n  You'll probably see an error message about a clashing `related_name`,\n  which you'll find a solution for if you look around the Django docs.\n\n8. It will need a database migration.\n\n9. That should get the model tests passing. Pop back up to fix the view test.\n\n10. You may find that the redirect view test fails,\n  because it's not sending a valid POST request.\n  You can either choose to ignore invalid inputs,\n  or adjust the test to send a valid POST.\n\n11. Then, head back up to the template level; on the \"My lists\" page, you'll want a `<ul>`\n  with a +for+ loop of the lists shared with the user.\n  On the lists page, you also want to show who the list is shared with,\n  and mention who the list owner is.\n  Look back at the FT for the correct classes and IDs to use.\n  You could have brief unit tests for each of these if you like, as well.\n\n12. You might find that spinning up the site with `runserver`\n  helps you iron out any bugs and fine-tune the layout and aesthetics.\n  If you use a private browser session, you'll be able to log multiple users in.\n\n[role=\"pagebreak-before\"]\nBy the end, you might end up with something that looks like\n<<list-sharing-example>>.\n\n\n[[list-sharing-example]]\n.Sharing lists\nimage::images/tdd3_2601.png[\"Screenshot of list sharing UI\"]\n\n[role=\"pagebreak-before less_space\"]\n.The Page Pattern, and the Real Exercise for the Reader\n*******************************************************************************\n\nApplying DRY to your functional tests::\n    Once your FT suite starts to grow,\n    you'll find different tests using similar parts of the UI.\n    Try to avoid having constants—like the HTML IDs or classes of particular UI elements—duplicated across your FTs.\n    (((\"Don’t Repeat Yourself (DRY)\")))\n\n\nThe page pattern::\n    Moving helper methods into a base `FunctionalTest` class can become unwieldy.\n    Consider using individual page objects to hold all the logic\n    for dealing with particular parts of your site.\n    (((\"Page pattern\", \"benefits of\")))\n\n\nAn exercise for the reader::\n    I hope you've actually tried this out!\n    Try to follow the outside-in method,\n    and occasionally try things out manually if you get stuck.\n    The real exercise for the reader, of course,\n    is to apply TDD to your next project.\n    I hope you'll enjoy it!\n    (((\"\", startref=\"FTmultiple25\")))\n    (((\"\", startref=\"FTstructure25\")))\n\n*******************************************************************************\n\nIn the next chapter, we'll wrap up with a discussion of the trade-offs in testing,\nand some of the considerations involved in choosing which kinds of tests to use, and when.\n\n"
  },
  {
    "path": "chapter_27_hot_lava.asciidoc",
    "content": "[[chapter_27_hot_lava]]\n== Fast Tests, Slow Tests, and Hot Lava\n\n[quote, Casey Kinsey]\n______________________________________________________________\nThe database is hot lava!\n______________________________________________________________\n\nWe've come to the end of the book,\nand the end of our journey with this to-do app and its tests.\nLet's recap our test structure so far:\n\n* We have a suite of functional tests that use Selenium to test that the whole app really works.\n  On several occasions, the FTs have saved us from shipping broken code--whether it was broken CSS,\n  a broken database due to filesystem permissions, or broken email integration.\n\n* And we have a suite of unit tests that use Django test client, enabling us to test-drive our code for models, forms, views, URLs, and even (to some extent) templates.\n  They've enabled us to build the app incrementally, to refactor with confidence,\n  and they've supported a fast unit-test/code cycle.\n\n* We've also spent a good bit of time on our infrastructure,\npackaging up our app with Docker for ease of deployment,\nand we've set up a CI pipeline to run our tests automatically on push.\n\nHowever, being a simple app that could fit in a book,\nthere are inevitably some limitations and simplifications in our approach.\nIn this chapter, I'd like to talk about how to carry your testing principles forward,\nas you move into larger, more complex applications in the real world.\n\nLet's find out why someone might say that the database is hot lava!(((\"tests\", \"desiderata for effective tests\")))\n\n\n=== Why Do We Test? Our Desiderata for Effective Tests\n\nAt https://testdesiderata.com[testdesiderata.com], Kent Beck and Kelly Sutton\noutline several desiderata (desirable characteristics) for tests, as outlined in <<test_desiderata>>:\n\n[[test_desiderata]]\n.Test desiderata\n|===\n| *Isolated*: Tests should return the same results regardless of the order in which they are run.\n| *Composable*: We should be able to test different dimensions of variability separately and combine the results.\n| *Deterministic*: If nothing changes, the test results shouldn’t change.\n| *Fast*: Tests should run quickly.\n| *Writable*: Tests should be cheap to write, relative to the cost of the code being tested.\n| *Readable*: Tests should be comprehensible for readers, invoking the motivation for writing the particular test.\n| *Behavioural*: Tests should be sensitive to changes in the behaviour of the code under test. If the behaviour changes, the test result should change.\n| *Structure-agnostic*: Tests should not change their results if the structure of the code changes.\n| *Automated*: Tests should run without human intervention.\n| *Specific*: If a test fails, the cause of the failure should be obvious.\n| *Predictive*: If the tests all pass, then the code under test should be suitable for production.\n| *Inspiring*: Passing the tests should inspire confidence.\n|===\n\nWe've talked about almost all of these desiderata in the book:\nwe talked about _isolation_ when we switched to using the Django test runner.\nWe talked about _composability_ when discussing the car factory example in <<chapter_21_mocking_2>>.\nWe talked about tests being _readable_ when we talked about the given-when-then structure\nand when implementing helper methods in our FTs.\nWe talked about testing _behaviour_ rather than implementation at several points,\nincluding in the mocking chapters.\nWe talked about _structure_ in the forms chapters,\nwhen we showed that the higher-level views tests enabled us to refactor more freely than the lower-level forms tests.\nWe've talked about splitting up our tests to have fewer assertions to make them more _specific_.\nWe talked about _determinism_ when discussing flaky tests and the use of `wait_for()` in our FTs, for example, as well as in the production debugging chapter.\n\nAnd in this chapter, we're going to talk primarily about _speed_, and about what makes tests _inspiring_.\n\nBut first, it's worth taking a step back from the list in <<test_desiderata>>, and asking:\n\"What do we want from our tests?\"\n\n[role=\"pagebreak-before less_space\"]\n==== Confidence and Correctness (Preventing Regression)\n\nA fundamental part of programming is that, now and again,\nyou need to check whether \"it works\".(((\"regression\", \"preventing\")))\nAutomated testing is the solution to the problem that checking things manually can\nquickly become tedious and be unreliable.\nWe want our tests to tell us that our code works—both at the low level of individual functions or classes,\nand at the higher level of \"does it all hang together?\"\n\n==== A Productive Workflow\n\nOur tests need to be fast enough to write,\nbut more importantly, fast to run.\nWe want to get into a smooth, productive workflow,\nand try to enter that holy credo of programmers—the \"flow state\".\nBeyond that, we want our tests to take some of the stress out of programming,\nencouraging us to work in small increments,\nwith frequent bursts of dopamine from seeing green tests.\n\n==== Driving Better Design\n\nAnd our tests should help us to write _better_ code:\nfirst, by enabling fearless refactoring and, second, by giving us feedback on the design of our code.(((\"code design, better, tests driving\")))\nWriting the tests first lets us think about our API from the outside in,\nbefore we write it--and we've seen that.\nBut in this chapter, we'll also talk about the potential for\ntests to give you feedback on your design in more subtle ways.\nAs we'll see, designing code to be more testable\noften leads to code that has clearly identified dependencies,\nand is more modular and more decoupled.\n\nAs we continuously think about what kinds of tests to write,\nwe are trying to achieve the optimum balance of these different desiderata.\n\n\n\n=== Were Our Unit Tests Integration Tests All Along? [.keep-together]#What Is That Warm# Glow Coming from the Database?\n\n(((\"integration tests\", \"versus unit tests\", secondary-sortas=\"unit\")))\n(((\"unit tests\", \"versus integration tests\", secondary-sortas=\"integration\")))\nAlmost all of the \"unit\" tests in the book\nperhaps should have been called _integration_ tests,\nbecause they all rely on Django's test runner,\nwhich gives us a real database to talk to.\nMany also use the Django test client,\nwhich does a lot of magic with the middleware layers that sit between requests.\nThe end result is that our tests are heavily integrated with both the database\nand Django itself.\n\n[role=\"pagebreak-before less_space\"]\n==== We've Been in the \"Sweet Spot\"\n\nNow, actually, this has been a pretty good thing for us so far.\nWe're very much in the \"sweet spot\" of Django's testing tools.(((\"Django framework\", \"sweet spot of Django&#x27;s testing tools\")))\nOur unit tests have been fast enough to enable a smooth workflow,\nand they've given us a strong reassurance that our application really works—from the models all the way up to the templates.\nBy allying them with a small-ish suite of functional tests,\nwe've got a lot of confidence in our code.\nAnd we've been able to use them to get at least a bit of feedback on our design,\nand to enable lots of refactoring.\n\n\n==== What Is a \"True\" Unit Test?  Does it Matter?\n\nBut people will often tell you that a \"true\" unit test should be more isolated.\nIt's meant to test a single \"unit\" of software,\nand your database \"should\" be outside of that.(((\"unit tests\", \"true unit tests\")))\nWhy do they say that (other than for the smugness they get from should-ing us)?\n\nAs you can tell,\nI think the argument from _definitions_ is a bit of a red herring.\nBut you might hear instead, \"the database is hot lava!\"—as Casey Kinsey put it in a memorable DjangoCon talk.\nThere is real feeling and real experience behind these comments.\nWhat are people getting at?\n\n\n==== Integration and Functional Tests Get Slower Over Time\n\nThe problem is that, as your application and codebase grow,\ninvolving the database in every single test starts to carry an unacceptable cost—in terms of execution speed.(((\"database testing\", \"functional and integration tests getting slower\")))(((\"integration tests\", \"involving the database, getting slower\")))(((\"functional  tests (FTs)\", \"involving the database, getting slower\"))) Casey's company, for example, was struggling with test suites that took several hours.\n\nAt PythonAnywhere, our functional test suite didn't just rely on the database;\nit would spin up a full test cluster of six virtual machines.\nA full run used to take at least 12 hours,\nand we'd have to wait overnight for our results.\nThat was one of the least productive parts of an otherwise extraordinary workflow.\n\nAt Kraken, the full test suite does only take about 45 minutes,\nwhich is not bad for nearly 10 million lines of code,\nbut that's only thanks to a quite frankly ridiculous level of parallelisation\nand associated expenditure on CI.\nWe're now spending a lot of effort on trying to move more of our unit\ntests to being \"true\" unit tests.\n\nThe problem is that these things don't scale linearly.\nThe more database tables you have,\nthe more relationships between them,\nand that starts to increase geometrically.\n\nSo you can see why, over time, these kinds of tests\nare going to fail to meet our desiderata because they're too slow\nto enable a productive workflow and a fast enough feedback cycle.\n\n\nNOTE: Don't take it from me!\n  Gary Bernhardt, a legend in both the Ruby and Python testing communities,\n  has a talk simply called\n  https://oreil.ly/ga28I[\"Fast Test, Slow Test\"],\n  which is a great tour of the problems I'm discussing here.\n\n\n.The Holy Flow State\n*******************************************************************************\nThinking sociologically for a moment, we programmers have our own culture\nand our own \"religion\" in a way.(((\"flow, holy state of\")))(((\"holy flow state\")))\nIt has many congregations within it—such as the cult of TDD, to which you are now initiated.\nThere are the followers of Vim and the heretics of Emacs.\nBut one thing we all agree on—one particular spiritual practice,\nour own transcendental meditation—is the holy flow state.\nThat feeling of pure focus, of concentration,\nwhere hours pass like no time at all,\nwhere code flows naturally from our fingers,\nwhere problems are just tricky enough to be interesting\nbut not so hard that they defeat us...\n\nThere is absolutely no hope of achieving flow\nif you spend your time waiting for a slow test suite to run.\nAnything longer than a few seconds and you're going to let your attention wander,\nyou context-switch, and the flow state is gone.\nAnd the flow state is a fragile dream;\nonce it's gone, it takes a long time to come back.footnote:[\nSome people say it takes at least 15 minutes to get back into the flow state.\nIn my experience, that's overblown,\nand I sometimes wonder if it's thanks to TDD.\nI think TDD reduces the cognitive load of programming.\nBy breaking our work down into small increments,\nby simplifying our thinking—\"What's the current failing test?\nWhat's the simplest code I can write to make it pass?\"—it's often actually quite easy to context-switch back into coding.\nMaybe it's less true for the times when we're\ndoing design work and thinking about what the abstractions in our code should be though.\nBut also there's absolutely no hope for you\nif you've started scrolling social media while waiting for your tests to finish.\nSee you in 20 minutes to an hour!]\n\n\n*******************************************************************************\n\n\n==== We're Not Getting the Full Potential Benefits of Testing\n\n\nTDD experts often say, \"It should be called test-driven _design_,\nnot test-driven development\".  What do they mean by that?(((\"tests\", \"giving maximum feedback on code design\")))\n\nWe have definitely seen a bit of the positive influence of TDD on our design.\nWe've talked about how our tests are the first clients of any API we create,\nand we've talked about the benefits of \"programming by wishful thinking\"\nand outside-in.\n\nBut there's more to it.\nThese same TDD experts also often say that you should \"listen to your tests\".\nUnless you've read the\nhttps://www.obeythetestinggoat.com/book/appendix_purist_unit_tests.html[online Appendix: Test Isolation and \"Listening to Your Tests\"],\nthat will still sound like a bit of a mystery.\n\nSo, how can we get to a position where our tests are giving us maximum feedback\non our design?\n\n\n\n=== The Ideal of the Test Pyramid\n\nI know I said I didn't want to get bogged down (((\"test pyramid\")))into arguments based on definitions,\nbut let's set out the way people normally think about these three types of tests:\n\nFunctional/end-to-end tests::\n    FTs check that the system works end-to-end,\n    exercising the full stack (((\"functional  tests (FTs)\")))of the application,\n    including all dependencies and connected external systems.\n    An FT is the ultimate test that it all hangs together,\n    and that things are \"really\" going to work.\n    // CSANAD: I find the expression 'things are \"really\" going to work' too vague.\n    // I would rather mention User Stories here, since they very often can be turned into\n    // functional/end-to-end tests: they are worded similarly and they both cover specific\n    // functionalities that are valuable for a given user (edit: well, you talk\n    // about this below, under \"On Acceptance Tests\").\n    // Furthermore, I would maybe give an example for each:\n    // \"Francis starts a new list by entering a new item.\"\n\nIntegration tests::\n    The purpose of an integration test should be to check that the code\n    you write is integrated correctly with some \"external\" system or dependency.(((\"integration tests\")))\n    // CSANAD: this one is more tricky to find integration tests for, since we\n    // didn't create separate 'integration tests'. Maybe an example could be\n    // checking whether Bootstrap is loaded correctly, or perhaps the email.\n    // Something like that would be helpful in my opinion, especially because\n    // we promised in Chapter 05 (\"Unit Tests Versus Integration Tests, and the Database\")\n    // that we would further clarify the difference.\n\n\n(True) unit tests::\n    Unit tests are the lowest-level tests,\n    and are supposed to test a single \"unit\" of code or behaviour.\n    The ideal unit test is fully isolated(((\"unit tests\")))\n    from everything external to the unit under test,\n    such that changes to things outside cannot break the test.\n    // CSANAD: I was trying to find an example of a pure unit test. I recall\n    // we may have had some helper function at some point, for which there was\n    // no need to use Django's TestCase but I can't find it. Maybe I'm\n    // remembering wrong.\n\nThe canonical advice is that you should aim to have the majority of your tests\nbe unit tests, with a smaller number of integration tests,\nand an even smaller number of functional tests—as in the classic \"test pyramid\" of <<test_pyramid>>.\n// CSANAD: in the HTML, it read: \"as in the classic 'Test Pyramid' of The Test Pyramid\".\n\n\n[[test_pyramid]]\n.The test pyramid\nimage::images/tdd3_2701.png[\"A Pyramid shape, with a large bottom layer of unit tests, a medium layer of integration tests, and a small peak of FTs\"]\n\n\nBottom layer: unit tests (the vast majority)::\n    These isolated tests are fast and they pinpoint failures precisely.\n    We want these to cover the majority of our functionality,\n    and the entirety of our business logic if possible.\n\nMiddle layer: integration tests (a significant portion)::\n    In an ideal world, these are reserved purely for testing the interactions\n    between our code and external systems—like the database,\n    or even (arguably) Django itself.\n    These are slower, but they give us the confidence that our components\n    work together.\n\nTop layer: a minimal set of functional/end-to-end tests::\n    These tests are there to give us the ultimate reassurance\n    that everything works end-to-end and top-to-bottom.\n    But because they are the slowest and most brittle,\n    we want as few of them as possible.\n\n// CSANAD: I think explaining the layers after having explained the types of\n// tests just above it, seems a little redundant. I wonder if we should combine\n// them.\n\n\n[[acceptance_tests]]\n.On Acceptance Tests\n*******************************************************************************\n\nWhat about \"acceptance tests\"? You might have heard this term bandied about.\nOften, people use it to mean the same thing as functional tests or end-to-end tests.(((\"acceptance tests\"))) But, as taught to me by one of the legends of quality assurance at MADE.com (Hi, Marta!),\n_any_ kind of test can be an acceptance test\nif it maps onto one of your acceptance criteria.\n\nThe point of an acceptance test is to validate a piece of behaviour\nthat's important to the user.\nIn our application, that's how we've been thinking about our FTs.\n\nBut, ultimately, using FTs to test every single piece of user-relevant functionality\nis not sustainable.\nWe need to figure out ways to have our integration tests\nand unit tests do the work of verifying user-visible behaviour,\nunderstood at the right level of abstraction.\n\nLearn more in\nhttps://oreil.ly/Pf8Np[the video on acceptance test-driven development (ATDD)]\nby Dave Farley.\n*******************************************************************************\n\n\n=== Avoiding Mock Hell\n\nWell that's all very well, Harry (you might say),\nbut our current test setup is nothing like this!(((\"mocks\", \"avoiding mock hell\")))\nHow do we get there from _here_? We've seen how to use mocks to isolate ourselves from external dependencies.\nAre they the solution then?\n\nAs I was at pains to point out the mocking chapters,\nthe use of mocks comes with painful trade-offs:\n\n* They make tests harder to read and write.\n* They leave your tests tightly coupled to implementation details.\n* As a result, they tend to impede refactoring.\n* And, in the extreme, you can sometimes end up with mocks testing mocks,\n  almost entirely disconnected from what the code actually does.\n\nEd Jung calls this https://oreil.ly/sm16H[Mock Hell].\n\nThis isn't to say that mocks are always bad!\nBut just that, from experience,\nattempting to use them as your primary tool for decoupling\n// CSANAD: I think we could actually argue that by using mocks, we\n// accept that the code is tightly coupled with its dependencies.\nyour tests from external dependencies is not a viable solution;\nit carries costs that often outweigh the benefits.\n\nNOTE: I'm glossing over the use of mocks in a London-school\n    approach to TDD. See the\n    https://www.obeythetestinggoat.com/book/appendix_purist_unit_tests.html[Online Appendix: Test Isolation and \"Listening to Your Tests\"].\n\n\n=== The Actual Solutions Are Architectural\n\nThe actual solution to the problem isn't obvious from where we're standing. It lies in rethinking the architecture of our application.(((\"architectures of applications\")))\nIn brief, if we can _decouple_ the core business logic of our application\nfrom its dependencies, then we can write true, isolated unit tests for it\nthat do not depend on those, um, dependencies.\n(((\"business logic, decoupling from dependencies\")))(((\"dependencies\", \"decoupling business logic from\")))\n\nIntegration tests are most necessary at the _boundaries_ of a system--at\nthe points where our code integrates with external systems—like the database, filesystem, network, or a UI.(((\"boundaries between system components\", \"integration tests and\")))\nSimilarly, it's at the boundaries that the downsides of test isolation and\nmocks are at their worst, because it's at the boundaries that you're most\nlikely to be annoyed if your tests are tightly coupled to an implementation,\nor to need more reassurance that things are integrated properly.\n\nConversely, code at the _core_ of our application--code\nthat's purely concerned with our business domain and business rules,\ncode that's entirely under our control--has no intrinsic need\nfor integration tests.(((\"core application code\")))\n\nSo, the way to get what we want is to minimise the amount of our code\nthat has to deal with boundaries.\nThen we test our core business logic with unit tests,\nand test the rest with integration and functional tests.\n\nBut how do we do that?\n\n\n[role=\"pagebreak-before less_space\"]\n==== Ports and Adapters/Hexagonal/Onion/Clean Architecture\n\nThe classic solutions to this problem from the object-oriented world\ncome under different names, but they're all variations of the same trick:\nidentifying the boundaries, creating an interface to define those boundaries,\nand then using that interface at test time to swap out fake versions of your real dependencies.(((\"architectures of applications\", \"Hexagonal/Clean/Onion architectures\")))(((\"adapters, ports and\")))(((\"object-oriented architecture, ports and adapters\")))(((\"Hexagonal Architecture pattern\")))(((\"Clean Architecture pattern\")))(((\"Onion Architecture pattern\")))\n\nSteve Freeman and Nat Pryce, in their book\n<<GOOSGBT, _Growing Object-Oriented Software, Guided by Tests_>>,\ncall this approach \"Ports and Adapters\" (see <<ports-and-adapters>>).\n\n[[ports-and-adapters]]\n.Ports and Adapters (diagram by Nat Pryce)\nimage::images/tdd3_2702.png[\"Illustration of ports and adapaters architecture, with isolated core and integration points\"]\n\n\n// CSANAD: I haven't found the original diagram by Nat Pryce. I would recommend\n// maybe a making the next header \"Functional Core, Imperative Shell\" formatted\n// differently, making it more obvious that it's an explanation of the diagram.\n// Or, we could just add a \"Legend\" under the diagram, explaining what the\n// nodes, arrows and different shades of the layers depict.\n\nThis pattern, or variations on it, are known as\n\"Hexagonal Architecture\" (by Alistair Cockburn),\n\"Clean Architecture\" (by Robert C. Martin, aka Uncle Bob),\nor \"Onion Architecture\" (by Jeffrey Palermo).\n\n\n.Time for a Plug! Read More in \"Cosmic Python\"\n*******************************************************************************\n\nAt the end of the process of writing this book\n(the first time around)\nI realised that I was going to have to learn about these architectural solutions,\nand it was at MADE.com that I met Bob Gregory who was to become my coauthor.\nThere, we explored \"ports and adapters\" and related architectures,\nwhich were quite rare at the time in the Python world.\n\nSo if you'd like a take on these architectural patterns\nwith a Pythonic twist,\ncheck out https://www.cosmicpython.com[_Architecture Patterns with Python_],\nwhich we subtitled \"Cosmic Python\",\nbecause \"cosmos\" is the opposite of \"chaos\", in Greek.\n\n*******************************************************************************\n\n\n\n==== Functional Core, Imperative Shell\n\nGary Bernhardt pushes this further,\nrecommending an architecture he calls \"Functional Core, Imperative Shell\",\nwhereby the \"shell\" of the application(((\"shell of application, Imperative Shell pattern\")))\n(the place where interaction with boundaries happens)\nfollows the imperative programming paradigm, and can be tested by integration tests,\nfunctional tests, or even (gasp!) not at all (if it's kept minimal enough).\n(((\"Functional Core, Imperative Shell Architecture pattern\")))\n(((\"architectures of applications\", \"Functional Core, Imperative Shell\")))\n(((\"core application code\", \"functional core, imperative shell\")))\n\nBut the core of the application is actually written\nfollowing the functional programming paradigm\n(complete with the \"no side effects\" corollary),\nwhich allows fully isolated, \"pure\" unit tests—_without any mocks or fakes_.\n\nCheck out Gary's presentation titled\nhttps://oreil.ly/of8pU[\"Boundaries\"] for more on this\napproach.\n\n\n==== The Central Conceit: These Architectures Are \"Better\"\n\nThese patterns do not come for free!\nIntroducing the extra indirection and abstraction can add complexity to your code.(((\"architectures of applications\", \"upside of architectural patterns\")))\nIn fact, the creator of Ruby on Rails, David Heinemeier Hansson (DHH),\nhas a famous blog post where he describes these architectures as\nhttps://dhh.dk/2014/test-induced-design-damage.html[test-induced design damage].\nThat post eventually led to quite a thoughtful and https://martinfowler.com/articles/is-tdd-dead[nuanced discussion] between DHH,\nMartin Fowler, and Kent Beck.\n\nLike any technique, these patterns can be misused,\nbut I wanted to make the case for their upside:\nby making our software more testable,\nwe also make it more modular and maintainable.\nWe are forced to clearly separate our concerns,\nand we make it easier to do things like upgrade our infrastructure when we need to.\nThis is the place where the \"improved design\" desiderata comes in.\n\nTIP: Making our software more testable\n  also often leads to a better design.\n\n\n.Testing in Production\n*******************************************************************************\nI should also make brief mention of the power of observability and monitoring.\n\nKent Beck tells a story about his first few weeks at Facebook,\n(((\"observability and monitoring\")))\n(((\"production, testing in\")))\nwhen one of the first tests he wrote turned out to be flaky in the build.\nSomeone just deleted it.  Shocked and asking why,\nhe was told, \"We know production is up. Your test is just producing noise; we don't need it\".\nfootnote:[There's a https://oreil.ly/jhXg8[transcript of this story].]\n\nFacebook has such confidence in its production monitoring and observability\nthat it can provide them with most of the feedback they need about whether the system is working.\n\nNot everywhere is Facebook!  But it's a good indication that automated tests\naren't the be-all and end-all.\n*******************************************************************************\n\n\n=== The Hardest Part: Knowing When to Make the Switch\n\nimage::images/tdd3_2703.png[\"An illustration of a frog being slowly boiled in a pan\"]\n\nWhen is it time to hop out?\n\nFor small- to medium-sized applications, as we've seen, the Django test runner\nand the integration tests it encourages us to write are just fine. The problem is knowing when it's time to make the change\nto a more decoupled architecture, and to start striving explicitly for the test pyramid.(((\"test pyramid\", \"striving explicitly for\")))\n\nIt's hard to give good advice here,\nas I've only experienced environments where either someone else made the decision\nbefore I joined, or the company is already struggling with a point where it's\n(at least arguably) too late.\n\nOne thing to bear in mind, though, is that the longer you leave it, the harder it is.\nAnother is that because the pain is only going to set in gradually,\nlike the apocryphal boiled frogs, you're unlikely to notice\nuntil you're past the \"perfect\" moment to switch.\nAnd on top of that, it's _never_ going to be a convenient time to switch.\nThis is one of those things, like tech debt,\nthat is always going to struggle to justify itself in the face of more\nimmediate priorities.\n\nSo, perhaps one strategy would be an Odysseus pact:\ntie yourself to the mast, and make a commitment--while the tests are still fast--to\nset a \"red line\" for when to switch.\nFor example, \"If the tests ever take more than 10 seconds to run locally,\nthen it's time to rethink the architecture\".\n\n\nI'm not saying 10 seconds is the right number, by the way.\nI know plenty of people who are perfectly happy to wait 30 seconds.\nAnd I know Gary Bernhardt, for one, would get very nervous\nat a test suite that takes more than 100 milliseconds.\n\nBut I think the idea of drawing that line in the sand, wherever it is,\n_before_ you get there, might be a good way to fight the \"boiled frog\" problem. Failing all of that, if the best time to make the change was \"ages ago\",\nthen the second best time is \"right now\".\n\nOther than that, I can only wish you good luck,\nand hope that by warning you of the dangers,\nyou'll keep an eye on your test suite\nand spot the problems before they get too large.\n\nHappy testing!\n\n=== Wrap-Up\n\nIn this book, I've been able to show you how to use TDD,\nand have talked a bit about why we do it and what makes a good test.\nBut we're inevitably limited by the scope of the project.\nWhat that means is that some of the more advanced uses of TDD,\nparticularly the interplay between testing and architecture,\nhave been beyond the scope of this book.\n\nBut I hope that this chapter has been a bit of a guide to find your way\naround that topic as your career progresses.\n\n[role=\"pagebreak-before less_space\"]\n==== Further Reading\n\nA few places to go for(((\"Test-Driven Development (TDD)\", \"resources for further reading\"))) more inspiration:\n\n\"Fast Test, Slow Test\" and \"Boundaries\"::\n    Gary Bernhardt's talks from Pycon\n    https://oreil.ly/6OJKP[2012] and\n    https://oreil.ly/aw-rF[2013].  His\n    http://www.destroyallsoftware.com[screencasts] are also well worth a look.\n\nIntegration tests are a scam::\n    J.B. Rainsberger has a\n    https://oreil.ly/j4ck-[famous rant]\n    about the way integration tests will ruin your life.footnote:[\n    Rainsberger actually distinguishes \"integrated\" tests from \"integration\" tests:\n    an integrated test is any test that's not fully isolated from things outside\n    the unit under test.]\n    Then check out a couple of follow-up posts, particularly\n    http://www.jbrains.ca/permalink/using-integration-tests-mindfully-a-case-study[the\n    defence of acceptance tests], and\n    http://www.jbrains.ca/permalink/part-2-some-hidden-costs-of-integration-tests[the\n    analysis of how slow tests kill productivity].\n    (((\"integration tests\", \"benefits and drawbacks of\")))\n\nPorts and Adapters::\n    Steve Freeman and Nat Pryce wrote about this in <<GOOSGBT, their book>>.\n    You can also catch a good discussion in\n    http://vimeo.com/83960706[Steve's talk].\n    See also\n    https://oreil.ly/2UExy[Uncle\n    Bob's description of the clean architecture], and\n    https://alistair.cockburn.us/hexagonal-architecture[Alistair Cockburn\n    coining the term \"Hexagonal Architecture\"].\n\nThe test-double testing wiki::\n    Justin Searls' online resource is a great source of definitions\n    and discussions on testing pros and cons,\n    and arrives at its own conclusions of the right way to do things:\n    https://github.com/testdouble/contributing-tests/wiki/Test-Driven-Development[testing wiki].\n\n\nFowler on unit tests::\n    Martin Fowler (author of _Refactoring_) offers a\n    http://martinfowler.com/bliki/UnitTest.html[balanced and pragmatic tour]\n    of what unit tests are, and of the trade-offs around speed.\n\nA take from the world of functional programming::\n    _Grokking Simplicity_ by Eric Normand\n    explores the idea of \"Functional Core, Imperative Shell\".\n    Don't worry; you don't need a crazy functional programming language like Haskell or Clojure to understand it—it's written in perfectly sensible JavaScript.\n    // CSANAD: Shouldn't we provide a link to this book too?\n    // https://www.oreilly.com/library/view/grokking-simplicity/9781617296208/\n    // O'Reilly resources usually have a different kind of link though.\n\n\nHappy testing!"
  },
  {
    "path": "check-links.py",
    "content": "#!python\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nimport httpx\nfrom bs4 import BeautifulSoup\n\n\ndef find_links(path):\n    html_content = Path(path).read_text()\n    soup = BeautifulSoup(html_content, \"html.parser\")\n    links = soup.find_all(\"a\", href=True)\n    return [\n        link[\"href\"]\n        for link in links\n        if link[\"href\"].startswith(\"http\")\n        and \"localhost\" not in link[\"href\"]\n        and \"127.0.0.1\" not in link[\"href\"]\n    ]\n\n\nasync def check_url(url, client):\n    print(f\"Checking {url}\")\n    try:\n        await client.head(url, follow_redirects=True, timeout=5)\n    except httpx.RequestError as e:\n        print(f\"Link {url} errored {e}\")\n        return False\n    except httpx.HTTPStatusError as e:\n        print(f\"Link {url} errored {e.response.status_code}\")\n        return False\n    return True\n\n\nasync def main(path):\n    links = find_links(path)\n    async with httpx.AsyncClient() as client:\n        tasks = [check_url(link, client) for link in links]\n        results = await asyncio.gather(*tasks)\n    success_count = sum(results)\n    failure_count = len(links) - success_count\n    print(\n        f\"Checked {len(links)} links, {success_count} succeeded, {failure_count} failed.\"\n    )\n    if failure_count > 0:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    path = sys.argv[1] if len(sys.argv) > 1 else \"book.html\"\n    asyncio.run(main(path))\n"
  },
  {
    "path": "coderay-asciidoctor.css",
    "content": "/*! Stylesheet for CodeRay to loosely match GitHub themes | MIT License */\npre.CodeRay{background:#f7f7f8}\n.CodeRay .line-numbers{border-right:1px solid;opacity:.35;padding:0 .5em 0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}\n.CodeRay span.line-numbers{display:inline-block;margin-right:.75em}\n.CodeRay .line-numbers strong{color:#000}\ntable.CodeRay{border-collapse:separate;border:0;margin-bottom:0;background:none}\ntable.CodeRay td{vertical-align:top;line-height:inherit}\ntable.CodeRay td.line-numbers{text-align:right}\ntable.CodeRay td.code{padding:0 0 0 .75em}\n.CodeRay .debug{color:#fff!important;background:navy!important}\n.CodeRay .annotation{color:#007}\n.CodeRay .attribute-name{color:navy}\n.CodeRay .attribute-value{color:#700}\n.CodeRay .binary{color:#509}\n.CodeRay .comment{color:#998;font-style:italic}\n.CodeRay .char{color:#04d}\n.CodeRay .char .content{color:#04d}\n.CodeRay .char .delimiter{color:#039}\n.CodeRay .class{color:#458;font-weight:bold}\n.CodeRay .complex{color:#a08}\n.CodeRay .constant,.CodeRay .predefined-constant{color:teal}\n.CodeRay .color{color:#099}\n.CodeRay .class-variable{color:#369}\n.CodeRay .decorator{color:#b0b}\n.CodeRay .definition{color:#099}\n.CodeRay .delimiter{color:#000}\n.CodeRay .doc{color:#970}\n.CodeRay .doctype{color:#34b}\n.CodeRay .doc-string{color:#d42}\n.CodeRay .escape{color:#666}\n.CodeRay .entity{color:#800}\n.CodeRay .error{color:#808}\n.CodeRay .exception{color:inherit}\n.CodeRay .filename{color:#099}\n.CodeRay .function{color:#900;font-weight:bold}\n.CodeRay .global-variable{color:teal}\n.CodeRay .hex{color:#058}\n.CodeRay .integer,.CodeRay .float{color:#099}\n.CodeRay .include{color:#555}\n.CodeRay .inline{color:#000}\n.CodeRay .inline .inline{background:#ccc}\n.CodeRay .inline .inline .inline{background:#bbb}\n.CodeRay .inline .inline-delimiter{color:#d14}\n.CodeRay .inline-delimiter{color:#d14}\n.CodeRay .important{color:#555;font-weight:bold}\n.CodeRay .interpreted{color:#b2b}\n.CodeRay .instance-variable{color:teal}\n.CodeRay .label{color:#970}\n.CodeRay .local-variable{color:#963}\n.CodeRay .octal{color:#40e}\n.CodeRay .predefined{color:#369}\n.CodeRay .preprocessor{color:#579}\n.CodeRay .pseudo-class{color:#555}\n.CodeRay .directive{font-weight:bold}\n.CodeRay .type{font-weight:bold}\n.CodeRay .predefined-type{color:inherit}\n.CodeRay .reserved,.CodeRay .keyword{color:#000;font-weight:bold}\n.CodeRay .key{color:#808}\n.CodeRay .key .delimiter{color:#606}\n.CodeRay .key .char{color:#80f}\n.CodeRay .value{color:#088}\n.CodeRay .regexp .delimiter{color:#808}\n.CodeRay .regexp .content{color:#808}\n.CodeRay .regexp .modifier{color:#808}\n.CodeRay .regexp .char{color:#d14}\n.CodeRay .regexp .function{color:#404;font-weight:bold}\n.CodeRay .string{color:#d20}\n.CodeRay .string .string .string{background:#ffd0d0}\n.CodeRay .string .content{color:#d14}\n.CodeRay .string .char{color:#d14}\n.CodeRay .string .delimiter{color:#d14}\n.CodeRay .shell{color:#d14}\n.CodeRay .shell .delimiter{color:#d14}\n.CodeRay .symbol{color:#990073}\n.CodeRay .symbol .content{color:#a60}\n.CodeRay .symbol .delimiter{color:#630}\n.CodeRay .tag{color:teal}\n.CodeRay .tag-special{color:#d70}\n.CodeRay .variable{color:#036}\n.CodeRay .insert{background:#afa}\n.CodeRay .delete{background:#faa}\n.CodeRay .change{color:#aaf;background:#007}\n.CodeRay .head{color:#f8f;background:#505}\n.CodeRay .insert .insert{color:#080}\n.CodeRay .delete .delete{color:#800}\n.CodeRay .change .change{color:#66f}\n.CodeRay .head .head{color:#f4f}"
  },
  {
    "path": "colo.html",
    "content": "<section id=\"colophon\" data-type=\"colophon\" xmlns=\"http://www.w3.org/1999/xhtml\">\n  <h1>Colophon</h1>\n\n  <p>The animal on the cover of <em>Test-Driven Development with Python</em> is a cashmere goat. Though all goats can produce a cashmere undercoat, only those goats selectively bred to produce cashmere in commercially viable amounts are typically considered “cashmere goats”. Cashmere goats thus belong to the domestic goat species <em>Capra hircus</em>.</p>\n\n<p>The exceptionally fine, soft hair of the undercoat of a cashmere goat grows alongside an outercoat of coarser hair as part of the goat’s double fleece. The cashmere undercoat appears in winter to supplement the protection offered by the outercoat, called \"guard hair\". The crimped quality of cashmere hair in the undercoat accounts for its lightweight yet effective insulation properties.</p>\n\n<p>The name “cashmere” is derived from the Kashmir Valley region on the Indian subcontinent, where the textile has been manufactured for thousands of years. A diminishing population of cashmere goats in modern Kashmir has led to the cessation of exports of cashmere fiber from the area. Most cashmere wool now originates in Afghanistan, Iran, Outer Mongolia, India, and—predominantly—China.</p>\n\n<p>Cashmere goats grow hair of varying colors and color combinations. Both males and females have horns, which serve to keep the animals cool in summer and provide the goats’ owners with effective handles during farming activities.</p>\n\n  <p>Many of the animals on O'Reilly covers are endangered; all of them are important to the world.</p>\n\n<p>The cover image is from Wood's Animate Creation. The series design is by Edie Freedman, Ellie Volckhausen, and Karen Montgomery. The cover fonts are Gilroy Semibold and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; the code font is Dalton Maag's Ubuntu Mono; and the Scratchpad font is ORAHand-Medium.</p>\n\n\n\n</section>\n"
  },
  {
    "path": "copy_html_to_site_and_print_toc.py",
    "content": "#!/usr/bin/env python\n\nimport re\nimport subprocess\nfrom collections.abc import Iterator\nfrom pathlib import Path\nfrom typing import NamedTuple\n\nfrom lxml import html\n\nDEST = Path(\"~/workspace/www.obeythetestinggoat.com/content/book\").expanduser()\n\nEXCLUDE = [\n    \"titlepage.html\",\n    \"copyright.html\",\n    \"toc.html\",\n    \"ix.html\",\n    \"author_bio.html\",\n    \"colo.html\",\n]\nADOC_INCLUDE_RE = re.compile(r\"include::(.+.asciidoc)\\[\\]\")\n\n\ndef _chapters():\n    for l in Path(\"book.asciidoc\").read_text().splitlines():\n        if not l.startswith(\"include::\"):\n            continue\n        if m := re.match(ADOC_INCLUDE_RE, l):\n            chap = m.group(1).replace(\".asciidoc\", \".html\")\n            if chap in EXCLUDE:\n                continue\n            yield chap\n        else:\n            raise ValueError(f\"Could not parse include line in book.asciidoc: {l}\")\n\n\nCHAPTERS = list(_chapters())\n\n\nclass ChapterInfo(NamedTuple):\n    href_id: str\n    chapter_title: str\n    subheaders: list[str]\n    xrefs: list[str]\n\n\ndef make_chapters():\n    for chapter in CHAPTERS:\n        subprocess.check_call([\"make\", chapter], stdout=subprocess.PIPE)\n\n\ndef parse_chapters() -> Iterator[tuple[str, html.HtmlElement]]:\n    for chapter in CHAPTERS:\n        raw_html = Path(chapter).read_text()\n        yield chapter, html.fromstring(raw_html)\n\n\ndef get_anchor_targets(parsed_html) -> list[str]:\n    ignores = {\"header\", \"content\", \"footnotes\", \"footer\", \"footer-text\"}\n    all_ids = [a.get(\"id\") for a in parsed_html.cssselect(\"*[id]\")]\n    return [i for i in all_ids if not i.startswith(\"_\") and i not in ignores]\n\n\ndef get_chapter_info():\n    chapter_info = {}\n    appendix_numbers = list(\"ABCDEFGHIJKL\")\n    chapter_numbers = list(range(1, 100))\n    part_numbers = list(range(1, 10))\n\n    for chapter, parsed_html in parse_chapters():\n        print(\"getting info from\", chapter)\n\n        if not parsed_html.cssselect(\"h2\"):\n            header = parsed_html.cssselect(\"h1\")[0]\n        else:\n            header = parsed_html.cssselect(\"h2\")[0]\n        href_id = header.get(\"id\")\n        if href_id is None:\n            href_id = parsed_html.cssselect(\"body\")[0].get(\"id\")\n        subheaders = [h.get(\"id\") for h in parsed_html.cssselect(\"h3\")]\n\n        chapter_title = header.text_content()\n        chapter_title = chapter_title.replace(\"Appendix A: \", \"\")\n\n        if chapter.startswith(\"chapter_\"):\n            chapter_no = chapter_numbers.pop(0)\n            chapter_title = f\"Chapter {chapter_no}: {chapter_title}\"\n\n        if chapter.startswith(\"appendix_\"):\n            appendix_no = appendix_numbers.pop(0)\n            chapter_title = f\"Appendix {appendix_no}: {chapter_title}\"\n\n        if chapter.startswith(\"part\"):\n            part_no = part_numbers.pop(0)\n            chapter_title = f\"Part {part_no}: {chapter_title}\"\n\n        if chapter.startswith(\"epilogue\"):\n            chapter_title = f\"Epilogue: {chapter_title}\"\n\n        xrefs = get_anchor_targets(parsed_html)\n        chapter_info[chapter] = ChapterInfo(href_id, chapter_title, subheaders, xrefs)\n\n    return chapter_info\n\n\ndef fix_xrefs(contents, chapter, chapter_info):\n    parsed = html.fromstring(contents)\n    links = parsed.cssselect(r\"a[href^=\\#]\")\n    for link in links:\n        for other_chap in CHAPTERS:\n            if other_chap == chapter:\n                continue\n            chapter_id = chapter_info[other_chap].href_id\n            href = link.get(\"href\")\n            targets = [\"#\" + x for x in chapter_info[other_chap].xrefs]\n            if href == \"#\" + chapter_id:\n                link.set(\"href\", f\"/book/{other_chap}\")\n            elif href in targets:\n                link.set(\"href\", f\"/book/{other_chap}{href}\")\n\n    return html.tostring(parsed)\n\n\ndef fix_appendix_titles(contents, chapter, chapter_info):\n    parsed = html.fromstring(contents)\n    titles = parsed.cssselect(\"h2\")\n    if titles and titles[0].text.startswith(\"Appendix A\"):\n        title = titles[0]\n        title.text = chapter_info[chapter].chapter_title\n    return html.tostring(parsed)\n\n\ndef copy_chapters_across_with_fixes(chapter_info, fixed_toc):\n    comments_html = Path(\"disqus_comments.html\").read_text()\n    buy_book_div = html.fromstring(Path(\"buy_the_book_banner.html\").read_text())\n    analytics_div = html.fromstring(Path(\"analytics.html\").read_text())\n    load_toc_script = Path(\"load_toc.js\").read_text()\n\n    for chapter in CHAPTERS:\n        old_contents = Path(chapter).read_text()\n        new_contents = fix_xrefs(old_contents, chapter, chapter_info)\n        new_contents = fix_appendix_titles(new_contents, chapter, chapter_info)\n        parsed = html.fromstring(new_contents)\n        body = parsed.cssselect(\"body\")[0]\n        if parsed.cssselect(\"#header\"):\n            head = parsed.cssselect(\"head\")[0]\n            head.append(\n                html.fragment_fromstring(\"<script>\" + load_toc_script + \"</script>\")\n            )\n            body.set(\"class\", \"article toc2 toc-left\")\n        body.insert(0, buy_book_div)\n        body.append(\n            html.fromstring(\n                comments_html.replace(\"CHAPTER_NAME\", chapter.split(\".\")[0])\n            )\n        )\n        body.append(analytics_div)\n        fixed_contents = html.tostring(parsed)\n\n        with open(DEST / chapter, \"w\") as f:\n            f.write(fixed_contents.decode(\"utf8\"))\n        with open(DEST / \"toc.html\", \"w\") as f:\n            f.write(html.tostring(fixed_toc).decode(\"utf8\"))\n\n\ndef extract_toc_from_book():\n    subprocess.check_call([\"make\", \"book.html\"], stdout=subprocess.PIPE)\n    parsed = html.fromstring(Path(\"book.html\").read_text())\n    return parsed.cssselect(\"#toc\")[0]\n\n\ndef fix_toc(toc, chapter_info):\n    href_mappings = {}\n    for chapter in CHAPTERS:\n        chap = chapter_info[chapter]\n        if chap.href_id:\n            href_mappings[\"#\" + chap.href_id] = f\"/book/{chapter}\"\n        for subheader in chap.subheaders:\n            if subheader:\n                href_mappings[\"#\" + subheader] = f\"/book/{chapter}#{subheader}\"\n            else:\n                print(f\"Warning: {chapter} has a subheader with no ID\")\n\n    def fix_link(href):\n        if href in href_mappings:\n            return href_mappings[href]\n        else:\n            return href\n\n    toc.rewrite_links(fix_link)\n    toc.set(\"class\", \"toc2\")\n    return toc\n\n\ndef print_toc_md(chapter_info):\n    for chapter in CHAPTERS:\n        title = chapter_info[chapter].chapter_title\n        print(f\"* [{title}](/book/{chapter})\")\n\n\ndef rsync_images():\n    subprocess.run(\n        [\"rsync\", \"-a\", \"-v\", \"images/\", DEST / \"images/\"],\n        check=True,\n    )\n\n\ndef main():\n    make_chapters()\n    toc = extract_toc_from_book()\n    chapter_info = get_chapter_info()\n    fixed_toc = fix_toc(toc, chapter_info)\n    copy_chapters_across_with_fixes(chapter_info, fixed_toc)\n    rsync_images()\n    print_toc_md(chapter_info)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "copyright.html",
    "content": "<section data-type=\"copyright-page\">\n      \n  <!--TITLE-->\n  <h1>Test-Driven Development with Python</h1>\n  \n  <!--AUTHOR-->\n  <p class=\"author\">by <span class=\"firstname\">Harry</span> <span class=\"othername mi\">J.W.</span> <span class=\"surname\">Percival</span></p>\n  \n  <!-- COPYRIGHT -->\n  <p class=\"copyright\">Copyright © 2025 Harry Percival. All rights reserved.</p>\n  \n  <!--PUBLISHER-->\n  <p class=\"publisher\">Published by <span class=\"publishername\">O’Reilly Media, Inc.</span>, 141 Stony Circle, Suite 195, Santa Rosa, CA 95401.</p>\n  \n  <p>O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most\n\t  titles (<a href=\"https://oreilly.com\"><em>https://oreilly.com</em></a>). For more information, contact our corporate/institutional sales department: 800-998-9938 or <a class=\"email\" href=\"mailto:corporate@oreilly.com\"><em>corporate@oreilly.com</em></a>.</p>\n\n  <!--STAFF LIST-->\n  <ul class=\"stafflist\">\n   <li><span class=\"staffrole\">Acquisitions Editor: </span>Brian Guerin</li>    \n    <li><span class=\"staffrole\">Development Editor: </span>Rita Fernando</li>\n    <li><span class=\"staffrole\">Production Editor: </span>Christopher Faucher</li>\n    <li><span class=\"staffrole\">Copyeditor: </span>Piper Content Partners</li>\n    <li><span class=\"staffrole\">Proofreader: </span>Kim Cofer</li>\n    <li><span class=\"staffrole\">Indexer: </span>Ellen Troutman-Zaig</li>\n    <li><span class=\"staffrole\">Cover Designer: </span>Susan Brown</li>\n    <li><span class=\"staffrole\">Cover Illustrator: </span>Karen Montgomery</li>\n    <li><span class=\"staffrole\">Interior Designer: </span>David Futato</li>\n    <li><span class=\"staffrole\">Interior Illustrator: </span>Kate Dullea</li>\n  </ul>\n  \n  <!-- PRINTINGS -->\n  <ul class=\"printings\">\n    <li><span class=\"printedition\">June 2014:</span> First Edition</li>\n\t\t<li><span class=\"printedition\">August 2017:</span> Second Edition</li>\n\t\t<li><span class=\"printedition\">October 2025:</span> Third Edition</li>\n  </ul>  \n\n  <!-- REVISIONS -->\n  <div>\n  <h1 class=\"revisions\">Revision History for the Third Edition</h1>\n  <ul class=\"releases\">\n    <li><span class=\"revdate\">2025-10-15:</span> First Release</li>\n  </ul>\n  </div>\n \n  <!-- ERRATA -->\n  <p class=\"errata\">See <a href=\"https://www.oreilly.com/catalog/errata.csp?isbn=0636920873884\">https://www.oreilly.com/catalog/errata.csp?isbn=0636920873884</a> for release details.</p>\n\n  <!--LEGAL-->\n  <div class=\"legal\">\n    <p>The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. <em>Test-Driven Development with Python</em>, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.</p>\n\n\t<p>The views expressed in this work are those of the author, and do not represent the publisher's views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights. <!--PROD: Uncomment the following sentence if appropriate and add it to the above para:--><!--This book is not intended as [legal/medical/financial; use the appropriate reference] advice. Please consult a qualified professional if you require [legal/medical/financial] advice.--></p>\n  </div>\n\n  <div class=\"copyright-bottom\">\n    <p class=\"isbn\">978-1-098-14871-3</p>\n    <p class=\"printer\">[LSI]</p>\n  </div>\n      \n</section>\n"
  },
  {
    "path": "count-todos.py",
    "content": "import csv\nimport datetime\nimport re\nimport sys\nfrom pathlib import Path\n\nMARKERS = [\"TODO\", \"RITA\", \"DAVID\", \"SEBASTIAN\", \"JAN\", \"CSANAD\"]\n\nout = csv.writer(sys.stdout)\nout.writerow([\"Date\", \"Chapter\"] + MARKERS)\ntoday = datetime.date.today()\nfor path in sorted(\n    list(Path(\".\").rglob(\"chapter*.asciidoc\"))\n    + list(Path(\".\").rglob(\"appendix*.asciidoc\"))\n):\n    chapter_name = str(path).replace(\".asciidoc\", \"\")\n    contents = path.read_text()\n    todos = [len(re.findall(rf\"\\b{thing}\\b\", contents)) for thing in MARKERS]\n    out.writerow([today.isoformat(), chapter_name] + todos)\n"
  },
  {
    "path": "cover.html",
    "content": "<figure data-type=\"cover\">\n<img src=\"images/cover.png\"/>\n</figure>"
  },
  {
    "path": "disqus_comments.html",
    "content": "<div class=\"comments\" style=\"padding: 20px\">\n  <h3>Comments</h3>\n  <div id=\"disqus_thread\"></div>\n  <script type=\"text/javascript\">\n    var disqus_config = function () {\n        this.page.identifier = 'CHAPTER_NAME';\n    };\n    \n    (function() {\n        var d = document, s = d.createElement('script');\n        s.src = '//obeythetestinggoat.disqus.com/embed.js';\n        s.setAttribute('data-timestamp', +new Date());\n        (d.head || d.body).appendChild(s);\n    })();\n  </script>\n  <noscript>Please enable JavaScript to view the <a href=\"https://disqus.com/?ref_noscript\" rel=\"nofollow\">comments powered by Disqus.</a></noscript>\n</div>\n\n"
  },
  {
    "path": "docs/ORM_style_guide.htm",
    "content": "<!DOCTYPE html>\r\n<html><head>\r\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\r\n\t\t\t<title>O'Reilly Style Guide</title>\r\n\t\t\t<!-- link to main stylesheet -->\r\n\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"ORM_style_guide_files/main.css\">\r\n\t\t</head>\r\n\t\t<body>\r\n\t\t\t<nav>\r\n\t    \t\t<ul>\r\n\t        \t\t<li><a href=\"http://oreillymedia.github.io/production-resources/\">Home</a></li>\r\n\t    \t\t</ul>\r\n\t\t\t</nav>\r\n\t\t\t<div class=\"container\">\r\n\t\t\t\r\n\t\t\t<h1 id=\"oreilly-style-guide-and-word-list\">O’Reilly Style Guide and Word List</h1>\r\n\r\n<ul>\r\n  <li><a href=\"#getting_started\">About O’Reilly Style</a></li>\r\n  <li><a href=\"#considering_electronic_formats\">Considering Electronic Formats</a></li>\r\n  <li><a href=\"#orm_grammar_punctuation_etc\">O’Reilly Grammar, Punctuation, etc.\r\n</a>    <ul>\r\n      <li><a href=\"#abbreviationsacronyms\">Abbreviations/Acronyms</a></li>\r\n      <li><a href=\"#bibliographical_entries\">Bibliographical Entries</a>\r\n        <ul>\r\n          <li><a href=\"#footnotes\">Footnotes</a></li>\r\n        </ul>\r\n      </li>\r\n      <li><a href=\"#cross_references\">Cross References</a></li>\r\n      <li><a href=\"#headings\">Headings</a></li>\r\n      <li><a href=\"#dates_and_numbers\">Dates and Numbers</a></li>\r\n      <li><a href=\"#figures_tables_and_examples\">Figures, Tables, and Examples</a></li>\r\n      <li><a href=\"#code\">Code</a>\r\n        <ul>\r\n          <li><a href=\"#line-length-ZKs1FLck\">Line Length</a></li>\r\n          <li><a href=\"#syntax-highlighting-zVsXIecp\">Syntax Highlighting</a></li>\r\n          <li><a href=\"#formatting-code-in-word-yDsgtXca\">Formatting Code in Word</a></li>\r\n        </ul>\r\n      </li>\r\n      <li><a href=\"#links\">Links</a></li>\r\n      <li><a href=\"#lists\">Lists</a>\r\n        <ul>\r\n          <li><a href=\"#numbered_list\">Numbered list</a></li>\r\n          <li><a href=\"#variable_list\">Variable list</a></li>\r\n          <li><a href=\"#bulleted_list\">Bulleted list</a></li>\r\n        </ul>\r\n      </li>\r\n      <li><a href=\"#miscellaneous\">Miscellaneous</a></li>\r\n      <li><a href=\"#punctuation\">Punctuation</a></li>\r\n      <li><a href=\"#typography_and_font_conventions\">Typography and Font Conventions</a></li>\r\n    </ul>\r\n  </li>\r\n  <li><a href=\"#word-list\">O’Reilly Word List</a></li>\r\n</ul>\r\n\r\n<section data-type=\"sect1\" id=\"getting_started\">\r\n<h1>About O'Reilly Style</h1>\r\n\r\n<p>This style guide is for authors, copyeditors, and proofreaders \r\nworking on books of all formats. As writers and editors, we know that \r\nlanguage changes over time, so <strong>please check back regularly for updates to terms and conventions</strong>. Recent additions will be set in bold for a few months.</p>\r\n\r\n<p>Authors, please also consult the authoring documentation for the format in which you’re writing (<a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html\">Asciidoc</a>, <a href=\"http://oreillymedia.github.io/HTMLBook/\">HTMLbook</a>, <a href=\"https://docbook.org/\">DocBook</a>, or <a href=\"http://oreillymedia.github.io/production-resources/word/\">Word</a>). For sponsored projects, please see our <a href=\"https://oreil.ly/editorial-independence\">statement of editorial independence</a>.</p>\r\n\r\n<p>For term conventions, check our guide and word list first, then <em>The Chicago Manual of Style</em>, 17th edition, then <em><a href=\"https://www.merriam-webster.com/\">Merriam-Webster’s Collegiate Dictionary</a></em>.\r\n Use your book-specific word list (provided by production) to document \r\nstyle choices that differ or are not covered here (e.g., A.M. or a.m., \r\ndata center or datacenter).</p>\r\n\r\n<p>To avoid unintentional bias, when writing about groups of people, \r\ncheck the group’s advocacy organization for guidance on appropriate \r\nlanguage. The <a href=\"https://consciousstyleguide.com/\">Conscious Style Guide</a> is one good resource, aggregating links to relevant organizations. <strong>The <a href=\"https://itconnect.uw.edu/work/inclusive-language-guide\">University of Washington has another</a> that is tech-specific.</strong> The <a href=\"https://ncdj.org/style-guide\">Disability Language Style Guide</a>\r\n is a thorough guide to writing about disabilities with sensitivity. \r\nAlways follow a person’s preference and note exceptions, if necessary \r\n(e.g., quoting research that is decades old).</p>\r\n\r\n<p>For questions specific to your book or assignment, please consult with your editor or production editor.</p>\r\n</section>\r\n\r\n<section data-type=\"sect1\" id=\"considering_electronic_formats\">\r\n<h1>Considering Electronic Formats</h1>\r\n\r\n<p>Because we use a single set of source files to produce the print and \r\nelectronic versions of our books, it’s important to keep all formats in \r\nmind while writing and editing:</p>\r\n\r\n<ul>\r\n  <li>\r\n    <p>Avoid using \"above\" and \"below\" to reference figures, tables, \r\nexamples, unnumbered code blocks, equations, etc. (e.g., \"In the example\r\n below…\"). Using live cross references (e.g., \"see Figure 2-1\") is best,\r\n but when that’s not possible, use \"preceding\" or \"following,\" as the \r\nphysical placement of elements could be different in reflowable formats.</p>\r\n  </li>\r\n  <li>\r\n    <p>Anchor URLs to text nodes whenever possible, like you would on a website. See <a href=\"#links\">Links</a> for more information.</p>\r\n\r\n<div data-type=\"tip\" id=\"id-BeU0teho\">\r\n  <p>Be as descriptive as possible because the print version of your book renders hyperlinks like this: \"text anchor (<a href=\"http://url.example.com/\"><em class=\"hyperlink\">http://url.example.com/</em></a>).\"</p>\r\n\r\n<p>For example, this:</p>\r\n<blockquote>\r\n<p>Download the source code (<a href=\"http://www.url.thisismadeup.com/\"><em class=\"hyperlink\">http://www.url.thisismadeup.com</em></a>) and install the package\"</p></blockquote>\r\n\r\n<p>is more useful than this:</p>\r\n<blockquote>\r\n<p>\"Download the source code from this website (<a href=\"http://www.url.thisismadeup.com/\"><em class=\"hyperlink\">http://www.url.thisismadeup.com</em></a>) and install the package.\"</p></blockquote>\r\n\r\n<p>Avoid anchoring URLs to generic words or phrases such as \"here,\" \"this website,\" etc.</p>\r\n</div>\r\n  </li>\r\n\r\n<li>\r\n<p>Long URLs will be shortened so that they’re easy for print readers to type manually.</p>\r\n \r\n<div data-type=\"warning\" id=\"id-warning-amazon\">\r\n  <p>Do not link to products on any sales channels other than \r\noreilly.com, including Apple, Google, or Amazon. Apple and Google will \r\nrefuse to sell content that links to products on Amazon. Vendors, please\r\n flag any links to these sales channels and let the production editor \r\nknow they exist.</p>\r\n  \r\n  <p>Saying \"XX book is available on Amazon\"—sans link—is OK.</p>\r\n</div>\r\n</li>\r\n</ul>\r\n</section>\r\n\r\n<section data-type=\"sect1\" id=\"orm_grammar_punctuation_etc\">\r\n<h1>O’Reilly Grammar, Punctuation, etc.</h1>\r\n\r\n<p>For any words or conventions not covered here, refer to <em>The Chicago Manual of Style</em>, 17th edition and <em><a href=\"https://www.merriam-webster.com/\">Merriam-Webster</a></em>.</p>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n\r\n<section data-type=\"sect2\" id=\"abbreviationsacronyms\">\r\n<h2>Abbreviations/Acronyms</h2>\r\n\r\n<ul>\r\n <li>\r\n  <p>Generic pronouns should be they/their when needed, not he, she, or he/she.\r\n  </p>\r\n </li>\r\n<li>\r\n <p>Acronyms should <em>generally</em> be spelled out the first time they appear in a book, as in: \"collaborative development environment (CDE).\" See the <a href=\"#word-list\">Word List</a>\r\n for common exceptions. After the acronym has been defined, you should \r\ngenerally use the acronym only (not the whole term, unless it makes more\r\n sense contextually to use the whole term). Usually, acronyms are \r\ndefined only once per book. But if the author prefers, we can also \r\ndefine certain terms the first time they appear in each chapter.</p>\r\n</li>\r\n <li>\r\n  <p>Acronyms should be capitalized when expanded only if the term is a \r\nproper noun (and spelled that way by the company). For example, key \r\nperformance indicator (KPI), but Amazon Web Services (AWS).</p>\r\n </li>\r\n<li>\r\n<p>A.M. and P.M. or a.m. and p.m.—be consistent.</p>\r\n</li>\r\n<li>\r\n<p>K = 1,024; k = 1,000. So a 56 kbps modem is equal to 56,000 bps, while 64 K of memory is equal to 65,536.</p>\r\n</li>\r\n<li>\r\n<p>In units of measure, do not use a hyphen. For example, it’s 32 MB \r\nhard drive, not 32-MB hard drive. (Though when the unit is spelled out, \r\nuse a hyphen, e.g., 32-megabyte hard drive.)</p>\r\n</li>\r\n<li>\r\n<p>University degrees (e.g., B.A., B.S., M.A., M.S., Ph.D., etc.) can appear with or without periods—just be consistent.</p>\r\n</li>\r\n<li>\r\n<p>United States and United Kingdom should be spelled out on first \r\nmention. After that, just use the acronym with no periods (so, US or \r\nUK).</p>\r\n</li>\r\n</ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"bibliographical_entries\">\r\n<h2>Bibliographical Entries and Citations</h2>\r\n\r\n<p>In general, when referring to another book within a book’s text, \r\ninclude the author name(s) for up to two authors. For three or more \r\nauthors, state the first author name, followed by “et al.” (be sure to \r\ninclude the period).</p>\r\n\r\n<p>On first reference to another book, include author and publisher name. For example, \"You can find more information in <em>The Elements of Typographic Style</em> by Robert Bringhurst (H&amp;M),\" or \"For more information, consult Robert Bringhurst’s <em>The Elements of Typographic Style</em> (H&amp;M).\" On subsequent references, just use the book title.</p>\r\n\r\n<p>When referencing an O’Reilly book within the text, note only \r\n\"O’Reilly\" in parentheses, not \"O’Reilly Media, Inc.\" References to \r\nother O’Reilly books should be linked to the book’s <a href=\"http://shop.oreilly.com/category/browse-subjects.do\">catalog page</a>.</p>\r\n\r\n<div data-type=\"warning\">\r\n<p>Make sure that the catalog page is anchored to the book’s title, rather than standing on its own like this: \"See <a href=\"http://shop.oreilly.com/product/0636920024033.do\"><em>Programming F# 3.0</em></a>.\" <em>NOT THIS:</em> \"See <em>Programming F# 3.0</em> (<a href=\"http://shop.oreilly.com/product/0636920024033.do\"><em class=\"hyperlink\">http://shop.oreilly.com/product/0636920024033.do</em></a>).\"</p>\r\n</div>\r\n\r\n<h3>Citations</h3>\r\n<p>When citing other materials in bibliographies, reference lists, or footnotes, use the “Notes and Bibliography” system from the <a href=\"https://www.chicagomanualofstyle.org/tools_citationguide.html\"><em>The Chicago Manual of Style</em></a>, 17th edition. <strong>Chicago\r\n also has an Author-Date system that some authors prefer, which is \r\nperfectly acceptable. If there is no discernible consistency, suggest \r\nChicago's Notes for footnotes and Bibliography for endnotes or back \r\nmatter.</strong></p> \r\n\r\n<p><strong>Let your production editor know which of Chicago's systems you applied by adding a note to the Word List Doc.</strong></p>\r\n\r\n<section data-type=\"sect3\" id=\"footnotes\">\r\n<h3>Footnotes</h3>\r\n\r\n<ul>\r\n <li>\r\n<p>Footnotes in running text are numbered and start over at 1 in each \r\nchapter. Footnote markers in running text should always appear after \r\npunctuation.</p>\r\n\r\n<div data-type=\"tip\">\r\n<p>This: The following query selects the <code>symbol</code> column and all columns from <code>stocks</code> whose names start with the prefix price.<sup>1</sup></p>\r\n\r\n<p><em>Not this:</em> The following query selects the <code>symbol</code> column and all columns from <code>stocks</code> whose names start with the prefix price<sup>1</sup>.</p>\r\n</div>\r\n</li>\r\n<li>\r\n<p>Footnotes should contain more than just a URL, whether a full \r\ncitation for the text the URL points to or context for where the link \r\nleads.</p>\r\n\r\n<div data-type=\"tip\">\r\n <p>This:</p>\r\n <ol>\r\n  <li>The Wikipedia entry on JavaScript (https://en.wikipedia.org/wiki/JavaScript) provides more information.</li>\r\n  <li>Grove, John. 2015. “Calhoun and Conservative Reform.” <em>American Political Thought</em> 4, no. 2 (March): 203–27. https://doi.org/10.1086/680389.</li>\r\n </ol>\r\n\r\n<p><em>Not this:</em></p>\r\n<ol>\r\n  <li>https://en.wikipedia.org/wiki/JavaScript</li>\r\n <li>https://doi.org/10.1086/680389</li>\r\n </ol>\r\n </div>\r\n </li>\r\n<li>\r\n<p>Table footnotes are lettered (a, b, c, etc.) and appear directly after the table. They should be kept to a minimum.</p>\r\n</li>\r\n</ul>\r\n\r\n<p>More details about styling footnotes in AsciiDoc are in <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#adding_footnotes\">Writing in AsciiDoc</a>.</p>\r\n</section>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n</section>\r\n\r\n<section data-type=\"sect2\" id=\"cross_references\">\r\n<h2>Cross References</h2>\r\n\r\n<p>Here are a few examples of cross references:</p>\r\n\r\n<ul>\r\n<li>\r\n<p>Chapter: See Chapter 27.</p>\r\n</li>\r\n<li>\r\n<p>Section: See “Treatment” on page xx. (The text “on page xx” will be dynamic in Atlas, updating as page numbers change.)</p>\r\n</li>\r\n<li>\r\n<p>Figure: ...as shown in Figure 1-1.</p>\r\n</li>\r\n<li>\r\n<p>Sidebars: See “A Note for Mac Users” on page xx. (As with section xrefs, the page number will update automatically in Atlas.)</p>\r\n</li>\r\n</ul>\r\n \r\n<p>More details on cross-references in Asciidoc are available in our <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#XREFS\">Writing in AsciiDoc</a> guide.</p>\r\n\r\n<p>These cross-reference styles are also available in DocBook under various &lt;xref&gt;: formats. Please refer to the <a href=\"http://chimera.labs.oreilly.com/books/1234000000058/ch02.html#creating_xrefs\">DocBook Authoring Guidelines</a>.</p>\r\n\r\n<p>For information about styling URLs and hyperlinks, see <a data-type=\"xref\" href=\"#considering_electronic_formats\">Considering Electronic Formats</a>.</p>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"headings\">\r\n<h2>Headings</h2>\r\n\r\n <p>Capitalization in headings:</p>\r\n \r\n <ul>\r\n  <li><p>In most of our design templates, A- and B-level headings are \r\ninitial-capped (or title case): cap the first letter of each word, with \r\nthe exception of articles, conjunctions, and program names or technical \r\nwords that are always lowercase.</p></li> \r\n  <li><p>Prepositions of four letters or fewer are not initial-capped, \r\nunless they function as part of a verb (e.g., “Set Up Your Operating \r\nSystem”).</p></li> \r\n  <li><p>Hyphenated words in subordinating conjunctions (e.g., as, if, \r\nthat, because, etc.) are always initial-capped (even if they are four \r\nletters or less). Hyphenated words in titles or captions should both be \r\ncapped if the second word is a main word, but only the first should be \r\ncapped if the second word isn’t too important (it’s a bit of a judgment \r\ncall). For example: Big-Endian, Built-in. See <em>The Chicago Manual of Style</em>.</p>\r\n</li>\r\n  <li>\r\n<p>C-level headings have initial cap on the first word only (also called\r\n sentence-case), with the exception of proper nouns and the first word \r\nthat follows a colon (unless that word refers to code and should be \r\nlowercase).</p>\r\n</li>\r\n  <li>\r\n<p>D-level headings (rare) are run-in with the following paragraph and \r\nhave an initial cap on the first word only, with the exception of proper\r\n nouns and the first word that follows a colon (unless that word refers \r\nto code and should be lowercase), with a period at the end of the \r\nheading.</p>\r\n</li>\r\n  <li>\r\n<p>Sidebar titles are initial-capped, or title case (like A- and B-level headings, mentioned previously).</p>\r\n</li>\r\n  <li>\r\n<p>Admonition (note/tip/warning) titles are initial-capped, or title \r\ncase (like A- and B-level headings, mentioned previously). Admonition \r\ntitles are optional.</p>\r\n</li>\r\n </ul>\r\n \r\n\r\n<p>Headings should not contain inline code font or style formatting such as bold, italic, or code font.</p>\r\n\r\n<p>Headings should always immediately precede body text. Do not follow a\r\n heading with an admonition or another heading without some form of \r\nintroductory or descriptive text.</p>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"dates_and_numbers\">\r\n<h2>Dates and Numbers</h2>\r\n\r\n<p>What to spell out and when:</p>\r\n<ul>\r\n<li>\r\n <p>Spell out numbers from zero to nine and certain round multiples of \r\nthose numbers unless the same object appears in a sentence with an \r\nobject 10 or over (five apples; five apples and one hundred oranges; 5 \r\napples and 110 oranges). </p>\r\n </li>\r\n  <li><p>Whole numbers one through nine followed by hundred, thousand, \r\nmillion, billion, and so forth are usually spelled out (except in the \r\nsciences or with monetary amounts).</p>\r\n</li>\r\n <li><p>Centuries follow the same zero through nine rule, so those will usually be numerals (i.e., 20th century, 21st century).</p>\r\n  </li>\r\n<li>\r\n<p>In most numbers of one thousand or more, commas should be used between groups of three digits, counting from the right (32,904 <em>NOT 32904</em>). Exceptions: page numbers, addresses, port numbers, etc.</p>\r\n</li>\r\n<li>\r\n<p>Use numerals for versions (version 5 or v5).</p>\r\n</li>\r\n<li>\r\n<p>Use a numeral if it’s an actual value (e.g., 5% 7″ $6.00).</p>\r\n</li>\r\n<li>\r\n <p>Always use the symbol % with numerals rather than the spelled out \r\nword (percent), and make sure it is closed up to number: 0.05%. Unless \r\nthe percentage begins a sentence or title/caption, the number should be a\r\n numeral with the % symbol.</p>\r\n </li>\r\n <li>\r\n<p>Ordinal numbers: Spell out first through ninth, use numerals for 10th and above. No superscript.</p>\r\n</li>\r\n </ul>\r\n \r\n <p>Formatting:</p>\r\n \r\n <ul>\r\n <li>\r\n  <p>Use spaces around inline operators (1 + 1 = 2. <em>NOT 1+1=2</em>).</p>\r\n </li>\r\n<li>\r\n<p>32-bit integer.</p>\r\n</li>\r\n<li>\r\n<p>1980s or ’80s.</p>\r\n</li>\r\n<li>\r\n<p>Phone numbers can appear in the format xxx-xxx-xxxx.</p>\r\n</li>\r\n<li>\r\n<p>Use an en dash (–) with negative numbers or for minus signs, rather than a hyphen.</p>\r\n</li>\r\n<li>\r\n<p>Use multiplication symbol “×” for dimensions, not \"by\" (e.g., \"8.5 × 11\").</p>\r\n</li>\r\n</ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"figures_tables_and_examples\">\r\n<h2>Figures, Tables, and Examples</h2>\r\n\r\n<p>Every formally numbered figure, table, and example should be preceded\r\n by a specific in-text reference (for example: see Figure 99-1; Example \r\n1-99 shows; Table 1-1 lists, etc.). Formal figures, tables, and examples\r\n should not be introduced with colons or phrases like “in the following \r\nfigure,” or “as shown in this table.” Though we do support unnumbered \r\ninformal figures/tables/examples, these should be used only for elements\r\n whose contents are not discussed at length or referred back to. Lack of\r\n specific in-text references may cause incorrect placement of figures. <strong>See <a href=\"#cross_references\">Cross References</a> above for more detail on including cross references.</strong></p>\r\n\r\n<div data-type=\"tip\">\r\n<p>If you are writing or copyediting in Word, figure, table, and example\r\n numbers should be numbered as follows: 1-2 (note hyphen [-], not en \r\ndash [–] between numbers). The first number is the chapter number. This \r\nwill be soft-coded in production if not during the writing process.</p>\r\n\r\n<p>If you are writing or copyediting in Asciidoc, please refer to <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#XREFS\">Writing in AsciiDoc</a> for examples of Asciidoc cross references.</p>\r\n\r\n<p>If you are writing or copyediting in DocBook, please reference each figure, table, and example with an &lt;xref&gt;.</p>\r\n</div>\r\n\r\n<p>Any word groupings within a figure should have an initial cap on the \r\nfirst word only, with the exception of proper nouns. Generally, we don’t\r\n use periods at the end of these word groupings.</p>\r\n\r\n<ul>\r\n<li>\r\n<p>Figure 1-1. Figure captions are sentence-cased, with the exception of\r\n proper nouns. Code styling is allowed within the figure name or \r\ncaption. There is no period after figure captions. Exceptions should be \r\ndiscussed with your production editor (e.g., if several long captions \r\nrequire punctuation, we can collaborate on efficient ways to achieve \r\nconsistency). </p>\r\n</li>\r\n<li>\r\n<p>Table 1-1. Column heads and table titles are sentence-cased, with the\r\n exception of proper nouns.  Code styling is allowed within the table \r\nname or caption. There is no period after table titles.</p>\r\n</li>\r\n<li>\r\n<p>Example 1-1. Example titles are sentence-cased, with the exception of\r\n proper nouns. Code styling is allowed within the example name or \r\ncaption. There is no period after example titles.</p>\r\n</li>\r\n</ul>\r\n\r\n<div data-type=\"tip\">\r\n<p>When working in Word, make sure all table cells are tagged with a \r\ncell paragraph tag, even if they’re blank. Any bold “headings” that \r\nappear below the very first row of a table should be tagged \r\nCellSubheading rather than CellHeading.</p>\r\n\r\n<p>Also in Word, all figures must be within a FigureHolder paragraph followed directly by a FigureTitle paragraph.</p>\r\n</div>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"code\">\r\n<h2>Code</h2>\r\n\r\n\r\n\r\n<section data-type=\"sect3\" id=\"line-length-ZKs1FLck\">\r\n<h3>Line Length</h3>\r\n\r\n<p>Maximum line length for code varies slightly between book formats. \r\nConsult the table below to find the maximum line length for your book’s \r\nseries within Atlas v2. If writing in Word, please keep code within the \r\nmargins that appear in the Word template and indicate proper linebreaks \r\nand indents for all code. Indent using spaces, not tabs.</p>\r\n<table>\r\n\r\n<thead>\r\n<tr>\r\n<th><strong>Series</strong></th>\r\n<th><strong>Body (top-level code)</strong></th>\r\n<th><strong>Examples</strong></th>\r\n<th><strong>Lists</strong></th>\r\n<th><strong>Readeraids</strong></th>\r\n<th><strong>Sidebars</strong></th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n<td><p><strong>Animal</strong></p></td>\r\n<td><p>81</p></td>\r\n<td><p>85</p></td>\r\n<td><p>73</p></td>\r\n<td><p>57</p></td>\r\n<td><p>77</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Animal 6x9</strong></p></td>\r\n<td><p>64</p></td>\r\n<td><p>68</p></td>\r\n<td><p>56</p></td>\r\n<td><p>40</p></td>\r\n<td><p>60</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Report 6x9</strong></p></td>\r\n<td><p>64</p></td>\r\n<td><p>68</p></td>\r\n<td><p>56</p></td>\r\n<td><p>40</p></td>\r\n<td><p>60</p></td>\r\n</tr>\r\n <tr>\r\n  <td><p><strong>Trade 6x9</strong></p></td>\r\n  <td><p>76</p></td>\r\n  <td><p>72</p></td>\r\n  <td><p>65</p></td>\r\n  <td><p>80</p></td>\r\n  <td><p>69</p></td>\r\n </tr>\r\n<tr>\r\n<td><p><strong>Cookbook</strong></p></td>\r\n<td><p>81</p></td>\r\n<td><p>85</p></td>\r\n<td><p>73</p></td>\r\n<td><p>57</p></td>\r\n<td><p>77</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Make 1-column</strong></p></td>\r\n<td><p>89</p></td>\r\n<td><p>89</p></td>\r\n<td><p>81</p></td>\r\n<td><p>66</p></td>\r\n<td><p>39</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Make 2-column</strong></p></td>\r\n<td><p>45</p></td>\r\n<td><p>46</p></td>\r\n<td><p>35</p></td>\r\n<td><p>28</p></td>\r\n<td><p>40</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Make Getting Started</strong></p></td>\r\n<td><p>63</p></td>\r\n<td><p>67</p></td>\r\n<td><p>60</p></td>\r\n<td><p>51</p></td>\r\n<td><p>60</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Nutshell</strong></p></td>\r\n<td><p>71</p></td>\r\n<td><p>75</p></td>\r\n<td><p>67</p></td>\r\n<td><p>60</p></td>\r\n<td><p>75</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Pocket Ref</strong></p></td>\r\n<td><p>51</p></td>\r\n<td><p>55</p></td>\r\n<td><p>50</p></td>\r\n<td><p>42</p></td>\r\n<td><p>51</p></td>\r\n</tr>\r\n<tr>\r\n<td><p><strong>Theory in Practice</strong></p></td>\r\n<td><p>81</p></td>\r\n<td><p>85</p></td>\r\n<td><p>77</p></td>\r\n<td><p>51</p></td>\r\n<td><p>83</p></td>\r\n</tr>\r\n</tbody>\r\n</table>\r\n\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect3\" id=\"syntax-highlighting-zVsXIecp\">\r\n<h3>Syntax Highlighting</h3>\r\n\r\n<p>We use a tool called Pygments to colorize code. In most books, code \r\nwill appear in black and white in the print book and in color in all \r\nelectronic formats, including the web pdf. If you’re an author, please \r\nconsult the <a href=\"http://pygments.org/docs/lexers/\">list of available lexers</a> and apply them to your code as you write. To apply syntax highlighting in Asciidoc, consult <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#syntax_highlighting\">Writing in AsciiDoc</a>. To apply syntax highlighting in DocBook, consult <a href=\"http://chimera.labs.oreilly.com/books/1234000000058/ch02.html#syntax_highlighting\">the DocBook Authoring Guidelines</a>. To apply syntax highlighting in Word, consult the <a href=\"http://oreillymedia.github.io/production-resources/word/#syntax-highlighting\">O’Reilly Media Word Template Quickstart Guide</a>.</p>\r\n\r\n\r\n\r\n\r\n\r\n<div data-type=\"tip\" id=\"formatting-code-in-word-yDsgtXca\">\r\n<h4>Formatting Code in Word</h4>\r\n\r\n<p>When copyediting in Word, please do a global search and replace for \r\ntabs in code (search for \\^t to find them) before submitting files for \r\nconversion; tabs <em>will not</em> convert. A general rule of thumb is \r\none tab can be replaced with four spaces (which is the same number that \r\nthe clean-up macro in the ORA.dot template uses). However, this number \r\ncan vary, so the most important thing is that copyeditors replace tabs \r\nwith the numbers of spaces needed to match the indentation and make sure\r\n levels of indentation are preserved.</p>\r\n</div>\r\n\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"links\">\r\n<h2>Links</h2>\r\n\r\n<p>In books produced in Atlas, URLs should be anchored to descriptive \r\ntext where possible. In ebook versions, the markup will render like \r\nthis:</p>\r\n\r\n<ul>\r\n <li>\r\n<p>Navigate to the <a href=\"https://oreilly.com/\">O'Reilly home page</a> for more information.</p>\r\n </li>\r\n </ul>\r\n\r\n<p>In the print book, the URL will unfurl in a parenthetical after the linked text:</p>\r\n\r\n<ul>\r\n <li>\r\n<p>Navigate to the O'Reilly home page (<em>https://oreilly.com</em>) for more information.</p>\r\n </li>\r\n </ul>\r\n \r\n<p>Because of this difference in appearance of links in ebooks and print\r\n books, long and complex URLs are shortened during production. In the \r\npast, we used bit.ly to shorten these URLs, but as of May 2019, all \r\nshortened links will be hosted and tracked internally, using the \r\noreil.ly short link. </p>\r\n\r\n<div data-type=\"tip\">\r\n<p>We do not anchor URLs to text in books produced in InDesign.</p>\r\n </div>\r\n\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"lists\">\r\n<h2>Lists</h2>\r\n\r\n<p>Typically, we use three types of lists: numbered lists, for ordered \r\nsteps or chronological items; variable lists, for terms and \r\nexplanations/definitions; and bulleted lists, for series of items. List \r\nitems are sentence-capped. List items should be treated as separate \r\nitems and should not be strung together with punctuation or \r\nconjunctions. Unless one item in a list forms a complete sentence, the \r\nlist's items do not take periods. If one does form a complete sentence, \r\nuse periods for all items within that list, even fragments.</p> \r\n\r\n\r\n\r\n<div data-type=\"tip\">\r\n<p><em>NOT O'Reilly style:</em></p>\r\n<ul>\r\n <li><em>Here is an item, and</em></li>\r\n <li><em>here is another item; and</em></li>\r\n <li><em>here is the final item.</em></li>\r\n </ul>\r\n \r\n <p>O'Reilly style:</p>\r\n <ul>\r\n <li>Here is an item.</li>\r\n <li>Here is another item.</li>\r\n <li>Here is the final item.</li>\r\n </ul>\r\n </div>\r\n\r\n<p>Following are examples of each type of list.</p>\r\n\r\n\r\n<section data-type=\"sect3\" id=\"numbered_list\">\r\n<h3>Numbered list</h3>\r\n\r\n<p>The following list of step-by-step instructions is an example of a numbered list:</p>\r\n<ol>\r\n<li>\r\n<p>Save Example 2-1 as the file <em>hello.cs</em>.</p>\r\n</li>\r\n<li>\r\n<p>Open a command window.</p>\r\n</li>\r\n<li>\r\n<p>From the command line, enter <code>csc /debug hello.cs</code>.</p>\r\n</li>\r\n<li>\r\n<p>To run the program, enter <code>Hello</code>.</p>\r\n</li>\r\n\r\n</ol>\r\n</section>\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect3\" id=\"variable_list\">\r\n<h3>Variable list</h3>\r\n\r\n<p>The following list of defined terms is an example of a variable list:</p>\r\n<dl>\r\n<dt><em>Setup project</em></dt>\r\n<dd>\r\n<p>This creates a setup file that automatically installs your files and resources.</p>\r\n</dd>\r\n<dt><em>Web setup project</em></dt>\r\n<dd>\r\n<p>This helps deploy a web-based project.</p>\r\n</dd>\r\n</dl>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect3\" id=\"bulleted_list\">\r\n<h3>Bulleted list</h3>\r\n\r\n<p>The following series of items is an example of a bulleted list:</p>\r\n\r\n<ul>\r\n<li>\r\n<p>Labels</p>\r\n</li>\r\n<li>\r\n<p>Buttons</p>\r\n</li>\r\n<li>\r\n<p>A text box</p>\r\n</li>\r\n</ul>\r\n\r\n<p>“Bulleted” lists nested inside of bulleted lists should have em dashes as bullets.</p>\r\n\r\n<p>Frequently, bulleted lists should be converted to variable lists. Any\r\n bulleted list whose entries consist of a short term and its definition \r\nshould be converted. For example, the following bulleted list entries:</p>\r\n\r\n<ul>\r\n<li>\r\n<p>Spellchecking: process of correcting spelling</p>\r\n</li>\r\n<li>\r\n<p>Pagebreaking—process of breaking pages</p>\r\n</li>\r\n</ul>\r\n\r\n<p>should be variable list entries:</p>\r\n<dl>\r\n<dt><em>Spellchecking</em></dt>\r\n<dd>\r\n<p>Process of correcting spelling</p>\r\n</dd>\r\n<dt><em>Pagebreaking</em></dt>\r\n<dd>\r\n<p>Process of breaking pages</p>\r\n</dd>\r\n</dl>\r\n</section>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect2\" id=\"punctuation\">\r\n<h2>Punctuation</h2>\r\n\r\n <p>For anything not covered in this list, please consult the <em>Chicago Manual of Style</em>, 17th Edition.</p>\r\n\r\n<ul>\r\n<li>\r\n<p>Serial comma (this, that, and the other).</p>\r\n</li>\r\n<li>\r\n<p>Commas and periods go inside quotation marks.</p>\r\n</li>\r\n<li>\r\n<p>Curly quotes and apostrophes (“ ” not \" \") in regular text.</p>\r\n</li>\r\n<li>\r\n<p>Straight quotes (\" \" not “ ”) in constant-width text and all code. Some Unix commands use backticks (<code>`</code>), which must be preserved.</p>\r\n</li>\r\n<li>\r\n<p>No period after list items unless one item forms a complete sentence \r\n(then use periods for all items within that list, even fragments).</p>\r\n</li>\r\n <li>\r\n <p>Em dashes are always closed (no space around them).</p>\r\n  </li>\r\n <li>\r\n<p>Ellipses are always closed (no space around them).</p>\r\n  </li>\r\n<li>\r\n<p>For menu items that end with an ellipsis (e.g., \"New Folder…\"), do not include ellipsis in running text.</p>\r\n</li>\r\n<li>\r\n<p>Lowercase the first letter after a colon: this is how we do it. (Exception: headings.)</p>\r\n</li>\r\n<li>\r\n<p>Parentheses are always roman, even when the contents are italic. For \r\nparentheses within parentheses, use square brackets (here’s the first \r\nparenthetical [and here’s the second]).</p>\r\n</li>\r\n</ul>\r\n</section>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect1\" id=\"miscellaneous\">\r\n<h1>Miscellaneous</h1>\r\n\r\n<ul>\r\n<li>\r\n<p>Do not use a hyphen between an adverb and the word it modifies. So, “incredibly wide table” rather than “incredibly-wide table.”</p>\r\n</li>\r\n<li>\r\n<p>Close up words with the following prefixes (unless part of a proper \r\nnoun) “micro,” “meta,” “multi,” “pseudo,” “re,” “non,” “sub,” and \"co\" \r\n(e.g., “multiusers,” “pseudoattribute,” “nonprogrammer,” “subprocess,” \r\n\"coauthor\"). Exceptions are noted in the word list (e.g., \"re-create,\" \r\n\"re-identification\").</p>\r\n</li>\r\n<li>\r\n<p>Avoid using the possessive case for singular nouns ending in “s,” if \r\npossible. So, it’s “the Windows Start menu,” not “Windows’s Start menu.”</p>\r\n</li>\r\n<li>\r\n<p>Avoid wholesale changes to the author’s voice—for example, changing \r\nthe first-person plural (the royal “we”) to the first-person singular or\r\n the second person. However, do try to maintain a consistency within \r\nsentences or paragraphs, where appropriate.</p>\r\n</li>\r\n <li>\r\n  <p>We advise using a conversational, user-friendly tone that assumes \r\nthe reader is intelligent but doesn’t have this particular knowledge \r\nyet—like an experienced colleague onboarding a new hire. First-person \r\npronouns, contractions, and active verbs are all encouraged.<strong>(Copyeditors: please check with your production editor if you wish to suggest global changes to tone.)</strong></p>\r\n </li>\r\n<li>\r\n<p>Companies are always singular. So, for example, “Apple emphasizes the\r\n value of aesthetics in its product line. Consequently, it dominates the\r\n digital-music market” is correct. “Apple emphasize the value of \r\naesthetics in their product line. They dominate the digital-music \r\nmarket” is <em>not</em>. (Also applies to generic terms “organization,” “team,” “group,” etc.)</p>\r\n</li>\r\n<li>\r\n<p>When referring to software elements or labels, always capitalize \r\nwords that are capitalized on screen. Put quotes around any multiword \r\nelement names that are lowercase on screen and would thus be hard to \r\ndistinguish from the rest of the text (e.g., Click “Don’t select object \r\nuntil rendered” only if necessary.)</p>\r\n</li>\r\n<li>\r\n<p>Use “between” for two items, “among” for three or more. Use “each other” for two, “one another” for three or more.</p>\r\n</li>\r\n <li>\r\n  <p>Use the American spellings of words when they differ.</p>\r\n  </li>\r\n<li>\r\n<p>Common foreign terms (such as “en masse”) are roman.</p>\r\n</li>\r\n<li>\r\n<p>Introduce unnumbered code blocks with colons.</p>\r\n</li>\r\n <li>\r\n<p>Do not stack admonitions, sidebars, or headings.</p>\r\n</li>\r\n <li>\r\n<p>Avoid obscenities and slurs, and obscure if included (grawlix, a two-em dash, etc.)</p>\r\n</li>\r\n</ul>\r\n \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect1\" id=\"typography_and_font_conventions\">\r\n<h1>Typography and Font Conventions</h1>\r\n\r\n\r\n<p>The following shows the basic font conventions used in O’Reilly \r\nbooks. Follow these links for detailed instructions for applying these \r\nstyles in <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#INLINES\">Asciidoc</a>, <a href=\"http://chimera.labs.oreilly.com/books/1234000000058/index.html:\">DocBook</a>, and <a href=\"http://oreillymedia.github.io/production-resources/word/#paragraph-character-styles\">Word</a>.</p>\r\n\r\n\r\n<div data-type=\"warning\">\r\n<p>If you want to use a font convention that is slightly different for \r\none of the following items, check with your editor first—some things can\r\n change; some can’t. For example, URLs will not be anything but <em>italic</em>,\r\n but you might come up with a different font convention for function \r\nnames or menu items. If you have a “new” element, please consult with \r\nyour editor about which font to use.</p>\r\n</div>\r\n\r\n<table>\r\n\r\n<thead>\r\n<tr>\r\n<th>Type of element</th>\r\n<th>Final result</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n<td><p>Filenames, file extensions (such as .jpeg), directory paths, and libraries.</p></td>\r\n<td><p><em>Body font italic</em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>URLs, URIs, email addresses, domain names</p></td>\r\n<td><p><em>Body font italic</em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Emphasized words (shouting!).</p><p>Please use italics rather than bold for emphasis.</p></td>\r\n<td><p><em>Body font italic</em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>First instance of a technical term</p></td>\r\n<td><p><em>Body font italic</em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Code blocks</p></td>\r\n<td><p><code>Constant width</code></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Registry keys</p></td>\r\n<td><p><code>Constant width</code></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Language and script elements: class names, types, namespaces, \r\nattributes, methods, variables, keywords, functions, modules, commands, \r\nproperties, parameters, values, objects, events, XML and HTML tags, and \r\nsimilar elements. Some examples include: <code>System.Web.UI</code>, a <code>while</code> loop, the <code>Socket</code> class, the <code>grep</code> command, and the <code>Obsolete</code> attribute.</p></td>\r\n<td><p><code>Constant width</code></p></td>\r\n</tr>\r\n<tr>\r\n <td><p>SQL commands (<code>SELECT</code>, <code>INSERT</code>, <code>ALTER</code> <code>TABLE</code>, <code>CREATE</code> <code>INDEX</code>, etc.)</p></td>\r\n <td><p><code>CONSTANT</code> <code>WIDTH</code> <code>CAPS</code></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Replaceable items (placeholder items in syntax); “username” in the following example is a placeholder:\r\n <code>login:</code> <em><code>username</code></em></p></td>\r\n<td><p><em><code>Constant width italic</code></em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Commands or text to be typed by the user</p></td>\r\n<td><p><strong><code>Constant width bold</code></strong></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Line annotations</p></td>\r\n<td><p><em>Body font italic</em> (but smaller)</p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Placeholders in paths, directories, URLs, or other text that would be italic anyway</p></td>\r\n<td><p><em><a href=\"http://www.&lt;yourname&gt;.com\"><em class=\"hyperlink\">http://www.&lt;yourname&gt;.com</em></a></em></p></td>\r\n</tr>\r\n<tr>\r\n<td><p>Keyboard accelerators (Ctrl, Shift, etc.), menu titles, menu options, menu buttons</p></td>\r\n<td><p>Body text</p></td>\r\n</tr>\r\n</tbody>\r\n</table>\r\n\r\n<p>These font conventions may vary slightly for each project; please \r\nconsult your editor, the production editor, or the freelance coordinator\r\n if you have any questions. <em>Please note:</em> Word authors should refer to the <a href=\"http://docs.atlas.oreilly.com/writing_in_asciidoc.html#INLINES\">Word Template Quickstart Guide</a>; DocBook authors should refer to our <a href=\"https://prod.oreilly.com/external/tools/docbook/docs/authoring/\">DocBook Authoring Guidelines</a> (username: guest; leave the password blank).</p>\r\n\r\n<p>It’s <em>very</em> important to follow tagging conventions for terms.\r\n The method for applying conventions will vary depending on the format: \r\nWord/OpenOffice, DocBook XML, or InDesign. Please consult with your \r\neditor or  <em>toolsreq@oreilly.com</em> for instructions specific to each environment.</p>\r\n\r\n<p>For Word copyediting, please do the following before submitting files\r\n for conversion: replace any tabs in code with the appropriate number of\r\n spaces (see earlier section, <a data-type=\"xref\" href=\"#code\">Code</a>);\r\n convert any remaining Word comments to tagged Comment paragraphs \r\nhighlighted in blue; search for any manual linebreaks (^l) and delete or\r\n replace with paragraph breaks as appropriate; and accept all changes \r\nand make sure filenames adhere to house style.</p>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect1\" id=\"cover-style\">\r\n<h1>O'Reilly Cover Style</h1>\r\n<p>Use <em>Chicago Manual of Style</em>, 17th Edition for anything not mentioned here.</p>\r\n\r\n<p>Bulleted lists on the back cover should begin with a capitalized word and end with <em>no</em> punctuation. Even if the list item is a complete sentence, it will not take a period.</p>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section>\r\n\r\n\r\n\r\n\r\n<section data-type=\"sect1\" id=\"word-list\">\r\n<h1>O’Reilly Word List</h1>\r\n\r\n<p>Alphabetical Word List: Default spellings</p>\r\n\r\n<a href=\"#wordlist-A\">A</a> | <a href=\"#wordlist-B\">B</a> | <a href=\"#wordlist-C\">C</a> | <a href=\"#wordlist-D\">D</a> | <a href=\"#wordlist-E\">E</a> | <a href=\"#wordlist-F\">F</a> | <a href=\"#wordlist-G\">G</a> | <a href=\"#wordlist-H\">H</a> | <a href=\"#wordlist-I\">I</a> | <a href=\"#wordlist-J\">J</a> | <a href=\"#wordlist-K\">K</a> | <a href=\"#wordlist-L\">L</a> | <a href=\"#wordlist-M\">M</a> | <a href=\"#wordlist-N\">N</a> | <a href=\"#wordlist-O\">O</a> | <a href=\"#wordlist-P\">P</a> | <a href=\"#wordlist-Q\">Q</a> | <a href=\"#wordlist-R\">R</a> | <a href=\"#wordlist-S\">S</a> | <a href=\"#wordlist-T\">T</a> | <a href=\"#wordlist-U\">U</a> | <a href=\"#wordlist-V\">V</a> | <a href=\"#wordlist-W\">W</a> | <a href=\"#wordlist-X\">X</a> | <a href=\"#wordlist-Y\">Y</a> | <a href=\"#wordlist-Z\">Z</a> |\r\n\r\n<h2 id=\"wordlist-A\">A</h2>\r\n\r\n<ul>\r\n <li>acknowledgments</li>\r\n <li>ActionScript</li>\r\n <li>ActiveX control</li>\r\n <li>Addison-Wesley</li>\r\n <li>ad hoc</li>\r\n <li>ADO.NET</li>\r\n <li>Agile (cap when referring to Agile software development or when used on its own as a noun)</li>\r\n <li>AI (no need to expand acronym to artificial intelligence)</li>\r\n <li>Ajax</li>\r\n <li>a.k.a. or aka (be consistent)</li>\r\n <li>a.m. or A.M.</li>\r\n <li>Alt key</li>\r\n <li>Alt-N</li>\r\n <li>anonymous FTP</li>\r\n <li>antipattern</li>\r\n <li>API (no need to expand acronym to application programming interface)</li>\r\n <li>appendixes</li>\r\n <li>applet (or Java applet)</li>\r\n <li>AppleScript</li>\r\n <li>AppleScript Studio (ASS)</li>\r\n <li>ARPAnet</li>\r\n <li>ASCII</li>\r\n <li>ASP.NET</li>\r\n <li>at sign</li>\r\n <li>autogenerate</li>\r\n <li>awk</li>\r\n</ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-B\">B</h2>\r\n\r\n<ul>\r\n<li>backend</li>\r\n <li>background processes</li>\r\n <li>backpressure</li>\r\n <li>backquote</li>\r\n <li>backslash</li>\r\n <li>Backspace key</li>\r\n <li>backtick</li>\r\n <li>backup (n); back up (v)</li>\r\n <li>backward</li>\r\n <li>backward compatible</li>\r\n <li>bash (avoid starting sentence with this word, but if unavoidable, cap as Bash)</li> \r\n <li>BeOS</li>\r\n <li>Berkeley Software Distribution (BSD)</li>\r\n <li>Berkeley Unix (older books may have UNIX)</li>\r\n <li>BHOs</li>\r\n <li>big data</li>\r\n <li>Big Design Up Front (BDUF)</li>\r\n <li>bioinformatics</li>\r\n <li><strong>Bitcoin (capitalize the concept/network/currency in general; lowercase specific units of currency)</strong></li>\r\n <li>bitmap</li>\r\n <li>bit mask</li>\r\n <li>Bitnet</li>\r\n <li>bit plane</li>\r\n <li>bitwise operators</li>\r\n <li>BlackBerry</li>\r\n <li>–black-box/white-box testing s/b avoided (alternatives: \r\nbehavioral/structural testing, closed/open testing, opaque/clear \r\ntesting)</li>\r\n <li>–black hat/white hat s/b avoided (alternatives: unethical/ethical, malicious/preventative)</li>\r\n <li>–blacklist/whitelist s/b avoided (alternatives: block list/allow list, deny/permit, excluded/included)</li>\r\n <li>Boolean (unless referring to a datatype in code, in which case s/b lowercase)</li>\r\n <li>Bourne-again shell (bash)</li>\r\n <li>Bourne shell</li>\r\n <li>braces or curly braces</li>\r\n <li>brackets or square brackets</li>\r\n <li>browsable</li>\r\n <li>_build-&gt;measure-&gt;learn_ cycle</li>\r\n <li>built-in (a, n)</li>\r\n <li>button bar</li>\r\n </ul>\r\n \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-C\">C</h2>\r\n<ul>\r\n <li>CacheStorage</li>\r\n <li>call-to-action</li>\r\n <li>Caps Lock key</li>\r\n <li>caret or circumflex</li>\r\n <li>CAT-5</li>\r\n <li>CD-ROM</li>\r\n <li>C language (n)</li>\r\n <li> C-language (a)</li>\r\n <li>checkbox</li>\r\n <li>checkmark</li>\r\n <li>check-in (n)</li>\r\n <li>classpath</li>\r\n <li>CLI (no need to expand acronym to command-line interface)</li>\r\n <li>click-through (a)</li>\r\n <li>client/server</li>\r\n <li>client side (n)</li>\r\n <li>client-side (a)</li>\r\n <li><strong>cloud native (n or a)</strong></li>\r\n <li>co-class</li>\r\n <li>coauthor</li>\r\n <li>codebase</li>\r\n <li>code set</li>\r\n <li>colorcell</li>\r\n <li>colormap</li>\r\n <li>Command key (Mac)</li>\r\n <li>command line (n)</li>\r\n <li>command-line (a)</li>\r\n <li>Common Object Request</li>\r\n <li>Broker Architecture (CORBA)</li>\r\n <li>compact disc</li>\r\n <li>compile time (n)</li>\r\n <li>compile-time (a)</li>\r\n <li>CompuServe</li>\r\n <li>Control key (Mac)</li>\r\n <li>copyleft</li>\r\n <li>copyright</li>\r\n <li>coworker</li>\r\n <li>CPU (no need to expand to central processing unit)</li>\r\n <li>–crazy s/b avoided (alternatives: foolish, bizarre, etc.)</li>\r\n <li>criterion (s), criteria (p)</li>\r\n <li>cross-reference</li>\r\n <li>C shell</li>\r\n <li>&lt;CR&gt;&lt;LF&gt;</li>\r\n <li>Ctrl key (Windows)</li>\r\n <li>curly braces or braces</li>\r\n <li>cybersecurity</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-D\">D</h2>\r\n<ul>\r\n <li>data block</li>\r\n <li>datacenter or data center (be consistent)</li>\r\n <li>Data Encryption Standard (DES)</li>\r\n <li>datafile</li>\r\n <li>datatype or data type (be consistent)</li>\r\n <li>data is</li>\r\n <li>dataset or data set (be consistent)</li>\r\n <li>DB-9</li>\r\n <li>Debian GNU/Linux</li>\r\n <li>decision making (n)</li>\r\n <li>decision-making (a)</li>\r\n <li>deep learning (n and a, no hyphen)</li>\r\n <li>de-identification (hyphenate)</li>\r\n <li>DevOps</li>\r\n <li>dial-up (a)</li>\r\n <li>dial up (v)</li>\r\n <li>disk</li>\r\n <li>disk-imaging software</li>\r\n <li>Delete key</li>\r\n <li>design time (n)</li>\r\n <li>design-time (a)</li>\r\n <li>DNS</li>\r\n <li>DocBook</li>\r\n <li>Document Object Model (DOM)</li>\r\n <li>Domain Name System</li>\r\n <li>dot</li>\r\n <li>dot-com</li>\r\n <li>double-click</li>\r\n <li>double-precision (a)</li>\r\n <li>double quotes</li>\r\n <li>down arrow</li>\r\n <li>downlevel (a)</li>\r\n <li>drag-and-drop (n)</li>\r\n <li>drag and drop (v)</li>\r\n <li>drop-down (a)</li>\r\n <li>–dummy s/b avoided (alternatives include: placeholder)</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"worldlist-E\">E</h2>\r\n<ul>\r\n<li>ebook</li>\r\n <li>ebusiness</li> \r\n <li>ecommerce</li> \r\n <li>eBay</li>\r\n <li>Emacs</li>\r\n <li>email</li>\r\n <li>empty-element tag</li>\r\n <li>end-of-file (EOF)</li>\r\n <li>end-tag</li>\r\n <li>end user (n); end-user (a)</li>\r\n <li>Engines of Groth</li>\r\n <li>Enter key</li>\r\n <li>equals sign</li>\r\n <li>ereader</li>\r\n <li>Escape key (or Esc key)</li>\r\n <li>et al.</li>\r\n <li>Ethernet</li>\r\n <li>exclamation mark</li>\r\n <li>Exim</li>\r\n        </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-F\">F</h2>\r\n<ul>\r\n <li>failback</li>\r\n <li>failover</li>\r\n <li>fax</li>\r\n <li>file manager</li>\r\n <li>filename</li>\r\n <li><strong>filepath</strong></li>\r\n <li>file server</li>\r\n <li>filesystem</li>\r\n <li>file type</li>\r\n <li>FireWire</li>\r\n <li>foreground</li>\r\n <li>FORTRAN</li>\r\n <li>Fortran 90</li>\r\n <li>forward (adv)</li>\r\n <li>frame type</li>\r\n <li>FreeBSD</li>\r\n <li>Free Documentation License (FDL)</li>\r\n <li>Free Software Foundation (FSF)</li>\r\n <li>frontend</li>\r\n <li>_ftp_ (Unix command)</li>\r\n <li>FTP (protocol)</li>\r\n <li>FTP site</li>\r\n <li>full stack (Full Stack in headings), no hyphen, even if adjective</li>\r\n        </ul>\r\n    \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-G\">G</h2>\r\n<ul>\r\n <li>gateway</li>\r\n <li>Gb (gigabit)</li>\r\n <li>GB (gigabyte)</li>\r\n <li>GBps (gigabytes per second)</li>\r\n <li>GHz</li>\r\n <li>gid</li>\r\n <li>GIMP</li>\r\n <li><strong>Git</strong></li>\r\n <li><strong>GitHub</strong></li>\r\n <li>GNOME</li>\r\n <li>GNU Emacs</li>\r\n <li>GNU Public License (GPL)</li>\r\n <li>GNUstep</li>\r\n <li>Google PageRank</li>\r\n <li>grayscale</li>\r\n <li>greater-than sign or &gt;</li>\r\n <li>greenlight (v)</li>\r\n <li>GUI, GUIs</li>\r\n </ul>\r\n       \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-H\">H</h2>\r\n<ul>\r\n <li>handcode</li>\r\n <li>handoff (n)</li>\r\n <li>hardcode (v)</li>\r\n <li>hardcore</li>\r\n <li>hardcopy</li>\r\n <li>hard link</li>\r\n <li>hardware-in-the-loop</li>\r\n <li>hash sign or sharp sign</li>\r\n <li>high-level (a)</li>\r\n <li>home page</li>\r\n <li>hostname</li>\r\n <li>hotspot</li>\r\n <li>HTML</li>\r\n <li>HTTP</li>\r\n <li>hypertext</li>\r\n        </ul>\r\n        \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-I\">I</h2>\r\n<ul>\r\n <li>IDs</li>\r\n <li>IDE</li>\r\n <li>IndexedDB</li>\r\n <li>infrastructure as a service (IaaS)</li>\r\n <li>inline</li>\r\n <li>inode</li>\r\n <li>interclient</li>\r\n <li>internet, the internet</li>\r\n <li>Internet of Things (IoT)</li>\r\n <li>internetwork</li>\r\n <li>intranet</li>\r\n <li>Intrinsics</li>\r\n <li>I/O</li>\r\n <li>IP (Internet Protocol)</li>\r\n <li>IPsec</li>\r\n <li>ISO</li>\r\n <li>ISP</li>\r\n </ul>\r\n \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-J\">J</h2>\r\n\r\n<ul>\r\n <li>Jabber</li>\r\n <li>Jabber client</li>\r\n <li>Jabber server</li>\r\n <li>Jabber applet</li>\r\n <li>JAR archive</li>\r\n <li>JAR file</li>\r\n <li>JavaScript</li>\r\n <li>JPEG</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-K\">K</h2>\r\n<ul>\r\n <li>K Desktop Environment (KDE)</li>\r\n <li>Kb (kilobit)</li>\r\n <li>KB (kilobyte) (denotes file size or disk space)</li>\r\n <li>Kbps (kilobits per second)</li>\r\n <li>Kerberos</li>\r\n <li>keepalive (n or a)</li>\r\n <li>keyclick</li>\r\n <li>keycode</li>\r\n <li>keymaps</li>\r\n <li>keypad</li>\r\n <li>keystroke</li>\r\n <li>keysym</li>\r\n <li>keywords</li>\r\n <li>key performance indicators (KPIs)</li>\r\n <li>kHz (kilohertz)</li>\r\n <li>–kill s/b avoided (alternatives: end, exit, cancel)</li>\r\n <li>Korn shell</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-L\">L</h2>\r\n<ul>\r\n <li>lambda (lc unless referring to a product)</li>\r\n <li>Lean (capitalize noun or adjective when referring to Lean business methodology)</li>\r\n <li>local area network or LAN</li>\r\n <li>left angle bracket or &lt;</li>\r\n <li>lefthand (a)</li>\r\n <li>leftmost</li>\r\n <li>less-than sign or &lt;</li>\r\n <li>leveled (not levelled)</li>\r\n <li><strong>life cycle or lifecycle (be consistent)</strong></li>\r\n <li>line-feed (a)</li>\r\n <li>line feed (n)</li>\r\n <li>Linux</li>\r\n <li>LinuxPPC</li>\r\n <li>listbox</li>\r\n <li>logfile</li>\r\n <li>login, logout, or logon (n or a)</li>\r\n <li>log in, log out, or log on (v)</li>\r\n <li>lower-level (a)</li>\r\n <li>lower-right (a)</li>\r\n <li>Linux Professional Institute (LPI)</li>\r\n</ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-M\">M</h2>\r\n<ul>\r\n <li><strong>Mac (or MacBook)</strong></li>\r\n <li>macOS (replaces Mac OS X)</li>\r\n <li><strong>machine learning (n and a, no hyphen)</strong></li>\r\n <li>mail-handling (adjective)</li>\r\n <li>– man hours s/b avoided (alternatives: work hours, employee hours)</li>\r\n <li>manpage</li>\r\n <li>markup</li>\r\n <li>–master/slave (n, a) s/b avoided (alternatives: parent/child, leader/follower, primary/secondary)</li>\r\n <li>Mb (megabit)</li>\r\n <li>MB (megabyte)</li>\r\n <li>MBps (megabytes per second)</li>\r\n <li>McGraw-Hill</li>\r\n <li>menu bar</li>\r\n <li>metacharacter</li>\r\n <li>Meta key</li>\r\n <li>Meta-N</li>\r\n <li>MHz (megahertz)</li>\r\n <li>mice or mouses (be consistent)</li>\r\n <li>microservices</li>\r\n <li>Microsoft Windows</li>\r\n <li>Microsoft Windows Me</li>\r\n <li>Microsoft Windows NT</li>\r\n <li>Microsoft Windows XP</li>\r\n <li>Microsoft Windows 2000</li>\r\n <li>–middleman s/b avoided (alternatives: go-between, link, etc.)</li>\r\n <li>MIDlet</li>\r\n <li>MKS Toolkit</li>\r\n <li>model-in-the-loop</li>\r\n <li>MS-DOS</li>\r\n <li>multiline </li>\r\n <li>Multi-Touch (when referring to Apple's trademark)</li>\r\n <li>My Services</li>\r\n <li>MySpace</li>\r\n </ul>\r\n      \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-N\">N</h2>\r\n<ul>\r\n <li>nameserver</li>\r\n <li>name service</li>\r\n <li>namespace</li>\r\n <li>the Net</li>\r\n <li>.NET</li>\r\n <li>NetBIOS</li>\r\n <li>NetBSD</li>\r\n <li>NetInfo</li>\r\n <li>newline</li>\r\n <li>newsgroups</li>\r\n <li>NeXTSTEP</li>\r\n <li>NGINX (company), nginx (server)</li>\r\n <li>NOOP</li>\r\n <li>nonlocal</li>\r\n <li>NoSQL</li>\r\n <li>Novell NetWare</li>\r\n <li>the <em>New York Times</em></li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-O\">O</h2>\r\n<ul>\r\n <li>Objective-C</li>\r\n <li>object linking and embedding (OLE)</li>\r\n <li>object-oriented programming (OOP)</li>\r\n <li>object request broker (ORB)</li>\r\n <li>OK</li>\r\n <li>offline</li>\r\n <li>offload</li>\r\n <li>online</li>\r\n <li>on premises (prep. phrase) on-premises (modifier); may be abbreviated to on prem/on-prem</li>\r\n <li>open source (n or a, rewrite to avoid using in a verb form)</li>\r\n <li>open source software (OSS)</li>\r\n <li>OpenBSD</li>\r\n <li>OpenMotif</li>\r\n <li>OpenStep</li>\r\n <li>OpenWindows</li>\r\n <li>Option key (Mac)</li>\r\n <li>Oracle7</li>\r\n <li>Oracle8</li>\r\n <li>Oracle 8.0</li>\r\n <li>Oracle 8<em>i</em> (italic “i”)</li>\r\n <li>Oracle 9<em>i</em> (italic “i”)</li>\r\n <li>Oracle Parallel Query Option</li>\r\n <li>O’Reilly Media, Inc.\r\n  <ul>\r\n  <li><strong>O’Reilly’s platform s/b \"the O’Reilly platform\" or \"the O’Reilly learning platform\" and then \"O’Reilly\" on subsequent mentions.</strong></li>\r\n  </ul>\r\n </li>\r\n <li>OS/2</li>\r\n <li>OSA</li>\r\n <li>OSF/Motif</li>\r\n <li>OS X</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-P\">P</h2>\r\n<ul>\r\n <li>packet switch networks</li>\r\n <li>Paint Shop Pro</li>\r\n <li>pagefile</li>\r\n <li>page rank (but Google PageRank)</li>\r\n <li>parentheses (p)</li>\r\n <li>parenthesis (s)</li>\r\n <li>Pascal</li>\r\n <li>pathname</li>\r\n <li>pattern-matching (a)</li>\r\n <li>peer-to-peer (or P2P)</li>\r\n <li>% (not percent)</li>\r\n <li>performant (Oracle)</li>\r\n <li>period</li>\r\n <li>Perl</li>\r\n <li>Perl DBI</li>\r\n <li>plain text (n)</li>\r\n <li>plain-text (a)</li>\r\n <li>platform as a service (PaaS)</li>\r\n <li>Plug and Play (PnP)</li>\r\n <li>plug in (v)</li>\r\n <li>plug-in (a, n)</li>\r\n <li>p.m. or P.M.</li>\r\n <li>Point-to-Point Protocol (PPP)</li>\r\n <li>pop up (v)</li>\r\n <li>pop-up (n, a)</li>\r\n <li>POP-3</li>\r\n <li>Portable Document Format (PDF)</li>\r\n <li>Portable Network Graphics (PNG)</li>\r\n <li>Portable Operating\r\n  <ul>\r\n   <li>System Interface (POSIX)</li>\r\n  </ul>\r\n </li>\r\n <li>POSIX-compliant</li>\r\n <li>Post Office Protocol (POP)</li>\r\n <li>postprocess</li>\r\n <li>PostScript</li>\r\n <li>Prentice Hall</li>\r\n <li>process ID</li>\r\n <li>progress bar</li>\r\n <li>pseudoattribute</li>\r\n <li>pseudo-tty</li>\r\n <li>public key (n)</li>\r\n <li>public-key (a)</li>\r\n <li>publish/subscribe or pub/sub</li>\r\n <li>pull-down (a)</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-Q\">Q</h2>\r\n<ul>\r\n <li>qmail</li>\r\n <li>Qt</li>\r\n <li>QuarkXPress</li>\r\n <li>Quartz</li>\r\n <li>Quartz Extreme</li>\r\n <li>QuickTime</li>\r\n <li>quotation marks (spell out first time; it can be “quotes” thereafter)</li>\r\n </ul>\r\n \r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-R\">R</h2>\r\n<ul>\r\n <li>random-access (a)</li>\r\n <li>RCS</li>\r\n <li>read-only (a)</li>\r\n <li>read/write</li>\r\n <li>real time (n)</li>\r\n <li>real-time (a)</li>\r\n <li>re-create</li>\r\n <li>Red Hat Linux</li>\r\n <li>Red Hat Package Manager (RPM)</li>\r\n <li>redirection</li>\r\n <li>reference page or manpage</li>\r\n <li>re-identification (hyphenate)</li>\r\n <li>remote-access server</li>\r\n <li>Rendezvous (<em>Mac OS X zeroconf networking</em>)</li>\r\n <li>Return (key)</li>\r\n <li>RFC 822</li>\r\n <li>rich text (n)</li>\r\n <li>rich-text (a)</li>\r\n <li>right angle bracket or greater-than sign (&gt;)</li>\r\n <li>right-click</li>\r\n <li>righthand (a)</li>\r\n <li>rmail</li>\r\n <li><strong>road map or roadmap (be consistent)</strong></li>\r\n <li>rollback (n); roll back (v)</li>\r\n <li>rollout (n); roll out (v)</li>\r\n <li>rootkit</li>\r\n <li>Rubout key</li>\r\n <li>rulebase</li>\r\n <li>ruleset</li>\r\n <li>runtime (n, a)</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-S\">S</h2>\r\n<ul>\r\n <li>Samba</li>\r\n <li>saveset</li>\r\n <li>screen dump</li>\r\n <li>screenful</li>\r\n <li>screensaver</li>\r\n <li>scroll bar</li>\r\n <li>securelevel (in Linux)</li>\r\n <li>Secure Shell (SSH)</li>\r\n <li>Secure Sockets Layer (SSL)</li>\r\n <li>sed scripts</li>\r\n <li>server-dependent</li>\r\n <li>server side (n)</li>\r\n <li>server-side (a)</li>\r\n <li>service worker</li>\r\n <li>servlet</li>\r\n <li>set up (v)</li>\r\n <li>setup (n)</li>\r\n <li>SGML</li>\r\n <li>sharp sign or hash sign</li>\r\n <li>shell (lowercase even in shell name: Bourne shell)</li>\r\n <li>shell scripts</li>\r\n <li>Shift key</li>\r\n <li>Simple API for XML (SAX)</li>\r\n <li>single-precision (a)</li>\r\n <li>single quote</li>\r\n <li>site map</li>\r\n <li>–slave/master (n, a) s/b avoided (alternatives: child/parent, follower/leader, secondary/primary)</li>\r\n <li>Smalltalk</li>\r\n <li>SMP (a, n)</li>\r\n <li>SOAP</li>\r\n <li>Social Security number (SSN)</li>\r\n <li>software as a service (SaaS)</li>\r\n <li>software-in-the-loop</li>\r\n <li>source code</li>\r\n <li>space bar</li>\r\n <li>spam (not SPAM)</li>\r\n <li>spellcheck</li>\r\n <li>spellchecker</li>\r\n <li>split screen</li>\r\n <li>square brackets or brackets</li>\r\n <li>standalone</li>\r\n <li>standard input (stdin)</li>\r\n <li>standard output (stdout)</li>\r\n <li>start tag</li>\r\n <li>startup file</li>\r\n <li>stateful</li>\r\n <li>stateless</li>\r\n <li>status bar</li>\r\n <li>stylesheet</li>\r\n <li>subprocess</li>\r\n <li>SUSE Linux</li>\r\n <li>swapfile</li>\r\n <li>swapspace</li>\r\n <li>sync</li>\r\n <li>system administrator</li>\r\n <li>system-wide</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-T\">T</h2>\r\n<ul>\r\n <li>10-baseT</li>\r\n <li>T1</li>\r\n <li>t-shirt</li>\r\n <li>Tab key</li>\r\n <li>TAR file</li>\r\n <li>TCP/IP</li>\r\n <li>Telnet (the protocol)</li>\r\n <li>telnet (v)</li>\r\n <li>terabyte</li>\r\n <li>T<subscript>E</subscript>X</li>\r\n <li>texinfo</li>\r\n <li>text box</li>\r\n <li>text-input mode</li>\r\n <li>thread pooling (n)</li>\r\n <li><strong>timeout (in tech/computing contexts)</strong></li>\r\n <li>time-sharing processes</li>\r\n <li>timestamp</li>\r\n <li>time zone</li>\r\n <li>title bar</li>\r\n <li>Token Ring</li>\r\n <li>toolbar</li>\r\n <li><strong>toolchain</strong></li>\r\n <li>toolkit</li>\r\n <li>tool tip</li>\r\n <li>top-level (a)</li>\r\n <li>toward</li>\r\n <li>trade-off</li>\r\n <li>– tribe s/b avoided (alternatives: company, institution, network, community)</li>\r\n <li>tweet, retweet, live-tweet v, n (avoid “tweet out”)</li>\r\n <li>Twitter user (preferred to \"tweeter\")</li>\r\n <li>Twitterstorm, tweetstorm</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-U\">U</h2>\r\n<ul>\r\n <li>UI (no need to expand to user interface)</li>\r\n <li>UK (for United Kingdom)</li>\r\n <li>Ultrix</li>\r\n <li>Universal Serial Bus (USB)</li>\r\n <li>Unix (UNIX in many books, esp. older ones)</li>\r\n <li>up arrow</li>\r\n <li>upper- and lowercase</li>\r\n <li>uppercase</li>\r\n <li>upper-left corner</li>\r\n <li>UPSs</li>\r\n <li>up-to-date</li>\r\n <li>URLs</li>\r\n <li>US (for United States)</li>\r\n <li>Usenet</li>\r\n <li>user ID (n)</li>\r\n <li>user-ID (a)</li>\r\n <li>username</li>\r\n <li>UX (no need to expand to user experience)</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-V\">V</h2>\r\n<ul>\r\n <li>v2 or version 2</li>\r\n <li>VAX/VMS</li>\r\n <li>VB.NET</li>\r\n <li>versus (avoid vs.)</li>\r\n <li>vice versa</li>\r\n <li>VoiceXML</li>\r\n <li>Visual Basic .NET</li>\r\n <li>Visual Basic 6 or VB 6</li>\r\n <li>Visual C++ .NET</li>\r\n <li>Visual Studio .NET</li>\r\n <li>VS.NET</li>\r\n <li>Volume One</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-W\">W</h2>\r\n<ul>\r\n <li>the <em>Wall Street Journal</em></li>\r\n <li>the web (n)</li>\r\n <li>web (a)</li>\r\n <li>web client</li>\r\n <li>webmaster</li>\r\n <li>web page</li>\r\n <li>web server</li>\r\n <li>web services (unless preceded by a proper noun, as in Microsoft Web Services)</li>\r\n <li>website</li>\r\n <li>–white-box testing s/b avoided (alternatives: structural/behavioral testing open/closed testing, clear/opaque testing)</li>\r\n <li>–white hat/black hat s/b avoided (alternatives: ethical/unethical, preventative/malicious)</li>\r\n <li>white pages</li>\r\n <li>–whitelist/blacklist s/b avoided (alternatives: allow list/block list, permit/deny, included/excluded)</li>\r\n <li>whitepaper (I printed my whitepaper on white paper.)</li>\r\n <li>whitespace</li>\r\n <li>wide area network or WAN</li>\r\n <li>WiFi</li>\r\n <li>wiki</li>\r\n <li>wildcard</li>\r\n <li>Windows 95</li>\r\n <li>Windows 98</li>\r\n <li>Windows 2000</li>\r\n <li>Windows NT</li>\r\n <li>Windows Vista</li>\r\n <li>Windows XP</li>\r\n <li>Wizard (proper noun)</li>\r\n <li>wizard (a, n)</li>\r\n <li>workaround</li>\r\n <li>workbench</li>\r\n <li>workgroup</li>\r\n <li>workstation</li>\r\n <li>World Wide Web (WWW)</li>\r\n <li>wraparound</li>\r\n <li>writable</li>\r\n <li>write-only (a)</li>\r\n <li>WYSIWYG</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-X\">X</h2>\r\n<ul>\r\n <li>(x,y) (no space)</li>\r\n <li>x-axis</li>\r\n <li>Xbox</li>\r\n <li>X client</li>\r\n <li><em>x</em> coordinate</li>\r\n <li>X protocol</li>\r\n <li>X server</li>\r\n <li>X Toolkit</li>\r\n <li>XView</li>\r\n <li>X Window series</li>\r\n <li>X Window System</li>\r\n <li>x86</li>\r\n <li>xFree86</li>\r\n <li>XHTML</li>\r\n <li>XLink</li>\r\n <li>XML</li>\r\n <li>XML Query Language (XQuery)</li>\r\n <li>XML-RPC</li>\r\n <li>XPath</li>\r\n <li>XPointer</li>\r\n <li>XSL</li>\r\n <li>XSLT</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-Y\">Y</h2>\r\n<ul>\r\n <li>Yahoo!</li>\r\n <li>y-axis</li>\r\n <li><em>y</em> coordinate</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n\r\n<h2 id=\"wordlist-Z\">Z</h2>\r\n<ul>\r\n <li>Zeroconf (short for “Zero Configuration”)</li>\r\n <li>zeros</li>\r\n <li>zip code</li>\r\n <li>zip (v)</li>\r\n <li>ZIP file</li>\r\n </ul>\r\n\r\n<p><a href=\"#getting_started\">back to top</a></p>\r\n</section></section>\r\n\r\n\t\t\t\r\n\t\t\t</div><!-- /.container -->\r\n\t\t\t<footer>\r\n\t    \t\r\n\t\t\t</footer>\r\n\t\t\r\n\t\r\n</body></html>"
  },
  {
    "path": "docs/ORM_style_guide_files/main.css",
    "content": "\nbody {\n    font-family: \"Helvetica Neue\", Helvetica, Arial, Sans-Serif;\n    font-size: 14px;\n    color: #333333;\n    margin: 60px auto;\n    width: 70%;\n}\n\n.hyperlink {\n    word-break: break-all;\n}\n\nnav ul, footer ul {\n    font-family:'Helvetica', 'Arial', 'Sans-Serif';\n    padding: 0;\n    list-style: none;\n    font-weight: bold;\n}\nnav ul li, footer ul li {\n    display: inline;\n    margin-right: 20px;\n}\na {\n    text-decoration: none;\n    color: #003399;\n}\na:hover {\n    text-decoration: underline;\n}\nh1 {\n    font-size: 24pt;\n}\np {\n    line-height: 1.4em;\n    color: #333;\n}\nfooter {\n    border-top: 1px solid #d5d5d5;\n    font-size: .8em;\n}\n\nul.posts { \n    margin: 20px auto 40px; \n    font-size: 1.5em;\n}\n\nul.posts li,\n.no-bullets {\n    list-style: none;\n    font-size: 14px;\n    margin-left: 0;\n    padding-left: 0;\n}\n\n/* Lists */\n\ndt {\n    padding: 1em 0 .5em .75em;\n}\n\nul {\n    list-style-type: square;\n}\n\nul, ol {\n    line-height: 1.4em;\n}\n\n/* Tables */\n\ntable {\n  margin: 12.5pt 0;\n  border-collapse: collapse;\n  max-width: 100%;\n  hyphens: none;\n}\n\ntable caption {\n  font-style: italic;\n  margin-bottom: 4pt;\n  display: table-caption;\n  text-align: left;\n  }\n\n\ntd, th { \n  padding: 0.2em 0.4em 0.2em 0.4em;\n  vertical-align: top;\n  text-align: left;\n  word-spacing: 0.3pt;\n}\n  \nthead tr th {\n  font-weight: 600;\n}\n\n/*lists within tables*/\ntable ul li,\ntable ol li{\n margin: 0 0 0 8pt;\n}\n\ntable, th, td {\n   border: 1px solid black;\n}\n\nth {\n    background-color: #C8C8C8;\n    color: black;\n}\n\ndiv[data-type=\"warning\"] {\n background: #ffe9ec;\n}\n\ndiv[data-type=\"note\"], div[data-type=\"warning\"], div[data-type=\"tip\"] {\n  border: solid 1pt black;\n  padding-left: 2em;\n  padding-right: 2em;\n}\n\n\n/* Headers */\n\nh2 {\n    font-size: 20pt;\n    text-align: center;\n    border-bottom: 1px solid #009999;\n    color: #009999;\n}\n\nh3 {\n    font-size: 16pt;\n    color: #009999;\n    padding-top: 10px;\n}\n\nh4 {\n    font-size: 12pt;\n    color: #009999;\n}\n\n\n\n\n/* ORM Word Template Quickstart Guide */\n\n.no-bullets li {\n    background-image: url('../illustrations/download.svg');\n    background-repeat: no-repeat;\n    display: block;\n    min-height: 40px;\n    background-size: 30px;\n    padding: 0 0 0 50px;\n    margin-right: 50px;\n    margin-top: 20px;\n\n}\n\n\n\n\n/* Styled Admonitions */\n\ndiv[data-type=\"note\"] {\n    border: 1px solid #86aac8;\n    font-size: 10pt;\n    padding: 1.5em;\n    color: #86aac8;\n    padding-bottom: 5px;\n}\n\ndiv[data-type=\"warning\"] {\n    border: 1px solid #ac2e3d;\n    font-size: 12pt;\n    padding: 1.5em;\n    color: #ac2e3d;\n    padding-bottom: 5px;\n}\n\ndiv[data-type=\"note\"] h6 {\n    background-image: url('../illustrations/note.png');\n    background-size:40px;\n    padding-top: .5em;\n    padding-bottom: .5em;\n    background-repeat: no-repeat;\n    display: block;\n    min-height: 20px;\n    font-size: 1.5em;\n    padding-left: 50px;\n    margin: 0 0 0 0;\n}\n\ndiv[data-type=\"warning\"] h6 {\n    background-image: url('../illustrations/warning.png');\n    background-size:40px;\n    padding-top: .5em;\n    padding-bottom: .5em;\n    background-repeat: no-repeat;\n    display: block;\n    min-height: 20px;\n    font-size: 1.5em;\n    padding-left: 60px;\n    margin: 0 0 0 0;\n\n}\n\n\n\n/* Responsive */\n\n@media only screen and (min-width: 800px) {\nbody {\n    font-size: 14pt;\n}\n\n/* Headers */\n\nh2 {\n    font-size: 24pt;\n    text-align: center;\n    border-bottom: 1px solid #009999;\n    color: #009999;\n}\n\nh3 {\n    font-size: 20pt;\n    color: #009999;\n    padding-top: 10px;\n}\n\nh4 {\n    font-size: 16pt;\n    color: #009999;\n}\n\n.no-bullets li  {\n    float: left;\n    margin: 0 0 35px 3px;\n    padding: 15px 20px 0 35px;\n}\n\n#table-of-contents-word {\n    clear: left;\n    \n}\n\n.no-bullets {\n    padding: 0;\n    margin: 0;\n}\n\ndiv[data-type=\"note\"],\ndiv[data-type=\"warning\"] {\n    font-size: 12pt;\n}\n\nul.no-bullets {\n    display: inline-block;\n}\n\n.center {\n    width: 100%;\n    text-align: center;\n}\n\n}\n\n@media only screen and (min-width: 950px) {\n.no-bullets li  {\n    float: left;\n    margin: 0 0 35px 5px;\n    padding: 15px 50px 0px 35px;\n}\n\n}\n\n@media only screen and (min-width: 1000px) {\n\n#table-of-contents-word+ul {\n    width: 375px;\n    margin: 0 auto;\n}\n\n}\n"
  },
  {
    "path": "docs/asciidoc-cheatsheet.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\"><head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n<meta name=\"generator\" content=\"AsciiDoc 8.6.7\">\n<title>AsciiDoc cheatsheet</title>\n<link rel=\"stylesheet\" href=\"asciidoc-cheatsheet_files/asciidoc.css\" type=\"text/css\">\n<link rel=\"stylesheet\" href=\"asciidoc-cheatsheet_files/pygments.css\" type=\"text/css\">\n<script type=\"text/javascript\" src=\"asciidoc-cheatsheet_files/asciidoc.js\"></script>\n<script type=\"text/javascript\" src=\"asciidoc-cheatsheet_files/asciidoc.asc\"></script>\n<script type=\"text/javascript\">\n/*<![CDATA[*/\nasciidoc.install(2);\n/*]]>*/\n</script>\n<link href=\"asciidoc-cheatsheet_files/Content.css\" type=\"text/css\" rel=\"stylesheet\"></head>\n<body class=\"article\">\n<style type=\"text/css\">\nhtml { height: 100%; margin-bottom: 1px; } /* force vertical scrollbar */\nbody { width: 800px; margin: 0 auto 20px auto; padding: 150px 0 20px 0; }\nbody { background-color: white; }\n#powerman {\n    position: absolute; top:  15px;   width: 800px; height: 150px;\n    background: url(\"/images/logo_bg.png\") left 60px repeat-x;\n    font-family: Comic Sans MS, cursive;\n    color: #8b7f7f;\n}\n#powerman-header {\n    position: absolute; top:  20px; left: 200px;\n    font-size: 45px;\n    font-weight: normal;\n    letter-spacing: 0.1em;\n}\n#powerman-epigraph {\n    font-size: 15px;\n    text-align: right;\n}\n#powerman-logo {\n    position: absolute; top:   0;   left:  11px; width: 114px;\n    border: 2px solid #8b7f7f;\n    padding: 114px 0 0 0;\n    overflow: hidden;\n    background: url(\"/images/logo.jpg\") no-repeat;\n    height: 0px !important; /* for most browsers */\n    height /**/:114px;      /* for IE5.5's bad box model */\n}\n</style>\n<div id=\"powerman\">\n    <div id=\"powerman-header\">POWERMAN</div>\n    <div id=\"powerman-epigraph\">\"In each of us sleeps a genius...<br>\n        and his sleep gets deeper everyday.\"</div>\n    <a id=\"powerman-logo\" href=\"http://powerman.name/\">Home</a>\n</div>\n\n\n\n<div id=\"header\">\n<h1>AsciiDoc cheatsheet</h1>\n<span id=\"author\">Alex Efros</span><br>\n<span id=\"email\"><tt>&lt;<a href=\"mailto:powerman@powerman.name\">powerman@powerman.name</a>&gt;</tt></span><br>\n<span id=\"revnumber\">version 2.2.2</span>\n<div id=\"toc\">\n  <div id=\"toctitle\">Table of Contents</div>\n  <noscript><p><b>JavaScript must be enabled in your browser to display the table of contents.</b></p></noscript>\n</div>\n</div>\n<div id=\"content\">\n<div id=\"preamble\">\n<div class=\"sectionbody\">\n<style>\ntable.cs div.literalblock,\ntable.cs div.admonitionblock {\n    margin:0;\n}\ntable.cs { width: 100%; }\ntable.cs td.col1 { width: 50%; padding: 1% 0; }\ntable.cs td.col2 { width: 50%; padding:1% 0 1% 1%; border-left:solid,2px,#ddd; }\ntable.cs tr.even { background-color: #FAFAFA; }\n</style>\n<script src=\"asciidoc-cheatsheet_files/jquery-1.js\" type=\"text/javascript\"></script>\n<script type=\"text/javascript\">\nvar toc = window.onload;\nwindow.onload = null;\n$(document).ready(function() {\n    toc();\n    $(\"div.toclevel1:has('a:contains(\\'Level \\')')\").remove();\n    $(\"div.toclevel2:has('a:contains(\\'Level \\')')\").remove();\n});\n</script>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_abstract\">Abstract</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>This is a cheatsheet for <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc</a> -\n“Text based document generation” script. The cheatsheet available for\ndifferent AsciiDoc versions (because of some markup syntax changes) and\nusing different css styles. Here is list with <a href=\"http://powerman.name/doc/asciidoc-index\">all\navailable cheatsheets for different AsciiDoc version and using different\ncss styles</a>.</p></div>\n<div class=\"paragraph\"><p>This cheatsheet is for AsciiDoc <strong>8.6.7</strong>, using <strong>default css</strong>.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_document_header\">Document header</h2>\n<div class=\"sectionbody\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Main Header\n===========\nOptional Author Name &lt;optional@author.email&gt;\nOptional version, optional date\n:Author:    AlternativeWayToSetOptional Author Name\n:Email:     &lt;AlternativeWayToSetOptional@author.email&gt;\n:Date:      AlternativeWayToSetOptional date\n:Revision:  AlternativeWayToSetOptional version</tt></pre>\n</div></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_attributes\">Attributes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>There a lot of predefined attributes in AsciiDoc, plus you can add your own.\nTo get attribute value use {attributename} syntax.</p></div>\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Author is {author}\n\nVersion is {revision}</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Author is Alex Efros</p></div>\n<div class=\"paragraph\"><p>Version is 2.2.2</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>:My name: Alex Efros\nMy name is {myname}</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>My name is Alex Efros</p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Line\nwith bad attribute {qwe} will be\ndeleted</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Line\ndeleted</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Escaped: \\{qwe} and +++{qwe}+++</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Escaped: {qwe} and {qwe}</p></div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_headers\">Headers</h2>\n<div class=\"sectionbody\">\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Level 1\n-------\nText.\n\nLevel 2\n~~~~~~~\nText.\n\nLevel 3\n^^^^^^^\nText.\n\nLevel 4\n+++++++\nText.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n\n\n<div class=\"sect1\">\n<h2 id=\"_level_1\">Level 1</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_level_2\">Level 2</h3>\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect3\">\n<h4 id=\"_level_3\">Level 3</h4>\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect4\">\n<h5 id=\"_level_4\">Level 4</h5>\n<div class=\"paragraph\"><p>Text.</p></div>\n</div></div></div></div></div></td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>== Level 1\nText.\n\n=== Level 2\nText.\n\n==== Level 3\nText.\n\n===== Level 4\nText.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n\n\n\n\n\n<div class=\"sect1\">\n<h2 id=\"_level_1_2\">Level 1</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_level_2_2\">Level 2</h3>\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect3\">\n<h4 id=\"_level_3_2\">Level 3</h4>\n<div class=\"paragraph\"><p>Text.</p></div>\n<div class=\"sect4\">\n<h5 id=\"_level_4_2\">Level 4</h5>\n<div class=\"paragraph\"><p>Text.</p></div>\n</div></div></div></div></div></td></tr>\n</tbody></table>\n</div>\n</div>\n</div>\n\n\n<div class=\"sect1\">\n<h2 id=\"_paragraphs\">Paragraphs</h2>\n<div class=\"sectionbody\">\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n\nUsual\nparagraph.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><div class=\"title\">Optional Title</div><p>Usual\nparagraph.</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n\n Literal paragraph.\n  Must be indented.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"literalblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\">\n<pre><tt>Literal paragraph.\n Must be indented.</tt></pre>\n</div></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n\n[source,perl]\ndie 'connect: '.$dbh-&gt;errstr;\n\nNot a code in next paragraph.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"listingblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\"><div class=\"highlight\"><pre><span class=\"nb\">die</span> <span class=\"s\">'connect: '</span><span class=\"o\">.</span><span class=\"nv\">$dbh</span><span class=\"o\">-&gt;</span><span class=\"n\">errstr</span><span class=\"p\">;</span>\n</pre></div></div></div>\n<div class=\"paragraph\"><p>Not a code in next paragraph.</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\nNOTE: This is an example\n      single-paragraph note.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"title\">Optional Title</div>This is an example\n      single-paragraph note.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n[NOTE]\nThis is an example\nsingle-paragraph note.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"title\">Optional Title</div>This is an example\nsingle-paragraph note.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>TIP: Tip.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">Tip.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>IMPORTANT: Important.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/important.png\" alt=\"Important\">\n</td>\n<td class=\"content\">Important.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>WARNING: Warning.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/warning.png\" alt=\"Warning\">\n</td>\n<td class=\"content\">Warning.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>CAUTION: Caution.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/caution.png\" alt=\"Caution\">\n</td>\n<td class=\"content\">Caution.</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_blocks\">Blocks</h2>\n<div class=\"sectionbody\">\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n----\n*Listing* Block\n\nUse: code or file listings\n----</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"listingblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\">\n<pre><tt>*Listing* Block\n\nUse: code or file listings</tt></pre>\n</div></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n[source,perl]\n----\n# *Source* block\n# Use: highlight code listings\n# (require `source-highlight` or `pygmentize`)\nuse DBI;\nmy $dbh = DBI-&gt;connect('...',$u,$p)\n    or die \"connect: $dbh-&gt;errstr\";\n----</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"listingblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\"><div class=\"highlight\"><pre><span class=\"c1\"># *Source* block</span>\n<span class=\"c1\"># Use: highlight code listings</span>\n<span class=\"c1\"># (require `source-highlight` or `pygmentize`)</span>\n<span class=\"k\">use</span> <span class=\"n\">DBI</span><span class=\"p\">;</span>\n<span class=\"k\">my</span> <span class=\"nv\">$dbh</span> <span class=\"o\">=</span> <span class=\"n\">DBI</span><span class=\"o\">-&gt;</span><span class=\"nb\">connect</span><span class=\"p\">(</span><span class=\"s\">'...'</span><span class=\"p\">,</span><span class=\"nv\">$u</span><span class=\"p\">,</span><span class=\"nv\">$p</span><span class=\"p\">)</span>\n    <span class=\"ow\">or</span> <span class=\"nb\">die</span> <span class=\"s\">\"connect: $dbh-&gt;errstr\"</span><span class=\"p\">;</span>\n</pre></div></div></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n****\n*Sidebar* Block\n\nUse: sidebar notes :)\n****</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">Optional Title</div>\n<div class=\"paragraph\"><p><strong>Sidebar</strong> Block</p></div>\n<div class=\"paragraph\"><p>Use: sidebar notes :)</p></div>\n</div></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n==========================\n*Example* Block\n\nUse: examples :)\n\nDefault caption \"Example:\"\ncan be changed using\n\n [caption=\"Custom: \"]\n\nbefore example block.\n==========================</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"exampleblock\">\n<div class=\"title\">Example 1. Optional Title</div>\n<div class=\"content\">\n<div class=\"paragraph\"><p><strong>Example</strong> Block</p></div>\n<div class=\"paragraph\"><p>Use: examples :)</p></div>\n<div class=\"paragraph\"><p>Default caption \"Example:\"\ncan be changed using</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[caption=\"Custom: \"]</tt></pre>\n</div></div>\n<div class=\"paragraph\"><p>before example block.</p></div>\n</div></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n[NOTE]\n===============================\n*NOTE* Block\n\nUse: multi-paragraph notes.\n===============================</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-cheatsheet_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"title\">Optional Title</div>\n<div class=\"paragraph\"><p><strong>NOTE</strong> Block</p></div>\n<div class=\"paragraph\"><p>Use: multi-paragraph notes.</p></div>\n</td>\n</tr></tbody></table>\n</div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>////\n*Comment* block\n\nUse: hide comments\n////</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>++++\n*Passthrough* Block\n&lt;p&gt;\nUse: backend-specific markup like\n&lt;table border=\"1\"&gt;\n&lt;tr&gt;&lt;td&gt;1&lt;td&gt;2\n&lt;/table&gt;\n++++</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n*Passthrough* Block\n<p>\nUse: backend-specific markup like\n</p><table border=\"1\">\n<tbody><tr><td>1</td><td>2\n</td></tr></tbody></table>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt> .Optional Title\n ....\n *Literal* Block\n\n Use: workaround when literal\n paragraph (indented) like\n   1. First.\n   2. Second.\n incorrectly processed as list.\n ....</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"literalblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\">\n<pre><tt>*Literal* Block\n\nUse: workaround when literal\nparagraph (indented) like\n  1. First.\n  2. Second.\nincorrectly processed as list.</tt></pre>\n</div></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Optional Title\n[quote, cite author, cite source]\n____\n*Quote* Block\n\nUse: cite somebody\n____</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"quoteblock\">\n<div class=\"title\">Optional Title</div>\n<div class=\"content\">\n<div class=\"paragraph\"><p><strong>Quote</strong> Block</p></div>\n<div class=\"paragraph\"><p>Use: cite somebody</p></div>\n</div>\n<div class=\"attribution\">\n<em>cite source</em><br>\n— cite author\n</div></div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_text\">Text</h2>\n<div class=\"sectionbody\">\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>forced +\nline break</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>forced<br>\nline break</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>normal, _italic_, *bold*, +mono+.\n\n``double quoted'', `single quoted'.\n\nnormal, ^super^, ~sub~.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>normal, <em>italic</em>, <strong>bold</strong>, <tt>mono</tt>.</p></div>\n<div class=\"paragraph\"><p>“double quoted”, ‘single quoted’.</p></div>\n<div class=\"paragraph\"><p>normal, <sup>super</sup>, <sub>sub</sub>.</p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Command: `ls -al`\n\n+mono *bold*+\n\n`passthru *bold*`</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Command: <tt>ls -al</tt></p></div>\n<div class=\"paragraph\"><p><tt>mono <strong>bold</strong></tt></p></div>\n<div class=\"paragraph\"><p><tt>passthru *bold*</tt></p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Path: '/some/filez.txt', '.b'</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Path: <em>/some/filez.txt</em>, <em>.b</em></p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[red]#red text# [yellow-background]#on yellow#\n[big]#large# [red yellow-background big]*all bold*</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p><span class=\"red\">red text</span> <span class=\"yellow-background\">on yellow</span>\n<span class=\"big\">large</span> <strong><span class=\"red yellow-background big\">all bold</span></strong></p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Chars: n__i__**b**++m++[red]##r##</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Chars: n<em>i</em><strong>b</strong><tt>m</tt><span class=\"red\">r</span></p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>// Comment</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>(C) (R) (TM) -- ... -&gt; &lt;- =&gt; &lt;= &amp;#182;</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>© ® ™ — … → ← ⇒ ⇐ ¶</p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>''''</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<hr>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>Escaped:\n\\_italic_, +++_italic_+++,\nt\\__e__st, +++t__e__st+++,\n+++&lt;b&gt;bold&lt;/b&gt;+++, $$&lt;b&gt;normal&lt;/b&gt;$$\n\\&amp;#182;\n\\`not single quoted'\n\\`\\`not double quoted''</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>Escaped:\n_italic_, _italic_,\nt__e__st, t__e__st,\n<b>bold</b>, &lt;b&gt;normal&lt;/b&gt;\n&amp;#182;\n`not single quoted'\n``not double quoted''</p></div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_macros_links_images_amp_include\">Macros: links, images &amp; include</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>If you’ll need to use space in url/path you should replace it with %20.</p></div>\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[[anchor-1]]\nParagraph or block 1.\n\nanchor:anchor-2[]\nParagraph or block 2.\n\n&lt;&lt;anchor-1&gt;&gt;,\n&lt;&lt;anchor-1,First anchor&gt;&gt;,\nxref:anchor-2[],\nxref:anchor-2[Second anchor].</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\" id=\"anchor-1\"><p>Paragraph or block 1.</p></div>\n<div class=\"paragraph\"><p><a id=\"anchor-2\"></a>\nParagraph or block 2.</p></div>\n<div class=\"paragraph\"><p><a href=\"#anchor-1\">[anchor-1]</a>,\n<a href=\"#anchor-1\">First anchor</a>,\n<a href=\"#anchor-2\">[anchor-2]</a>,\n<a href=\"#anchor-2\">Second anchor</a>.</p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>link:asciidoc[This document]\nlink:asciidoc.html[]\nlink:/index.html[This site root]</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p><a href=\"http://powerman.name/doc/asciidoc\">This document</a>\n<a href=\"http://powerman.name/doc/asciidoc.html\">asciidoc.html</a>\n<a href=\"http://powerman.name/index.html\">This site root</a></p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>http://google.com\nhttp://google.com[Google Search]\nmailto:root@localhost[email admin]</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p><a href=\"http://google.com/\">http://google.com</a>\n<a href=\"http://google.com/\">Google Search</a>\n<a href=\"mailto:root@localhost\">email admin</a></p></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>First home\nimage:images/icons/home.png[]\n, second home\nimage:images/icons/home.png[Alt text]\n.\n\n.Block image\nimage::images/icons/home.png[]\nimage::images/icons/home.png[Alt text]\n\n.Thumbnail linked to full image\nimage:/images/font/640-screen2.gif[\n\"My screenshot\",width=128,\nlink=\"/images/font/640-screen2.gif\"]</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>First home\n<span class=\"image\">\n<img src=\"asciidoc-cheatsheet_files/home.png\" alt=\"images/icons/home.png\">\n</span>\n, second home\n<span class=\"image\">\n<img src=\"asciidoc-cheatsheet_files/home.png\" alt=\"Alt text\">\n</span>\n.</p></div>\n<div class=\"imageblock\">\n<div class=\"content\">\n<img src=\"asciidoc-cheatsheet_files/home.png\" alt=\"images/icons/home.png\">\n</div>\n<div class=\"title\">Figure 1. Block image</div>\n</div>\n<div class=\"imageblock\">\n<div class=\"content\">\n<img src=\"asciidoc-cheatsheet_files/home.png\" alt=\"Alt text\">\n</div>\n</div>\n<div class=\"paragraph\"><div class=\"title\">Thumbnail linked to full image</div><p><span class=\"image\">\n<a class=\"image\" href=\"http://powerman.name/images/font/640-screen2.gif\">\n<img src=\"asciidoc-cheatsheet_files/640-screen2.gif\" alt=\"My screenshot\" width=\"128\">\n</a>\n</span></p></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>This is example how files\ncan be included.\nIt's commented because\nthere no such files. :)\n\n// include::footer.txt[]\n\n// [source,perl]\n// ----\n// include::script.pl[]\n// ----</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"paragraph\"><p>This is example how files\ncan be included.\nIt’s commented because\nthere no such files. :)</p></div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_lists\">Lists</h2>\n<div class=\"sectionbody\">\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Bulleted\n* bullet\n* bullet\n  - bullet\n  - bullet\n* bullet\n** bullet\n** bullet\n*** bullet\n*** bullet\n**** bullet\n**** bullet\n***** bullet\n***** bullet\n**** bullet\n*** bullet\n** bullet\n* bullet</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"ulist\"><div class=\"title\">Bulleted</div><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Bulleted 2\n- bullet\n  * bullet</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"ulist\"><div class=\"title\">Bulleted 2</div><ul>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n</ul></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Ordered\n. number\n. number\n  .. letter\n  .. letter\n. number\n.. loweralpha\n.. loweralpha\n... lowerroman\n... lowerroman\n.... upperalpha\n.... upperalpha\n..... upperroman\n..... upperroman\n.... upperalpha\n... lowerroman\n.. loweralpha\n. number</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"olist arabic\"><div class=\"title\">Ordered</div><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n</li>\n<li>\n<p>\nletter\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nnumber\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nloweralpha\n</p>\n</li>\n<li>\n<p>\nloweralpha\n</p>\n<div class=\"olist lowerroman\"><ol class=\"lowerroman\">\n<li>\n<p>\nlowerroman\n</p>\n</li>\n<li>\n<p>\nlowerroman\n</p>\n<div class=\"olist upperalpha\"><ol class=\"upperalpha\">\n<li>\n<p>\nupperalpha\n</p>\n</li>\n<li>\n<p>\nupperalpha\n</p>\n<div class=\"olist upperroman\"><ol class=\"upperroman\">\n<li>\n<p>\nupperroman\n</p>\n</li>\n<li>\n<p>\nupperroman\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nupperalpha\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nlowerroman\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nloweralpha\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Ordered 2\na. letter\nb. letter\n   .. letter2\n   .. letter2\n       .  number\n       .  number\n           1. number2\n           2. number2\n           3. number2\n           4. number2\n       .  number\n   .. letter2\nc. letter</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"olist loweralpha\"><div class=\"title\">Ordered 2</div><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n</li>\n<li>\n<p>\nletter\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter2\n</p>\n</li>\n<li>\n<p>\nletter2\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber2\n</p>\n</li>\n<li>\n<p>\nnumber2\n</p>\n</li>\n<li>\n<p>\nnumber2\n</p>\n</li>\n<li>\n<p>\nnumber2\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nletter2\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nletter\n</p>\n</li>\n</ol></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Labeled\nTerm 1::\n    Definition 1\nTerm 2::\n    Definition 2\n    Term 2.1;;\n        Definition 2.1\n    Term 2.2;;\n        Definition 2.2\nTerm 3::\n    Definition 3\nTerm 4:: Definition 4\nTerm 4.1::: Definition 4.1\nTerm 4.2::: Definition 4.2\nTerm 4.2.1:::: Definition 4.2.1\nTerm 4.2.2:::: Definition 4.2.2\nTerm 4.3::: Definition 4.3\nTerm 5:: Definition 5</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"dlist\"><div class=\"title\">Labeled</div><dl>\n<dt class=\"hdlist1\">\nTerm 1\n</dt>\n<dd>\n<p>\n    Definition 1\n</p>\n</dd>\n<dt class=\"hdlist1\">\nTerm 2\n</dt>\n<dd>\n<p>\n    Definition 2\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nTerm 2.1\n</dt>\n<dd>\n<p>\n        Definition 2.1\n</p>\n</dd>\n<dt class=\"hdlist1\">\nTerm 2.2\n</dt>\n<dd>\n<p>\n        Definition 2.2\n</p>\n</dd>\n</dl></div>\n</dd>\n<dt class=\"hdlist1\">\nTerm 3\n</dt>\n<dd>\n<p>\n    Definition 3\n</p>\n</dd>\n<dt class=\"hdlist1\">\nTerm 4\n</dt>\n<dd>\n<p>\nDefinition 4\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nTerm 4.1\n</dt>\n<dd>\n<p>\nDefinition 4.1\n</p>\n</dd>\n<dt class=\"hdlist1\">\nTerm 4.2\n</dt>\n<dd>\n<p>\nDefinition 4.2\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nTerm 4.2.1\n</dt>\n<dd>\n<p>\nDefinition 4.2.1\n</p>\n</dd>\n<dt class=\"hdlist1\">\nTerm 4.2.2\n</dt>\n<dd>\n<p>\nDefinition 4.2.2\n</p>\n</dd>\n</dl></div>\n</dd>\n<dt class=\"hdlist1\">\nTerm 4.3\n</dt>\n<dd>\n<p>\nDefinition 4.3\n</p>\n</dd>\n</dl></div>\n</dd>\n<dt class=\"hdlist1\">\nTerm 5\n</dt>\n<dd>\n<p>\nDefinition 5\n</p>\n</dd>\n</dl></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Labeled 2\nTerm 1;;\n    Definition 1\n    Term 1.1::\n        Definition 1.1</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"dlist\"><div class=\"title\">Labeled 2</div><dl>\n<dt class=\"hdlist1\">\nTerm 1\n</dt>\n<dd>\n<p>\n    Definition 1\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nTerm 1.1\n</dt>\n<dd>\n<p>\n        Definition 1.1\n</p>\n</dd>\n</dl></div>\n</dd>\n</dl></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[horizontal]\n.Labeled horizontal\nTerm 1:: Definition 1\nTerm 2:: Definition 2\n[horizontal]\n    Term 2.1;;\n        Definition 2.1\n    Term 2.2;;\n        Definition 2.2\nTerm 3::\n    Definition 3\nTerm 4:: Definition 4\n[horizontal]\nTerm 4.1::: Definition 4.1\nTerm 4.2::: Definition 4.2\n[horizontal]\nTerm 4.2.1:::: Definition 4.2.1\nTerm 4.2.2:::: Definition 4.2.2\nTerm 4.3::: Definition 4.3\nTerm 5:: Definition 5</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"hdlist\"><div class=\"title\">Labeled horizontal</div><table>\n<tbody><tr>\n<td class=\"hdlist1\">\nTerm 1\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 1\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 2\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 2\n</p>\n<div class=\"hdlist\"><table>\n<tbody><tr>\n<td class=\"hdlist1\">\nTerm 2.1\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\n        Definition 2.1\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 2.2\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\n        Definition 2.2\n</p>\n</td>\n</tr>\n</tbody></table></div>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 3\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\n    Definition 3\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 4\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4\n</p>\n<div class=\"hdlist\"><table>\n<tbody><tr>\n<td class=\"hdlist1\">\nTerm 4.1\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4.1\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 4.2\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4.2\n</p>\n<div class=\"hdlist\"><table>\n<tbody><tr>\n<td class=\"hdlist1\">\nTerm 4.2.1\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4.2.1\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 4.2.2\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4.2.2\n</p>\n</td>\n</tr>\n</tbody></table></div>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 4.3\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 4.3\n</p>\n</td>\n</tr>\n</tbody></table></div>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\nTerm 5\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nDefinition 5\n</p>\n</td>\n</tr>\n</tbody></table></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[qanda]\n.Q&amp;A\nQuestion 1::\n    Answer 1\nQuestion 2:: Answer 2</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"qlist qanda\"><div class=\"title\">Q&amp;A</div><ol>\n<li>\n<p><em>\nQuestion 1\n</em></p>\n<p>\n    Answer 1\n</p>\n</li>\n<li>\n<p><em>\nQuestion 2\n</em></p>\n<p>\nAnswer 2\n</p>\n</li>\n</ol></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Indent is optional\n- bullet\n    * another bullet\n        1. number\n        .  again number\n            a. letter\n            .. again letter\n\n.. letter\n. number\n\n* bullet\n- bullet</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"ulist\"><div class=\"title\">Indent is optional</div><ul>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nanother bullet\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nagain number\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nagain letter\n</p>\n</li>\n<li>\n<p>\nletter\n</p>\n</li>\n</ol></div>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Break two lists\n. number\n. number\n\nIndependent paragraph break list.\n\n. number\n\n.Header break list too\n. number\n\n--\n. List block define list boundary too\n. number\n. number\n--\n\n--\n. number\n. number\n--</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"olist arabic\"><div class=\"title\">Break two lists</div><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>Independent paragraph break list.</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n<div class=\"olist arabic\"><div class=\"title\">Header break list too</div><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nList block define list boundary too\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</div></div>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</div></div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Continuation\n- bullet\ncontinuation\n. number\n  continuation\n* bullet\n\n  literal continuation\n\n.. letter\n+\nNon-literal continuation.\n+\n----\nany block can be\n\nincluded in list\n----\n+\nLast continuation.</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"ulist\"><div class=\"title\">Continuation</div><ul>\n<li>\n<p>\nbullet\ncontinuation\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n  continuation\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>literal continuation</tt></pre>\n</div></div>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n<div class=\"paragraph\"><p>Non-literal continuation.</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><tt>any block can be\n\nincluded in list</tt></pre>\n</div></div>\n<div class=\"paragraph\"><p>Last continuation.</p></div>\n</li>\n</ol></div>\n</li>\n</ul></div>\n</li>\n</ol></div>\n</li>\n</ul></div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.List block allow sublist inclusion\n- bullet\n  * bullet\n+\n--\n    - bullet\n      * bullet\n--\n  * bullet\n- bullet\n  . number\n    .. letter\n+\n--\n      . number\n        .. letter\n--\n    .. letter\n  . number</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"ulist\"><div class=\"title\">List block allow sublist inclusion</div><ul>\n<li>\n<p>\nbullet\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</div></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nbullet\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nletter\n</p>\n</li>\n</ol></div>\n</div></div>\n</li>\n<li>\n<p>\nletter\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nnumber\n</p>\n</li>\n</ol></div>\n</li>\n</ul></div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_tables\">Tables</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>You can fill table from CSV file using <tt>include::</tt> macros inside table.</p></div>\n<table class=\"cs\">\n<tbody><tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.An example table\n[options=\"header,footer\"]\n|=======================\n|Col 1|Col 2      |Col 3\n|1    |Item 1     |a\n|2    |Item 2     |b\n|3    |Item 3     |c\n|6    |Three items|d\n|=======================</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 1. An example table</caption>\n<colgroup><col width=\"33%\">\n<col width=\"33%\">\n<col width=\"33%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Col 1</th>\n<th valign=\"top\" align=\"left\">Col 2      </th>\n<th valign=\"top\" align=\"left\">Col 3</th>\n</tr>\n</thead>\n<tfoot>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">6</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Three items</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">d</p></td>\n</tr>\n</tfoot>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">1</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Item 1</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">a</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">2</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Item 2</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">b</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">3</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Item 3</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">c</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.CSV data, 15% each column\n[format=\"csv\",width=\"60%\",cols=\"4\"]\n[frame=\"topbot\",grid=\"none\"]\n|======\n1,2,3,4\na,b,c,d\nA,B,C,D\n|======</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"tableblock\">\n<table rules=\"none\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"60%\">\n<caption class=\"title\">Table 2. CSV data, 15% each column</caption>\n<colgroup><col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n</colgroup><tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">1</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">2</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">3</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">4</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">a</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">b</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">c</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">d</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">B</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">C</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">D</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</td></tr>\n<tr class=\"odd\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>[grid=\"rows\",format=\"csv\"]\n[options=\"header\",cols=\"^,&lt;,&lt;s,&lt;,&gt;m\"]\n|===========================\nID,FName,LName,Address,Phone\n1,Vasya,Pupkin,London,+123\n2,X,Y,\"A,B\",45678\n|===========================</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"tableblock\">\n<table rules=\"rows\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<colgroup><col width=\"20%\">\n<col width=\"20%\">\n<col width=\"20%\">\n<col width=\"20%\">\n<col width=\"20%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"center\">ID</th>\n<th valign=\"top\" align=\"left\">FName</th>\n<th valign=\"top\" align=\"left\">LName</th>\n<th valign=\"top\" align=\"left\">Address</th>\n<th valign=\"top\" align=\"right\">Phone</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">1</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Vasya</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><strong>Pupkin</strong></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">London</p></td>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><tt>+123</tt></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">2</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">X</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><strong>Y</strong></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A,B</p></td>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><tt>45678</tt></p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</td></tr>\n<tr class=\"even\"><td class=\"col1\">\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><tt>.Multiline cells, row/col span\n|====\n|Date |Duration |Avg HR |Notes\n\n|22-Aug-08 .2+^.^|10:24 | 157 |\nWorked out MSHR (max sustainable\nheart rate) by going hard\nfor this interval.\n\n|22-Aug-08 | 152 |\nBack-to-back with previous interval.\n\n|24-Aug-08 3+^|none\n\n|====</tt></pre>\n</div></div>\n</td><td class=\"col2\">\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 3. Multiline cells, row/col span</caption>\n<colgroup><col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n</colgroup><tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Date</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Duration</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Avg HR</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Notes</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">22-Aug-08</p></td>\n<td rowspan=\"2\" valign=\"middle\" align=\"center\"><p class=\"table\">10:24</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">157</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Worked out MSHR (max sustainable\nheart rate) by going hard\nfor this interval.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">22-Aug-08</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">152</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Back-to-back with previous interval.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">24-Aug-08</p></td>\n<td colspan=\"3\" valign=\"top\" align=\"center\"><p class=\"table\">none</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</td></tr>\n</tbody></table>\n</div>\n</div>\n\n<div id=\"footnotes\"><hr></div>\n<div id=\"footer\">\n<div id=\"footer-text\">\nVersion 2.2.2<br>\nLast updated 2012-05-10 16:29:30 EEST\n</div>\n</div>\n\n\n</body></html>"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/Content.css",
    "content": "/*\nShareMeNot is licensed under the MIT license:\nhttp://www.opensource.org/licenses/mit-license.php\n\n\nCopyright (c) 2012 University of Washington\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n/* \n * Every property is !important to prevent any styles declared on the web page\n * from overriding ours.\n */\n\n.sharemenotReplacementButton {\n\tborder: none !important;\n\tcursor: pointer !important;\n\theight: auto !important;\n\twidth: auto !important;\n}\n\n.sharemenotOriginalButton {\n\tborder: none !important;\n\theight: 1.5em !important;\n}"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/asciidoc.asc",
    "content": "Not found: /doc/javascripts/867/asciidoc.js\n"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/asciidoc.css",
    "content": "/* Shared CSS for AsciiDoc xhtml11 and html5 backends */\n\n/* Default font. */\nbody {\n  font-family: Georgia,serif;\n}\n\n/* Title font. */\nh1, h2, h3, h4, h5, h6,\ndiv.title, caption.title,\nthead, p.table.header,\n#toctitle,\n#author, #revnumber, #revdate, #revremark,\n#footer {\n  font-family: Arial,Helvetica,sans-serif;\n}\n\nbody {\n  margin: 1em 5% 1em 5%;\n}\n\na {\n  color: blue;\n  text-decoration: underline;\n}\na:visited {\n  color: fuchsia;\n}\n\nem {\n  font-style: italic;\n  color: navy;\n}\n\nstrong {\n  font-weight: bold;\n  color: #083194;\n}\n\nh1, h2, h3, h4, h5, h6 {\n  color: #527bbd;\n  margin-top: 1.2em;\n  margin-bottom: 0.5em;\n  line-height: 1.3;\n}\n\nh1, h2, h3 {\n  border-bottom: 2px solid silver;\n}\nh2 {\n  padding-top: 0.5em;\n}\nh3 {\n  float: left;\n}\nh3 + * {\n  clear: left;\n}\nh5 {\n  font-size: 1.0em;\n}\n\ndiv.sectionbody {\n  margin-left: 0;\n}\n\nhr {\n  border: 1px solid silver;\n}\n\np {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n\nul, ol, li > p {\n  margin-top: 0;\n}\nul > li     { color: #aaa; }\nul > li > * { color: black; }\n\npre {\n  padding: 0;\n  margin: 0;\n}\n\n#author {\n  color: #527bbd;\n  font-weight: bold;\n  font-size: 1.1em;\n}\n#email {\n}\n#revnumber, #revdate, #revremark {\n}\n\n#footer {\n  font-size: small;\n  border-top: 2px solid silver;\n  padding-top: 0.5em;\n  margin-top: 4.0em;\n}\n#footer-text {\n  float: left;\n  padding-bottom: 0.5em;\n}\n#footer-badges {\n  float: right;\n  padding-bottom: 0.5em;\n}\n\n#preamble {\n  margin-top: 1.5em;\n  margin-bottom: 1.5em;\n}\ndiv.imageblock, div.exampleblock, div.verseblock,\ndiv.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,\ndiv.admonitionblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\ndiv.admonitionblock {\n  margin-top: 2.0em;\n  margin-bottom: 2.0em;\n  margin-right: 10%;\n  color: #606060;\n}\n\ndiv.content { /* Block element content. */\n  padding: 0;\n}\n\n/* Block element titles. */\ndiv.title, caption.title {\n  color: #527bbd;\n  font-weight: bold;\n  text-align: left;\n  margin-top: 1.0em;\n  margin-bottom: 0.5em;\n}\ndiv.title + * {\n  margin-top: 0;\n}\n\ntd div.title:first-child {\n  margin-top: 0.0em;\n}\ndiv.content div.title:first-child {\n  margin-top: 0.0em;\n}\ndiv.content + div.title {\n  margin-top: 0.0em;\n}\n\ndiv.sidebarblock > div.content {\n  background: #ffffee;\n  border: 1px solid #dddddd;\n  border-left: 4px solid #f0f0f0;\n  padding: 0.5em;\n}\n\ndiv.listingblock > div.content {\n  border: 1px solid #dddddd;\n  border-left: 5px solid #f0f0f0;\n  background: #f8f8f8;\n  padding: 0.5em;\n}\n\ndiv.quoteblock, div.verseblock {\n  padding-left: 1.0em;\n  margin-left: 1.0em;\n  margin-right: 10%;\n  border-left: 5px solid #f0f0f0;\n  color: #888;\n}\n\ndiv.quoteblock > div.attribution {\n  padding-top: 0.5em;\n  text-align: right;\n}\n\ndiv.verseblock > pre.content {\n  font-family: inherit;\n  font-size: inherit;\n}\ndiv.verseblock > div.attribution {\n  padding-top: 0.75em;\n  text-align: left;\n}\n/* DEPRECATED: Pre version 8.2.7 verse style literal block. */\ndiv.verseblock + div.attribution {\n  text-align: left;\n}\n\ndiv.admonitionblock .icon {\n  vertical-align: top;\n  font-size: 1.1em;\n  font-weight: bold;\n  text-decoration: underline;\n  color: #527bbd;\n  padding-right: 0.5em;\n}\ndiv.admonitionblock td.content {\n  padding-left: 0.5em;\n  border-left: 3px solid #dddddd;\n}\n\ndiv.exampleblock > div.content {\n  border-left: 3px solid #dddddd;\n  padding-left: 0.5em;\n}\n\ndiv.imageblock div.content { padding-left: 0; }\nspan.image img { border-style: none; }\na.image:visited { color: white; }\n\ndl {\n  margin-top: 0.8em;\n  margin-bottom: 0.8em;\n}\ndt {\n  margin-top: 0.5em;\n  margin-bottom: 0;\n  font-style: normal;\n  color: navy;\n}\ndd > *:first-child {\n  margin-top: 0.1em;\n}\n\nul, ol {\n    list-style-position: outside;\n}\nol.arabic {\n  list-style-type: decimal;\n}\nol.loweralpha {\n  list-style-type: lower-alpha;\n}\nol.upperalpha {\n  list-style-type: upper-alpha;\n}\nol.lowerroman {\n  list-style-type: lower-roman;\n}\nol.upperroman {\n  list-style-type: upper-roman;\n}\n\ndiv.compact ul, div.compact ol,\ndiv.compact p, div.compact p,\ndiv.compact div, div.compact div {\n  margin-top: 0.1em;\n  margin-bottom: 0.1em;\n}\n\ntfoot {\n  font-weight: bold;\n}\ntd > div.verse {\n  white-space: pre;\n}\n\ndiv.hdlist {\n  margin-top: 0.8em;\n  margin-bottom: 0.8em;\n}\ndiv.hdlist tr {\n  padding-bottom: 15px;\n}\ndt.hdlist1.strong, td.hdlist1.strong {\n  font-weight: bold;\n}\ntd.hdlist1 {\n  vertical-align: top;\n  font-style: normal;\n  padding-right: 0.8em;\n  color: navy;\n}\ntd.hdlist2 {\n  vertical-align: top;\n}\ndiv.hdlist.compact tr {\n  margin: 0;\n  padding-bottom: 0;\n}\n\n.comment {\n  background: yellow;\n}\n\n.footnote, .footnoteref {\n  font-size: 0.8em;\n}\n\nspan.footnote, span.footnoteref {\n  vertical-align: super;\n}\n\n#footnotes {\n  margin: 20px 0 20px 0;\n  padding: 7px 0 0 0;\n}\n\n#footnotes div.footnote {\n  margin: 0 0 5px 0;\n}\n\n#footnotes hr {\n  border: none;\n  border-top: 1px solid silver;\n  height: 1px;\n  text-align: left;\n  margin-left: 0;\n  width: 20%;\n  min-width: 100px;\n}\n\ndiv.colist td {\n  padding-right: 0.5em;\n  padding-bottom: 0.3em;\n  vertical-align: top;\n}\ndiv.colist td img {\n  margin-top: 0.3em;\n}\n\n@media print {\n  #footer-badges { display: none; }\n}\n\n#toc {\n  margin-bottom: 2.5em;\n}\n\n#toctitle {\n  color: #527bbd;\n  font-size: 1.1em;\n  font-weight: bold;\n  margin-top: 1.0em;\n  margin-bottom: 0.1em;\n}\n\ndiv.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {\n  margin-top: 0;\n  margin-bottom: 0;\n}\ndiv.toclevel2 {\n  margin-left: 2em;\n  font-size: 0.9em;\n}\ndiv.toclevel3 {\n  margin-left: 4em;\n  font-size: 0.9em;\n}\ndiv.toclevel4 {\n  margin-left: 6em;\n  font-size: 0.9em;\n}\n\nspan.aqua { color: aqua; }\nspan.black { color: black; }\nspan.blue { color: blue; }\nspan.fuchsia { color: fuchsia; }\nspan.gray { color: gray; }\nspan.green { color: green; }\nspan.lime { color: lime; }\nspan.maroon { color: maroon; }\nspan.navy { color: navy; }\nspan.olive { color: olive; }\nspan.purple { color: purple; }\nspan.red { color: red; }\nspan.silver { color: silver; }\nspan.teal { color: teal; }\nspan.white { color: white; }\nspan.yellow { color: yellow; }\n\nspan.aqua-background { background: aqua; }\nspan.black-background { background: black; }\nspan.blue-background { background: blue; }\nspan.fuchsia-background { background: fuchsia; }\nspan.gray-background { background: gray; }\nspan.green-background { background: green; }\nspan.lime-background { background: lime; }\nspan.maroon-background { background: maroon; }\nspan.navy-background { background: navy; }\nspan.olive-background { background: olive; }\nspan.purple-background { background: purple; }\nspan.red-background { background: red; }\nspan.silver-background { background: silver; }\nspan.teal-background { background: teal; }\nspan.white-background { background: white; }\nspan.yellow-background { background: yellow; }\n\nspan.big { font-size: 2em; }\nspan.small { font-size: 0.6em; }\n\nspan.underline { text-decoration: underline; }\nspan.overline { text-decoration: overline; }\nspan.line-through { text-decoration: line-through; }\n\ndiv.unbreakable { page-break-inside: avoid; }\n\n\n/*\n * xhtml11 specific\n *\n * */\n\ntt {\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: inherit;\n  color: navy;\n}\n\ndiv.tableblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\ndiv.tableblock > table {\n  border: 3px solid #527bbd;\n}\nthead, p.table.header {\n  font-weight: bold;\n  color: #527bbd;\n}\np.table {\n  margin-top: 0;\n}\n/* Because the table frame attribute is overriden by CSS in most browsers. */\ndiv.tableblock > table[frame=\"void\"] {\n  border-style: none;\n}\ndiv.tableblock > table[frame=\"hsides\"] {\n  border-left-style: none;\n  border-right-style: none;\n}\ndiv.tableblock > table[frame=\"vsides\"] {\n  border-top-style: none;\n  border-bottom-style: none;\n}\n\n\n/*\n * html5 specific\n *\n * */\n\n.monospaced {\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: inherit;\n  color: navy;\n}\n\ntable.tableblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\nthead, p.tableblock.header {\n  font-weight: bold;\n  color: #527bbd;\n}\np.tableblock {\n  margin-top: 0;\n}\ntable.tableblock {\n  border-width: 3px;\n  border-spacing: 0px;\n  border-style: solid;\n  border-color: #527bbd;\n  border-collapse: collapse;\n}\nth.tableblock, td.tableblock {\n  border-width: 1px;\n  padding: 4px;\n  border-style: solid;\n  border-color: #527bbd;\n}\n\ntable.tableblock.frame-topbot {\n  border-left-style: hidden;\n  border-right-style: hidden;\n}\ntable.tableblock.frame-sides {\n  border-top-style: hidden;\n  border-bottom-style: hidden;\n}\ntable.tableblock.frame-none {\n  border-style: hidden;\n}\n\nth.tableblock.halign-left, td.tableblock.halign-left {\n  text-align: left;\n}\nth.tableblock.halign-center, td.tableblock.halign-center {\n  text-align: center;\n}\nth.tableblock.halign-right, td.tableblock.halign-right {\n  text-align: right;\n}\n\nth.tableblock.valign-top, td.tableblock.valign-top {\n  vertical-align: top;\n}\nth.tableblock.valign-middle, td.tableblock.valign-middle {\n  vertical-align: middle;\n}\nth.tableblock.valign-bottom, td.tableblock.valign-bottom {\n  vertical-align: bottom;\n}\n\n\n/*\n * manpage specific\n *\n * */\n\nbody.manpage h1 {\n  padding-top: 0.5em;\n  padding-bottom: 0.5em;\n  border-top: 2px solid silver;\n  border-bottom: 2px solid silver;\n}\nbody.manpage h2 {\n  border-style: none;\n}\nbody.manpage div.sectionbody {\n  margin-left: 3em;\n}\n\n@media print {\n  body.manpage div#toc { display: none; }\n}\n"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/asciidoc.js",
    "content": "var asciidoc = {  // Namespace.\n\n/////////////////////////////////////////////////////////////////////\n// Table Of Contents generator\n/////////////////////////////////////////////////////////////////////\n\n/* Author: Mihai Bazon, September 2002\n * http://students.infoiasi.ro/~mishoo\n *\n * Table Of Content generator\n * Version: 0.4\n *\n * Feel free to use this script under the terms of the GNU General Public\n * License, as long as you do not remove or alter this notice.\n */\n\n /* modified by Troy D. Hanson, September 2006. License: GPL */\n /* modified by Stuart Rackham, 2006, 2009. License: GPL */\n\n// toclevels = 1..4.\ntoc: function (toclevels) {\n\n  function getText(el) {\n    var text = \"\";\n    for (var i = el.firstChild; i != null; i = i.nextSibling) {\n      if (i.nodeType == 3 /* Node.TEXT_NODE */) // IE doesn't speak constants.\n        text += i.data;\n      else if (i.firstChild != null)\n        text += getText(i);\n    }\n    return text;\n  }\n\n  function TocEntry(el, text, toclevel) {\n    this.element = el;\n    this.text = text;\n    this.toclevel = toclevel;\n  }\n\n  function tocEntries(el, toclevels) {\n    var result = new Array;\n    var re = new RegExp('[hH]([1-'+(toclevels+1)+'])');\n    // Function that scans the DOM tree for header elements (the DOM2\n    // nodeIterator API would be a better technique but not supported by all\n    // browsers).\n    var iterate = function (el) {\n      for (var i = el.firstChild; i != null; i = i.nextSibling) {\n        if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {\n          var mo = re.exec(i.tagName);\n          if (mo && (i.getAttribute(\"class\") || i.getAttribute(\"className\")) != \"float\") {\n            result[result.length] = new TocEntry(i, getText(i), mo[1]-1);\n          }\n          iterate(i);\n        }\n      }\n    }\n    iterate(el);\n    return result;\n  }\n\n  var toc = document.getElementById(\"toc\");\n  if (!toc) {\n    return;\n  }\n\n  // Delete existing TOC entries in case we're reloading the TOC.\n  var tocEntriesToRemove = [];\n  var i;\n  for (i = 0; i < toc.childNodes.length; i++) {\n    var entry = toc.childNodes[i];\n    if (entry.nodeName.toLowerCase() == 'div'\n     && entry.getAttribute(\"class\")\n     && entry.getAttribute(\"class\").match(/^toclevel/))\n      tocEntriesToRemove.push(entry);\n  }\n  for (i = 0; i < tocEntriesToRemove.length; i++) {\n    toc.removeChild(tocEntriesToRemove[i]);\n  }\n  \n  // Rebuild TOC entries.\n  var entries = tocEntries(document.getElementById(\"content\"), toclevels);\n  for (var i = 0; i < entries.length; ++i) {\n    var entry = entries[i];\n    if (entry.element.id == \"\")\n      entry.element.id = \"_toc_\" + i;\n    var a = document.createElement(\"a\");\n    a.href = \"#\" + entry.element.id;\n    a.appendChild(document.createTextNode(entry.text));\n    var div = document.createElement(\"div\");\n    div.appendChild(a);\n    div.className = \"toclevel\" + entry.toclevel;\n    toc.appendChild(div);\n  }\n  if (entries.length == 0)\n    toc.parentNode.removeChild(toc);\n},\n\n\n/////////////////////////////////////////////////////////////////////\n// Footnotes generator\n/////////////////////////////////////////////////////////////////////\n\n/* Based on footnote generation code from:\n * http://www.brandspankingnew.net/archive/2005/07/format_footnote.html\n */\n\nfootnotes: function () {\n  // Delete existing footnote entries in case we're reloading the footnodes.\n  var i;\n  var noteholder = document.getElementById(\"footnotes\");\n  if (!noteholder) {\n    return;\n  }\n  var entriesToRemove = [];\n  for (i = 0; i < noteholder.childNodes.length; i++) {\n    var entry = noteholder.childNodes[i];\n    if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute(\"class\") == \"footnote\")\n      entriesToRemove.push(entry);\n  }\n  for (i = 0; i < entriesToRemove.length; i++) {\n    noteholder.removeChild(entriesToRemove[i]);\n  }\n\n  // Rebuild footnote entries.\n  var cont = document.getElementById(\"content\");\n  var spans = cont.getElementsByTagName(\"span\");\n  var refs = {};\n  var n = 0;\n  for (i=0; i<spans.length; i++) {\n    if (spans[i].className == \"footnote\") {\n      n++;\n      var note = spans[i].getAttribute(\"data-note\");\n      if (!note) {\n        // Use [\\s\\S] in place of . so multi-line matches work.\n        // Because JavaScript has no s (dotall) regex flag.\n        note = spans[i].innerHTML.match(/\\s*\\[([\\s\\S]*)]\\s*/)[1];\n        spans[i].innerHTML =\n          \"[<a id='_footnoteref_\" + n + \"' href='#_footnote_\" + n +\n          \"' title='View footnote' class='footnote'>\" + n + \"</a>]\";\n        spans[i].setAttribute(\"data-note\", note);\n      }\n      noteholder.innerHTML +=\n        \"<div class='footnote' id='_footnote_\" + n + \"'>\" +\n        \"<a href='#_footnoteref_\" + n + \"' title='Return to text'>\" +\n        n + \"</a>. \" + note + \"</div>\";\n      var id =spans[i].getAttribute(\"id\");\n      if (id != null) refs[\"#\"+id] = n;\n    }\n  }\n  if (n == 0)\n    noteholder.parentNode.removeChild(noteholder);\n  else {\n    // Process footnoterefs.\n    for (i=0; i<spans.length; i++) {\n      if (spans[i].className == \"footnoteref\") {\n        var href = spans[i].getElementsByTagName(\"a\")[0].getAttribute(\"href\");\n        href = href.match(/#.*/)[0];  // Because IE return full URL.\n        n = refs[href];\n        spans[i].innerHTML =\n          \"[<a href='#_footnote_\" + n +\n          \"' title='View footnote' class='footnote'>\" + n + \"</a>]\";\n      }\n    }\n  }\n},\n\ninstall: function(toclevels) {\n  var timerId;\n\n  function reinstall() {\n    asciidoc.footnotes();\n    if (toclevels) {\n      asciidoc.toc(toclevels);\n    }\n  }\n\n  function reinstallAndRemoveTimer() {\n    clearInterval(timerId);\n    reinstall();\n  }\n\n  timerId = setInterval(reinstall, 500);\n  if (document.addEventListener)\n    document.addEventListener(\"DOMContentLoaded\", reinstallAndRemoveTimer, false);\n  else\n    window.onload = reinstallAndRemoveTimer;\n}\n\n}\n"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/jquery-1.js",
    "content": "/*\n * jQuery 1.2 - New Wave Javascript\n *\n * Copyright (c) 2007 John Resig (jquery.com)\n * Dual licensed under the MIT (MIT-LICENSE.txt)\n * and GPL (GPL-LICENSE.txt) licenses.\n *\n * $Date: 2007-09-10 15:45:49 -0400 (Mon, 10 Sep 2007) $\n * $Rev: 3219 $\n */\n(function(){if(typeof jQuery!=\"undefined\")var _jQuery=jQuery;var jQuery=window.jQuery=function(a,c){if(window==this||!this.init)return new jQuery(a,c);return this.init(a,c);};if(typeof $!=\"undefined\")var _$=$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\\s)+>)[^>]*$|^#(\\w+)$/;jQuery.fn=jQuery.prototype={init:function(a,c){a=a||document;if(typeof a==\"string\"){var m=quickExpr.exec(a);if(m&&(m[1]||!c)){if(m[1])a=jQuery.clean([m[1]],c);else{var tmp=document.getElementById(m[3]);if(tmp)if(tmp.id!=m[3])return jQuery().find(a);else{this[0]=tmp;this.length=1;return this;}else\na=[];}}else\nreturn new jQuery(c).find(a);}else if(jQuery.isFunction(a))return new jQuery(document)[jQuery.fn.ready?\"ready\":\"load\"](a);return this.setArray(a.constructor==Array&&a||(a.jquery||a.length&&a!=window&&!a.nodeType&&a[0]!=undefined&&a[0].nodeType)&&jQuery.makeArray(a)||[a]);},jquery:\"1.2\",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(a){var ret=jQuery(a);ret.prevObject=this;return ret;},setArray:function(a){this.length=0;Array.prototype.push.apply(this,a);return this;},each:function(fn,args){return jQuery.each(this,fn,args);},index:function(obj){var pos=-1;this.each(function(i){if(this==obj)pos=i;});return pos;},attr:function(key,value,type){var obj=key;if(key.constructor==String)if(value==undefined)return this.length&&jQuery[type||\"attr\"](this[0],key)||undefined;else{obj={};obj[key]=value;}return this.each(function(index){for(var prop in obj)jQuery.attr(type?this.style:this,prop,jQuery.prop(this,obj[prop],type,index,prop));});},css:function(key,value){return this.attr(key,value,\"curCSS\");},text:function(e){if(typeof e!=\"object\"&&e!=null)return this.empty().append(document.createTextNode(e));var t=\"\";jQuery.each(e||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)t+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return t;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,1,function(a){this.appendChild(a);});},prepend:function(){return this.domManip(arguments,true,-1,function(a){this.insertBefore(a,this.firstChild);});},before:function(){return this.domManip(arguments,false,1,function(a){this.parentNode.insertBefore(a,this);});},after:function(){return this.domManip(arguments,false,-1,function(a){this.parentNode.insertBefore(a,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(t){var data=jQuery.map(this,function(a){return jQuery.find(t,a);});return this.pushStack(/[^+>] [^+>]/.test(t)||t.indexOf(\"..\")>-1?jQuery.unique(data):data);},clone:function(events){var ret=this.map(function(){return this.outerHTML?jQuery(this.outerHTML)[0]:this.cloneNode(true);});if(events===true){var clone=ret.find(\"*\").andSelf();this.find(\"*\").andSelf().each(function(i){var events=jQuery.data(this,\"events\");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});}return ret;},filter:function(t){return this.pushStack(jQuery.isFunction(t)&&jQuery.grep(this,function(el,index){return t.apply(el,[index]);})||jQuery.multiFilter(t,this));},not:function(t){return this.pushStack(t.constructor==String&&jQuery.multiFilter(t,this,true)||jQuery.grep(this,function(a){return(t.constructor==Array||t.jquery)?jQuery.inArray(a,t)<0:a!=t;}));},add:function(t){return this.pushStack(jQuery.merge(this.get(),t.constructor==String?jQuery(t).get():t.length!=undefined&&(!t.nodeName||t.nodeName==\"FORM\")?t:[t]));},is:function(expr){return expr?jQuery.multiFilter(expr,this).length>0:false;},hasClass:function(expr){return this.is(\".\"+expr);},val:function(val){if(val==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,\"select\")){var index=elem.selectedIndex,a=[],options=elem.options,one=elem.type==\"select-one\";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){var val=jQuery.browser.msie&&!option.attributes[\"value\"].specified?option.text:option.value;if(one)return val;a.push(val);}}return a;}else\nreturn this[0].value.replace(/\\r/g,\"\");}}else\nreturn this.each(function(){if(val.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,val)>=0||jQuery.inArray(this.name,val)>=0);else if(jQuery.nodeName(this,\"select\")){var tmp=val.constructor==Array?val:[val];jQuery(\"option\",this).each(function(){this.selected=(jQuery.inArray(this.value,tmp)>=0||jQuery.inArray(this.text,tmp)>=0);});if(!tmp.length)this.selectedIndex=-1;}else\nthis.value=val;});},html:function(val){return val==undefined?(this.length?this[0].innerHTML:null):this.empty().append(val);},replaceWith:function(val){return this.after(val).remove();},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(fn){return this.pushStack(jQuery.map(this,function(elem,i){return fn.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},domManip:function(args,table,dir,fn){var clone=this.length>1,a;return this.each(function(){if(!a){a=jQuery.clean(args,this.ownerDocument);if(dir<0)a.reverse();}var obj=this;if(table&&jQuery.nodeName(this,\"table\")&&jQuery.nodeName(a[0],\"tr\"))obj=this.getElementsByTagName(\"tbody\")[0]||this.appendChild(document.createElement(\"tbody\"));jQuery.each(a,function(){if(jQuery.nodeName(this,\"script\")){if(this.src)jQuery.ajax({url:this.src,async:false,dataType:\"script\"});else\njQuery.globalEval(this.text||this.textContent||this.innerHTML||\"\");}else\nfn.apply(obj,[clone?this.cloneNode(true):this]);});});}};jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},a=1,al=arguments.length,deep=false;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};}if(al==1){target=this;a=0;}var prop;for(;a<al;a++)if((prop=arguments[a])!=null)for(var i in prop){if(target==prop[i])continue;if(deep&&typeof prop[i]=='object'&&target[i])jQuery.extend(target[i],prop[i]);else if(prop[i]!=undefined)target[i]=prop[i];}return target;};var expando=\"jQuery\"+(new Date()).getTime(),uuid=0,win={};jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!=\"string\"&&!fn.nodeName&&fn.constructor!=Array&&/function/i.test(fn+\"\");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){if(window.execScript)window.execScript(data);else if(jQuery.browser.safari)window.setTimeout(data,0);else\neval.call(window,data);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?win:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!=undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?win:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name=\"\";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(obj,fn,args){if(args){if(obj.length==undefined)for(var i in obj)fn.apply(obj[i],args);else\nfor(var i=0,ol=obj.length;i<ol;i++)if(fn.apply(obj[i],args)===false)break;}else{if(obj.length==undefined)for(var i in obj)fn.call(obj[i],i,obj[i]);else\nfor(var i=0,ol=obj.length,val=obj[0];i<ol&&fn.call(val,i,val)!==false;val=obj[++i]){}}return obj;},prop:function(elem,value,type,index,prop){if(jQuery.isFunction(value))value=value.call(elem,[index]);var exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i;return value&&value.constructor==Number&&type==\"curCSS\"&&!exclude.test(prop)?value+\"px\":value;},className:{add:function(elem,c){jQuery.each((c||\"\").split(/\\s+/),function(i,cur){if(!jQuery.className.has(elem.className,cur))elem.className+=(elem.className?\" \":\"\")+cur;});},remove:function(elem,c){elem.className=c!=undefined?jQuery.grep(elem.className.split(/\\s+/),function(cur){return!jQuery.className.has(c,cur);}).join(\" \"):\"\";},has:function(t,c){return jQuery.inArray(c,(t.className||t).toString().split(/\\s+/))>-1;}},swap:function(e,o,f){for(var i in o){e.style[\"old\"+i]=e.style[i];e.style[i]=o[i];}f.apply(e,[]);for(var i in o)e.style[i]=e.style[\"old\"+i];},css:function(e,p){if(p==\"height\"||p==\"width\"){var old={},oHeight,oWidth,d=[\"Top\",\"Bottom\",\"Right\",\"Left\"];jQuery.each(d,function(){old[\"padding\"+this]=0;old[\"border\"+this+\"Width\"]=0;});jQuery.swap(e,old,function(){if(jQuery(e).is(':visible')){oHeight=e.offsetHeight;oWidth=e.offsetWidth;}else{e=jQuery(e.cloneNode(true)).find(\":radio\").removeAttr(\"checked\").end().css({visibility:\"hidden\",position:\"absolute\",display:\"block\",right:\"0\",left:\"0\"}).appendTo(e.parentNode)[0];var parPos=jQuery.css(e.parentNode,\"position\")||\"static\";if(parPos==\"static\")e.parentNode.style.position=\"relative\";oHeight=e.clientHeight;oWidth=e.clientWidth;if(parPos==\"static\")e.parentNode.style.position=\"static\";e.parentNode.removeChild(e);}});return p==\"height\"?oHeight:oWidth;}return jQuery.curCSS(e,p);},curCSS:function(elem,prop,force){var ret,stack=[],swap=[];function color(a){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(a,null);return!ret||ret.getPropertyValue(\"color\")==\"\";}if(prop==\"opacity\"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,\"opacity\");return ret==\"\"?\"1\":ret;}if(prop.match(/float/i))prop=styleFloat;if(!force&&elem.style[prop])ret=elem.style[prop];else if(document.defaultView&&document.defaultView.getComputedStyle){if(prop.match(/float/i))prop=\"float\";prop=prop.replace(/([A-Z])/g,\"-$1\").toLowerCase();var cur=document.defaultView.getComputedStyle(elem,null);if(cur&&!color(elem))ret=cur.getPropertyValue(prop);else{for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(a=0;a<stack.length;a++)if(color(stack[a])){swap[a]=stack[a].style.display;stack[a].style.display=\"block\";}ret=prop==\"display\"&&swap[stack.length-1]!=null?\"none\":document.defaultView.getComputedStyle(elem,null).getPropertyValue(prop)||\"\";for(a=0;a<swap.length;a++)if(swap[a]!=null)stack[a].style.display=swap[a];}if(prop==\"opacity\"&&ret==\"\")ret=\"1\";}else if(elem.currentStyle){var newProp=prop.replace(/\\-(\\w)/g,function(m,c){return c.toUpperCase();});ret=elem.currentStyle[prop]||elem.currentStyle[newProp];if(!/^\\d+(px)?$/i.test(ret)&&/^\\d/.test(ret)){var style=elem.style.left;var runtimeStyle=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;elem.style.left=ret||0;ret=elem.style.pixelLeft+\"px\";elem.style.left=style;elem.runtimeStyle.left=runtimeStyle;}}return ret;},clean:function(a,doc){var r=[];doc=doc||document;jQuery.each(a,function(i,arg){if(!arg)return;if(arg.constructor==Number)arg=arg.toString();if(typeof arg==\"string\"){arg=arg.replace(/(<(\\w+)[^>]*?)\\/>/g,function(m,all,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area)$/i)?m:all+\"></\"+tag+\">\";});var s=jQuery.trim(arg).toLowerCase(),div=doc.createElement(\"div\"),tb=[];var wrap=!s.indexOf(\"<opt\")&&[1,\"<select>\",\"</select>\"]||!s.indexOf(\"<leg\")&&[1,\"<fieldset>\",\"</fieldset>\"]||s.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,\"<table>\",\"</table>\"]||!s.indexOf(\"<tr\")&&[2,\"<table><tbody>\",\"</tbody></table>\"]||(!s.indexOf(\"<td\")||!s.indexOf(\"<th\"))&&[3,\"<table><tbody><tr>\",\"</tr></tbody></table>\"]||!s.indexOf(\"<col\")&&[2,\"<table><tbody></tbody><colgroup>\",\"</colgroup></table>\"]||jQuery.browser.msie&&[1,\"div<div>\",\"</div>\"]||[0,\"\",\"\"];div.innerHTML=wrap[1]+arg+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){if(!s.indexOf(\"<table\")&&s.indexOf(\"<tbody\")<0)tb=div.firstChild&&div.firstChild.childNodes;else if(wrap[1]==\"<table>\"&&s.indexOf(\"<tbody\")<0)tb=div.childNodes;for(var n=tb.length-1;n>=0;--n)if(jQuery.nodeName(tb[n],\"tbody\")&&!tb[n].childNodes.length)tb[n].parentNode.removeChild(tb[n]);if(/^\\s/.test(arg))div.insertBefore(doc.createTextNode(arg.match(/^\\s*/)[0]),div.firstChild);}arg=jQuery.makeArray(div.childNodes);}if(0===arg.length&&(!jQuery.nodeName(arg,\"form\")&&!jQuery.nodeName(arg,\"select\")))return;if(arg[0]==undefined||jQuery.nodeName(arg,\"form\")||arg.options)r.push(arg);else\nr=jQuery.merge(r,arg);});return r;},attr:function(elem,name,value){var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name==\"selected\"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name==\"style\")return jQuery.attr(elem.style,\"cssText\",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,\"form\")&&(name==\"action\"||name==\"method\"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name==\"type\"&&jQuery.nodeName(elem,\"input\")&&elem.parentNode)throw\"type property can't be changed\";elem.setAttribute(name,value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name==\"opacity\"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||\"\").replace(/alpha\\([^)]*\\)/,\"\")+(parseFloat(value).toString()==\"NaN\"?\"\":\"alpha(opacity=\"+value*100+\")\");}return elem.filter?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():\"\";}name=name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(t){return(t||\"\").replace(/^\\s+|\\s+$/g,\"\");},makeArray:function(a){var r=[];if(typeof a!=\"array\")for(var i=0,al=a.length;i<al;i++)r.push(a[i]);else\nr=a.slice(0);return r;},inArray:function(b,a){for(var i=0,al=a.length;i<al;i++)if(a[i]==b)return i;return-1;},merge:function(first,second){if(jQuery.browser.msie){for(var i=0;second[i];i++)if(second[i].nodeType!=8)first.push(second[i]);}else\nfor(var i=0;second[i];i++)first.push(second[i]);return first;},unique:function(first){var r=[],done={};try{for(var i=0,fl=first.length;i<fl;i++){var id=jQuery.data(first[i]);if(!done[id]){done[id]=true;r.push(first[i]);}}}catch(e){r=first;}return r;},grep:function(elems,fn,inv){if(typeof fn==\"string\")fn=eval(\"false||function(a,i){return \"+fn+\"}\");var result=[];for(var i=0,el=elems.length;i<el;i++)if(!inv&&fn(elems[i],i)||inv&&!fn(elems[i],i))result.push(elems[i]);return result;},map:function(elems,fn){if(typeof fn==\"string\")fn=eval(\"false||function(a){return \"+fn+\"}\");var result=[];for(var i=0,el=elems.length;i<el;i++){var val=fn(elems[i],i);if(val!==null&&val!=undefined){if(val.constructor!=Array)val=[val];result=result.concat(val);}}return result;}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\\/: ]([\\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?\"styleFloat\":\"cssFloat\";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode==\"CSS1Compat\",styleFloat:jQuery.browser.msie?\"styleFloat\":\"cssFloat\",props:{\"for\":\"htmlFor\",\"class\":\"className\",\"float\":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,innerHTML:\"innerHTML\",className:\"className\",value:\"value\",disabled:\"disabled\",checked:\"checked\",readonly:\"readOnly\",selected:\"selected\",maxlength:\"maxLength\"}});jQuery.each({parent:\"a.parentNode\",parents:\"jQuery.dir(a,'parentNode')\",next:\"jQuery.nth(a,2,'nextSibling')\",prev:\"jQuery.nth(a,2,'previousSibling')\",nextAll:\"jQuery.dir(a,'nextSibling')\",prevAll:\"jQuery.dir(a,'previousSibling')\",siblings:\"jQuery.sibling(a.parentNode.firstChild,a)\",children:\"jQuery.sibling(a.firstChild)\",contents:\"jQuery.nodeName(a,'iframe')?a.contentDocument||a.contentWindow.document:jQuery.makeArray(a.childNodes)\"},function(i,n){jQuery.fn[i]=function(a){var ret=jQuery.map(this,n);if(a&&typeof a==\"string\")ret=jQuery.multiFilter(a,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:\"append\",prependTo:\"prepend\",insertBefore:\"before\",insertAfter:\"after\",replaceAll:\"replaceWith\"},function(i,n){jQuery.fn[i]=function(){var a=arguments;return this.each(function(){for(var j=0,al=a.length;j<al;j++)jQuery(a[j])[n](this);});};});jQuery.each({removeAttr:function(key){jQuery.attr(this,key,\"\");this.removeAttribute(key);},addClass:function(c){jQuery.className.add(this,c);},removeClass:function(c){jQuery.className.remove(this,c);},toggleClass:function(c){jQuery.className[jQuery.className.has(this,c)?\"remove\":\"add\"](this,c);},remove:function(a){if(!a||jQuery.filter(a,[this]).r.length){jQuery.removeData(this);this.parentNode.removeChild(this);}},empty:function(){jQuery(\"*\",this).each(function(){jQuery.removeData(this);});while(this.firstChild)this.removeChild(this.firstChild);}},function(i,n){jQuery.fn[i]=function(){return this.each(n,arguments);};});jQuery.each([\"Height\",\"Width\"],function(i,name){var n=name.toLowerCase();jQuery.fn[n]=function(h){return this[0]==window?jQuery.browser.safari&&self[\"inner\"+name]||jQuery.boxModel&&Math.max(document.documentElement[\"client\"+name],document.body[\"client\"+name])||document.body[\"client\"+name]:this[0]==document?Math.max(document.body[\"scroll\"+name],document.body[\"offset\"+name]):h==undefined?(this.length?jQuery.css(this[0],n):null):this.css(n,h.constructor==String?h:h+\"px\");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?\"(?:[\\\\w*_-]|\\\\\\\\.)\":\"(?:[\\\\w\\u0128-\\uFFFF*_-]|\\\\\\\\.)\",quickChild=new RegExp(\"^>\\\\s*(\"+chars+\"+)\"),quickID=new RegExp(\"^(\"+chars+\"+)(#)(\"+chars+\"+)\"),quickClass=new RegExp(\"^([#.]?)(\"+chars+\"*)\");jQuery.extend({expr:{\"\":\"m[2]=='*'||jQuery.nodeName(a,m[2])\",\"#\":\"a.getAttribute('id')==m[2]\",\":\":{lt:\"i<m[3]-0\",gt:\"i>m[3]-0\",nth:\"m[3]-0==i\",eq:\"m[3]-0==i\",first:\"i==0\",last:\"i==r.length-1\",even:\"i%2==0\",odd:\"i%2\",\"first-child\":\"a.parentNode.getElementsByTagName('*')[0]==a\",\"last-child\":\"jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a\",\"only-child\":\"!jQuery.nth(a.parentNode.lastChild,2,'previousSibling')\",parent:\"a.firstChild\",empty:\"!a.firstChild\",contains:\"(a.textContent||a.innerText||'').indexOf(m[3])>=0\",visible:'\"hidden\"!=a.type&&jQuery.css(a,\"display\")!=\"none\"&&jQuery.css(a,\"visibility\")!=\"hidden\"',hidden:'\"hidden\"==a.type||jQuery.css(a,\"display\")==\"none\"||jQuery.css(a,\"visibility\")==\"hidden\"',enabled:\"!a.disabled\",disabled:\"a.disabled\",checked:\"a.checked\",selected:\"a.selected||jQuery.attr(a,'selected')\",text:\"'text'==a.type\",radio:\"'radio'==a.type\",checkbox:\"'checkbox'==a.type\",file:\"'file'==a.type\",password:\"'password'==a.type\",submit:\"'submit'==a.type\",image:\"'image'==a.type\",reset:\"'reset'==a.type\",button:'\"button\"==a.type||jQuery.nodeName(a,\"button\")',input:\"/input|select|textarea|button/i.test(a.nodeName)\",has:\"jQuery.find(m[3],a).length\",header:\"/h\\\\d/i.test(a.nodeName)\",animated:\"jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length\"}},parse:[/^(\\[) *@?([\\w-]+) *([!*$^~=]*) *('?\"?)(.*?)\\4 *\\]/,/^(:)([\\w-]+)\\(\"?'?(.*?(\\(.*?\\))?[^(]*?)\"?'?\\)/,new RegExp(\"^([:.#]*)(\"+chars+\"+)\")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\\s*,\\s*/,\"\");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!=\"string\")return[t];if(context&&!context.nodeType)context=null;context=context||document;var ret=[context],done=[],last;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){var nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName==\"*\"||c.nodeName.toUpperCase()==nodeName.toUpperCase()))r.push(c);ret=r;t=t.replace(re,\"\");if(t.indexOf(\" \")==0)continue;foundToken=true;}else{re=/^([>+~])\\s*(\\w*)/i;if((m=re.exec(t))!=null){r=[];var nodeName=m[2],merge={};m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m==\"~\"||m==\"+\"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m==\"~\"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName.toUpperCase()){if(m==\"~\")merge[id]=true;r.push(n);}if(m==\"+\")break;}}ret=r;t=jQuery.trim(t.replace(re,\"\"));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(\",\")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=\" \"+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\\\/g,\"\");var elem=ret[ret.length-1];if(m[1]==\"#\"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id==\"string\"&&oid.id!=m[2])oid=jQuery('[@id=\"'+m[2]+'\"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]==\"#\"&&m[3]?m[3]:m[1]!=\"\"||m[0]==\"\"?\"*\":m[2];if(tag==\"*\"&&ret[i].nodeName.toLowerCase()==\"object\")tag=\"param\";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==\".\")r=jQuery.classFilter(r,m[2]);if(m[1]==\"#\"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute(\"id\")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,\"\");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=\" \"+m+\" \";var tmp=[];for(var i=0;r[i];i++){var pass=(\" \"+r[i].className+\" \").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\\\/g,\"\");break;}}if(!m)break;if(m[1]==\":\"&&m[2]==\"not\")r=jQuery.filter(m[3],r,true).r;else if(m[1]==\".\")r=jQuery.classFilter(r,m[2],not);else if(m[1]==\"[\"){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==\"\"&&!!z||type==\"=\"&&z==m[5]||type==\"!=\"&&z!=m[5]||type==\"^=\"&&z&&!z.indexOf(m[5])||type==\"$=\"&&z.substr(z.length-m[5].length)==m[5]||(type==\"*=\"||type==\"~=\")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==\":\"&&m[2]==\"nth-child\"){var merge={},tmp=[],test=/(\\d*)n\\+?(\\d*)/.exec(m[3]==\"even\"&&\"2n\"||m[3]==\"odd\"&&\"2n+1\"||!/\\D/.test(m[3])&&\"n+\"+m[3]||m[3]),first=(test[1]||1)-0,last=test[2]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==1){if(last==0||node.nodeIndex==last)add=true;}else if((node.nodeIndex+last)%first==0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var f=jQuery.expr[m[1]];if(typeof f!=\"string\")f=jQuery.expr[m[1]][m[2]];f=eval(\"false||function(a,i){return \"+f+\"}\");r=jQuery.grep(r,f,not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(element,type,handler,data){if(jQuery.browser.msie&&element.setInterval!=undefined)element=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var parts=type.split(\".\");type=parts[0];handler.type=parts[1];var events=jQuery.data(element,\"events\")||jQuery.data(element,\"events\",{});var handle=jQuery.data(element,\"handle\",function(){var val;if(typeof jQuery==\"undefined\"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(element,arguments);return val;});var handlers=events[type];if(!handlers){handlers=events[type]={};if(element.addEventListener)element.addEventListener(type,handle,false);else\nelement.attachEvent(\"on\"+type,handle);}handlers[handler.guid]=handler;this.global[type]=true;},guid:1,global:{},remove:function(element,type,handler){var events=jQuery.data(element,\"events\"),ret,index;if(typeof type==\"string\"){var parts=type.split(\".\");type=parts[0];}if(events){if(type&&type.type){handler=type.handler;type=type.type;}if(!type){for(type in events)this.remove(element,type);}else if(events[type]){if(handler)delete events[type][handler.guid];else\nfor(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(element.removeEventListener)element.removeEventListener(type,jQuery.data(element,\"handle\"),false);else\nelement.detachEvent(\"on\"+type,jQuery.data(element,\"handle\"));ret=null;delete events[type];}}for(ret in events)break;if(!ret){jQuery.removeData(element,\"events\");jQuery.removeData(element,\"handle\");}}},trigger:function(type,data,element,donative,extra){data=jQuery.makeArray(data||[]);if(!element){if(this.global[type])jQuery(\"*\").add([window,document]).trigger(type,data);}else{var val,ret,fn=jQuery.isFunction(element[type]||null),evt=!data[0]||!data[0].preventDefault;if(evt)data.unshift(this.fix({type:type,target:element}));if(jQuery.isFunction(jQuery.data(element,\"handle\")))val=jQuery.data(element,\"handle\").apply(element,data);if(!fn&&element[\"on\"+type]&&element[\"on\"+type].apply(element,data)===false)val=false;if(evt)data.shift();if(extra&&extra.apply(element,data)===false)val=false;if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(element,'a')&&type==\"click\")){this.triggered=true;element[type]();}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(\".\");event.type=parts[0];var c=jQuery.data(this,\"events\")&&jQuery.data(this,\"events\")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in c){args[0].handler=c[j];args[0].data=c[j].data;if(!parts[1]||c[j].type==parts[1]){var tmp=c[j].apply(this,args);if(val!==false)val=tmp;if(tmp===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target&&event.srcElement)event.target=event.srcElement;if(jQuery.browser.safari&&event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var e=document.documentElement,b=document.body;event.pageX=event.clientX+(e&&e.scrollLeft||b.scrollLeft||0);event.pageY=event.clientY+(e&&e.scrollTop||b.scrollTop||0);}if(!event.which&&(event.charCode||event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;}};jQuery.fn.extend({bind:function(type,data,fn){return type==\"unload\"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(){var a=arguments;return this.click(function(e){this.lastToggle=0==this.lastToggle?1:0;e.preventDefault();return a[this.lastToggle].apply(this,[e])||false;});},hover:function(f,g){function handleHover(e){var p=e.relatedTarget;while(p&&p!=this)try{p=p.parentNode;}catch(e){p=this;};if(p==this)return false;return(e.type==\"mouseover\"?f:g).apply(this,[e]);}return this.mouseover(handleHover).mouseout(handleHover);},ready:function(f){bindReady();if(jQuery.isReady)f.apply(document,[jQuery]);else\njQuery.readyList.push(function(){return f.apply(this,[jQuery]);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}if(jQuery.browser.mozilla||jQuery.browser.opera)document.removeEventListener(\"DOMContentLoaded\",jQuery.ready,false);if(!window.frames.length)jQuery(window).load(function(){jQuery(\"#__ie_init\").remove();});}}});jQuery.each((\"blur,focus,load,resize,scroll,unload,click,dblclick,\"+\"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,\"+\"submit,keydown,keypress,keyup,error\").split(\",\"),function(i,o){jQuery.fn[o]=function(f){return f?this.bind(o,f):this.trigger(o);};});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(jQuery.browser.mozilla||jQuery.browser.opera)document.addEventListener(\"DOMContentLoaded\",jQuery.ready,false);else if(jQuery.browser.msie){document.write(\"<scr\"+\"ipt id=__ie_init defer=true \"+\"src=//:><\\/script>\");var script=document.getElementById(\"__ie_init\");if(script)script.onreadystatechange=function(){if(this.readyState!=\"complete\")return;jQuery.ready();};script=null;}else if(jQuery.browser.safari)jQuery.safariTimer=setInterval(function(){if(document.readyState==\"loaded\"||document.readyState==\"complete\"){clearInterval(jQuery.safariTimer);jQuery.safariTimer=null;jQuery.ready();}},10);jQuery.event.add(window,\"load\",jQuery.ready);}jQuery.fn.extend({load:function(url,params,callback){if(jQuery.isFunction(url))return this.bind(\"load\",url);var off=url.indexOf(\" \");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type=\"GET\";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type=\"POST\";}var self=this;jQuery.ajax({url:url,type:type,data:params,complete:function(res,status){if(status==\"success\"||status==\"notmodified\")self.html(selector?jQuery(\"<div/>\").append(res.responseText.replace(/<script(.|\\s)*?\\/script>/g,\"\")).find(selector):res.responseText);setTimeout(function(){self.each(callback,[res.responseText,status,res]);},13);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,\"form\")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(i,val){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each(\"ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend\".split(\",\"),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:\"GET\",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,\"script\");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,\"json\");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:\"POST\",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:\"GET\",timeout:0,contentType:\"application/x-www-form-urlencoded\",processData:true,async:true,data:null},lastModified:{},ajax:function(s){var jsonp,jsre=/=(\\?|%3F)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!=\"string\")s.data=jQuery.param(s.data);var q=s.url.indexOf(\"?\");if(q>-1){s.data=(s.data?s.data+\"&\":\"\")+s.url.slice(q+1);s.url=s.url.slice(0,q);}if(s.dataType==\"jsonp\"){if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+\"&\":\"\")+(s.jsonp||\"callback\")+\"=?\";s.dataType=\"json\";}if(s.dataType==\"json\"&&s.data&&s.data.match(jsre)){jsonp=\"jsonp\"+jsc++;s.data=s.data.replace(jsre,\"=\"+jsonp);s.dataType=\"script\";window[jsonp]=function(tmp){data=tmp;success();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}};}if(s.dataType==\"script\"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()==\"get\")s.data=(s.data?s.data+\"&\":\"\")+\"_=\"+(new Date()).getTime();if(s.data&&s.type.toLowerCase()==\"get\"){s.url+=\"?\"+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger(\"ajaxStart\");if(!s.url.indexOf(\"http\")&&s.dataType==\"script\"){var head=document.getElementsByTagName(\"head\")[0];var script=document.createElement(\"script\");script.src=s.url;if(!jsonp&&(s.success||s.complete)){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState==\"loaded\"||this.readyState==\"complete\")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject(\"Microsoft.XMLHTTP\"):new XMLHttpRequest();xml.open(s.type,s.url,s.async);if(s.data)xml.setRequestHeader(\"Content-Type\",s.contentType);if(s.ifModified)xml.setRequestHeader(\"If-Modified-Since\",jQuery.lastModified[s.url]||\"Thu, 01 Jan 1970 00:00:00 GMT\");xml.setRequestHeader(\"X-Requested-With\",\"XMLHttpRequest\");if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger(\"ajaxSend\",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout==\"timeout\")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout==\"timeout\"&&\"timeout\"||!jQuery.httpSuccess(xml)&&\"error\"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&\"notmodified\"||\"success\";if(status==\"success\"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status=\"parsererror\";}}if(status==\"success\"){var modRes;try{modRes=xml.getResponseHeader(\"Last-Modified\");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else\njQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange(\"timeout\");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();return xml;function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger(\"ajaxSuccess\",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger(\"ajaxComplete\",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger(\"ajaxStop\");}},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger(\"ajaxError\",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol==\"file:\"||(r.status>=200&&r.status<300)||r.status==304||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader(\"Last-Modified\");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader(\"content-type\");var xml=type==\"xml\"||!type&&ct&&ct.indexOf(\"xml\")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName==\"parsererror\")throw\"parsererror\";if(type==\"script\")jQuery.globalEval(data);if(type==\"json\")data=eval(\"(\"+data+\")\");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+\"=\"+encodeURIComponent(this.value));});else\nfor(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+\"=\"+encodeURIComponent(this));});else\ns.push(encodeURIComponent(j)+\"=\"+encodeURIComponent(a[j]));return s.join(\"&\").replace(/%20/g,\"+\");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:\"show\",width:\"show\",opacity:\"show\"},speed,callback):this.filter(\":hidden\").each(function(){this.style.display=this.oldblock?this.oldblock:\"\";if(jQuery.css(this,\"display\")==\"none\")this.style.display=\"block\";}).end();},hide:function(speed,callback){return speed?this.animate({height:\"hide\",width:\"hide\",opacity:\"hide\"},speed,callback):this.filter(\":visible\").each(function(){this.oldblock=this.oldblock||jQuery.css(this,\"display\");if(this.oldblock==\"none\")this.oldblock=\"block\";this.style.display=\"none\";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:\"toggle\",width:\"toggle\",opacity:\"toggle\"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(\":hidden\")?\"show\":\"hide\"]();});},slideDown:function(speed,callback){return this.animate({height:\"show\"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:\"hide\"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:\"toggle\"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:\"show\"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:\"hide\"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var opt=jQuery.speed(speed,easing,callback);return this[opt.queue===false?\"each\":\"queue\"](function(){opt=jQuery.extend({},opt);var hidden=jQuery(this).is(\":hidden\"),self=this;for(var p in prop){if(prop[p]==\"hide\"&&hidden||prop[p]==\"show\"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p==\"height\"||p==\"width\"){opt.display=jQuery.css(this,\"display\");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow=\"hidden\";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val==\"toggle\"?hidden?\"show\":\"hide\":val](prop);else{var parts=val.toString().match(/^([+-]?)([\\d.]+)(.*)$/),start=e.cur(true)||0;if(parts){end=parseFloat(parts[2]),unit=parts[3]||\"px\";if(unit!=\"px\"){self.style[name]=end+unit;start=(end/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]==\"-\"?-1:1)*end)+start;e.custom(start,end,unit);}else\ne.custom(start,val,\"\");}});return true;});},queue:function(type,fn){if(!fn){fn=type;type=\"fx\";}if(!arguments.length)return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(){var timers=jQuery.timers;return this.each(function(){for(var i=0;i<timers.length;i++)if(timers[i].elem==this)timers.splice(i--,1);}).dequeue();}});var queue=function(elem,type,array){if(!elem)return;var q=jQuery.data(elem,type+\"queue\");if(!q||array)q=jQuery.data(elem,type+\"queue\",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||\"fx\";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop==\"height\"||this.prop==\"width\")this.elem.style.display=\"block\";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.curCSS(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.css(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||\"px\";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(){return self.step();}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timers.length==1){var timer=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length)clearInterval(timer);},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop==\"width\"||this.prop==\"height\")this.elem.style[this.prop]=\"1px\";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(){var t=(new Date()).getTime();if(t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,\"display\")==\"none\")this.elem.style.display=\"block\";}if(this.options.hide)this.elem.style.display=\"none\";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?\"swing\":\"linear\")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,\"opacity\",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var absolute=jQuery.css(elem,\"position\")==\"absolute\",parent=elem.parentNode,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&!absolute&&parseInt(version)<522;if(elem.getBoundingClientRect){box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));if(msie){var border=jQuery(\"html\").css(\"borderWidth\");border=(border==\"medium\"||jQuery.boxModel&&parseInt(version)>=7)&&2||border;add(-border,-border);}}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&/^t[d|h]$/i.test(parent.tagName)||!safari2)border(offsetParent);if(safari2&&!absolute&&jQuery.css(offsetParent,\"position\")==\"absolute\")absolute=true;offsetParent=offsetParent.offsetParent;}while(parent.tagName&&/^body|html$/i.test(parent.tagName)){if(/^inline|table-row.*$/i.test(jQuery.css(parent,\"display\")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,\"overflow\")!=\"visible\")border(parent);parent=parent.parentNode;}if(safari&&absolute)add(-doc.body.offsetLeft,-doc.body.offsetTop);}results={top:top,left:left};}return results;function border(elem){add(jQuery.css(elem,\"borderLeftWidth\"),jQuery.css(elem,\"borderTopWidth\"));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}};})();"
  },
  {
    "path": "docs/asciidoc-cheatsheet_files/pygments.css",
    "content": ".highlight  { background: #f4f4f4; }\n.highlight .hll { background-color: #ffffcc }\n.highlight .c { color: #408080; font-style: italic } /* Comment */\n.highlight .err { border: 1px solid #FF0000 } /* Error */\n.highlight .k { color: #008000; font-weight: bold } /* Keyword */\n.highlight .o { color: #666666 } /* Operator */\n.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */\n.highlight .cp { color: #BC7A00 } /* Comment.Preproc */\n.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */\n.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */\n.highlight .gd { color: #A00000 } /* Generic.Deleted */\n.highlight .ge { font-style: italic } /* Generic.Emph */\n.highlight .gr { color: #FF0000 } /* Generic.Error */\n.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */\n.highlight .gi { color: #00A000 } /* Generic.Inserted */\n.highlight .go { color: #808080 } /* Generic.Output */\n.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */\n.highlight .gs { font-weight: bold } /* Generic.Strong */\n.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */\n.highlight .gt { color: #0040D0 } /* Generic.Traceback */\n.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */\n.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\n.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\n.highlight .kp { color: #008000 } /* Keyword.Pseudo */\n.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\n.highlight .kt { color: #B00040 } /* Keyword.Type */\n.highlight .m { color: #666666 } /* Literal.Number */\n.highlight .s { color: #BA2121 } /* Literal.String */\n.highlight .na { color: #7D9029 } /* Name.Attribute */\n.highlight .nb { color: #008000 } /* Name.Builtin */\n.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */\n.highlight .no { color: #880000 } /* Name.Constant */\n.highlight .nd { color: #AA22FF } /* Name.Decorator */\n.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */\n.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */\n.highlight .nf { color: #0000FF } /* Name.Function */\n.highlight .nl { color: #A0A000 } /* Name.Label */\n.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\n.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */\n.highlight .nv { color: #19177C } /* Name.Variable */\n.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\n.highlight .w { color: #bbbbbb } /* Text.Whitespace */\n.highlight .mf { color: #666666 } /* Literal.Number.Float */\n.highlight .mh { color: #666666 } /* Literal.Number.Hex */\n.highlight .mi { color: #666666 } /* Literal.Number.Integer */\n.highlight .mo { color: #666666 } /* Literal.Number.Oct */\n.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */\n.highlight .sc { color: #BA2121 } /* Literal.String.Char */\n.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\n.highlight .s2 { color: #BA2121 } /* Literal.String.Double */\n.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */\n.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */\n.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */\n.highlight .sx { color: #008000 } /* Literal.String.Other */\n.highlight .sr { color: #BB6688 } /* Literal.String.Regex */\n.highlight .s1 { color: #BA2121 } /* Literal.String.Single */\n.highlight .ss { color: #19177C } /* Literal.String.Symbol */\n.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */\n.highlight .vc { color: #19177C } /* Name.Variable.Class */\n.highlight .vg { color: #19177C } /* Name.Variable.Global */\n.highlight .vi { color: #19177C } /* Name.Variable.Instance */\n.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */\n"
  },
  {
    "path": "docs/asciidoc-userguide.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\"><head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n<meta name=\"generator\" content=\"AsciiDoc 8.6.8\">\n<title>AsciiDoc User Guide</title>\n<link rel=\"stylesheet\" href=\"asciidoc-userguide_files/asciidoc.css\" type=\"text/css\">\n<link rel=\"stylesheet\" href=\"asciidoc-userguide_files/layout2.css\" type=\"text/css\">\n<script type=\"text/javascript\" src=\"asciidoc-userguide_files/asciidoc.js\"></script>\n<script type=\"text/javascript\">\n/*<![CDATA[*/\nasciidoc.install(2);\n/*]]>*/\n</script>\n<link href=\"asciidoc-userguide_files/Content.css\" type=\"text/css\" rel=\"stylesheet\"></head>\n<body style=\"max-width:70em\">\n<div id=\"layout-menu-box\">\n<div id=\"layout-menu\">\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/index.html\">Home</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/userguide.html\">User&nbsp;Guide</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/INSTALL.html\">Installation</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/faq.html\">FAQ</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/manpage.html\">asciidoc(1)</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/a2x.1.html\">a2x(1)</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/asciidocapi.html\">API</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/plugins.html\">Plugins</a></div>\n  <div>»<a href=\"http://powerman.name/doc/asciidoc\">Cheatsheet</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/testasciidoc.html\">Tests</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/CHANGELOG.html\">ChangeLog</a></div>\n  <div>»<a href=\"http://www.methods.co.nz/asciidoc/support.html\">Support</a></div>\n  <div id=\"page-source\">»<a href=\"http://www.methods.co.nz/asciidoc/userguide.txt\">Page&nbsp;Source</a></div>\n</div>\n</div>\n<div id=\"layout-content-box\">\n<div id=\"layout-banner\">\n  <div id=\"layout-title\">AsciiDoc</div>\n  <div id=\"layout-description\">Text based document generation</div>\n</div>\n<div id=\"layout-content\">\n<div id=\"header\">\n<h1>AsciiDoc User Guide</h1>\n<span id=\"author\">Stuart Rackham</span><br>\n<span id=\"email\"><code>&lt;<a href=\"mailto:srackham@gmail.com\">srackham@gmail.com</a>&gt;</code></span><br>\n<span id=\"revision\">version 8.6.8,</span>\n17 July 2012\n<div id=\"toc\">\n  <div id=\"toctitle\">Table of Contents</div>\n  <noscript><p><b>JavaScript must be enabled in your browser to display the table of contents.</b></p></noscript>\n</div>\n</div>\n<div id=\"content\">\n<div id=\"preamble\">\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc is a text document format for writing notes, documentation,\narticles, books, ebooks, slideshows, web pages, blogs and UNIX man\npages.  AsciiDoc files can be translated to many formats including\nHTML, PDF, EPUB, man page.  AsciiDoc is highly configurable: both the\nAsciiDoc source file syntax and the backend output markups (which can\nbe almost any type of SGML/XML markup) can be customized and extended\nby the user.</p></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">This document</div>\n<div class=\"paragraph\"><p>This is an overly large document, it probably needs to be refactored\ninto a Tutorial, Quick Reference and Formal Reference.</p></div>\n<div class=\"paragraph\"><p>If you’re new to AsciiDoc read this section and the <a href=\"#X6\">Getting Started</a> section and take a look at the example AsciiDoc (<code>*.txt</code>)\nsource files in the distribution <code>doc</code> directory.</p></div>\n</div></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_introduction\">1. Introduction</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc is a plain text human readable/writable document format that\ncan be translated to DocBook or HTML using the <code>asciidoc(1)</code> command.\nYou can then either use <code>asciidoc(1)</code> generated HTML directly or run\n<code>asciidoc(1)</code> DocBook output through your favorite DocBook toolchain or\nuse the AsciiDoc <code>a2x(1)</code> toolchain wrapper to produce PDF, EPUB, DVI,\nLaTeX, PostScript, man page, HTML and text formats.</p></div>\n<div class=\"paragraph\"><p>The AsciiDoc format is a useful presentation format in its own right:\nAsciiDoc markup is simple, intuitive and as such is easily proofed and\nedited.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc is light weight: it consists of a single Python script and a\nbunch of configuration files. Apart from <code>asciidoc(1)</code> and a Python\ninterpreter, no other programs are required to convert AsciiDoc text\nfiles to DocBook or HTML. See <a href=\"#X11\">Example AsciiDoc Documents</a>\nbelow.</p></div>\n<div class=\"paragraph\"><p>Text markup conventions tend to be a matter of (often strong) personal\npreference: if the default syntax is not to your liking you can define\nyour own by editing the text based <code>asciidoc(1)</code> configuration files.\nYou can also create configuration files to translate AsciiDoc\ndocuments to almost any SGML/XML markup.</p></div>\n<div class=\"paragraph\"><p><code>asciidoc(1)</code> comes with a set of configuration files to translate\nAsciiDoc articles, books and man pages to HTML or DocBook backend\nformats.</p></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">My AsciiDoc Itch</div>\n<div class=\"paragraph\"><p>DocBook has emerged as the de facto standard Open Source documentation\nformat. But DocBook is a complex language, the markup is difficult to\nread and even more difficult to write directly — I found I was\nspending more time typing markup tags, consulting reference manuals\nand fixing syntax errors, than I was writing the documentation.</p></div>\n</div></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X6\">2. Getting Started</h2>\n<div class=\"sectionbody\">\n<div class=\"sect2\">\n<h3 id=\"_installing_asciidoc\">2.1. Installing AsciiDoc</h3>\n<div class=\"paragraph\"><p>See the <code>README</code> and <code>INSTALL</code> files for install prerequisites and\nprocedures. Packagers take a look at <a href=\"#X38\">Packager Notes</a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X11\">2.2. Example AsciiDoc Documents</h3>\n<div class=\"paragraph\"><p>The best way to quickly get a feel for AsciiDoc is to view the\nAsciiDoc web site and/or distributed examples:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nTake a look at the linked examples on the AsciiDoc web site home\n  page <a href=\"http://www.methods.co.nz/asciidoc/\">http://www.methods.co.nz/asciidoc/</a>.  Press the <em>Page Source</em> sidebar menu item to view\n  corresponding AsciiDoc source.\n</p>\n</li>\n<li>\n<p>\nRead the <code>*.txt</code> source files in the distribution <code>./doc</code> directory\n  along with the corresponding HTML and DocBook XML files.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_asciidoc_document_types\">3. AsciiDoc Document Types</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>There are three types of AsciiDoc documents: article, book and\nmanpage. All document types share the same AsciiDoc format with some\nminor variations. If you are familiar with DocBook you will have\nnoticed that AsciiDoc document types correspond to the same-named\nDocBook document types.</p></div>\n<div class=\"paragraph\"><p>Use the <code>asciidoc(1)</code> <code>-d</code> (<code>--doctype</code>) option to specify the AsciiDoc\ndocument type — the default document type is <em>article</em>.</p></div>\n<div class=\"paragraph\"><p>By convention the <code>.txt</code> file extension is used for AsciiDoc document\nsource files.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_article\">3.1. article</h3>\n<div class=\"paragraph\"><p>Used for short documents, articles and general documentation.  See the\nAsciiDoc distribution <code>./doc/article.txt</code> example.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc defines standard DocBook article frontmatter and backmatter\n<a href=\"#X93\">section markup templates</a> (appendix, abstract, bibliography,\nglossary, index).</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_book\">3.2. book</h3>\n<div class=\"paragraph\"><p>Books share the same format as articles, with the following\ndifferences:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe part titles in multi-part books are <a href=\"#X17\">top level titles</a>\n  (same level as book title).\n</p>\n</li>\n<li>\n<p>\nSome sections are book specific e.g. preface and colophon.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Book documents will normally be used to produce DocBook output since\nDocBook processors can automatically generate footnotes, table of\ncontents, list of tables, list of figures, list of examples and\nindexes.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc defines standard DocBook book frontmatter and backmatter\n<a href=\"#X93\">section markup templates</a> (appendix, dedication, preface,\nbibliography, glossary, index, colophon).</p></div>\n<div class=\"dlist\"><div class=\"title\">Example book documents</div><dl>\n<dt class=\"hdlist1\">\nBook\n</dt>\n<dd>\n<p>\n  The <code>./doc/book.txt</code> file in the AsciiDoc distribution.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nMulti-part book\n</dt>\n<dd>\n<p>\n  The <code>./doc/book-multi.txt</code> file in the AsciiDoc distribution.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_manpage\">3.3. manpage</h3>\n<div class=\"paragraph\"><p>Used to generate roff format UNIX manual pages.  AsciiDoc manpage\ndocuments observe special header title and section naming conventions — see the <a href=\"#X1\">Manpage Documents</a> section for details.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc defines the <em>synopsis</em> <a href=\"#X93\">section markup template</a> to\ngenerate the DocBook <code>refsynopsisdiv</code> section.</p></div>\n<div class=\"paragraph\"><p>See also the <code>asciidoc(1)</code> man page source (<code>./doc/asciidoc.1.txt</code>) from\nthe AsciiDoc distribution.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X5\">4. AsciiDoc Backends</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <code>asciidoc(1)</code> command translates an AsciiDoc formatted file to the\nbackend format specified by the <code>-b</code> (<code>--backend</code>) command-line\noption. <code>asciidoc(1)</code> itself has little intrinsic knowledge of backend\nformats, all translation rules are contained in customizable cascading\nconfiguration files. Backend specific attributes are listed in the\n<a href=\"#X88\">Backend Attributes</a> section.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndocbook45\n</dt>\n<dd>\n<p>\n  Outputs DocBook XML 4.5 markup.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nhtml4\n</dt>\n<dd>\n<p>\n  This backend generates plain HTML 4.01 Transitional markup.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nxhtml11\n</dt>\n<dd>\n<p>\n  This backend generates XHTML 1.1 markup styled with CSS2. Output\n  files have an <code>.html</code> extension.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nhtml5\n</dt>\n<dd>\n<p>\n  This backend generates HTML 5 markup, apart from the inclusion of\n  <a href=\"#X98\">audio and video block macros</a> it is functionally identical to\n  the <em>xhtml11</em> backend.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nslidy\n</dt>\n<dd>\n<p>\n  Use this backend to generate self-contained\n  <a href=\"http://www.w3.org/Talks/Tools/Slidy2/\">Slidy</a> HTML slideshows for\n  your web browser from AsciiDoc documents. The Slidy backend is\n  documented in the distribution <code>doc/slidy.txt</code> file and\n  <a href=\"http://www.methods.co.nz/asciidoc/slidy.html\">online</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nwordpress\n</dt>\n<dd>\n<p>\n  A minor variant of the <em>html4</em> backend to support\n  <a href=\"http://srackham.wordpress.com/blogpost1/\">blogpost</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nlatex\n</dt>\n<dd>\n<p>\n  Experimental LaTeX backend.\n</p>\n</dd>\n</dl></div>\n<div class=\"sect2\">\n<h3 id=\"_backend_aliases\">4.1. Backend Aliases</h3>\n<div class=\"paragraph\"><p>Backend aliases are alternative names for AsciiDoc backends.  AsciiDoc\ncomes with two backend aliases: <em>html</em> (aliased to <em>xhtml11</em>) and\n<em>docbook</em> (aliased to <em>docbook45</em>).</p></div>\n<div class=\"paragraph\"><p>You can assign (or reassign) backend aliases by setting an AsciiDoc\nattribute named like <code>backend-alias-&lt;alias&gt;</code> to an AsciiDoc backend\nname. For example, the following backend alias attribute definitions\nappear in the <code>[attributes]</code> section of the global <code>asciidoc.conf</code>\nconfiguration file:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>backend-alias-html=xhtml11\nbackend-alias-docbook=docbook45</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X100\">4.2. Backend Plugins</h3>\n<div class=\"paragraph\"><p>The <code>asciidoc(1)</code> <code>--backend</code> option is also used to install and manage\nbackend <a href=\"#X101\">plugins</a>.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nA backend plugin is used just like the built-in backends.\n</p>\n</li>\n<li>\n<p>\nBackend plugins <a href=\"#X27\">take precedence</a> over built-in backends with\n  the same name.\n</p>\n</li>\n<li>\n<p>\nYou can use the <code>{asciidoc-confdir}</code> <a href=\"#X60\">intrinsic attribute</a> to\n  refer to the built-in backend configuration file location from\n  backend plugin configuration files.\n</p>\n</li>\n<li>\n<p>\nYou can use the <code>{backend-confdir}</code> <a href=\"#X60\">intrinsic attribute</a> to\n  refer to the backend plugin configuration file location.\n</p>\n</li>\n<li>\n<p>\nBy default backends plugins are installed in\n  <code>$HOME/.asciidoc/backends/&lt;backend&gt;</code> where <code>&lt;backend&gt;</code> is the\n  backend name.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_docbook\">5. DocBook</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc generates <em>article</em>, <em>book</em> and <em>refentry</em>\n<a href=\"http://www.docbook.org/\">DocBook</a> documents (corresponding to the\nAsciiDoc <em>article</em>, <em>book</em> and <em>manpage</em> document types).</p></div>\n<div class=\"paragraph\"><p>Most Linux distributions come with conversion tools (collectively\ncalled a toolchain) for <a href=\"#X12\">converting DocBook files</a> to\npresentation formats such as Postscript, HTML, PDF, EPUB, DVI,\nPostScript, LaTeX, roff (the native man page format), HTMLHelp,\nJavaHelp and text.  There are also programs that allow you to view\nDocBook files directly, for example <a href=\"http://live.gnome.org/Yelp\">Yelp</a>\n(the GNOME help viewer).</p></div>\n<div class=\"sect2\">\n<h3 id=\"X12\">5.1. Converting DocBook to other file formats</h3>\n<div class=\"paragraph\"><p>DocBook files are validated, parsed and translated various\npresentation file formats using a combination of applications\ncollectively called a DocBook <em>tool chain</em>. The function of a tool\nchain is to read the DocBook markup (produced by AsciiDoc) and\ntransform it to a presentation format (for example HTML, PDF, HTML\nHelp, EPUB, DVI, PostScript, LaTeX).</p></div>\n<div class=\"paragraph\"><p>A wide range of user output format requirements coupled with a choice\nof available tools and stylesheets results in many valid tool chain\ncombinations.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X43\">5.2. a2x Toolchain Wrapper</h3>\n<div class=\"paragraph\"><p>One of the biggest hurdles for new users is installing, configuring\nand using a DocBook XML toolchain. <code>a2x(1)</code> can help — it’s a\ntoolchain wrapper command that will generate XHTML (chunked and\nunchunked), PDF, EPUB, DVI, PS, LaTeX, man page, HTML Help and text\nfile outputs from an AsciiDoc text file.  <code>a2x(1)</code> does all the grunt\nwork associated with generating and sequencing the toolchain commands\nand managing intermediate and output files.  <code>a2x(1)</code> also optionally\ndeploys admonition and navigation icons and a CSS stylesheet. See the\n<code>a2x(1)</code> man page for more details. In addition to <code>asciidoc(1)</code> you\nalso need <a href=\"#X40\">xsltproc(1)</a>, <a href=\"#X13\">DocBook XSL Stylesheets</a> and\noptionally: <a href=\"#X31\">dblatex</a> or <a href=\"#X14\">FOP</a> (to generate PDF);\n<code>w3m(1)</code> or <code>lynx(1)</code> (to generate text).</p></div>\n<div class=\"paragraph\"><p>The following examples generate <code>doc/source-highlight-filter.pdf</code> from\nthe AsciiDoc <code>doc/source-highlight-filter.txt</code> source file. The first\nexample uses <code>dblatex(1)</code> (the default PDF generator) the second\nexample forces FOP to be used:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ a2x -f pdf doc/source-highlight-filter.txt\n$ a2x -f pdf --fop doc/source-highlight-filter.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>See the <code>a2x(1)</code> man page for details.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">Use the <code>--verbose</code> command-line option to view executed\ntoolchain commands.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_html_generation\">5.3. HTML generation</h3>\n<div class=\"paragraph\"><p>AsciiDoc produces nicely styled HTML directly without requiring a\nDocBook toolchain but there are also advantages in going the DocBook\nroute:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nHTML from DocBook can optionally include automatically generated\n  indexes, tables of contents, footnotes, lists of figures and tables.\n</p>\n</li>\n<li>\n<p>\nDocBook toolchains can also (optionally) generate separate (chunked)\n  linked HTML pages for each document section.\n</p>\n</li>\n<li>\n<p>\nToolchain processing performs link and document validity checks.\n</p>\n</li>\n<li>\n<p>\nIf the DocBook <em>lang</em> attribute is set then things like table of\n  contents, figure and table captions and admonition captions will be\n  output in the specified language (setting the AsciiDoc <em>lang</em>\n  attribute sets the DocBook <em>lang</em> attribute).\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>On the other hand, HTML output directly from AsciiDoc is much faster,\nis easily customized and can be used in situations where there is no\nsuitable DocBook toolchain (for example, see the <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc\nwebsite</a>).</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_pdf_generation\">5.4. PDF generation</h3>\n<div class=\"paragraph\"><p>There are two commonly used tools to generate PDFs from DocBook,\n<a href=\"#X31\">dblatex</a> and <a href=\"#X14\">FOP</a>.</p></div>\n<div class=\"ulist\"><div class=\"title\">dblatex or FOP?</div><ul>\n<li>\n<p>\n<em>dblatex</em> is easier to install, there’s zero configuration\n  required and no Java VM to install — it just works out of the box.\n</p>\n</li>\n<li>\n<p>\n<em>dblatex</em> source code highlighting and numbering is superb.\n</p>\n</li>\n<li>\n<p>\n<em>dblatex</em> is easier to use as it converts DocBook directly to PDF\n  whereas before using <em>FOP</em> you have to convert DocBook to XML-FO\n  using <a href=\"#X13\">DocBook XSL Stylesheets</a>.\n</p>\n</li>\n<li>\n<p>\n<em>FOP</em> is more feature complete (for example, callouts are processed\n  inside literal layouts) and arguably produces nicer looking output.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_html_help_generation\">5.5. HTML Help generation</h3>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nConvert DocBook XML documents to HTML Help compiler source files\n  using <a href=\"#X13\">DocBook XSL Stylesheets</a> and <a href=\"#X40\">xsltproc(1)</a>.\n</p>\n</li>\n<li>\n<p>\nConvert the HTML Help source (<code>.hhp</code> and <code>.html</code>) files to HTML Help\n  (<code>.chm</code>) files using the <a href=\"#X67\">Microsoft HTML Help Compiler</a>.\n</p>\n</li>\n</ol></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_toolchain_components_summary\">5.6. Toolchain components summary</h3>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nAsciiDoc\n</dt>\n<dd>\n<p>\n    Converts AsciiDoc (<code>.txt</code>) files to DocBook XML (<code>.xml</code>) files.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X13\"></a><a href=\"http://docbook.sourceforge.net/projects/xsl/\">DocBook XSL Stylesheets</a>\n</dt>\n<dd>\n<p>\n  These are a set of XSL stylesheets containing rules for converting\n  DocBook XML documents to HTML, XSL-FO, manpage and HTML Help files.\n  The stylesheets are used in conjunction with an XML parser such as\n  <a href=\"#X40\">xsltproc(1)</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X40\"></a><a href=\"http://www.xmlsoft.org/\">xsltproc</a>\n</dt>\n<dd>\n<p>\n  An XML parser for applying XSLT stylesheets (in our case the\n  <a href=\"#X13\">DocBook XSL Stylesheets</a>) to XML documents.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X31\"></a><a href=\"http://dblatex.sourceforge.net/\">dblatex</a>\n</dt>\n<dd>\n<p>\n  Generates PDF, DVI, PostScript and LaTeX formats directly from\n  DocBook source via the intermediate LaTeX typesetting language —   uses <a href=\"#X13\">DocBook XSL Stylesheets</a>, <a href=\"#X40\">xsltproc(1)</a> and\n  <code>latex(1)</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X14\"></a><a href=\"http://xml.apache.org/fop/\">FOP</a>\n</dt>\n<dd>\n<p>\n  The Apache Formatting Objects Processor converts XSL-FO (<code>.fo</code>)\n  files to PDF files.  The XSL-FO files are generated from DocBook\n  source files using <a href=\"#X13\">DocBook XSL Stylesheets</a> and\n  <a href=\"#X40\">xsltproc(1)</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X67\"></a>Microsoft Help Compiler\n</dt>\n<dd>\n<p>\n  The Microsoft HTML Help Compiler (<code>hhc.exe</code>) is a command-line tool\n  that converts HTML Help source files to a single HTML Help (<code>.chm</code>)\n  file. It runs on MS Windows platforms and can be downloaded from\n  <a href=\"http://www.microsoft.com/\">http://www.microsoft.com</a>.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_asciidoc_dblatex_configuration_files\">5.7. AsciiDoc dblatex configuration files</h3>\n<div class=\"paragraph\"><p>The AsciiDoc distribution <code>./dblatex</code> directory contains\n<code>asciidoc-dblatex.xsl</code> (customized XSL parameter settings) and\n<code>asciidoc-dblatex.sty</code> (customized LaTeX settings). These are examples\nof optional <a href=\"#X31\">dblatex</a> output customization and are used by\n<a href=\"#X43\"><code>a2x(1)</code></a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_asciidoc_docbook_xsl_stylesheets_drivers\">5.8. AsciiDoc DocBook XSL Stylesheets drivers</h3>\n<div class=\"paragraph\"><p>You will have noticed that the distributed HTML and HTML Help\ndocumentation files (for example <code>./doc/asciidoc.html</code>) are not the\nplain outputs produced using the default <em>DocBook XSL Stylesheets</em>\nconfiguration.  This is because they have been processed using\ncustomized DocBook XSL Stylesheets along with (in the case of HTML\noutputs) the custom <code>./stylesheets/docbook-xsl.css</code> CSS stylesheet.</p></div>\n<div class=\"paragraph\"><p>You’ll find the customized DocBook XSL drivers along with additional\ndocumentation in the distribution <code>./docbook-xsl</code> directory. The\nexamples that follow are executed from the distribution documentation\n(<code>./doc</code>) directory. These drivers are also used by <a href=\"#X43\"><code>a2x(1)</code></a>.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>common.xsl</code>\n</dt>\n<dd>\n<p>\n    Shared driver parameters.  This file is not used directly but is\n    included in all the following drivers.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>chunked.xsl</code>\n</dt>\n<dd>\n<p>\n    Generate chunked XHTML (separate HTML pages for each document\n    section) in the <code>./doc/chunked</code> directory. For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ python ../asciidoc.py -b docbook asciidoc.txt\n$ xsltproc --nonet ../docbook-xsl/chunked.xsl asciidoc.xml</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>epub.xsl</code>\n</dt>\n<dd>\n<p>\n    Used by <a href=\"#X43\"><code>a2x(1)</code></a> to generate EPUB formatted documents.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>fo.xsl</code>\n</dt>\n<dd>\n<p>\n    Generate XSL Formatting Object (<code>.fo</code>) files for subsequent PDF\n    file generation using FOP. For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ python ../asciidoc.py -b docbook article.txt\n$ xsltproc --nonet ../docbook-xsl/fo.xsl article.xml &gt; article.fo\n$ fop article.fo article.pdf</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>htmlhelp.xsl</code>\n</dt>\n<dd>\n<p>\n    Generate Microsoft HTML Help source files for the MS HTML Help\n    Compiler in the <code>./doc/htmlhelp</code> directory. This example is run on\n    MS Windows from a Cygwin shell prompt:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ python ../asciidoc.py -b docbook asciidoc.txt\n$ xsltproc --nonet ../docbook-xsl/htmlhelp.xsl asciidoc.xml\n$ c:/Program\\ Files/HTML\\ Help\\ Workshop/hhc.exe htmlhelp.hhp</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>manpage.xsl</code>\n</dt>\n<dd>\n<p>\n    Generate a <code>roff(1)</code> format UNIX man page from a DocBook XML\n    <em>refentry</em> document. This example generates an <code>asciidoc.1</code> man\n    page file:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ python ../asciidoc.py -d manpage -b docbook asciidoc.1.txt\n$ xsltproc --nonet ../docbook-xsl/manpage.xsl asciidoc.1.xml</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>xhtml.xsl</code>\n</dt>\n<dd>\n<p>\n    Convert a DocBook XML file to a single XHTML file. For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ python ../asciidoc.py -b docbook asciidoc.txt\n$ xsltproc --nonet ../docbook-xsl/xhtml.xsl asciidoc.xml &gt; asciidoc.html</code></pre>\n</div></div>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>If you want to see how the complete documentation set is processed\ntake a look at the A-A-P script <code>./doc/main.aap</code>.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_generating_plain_text_files\">6. Generating Plain Text Files</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc does not have a text backend (for most purposes AsciiDoc\nsource text is fine), however you can convert AsciiDoc text files to\nformatted text using the AsciiDoc <a href=\"#X43\"><code>a2x(1)</code></a> toolchain wrapper\nutility.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X35\">7. HTML5 and XHTML 1.1</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <em>xhtml11</em> and <em>html5</em> backends embed or link CSS and JavaScript\nfiles in their outputs, there is also a <a href=\"#X99\">themes</a> plugin\nframework.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf the AsciiDoc <em>linkcss</em> attribute is defined then CSS and\n  JavaScript files are linked to the output document, otherwise they\n  are embedded (the default behavior).\n</p>\n</li>\n<li>\n<p>\nThe default locations for CSS and JavaScript files can be changed by\n  setting the AsciiDoc <em>stylesdir</em> and <em>scriptsdir</em> attributes\n  respectively.\n</p>\n</li>\n<li>\n<p>\nThe default locations for embedded and linked files differ and are\n  calculated at different times — embedded files are loaded when\n  <code>asciidoc(1)</code> generates the output document, linked files are loaded\n  by the browser when the user views the output document.\n</p>\n</li>\n<li>\n<p>\nEmbedded files are automatically inserted in the output files but\n  you need to manually copy linked CSS and Javascript files from\n  AsciiDoc <a href=\"#X27\">configuration directories</a> to the correct location\n  relative to the output document.\n</p>\n</li>\n</ul></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 1. Stylesheet file locations</caption>\n<colgroup><col width=\"33%\">\n<col width=\"33%\">\n<col width=\"33%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\"><em>stylesdir</em> attribute</th>\n<th valign=\"top\" align=\"left\">Linked location (<em>linkcss</em> attribute defined)</th>\n<th valign=\"top\" align=\"left\">Embedded location (<em>linkcss</em> attribute undefined)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Undefined (default).</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Same directory as the output document.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><code>stylesheets</code> subdirectory in the AsciiDoc configuration directory\n(the directory containing the backend conf file).</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative directory name.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative to the output document.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative to the AsciiDoc configuration directory (the\ndirectory containing the backend conf file).</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 2. JavaScript file locations</caption>\n<colgroup><col width=\"33%\">\n<col width=\"33%\">\n<col width=\"33%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\"><em>scriptsdir</em> attribute</th>\n<th valign=\"top\" align=\"left\">Linked location (<em>linkcss</em> attribute defined)</th>\n<th valign=\"top\" align=\"left\">Embedded location (<em>linkcss</em> attribute undefined)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Undefined (default).</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Same directory as the output document.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><code>javascripts</code> subdirectory in the AsciiDoc configuration directory\n(the directory containing the backend conf file).</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative directory name.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative to the output document.</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Absolute or relative to the AsciiDoc configuration directory (the\ndirectory containing the backend conf file).</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X99\">7.1. Themes</h3>\n<div class=\"paragraph\"><p>The AsciiDoc <em>theme</em> attribute is used to select an alternative CSS\nstylesheet and to optionally include additional JavaScript code.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nTheme files reside in an AsciiDoc <a href=\"#X27\">configuration directory</a>\n  named <code>themes/&lt;theme&gt;/</code> (where <code>&lt;theme&gt;</code> is the the theme name set\n  by the <em>theme</em> attribute). <code>asciidoc(1)</code> sets the <em>themedir</em> attribute\n  to the theme directory path name.\n</p>\n</li>\n<li>\n<p>\nThe <em>theme</em> attribute can also be set using the <code>asciidoc(1)</code>\n  <code>--theme</code> option, the <code>--theme</code> option can also be used to manage\n  theme <a href=\"#X101\">plugins</a>.\n</p>\n</li>\n<li>\n<p>\nAsciiDoc ships with two themes: <em>flask</em> and <em>volnitsky</em>.\n</p>\n</li>\n<li>\n<p>\nThe <code>&lt;theme&gt;.css</code> file replaces the default <code>asciidoc.css</code> CSS file.\n</p>\n</li>\n<li>\n<p>\nThe <code>&lt;theme&gt;.js</code> file is included in addition to the default\n  <code>asciidoc.js</code> JavaScript file.\n</p>\n</li>\n<li>\n<p>\nIf the <a href=\"#X66\">data-uri</a> attribute is defined then icons are loaded\n  from the theme <code>icons</code> sub-directory if it exists (i.e.  the\n  <em>iconsdir</em> attribute is set to theme <code>icons</code> sub-directory path).\n</p>\n</li>\n<li>\n<p>\nEmbedded theme files are automatically inserted in the output files\n  but you need to manually copy linked CSS and Javascript files to the\n  location of the output documents.\n</p>\n</li>\n<li>\n<p>\nLinked CSS and JavaScript theme files are linked to the same linked\n  locations as <a href=\"#X35\">other CSS and JavaScript files</a>.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>For example, the command-line option <code>--theme foo</code> (or <code>--attribute\ntheme=foo</code>) will cause <code>asciidoc(1)</code> to search <a href=\"#X27\">configuration file locations 1, 2 and 3</a> for a sub-directory called <code>themes/foo</code>\ncontaining the stylesheet <code>foo.css</code> and optionally a JavaScript file\nname <code>foo.js</code>.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_document_structure\">8. Document Structure</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>An AsciiDoc document consists of a series of <a href=\"#X8\">block elements</a>\nstarting with an optional document Header, followed by an optional\nPreamble, followed by zero or more document Sections.</p></div>\n<div class=\"paragraph\"><p>Almost any combination of zero or more elements constitutes a valid\nAsciiDoc document: documents can range from a single sentence to a\nmulti-part book.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_block_elements\">8.1. Block Elements</h3>\n<div class=\"paragraph\"><p>Block elements consist of one or more lines of text and may contain\nother block elements.</p></div>\n<div class=\"paragraph\"><p>The AsciiDoc block structure can be informally summarized as follows\n<span class=\"footnote\"><br>[This is a rough structural guide, not a rigorous syntax\ndefinition]<br></span>:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Document      ::= (Header?,Preamble?,Section*)\nHeader        ::= (Title,(AuthorInfo,RevisionInfo?)?)\nAuthorInfo    ::= (FirstName,(MiddleName?,LastName)?,EmailAddress?)\nRevisionInfo  ::= (RevisionNumber?,RevisionDate,RevisionRemark?)\nPreamble      ::= (SectionBody)\nSection       ::= (Title,SectionBody?,(Section)*)\nSectionBody   ::= ((BlockTitle?,Block)|BlockMacro)+\nBlock         ::= (Paragraph|DelimitedBlock|List|Table)\nList          ::= (BulletedList|NumberedList|LabeledList|CalloutList)\nBulletedList  ::= (ListItem)+\nNumberedList  ::= (ListItem)+\nCalloutList   ::= (ListItem)+\nLabeledList   ::= (ListEntry)+\nListEntry     ::= (ListLabel,ListItem)\nListLabel     ::= (ListTerm+)\nListItem      ::= (ItemText,(List|ListParagraph|ListContinuation)*)</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Where:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>?</em> implies zero or one occurrence, <em>+</em> implies one or more\n  occurrences, <em>*</em> implies zero or more occurrences.\n</p>\n</li>\n<li>\n<p>\nAll block elements are separated by line boundaries.\n</p>\n</li>\n<li>\n<p>\n<code>BlockId</code>, <code>AttributeEntry</code> and <code>AttributeList</code> block elements (not\n  shown) can occur almost anywhere.\n</p>\n</li>\n<li>\n<p>\nThere are a number of document type and backend specific\n  restrictions imposed on the block syntax.\n</p>\n</li>\n<li>\n<p>\nThe following elements cannot contain blank lines: Header, Title,\n  Paragraph, ItemText.\n</p>\n</li>\n<li>\n<p>\nA ListParagraph is a Paragraph with its <em>listelement</em> option set.\n</p>\n</li>\n<li>\n<p>\nA ListContinuation is a <a href=\"#X15\">list continuation element</a>.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X95\">8.2. Header</h3>\n<div class=\"paragraph\"><p>The Header contains document meta-data, typically title plus optional\nauthorship and revision information:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe Header is optional, but if it is used it must start with a\n  document <a href=\"#X17\">title</a>.\n</p>\n</li>\n<li>\n<p>\nOptional Author and Revision information immediately follows the\n  header title.\n</p>\n</li>\n<li>\n<p>\nThe document header must be separated from the remainder of the\n  document by one or more blank lines and cannot contain blank lines.\n</p>\n</li>\n<li>\n<p>\nThe header can include comments.\n</p>\n</li>\n<li>\n<p>\nThe header can include <a href=\"#X18\">attribute entries</a>, typically\n  <em>doctype</em>, <em>lang</em>, <em>encoding</em>, <em>icons</em>, <em>data-uri</em>, <em>toc</em>,\n  <em>numbered</em>.\n</p>\n</li>\n<li>\n<p>\nHeader attributes are overridden by command-line attributes.\n</p>\n</li>\n<li>\n<p>\nIf the header contains non-UTF-8 characters then the <em>encoding</em> must\n  precede the header (either in the document or on the command-line).\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Here’s an example AsciiDoc document header:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Writing Documentation using AsciiDoc\n====================================\nJoe Bloggs &lt;jbloggs@mymail.com&gt;\nv2.0, February 2003:\nRewritten for version 2 release.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The author information line contains the author’s name optionally\nfollowed by the author’s email address. The author’s name is formatted\nlike:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>firstname[ [middlename ]lastname][ &lt;email&gt;]]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>i.e. a first name followed by optional middle and last names followed\nby an email address in that order.  Multi-word first, middle and last\nnames can be entered using the underscore as a word separator.  The\nemail address comes last and must be enclosed in angle &lt;&gt; brackets.\nHere a some examples of author information lines:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Joe Bloggs &lt;jbloggs@mymail.com&gt;\nJoe Bloggs\nVincent Willem van_Gogh</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If the author line does not match the above specification then the\nentire author line is treated as the first name.</p></div>\n<div class=\"paragraph\"><p>The optional revision information line follows the author information\nline. The revision information can be one of two formats:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nAn optional document revision number followed by an optional\n  revision date followed by an optional revision remark:\n</p>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf the revision number is specified it must be followed by a\n    comma.\n</p>\n</li>\n<li>\n<p>\nThe revision number must contain at least one numeric character.\n</p>\n</li>\n<li>\n<p>\nAny non-numeric characters preceding the first numeric character\n    will be dropped.\n</p>\n</li>\n<li>\n<p>\nIf a revision remark is specified it must be preceded by a colon.\n    The revision remark extends from the colon up to the next blank\n    line, attribute entry or comment and is subject to normal text\n    substitutions.\n</p>\n</li>\n<li>\n<p>\nIf a revision number or remark has been set but the revision date\n    has not been set then the revision date is set to the value of the\n    <em>docdate</em> attribute.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>v2.0, February 2003\nFebruary 2003\nv2.0,\nv2.0, February 2003: Rewritten for version 2 release.\nFebruary 2003: Rewritten for version 2 release.\nv2.0,: Rewritten for version 2 release.\n:Rewritten for version 2 release.</code></pre>\n</div></div>\n</div></div>\n</li>\n<li>\n<p>\nThe revision information line can also be an RCS/CVS/SVN $Id$\n  marker:\n</p>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAsciiDoc extracts the <em>revnumber</em>, <em>revdate</em>, and <em>author</em>\n    attributes from the $Id$ revision marker and displays them in the\n    document header.\n</p>\n</li>\n<li>\n<p>\nIf an $Id$ revision marker is used the header author line can be\n    omitted.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$Id: mydoc.txt,v 1.5 2009/05/17 17:58:44 jbloggs Exp $</code></pre>\n</div></div>\n</div></div>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>You can override or set header parameters by passing <em>revnumber</em>,\n<em>revremark</em>, <em>revdate</em>, <em>email</em>, <em>author</em>, <em>authorinitials</em>,\n<em>firstname</em> and <em>lastname</em> attributes using the <code>asciidoc(1)</code> <code>-a</code>\n(<code>--attribute</code>) command-line option. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a revdate=2004/07/27 article.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Attribute entries can also be added to the header for substitution in\nthe header template with <a href=\"#X18\">Attribute Entry</a> elements.</p></div>\n<div class=\"paragraph\"><p>The <em>title</em> element in HTML outputs is set to the AsciiDoc document\ntitle, you can set it to a different value by including a <em>title</em>\nattribute entry in the document header.</p></div>\n<div class=\"sect3\">\n<h4 id=\"X87\">8.2.1. Additional document header information</h4>\n<div class=\"paragraph\"><p>AsciiDoc has two mechanisms for optionally including additional\nmeta-data in the header of the output document:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<em>docinfo</em> configuration file sections\n</dt>\n<dd>\n<p>\nIf a <a href=\"#X7\">configuration file</a> section named <em>docinfo</em> has been loaded\nthen it will be included in the document header. Typically the\n<em>docinfo</em> section name will be prefixed with a <em>+</em> character so that it\nis appended to (rather than replace) other <em>docinfo</em> sections.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<em>docinfo</em> files\n</dt>\n<dd>\n<p>\nTwo docinfo files are recognized: one named <code>docinfo</code> and a second\nnamed like the AsciiDoc source file with a <code>-docinfo</code> suffix.  For\nexample, if the source document is called <code>mydoc.txt</code> then the\ndocument information files would be <code>docinfo.xml</code> and\n<code>mydoc-docinfo.xml</code> (for DocBook outputs) and <code>docinfo.html</code> and\n<code>mydoc-docinfo.html</code> (for HTML outputs).  The <a href=\"#X97\">docinfo</a> attributes control which docinfo files are included in\nthe output files.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>The contents docinfo templates and files is dependent on the type of\noutput:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nHTML\n</dt>\n<dd>\n<p>\n  Valid <em>head</em> child elements. Typically <em>style</em> and <em>script</em> elements\n  for CSS and JavaScript inclusion.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nDocBook\n</dt>\n<dd>\n<p>\n  Valid <em>articleinfo</em> or <em>bookinfo</em> child elements.  DocBook defines\n  numerous elements for document meta-data, for example: copyrights,\n  document history and authorship information.  See the DocBook\n  <code>./doc/article-docinfo.xml</code> example that comes with the AsciiDoc\n  distribution.  The rendering of meta-data elements (or not) is\n  DocBook processor dependent.\n</p>\n</dd>\n</dl></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X86\">8.3. Preamble</h3>\n<div class=\"paragraph\"><p>The Preamble is an optional untitled section body between the document\nHeader and the first Section title.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_sections\">8.4. Sections</h3>\n<div class=\"paragraph\"><p>In addition to the document title (level 0), AsciiDoc supports four\nsection levels: 1 (top) to 4 (bottom).  Section levels are delimited\nby section <a href=\"#X17\">titles</a>.  Sections are translated using\nconfiguration file <a href=\"#X93\">section markup templates</a>. AsciiDoc\ngenerates the following <a href=\"#X60\">intrinsic attributes</a> specifically for\nuse in section markup templates:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nlevel\n</dt>\n<dd>\n<p>\nThe <code>level</code> attribute is the section level number, it is normally just\nthe <a href=\"#X17\">title</a> level number (1..4). However, if the <code>leveloffset</code>\nattribute is defined it will be added to the <code>level</code> attribute. The\n<code>leveloffset</code> attribute is useful for <a href=\"#X90\">combining documents</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nsectnum\n</dt>\n<dd>\n<p>\nThe <code>-n</code> (<code>--section-numbers</code>) command-line option generates the\n<code>sectnum</code> (section number) attribute.  The <code>sectnum</code> attribute is used\nfor section numbers in HTML outputs (DocBook section numbering are\nhandled automatically by the DocBook toolchain commands).\n</p>\n</dd>\n</dl></div>\n<div class=\"sect3\">\n<h4 id=\"X93\">8.4.1. Section markup templates</h4>\n<div class=\"paragraph\"><p>Section markup templates specify output markup and are defined in\nAsciiDoc configuration files.  Section markup template names are\nderived as follows (in order of precedence):</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nFrom the title’s first positional attribute or <em>template</em>\n   attribute. For example, the following three section titles are\n   functionally equivalent:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[[terms]]\n[glossary]\nList of Terms\n-------------\n\n[\"glossary\",id=\"terms\"]\nList of Terms\n-------------\n\n[template=\"glossary\",id=\"terms\"]\nList of Terms\n-------------</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nWhen the title text matches a configuration file\n   <a href=\"#X16\"><code>[specialsections]</code></a> entry.\n</p>\n</li>\n<li>\n<p>\nIf neither of the above the default <code>sect&lt;level&gt;</code> template is used\n   (where <code>&lt;level&gt;</code> is a number from 1 to 4).\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>In addition to the normal section template names (<em>sect1</em>, <em>sect2</em>,\n<em>sect3</em>, <em>sect4</em>) AsciiDoc has the following templates for\nfrontmatter, backmatter and other special sections: <em>abstract</em>,\n<em>preface</em>, <em>colophon</em>, <em>dedication</em>, <em>glossary</em>, <em>bibliography</em>,\n<em>synopsis</em>, <em>appendix</em>, <em>index</em>.  These special section templates\ngenerate the corresponding Docbook elements; for HTML outputs they\ndefault to the <em>sect1</em> section template.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_section_ids\">8.4.2. Section IDs</h4>\n<div class=\"paragraph\"><p>If no explicit section ID is specified an ID will be synthesised from\nthe section title.  The primary purpose of this feature is to ensure\npersistence of table of contents links (permalinks): the missing\nsection IDs are generated dynamically by the JavaScript TOC generator\n<strong>after</strong> the page is loaded. If you link to a dynamically generated TOC\naddress the page will load but the browser will ignore the (as yet\nungenerated) section ID.</p></div>\n<div class=\"paragraph\"><p>The IDs are generated by the following algorithm:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nReplace all non-alphanumeric title characters with underscores.\n</p>\n</li>\n<li>\n<p>\nStrip leading or trailing underscores.\n</p>\n</li>\n<li>\n<p>\nConvert to lowercase.\n</p>\n</li>\n<li>\n<p>\nPrepend the <code>idprefix</code> attribute (so there’s no possibility of name\n  clashes with existing document IDs). Prepend an underscore if the\n  <code>idprefix</code> attribute is not defined.\n</p>\n</li>\n<li>\n<p>\nA numbered suffix (<code>_2</code>, <code>_3</code> …) is added if a same named\n  auto-generated section ID exists.\n</p>\n</li>\n<li>\n<p>\nIf the <code>ascii-ids</code> attribute is defined then non-ASCII characters\n  are replaced with ASCII equivalents. This attribute may be\n  deprecated in future releases and <strong>should be avoided</strong>, it’s sole\n  purpose is to accommodate deficient downstream applications that\n  cannot process non-ASCII ID attributes.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Example: the title <em>Jim’s House</em> would generate the ID <code>_jim_s_house</code>.</p></div>\n<div class=\"paragraph\"><p>Section ID synthesis can be disabled by undefining the <code>sectids</code>\nattribute.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X16\">8.4.3. Special Section Titles</h4>\n<div class=\"paragraph\"><p>AsciiDoc has a mechanism for mapping predefined section titles\nauto-magically to specific markup templates. For example a title\n<em>Appendix A: Code Reference</em> will automatically use the <em>appendix</em>\n<a href=\"#X93\">section markup template</a>. The mappings from title to template\nname are specified in <code>[specialsections]</code> sections in the Asciidoc\nlanguage configuration files (<code>lang-*.conf</code>).  Section entries are\nformatted like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;title&gt;=&lt;template&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><code>&lt;title&gt;</code> is a Python regular expression and <code>&lt;template&gt;</code> is the name\nof a configuration file markup template section. If the <code>&lt;title&gt;</code>\nmatches an AsciiDoc document section title then the backend output is\nmarked up using the <code>&lt;template&gt;</code> markup template (instead of the\ndefault <code>sect&lt;level&gt;</code> section template). The <code>{title}</code> attribute value\nis set to the value of the matched regular expression group named\n<em>title</em>, if there is no <em>title</em> group <code>{title}</code> defaults to the whole\nof the AsciiDoc section title. If <code>&lt;template&gt;</code> is blank then any\nexisting entry with the same <code>&lt;title&gt;</code> will be deleted.</p></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">Special section titles vs. explicit template names</div>\n<div class=\"paragraph\"><p>AsciiDoc has two mechanisms for specifying non-default section markup\ntemplates: you can specify the template name explicitly (using the\n<em>template</em> attribute) or indirectly (using <em>special section titles</em>).\nSpecifying a <a href=\"#X93\">section template</a> attribute explicitly is\npreferred.  Auto-magical <em>special section titles</em> have the following\ndrawbacks:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThey are non-obvious, you have to know the exact matching\n  title for each special section on a language by language basis.\n</p>\n</li>\n<li>\n<p>\nSection titles are predefined and can only be customised with a\n  configuration change.\n</p>\n</li>\n<li>\n<p>\nThe implementation is complicated by multiple languages: every\n  special section title has to be defined for each language (in each\n  of the <code>lang-*.conf</code> files).\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Specifying special section template names explicitly does add more\nnoise to the source document (the <em>template</em> attribute declaration),\nbut the intention is obvious and the syntax is consistent with other\nAsciiDoc elements c.f.  bibliographic, Q&amp;A and glossary lists.</p></div>\n<div class=\"paragraph\"><p>Special section titles have been deprecated but are retained for\nbackward compatibility.</p></div>\n</div></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_inline_elements\">8.5. Inline Elements</h3>\n<div class=\"paragraph\"><p><a href=\"#X34\">Inline document elements</a> are used to format text and to\nperform various types of text substitution. Inline elements and inline\nelement syntax is defined in the <code>asciidoc(1)</code> configuration files.</p></div>\n<div class=\"paragraph\"><p>Here is a list of AsciiDoc inline elements in the (default) order in\nwhich they are processed:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nSpecial characters\n</dt>\n<dd>\n<p>\n        These character sequences escape special characters used by\n        the backend markup (typically <code>&lt;</code>, <code>&gt;</code>, and <code>&amp;</code> characters).\n        See <code>[specialcharacters]</code> configuration file sections.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nQuotes\n</dt>\n<dd>\n<p>\n        Elements that markup words and phrases; usually for character\n        formatting. See <code>[quotes]</code> configuration file sections.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nSpecial Words\n</dt>\n<dd>\n<p>\n        Word or word phrase patterns singled out for markup without\n        the need for further annotation.  See <code>[specialwords]</code>\n        configuration file sections.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nReplacements\n</dt>\n<dd>\n<p>\n        Each replacement defines a word or word phrase pattern to\n        search for along with corresponding replacement text. See\n        <code>[replacements]</code> configuration file sections.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nAttribute references\n</dt>\n<dd>\n<p>\n        Document attribute names enclosed in braces are replaced by\n        the corresponding attribute value.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nInline Macros\n</dt>\n<dd>\n<p>\n        Inline macros are replaced by the contents of parametrized\n        configuration file sections.\n</p>\n</dd>\n</dl></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_document_processing\">9. Document Processing</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The AsciiDoc source document is read and processed as follows:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nThe document <em>Header</em> is parsed, header parameter values are\n   substituted into the configuration file <code>[header]</code> template section\n   which is then written to the output file.\n</p>\n</li>\n<li>\n<p>\nEach document <em>Section</em> is processed and its constituent elements\n   translated to the output file.\n</p>\n</li>\n<li>\n<p>\nThe configuration file <code>[footer]</code> template section is substituted\n   and written to the output file.\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>When a block element is encountered <code>asciidoc(1)</code> determines the type of\nblock by checking in the following order (first to last): (section)\nTitles, BlockMacros, Lists, DelimitedBlocks, Tables, AttributeEntrys,\nAttributeLists, BlockTitles, Paragraphs.</p></div>\n<div class=\"paragraph\"><p>The default paragraph definition <code>[paradef-default]</code> is last element\nto be checked.</p></div>\n<div class=\"paragraph\"><p>Knowing the parsing order will help you devise unambiguous macro, list\nand block syntax rules.</p></div>\n<div class=\"paragraph\"><p>Inline substitutions within block elements are performed in the\nfollowing default order:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nSpecial characters\n</p>\n</li>\n<li>\n<p>\nQuotes\n</p>\n</li>\n<li>\n<p>\nSpecial words\n</p>\n</li>\n<li>\n<p>\nReplacements\n</p>\n</li>\n<li>\n<p>\nAttributes\n</p>\n</li>\n<li>\n<p>\nInline Macros\n</p>\n</li>\n<li>\n<p>\nReplacements2\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>The substitutions and substitution order performed on\nTitle, Paragraph and DelimitedBlock elements is determined by\nconfiguration file parameters.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_text_formatting\">10. Text Formatting</h2>\n<div class=\"sectionbody\">\n<div class=\"sect2\">\n<h3 id=\"X51\">10.1. Quoted Text</h3>\n<div class=\"paragraph\"><p>Words and phrases can be formatted by enclosing inline text with\nquote characters:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<em>Emphasized text</em>\n</dt>\n<dd>\n<p>\n        Word phrases 'enclosed in single quote characters' (acute\n        accents) or _underline characters_ are emphasized.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<strong>Strong text</strong>\n</dt>\n<dd>\n<p>\n        Word phrases *enclosed in asterisk characters* are rendered\n        in a strong font (usually bold).\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X81\"></a><code>Monospaced text</code>\n</dt>\n<dd>\n<p>\n        Word phrases +enclosed in plus characters+ are rendered in a\n        monospaced font. Word phrases `enclosed in backtick\n        characters` (grave accents) are also rendered in a monospaced\n        font but in this case the enclosed text is rendered literally\n        and is not subject to further expansion (see <a href=\"#X80\">inline         literal passthrough</a>).\n</p>\n</dd>\n<dt class=\"hdlist1\">\n‘Single quoted text’\n</dt>\n<dd>\n<p>\n        Phrases enclosed with a `single grave accent to the left and\n        a single acute accent to the right' are rendered in single\n        quotation marks.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n“Double quoted text”\n</dt>\n<dd>\n<p>\n        Phrases enclosed with ``two grave accents to the left and\n        two acute accents to the right'' are rendered in quotation\n        marks.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nUnquoted text\n</dt>\n<dd>\n<p>\n        Placing #hashes around text# does nothing, it is a mechanism\n        to allow inline attributes to be applied to otherwise\n        unformatted text.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>New quote types can be defined by editing <code>asciidoc(1)</code> configuration\nfiles. See the <a href=\"#X7\">Configuration Files</a> section for details.</p></div>\n<div class=\"ulist\"><div class=\"title\">Quoted text behavior</div><ul>\n<li>\n<p>\nQuoting cannot be overlapped.\n</p>\n</li>\n<li>\n<p>\nDifferent quoting types can be nested.\n</p>\n</li>\n<li>\n<p>\nTo suppress quoted text formatting place a backslash character\n  immediately in front of the leading quote character(s). In the case\n  of ambiguity between escaped and non-escaped text you will need to\n  escape both leading and trailing quotes, in the case of\n  multi-character quotes you may even need to escape individual\n  characters.\n</p>\n</li>\n</ul></div>\n<div class=\"sect3\">\n<h4 id=\"X96\">10.1.1. Quoted text attributes</h4>\n<div class=\"paragraph\"><p>Quoted text can be prefixed with an <a href=\"#X21\">attribute list</a>.  The first\npositional attribute (<em>role</em> attribute) is translated by AsciiDoc to\nan HTML <em>span</em> element <em>class</em> attribute or a DocBook <em>phrase</em> element\n<em>role</em> attribute.</p></div>\n<div class=\"paragraph\"><p>DocBook XSL Stylesheets translate DocBook <em>phrase</em> elements with\n<em>role</em> attributes to corresponding HTML <em>span</em> elements with the same\n<em>class</em> attributes; CSS can then be used\n<a href=\"http://www.sagehill.net/docbookxsl/UsingCSS.html\">to style the\ngenerated HTML</a>.  Thus CSS styling can be applied to both DocBook and\nAsciiDoc generated HTML outputs.  You can also specify multiple class\nnames separated by spaces.</p></div>\n<div class=\"paragraph\"><p>CSS rules for text color, text background color, text size and text\ndecorators are included in the distributed AsciiDoc CSS files and are\nused in conjunction with AsciiDoc <em>xhtml11</em>, <em>html5</em> and <em>docbook</em>\noutputs. The CSS class names are:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>&lt;color&gt;</em> (text foreground color).\n</p>\n</li>\n<li>\n<p>\n<em>&lt;color&gt;-background</em> (text background color).\n</p>\n</li>\n<li>\n<p>\n<em>big</em> and <em>small</em> (text size).\n</p>\n</li>\n<li>\n<p>\n<em>underline</em>, <em>overline</em> and <em>line-through</em> (strike through) text\n  decorators.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Where <em>&lt;color&gt;</em> can be any of the\n<a href=\"http://en.wikipedia.org/wiki/Web_colors#HTML_color_names\">sixteen HTML\ncolor names</a>.  Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[red]#Obvious# and [big red yellow-background]*very obvious*.</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[underline]#Underline text#, [overline]#overline text# and\n[blue line-through]*bold blue and line-through*.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>is rendered as:</p></div>\n<div class=\"paragraph\"><p><span class=\"red\">Obvious</span> and <strong><span class=\"big red yellow-background\">very obvious</span></strong>.</p></div>\n<div class=\"paragraph\"><p><span class=\"underline\">Underline text</span>, <span class=\"overline\">overline text</span> and\n<strong><span class=\"bold blue line-through\">bold blue and line-through</span></strong>.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">Color and text decorator attributes are rendered for XHTML and\nHTML 5 outputs using CSS stylesheets.  The mechanism to implement\ncolor and text decorator attributes is provided for DocBook toolchains\nvia the DocBook <em>phrase</em> element <em>role</em> attribute, but the actual\nrendering is toolchain specific and is not part of the AsciiDoc\ndistribution.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X52\">10.1.2. Constrained and Unconstrained Quotes</h4>\n<div class=\"paragraph\"><p>There are actually two types of quotes:</p></div>\n<div class=\"sect4\">\n<h5 id=\"_constrained_quotes\">Constrained quotes</h5>\n<div class=\"paragraph\"><p>Quoted must be bounded by white space or commonly adjoining\npunctuation characters. These are the most commonly used type of\nquote.</p></div>\n</div>\n<div class=\"sect4\">\n<h5 id=\"_unconstrained_quotes\">Unconstrained quotes</h5>\n<div class=\"paragraph\"><p>Unconstrained quotes have no boundary constraints and can be placed\nanywhere within inline text. For consistency and to make them easier\nto remember unconstrained quotes are double-ups of the <code>_</code>, <code>*</code>, <code>+</code>\nand <code>#</code> constrained quotes:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>__unconstrained emphasized text__\n**unconstrained strong text**\n++unconstrained monospaced text++\n##unconstrained unquoted text##</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The following example emboldens the letter F:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>**F**ile Open...</code></pre>\n</div></div>\n</div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_superscripts_and_subscripts\">10.2. Superscripts and Subscripts</h3>\n<div class=\"paragraph\"><p>Put ^carets on either^ side of the text to be superscripted, put\n~tildes on either side~ of text to be subscripted.  For example, the\nfollowing line:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>e^&amp;#960;i^+1 = 0. H~2~O and x^10^. Some ^super text^\nand ~some sub text~</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Is rendered like:</p></div>\n<div class=\"paragraph\"><p>e<sup>πi</sup>+1 = 0. H<sub>2</sub>O and x<sup>10</sup>. Some <sup>super text</sup>\nand <sub>some sub text</sub></p></div>\n<div class=\"paragraph\"><p>Superscripts and subscripts are implemented as <a href=\"#X52\">unconstrained quotes</a> and they can be escaped with a leading backslash and prefixed\nwith with an attribute list.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_line_breaks\">10.3. Line Breaks</h3>\n<div class=\"paragraph\"><p>A plus character preceded by at least one space character at the end\nof a non-blank line forces a line break. It generates a line break\n(<code>br</code>) tag for HTML outputs and a custom XML <code>asciidoc-br</code> processing\ninstruction for DocBook outputs. The <code>asciidoc-br</code> processing\ninstruction is handled by <a href=\"#X43\"><code>a2x(1)</code></a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_page_breaks\">10.4. Page Breaks</h3>\n<div class=\"paragraph\"><p>A line of three or more less-than (<code>&lt;&lt;&lt;</code>) characters will generate a\nhard page break in DocBook and printed HTML outputs.  It uses the CSS\n<code>page-break-after</code> property for HTML outputs and a custom XML\n<code>asciidoc-pagebreak</code> processing instruction for DocBook outputs. The\n<code>asciidoc-pagebreak</code> processing instruction is handled by\n<a href=\"#X43\"><code>a2x(1)</code></a>. Hard page breaks are sometimes handy but as a general\nrule you should let your page processor generate page breaks for you.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_rulers\">10.5. Rulers</h3>\n<div class=\"paragraph\"><p>A line of three or more apostrophe characters will generate a ruler\nline.  It generates a ruler (<code>hr</code>) tag for HTML outputs and a custom\nXML <code>asciidoc-hr</code> processing instruction for DocBook outputs. The\n<code>asciidoc-hr</code> processing instruction is handled by <a href=\"#X43\"><code>a2x(1)</code></a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_tabs\">10.6. Tabs</h3>\n<div class=\"paragraph\"><p>By default tab characters input files will translated to 8 spaces. Tab\nexpansion is set with the <em>tabsize</em> entry in the configuration file\n<code>[miscellaneous]</code> section and can be overridden in included files by\nsetting a <em>tabsize</em> attribute in the <code>include</code> macro’s attribute list.\nFor example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>include::addendum.txt[tabsize=2]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The tab size can also be set using the attribute command-line option,\nfor example <code>--attribute tabsize=4</code></p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_replacements\">10.7. Replacements</h3>\n<div class=\"paragraph\"><p>The following replacements are defined in the default AsciiDoc\nconfiguration:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>(C) copyright, (TM) trademark, (R) registered trademark,\n-- em dash, ... ellipsis, -&gt; right arrow, &lt;- left arrow, =&gt; right\ndouble arrow, &lt;= left double arrow.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which are rendered as:</p></div>\n<div class=\"paragraph\"><p>© copyright, ™ trademark, ® registered trademark, — em dash, … ellipsis, → right arrow, ← left arrow, ⇒ right\ndouble arrow, ⇐ left double arrow.</p></div>\n<div class=\"paragraph\"><p>You can also include arbitrary entity references in the AsciiDoc\nsource. Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&amp;#x278a; &amp;#182;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>renders:</p></div>\n<div class=\"paragraph\"><p>➊ ¶</p></div>\n<div class=\"paragraph\"><p>To render a replacement literally escape it with a leading back-slash.</p></div>\n<div class=\"paragraph\"><p>The <a href=\"#X7\">Configuration Files</a> section explains how to configure your\nown replacements.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_special_words\">10.8. Special Words</h3>\n<div class=\"paragraph\"><p>Words defined in <code>[specialwords]</code> configuration file sections are\nautomatically marked up without having to be explicitly notated.</p></div>\n<div class=\"paragraph\"><p>The <a href=\"#X7\">Configuration Files</a> section explains how to add and replace\nspecial words.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X17\">11. Titles</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Document and section titles can be in either of two formats:</p></div>\n<div class=\"sect2\">\n<h3 id=\"_two_line_titles\">11.1. Two line titles</h3>\n<div class=\"paragraph\"><p>A two line title consists of a title line, starting hard against the\nleft margin, and an underline. Section underlines consist a repeated\ncharacter pairs spanning the width of the preceding title (give or\ntake up to two characters):</p></div>\n<div class=\"paragraph\"><p>The default title underlines for each of the document levels are:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Level 0 (top level):     ======================\nLevel 1:                 ----------------------\nLevel 2:                 ~~~~~~~~~~~~~~~~~~~~~~\nLevel 3:                 ^^^^^^^^^^^^^^^^^^^^^^\nLevel 4 (bottom level):  ++++++++++++++++++++++</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Level One Section Title\n-----------------------</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Level 2 Subsection Title\n~~~~~~~~~~~~~~~~~~~~~~~~</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X46\">11.2. One line titles</h3>\n<div class=\"paragraph\"><p>One line titles consist of a single line delimited on either side by\none or more equals characters (the number of equals characters\ncorresponds to the section level minus one).  Here are some examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>= Document Title (level 0) =\n== Section title (level 1) ==\n=== Section title (level 2) ===\n==== Section title (level 3) ====\n===== Section title (level 4) =====</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nOne or more spaces must fall between the title and the delimiters.\n</p>\n</li>\n<li>\n<p>\nThe trailing title delimiter is optional.\n</p>\n</li>\n<li>\n<p>\nThe one-line title syntax can be changed by editing the\n  configuration file <code>[titles]</code> section <code>sect0</code>…<code>sect4</code> entries.\n</p>\n</li>\n</ul></div>\n</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_floating_titles\">11.3. Floating titles</h3>\n<div class=\"paragraph\"><p>Setting the title’s first positional attribute or <em>style</em> attribute to\n<em>float</em> generates a free-floating title. A free-floating title is\nrendered just like a normal section title but is not formally\nassociated with a text body and is not part of the regular section\nhierarchy so the normal ordering rules do not apply. Floating titles\ncan also be used in contexts where section titles are illegal: for\nexample sidebar and admonition blocks.  Example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[float]\nThe second day\n~~~~~~~~~~~~~~</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Floating titles do not appear in a document’s table of contents.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X42\">12. Block Titles</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>A <em>BlockTitle</em> element is a single line beginning with a period\nfollowed by the title text. A BlockTitle is applied to the immediately\nfollowing Paragraph, DelimitedBlock, List, Table or BlockMacro. For\nexample:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>.Notes\n- Note 1.\n- Note 2.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>is rendered as:</p></div>\n<div class=\"ulist\"><div class=\"title\">Notes</div><ul>\n<li>\n<p>\nNote 1.\n</p>\n</li>\n<li>\n<p>\nNote 2.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X41\">13. BlockId Element</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>A <em>BlockId</em> is a single line block element containing a unique\nidentifier enclosed in double square brackets. It is used to assign an\nidentifier to the ensuing block element. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[[chapter-titles]]\nChapter titles can be ...</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The preceding example identifies the ensuing paragraph so it can be\nreferenced from other locations, for example with\n<code>&lt;&lt;chapter-titles,chapter titles&gt;&gt;</code>.</p></div>\n<div class=\"paragraph\"><p><em>BlockId</em> elements can be applied to Title, Paragraph, List,\nDelimitedBlock, Table and BlockMacro elements.  The BlockId element\nsets the <code>{id}</code> attribute for substitution in the subsequent block’s\nmarkup template. If a second positional argument is supplied it sets\nthe <code>{reftext}</code> attribute which is used to set the DocBook <code>xreflabel</code>\nattribute.</p></div>\n<div class=\"paragraph\"><p>The <em>BlockId</em> element has the same syntax and serves the same function\nto the <a href=\"#X30\">anchor inline macro</a>.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X79\">14. AttributeList Element</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>An <em>AttributeList</em> block element is an <a href=\"#X21\">attribute list</a> on a\nline by itself:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>AttributeList</em> attributes are only applied to the immediately\n  following block element — the attributes are made available to the\n  block’s markup template.\n</p>\n</li>\n<li>\n<p>\nMultiple contiguous <em>AttributeList</em> elements are additively combined\n  in the order they appear..\n</p>\n</li>\n<li>\n<p>\nThe first positional attribute in the list is often used to specify\n  the ensuing element’s <a href=\"#X23\">style</a>.\n</p>\n</li>\n</ul></div>\n<div class=\"sect2\">\n<h3 id=\"_attribute_value_substitution\">14.1. Attribute value substitution</h3>\n<div class=\"paragraph\"><p>By default, only substitutions that take place inside attribute list\nvalues are attribute references, this is because not all attributes\nare destined to be marked up and rendered as text (for example the\ntable <em>cols</em> attribute). To perform normal inline text substitutions\n(special characters, quotes, macros, replacements) on an attribute\nvalue you need to enclose it in single quotes. In the following quote\nblock the second attribute value in the AttributeList is quoted to\nensure the <em>http</em> macro is expanded to a hyperlink.</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[quote,'http://en.wikipedia.org/wiki/Samuel_Johnson[Samuel Johnson]']\n_____________________________________________________________________\nSir, a woman's preaching is like a dog's walking on his hind legs. It\nis not done well; but you are surprised to find it done at all.\n_____________________________________________________________________</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_common_attributes\">14.2. Common attributes</h3>\n<div class=\"paragraph\"><p>Most block elements support the following attributes:</p></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<colgroup><col width=\"14%\">\n<col width=\"14%\">\n<col width=\"71%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Name </th>\n<th valign=\"top\" align=\"left\">Backends </th>\n<th valign=\"top\" align=\"left\">Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>id</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Unique identifier typically serve as link targets.\nCan also be set by the <em>BlockId</em> element.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>role</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Role contains a string used to classify or subclassify an element and\ncan be applied to AsciiDoc block elements.  The AsciiDoc <em>role</em>\nattribute is translated to the <em>role</em> attribute in DocBook outputs and\nis included in the <em>class</em> attribute in HTML outputs, in this respect\nit behaves like the <a href=\"#X96\">quoted text role attribute</a>.</p></div>\n<div class=\"paragraph\"><p>DocBook XSL Stylesheets translate DocBook <em>role</em> attributes to HTML\n<em>class</em> attributes; CSS can then be used\n<a href=\"http://www.sagehill.net/docbookxsl/UsingCSS.html\">to style the\ngenerated HTML</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>reftext</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p><em>reftext</em> is used to set the DocBook <em>xreflabel</em> attribute.\nThe <em>reftext</em> attribute can an also be set by the <em>BlockId</em> element.</p></div></div></td>\n</tr>\n</tbody>\n</table>\n</div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_paragraphs\">15. Paragraphs</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Paragraphs are blocks of text terminated by a blank line, the end of\nfile, or the start of a delimited block or a list.  There are three\nparagraph syntaxes: normal, indented (literal) and admonition which\nare rendered, by default, with the corresponding paragraph style.</p></div>\n<div class=\"paragraph\"><p>Each syntax has a default style, but you can explicitly apply any\nparagraph style to any paragraph syntax. You can also apply\n<a href=\"#X104\">delimited block</a> styles to single paragraphs.</p></div>\n<div class=\"paragraph\"><p>The built-in paragraph styles are: <em>normal</em>, <em>literal</em>, <em>verse</em>,\n<em>quote</em>, <em>listing</em>, <em>TIP</em>, <em>NOTE</em>, <em>IMPORTANT</em>, <em>WARNING</em>, <em>CAUTION</em>,\n<em>abstract</em>, <em>partintro</em>, <em>comment</em>, <em>example</em>, <em>sidebar</em>, <em>source</em>,\n<em>music</em>, <em>latex</em>, <em>graphviz</em>.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_normal_paragraph_syntax\">15.1. normal paragraph syntax</h3>\n<div class=\"paragraph\"><p>Normal paragraph syntax consists of one or more non-blank lines of\ntext. The first line must start hard against the left margin (no\nintervening white space). The default processing expectation is that\nof a normal paragraph of text.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X85\">15.2. literal paragraph syntax</h3>\n<div class=\"paragraph\"><p>Literal paragraphs are rendered verbatim in a monospaced font without\nany distinguishing background or border.  By default there is no text\nformatting or substitutions within Literal paragraphs apart from\nSpecial Characters and Callouts.</p></div>\n<div class=\"paragraph\"><p>The <em>literal</em> style is applied implicitly to indented paragraphs i.e.\nwhere the first line of the paragraph is indented by one or more space\nor tab characters.  For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>  Consul *necessitatibus* per id,\n  consetetur, eu pro everti postulant\n  homero verear ea mea, qui.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Consul *necessitatibus* per id,\nconsetetur, eu pro everti postulant\nhomero verear ea mea, qui.</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">Because <a href=\"#X64\">lists</a> can be indented it’s possible for your\nindented paragraph to be misinterpreted as a list — in situations\nlike this apply the <em>literal</em> style to a normal paragraph.</td>\n</tr></tbody></table>\n</div>\n<div class=\"paragraph\"><p>Instead of using a paragraph indent you could apply the <em>literal</em>\nstyle explicitly, for example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[literal]\nConsul *necessitatibus* per id,\nconsetetur, eu pro everti postulant\nhomero verear ea mea, qui.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Consul *necessitatibus* per id,\nconsetetur, eu pro everti postulant\nhomero verear ea mea, qui.</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X94\">15.3. quote and verse paragraph styles</h3>\n<div class=\"paragraph\"><p>The optional <em>attribution</em> and <em>citetitle</em> attributes (positional\nattributes 2 and 3) specify the author and source respectively.</p></div>\n<div class=\"paragraph\"><p>The <em>verse</em> style retains the line breaks, for example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[verse, William Blake, from Auguries of Innocence]\nTo see a world in a grain of sand,\nAnd a heaven in a wild flower,\nHold infinity in the palm of your hand,\nAnd eternity in an hour.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which is rendered as:</p></div>\n<div class=\"verseblock\">\n<pre class=\"content\">To see a world in a grain of sand,\nAnd a heaven in a wild flower,\nHold infinity in the palm of your hand,\nAnd eternity in an hour.</pre>\n<div class=\"attribution\">\n<em>from Auguries of Innocence</em><br>\n— William Blake\n</div></div>\n<div class=\"paragraph\"><p>The <em>quote</em> style flows the text at left and right margins, for\nexample:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[quote, Bertrand Russell, The World of Mathematics (1956)]\nA good notation has subtlety and suggestiveness which at times makes\nit almost seem like a live teacher.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which is rendered as:</p></div>\n<div class=\"quoteblock\">\n<div class=\"content\">A good notation has subtlety and suggestiveness which at times makes\nit almost seem like a live teacher.</div>\n<div class=\"attribution\">\n<em>The World of Mathematics (1956)</em><br>\n— Bertrand Russell\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X28\">15.4. Admonition Paragraphs</h3>\n<div class=\"paragraph\"><p><em>TIP</em>, <em>NOTE</em>, <em>IMPORTANT</em>, <em>WARNING</em> and <em>CAUTION</em> admonishment\nparagraph styles are generated by placing <code>NOTE:</code>, <code>TIP:</code>,\n<code>IMPORTANT:</code>, <code>WARNING:</code> or <code>CAUTION:</code> as the first word of the\nparagraph. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>NOTE: This is an example note.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Alternatively, you can specify the paragraph admonition style\nexplicitly using an <a href=\"#X79\">AttributeList element</a>. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[NOTE]\nThis is an example note.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">This is an example note.</td>\n</tr></tbody></table>\n</div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">If your admonition requires more than a single paragraph use an\n<a href=\"#X22\">admonition block</a> instead.</td>\n</tr></tbody></table>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X47\">15.4.1. Admonition Icons and Captions</h4>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">Admonition customization with <code>icons</code>, <code>iconsdir</code>, <code>icon</code> and\n<code>caption</code> attributes does not apply when generating DocBook output. If\nyou are going the DocBook route then the <a href=\"#X43\"><code>a2x(1)</code></a> <code>--no-icons</code>\nand <code>--icons-dir</code> options can be used to set the appropriate XSL\nStylesheets parameters.</td>\n</tr></tbody></table>\n</div>\n<div class=\"paragraph\"><p>By default the <code>asciidoc(1)</code> HTML backends generate text captions\ninstead of admonition icon image links. To generate links to icon\nimages define the <a href=\"#X45\"><code>icons</code></a> attribute, for example using the <code>-a\nicons</code> command-line option.</p></div>\n<div class=\"paragraph\"><p>The <a href=\"#X44\"><code>iconsdir</code></a> attribute sets the location of linked icon\nimages.</p></div>\n<div class=\"paragraph\"><p>You can override the default icon image using the <code>icon</code> attribute to\nspecify the path of the linked image. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[icon=\"./images/icons/wink.png\"]\nNOTE: What lovely war.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Use the <code>caption</code> attribute to customize the admonition captions (not\napplicable to <code>docbook</code> backend). The following example suppresses the\nicon image and customizes the caption of a <em>NOTE</em> admonition\n(undefining the <code>icons</code> attribute with <code>icons=None</code> is only necessary\nif <a href=\"#X45\">admonition icons</a> have been enabled):</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[icons=None, caption=\"My Special Note\"]\nNOTE: This is my special note.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>This subsection also applies to <a href=\"#X22\">Admonition Blocks</a>.</p></div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X104\">16. Delimited Blocks</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Delimited blocks are blocks of text enveloped by leading and trailing\ndelimiter lines (normally a series of four or more repeated\ncharacters). The behavior of Delimited Blocks is specified by entries\nin configuration file <code>[blockdef-*]</code> sections.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_predefined_delimited_blocks\">16.1. Predefined Delimited Blocks</h3>\n<div class=\"paragraph\"><p>AsciiDoc ships with a number of predefined DelimitedBlocks (see the\n<code>asciidoc.conf</code> configuration file in the <code>asciidoc(1)</code> program\ndirectory):</p></div>\n<div class=\"paragraph\"><p>Predefined delimited block underlines:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>CommentBlock:     //////////////////////////\nPassthroughBlock: ++++++++++++++++++++++++++\nListingBlock:     --------------------------\nLiteralBlock:     ..........................\nSidebarBlock:     **************************\nQuoteBlock:       __________________________\nExampleBlock:     ==========================\nOpenBlock:        --</code></pre>\n</div></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\">\n<caption class=\"title\">Table 3. Default DelimitedBlock substitutions</caption>\n<colgroup><col>\n<col>\n<col>\n<col>\n<col>\n<col>\n<col>\n<col>\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\"> </th>\n<th valign=\"top\" align=\"center\">Attributes </th>\n<th valign=\"top\" align=\"center\">Callouts </th>\n<th valign=\"top\" align=\"center\">Macros </th>\n<th valign=\"top\" align=\"center\"> Quotes </th>\n<th valign=\"top\" align=\"center\">Replacements</th>\n<th valign=\"top\" align=\"center\">Special chars </th>\n<th valign=\"top\" align=\"center\">Special words</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>PassthroughBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>ListingBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>LiteralBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>SidebarBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>QuoteBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>ExampleBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>OpenBlock</em></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">No</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">Yes</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_listing_blocks\">16.2. Listing Blocks</h3>\n<div class=\"paragraph\"><p><em>ListingBlocks</em> are rendered verbatim in a monospaced font, they\nretain line and whitespace formatting and are often distinguished by a\nbackground or border. There is no text formatting or substitutions\nwithin Listing blocks apart from Special Characters and Callouts.\nListing blocks are often used for computer output and file listings.</p></div>\n<div class=\"paragraph\"><p>Here’s an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>--------------------------------------\n#include &lt;stdio.h&gt;\n\nint main() {\n   printf(\"Hello World!\\n\");\n   exit(0);\n}\n--------------------------------------</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which will be rendered like:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>#include &lt;stdio.h&gt;\n\nint main() {\n    printf(\"Hello World!\\n\");\n    exit(0);\n}</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>By convention <a href=\"#X59\">filter blocks</a> use the listing block syntax and\nare implemented as distinct listing block styles.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X65\">16.3. Literal Blocks</h3>\n<div class=\"paragraph\"><p><em>LiteralBlocks</em> are rendered just like <a href=\"#X85\">literal paragraphs</a>.\nExample:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>...................................\nConsul *necessitatibus* per id,\nconsetetur, eu pro everti postulant\nhomero verear ea mea, qui.\n...................................</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Consul *necessitatibus* per id,\nconsetetur, eu pro everti postulant\nhomero verear ea mea, qui.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If the <em>listing</em> style is applied to a LiteralBlock it will be\nrendered as a ListingBlock (this is handy if you have a listing\ncontaining a ListingBlock).</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_sidebar_blocks\">16.4. Sidebar Blocks</h3>\n<div class=\"paragraph\"><p>A sidebar is a short piece of text presented outside the narrative\nflow of the main text. The sidebar is normally presented inside a\nbordered box to set it apart from the main text.</p></div>\n<div class=\"paragraph\"><p>The sidebar body is treated like a normal section body.</p></div>\n<div class=\"paragraph\"><p>Here’s an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>.An Example Sidebar\n************************************************\nAny AsciiDoc SectionBody element (apart from\nSidebarBlocks) can be placed inside a sidebar.\n************************************************</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which will be rendered like:</p></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">An Example Sidebar</div>\n<div class=\"paragraph\"><p>Any AsciiDoc SectionBody element (apart from\nSidebarBlocks) can be placed inside a sidebar.</p></div>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X26\">16.5. Comment Blocks</h3>\n<div class=\"paragraph\"><p>The contents of <em>CommentBlocks</em> are not processed; they are useful for\nannotations and for excluding new or outdated content that you don’t\nwant displayed. CommentBlocks are never written to output files.\nExample:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>//////////////////////////////////////////\nCommentBlock contents are not processed by\nasciidoc(1).\n//////////////////////////////////////////</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>See also <a href=\"#X25\">Comment Lines</a>.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">System macros are executed inside comment blocks.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X76\">16.6. Passthrough Blocks</h3>\n<div class=\"paragraph\"><p>By default the block contents is subject only to <em>attributes</em> and\n<em>macros</em> substitutions (use an explicit <em>subs</em> attribute to apply\ndifferent substitutions).  PassthroughBlock content will often be\nbackend specific. Here’s an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[subs=\"quotes\"]\n++++++++++++++++++++++++++++++++++++++\n&lt;table border=\"1\"&gt;&lt;tr&gt;\n  &lt;td&gt;*Cell 1*&lt;/td&gt;\n  &lt;td&gt;*Cell 2*&lt;/td&gt;\n&lt;/tr&gt;&lt;/table&gt;\n++++++++++++++++++++++++++++++++++++++</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The following styles can be applied to passthrough blocks:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\npass\n</dt>\n<dd>\n<p>\n  No substitutions are performed. This is equivalent to <code>subs=\"none\"</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nasciimath, latexmath\n</dt>\n<dd>\n<p>\n  By default no substitutions are performed, the contents are rendered\n  as <a href=\"#X78\">mathematical formulas</a>.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_quote_blocks\">16.7. Quote Blocks</h3>\n<div class=\"paragraph\"><p><em>QuoteBlocks</em> are used for quoted passages of text. There are two\nstyles: <em>quote</em> and <em>verse</em>. The style behavior is identical to\n<a href=\"#X94\">quote and verse paragraphs</a> except that blocks can contain\nmultiple paragraphs and, in the case of the <em>quote</em> style, other\nsection elements.  The first positional attribute sets the style, if\nno attributes are specified the <em>quote</em> style is used.  The optional\n<em>attribution</em> and <em>citetitle</em> attributes (positional attributes 2 and\n3) specify the quote’s author and source. For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]\n____________________________________________________________________\nAs he spoke there was the sharp sound of horses' hoofs and\ngrating wheels against the curb, followed by a sharp pull at the\nbell. Holmes whistled.\n\n\"A pair, by the sound,\" said he. \"Yes,\" he continued, glancing\nout of the window. \"A nice little brougham and a pair of\nbeauties. A hundred and fifty guineas apiece. There's money in\nthis case, Watson, if there is nothing else.\"\n____________________________________________________________________</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which is rendered as:</p></div>\n<div class=\"quoteblock\">\n<div class=\"content\">\n<div class=\"paragraph\"><p>As he spoke there was the sharp sound of horses' hoofs and\ngrating wheels against the curb, followed by a sharp pull at the\nbell. Holmes whistled.</p></div>\n<div class=\"paragraph\"><p>\"A pair, by the sound,\" said he. \"Yes,\" he continued, glancing\nout of the window. \"A nice little brougham and a pair of\nbeauties. A hundred and fifty guineas apiece. There’s money in\nthis case, Watson, if there is nothing else.\"</p></div>\n</div>\n<div class=\"attribution\">\n<em>The Adventures of Sherlock Holmes</em><br>\n— Sir Arthur Conan Doyle\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X48\">16.8. Example Blocks</h3>\n<div class=\"paragraph\"><p><em>ExampleBlocks</em> encapsulate the DocBook Example element and are used\nfor, well, examples.  Example blocks can be titled by preceding them\nwith a <em>BlockTitle</em>.  DocBook toolchains will normally automatically\nnumber examples and generate a <em>List of Examples</em> backmatter section.</p></div>\n<div class=\"paragraph\"><p>Example blocks are delimited by lines of equals characters and can\ncontain any block elements apart from Titles, BlockTitles and\nSidebars) inside an example block. For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>.An example\n=====================================================================\nQui in magna commodo, est labitur dolorum an. Est ne magna primis\nadolescens.\n=====================================================================</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"exampleblock\">\n<div class=\"title\">Example 1. An example</div>\n<div class=\"content\">\n<div class=\"paragraph\"><p>Qui in magna commodo, est labitur dolorum an. Est ne magna primis\nadolescens.</p></div>\n</div></div>\n<div class=\"paragraph\"><p>A title prefix that can be inserted with the <code>caption</code> attribute\n(HTML backends). For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[caption=\"Example 1: \"]\n.An example with a custom caption\n=====================================================================\nQui in magna commodo, est labitur dolorum an. Est ne magna primis\nadolescens.\n=====================================================================</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X22\">16.9. Admonition Blocks</h3>\n<div class=\"paragraph\"><p>The <em>ExampleBlock</em> definition includes a set of admonition\n<a href=\"#X23\">styles</a> (<em>NOTE</em>, <em>TIP</em>, <em>IMPORTANT</em>, <em>WARNING</em>, <em>CAUTION</em>) for\ngenerating admonition blocks (admonitions containing more than a\n<a href=\"#X28\">single paragraph</a>).  Just precede the <em>ExampleBlock</em> with an\nattribute list specifying the admonition style name. For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[NOTE]\n.A NOTE admonition block\n=====================================================================\nQui in magna commodo, est labitur dolorum an. Est ne magna primis\nadolescens.\n\n. Fusce euismod commodo velit.\n. Vivamus fringilla mi eu lacus.\n  .. Fusce euismod commodo velit.\n  .. Vivamus fringilla mi eu lacus.\n. Donec eget arcu bibendum\n  nunc consequat lobortis.\n=====================================================================</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"title\">A NOTE admonition block</div>\n<div class=\"paragraph\"><p>Qui in magna commodo, est labitur dolorum an. Est ne magna primis\nadolescens.</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum\n  nunc consequat lobortis.\n</p>\n</li>\n</ol></div>\n</td>\n</tr></tbody></table>\n</div>\n<div class=\"paragraph\"><p>See also <a href=\"#X47\">Admonition Icons and Captions</a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X29\">16.10. Open Blocks</h3>\n<div class=\"paragraph\"><p>Open blocks are special:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe open block delimiter is line containing two hyphen characters\n  (instead of four or more repeated characters).\n</p>\n</li>\n<li>\n<p>\nThey can be used to group block elements for <a href=\"#X15\">List item   continuation</a>.\n</p>\n</li>\n<li>\n<p>\nOpen blocks can be styled to behave like any other type of delimited\n  block.  The  following built-in styles can be applied to open\n  blocks: <em>literal</em>, <em>verse</em>, <em>quote</em>, <em>listing</em>, <em>TIP</em>, <em>NOTE</em>,\n  <em>IMPORTANT</em>, <em>WARNING</em>, <em>CAUTION</em>, <em>abstract</em>, <em>partintro</em>,\n  <em>comment</em>, <em>example</em>, <em>sidebar</em>, <em>source</em>, <em>music</em>, <em>latex</em>,\n  <em>graphviz</em>. For example, the following open block and listing block\n  are functionally identical:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[listing]\n--\nLorum ipsum ...\n--</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>---------------\nLorum ipsum ...\n---------------</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nAn unstyled open block groups section elements but otherwise does\n  nothing.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Open blocks are used to generate document abstracts and book part\nintroductions:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nApply the <em>abstract</em> style to generate an abstract, for example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[abstract]\n--\nIn this paper we will ...\n--</code></pre>\n</div></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nApply the <em>partintro</em> style to generate a book part introduction for\n  a multi-part book, for example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[partintro]\n.Optional part introduction title\n--\nOptional part introduction goes here.\n--</code></pre>\n</div></div>\n</li>\n</ol></div>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X64\">17. Lists</h2>\n<div class=\"sectionbody\">\n<div class=\"ulist\"><div class=\"title\">List types</div><ul>\n<li>\n<p>\nBulleted lists. Also known as itemized or unordered lists.\n</p>\n</li>\n<li>\n<p>\nNumbered lists. Also called ordered lists.\n</p>\n</li>\n<li>\n<p>\nLabeled lists. Sometimes called variable or definition lists.\n</p>\n</li>\n<li>\n<p>\nCallout lists (a list of callout annotations).\n</p>\n</li>\n</ul></div>\n<div class=\"ulist\"><div class=\"title\">List behavior</div><ul>\n<li>\n<p>\nList item indentation is optional and does not determine nesting,\n  indentation does however make the source more readable.\n</p>\n</li>\n<li>\n<p>\nAnother list or a literal paragraph immediately following a list\n  item will be implicitly included in the list item; use <a href=\"#X15\">list   item continuation</a> to explicitly append other block elements to a\n  list item.\n</p>\n</li>\n<li>\n<p>\nA comment block or a comment line block macro element will terminate\n  a list — use inline comment lines to put comments inside lists.\n</p>\n</li>\n<li>\n<p>\nThe <code>listindex</code> <a href=\"#X60\">intrinsic attribute</a> is the current list item\n  index (1..). If this attribute is used outside a list then it’s value\n  is the number of items in the most recently closed list. Useful for\n  displaying the number of items in a list.\n</p>\n</li>\n</ul></div>\n<div class=\"sect2\">\n<h3 id=\"_bulleted_lists\">17.1. Bulleted Lists</h3>\n<div class=\"paragraph\"><p>Bulleted list items start with a single dash or one to five asterisks\nfollowed by some white space then some text. Bulleted list syntaxes\nare:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>- List item.\n* List item.\n** List item.\n*** List item.\n**** List item.\n***** List item.</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_numbered_lists\">17.2. Numbered Lists</h3>\n<div class=\"paragraph\"><p>List item numbers are explicit or implicit.</p></div>\n<div class=\"paragraph\"><div class=\"title\">Explicit numbering</div><p>List items begin with a number followed by some white space then the\nitem text. The numbers can be decimal (arabic), roman (upper or lower\ncase) or alpha (upper or lower case). Decimal and alpha numbers are\nterminated with a period, roman numbers are terminated with a closing\nparenthesis. The different terminators are necessary to ensure <em>i</em>,\n<em>v</em> and <em>x</em> roman numbers are are distinguishable from <em>x</em>, <em>v</em> and\n<em>x</em> alpha numbers. Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>1.   Arabic (decimal) numbered list item.\na.   Lower case alpha (letter) numbered list item.\nF.   Upper case alpha (letter) numbered list item.\niii) Lower case roman numbered list item.\nIX)  Upper case roman numbered list item.</code></pre>\n</div></div>\n<div class=\"paragraph\"><div class=\"title\">Implicit numbering</div><p>List items begin one to five period characters, followed by some white\nspace then the item text. Examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>. Arabic (decimal) numbered list item.\n.. Lower case alpha (letter) numbered list item.\n... Lower case roman numbered list item.\n.... Upper case alpha (letter) numbered list item.\n..... Upper case roman numbered list item.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You can use the <em>style</em> attribute (also the first positional\nattribute) to specify an alternative numbering style.  The numbered\nlist style can be one of the following values: <em>arabic</em>, <em>loweralpha</em>,\n<em>upperalpha</em>, <em>lowerroman</em>, <em>upperroman</em>.</p></div>\n<div class=\"paragraph\"><p>Here are some examples of bulleted and numbered lists:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>- Praesent eget purus quis magna eleifend eleifend.\n  1. Fusce euismod commodo velit.\n    a. Fusce euismod commodo velit.\n    b. Vivamus fringilla mi eu lacus.\n    c. Donec eget arcu bibendum nunc consequat lobortis.\n  2. Vivamus fringilla mi eu lacus.\n    i)  Fusce euismod commodo velit.\n    ii) Vivamus fringilla mi eu lacus.\n  3. Donec eget arcu bibendum nunc consequat lobortis.\n  4. Nam fermentum mattis ante.\n- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.\n  * Fusce euismod commodo velit.\n  ** Qui in magna commodo, est labitur dolorum an. Est ne magna primis\n     adolescens. Sit munere ponderum dignissim et. Minim luptatum et\n     vel.\n  ** Vivamus fringilla mi eu lacus.\n  * Donec eget arcu bibendum nunc consequat lobortis.\n- Nulla porttitor vulputate libero.\n  . Fusce euismod commodo velit.\n  . Vivamus fringilla mi eu lacus.\n[upperroman]\n    .. Fusce euismod commodo velit.\n    .. Vivamus fringilla mi eu lacus.\n  . Donec eget arcu bibendum nunc consequat lobortis.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which render as:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nPraesent eget purus quis magna eleifend eleifend.\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n<div class=\"olist lowerroman\"><ol class=\"lowerroman\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n<li>\n<p>\nNam fermentum mattis ante.\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nLorem ipsum dolor sit amet, consectetuer adipiscing elit.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nQui in magna commodo, est labitur dolorum an. Est ne magna primis\n     adolescens. Sit munere ponderum dignissim et. Minim luptatum et\n     vel.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\nNulla porttitor vulputate libero.\n</p>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n<div class=\"olist upperroman\"><ol class=\"upperroman\">\n<li>\n<p>\nFusce euismod commodo velit.\n</p>\n</li>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n</ol></div>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ol></div>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>A predefined <em>compact</em> option is available to bulleted and numbered\nlists — this translates to the DocBook <em>spacing=\"compact\"</em> lists\nattribute which may or may not be processed by the DocBook toolchain.\nExample:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[options=\"compact\"]\n- Compact list item.\n- Another compact list item.</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">To apply the <em>compact</em> option globally define a document-wide\n<em>compact-option</em> attribute, e.g. using the <code>-a compact-option</code>\ncommand-line option.</td>\n</tr></tbody></table>\n</div>\n<div class=\"paragraph\"><p>You can set the list start number using the <em>start</em> attribute (works\nfor HTML outputs and DocBook outputs processed by DocBook XSL\nStylesheets). Example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[start=7]\n. List item 7.\n. List item 8.</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_labeled_lists\">17.3. Labeled Lists</h3>\n<div class=\"paragraph\"><p>Labeled list items consist of one or more text labels followed by the\ntext of the list item.</p></div>\n<div class=\"paragraph\"><p>An item label begins a line with an alphanumeric character hard\nagainst the left margin and ends with two, three or four colons or two\nsemi-colons. A list item can have multiple labels, one per line.</p></div>\n<div class=\"paragraph\"><p>The list item text consists of one or more lines of text starting\nafter the last label (either on the same line or a new line) and can\nbe followed by nested List or ListParagraph elements. Item text can be\noptionally indented.</p></div>\n<div class=\"paragraph\"><p>Here are some examples:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>In::\nLorem::\n  Fusce euismod commodo velit.\n\n  Fusce euismod commodo velit.\n\nIpsum:: Vivamus fringilla mi eu lacus.\n  * Vivamus fringilla mi eu lacus.\n  * Donec eget arcu bibendum nunc consequat lobortis.\nDolor::\n  Donec eget arcu bibendum nunc consequat lobortis.\n  Suspendisse;;\n    A massa id sem aliquam auctor.\n  Morbi;;\n    Pretium nulla vel lorem.\n  In;;\n    Dictum mauris in urna.\n    Vivamus::: Fringilla mi eu lacus.\n    Donec:::   Eget arcu bibendum nunc consequat lobortis.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which render as:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nIn\n</dt>\n<dt class=\"hdlist1\">\nLorem\n</dt>\n<dd>\n<p>\n  Fusce euismod commodo velit.\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Fusce euismod commodo velit.</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nIpsum\n</dt>\n<dd>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\nDolor\n</dt>\n<dd>\n<p>\n  Donec eget arcu bibendum nunc consequat lobortis.\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nSuspendisse\n</dt>\n<dd>\n<p>\n    A massa id sem aliquam auctor.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nMorbi\n</dt>\n<dd>\n<p>\n    Pretium nulla vel lorem.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nIn\n</dt>\n<dd>\n<p>\n    Dictum mauris in urna.\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nVivamus\n</dt>\n<dd>\n<p>\nFringilla mi eu lacus.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nDonec\n</dt>\n<dd>\n<p>\nEget arcu bibendum nunc consequat lobortis.\n</p>\n</dd>\n</dl></div>\n</dd>\n</dl></div>\n</dd>\n</dl></div>\n<div class=\"sect3\">\n<h4 id=\"_horizontal_labeled_list_style\">17.3.1. Horizontal labeled list style</h4>\n<div class=\"paragraph\"><p>The <em>horizontal</em> labeled list style (also the first positional\nattribute) places the list text side-by-side with the label instead of\nunder the label. Here is an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[horizontal]\n*Lorem*:: Fusce euismod commodo velit.  Qui in magna commodo, est\nlabitur dolorum an. Est ne magna primis adolescens.\n\n  Fusce euismod commodo velit.\n\n*Ipsum*:: Vivamus fringilla mi eu lacus.\n- Vivamus fringilla mi eu lacus.\n- Donec eget arcu bibendum nunc consequat lobortis.\n\n*Dolor*::\n  - Vivamus fringilla mi eu lacus.\n  - Donec eget arcu bibendum nunc consequat lobortis.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which render as:</p></div>\n<div class=\"hdlist\"><table>\n<tbody><tr>\n<td class=\"hdlist1\">\n<strong>Lorem</strong>\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nFusce euismod commodo velit.  Qui in magna commodo, est\nlabitur dolorum an. Est ne magna primis adolescens.\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Fusce euismod commodo velit.</code></pre>\n</div></div>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\n<strong>Ipsum</strong>\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nVivamus fringilla mi eu lacus.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ul></div>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\n<strong>Dolor</strong>\n<br>\n</td>\n<td class=\"hdlist2\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nVivamus fringilla mi eu lacus.\n</p>\n</li>\n<li>\n<p>\nDonec eget arcu bibendum nunc consequat lobortis.\n</p>\n</li>\n</ul></div>\n</td>\n</tr>\n</tbody></table></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nCurrent PDF toolchains do not make a good job of determining\n  the relative column widths for horizontal labeled lists.\n</p>\n</li>\n<li>\n<p>\nNested horizontal labeled lists will generate DocBook validation\n  errors because the <em>DocBook XML V4.2</em> DTD does not permit nested\n  informal tables (although <a href=\"#X13\">DocBook XSL Stylesheets</a> and\n  <a href=\"#X31\">dblatex</a> process them correctly).\n</p>\n</li>\n<li>\n<p>\nThe label width can be set as a percentage of the total width by\n  setting the <em>width</em> attribute e.g. <code>width=\"10%\"</code>\n</p>\n</li>\n</ul></div>\n</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_question_and_answer_lists\">17.4. Question and Answer Lists</h3>\n<div class=\"paragraph\"><p>AsciiDoc comes pre-configured with a <em>qanda</em> style labeled list for generating\nDocBook question and answer (Q&amp;A) lists. Example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[qanda]\nQuestion one::\n        Answer one.\nQuestion two::\n        Answer two.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"qlist qanda\"><ol>\n<li>\n<p><em>\nQuestion one\n</em></p>\n<p>\n        Answer one.\n</p>\n</li>\n<li>\n<p><em>\nQuestion two\n</em></p>\n<p>\n        Answer two.\n</p>\n</li>\n</ol></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_glossary_lists\">17.5. Glossary Lists</h3>\n<div class=\"paragraph\"><p>AsciiDoc comes pre-configured with a <em>glossary</em> style labeled list for\ngenerating DocBook glossary lists. Example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[glossary]\nA glossary term::\n    The corresponding definition.\nA second glossary term::\n    The corresponding definition.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>For working examples see the <code>article.txt</code> and <code>book.txt</code> documents in\nthe AsciiDoc <code>./doc</code> distribution directory.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">To generate valid DocBook output glossary lists must be located\nin a section that uses the <em>glossary</em> <a href=\"#X93\">section markup template</a>.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_bibliography_lists\">17.6. Bibliography Lists</h3>\n<div class=\"paragraph\"><p>AsciiDoc comes with a predefined <em>bibliography</em> bulleted list style\ngenerating DocBook bibliography entries. Example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[bibliography]\n.Optional list title\n- [[[taoup]]] Eric Steven Raymond. 'The Art of UNIX\n  Programming'. Addison-Wesley. ISBN 0-13-142901-9.\n- [[[walsh-muellner]]] Norman Walsh &amp; Leonard Muellner.\n  'DocBook - The Definitive Guide'. O'Reilly &amp; Associates.\n  1999. ISBN 1-56592-580-7.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>[[[&lt;reference&gt;]]]</code> syntax is a bibliography entry anchor, it\ngenerates an anchor named <code>&lt;reference&gt;</code> and additionally displays\n<code>[&lt;reference&gt;]</code> at the anchor position. For example <code>[[[taoup]]]</code>\ngenerates an anchor named <code>taoup</code> that displays <code>[taoup]</code> at the\nanchor position. Cite the reference from elsewhere your document using\n<code>&lt;&lt;taoup&gt;&gt;</code>, this displays a hyperlink (<code>[taoup]</code>) to the\ncorresponding bibliography entry anchor.</p></div>\n<div class=\"paragraph\"><p>For working examples see the <code>article.txt</code> and <code>book.txt</code> documents in\nthe AsciiDoc <code>./doc</code> distribution directory.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">To generate valid DocBook output bibliography lists must be\nlocated in a <a href=\"#X93\">bibliography section</a>.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X15\">17.7. List Item Continuation</h3>\n<div class=\"paragraph\"><p>Another list or a literal paragraph immediately following a list item\nis implicitly appended to the list item; to append other block\nelements to a list item you need to explicitly join them to the list\nitem with a <em>list continuation</em> (a separator line containing a single\nplus character). Multiple block elements can be appended to a list\nitem using list continuations (provided they are legal list item\nchildren in the backend markup).</p></div>\n<div class=\"paragraph\"><p>Here are some examples of list item continuations: list item one\ncontains multiple continuations; list item two is continued with an\n<a href=\"#X29\">OpenBlock</a> containing multiple elements:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>1. List item one.\n+\nList item one continued with a second paragraph followed by an\nIndented block.\n+\n.................\n$ ls *.sh\n$ mv *.sh ~/tmp\n.................\n+\nList item continued with a third paragraph.\n\n2. List item two continued with an open block.\n+\n--\nThis paragraph is part of the preceding list item.\n\na. This list is nested and does not require explicit item continuation.\n+\nThis paragraph is part of the preceding list item.\n\nb. List item b.\n\nThis paragraph belongs to item two of the outer list.\n--</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nList item one.\n</p>\n<div class=\"paragraph\"><p>List item one continued with a second paragraph followed by an\nIndented block.</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ ls *.sh\n$ mv *.sh ~/tmp</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>List item continued with a third paragraph.</p></div>\n</li>\n<li>\n<p>\nList item two continued with an open block.\n</p>\n<div class=\"openblock\">\n<div class=\"content\">\n<div class=\"paragraph\"><p>This paragraph is part of the preceding list item.</p></div>\n<div class=\"olist loweralpha\"><ol class=\"loweralpha\">\n<li>\n<p>\nThis list is nested and does not require explicit item continuation.\n</p>\n<div class=\"paragraph\"><p>This paragraph is part of the preceding list item.</p></div>\n</li>\n<li>\n<p>\nList item b.\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>This paragraph belongs to item two of the outer list.</p></div>\n</div></div>\n</li>\n</ol></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X92\">18. Footnotes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The shipped AsciiDoc configuration includes three footnote inline\nmacros:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>footnote:[&lt;text&gt;]</code>\n</dt>\n<dd>\n<p>\n  Generates a footnote with text <code>&lt;text&gt;</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>footnoteref:[&lt;id&gt;,&lt;text&gt;]</code>\n</dt>\n<dd>\n<p>\n  Generates a footnote with a reference ID <code>&lt;id&gt;</code> and text <code>&lt;text&gt;</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>footnoteref:[&lt;id&gt;]</code>\n</dt>\n<dd>\n<p>\n  Generates a reference to the footnote with ID <code>&lt;id&gt;</code>.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>The footnote text can span multiple lines.</p></div>\n<div class=\"paragraph\"><p>The <em>xhtml11</em> and <em>html5</em> backends render footnotes dynamically using\nJavaScript; <em>html4</em> outputs do not use JavaScript and leave the\nfootnotes inline; <em>docbook</em> footnotes are processed by the downstream\nDocBook toolchain.</p></div>\n<div class=\"paragraph\"><p>Example footnotes:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>A footnote footnote:[An example footnote.];\na second footnote with a reference ID footnoteref:[note2,Second footnote.];\nfinally a reference to the second footnote footnoteref:[note2].</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Renders:</p></div>\n<div class=\"paragraph\"><p>A footnote <span class=\"footnote\"><br>[An example footnote.]<br></span>;\na second footnote with a reference ID <span class=\"footnote\" id=\"_footnote_note2\"><br>[Second footnote.]<br></span>;\nfinally a reference to the second footnote <span class=\"footnoteref\"><br><a href=\"#_footnote_note2\">[note2]</a><br></span>.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_indexes\">19. Indexes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The shipped AsciiDoc configuration includes the inline macros for\ngenerating DocBook index entries.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>indexterm:[&lt;primary&gt;,&lt;secondary&gt;,&lt;tertiary&gt;]</code>\n</dt>\n<dt class=\"hdlist1\">\n<code>(((&lt;primary&gt;,&lt;secondary&gt;,&lt;tertiary&gt;)))</code>\n</dt>\n<dd>\n<p>\n    This inline macro generates an index term (the <code>&lt;secondary&gt;</code> and\n    <code>&lt;tertiary&gt;</code> positional attributes are optional). Example:\n    <code>indexterm:[Tigers,Big cats]</code> (or, using the alternative syntax\n    <code>(((Tigers,Big cats)))</code>.  Index terms that have secondary and\n    tertiary entries also generate separate index terms for the\n    secondary and tertiary entries. The index terms appear in the\n    index, not the primary text flow.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>indexterm2:[&lt;primary&gt;]</code>\n</dt>\n<dt class=\"hdlist1\">\n<code>((&lt;primary&gt;))</code>\n</dt>\n<dd>\n<p>\n    This inline macro generates an index term that appears in both the\n    index and the primary text flow.  The <code>&lt;primary&gt;</code> should not be\n    padded to the left or right with white space characters.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>For working examples see the <code>article.txt</code> and <code>book.txt</code> documents in\nthe AsciiDoc <code>./doc</code> distribution directory.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">Index entries only really make sense if you are generating\nDocBook markup — DocBook conversion programs automatically generate\nan index at the point an <em>Index</em> section appears in source document.</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X105\">20. Callouts</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Callouts are a mechanism for annotating verbatim text (for example:\nsource code, computer output and user input). Callout markers are\nplaced inside the annotated text while the actual annotations are\npresented in a callout list after the annotated text. Here’s an\nexample:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code> .MS-DOS directory listing\n -----------------------------------------------------\n 10/17/97   9:04         &lt;DIR&gt;    bin\n 10/16/97  14:11         &lt;DIR&gt;    DOS            &lt;1&gt;\n 10/16/97  14:40         &lt;DIR&gt;    Program Files\n 10/16/97  14:46         &lt;DIR&gt;    TEMP\n 10/17/97   9:04         &lt;DIR&gt;    tmp\n 10/16/97  14:37         &lt;DIR&gt;    WINNT\n 10/16/97  14:25             119  AUTOEXEC.BAT   &lt;2&gt;\n  2/13/94   6:21          54,619  COMMAND.COM    &lt;2&gt;\n 10/16/97  14:25             115  CONFIG.SYS     &lt;2&gt;\n 11/16/97  17:17      61,865,984  pagefile.sys\n  2/13/94   6:21           9,349  WINA20.386     &lt;3&gt;\n -----------------------------------------------------\n\n &lt;1&gt; This directory holds MS-DOS.\n &lt;2&gt; System startup code for DOS.\n &lt;3&gt; Some sort of Windows 3.1 hack.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which renders:</p></div>\n<div class=\"listingblock\">\n<div class=\"title\">MS-DOS directory listing</div>\n<div class=\"content\">\n<pre><code>10/17/97   9:04         &lt;DIR&gt;    bin\n10/16/97  14:11         &lt;DIR&gt;    DOS            <img src=\"asciidoc-userguide_files/1.png\" alt=\"1\">\n10/16/97  14:40         &lt;DIR&gt;    Program Files\n10/16/97  14:46         &lt;DIR&gt;    TEMP\n10/17/97   9:04         &lt;DIR&gt;    tmp\n10/16/97  14:37         &lt;DIR&gt;    WINNT\n10/16/97  14:25             119  AUTOEXEC.BAT   <img src=\"asciidoc-userguide_files/2.png\" alt=\"2\">\n 2/13/94   6:21          54,619  COMMAND.COM    <img src=\"asciidoc-userguide_files/2.png\" alt=\"2\">\n10/16/97  14:25             115  CONFIG.SYS     <img src=\"asciidoc-userguide_files/2.png\" alt=\"2\">\n11/16/97  17:17      61,865,984  pagefile.sys\n 2/13/94   6:21           9,349  WINA20.386     <img src=\"asciidoc-userguide_files/3.png\" alt=\"3\"></code></pre>\n</div></div>\n<div class=\"colist arabic\"><table>\n<tbody><tr><td><img src=\"asciidoc-userguide_files/1.png\" alt=\"1\"></td><td>\nThis directory holds MS-DOS.\n</td></tr>\n<tr><td><img src=\"asciidoc-userguide_files/2.png\" alt=\"2\"></td><td>\nSystem startup code for DOS.\n</td></tr>\n<tr><td><img src=\"asciidoc-userguide_files/3.png\" alt=\"3\"></td><td>\nSome sort of Windows 3.1 hack.\n</td></tr>\n</tbody></table></div>\n<div class=\"ulist\"><div class=\"title\">Explanation</div><ul>\n<li>\n<p>\nThe callout marks are whole numbers enclosed in angle brackets —   they \nrefer to the correspondingly numbered item in the following\n  callout list.\n</p>\n</li>\n<li>\n<p>\nBy default callout marks are confined to <em>LiteralParagraphs</em>,\n  <em>LiteralBlocks</em> and <em>ListingBlocks</em> (although this is a\n  configuration file option and can be changed).\n</p>\n</li>\n<li>\n<p>\nCallout list item numbering is fairly relaxed — list items can\n  start with <code>&lt;n&gt;</code>, <code>n&gt;</code> or <code>&gt;</code> where <code>n</code> is the optional list item\n  number (in the latter case list items starting with a single <code>&gt;</code>\n  character are implicitly numbered starting at one).\n</p>\n</li>\n<li>\n<p>\nCallout lists should not be nested.\n</p>\n</li>\n<li>\n<p>\nCallout lists start list items hard against the left margin.\n</p>\n</li>\n<li>\n<p>\nIf you want to present a number inside angle brackets you’ll need to\n  escape it with a backslash to prevent it being interpreted as a\n  callout mark.\n</p>\n</li>\n</ul></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">Define the AsciiDoc <em>icons</em> attribute (for example using the <code>-a\nicons</code> command-line option) to display callout icons.</td>\n</tr></tbody></table>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_implementation_notes\">20.1. Implementation Notes</h3>\n<div class=\"paragraph\"><p>Callout marks are generated by the <em>callout</em> inline macro while\ncallout lists are generated using the <em>callout</em> list definition. The\n<em>callout</em> macro and <em>callout</em> list are special in that they work\ntogether. The <em>callout</em> inline macro is not enabled by the normal\n<em>macros</em> substitutions option, instead it has its own <em>callouts</em>\nsubstitution option.</p></div>\n<div class=\"paragraph\"><p>The following attributes are available during inline callout macro\nsubstitution:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>{index}</code>\n</dt>\n<dd>\n<p>\n    The callout list item index inside the angle brackets.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{coid}</code>\n</dt>\n<dd>\n<p>\n    An identifier formatted like <code>CO&lt;listnumber&gt;-&lt;index&gt;</code> that\n    uniquely identifies the callout mark. For example <code>CO2-4</code>\n    identifies the fourth callout mark in the second set of callout\n    marks.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>The <code>{coids}</code> attribute can be used during callout list item\nsubstitution — it is a space delimited list of callout IDs that refer\nto the explanatory list item.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_including_callouts_in_included_code\">20.2. Including callouts in included code</h3>\n<div class=\"paragraph\"><p>You can annotate working code examples with callouts — just remember\nto put the callouts inside source code comments. This example displays\nthe <code>test.py</code> source file (containing a single callout) using the\n<em>source</em> (code highlighter) filter:</p></div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code> [source,python]\n -------------------------------------------\n \\include::test.py[]\n -------------------------------------------\n\n &lt;1&gt; Print statement.</code></pre>\n</div></div>\n<div class=\"listingblock\">\n<div class=\"title\">Included <code>test.py</code> source</div>\n<div class=\"content\">\n<pre><code>print 'Hello World!'   # &lt;1&gt;</code></pre>\n</div></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_macros\">21. Macros</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Macros are a mechanism for substituting parametrized text into output\ndocuments.</p></div>\n<div class=\"paragraph\"><p>Macros have a <em>name</em>, a single <em>target</em> argument and an <em>attribute\nlist</em>.  The usual syntax is <code>&lt;name&gt;:&lt;target&gt;[&lt;attrlist&gt;]</code> (for\ninline macros) and <code>&lt;name&gt;::&lt;target&gt;[&lt;attrlist&gt;]</code> (for block\nmacros).  Here are some examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>http://www.docbook.org/[DocBook.org]\ninclude::chapt1.txt[tabsize=2]\nmailto:srackham@gmail.com[]</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Macro behavior</div><ul>\n<li>\n<p>\n<code>&lt;name&gt;</code> is the macro name. It can only contain letters, digits or\n  dash characters and cannot start with a dash.\n</p>\n</li>\n<li>\n<p>\nThe optional <code>&lt;target&gt;</code> cannot contain white space characters.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;attrlist&gt;</code> is a <a href=\"#X21\">list of attributes</a> enclosed in square\n  brackets.\n</p>\n</li>\n<li>\n<p>\n<code>]</code> characters inside attribute lists must be escaped with a\n  backslash.\n</p>\n</li>\n<li>\n<p>\nExpansion of macro references can normally be escaped by prefixing a\n  backslash character (see the AsciiDoc <em>FAQ</em> for examples of\n  exceptions to this rule).\n</p>\n</li>\n<li>\n<p>\nAttribute references in block macros are expanded.\n</p>\n</li>\n<li>\n<p>\nThe substitutions performed prior to Inline macro macro expansion\n  are determined by the inline context.\n</p>\n</li>\n<li>\n<p>\nMacros are processed in the order they appear in the configuration\n  file(s).\n</p>\n</li>\n<li>\n<p>\nCalls to inline macros can be nested inside different inline macros\n  (an inline macro call cannot contain a nested call to itself).\n</p>\n</li>\n<li>\n<p>\nIn addition to <code>&lt;name&gt;</code>, <code>&lt;target&gt;</code> and <code>&lt;attrlist&gt;</code> the\n  <code>&lt;passtext&gt;</code> and <code>&lt;subslist&gt;</code> named groups are available to\n  <a href=\"#X77\">passthrough macros</a>. A macro is a passthrough macro if the\n  definition includes a <code>&lt;passtext&gt;</code> named group.\n</p>\n</li>\n</ul></div>\n<div class=\"sect2\">\n<h3 id=\"_inline_macros\">21.1. Inline Macros</h3>\n<div class=\"paragraph\"><p>Inline Macros occur in an inline element context. Predefined Inline\nmacros include <em>URLs</em>, <em>image</em> and <em>link</em> macros.</p></div>\n<div class=\"sect3\">\n<h4 id=\"_urls\">21.1.1. URLs</h4>\n<div class=\"paragraph\"><p><em>http</em>, <em>https</em>, <em>ftp</em>, <em>file</em>, <em>mailto</em> and <em>callto</em> URLs are\nrendered using predefined inline macros.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf you don’t need a custom link caption you can enter the <em>http</em>,\n  <em>https</em>, <em>ftp</em>, <em>file</em> URLs and email addresses without any special\n  macro syntax.\n</p>\n</li>\n<li>\n<p>\nIf the <code>&lt;attrlist&gt;</code> is empty the URL is displayed.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Here are some examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>http://www.docbook.org/[DocBook.org]\nhttp://www.docbook.org/\nmailto:joe.bloggs@foobar.com[email Joe Bloggs]\njoe.bloggs@foobar.com</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which are rendered:</p></div>\n<div class=\"paragraph\"><p><a href=\"http://www.docbook.org/\">DocBook.org</a></p></div>\n<div class=\"paragraph\"><p><a href=\"http://www.docbook.org/\">http://www.docbook.org/</a></p></div>\n<div class=\"paragraph\"><p><a href=\"mailto:joe.bloggs@foobar.com\">email Joe Bloggs</a></p></div>\n<div class=\"paragraph\"><p><a href=\"mailto:joe.bloggs@foobar.com\">joe.bloggs@foobar.com</a></p></div>\n<div class=\"paragraph\"><p>If the <code>&lt;target&gt;</code> necessitates space characters use <code>%20</code>, for example\n<code>large%20image.png</code>.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_internal_cross_references\">21.1.2. Internal Cross References</h4>\n<div class=\"paragraph\"><p>Two AsciiDoc inline macros are provided for creating hypertext links\nwithin an AsciiDoc document. You can use either the standard macro\nsyntax or the (preferred) alternative.</p></div>\n<div class=\"sect4\">\n<h5 id=\"X30\">anchor</h5>\n<div class=\"paragraph\"><p>Used to specify hypertext link targets:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[[&lt;id&gt;,&lt;xreflabel&gt;]]\nanchor:&lt;id&gt;[&lt;xreflabel&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>&lt;id&gt;</code> is a unique string that conforms to the output markup’s\nanchor syntax. The optional <code>&lt;xreflabel&gt;</code> is the text to be displayed\nby captionless <em>xref</em> macros that refer to this anchor. The optional\n<code>&lt;xreflabel&gt;</code> is only really useful when generating DocBook output.\nExample anchor:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[[X1]]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You may have noticed that the syntax of this inline element is the\nsame as that of the <a href=\"#X41\">BlockId block element</a>, this is no\ncoincidence since they are functionally equivalent.</p></div>\n</div>\n<div class=\"sect4\">\n<h5 id=\"_xref\">xref</h5>\n<div class=\"paragraph\"><p>Creates a hypertext link to a document anchor.</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;&lt;&lt;id&gt;,&lt;caption&gt;&gt;&gt;\nxref:&lt;id&gt;[&lt;caption&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>&lt;id&gt;</code> refers to an anchor ID. The optional <code>&lt;caption&gt;</code> is the\nlink’s displayed text. Example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;&lt;X21,attribute lists&gt;&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If <code>&lt;caption&gt;</code> is not specified then the displayed text is\nauto-generated:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe AsciiDoc <em>xhtml11</em> and <em>html5</em> backends display the <code>&lt;id&gt;</code>\n  enclosed in square brackets.\n</p>\n</li>\n<li>\n<p>\nIf DocBook is produced the DocBook toolchain is responsible for the\n  displayed text which will normally be the referenced figure, table\n  or section title number followed by the element’s title text.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Here is an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[[tiger_image]]\n.Tyger tyger\nimage::tiger.png[]\n\nThis can be seen in &lt;&lt;tiger_image&gt;&gt;.</code></pre>\n</div></div>\n</div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_linking_to_local_documents\">21.1.3. Linking to Local Documents</h4>\n<div class=\"paragraph\"><p>Hypertext links to files on the local file system are specified using\nthe <em>link</em> inline macro.</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>link:&lt;target&gt;[&lt;caption&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <em>link</em> macro generates relative URLs. The link macro <code>&lt;target&gt;</code> is\nthe target file name (relative to the file system location of the\nreferring document). The optional <code>&lt;caption&gt;</code> is the link’s displayed\ntext. If <code>&lt;caption&gt;</code> is not specified then <code>&lt;target&gt;</code> is displayed.\nExample:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>link:downloads/foo.zip[download foo.zip]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You can use the <code>&lt;filename&gt;#&lt;id&gt;</code> syntax to refer to an anchor within\na target document but this usually only makes sense when targeting\nHTML documents.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X9\">21.1.4. Images</h4>\n<div class=\"paragraph\"><p>Inline images are inserted into the output document using the <em>image</em>\nmacro. The inline syntax is:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image:&lt;target&gt;[&lt;attributes&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The contents of the image file <code>&lt;target&gt;</code> is displayed. To display the\nimage its file format must be supported by the target backend\napplication. HTML and DocBook applications normally support PNG or JPG\nfiles.</p></div>\n<div class=\"paragraph\"><p><code>&lt;target&gt;</code> file name paths are relative to the location of the\nreferring document.</p></div>\n<div class=\"ulist\" id=\"X55\"><div class=\"title\">Image macro attributes</div><ul>\n<li>\n<p>\nThe optional <em>alt</em> attribute is also the first positional attribute,\n  it specifies alternative text which is displayed if the output\n  application is unable to display the image file (see also\n  <a href=\"http://htmlhelp.com/feature/art3.htm\">Use of ALT texts in IMGs</a>). For\n  example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image:images/logo.png[Company Logo]</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nThe optional <em>title</em> attribute provides a title for the image. The\n  <a href=\"#X49\">block image macro</a> renders the title alongside the image.\n  The inline image macro displays the title as a popup “tooltip” in\n  visual browsers (AsciiDoc HTML outputs only).\n</p>\n</li>\n<li>\n<p>\nThe optional <code>width</code> and <code>height</code> attributes scale the image size\n  and can be used in any combination. The units are pixels.  The\n  following example scales the previous example to a height of 32\n  pixels:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image:images/logo.png[\"Company Logo\",height=32]</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nThe optional <code>link</code> attribute is used to link the image to an\n  external document. The following example links a screenshot\n  thumbnail to a full size version:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image:screen-thumbnail.png[height=32,link=\"screen.png\"]</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nThe optional <code>scaledwidth</code> attribute is only used in DocBook block\n  images (specifically for PDF documents). The following example\n  scales the images to 75% of the available print width:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image::images/logo.png[scaledwidth=\"75%\",alt=\"Company Logo\"]</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nThe image <code>scale</code> attribute sets the DocBook <code>imagedata</code> element\n  <code>scale</code> attribute.\n</p>\n</li>\n<li>\n<p>\nThe optional <code>align</code> attribute is used for horizontal image\n  alignment.  Allowed values are <code>center</code>, <code>left</code> and <code>right</code>. For\n  example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image::images/tiger.png[\"Tiger image\",align=\"left\"]</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nThe optional <code>float</code> attribute floats the image <code>left</code> or <code>right</code> on\n  the page (works with HTML outputs only, has no effect on DocBook\n  outputs). <code>float</code> and <code>align</code> attributes are mutually exclusive.\n  Use the <code>unfloat::[]</code> block macro to stop floating.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_comment_lines\">21.1.5. Comment Lines</h4>\n<div class=\"paragraph\"><p>See <a href=\"#X25\">comment block macro</a>.</p></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_block_macros\">21.2. Block Macros</h3>\n<div class=\"paragraph\"><p>A Block macro reference must be contained in a single line separated\neither side by a blank line or a block delimiter.</p></div>\n<div class=\"paragraph\"><p>Block macros behave just like Inline macros, with the following\ndifferences:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThey occur in a block context.\n</p>\n</li>\n<li>\n<p>\nThe default syntax is <code>&lt;name&gt;::&lt;target&gt;[&lt;attrlist&gt;]</code> (two\n  colons, not one).\n</p>\n</li>\n<li>\n<p>\nMarkup template section names end in <code>-blockmacro</code> instead of\n  <code>-inlinemacro</code>.\n</p>\n</li>\n</ul></div>\n<div class=\"sect3\">\n<h4 id=\"_block_identifier\">21.2.1. Block Identifier</h4>\n<div class=\"paragraph\"><p>The Block Identifier macro sets the <code>id</code> attribute and has the same\nsyntax as the <a href=\"#X30\">anchor inline macro</a> since it performs\nessentially the same function — block templates use the <code>id</code>\nattribute as a block element ID. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[[X30]]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>This is equivalent to the <code>[id=\"X30\"]</code> <a href=\"#X79\">AttributeList element</a>).</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X49\">21.2.2. Images</h4>\n<div class=\"paragraph\"><p>The <em>image</em> block macro is used to display images in a block context.\nThe syntax is:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image::&lt;target&gt;[&lt;attributes&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The block <code>image</code> macro has the same <a href=\"#X55\">macro attributes</a> as it’s\n<a href=\"#X9\">inline image macro</a> counterpart.</p></div>\n<div class=\"paragraph\"><p>Block images can be titled by preceding the <em>image</em> macro with a\n<em>BlockTitle</em>.  DocBook toolchains normally number titled block images\nand optionally list them in an automatically generated <em>List of\nFigures</em> backmatter section.</p></div>\n<div class=\"paragraph\"><p>This example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>.Main circuit board\nimage::images/layout.png[J14P main circuit board]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>is equivalent to:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>image::images/layout.png[\"J14P main circuit board\",\n                          title=\"Main circuit board\"]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>A title prefix that can be inserted with the <code>caption</code> attribute\n(HTML backends). For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>.Main circuit board\n[caption=\"Figure 2: \"]\nimage::images/layout.png[J14P main circuit board]</code></pre>\n</div></div>\n<div class=\"sidebarblock\" id=\"X66\">\n<div class=\"content\">\n<div class=\"title\">Embedding images in XHTML documents</div>\n<div class=\"paragraph\"><p>If you define the <code>data-uri</code> attribute then images will be embedded in\nXHTML outputs using the\n<a href=\"http://en.wikipedia.org/wiki/Data:_URI_scheme\">data URI scheme</a>.  You\ncan use the <em>data-uri</em> attribute with the <em>xhtml11</em> and <em>html5</em>\nbackends to produce single-file XHTML documents with embedded images\nand CSS, for example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a data-uri mydocument.txt</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAll current popular browsers support data URIs, although versions\n  of Internet Explorer prior to version 8 do not.\n</p>\n</li>\n<li>\n<p>\nSome browsers limit the size of data URIs.\n</p>\n</li>\n</ul></div>\n</td>\n</tr></tbody></table>\n</div>\n</div></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X25\">21.2.3. Comment Lines</h4>\n<div class=\"paragraph\"><p>Single lines starting with two forward slashes hard up against the\nleft margin are treated as comments. Comment lines do not appear in\nthe output unless the <em>showcomments</em> attribute is defined.  Comment\nlines have been implemented as both block and inline macros so a\ncomment line can appear as a stand-alone block or within block elements\nthat support inline macro expansion. Example comment line:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>// This is a comment.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If the <em>showcomments</em> attribute is defined comment lines are written\nto the output:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIn DocBook the comment lines are enclosed by the <em>remark</em> element\n  (which may or may not be rendered by your toolchain).\n</p>\n</li>\n<li>\n<p>\nThe <em>showcomments</em> attribute does not expose <a href=\"#X26\">Comment Blocks</a>.\n  Comment Blocks are never passed to the output.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_system_macros\">21.3. System Macros</h3>\n<div class=\"paragraph\"><p>System macros are block macros that perform a predefined task and are\nhardwired into the <code>asciidoc(1)</code> program.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nYou can escape system macros with a leading backslash character\n  (as you can with other macros).\n</p>\n</li>\n<li>\n<p>\nThe syntax and tasks performed by system macros is built into\n  <code>asciidoc(1)</code> so they don’t appear in configuration files.  You can\n  however customize the syntax by adding entries to a configuration\n  file <code>[macros]</code> section.\n</p>\n</li>\n</ul></div>\n<div class=\"sect3\">\n<h4 id=\"X63\">21.3.1. Include Macros</h4>\n<div class=\"paragraph\"><p>The <code>include</code> and <code>include1</code>  system macros to include the contents of\na named file into the source document.</p></div>\n<div class=\"paragraph\"><p>The <code>include</code> macro includes a file as if it were part of the parent\ndocument — tabs are expanded and system macros processed. The\ncontents of <code>include1</code> files are not subject to tab expansion or\nsystem macro processing nor are attribute or lower priority\nsubstitutions performed. The <code>include1</code> macro’s intended use is to\ninclude verbatim embedded CSS or scripts into configuration file\nheaders.  Example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>include::chapter1.txt[tabsize=4]</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Include macro behavior</div><ul>\n<li>\n<p>\nIf the included file name is specified with a relative path then the\n  path is relative to the location of the referring document.\n</p>\n</li>\n<li>\n<p>\nInclude macros can appear inside configuration files.\n</p>\n</li>\n<li>\n<p>\nFiles included from within <em>DelimitedBlocks</em> are read to completion\n  to avoid false end-of-block underline termination.\n</p>\n</li>\n<li>\n<p>\nAttribute references are expanded inside the include <em>target</em>; if an\n  attribute is undefined then the included file is silently skipped.\n</p>\n</li>\n<li>\n<p>\nThe <em>tabsize</em> macro attribute sets the number of space characters to\n  be used for tab expansion in the included file (not applicable to\n  <code>include1</code> macro).\n</p>\n</li>\n<li>\n<p>\nThe <em>depth</em> macro attribute sets the maximum permitted number of\n  subsequent nested includes (not applicable to <code>include1</code> macro which\n  does not process nested includes). Setting <em>depth</em> to <em>1</em> disables\n  nesting inside the included file. By default, nesting is limited to\n  a depth of ten.\n</p>\n</li>\n<li>\n<p>\nIf the he <em>warnings</em> attribute is set to <em>False</em> (or any other\n  Python literal that evaluates to boolean false) then no warning\n  message is printed if the included file does not exist. By default\n  <em>warnings</em> are enabled.\n</p>\n</li>\n<li>\n<p>\nInternally the <code>include1</code> macro is translated to the <code>include1</code>\n  system attribute which means it must be evaluated in a region where\n  attribute substitution is enabled. To inhibit nested substitution in\n  included files it is preferable to use the <code>include</code> macro and set\n  the attribute <code>depth=1</code>.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_conditional_inclusion_macros\">21.3.2. Conditional Inclusion Macros</h4>\n<div class=\"paragraph\"><p>Lines of text in the source document can be selectively included or\nexcluded from processing based on the existence (or not) of a document\nattribute.</p></div>\n<div class=\"paragraph\"><p>Document text between the <code>ifdef</code> and <code>endif</code> macros is included if a\ndocument attribute is defined:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>ifdef::&lt;attribute&gt;[]\n:\nendif::&lt;attribute&gt;[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Document text between the <code>ifndef</code> and <code>endif</code> macros is not included\nif a document attribute is defined:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>ifndef::&lt;attribute&gt;[]\n:\nendif::&lt;attribute&gt;[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><code>&lt;attribute&gt;</code> is an attribute name which is optional in the trailing\n<code>endif</code> macro.</p></div>\n<div class=\"paragraph\"><p>If you only want to process a single line of text then the text can be\nput inside the square brackets and the <code>endif</code> macro omitted, for\nexample:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>ifdef::revnumber[Version number 42]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Is equivalent to:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>ifdef::revnumber[]\nVersion number 42\nendif::revnumber[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><em>ifdef</em> and <em>ifndef</em> macros also accept multiple attribute names:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nMultiple <em>,</em> separated attribute names evaluate to defined if one\n  or more of the attributes is defined, otherwise it’s value is\n  undefined.\n</p>\n</li>\n<li>\n<p>\nMultiple <em>+</em> separated attribute names evaluate to defined if all\n  of the attributes is defined, otherwise it’s value is undefined.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Document text between the <code>ifeval</code> and <code>endif</code> macros is included if\nthe Python expression inside the square brackets is true. Example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>ifeval::[{rs458}==2]\n:\nendif::[]</code></pre>\n</div></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nDocument attribute references are expanded before the expression is\n  evaluated.\n</p>\n</li>\n<li>\n<p>\nIf an attribute reference is undefined then the expression is\n  considered false.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Take a look at the <code>*.conf</code> configuration files in the AsciiDoc\ndistribution for examples of conditional inclusion macro usage.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_executable_system_macros\">21.3.3. Executable system macros</h4>\n<div class=\"paragraph\"><p>The <em>eval</em>, <em>sys</em> and <em>sys2</em> block macros exhibit the same behavior as\ntheir same named <a href=\"#X24\">system attribute references</a>. The difference\nis that system macros occur in a block macro context whereas system\nattributes are confined to inline contexts where attribute\nsubstitution is enabled.</p></div>\n<div class=\"paragraph\"><p>The following example displays a long directory listing inside a\nliteral block:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>------------------\nsys::[ls -l *.txt]\n------------------</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">There are no block macro versions of the <em>eval3</em> and <em>sys3</em>\nsystem attributes.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_template_system_macro\">21.3.4. Template System Macro</h4>\n<div class=\"paragraph\"><p>The <code>template</code> block macro allows the inclusion of one configuration\nfile template section within another.  The following example includes\nthe <code>[admonitionblock]</code> section in the <code>[admonitionparagraph]</code>\nsection:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[admonitionparagraph]\ntemplate::[admonitionblock]</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Template macro behavior</div><ul>\n<li>\n<p>\nThe <code>template::[]</code> macro is useful for factoring configuration file\n  markup.\n</p>\n</li>\n<li>\n<p>\n<code>template::[]</code> macros cannot be nested.\n</p>\n</li>\n<li>\n<p>\n<code>template::[]</code> macro expansion is applied after all configuration\n  files have been read.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X77\">21.4. Passthrough macros</h3>\n<div class=\"paragraph\"><p>Passthrough macros are analogous to <a href=\"#X76\">passthrough blocks</a> and are\nused to pass text directly to the output. The substitution performed\non the text is determined by the macro definition but can be overridden\nby the <code>&lt;subslist&gt;</code>.  The usual syntax is\n<code>&lt;name&gt;:&lt;subslist&gt;[&lt;passtext&gt;]</code> (for inline macros) and\n<code>&lt;name&gt;::&lt;subslist&gt;[&lt;passtext&gt;]</code> (for block macros). Passthroughs, by\ndefinition, take precedence over all other text substitutions.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\npass\n</dt>\n<dd>\n<p>\n  Inline and block. Passes text unmodified (apart from explicitly\n  specified substitutions). Examples:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>pass:[&lt;q&gt;To be or not to be&lt;/q&gt;]\npass:attributes,quotes[&lt;u&gt;the '{author}'&lt;/u&gt;]</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nasciimath, latexmath\n</dt>\n<dd>\n<p>\n  Inline and block. Passes text unmodified.  Used for\n  <a href=\"#X78\">mathematical formulas</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n+++\n</dt>\n<dd>\n<p>\n  Inline and block. The triple-plus passthrough is functionally\n  identical to the <em>pass</em> macro but you don’t have to escape <code>]</code>\n  characters and you can prefix with quoted attributes in the inline\n  version. Example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Red [red]+++`sum_(i=1)\\^n i=(n(n+1))/2`$+++ AsciiMathML formula</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n$$\n</dt>\n<dd>\n<p>\n  Inline and block. The double-dollar passthrough is functionally\n  identical to the triple-plus passthrough with one exception: special\n  characters are escaped. Example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$$`[[a,b],[c,d]]((n),(k))`$$</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\n<a id=\"X80\"></a>`\n</dt>\n<dd>\n<p>\n  Text quoted with single backtick characters constitutes an <em>inline\n  literal</em> passthrough. The enclosed text is rendered in a monospaced\n  font and is only subject to special character substitution.  This\n  makes sense since monospace text is usually intended to be rendered\n  literally and often contains characters that would otherwise have to\n  be escaped. If you need monospaced text containing inline\n  substitutions use a <a href=\"#X81\">plus character instead of a backtick</a>.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_macro_definitions\">21.5. Macro Definitions</h3>\n<div class=\"paragraph\"><p>Each entry in the configuration <code>[macros]</code> section is a macro\ndefinition which can take one of the following forms:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>&lt;pattern&gt;=&lt;name&gt;[&lt;subslist]</code>\n</dt>\n<dd>\n<p>\nInline macro definition.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>&lt;pattern&gt;=#&lt;name&gt;[&lt;subslist]</code>\n</dt>\n<dd>\n<p>\nBlock macro definition.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>&lt;pattern&gt;=+&lt;name&gt;[&lt;subslist]</code>\n</dt>\n<dd>\n<p>\nSystem macro definition.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>&lt;pattern&gt;</code>\n</dt>\n<dd>\n<p>\nDelete the existing macro with this <code>&lt;pattern&gt;</code>.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p><code>&lt;pattern&gt;</code> is a Python regular expression and <code>&lt;name&gt;</code> is the name of\na markup template. If <code>&lt;name&gt;</code> is omitted then it is the value of the\nregular expression match group named <em>name</em>.  The optional\n<code>[&lt;subslist]</code> is a comma-separated list of substitution names enclosed\nin <code>[]</code> brackets, it sets the default substitutions for passthrough\ntext, if omitted then no passthrough substitutions are performed.</p></div>\n<div class=\"paragraph\"><div class=\"title\">Pattern named groups</div><p>The following named groups can be used in macro <code>&lt;pattern&gt;</code> regular\nexpressions and are available as markup template attributes:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nname\n</dt>\n<dd>\n<p>\n  The macro name.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntarget\n</dt>\n<dd>\n<p>\n  The macro target.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nattrlist\n</dt>\n<dd>\n<p>\n  The macro attribute list.\n</p>\n</dd>\n<dt class=\"hdlist1\">\npasstext\n</dt>\n<dd>\n<p>\n  Contents of this group are passed unmodified to the output subject\n  only to <em>subslist</em> substitutions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nsubslist\n</dt>\n<dd>\n<p>\n  Processed as a comma-separated list of substitution names for\n  <em>passtext</em> substitution, overrides the the macro definition\n  <em>subslist</em>.\n</p>\n</dd>\n</dl></div>\n<div class=\"ulist\"><div class=\"title\">Here’s what happens during macro substitution</div><ul>\n<li>\n<p>\nEach contextually relevant macro <em>pattern</em> from the <code>[macros]</code>\n  section is matched against the input source line.\n</p>\n</li>\n<li>\n<p>\nIf a match is found the text to be substituted is loaded from a\n  configuration markup template section named like\n  <code>&lt;name&gt;-inlinemacro</code> or <code>&lt;name&gt;-blockmacro</code> (depending on the macro\n  type).\n</p>\n</li>\n<li>\n<p>\nGlobal and macro attribute list attributes are substituted in the\n  macro’s markup template.\n</p>\n</li>\n<li>\n<p>\nThe substituted template replaces the macro reference in the output\n  document.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X98\">22. HTML 5 audio and video block macros</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <em>html5</em> backend <em>audio</em> and <em>video</em> block macros generate the HTML\n5 <em>audio</em> and <em>video</em> elements respectively.  They follow the usual\nAsciiDoc block macro syntax <code>&lt;name&gt;::&lt;target&gt;[&lt;attrlist&gt;]</code> where:</p></div>\n<div class=\"hdlist\"><table>\n<tbody><tr>\n<td class=\"hdlist1\">\n<code>&lt;name&gt;</code>\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\n<em>audio</em> or <em>video</em>.\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\n<code>&lt;target&gt;</code>\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nThe URL or file name of the video or audio file.\n</p>\n</td>\n</tr>\n<tr>\n<td class=\"hdlist1\">\n<code>&lt;attrlist&gt;</code>\n<br>\n</td>\n<td class=\"hdlist2\">\n<p style=\"margin-top: 0;\">\nA list of named attributes (see below).\n</p>\n</td>\n</tr>\n</tbody></table></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 4. Audio macro attributes</caption>\n<colgroup><col width=\"16%\">\n<col width=\"83%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Name </th>\n<th valign=\"top\" align=\"left\"> Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">options</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A comma separated list of one or more of the following items:\n<em>autoplay</em>, <em>loop</em> which correspond to the same-named HTML 5 <em>audio</em>\nelement boolean attributes.  By default the player <em>controls</em> are\nenabled, include the <em>nocontrols</em> option value to hide them.</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 5. Video macro attributes</caption>\n<colgroup><col width=\"16%\">\n<col width=\"83%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Name   </th>\n<th valign=\"top\" align=\"left\"> Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">height</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The height of the player in pixels.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">width</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The width of the player in pixels.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">poster</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The URL or file name of an image representing the video.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">options</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A comma separated list of one or more of the following items:\n<em>autoplay</em>, <em>loop</em> and <em>nocontrols</em>. The <em>autoplay</em> and <em>loop</em> options\ncorrespond to the same-named HTML 5 <em>video</em> element boolean\nattributes.  By default the player <em>controls</em> are enabled, include the\n<em>nocontrols</em> option value to hide them.</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"paragraph\"><p>Examples:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>audio::images/example.ogg[]\n\nvideo::gizmo.ogv[width=200,options=\"nocontrols,autoplay\"]\n\n.Example video\nvideo::gizmo.ogv[]\n\nvideo::http://www.808.dk/pics/video/gizmo.ogv[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If your needs are more complex put raw HTML 5 in a markup block, for\nexample (from <a href=\"http://www.808.dk/?code-html-5-video\">http://www.808.dk/?code-html-5-video</a>):</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>++++\n&lt;video poster=\"pics/video/gizmo.jpg\" id=\"video\" style=\"cursor: pointer;\" &gt;\n  &lt;source src=\"pics/video/gizmo.mp4\" /&gt;\n  &lt;source src=\"pics/video/gizmo.webm\" type=\"video/webm\" /&gt;\n  &lt;source src=\"pics/video/gizmo.ogv\" type=\"video/ogg\" /&gt;\n  Video not playing? &lt;a href=\"pics/video/gizmo.mp4\"&gt;Download file&lt;/a&gt; instead.\n&lt;/video&gt;\n\n&lt;script type=\"text/javascript\"&gt;\n  var video = document.getElementById('video');\n  video.addEventListener('click',function(){\n    video.play();\n  },false);\n&lt;/script&gt;\n++++</code></pre>\n</div></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_tables\">23. Tables</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The AsciiDoc table syntax looks and behaves like other delimited block\ntypes and supports standard <a href=\"#X73\">block configuration entries</a>.\nFormatting is easy to read and, just as importantly, easy to enter.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nCells and columns can be formatted using built-in customizable styles.\n</p>\n</li>\n<li>\n<p>\nHorizontal and vertical cell alignment can be set on columns and\n  cell.\n</p>\n</li>\n<li>\n<p>\nHorizontal and vertical cell spanning is supported.\n</p>\n</li>\n</ul></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">Use tables sparingly</div>\n<div class=\"paragraph\"><p>When technical users first start creating documents, tables (complete\nwith column spanning and table nesting) are often considered very\nimportant. The reality is that tables are seldom used, even in\ntechnical documentation.</p></div>\n<div class=\"paragraph\"><p>Try this exercise: thumb through your library of technical books,\nyou’ll be surprised just how seldom tables are actually used, even\nless seldom are tables containing block elements (such as paragraphs\nor lists) or spanned cells. This is no accident, like figures, tables\nare outside the normal document flow — tables are for consulting not\nfor reading.</p></div>\n<div class=\"paragraph\"><p>Tables are designed for, and should normally only be used for,\ndisplaying column oriented tabular data.</p></div>\n</div></div>\n<div class=\"sect2\">\n<h3 id=\"_example_tables\">23.1. Example tables</h3>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"15%\">\n<caption class=\"title\">Table 6. Simple table</caption>\n<colgroup><col width=\"33%\">\n<col width=\"33%\">\n<col width=\"33%\">\n</colgroup><tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">1</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">2</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">3</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">4</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">B</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">5</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">6</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">C</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code>[width=\"15%\"]\n|=======\n|1 |2 |A\n|3 |4 |B\n|5 |6 |C\n|=======</code></pre>\n</div></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"50%\">\n<caption class=\"title\">Table 7. Columns formatted with strong, monospaced and emphasis styles</caption>\n<colgroup><col width=\"33%\">\n<col width=\"33%\">\n<col width=\"33%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"right\">      </th>\n<th colspan=\"2\" valign=\"top\" align=\"center\">Columns 2 and 3</th>\n</tr>\n</thead>\n<tfoot>\n<tr>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>footer 1</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><code>footer 2</code></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>footer 3</em></p></td>\n</tr>\n</tfoot>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>1</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><code>Item 1</code></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>Item 1</em></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>2</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><code>Item 2</code></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>Item 2</em></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>3</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><code>Item 3</code></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>Item 3</em></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>4</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><code>Item 4</code></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>Item 4</em></p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code>.An example table\n[width=\"50%\",cols=\"&gt;s,^m,e\",frame=\"topbot\",options=\"header,footer\"]\n|==========================\n|      2+|Columns 2 and 3\n|1       |Item 1  |Item 1\n|2       |Item 2  |Item 2\n|3       |Item 3  |Item 3\n|4       |Item 4  |Item 4\n|footer 1|footer 2|footer 3\n|==========================</code></pre>\n</div></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"80%\">\n<caption class=\"title\">Table 8. Horizontal and vertical source data</caption>\n<colgroup><col width=\"17%\">\n<col width=\"11%\">\n<col width=\"11%\">\n<col width=\"58%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Date </th>\n<th valign=\"top\" align=\"center\">Duration </th>\n<th valign=\"top\" align=\"center\">Avg HR </th>\n<th valign=\"top\" align=\"left\">Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">22-Aug-08</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">10:24</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">157</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Worked out MSHR (max sustainable heart rate) by going hard\nfor this interval.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">22-Aug-08</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">23:03</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">152</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Back-to-back with previous interval.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\">24-Aug-08</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">40:00</p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">145</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Moderately hard interspersed with 3x 3min intervals (2min\nhard + 1min really hard taking the HR up to 160).</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"paragraph\"><p>Short cells can be entered horizontally, longer cells vertically.  The\ndefault behavior is to strip leading and trailing blank lines within a\ncell. These characteristics aid readability and data entry.</p></div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code>.Windtrainer workouts\n[width=\"80%\",cols=\"3,^2,^2,10\",options=\"header\"]\n|=========================================================\n|Date |Duration |Avg HR |Notes\n\n|22-Aug-08 |10:24 | 157 |\nWorked out MSHR (max sustainable heart rate) by going hard\nfor this interval.\n\n|22-Aug-08 |23:03 | 152 |\nBack-to-back with previous interval.\n\n|24-Aug-08 |40:00 | 145 |\nModerately hard interspersed with 3x 3min intervals (2min\nhard + 1min really hard taking the HR up to 160).\n\n|=========================================================</code></pre>\n</div></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 9. A table with externally sourced CSV data</caption>\n<colgroup><col width=\"11%\">\n<col width=\"22%\">\n<col width=\"22%\">\n<col width=\"22%\">\n<col width=\"22%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"center\">ID</th>\n<th valign=\"top\" align=\"left\">Customer Name</th>\n<th valign=\"top\" align=\"left\">Contact Name</th>\n<th valign=\"top\" align=\"left\">Customer Address</th>\n<th valign=\"top\" align=\"left\">Phone</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">AROUT</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Around the Horn</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Thomas Hardy</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">120 Hanover Sq.\nLondon</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">(171) 555-7788</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BERGS</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Berglunds snabbkop</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Christina Berglund</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Berguvsvagen  8\nLulea</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">0921-12 34 65</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BLAUS</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Blauer See Delikatessen</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Hanna Moos</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Forsterstr. 57\nMannheim</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">0621-08460</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BLONP</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Blondel pere et fils</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Frederique Citeaux</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">24, place Kleber\nStrasbourg</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">88.60.15.31</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BOLID</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Bolido Comidas preparadas</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Martin Sommer</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">C/ Araquil, 67\nMadrid</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">(91) 555 22 82</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BONAP</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Bon app'</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Laurence Lebihan</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">12, rue des Bouchers\nMarseille</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">91.24.45.40</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BOTTM</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Bottom-Dollar Markets</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Elizabeth Lincoln</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">23 Tsawassen Blvd.\nTsawassen</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">(604) 555-4729</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">BSBEV</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">B’s Beverages</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Victoria Ashworth</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Fauntleroy Circus\nLondon</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">(171) 555-1212</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\">CACTU</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Cactus Comidas para llevar</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Patricio Simpson</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Cerrito 333\nBuenos Aires</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">(1) 135-5555</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code>[format=\"csv\",cols=\"^1,4*2\",options=\"header\"]\n|===================================================\nID,Customer Name,Contact Name,Customer Address,Phone\ninclude::customers.csv[]\n|===================================================</code></pre>\n</div></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"border\" cellpadding=\"4\" cellspacing=\"0\" width=\"25%\">\n<caption class=\"title\">Table 10. Cell spans, alignments and styles</caption>\n<colgroup><col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n<col width=\"25%\">\n</colgroup><tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>1</em></p></td>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>2</strong></p></td>\n<td valign=\"top\" align=\"center\"><p class=\"table\">3</p></td>\n<td valign=\"top\" align=\"right\"><p class=\"table\"><strong>4</strong></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><em>5</em></p></td>\n<td colspan=\"2\" rowspan=\"2\" valign=\"middle\" align=\"center\"><p class=\"table\"><code>6</code></p></td>\n<td rowspan=\"3\" valign=\"bottom\" align=\"left\"><p class=\"table\"><code>7</code></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"center\"><p class=\"table\"><em>8</em></p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>9</em></p></td>\n<td colspan=\"2\" valign=\"top\" align=\"right\"><p class=\"table\"><code>10</code></p></td>\n</tr>\n</tbody>\n</table>\n</div>\n<div class=\"listingblock\">\n<div class=\"title\">AsciiDoc source</div>\n<div class=\"content\">\n<pre><code>[cols=\"e,m,^,&gt;s\",width=\"25%\"]\n|============================\n|1 &gt;s|2 |3 |4\n^|5 2.2+^.^|6 .3+&lt;.&gt;m|7\n^|8\n|9 2+&gt;|10\n|============================</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X68\">23.2. Table input data formats</h3>\n<div class=\"paragraph\"><p>AsciiDoc table data can be <em>psv</em>, <em>dsv</em> or <em>csv</em> formatted.  The\ndefault table format is <em>psv</em>.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc <em>psv</em> (<em>Prefix Separated Values</em>) and <em>dsv</em> (<em>Delimiter\nSeparated Values</em>) formats are cell oriented — the table is treated\nas a sequence of cells — there are no explicit row separators.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>psv</em> prefixes each cell with a separator whereas <em>dsv</em> delimits\n  cells with a separator.\n</p>\n</li>\n<li>\n<p>\n<em>psv</em> and <em>dsv</em> separators are Python regular expressions.\n</p>\n</li>\n<li>\n<p>\nThe default <em>psv</em> separator contains <a href=\"#X84\">cell specifier</a> related\n  named regular expression groups.\n</p>\n</li>\n<li>\n<p>\nThe default <em>dsv</em> separator is <code>:|\\n</code> (a colon or a new line\n  character).\n</p>\n</li>\n<li>\n<p>\n<em>psv</em> and <em>dsv</em> cell separators can be escaped by preceding them\n  with a backslash character.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Here are four <em>psv</em> cells (the second item spans two columns; the\nlast contains an escaped separator):</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>|One 2+|Two and three |A \\| separator character</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><em>csv</em>  is the quasi-standard row oriented <em>Comma Separated Values\n(CSV)</em> format commonly used to import and export spreadsheet and\ndatabase data.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X69\">23.3. Table attributes</h3>\n<div class=\"paragraph\"><p>Tables can be customized by the following attributes:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nformat\n</dt>\n<dd>\n<p>\n<em>psv</em> (default), <em>dsv</em> or <em>csv</em> (See <a href=\"#X68\">Table Data Formats</a>).\n</p>\n</dd>\n<dt class=\"hdlist1\">\nseparator\n</dt>\n<dd>\n<p>\nThe cell separator. A Python regular expression (<em>psv</em> and <em>dsv</em>\nformats) or a single character (<em>csv</em> format).\n</p>\n</dd>\n<dt class=\"hdlist1\">\nframe\n</dt>\n<dd>\n<p>\nDefines the table border and can take the following values: <em>topbot</em>\n(top and bottom), <em>all</em> (all sides), <em>none</em> and <em>sides</em> (left and\nright sides). The default value is <em>all</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ngrid\n</dt>\n<dd>\n<p>\nDefines which ruler lines are drawn between table rows and columns.\nThe <em>grid</em> attribute value can be any of the following values: <em>none</em>,\n<em>cols</em>, <em>rows</em> and <em>all</em>. The default value is <em>all</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nalign\n</dt>\n<dd>\n<p>\nUse the <em>align</em> attribute to horizontally align the table on the\npage (works with HTML outputs only, has no effect on DocBook outputs).\nThe following values are valid: <em>left</em>, <em>right</em>, and <em>center</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nfloat\n</dt>\n<dd>\n<p>\nUse the <em>float</em> attribute to float the table <em>left</em> or <em>right</em> on the\npage (works with HTML outputs only, has no effect on DocBook outputs).\nFloating only makes sense in conjunction with a table <em>width</em>\nattribute value of less than 100% (otherwise the table will take up\nall the available space).  <em>float</em> and <em>align</em> attributes are mutually\nexclusive.  Use the <code>unfloat::[]</code> block macro to stop floating.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nhalign\n</dt>\n<dd>\n<p>\nUse the <em>halign</em> attribute to horizontally align all cells in a table.\nThe following values are valid: <em>left</em>, <em>right</em>, and <em>center</em>\n(defaults to <em>left</em>). Overridden by <a href=\"#X70\">Column specifiers</a>  and\n<a href=\"#X84\">Cell specifiers</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nvalign\n</dt>\n<dd>\n<p>\nUse the <em>valign</em> attribute to vertically align all cells in a table.\nThe following values are valid: <em>top</em>, <em>bottom</em>, and <em>middle</em>\n(defaults to <em>top</em>). Overridden by <a href=\"#X70\">Column specifiers</a>  and\n<a href=\"#X84\">Cell specifiers</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\noptions\n</dt>\n<dd>\n<p>\nThe <em>options</em> attribute can contain comma separated values, for\nexample: <em>header</em>, <em>footer</em>. By default header and footer rows are\nomitted.  See <a href=\"#X74\">attribute options</a> for a complete list of\navailable table options.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncols\n</dt>\n<dd>\n<p>\nThe <em>cols</em> attribute is a comma separated list of <a href=\"#X70\">column specifiers</a>. For example <code>cols=\"2&lt;p,2*,4p,&gt;\"</code>.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf <em>cols</em> is present it must specify all columns.\n</p>\n</li>\n<li>\n<p>\nIf the <em>cols</em> attribute is not specified the number of columns is\n  calculated as the number of data items in the <strong>first line</strong> of the\n  table.\n</p>\n</li>\n<li>\n<p>\nThe degenerate form for the <em>cols</em> attribute is an integer\n  specifying the number of columns e.g. <code>cols=4</code>.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\nwidth\n</dt>\n<dd>\n<p>\nThe <em>width</em> attribute is expressed as a percentage value\n(<em>\"1%\"</em>…<em>\"99%\"</em>). The width specifies the table width relative to\nthe available width. HTML backends use this value to set the table\nwidth attribute. It’s a bit more complicated with DocBook, see the\n<a href=\"#X89\">DocBook table widths</a> sidebar.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nfilter\n</dt>\n<dd>\n<p>\nThe <em>filter</em> attribute defines an external shell command that is\ninvoked for each cell. The built-in <em>asciidoc</em> table style is\nimplemented using a filter.\n</p>\n</dd>\n</dl></div>\n<div class=\"sidebarblock\" id=\"X89\">\n<div class=\"content\">\n<div class=\"title\">DocBook table widths</div>\n<div class=\"paragraph\"><p>The AsciiDoc docbook backend generates CALS tables. CALS tables do not\nsupport a table width attribute — table width can only be controlled\nby specifying absolute column widths.</p></div>\n<div class=\"paragraph\"><p>Specifying absolute column widths is not media independent because\ndifferent presentation media have different physical dimensions. To\nget round this limitation both\n<a href=\"http://www.sagehill.net/docbookxsl/Tables.html#TableWidth\">DocBook XSL\nStylesheets</a> and\n<a href=\"http://dblatex.sourceforge.net/doc/manual/ch03s05.html#sec-table-width\">dblatex</a>\nhave implemented table width processing instructions for setting the\ntable width as a percentage of the available width. AsciiDoc emits\nthese processing instructions if the <em>width</em> attribute is set along\nwith proportional column widths (the AsciiDoc docbook backend\n<em>pageunits</em> attribute defaults to <em>*</em>).</p></div>\n<div class=\"paragraph\"><p>To generate DocBook tables with absolute column widths set the\n<em>pageunits</em> attribute to a CALS absolute unit such as <em>pt</em> and set the\n<em>pagewidth</em> attribute to match the width of the presentation media.</p></div>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X70\">23.4. Column Specifiers</h3>\n<div class=\"paragraph\"><p>Column specifiers define how columns are rendered and appear in the\ntable <a href=\"#X69\">cols attribute</a>.  A column specifier consists of an\noptional column multiplier followed by optional alignment, width and\nstyle values and is formatted like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[&lt;multiplier&gt;*][&lt;align&gt;][&lt;width&gt;][&lt;style&gt;]</code></pre>\n</div></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAll components are optional. The multiplier must be first and the\n  style last. The order of <code>&lt;align&gt;</code> or <code>&lt;width&gt;</code> is not important.\n</p>\n</li>\n<li>\n<p>\nColumn <code>&lt;width&gt;</code> can be either an integer proportional value (1…)\n  or a percentage (1%…100%). The default value is 1. To ensure\n  portability across different backends, there is no provision for\n  absolute column widths (not to be confused with output column width\n  <a href=\"#X72\">markup attributes</a> which are available in both percentage and\n  absolute units).\n</p>\n</li>\n<li>\n<p>\nThe <em>&lt;align&gt;</em> column alignment specifier is formatted like:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[&lt;horizontal&gt;][.&lt;vertical&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Where <code>&lt;horizontal&gt;</code> and <code>&lt;vertical&gt;</code> are one of the following\ncharacters: <code>&lt;</code>, <code>^</code> or <code>&gt;</code> which represent <em>left</em>, <em>center</em> and\n<em>right</em> horizontal alignment or <em>top</em>, <em>middle</em> and <em>bottom</em> vertical\nalignment respectively.</p></div>\n</li>\n<li>\n<p>\nA <code>&lt;multiplier&gt;</code> can be used to specify repeated columns e.g.\n  <code>cols=\"4*&lt;\"</code> specifies four left-justified columns. The default\n  multiplier value is 1.\n</p>\n</li>\n<li>\n<p>\nThe <code>&lt;style&gt;</code> name specifies a <a href=\"#X71\">table style</a> to used to markup\n  column cells (you can use the full style names if you wish but the\n  first letter is normally sufficient).\n</p>\n</li>\n<li>\n<p>\nColumn specific styles are not applied to header rows.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X84\">23.5. Cell Specifiers</h3>\n<div class=\"paragraph\"><p>Cell specifiers allow individual cells in <em>psv</em> formatted tables to be\nspanned, multiplied, aligned and styled.  Cell specifiers prefix <em>psv</em>\n<code>|</code> delimiters and are formatted like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[&lt;span&gt;*|+][&lt;align&gt;][&lt;style&gt;]</code></pre>\n</div></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>&lt;span&gt;</em> specifies horizontal and vertical cell spans (<em>+</em> operator) or\n  the number of times the cell is replicated (<em>*</em> operator). <em>&lt;span&gt;</em>\n  is formatted like:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[&lt;colspan&gt;][.&lt;rowspan&gt;]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Where <code>&lt;colspan&gt;</code> and <code>&lt;rowspan&gt;</code> are integers specifying the number of\ncolumns and rows to span.</p></div>\n</li>\n<li>\n<p>\n<code>&lt;align&gt;</code> specifies horizontal and vertical cell alignment an is the\n  same as in <a href=\"#X70\">column specifiers</a>.\n</p>\n</li>\n<li>\n<p>\nA <code>&lt;style&gt;</code> value is the first letter of <a href=\"#X71\">table style</a> name.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>For example, the following <em>psv</em> formatted cell will span two columns\nand the text will be centered and emphasized:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>`2+^e| Cell text`</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X71\">23.6. Table styles</h3>\n<div class=\"paragraph\"><p>Table styles can be applied to the entire table (by setting the\n<em>style</em> attribute in the table’s attribute list) or on a per column\nbasis (by specifying the style in the table’s <a href=\"#X69\">cols attribute</a>).\nTable data can be formatted using the following predefined styles:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndefault\n</dt>\n<dd>\n<p>\nThe default style: AsciiDoc inline text formatting; blank lines are\ntreated as paragraph breaks.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nemphasis\n</dt>\n<dd>\n<p>\nLike default but all text is emphasised.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nmonospaced\n</dt>\n<dd>\n<p>\nLike default but all text is in a monospaced font.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nstrong\n</dt>\n<dd>\n<p>\nLike default but all text is bold.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nheader\n</dt>\n<dd>\n<p>\nApply the same style as the table header. Normally used to create a\nvertical header in the first column.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nasciidoc\n</dt>\n<dd>\n<p>\nWith this style table cells can contain any of the AsciiDoc elements\nthat are allowed inside document sections. This style runs <code>asciidoc(1)</code>\nas a filter to process cell contents. See also <a href=\"#X83\">Docbook table limitations</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nliteral\n</dt>\n<dd>\n<p>\nNo text formatting; monospaced font; all line breaks are retained\n(the same as the AsciiDoc <a href=\"#X65\">LiteralBlock</a> element).\n</p>\n</dd>\n<dt class=\"hdlist1\">\nverse\n</dt>\n<dd>\n<p>\nAll line breaks are retained (just like the AsciiDoc <a href=\"#X94\">verse paragraph style</a>).\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X72\">23.7. Markup attributes</h3>\n<div class=\"paragraph\"><p>AsciiDoc makes a number of attributes available to table markup\ntemplates and tags. Column specific attributes are available when\nsubstituting the <em>colspec</em> cell data tags.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\npageunits\n</dt>\n<dd>\n<p>\nDocBook backend only. Specifies table column absolute width units.\nDefaults to <em>*</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\npagewidth\n</dt>\n<dd>\n<p>\nDocBook backend only. The nominal output page width in <em>pageunit</em>\nunits. Used to calculate CALS tables absolute column and table\nwidths. Defaults to <em>425</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntableabswidth\n</dt>\n<dd>\n<p>\nInteger value calculated from <em>width</em> and <em>pagewidth</em> attributes.\nIn <em>pageunit</em> units.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntablepcwidth\n</dt>\n<dd>\n<p>\nTable width expressed as a percentage of the available width. Integer\nvalue (0..100).\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolabswidth\n</dt>\n<dd>\n<p>\nInteger value calculated from <em>cols</em> column width, <em>width</em> and\n<em>pagewidth</em> attributes.  In <em>pageunit</em> units.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolpcwidth\n</dt>\n<dd>\n<p>\nColumn width expressed as a percentage of the table width. Integer\nvalue (0..100).\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolcount\n</dt>\n<dd>\n<p>\nTotal number of table columns.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nrowcount\n</dt>\n<dd>\n<p>\nTotal number of table rows.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nhalign\n</dt>\n<dd>\n<p>\nHorizontal cell content alignment: <em>left</em>, <em>right</em> or <em>center</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nvalign\n</dt>\n<dd>\n<p>\nVertical cell content alignment: <em>top</em>, <em>bottom</em> or <em>middle</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolnumber, colstart\n</dt>\n<dd>\n<p>\nThe number of the leftmost column occupied by the cell (1…).\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolend\n</dt>\n<dd>\n<p>\nThe number of the rightmost column occupied by the cell (1…).\n</p>\n</dd>\n<dt class=\"hdlist1\">\ncolspan\n</dt>\n<dd>\n<p>\nNumber of columns the cell should span.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nrowspan\n</dt>\n<dd>\n<p>\nNumber of rows the cell should span (1…).\n</p>\n</dd>\n<dt class=\"hdlist1\">\nmorerows\n</dt>\n<dd>\n<p>\nNumber of additional rows the cell should span (0…).\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_nested_tables\">23.8. Nested tables</h3>\n<div class=\"paragraph\"><p>An alternative <em>psv</em> separator character <em>!</em> can be used (instead of\n<em>|</em>) in nested tables. This allows a single level of table nesting.\nColumns containing nested tables must use the <em>asciidoc</em> style. An\nexample can be found in <code>./examples/website/newtables.txt</code>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X83\">23.9. DocBook table limitations</h3>\n<div class=\"paragraph\"><p>Fully implementing tables is not trivial, some DocBook toolchains do\nbetter than others.  AsciiDoc HTML table outputs are rendered\ncorrectly in all the popular browsers — if your DocBook generated\ntables don’t look right compare them with the output generated by the\nAsciiDoc <em>xhtml11</em> backend or try a different DocBook toolchain.  Here\nis a list of things to be aware of:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAlthough nested tables are not legal in DocBook 4 the FOP and\n  dblatex toolchains will process them correctly.  If you use <code>a2x(1)</code>\n  you will need to include the <code>--no-xmllint</code> option to suppress\n  DocBook validation errors.\n</p>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">In theory you can nest DocBook 4 tables one level using the\n<em>entrytbl</em> element, but not all toolchains process <em>entrytbl</em>.</td>\n</tr></tbody></table>\n</div>\n</li>\n<li>\n<p>\nDocBook only allows a subset of block elements inside table cells so\n  not all AsciiDoc elements produce valid DocBook inside table cells.\n  If you get validation errors running <code>a2x(1)</code> try the <code>--no-xmllint</code>\n  option, toolchains will often process nested block elements such as\n  sidebar blocks and floating titles correctly even though, strictly\n  speaking, they are not legal.\n</p>\n</li>\n<li>\n<p>\nText formatting in cells using the <em>monospaced</em> table style will\n  raise validation errors because the DocBook <em>literal</em> element was\n  not designed to support formatted text (using the <em>literal</em> element\n  is a kludge on the part of AsciiDoc as there is no easy way to set\n  the font style in DocBook.\n</p>\n</li>\n<li>\n<p>\nCell alignments are ignored for <em>verse</em>, <em>literal</em> or <em>asciidoc</em>\n  table styles.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X1\">24. Manpage Documents</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Sooner or later, if you program in a UNIX environment, you’re going\nto have to write a man page.</p></div>\n<div class=\"paragraph\"><p>By observing a couple of additional conventions (detailed below) you\ncan write AsciiDoc files that will generate HTML and PDF man pages\nplus the native manpage roff format.  The easiest way to generate roff\nmanpages from AsciiDoc source is to use the <code>a2x(1)</code> command. The\nfollowing example generates a roff formatted manpage file called\n<code>asciidoc.1</code> (<code>a2x(1)</code> uses <code>asciidoc(1)</code> to convert <code>asciidoc.1.txt</code> to\nDocBook which it then converts to roff using DocBook XSL Stylesheets):</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>a2x --doctype manpage --format manpage asciidoc.1.txt</code></pre>\n</div></div>\n<div class=\"sidebarblock\">\n<div class=\"content\">\n<div class=\"title\">Viewing and printing manpage files</div>\n<div class=\"paragraph\"><p>Use the <code>man(1)</code> command to view the manpage file:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ man -l asciidoc.1</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>To print a high quality man page to a postscript printer:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ man -l -Tps asciidoc.1 | lpr</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You could also create a PDF version of the man page by converting\nPostScript to PDF using <code>ps2pdf(1)</code>:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ man -l -Tps asciidoc.1 | ps2pdf - asciidoc.1.pdf</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>ps2pdf(1)</code> command is included in the Ghostscript distribution.</p></div>\n</div></div>\n<div class=\"paragraph\"><p>To find out more about man pages view the <code>man(7)</code> manpage\n(<code>man 7 man</code> and <code>man man-pages</code> commands).</p></div>\n<div class=\"sect2\">\n<h3 id=\"_document_header\">24.1. Document Header</h3>\n<div class=\"paragraph\"><p>A manpage document Header is mandatory. The title line contains the\nman page name followed immediately by the manual section number in\nbrackets, for example <em>ASCIIDOC(1)</em>. The title name should not contain\nwhite space and the manual section number is a single digit optionally\nfollowed by a single character.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_the_name_section\">24.2. The NAME Section</h3>\n<div class=\"paragraph\"><p>The first manpage section is mandatory, must be titled <em>NAME</em> and must\ncontain a single paragraph (usually a single line) consisting of a\nlist of one or more comma separated command name(s) separated from the\ncommand purpose by a dash character. The dash must have at least one\nwhite space character on either side. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>printf, fprintf, sprintf - print formatted output</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_the_synopsis_section\">24.3. The SYNOPSIS Section</h3>\n<div class=\"paragraph\"><p>The second manpage section is mandatory and must be titled <em>SYNOPSIS</em>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_refmiscinfo_attributes\">24.4. refmiscinfo attributes</h3>\n<div class=\"paragraph\"><p>In addition to the automatically created man page <a href=\"#X60\">intrinsic attributes</a> you can assign DocBook\n<a href=\"http://www.docbook.org/tdg5/en/html/refmiscinfo.html\">refmiscinfo</a>\nelement <em>source</em>, <em>version</em> and <em>manual</em> values using AsciiDoc\n<code>{mansource}</code>, <code>{manversion}</code> and <code>{manmanual}</code> attributes\nrespectively. This example is from the AsciiDoc header of a man page\nsource file:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:man source:   AsciiDoc\n:man version:  {revnumber}\n:man manual:   AsciiDoc Manual</code></pre>\n</div></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X78\">25. Mathematical Formulas</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <em>asciimath</em> and <em>latexmath</em> <a href=\"#X77\">passthrough macros</a> along with\n<em>asciimath</em> and <em>latexmath</em>  <a href=\"#X76\">passthrough blocks</a> provide a\n(backend dependent) mechanism for rendering mathematical formulas. You\ncan use the following math markups:</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">The <em>latexmath</em> macro used to include <em>LaTeX Math</em> in DocBook\noutputs is not the same as the <em>latexmath</em> macro used to include\n<em>LaTeX MathML</em> in XHTML outputs.  <em>LaTeX Math</em> applies to DocBook\noutputs that are processed by <a href=\"#X31\">dblatex</a> and is normally used to\ngenerate PDF files.  <em>LaTeXMathML</em> is very much a subset of <em>LaTeX\nMath</em> and applies to XHTML documents.</td>\n</tr></tbody></table>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_latex_math\">25.1. LaTeX Math</h3>\n<div class=\"paragraph\"><p><a href=\"ftp://ftp.ams.org/pub/tex/doc/amsmath/short-math-guide.pdf\">LaTeX\nmath</a> can be included in documents that are processed by\n<a href=\"#X31\">dblatex(1)</a>.  Example inline formula:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>latexmath:[$C = \\alpha + \\beta Y^{\\gamma} + \\epsilon$]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>For more examples see the <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc website</a> or the\ndistributed <code>doc/latexmath.txt</code> file.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_asciimathml\">25.2. ASCIIMathML</h3>\n<div class=\"paragraph\"><p><a href=\"http://www1.chapman.edu/%7Ejipsen/mathml/asciimath.html\">ASCIIMathML</a>\nformulas can be included in XHTML documents generated using the\n<em>xhtml11</em> and <em>html5</em> backends. To enable ASCIIMathML support you must\ndefine the <em>asciimath</em> attribute, for example using the <code>-a asciimath</code>\ncommand-line option.  Example inline formula:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>asciimath:[`x/x={(1,if x!=0),(text{undefined},if x=0):}`]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>For more examples see the <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc website</a> or the\ndistributed <code>doc/asciimathml.txt</code> file.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_latexmathml\">25.3. LaTeXMathML</h3>\n<div class=\"paragraph\"><p><em>LaTeXMathML</em> allows LaTeX Math style formulas to be included in XHTML\ndocuments generated using the AsciiDoc <em>xhtml11</em> and <em>html5</em> backends.\nAsciiDoc uses the\n<a href=\"http://www.maths.nottingham.ac.uk/personal/drw/lm.html\">original\nLaTeXMathML</a> by Douglas Woodall.  <em>LaTeXMathML</em> is derived from\nASCIIMathML and is for users who are more familiar with or prefer\nusing LaTeX math formulas (it recognizes a subset of LaTeX Math, the\ndifferences are documented on the <em>LaTeXMathML</em> web page).  To enable\nLaTeXMathML support you must define the <em>latexmath</em> attribute, for\nexample using the <code>-a latexmath</code> command-line option.  Example inline\nformula:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>latexmath:[$\\sum_{n=1}^\\infty \\frac{1}{2^n}$]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>For more examples see the <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc website</a> or the\ndistributed <code>doc/latexmathml.txt</code> file.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_mathml\">25.4. MathML</h3>\n<div class=\"paragraph\"><p><a href=\"http://www.w3.org/Math/\">MathML</a> is a low level XML markup for\nmathematics. AsciiDoc has no macros for MathML but users familiar with\nthis markup could use passthrough macros and passthrough blocks to\ninclude MathML in output documents.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X7\">26. Configuration Files</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc source file syntax and output file markup is largely\ncontrolled by a set of cascading, text based, configuration files.  At\nruntime The AsciiDoc default configuration files are combined with\noptional user and document specific configuration files.</p></div>\n<div class=\"sect2\">\n<h3 id=\"_configuration_file_format\">26.1. Configuration File Format</h3>\n<div class=\"paragraph\"><p>Configuration files contain named sections. Each section begins with a\nsection name in square brackets []. The section body consists of the\nlines of text between adjacent section headings.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nSection names consist of one or more alphanumeric, underscore or\n  dash characters and cannot begin or end with a dash.\n</p>\n</li>\n<li>\n<p>\nLines starting with a <em>#</em> character are treated as comments and\n  ignored.\n</p>\n</li>\n<li>\n<p>\nIf the section name is prefixed with a <em>+</em> character then the\n  section contents is appended to the contents of an already existing\n  same-named section.\n</p>\n</li>\n<li>\n<p>\nOtherwise same-named sections and section entries override\n  previously loaded sections and section entries (this is sometimes\n  referred to as <em>cascading</em>).  Consequently, downstream configuration\n  files need only contain those sections and section entries that need\n  to be overridden.\n</p>\n</li>\n</ul></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">When creating custom configuration files you only need to include\nthe sections and entries that differ from the default configuration.</td>\n</tr></tbody></table>\n</div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">The best way to learn about configuration files is to read the\ndefault configuration files in the AsciiDoc distribution in\nconjunction with <code>asciidoc(1)</code> output files. You can view configuration\nfile load sequence by turning on the <code>asciidoc(1)</code> <code>-v</code> (<code>--verbose</code>)\ncommand-line option.</td>\n</tr></tbody></table>\n</div>\n<div class=\"paragraph\"><p>AsciiDoc reserves the following section names for specific purposes:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nmiscellaneous\n</dt>\n<dd>\n<p>\n        Configuration options that don’t belong anywhere else.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nattributes\n</dt>\n<dd>\n<p>\n        Attribute name/value entries.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nspecialcharacters\n</dt>\n<dd>\n<p>\n        Special characters reserved by the backend markup.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntags\n</dt>\n<dd>\n<p>\n        Backend markup tags.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nquotes\n</dt>\n<dd>\n<p>\n        Definitions for quoted inline character formatting.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nspecialwords\n</dt>\n<dd>\n<p>\n        Lists of words and phrases singled out for special markup.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nreplacements, replacements2, replacements3\n</dt>\n<dd>\n<p>\n        Find and replace substitution definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nspecialsections\n</dt>\n<dd>\n<p>\n        Used to single out special section names for specific markup.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nmacros\n</dt>\n<dd>\n<p>\n        Macro syntax definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntitles\n</dt>\n<dd>\n<p>\n        Heading, section and block title definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nparadef-*\n</dt>\n<dd>\n<p>\n        Paragraph element definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nblockdef-*\n</dt>\n<dd>\n<p>\n        DelimitedBlock element definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nlistdef-*\n</dt>\n<dd>\n<p>\n        List element definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nlisttags-*\n</dt>\n<dd>\n<p>\n        List element tag definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntabledef-*\n</dt>\n<dd>\n<p>\n        Table element definitions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntabletags-*\n</dt>\n<dd>\n<p>\n        Table element tag definitions.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>Each line of text in these sections is a <em>section entry</em>. Section\nentries share the following syntax:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nname=value\n</dt>\n<dd>\n<p>\n        The entry value is set to value.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nname=\n</dt>\n<dd>\n<p>\n        The entry value is set to a zero length string.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nname!\n</dt>\n<dd>\n<p>\n        The entry is undefined (deleted from the configuration). This\n        syntax only applies to <em>attributes</em> and <em>miscellaneous</em>\n        sections.\n</p>\n</dd>\n</dl></div>\n<div class=\"ulist\"><div class=\"title\">Section entry behavior</div><ul>\n<li>\n<p>\nAll equals characters inside the <code>name</code> must be escaped with a\n  backslash character.\n</p>\n</li>\n<li>\n<p>\n<code>name</code> and <code>value</code> are stripped of leading and trailing white space.\n</p>\n</li>\n<li>\n<p>\nAttribute names, tag entry names and markup template section names\n  consist of one or more alphanumeric, underscore or dash characters.\n  Names should not begin or end with a dash.\n</p>\n</li>\n<li>\n<p>\nA blank configuration file section (one without any entries) deletes\n  any preceding section with the same name (applies to non-markup\n  template sections).\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_miscellaneous_section\">26.2. Miscellaneous section</h3>\n<div class=\"paragraph\"><p>The optional <code>[miscellaneous]</code> section specifies the following\n<code>name=value</code> options:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nnewline\n</dt>\n<dd>\n<p>\n        Output file line termination characters. Can include any\n        valid Python string escape sequences. The default value is\n        <code>\\r\\n</code> (carriage return, line feed). Should not be quoted or\n        contain explicit spaces (use <code>\\x20</code> instead). For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a 'newline=\\n' -b docbook mydoc.txt</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\noutfilesuffix\n</dt>\n<dd>\n<p>\n        The default extension for the output file, for example\n        <code>outfilesuffix=.html</code>. Defaults to backend name.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntabsize\n</dt>\n<dd>\n<p>\n        The number of spaces to expand tab characters, for example\n        <code>tabsize=4</code>. Defaults to 8. A <em>tabsize</em> of zero suppresses tab\n        expansion (useful when piping included files through block\n        filters). Included files can override this option using the\n        <em>tabsize</em> attribute.\n</p>\n</dd>\n<dt class=\"hdlist1\">\npagewidth, pageunits\n</dt>\n<dd>\n<p>\n        These global table related options are documented in the\n        <a href=\"#X4\">Table Configuration File Definitions</a> sub-section.\n</p>\n</dd>\n</dl></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\"><code>[miscellaneous]</code> configuration file entries can be set using\nthe <code>asciidoc(1)</code> <code>-a</code> (<code>--attribute</code>) command-line option.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_titles_section\">26.3. Titles section</h3>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nsectiontitle\n</dt>\n<dd>\n<p>\n        Two line section title pattern. The entry value is a Python\n        regular expression containing the named group <em>title</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nunderlines\n</dt>\n<dd>\n<p>\n        A comma separated list of document and section title underline\n        character pairs starting with the section level 0 and ending\n        with section level 4 underline. The default setting is:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>underlines=\"==\",\"--\",\"~~\",\"^^\",\"++\"</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nsect0…sect4\n</dt>\n<dd>\n<p>\n        One line section title patterns. The entry value is a Python\n        regular expression containing the named group <em>title</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nblocktitle\n</dt>\n<dd>\n<p>\n        <a href=\"#X42\">BlockTitle element</a> pattern.  The entry value is a\n        Python regular expression containing the named group <em>title</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nsubs\n</dt>\n<dd>\n<p>\n        A comma separated list of substitutions that are performed on\n        the document header and section titles. Defaults to <em>normal</em>\n        substitution.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_tags_section\">26.4. Tags section</h3>\n<div class=\"paragraph\"><p>The <code>[tags]</code> section contains backend tag definitions (one per\nline). Tags are used to translate AsciiDoc elements to backend\nmarkup.</p></div>\n<div class=\"paragraph\"><p>An AsciiDoc tag definition is formatted like\n<code>&lt;tagname&gt;=&lt;starttag&gt;|&lt;endtag&gt;</code>. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>emphasis=&lt;em&gt;|&lt;/em&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>In this example <code>asciidoc(1)</code> replaces the | character with the\nemphasized text from the AsciiDoc input file and writes the result to\nthe output file.</p></div>\n<div class=\"paragraph\"><p>Use the <code>{brvbar}</code> attribute reference if you need to include a | pipe\ncharacter inside tag text.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_attributes_section\">26.5. Attributes section</h3>\n<div class=\"paragraph\"><p>The optional <code>[attributes]</code> section contains predefined attributes.</p></div>\n<div class=\"paragraph\"><p>If the attribute value requires leading or trailing spaces then the\ntext text should be enclosed in quotation mark (\") characters.</p></div>\n<div class=\"paragraph\"><p>To delete a attribute insert a <code>name!</code> entry in a downstream\nconfiguration file or use the <code>asciidoc(1)</code> <code>--attribute name!</code>\ncommand-line option (an attribute name suffixed with a <code>!</code> character\ndeletes the attribute)</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_special_characters_section\">26.6. Special Characters section</h3>\n<div class=\"paragraph\"><p>The <code>[specialcharacters]</code> section specifies how to escape characters\nreserved by the backend markup. Each translation is specified on a\nsingle line formatted like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;special_character&gt;=&lt;translated_characters&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Special characters are normally confined to those that resolve\nmarkup ambiguity (in the case of HTML and XML markups the ampersand,\nless than and greater than characters).  The following example causes\nall occurrences of the <code>&lt;</code> character to be replaced by <code>&amp;lt;</code>.</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;=&amp;lt;</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_quoted_text_section\">26.7. Quoted Text section</h3>\n<div class=\"paragraph\"><p>Quoting is used primarily for text formatting.  The <code>[quotes]</code> section\ndefines AsciiDoc quoting characters and their corresponding backend\nmarkup tags.  Each section entry value is the name of a of a <code>[tags]</code>\nsection entry. The entry name is the character (or characters) that\nquote the text.  The following examples are taken from AsciiDoc\nconfiguration files:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[quotes]\n_=emphasis</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[tags]\nemphasis=&lt;em&gt;|&lt;/em&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You can specify the left and right quote strings separately by\nseparating them with a | character, for example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>``|''=quoted</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Omitting the tag will disable quoting, for example, if you don’t want\nsuperscripts or subscripts put the following in a custom configuration\nfile or edit the global <code>asciidoc.conf</code> configuration file:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[quotes]\n^=\n~=</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><a href=\"#X52\">Unconstrained quotes</a> are differentiated from constrained\nquotes by prefixing the tag name with a hash character, for example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>__=#emphasis</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Quoted text behavior</div><ul>\n<li>\n<p>\nQuote characters must be non-alphanumeric.\n</p>\n</li>\n<li>\n<p>\nTo minimize quoting ambiguity try not to use the same quote\n  characters in different quote types.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_special_words_section\">26.8. Special Words section</h3>\n<div class=\"paragraph\"><p>The <code>[specialwords]</code> section is used to single out words and phrases\nthat you want to consistently format in some way throughout your\ndocument without having to repeatedly specify the markup. The name of\neach entry corresponds to a markup template section and the entry\nvalue consists of a list of words and phrases to be marked up. For\nexample:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[specialwords]\nstrongwords=NOTE IMPORTANT</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[strongwords]\n&lt;strong&gt;{words}&lt;/strong&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The examples specifies that any occurrence of <code>NOTE</code> or <code>IMPORTANT</code>\nshould appear in a bold font.</p></div>\n<div class=\"paragraph\"><p>Words and word phrases are treated as Python regular expressions: for\nexample, the word <code>^NOTE</code> would only match <code>NOTE</code> if appeared at\nthe start of a line.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc comes with three built-in Special Word types:\n<em>emphasizedwords</em>, <em>monospacedwords</em> and <em>strongwords</em>, each has a\ncorresponding (backend specific) markup template section. Edit the\nconfiguration files to customize existing Special Words and to add new\nones.</p></div>\n<div class=\"ulist\"><div class=\"title\">Special word behavior</div><ul>\n<li>\n<p>\nWord list entries must be separated by space characters.\n</p>\n</li>\n<li>\n<p>\nWord list entries with embedded spaces should be enclosed in quotation (\")\n  characters.\n</p>\n</li>\n<li>\n<p>\nA <code>[specialwords]</code> section entry of the form\n  <code>name=word1&nbsp;[word2…]</code> adds words to existing <code>name</code> entries.\n</p>\n</li>\n<li>\n<p>\nA <code>[specialwords]</code> section entry of the form <code>name</code> undefines\n  (deletes) all existing <code>name</code> words.\n</p>\n</li>\n<li>\n<p>\nSince word list entries are processed as Python regular expressions\n  you need to be careful to escape regular expression special\n  characters.\n</p>\n</li>\n<li>\n<p>\nBy default Special Words are substituted before Inline Macros, this\n  may lead to undesirable consequences. For example the special word\n  <code>foobar</code> would be expanded inside the macro call\n  <code>http://www.foobar.com[]</code>.  A possible solution is to emphasize\n  whole words only by defining the word using regular expression\n  characters, for example <code>\\bfoobar\\b</code>.\n</p>\n</li>\n<li>\n<p>\nIf the first matched character of a special word is a backslash then\n  the remaining characters are output without markup i.e. the\n  backslash can be used to escape special word markup.  For example\n  the special word <code>\\\\?\\b[Tt]en\\b</code> will mark up the words <code>Ten</code> and\n  <code>ten</code> only if they are not preceded by a backslash.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X10\">26.9. Replacements section</h3>\n<div class=\"paragraph\"><p><code>[replacements]</code>, <code>[replacements2]</code> and <code>[replacements3]</code>\nconfiguration file entries specify find and replace text and are\nformatted like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>&lt;find_pattern&gt;=&lt;replacement_text&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The find text can be a Python regular expression; the replace text can\ncontain Python regular expression group references.</p></div>\n<div class=\"paragraph\"><p>Use Replacement shortcuts for often used macro references, for\nexample (the second replacement allows us to backslash escape the\nmacro name):</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>NEW!=image:./images/smallnew.png[New!]\n\\\\NEW!=NEW!</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The only difference between the three replacement types is how they\nare applied. By default <em>replacements</em> and <em>replacement2</em> are applied\nin <a href=\"#X102\">normal</a> substitution contexts whereas <em>replacements3</em> needs\nto be configured explicitly and should only be used in backend\nconfiguration files.</p></div>\n<div class=\"ulist\"><div class=\"title\">Replacement behavior</div><ul>\n<li>\n<p>\nThe built-in replacements can be escaped with a backslash.\n</p>\n</li>\n<li>\n<p>\nIf the find or replace text has leading or trailing spaces then the\n  text should be enclosed in quotation (\") characters.\n</p>\n</li>\n<li>\n<p>\nSince the find text is processed as a regular expression you need to\n  be careful to escape regular expression special characters.\n</p>\n</li>\n<li>\n<p>\nReplacements are performed in the same order they appear in the\n  configuration file replacements section.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_markup_template_sections\">26.10. Markup Template Sections</h3>\n<div class=\"paragraph\"><p>Markup template sections supply backend markup for translating\nAsciiDoc elements.  Since the text is normally backend dependent\nyou’ll find these sections in the backend specific configuration\nfiles. Template sections differ from other sections in that they\ncontain a single block of text instead of per line <em>name=value</em>\nentries. A markup template section body can contain:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAttribute references\n</p>\n</li>\n<li>\n<p>\nSystem macro calls.\n</p>\n</li>\n<li>\n<p>\nA document content placeholder\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>The document content placeholder is a single | character and is\nreplaced by text from the source element.  Use the <code>{brvbar}</code>\nattribute reference if you need a literal | character in the template.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X27\">26.11. Configuration file names, precedence and locations</h3>\n<div class=\"paragraph\"><p>Configuration files have a <code>.conf</code> file name extension; they are\nloaded from the following locations:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nThe directory containing the asciidoc executable.\n</p>\n</li>\n<li>\n<p>\nIf there is no <code>asciidoc.conf</code> file in the directory containing the\n   asciidoc executable then load from the global configuration\n   directory (normally <code>/etc/asciidoc</code> or <code>/usr/local/etc/asciidoc</code>)\n   i.e. the global configuration files directory is skipped if\n   AsciiDoc configuration files are installed in the same directory as\n   the asciidoc executable. This allows both a system wide copy and\n   multiple local copies of AsciiDoc to coexist on the same host PC.\n</p>\n</li>\n<li>\n<p>\nThe user’s <code>$HOME/.asciidoc</code> directory (if it exists).\n</p>\n</li>\n<li>\n<p>\nThe directory containing the AsciiDoc source file.\n</p>\n</li>\n<li>\n<p>\nExplicit configuration files specified using:\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe <code>conf-files</code> attribute (one or more file names separated by a\n     <code>|</code> character). These files are loaded in the order they are\n     specified and prior to files specified using the <code>--conf-file</code>\n     command-line option.\n</p>\n</li>\n<li>\n<p>\nThe <code>asciidoc(1)</code> <code>--conf-file</code>) command-line option.  The\n     <code>--conf-file</code> option can be specified multiple times, in which\n     case configuration files will be processed in the same order they\n     appear on the command-line.\n</p>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\n<a href=\"#X100\">Backend plugin</a> configuration files are loaded from\n   subdirectories named like <code>backends/&lt;backend&gt;</code> in locations 1, 2\n   and 3.\n</p>\n</li>\n<li>\n<p>\n<a href=\"#X59\">Filter</a> configuration files are loaded from subdirectories\n   named like <code>filters/&lt;filter&gt;</code> in locations 1, 2 and 3.\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>Configuration files from the above locations are loaded in the\nfollowing order:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe <code>[attributes]</code> section only from:\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<code>asciidoc.conf</code> in location 3\n</p>\n</li>\n<li>\n<p>\nFiles from location 5.\n</p>\n<div class=\"paragraph\"><p>This first pass makes locally set attributes available in the global\n<code>asciidoc.conf</code> file.</p></div>\n</li>\n</ul></div>\n</li>\n<li>\n<p>\n<code>asciidoc.conf</code> from locations 1, 2, 3.\n</p>\n</li>\n<li>\n<p>\n<em>attributes</em>, <em>titles</em> and <em>specialcharacters</em> sections from the\n  <code>asciidoc.conf</code> in location 4.\n</p>\n</li>\n<li>\n<p>\nThe document header is parsed at this point and we can assume the\n  <em>backend</em> and <em>doctype</em> have now been defined.\n</p>\n</li>\n<li>\n<p>\nBackend plugin <code>&lt;backend&gt;.conf</code> and <code>&lt;backend&gt;-&lt;doctype&gt;.conf</code> files\n  from locations 6.  If a backend plugin is not found then try\n  locations 1, 2 and 3 for <code>&lt;backend&gt;.conf</code> and\n  <code>&lt;backend&gt;-&lt;doctype&gt;.conf</code> backend configuration files.\n</p>\n</li>\n<li>\n<p>\nFilter conf files from locations 7.\n</p>\n</li>\n<li>\n<p>\n<code>lang-&lt;lang&gt;.conf</code> from locations 1, 2, 3.\n</p>\n</li>\n<li>\n<p>\n<code>asciidoc.conf</code> from location 4.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;backend&gt;.conf</code> and <code>&lt;backend&gt;-&lt;doctype&gt;.conf</code> from location 4.\n</p>\n</li>\n<li>\n<p>\nFilter conf files from location 4.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;docfile&gt;.conf</code> and <code>&lt;docfile&gt;-&lt;backend&gt;.conf</code> from location 4.\n</p>\n</li>\n<li>\n<p>\nConfiguration files from location 5.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Where:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<code>&lt;backend&gt;</code> and <code>&lt;doctype&gt;</code> are values specified by the <code>asciidoc(1)</code>\n  <code>-b</code> (<code>--backend</code>) and <code>-d</code> (<code>--doctype</code>) command-line options.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;infile&gt;</code> is the path name of the AsciiDoc input file without the\n  file name extension.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;lang&gt;</code> is a two letter country code set by the the AsciiDoc <em>lang</em>\n  attribute.\n</p>\n</li>\n</ul></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"paragraph\"><p>The backend and language global configuration files are loaded <strong>after</strong>\nthe header has been parsed.  This means that you can set most\nattributes in the document header. Here’s an example header:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Life's Mysteries\n================\n:author: Hu Nose\n:doctype: book\n:toc:\n:icons:\n:data-uri:\n:lang: en\n:encoding: iso-8859-1</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Attributes set in the document header take precedence over\nconfiguration file attributes.</p></div>\n</td>\n</tr></tbody></table>\n</div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">Use the <code>asciidoc(1)</code> <code>-v</code> (<code>--verbose</code>) command-line option to see\nwhich configuration files are loaded and the order in which they are\nloaded.</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_document_attributes\">27. Document Attributes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>A document attribute is comprised of a <em>name</em> and a textual <em>value</em>\nand is used for textual substitution in AsciiDoc documents and\nconfiguration files. An attribute reference (an attribute name\nenclosed in braces) is replaced by the corresponding attribute\nvalue. Attribute names are case insensitive and can only contain\nalphanumeric, dash and underscore characters.</p></div>\n<div class=\"paragraph\"><p>There are four sources of document attributes (from highest to lowest\nprecedence):</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nCommand-line attributes.\n</p>\n</li>\n<li>\n<p>\nAttributeEntry, AttributeList, Macro and BlockId elements.\n</p>\n</li>\n<li>\n<p>\nConfiguration file <code>[attributes]</code> sections.\n</p>\n</li>\n<li>\n<p>\nIntrinsic attributes.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Within each of these divisions the last processed entry takes\nprecedence.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">If an attribute is not defined then the line containing the\nattribute reference is dropped. This property is used extensively in\nAsciiDoc configuration files to facilitate conditional markup\ngeneration.</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X18\">28. Attribute Entries</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <code>AttributeEntry</code> block element allows document attributes to be\nassigned within an AsciiDoc document. Attribute entries are added to\nthe global document attributes dictionary. The attribute name/value\nsyntax is a single line like:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:&lt;name&gt;: &lt;value&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:Author Initials: JB</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>This will set an attribute reference <code>{authorinitials}</code> to the value\n<em>JB</em> in the current document.</p></div>\n<div class=\"paragraph\"><p>To delete (undefine) an attribute use the following syntax:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:&lt;name&gt;!:</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">AttributeEntry behavior</div><ul>\n<li>\n<p>\nThe attribute entry line begins with colon — no white space allowed\n  in left margin.\n</p>\n</li>\n<li>\n<p>\nAsciiDoc converts the <code>&lt;name&gt;</code> to a legal attribute name (lower\n  case, alphanumeric, dash and underscore characters only — all other\n  characters deleted). This allows more human friendly text to be\n  used.\n</p>\n</li>\n<li>\n<p>\nLeading and trailing white space is stripped from the <code>&lt;value&gt;</code>.\n</p>\n</li>\n<li>\n<p>\nLines ending in a space followed by a plus character are continued\n  to the next line, for example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:description: AsciiDoc is a text document format for writing notes, +\n              documentation, articles, books, slideshows, web pages +\n              and man pages.</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nIf the <code>&lt;value&gt;</code> is blank then the corresponding attribute value is\n  set to an empty string.\n</p>\n</li>\n<li>\n<p>\nAttribute references contained in the entry <code>&lt;value&gt;</code> will be\n  expanded.\n</p>\n</li>\n<li>\n<p>\nBy default AttributeEntry values are substituted for\n  <code>specialcharacters</code> and <code>attributes</code> (see above), if you want to\n  change or disable AttributeEntry substitution use the <a href=\"#X77\">   inline macro</a> syntax.\n</p>\n</li>\n<li>\n<p>\nAttribute entries in the document Header are available for header\n  markup template substitution.\n</p>\n</li>\n<li>\n<p>\nAttribute elements override configuration file and intrinsic\n  attributes but do not override command-line attributes.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Here are some more attribute entry examples:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>AsciiDoc User Manual\n====================\n:author:    Stuart Rackham\n:email:     srackham@gmail.com\n:revdate:   April 23, 2004\n:revnumber: 5.1.1</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Which creates these attributes:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>{author}, {firstname}, {lastname}, {authorinitials}, {email},\n{revdate}, {revnumber}</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The previous example is equivalent to this <a href=\"#X95\">document header</a>:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>AsciiDoc User Manual\n====================\nStuart Rackham &lt;srackham@gmail.com&gt;\n5.1.1, April 23, 2004</code></pre>\n</div></div>\n<div class=\"sect2\">\n<h3 id=\"_setting_configuration_entries\">28.1. Setting configuration entries</h3>\n<div class=\"paragraph\"><p>A variant of the Attribute Entry syntax allows configuration file\nsection entries and markup template sections to be set from within an\nAsciiDoc document:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:&lt;section_name&gt;.[&lt;entry_name&gt;]: &lt;entry_value&gt;</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Where <code>&lt;section_name&gt;</code> is the configuration section name,\n<code>&lt;entry_name&gt;</code> is the name of the entry and <code>&lt;entry_value&gt;</code> is the\noptional entry value. This example sets the default labeled list\nstyle to <em>horizontal</em>:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:listdef-labeled.style: horizontal</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>It is exactly equivalent to a configuration file containing:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[listdef-labeled]\nstyle=horizontal</code></pre>\n</div></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf the <code>&lt;entry_name&gt;</code> is omitted then the entire section is\n  substituted with the <code>&lt;entry_value&gt;</code>. This feature should only be\n  used to set markup template sections. The following example sets the\n  <em>xref2</em> inline macro markup template:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:xref2-inlinemacro.: &lt;a href=\"#{1}\"&gt;{2?{2}}&lt;/a&gt;</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nNo substitution is performed on configuration file attribute entries\n  and they cannot be undefined.\n</p>\n</li>\n<li>\n<p>\nThis feature can only be used in attribute entries — configuration\n  attributes cannot be set using the <code>asciidoc(1)</code> command <code>--attribute</code>\n  option.\n</p>\n</li>\n</ul></div>\n<div class=\"sidebarblock\" id=\"X62\">\n<div class=\"content\">\n<div class=\"title\">Attribute entries promote clarity and eliminate repetition</div>\n<div class=\"paragraph\"><p>URLs and file names in AsciiDoc macros are often quite long — they\nbreak paragraph flow and readability suffers.  The problem is\ncompounded by redundancy if the same name is used repeatedly.\nAttribute entries can be used to make your documents easier to read\nand write, here are some examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:1:         http://freshmeat.net/projects/asciidoc/\n:homepage:  http://methods.co.nz/asciidoc/[AsciiDoc home page]\n:new:       image:./images/smallnew.png[]\n:footnote1: footnote:[A meaningless latin term]</code></pre>\n</div></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Using previously defined attributes: See the {1}[Freshmeat summary]\nor the {homepage} for something new {new}. Lorem ispum {footnote1}.</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Note</div><ul>\n<li>\n<p>\nThe attribute entry definition must precede it’s usage.\n</p>\n</li>\n<li>\n<p>\nYou are not limited to URLs or file names, entire macro calls or\n  arbitrary lines of text can be abbreviated.\n</p>\n</li>\n<li>\n<p>\nShared attributes entries could be grouped into a separate file and\n  <a href=\"#X63\">included</a> in multiple documents.\n</p>\n</li>\n</ul></div>\n</div></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X21\">29. Attribute Lists</h2>\n<div class=\"sectionbody\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAn attribute list is a comma separated list of attribute values.\n</p>\n</li>\n<li>\n<p>\nThe entire list is enclosed in square brackets.\n</p>\n</li>\n<li>\n<p>\nAttribute lists are used to pass parameters to macros, blocks (using\n  the <a href=\"#X79\">AttributeList element</a>) and inline quotes.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>The list consists of zero or more positional attribute values followed\nby zero or more named attribute values.  Here are three examples: a\nsingle unquoted positional attribute; three unquoted positional\nattribute values; one positional attribute followed by two named\nattributes; the unquoted attribute value in the final example contains\ncomma (<code>&amp;#44;</code>) and double-quote (<code>&amp;#34;</code>) character entities:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[Hello]\n[quote, Bertrand Russell, The World of Mathematics (1956)]\n[\"22 times\", backcolor=\"#0e0e0e\", options=\"noborders,wide\"]\n[A footnote&amp;#44; &amp;#34;with an image&amp;#34; image:smallnew.png[]]</code></pre>\n</div></div>\n<div class=\"ulist\"><div class=\"title\">Attribute list behavior</div><ul>\n<li>\n<p>\nIf one or more attribute values contains a comma the all string\n  values must be quoted (enclosed in double quotation mark\n  characters).\n</p>\n</li>\n<li>\n<p>\nIf the list contains any named or quoted attributes then all string\n  attribute values must be quoted.\n</p>\n</li>\n<li>\n<p>\nTo include a double quotation mark (\") character in a quoted\n  attribute value the the quotation mark must be escaped with a\n  backslash.\n</p>\n</li>\n<li>\n<p>\nList attributes take precedence over existing attributes.\n</p>\n</li>\n<li>\n<p>\nList attributes can only be referenced in configuration file markup\n  templates and tags, they are not available elsewhere in the\n  document.\n</p>\n</li>\n<li>\n<p>\nSetting a named attribute to <code>None</code> undefines the attribute.\n</p>\n</li>\n<li>\n<p>\nPositional attributes are referred to as <code>{1}</code>,<code>{2}</code>,<code>{3}</code>,…\n</p>\n</li>\n<li>\n<p>\nAttribute <code>{0}</code> refers to the entire list (excluding the enclosing\n  square brackets).\n</p>\n</li>\n<li>\n<p>\nNamed attribute names cannot contain dash characters.\n</p>\n</li>\n</ul></div>\n<div class=\"sect2\">\n<h3 id=\"X75\">29.1. Options attribute</h3>\n<div class=\"paragraph\"><p>If the attribute list contains an attribute named <code>options</code> it is\nprocessed as a comma separated list of option names:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nEach name generates an attribute named like <code>&lt;option&gt;-option</code> (where\n  <code>&lt;option&gt;</code> is the option name) with an empty string value.  For\n  example <code>[options=\"opt1,opt2,opt3\"]</code> is equivalent to setting the\n  following three attributes\n  <code>[opt1-option=\"\",opt2-option=\"\",opt2-option=\"\"]</code>.\n</p>\n</li>\n<li>\n<p>\nIf you define a an option attribute globally (for example with an\n  <a href=\"#X18\">attribute entry</a>) then it will apply to all elements in the\n  document.\n</p>\n</li>\n<li>\n<p>\nAsciiDoc implements a number of predefined options which are listed\n  in the <a href=\"#X74\">Attribute Options appendix</a>.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_macro_attribute_lists\">29.2. Macro Attribute lists</h3>\n<div class=\"paragraph\"><p>Macros calls are suffixed with an attribute list. The list may be\nempty but it cannot be omitted. List entries are used to pass\nattribute values to macro markup templates.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_attribute_references\">30. Attribute References</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>An attribute reference is an attribute name (possibly followed by an\nadditional parameters) enclosed in curly braces.  When an attribute\nreference is encountered it is evaluated and replaced by its\ncorresponding text value.  If the attribute is undefined the line\ncontaining the attribute is dropped.</p></div>\n<div class=\"paragraph\"><p>There are three types of attribute reference: <em>Simple</em>, <em>Conditional</em>\nand <em>System</em>.</p></div>\n<div class=\"ulist\"><div class=\"title\">Attribute reference evaluation</div><ul>\n<li>\n<p>\nYou can suppress attribute reference expansion by placing a\n  backslash character immediately in front of the opening brace\n  character.\n</p>\n</li>\n<li>\n<p>\nBy default attribute references are not expanded in\n  <em>LiteralParagraphs</em>, <em>ListingBlocks</em> or <em>LiteralBlocks</em>.\n</p>\n</li>\n<li>\n<p>\nAttribute substitution proceeds line by line in reverse line order.\n</p>\n</li>\n<li>\n<p>\nAttribute reference evaluation is performed in the following order:\n  <em>Simple</em> then <em>Conditional</em> and finally <em>System</em>.\n</p>\n</li>\n</ul></div>\n<div class=\"sect2\">\n<h3 id=\"_simple_attributes_references\">30.1. Simple Attributes References</h3>\n<div class=\"paragraph\"><p>Simple attribute references take the form <code>{&lt;name&gt;}</code>. If the\nattribute name is defined its text value is substituted otherwise the\nline containing the reference is dropped from the output.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_conditional_attribute_references\">30.2. Conditional Attribute References</h3>\n<div class=\"paragraph\"><p>Additional parameters are used in conjunction with attribute names to\ncalculate a substitution value. Conditional attribute references take\nthe following forms:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;=&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value&gt;</code> is substituted if the attribute <code>&lt;names&gt;</code> is\n        undefined otherwise its value is substituted. <code>&lt;value&gt;</code> can\n        contain simple attribute references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;?&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value&gt;</code> is substituted if the attribute <code>&lt;names&gt;</code> is defined\n        otherwise an empty string is substituted.  <code>&lt;value&gt;</code> can\n        contain simple attribute references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;!&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value&gt;</code> is substituted if the attribute <code>&lt;names&gt;</code> is\n        undefined otherwise an empty string is substituted.  <code>&lt;value&gt;</code>\n        can contain simple attribute references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;#&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value&gt;</code> is substituted if the attribute <code>&lt;names&gt;</code> is defined\n        otherwise the undefined attribute entry causes the containing\n        line to be dropped.  <code>&lt;value&gt;</code> can contain simple attribute\n        references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;%&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value&gt;</code> is substituted if the attribute <code>&lt;names&gt;</code> is not\n        defined otherwise the containing line is dropped.  <code>&lt;value&gt;</code>\n        can contain simple attribute references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;@&lt;regexp&gt;:&lt;value1&gt;[:&lt;value2&gt;]}</code>\n</dt>\n<dd>\n<p>\n        <code>&lt;value1&gt;</code> is substituted if the value of attribute <code>&lt;names&gt;</code>\n        matches the regular expression <code>&lt;regexp&gt;</code> otherwise <code>&lt;value2&gt;</code>\n        is substituted. If attribute <code>&lt;names&gt;</code> is not defined the\n        containing line is dropped. If <code>&lt;value2&gt;</code> is omitted an empty\n        string is assumed. The values and the regular expression can\n        contain simple attribute references.  To embed colons in the\n        values or the regular expression escape them with backslashes.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;$&lt;regexp&gt;:&lt;value1&gt;[:&lt;value2&gt;]}</code>\n</dt>\n<dd>\n<p>\n        Same behavior as the previous ternary attribute except for\n        the following cases:\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;$&lt;regexp&gt;:&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n                Substitutes <code>&lt;value&gt;</code> if <code>&lt;names&gt;</code> matches <code>&lt;regexp&gt;</code>\n                otherwise the result is undefined and the containing\n                line is dropped.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{&lt;names&gt;$&lt;regexp&gt;::&lt;value&gt;}</code>\n</dt>\n<dd>\n<p>\n                Substitutes <code>&lt;value&gt;</code> if <code>&lt;names&gt;</code> does not match\n                <code>&lt;regexp&gt;</code> otherwise the result is undefined and the\n                containing line is dropped.\n</p>\n</dd>\n</dl></div>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>The attribute <code>&lt;names&gt;</code> parameter normally consists of a single\nattribute name but it can be any one of the following:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nA single attribute name which evaluates to the attributes value.\n</p>\n</li>\n<li>\n<p>\nMultiple <em>,</em> separated attribute names which evaluates to an empty\n  string if one or more of the attributes is defined, otherwise it’s\n  value is undefined.\n</p>\n</li>\n<li>\n<p>\nMultiple <em>+</em> separated attribute names which evaluates to an empty\n  string if all of the attributes are defined, otherwise it’s value is\n  undefined.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Conditional attributes with single attribute names are evaluated first\nso they can be used inside the multi-attribute conditional <code>&lt;value&gt;</code>.</p></div>\n<div class=\"sect3\">\n<h4 id=\"_conditional_attribute_examples\">30.2.1. Conditional attribute examples</h4>\n<div class=\"paragraph\"><p>Conditional attributes are mainly used in AsciiDoc configuration\nfiles — see the distribution <code>.conf</code> files for examples.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nAttribute equality test\n</dt>\n<dd>\n<p>\n  If <code>{backend}</code> is <em>docbook45</em> or <em>xhtml11</em> the example evaluates to\n  “DocBook 4.5 or XHTML 1.1 backend” otherwise it evaluates to\n  “some other backend”:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>{backend@docbook45|xhtml11:DocBook 4.5 or XHTML 1.1 backend:some other backend}</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nAttribute value map\n</dt>\n<dd>\n<p>\n  This example maps the <code>frame</code> attribute values [<code>topbot</code>, <code>all</code>,\n  <code>none</code>, <code>sides</code>] to [<code>hsides</code>, <code>border</code>, <code>void</code>, <code>vsides</code>]:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>{frame@topbot:hsides}{frame@all:border}{frame@none:void}{frame@sides:vsides}</code></pre>\n</div></div>\n</dd>\n</dl></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X24\">30.3. System Attribute References</h3>\n<div class=\"paragraph\"><p>System attribute references generate the attribute text value by\nexecuting a predefined action that is parametrized by one or more\narguments. The syntax is <code>{&lt;action&gt;:&lt;arguments&gt;}</code>.</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<code>{counter:&lt;attrname&gt;[:&lt;seed&gt;]}</code>\n</dt>\n<dd>\n<p>\n        Increments the document attribute (if the attribute is\n        undefined it is set to <code>1</code>). Returns the new attribute value.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nCounters generate global (document wide) attributes.\n</p>\n</li>\n<li>\n<p>\nThe optional <code>&lt;seed&gt;</code> specifies the counter’s initial value;\n          it can be a number or a single letter; defaults to <em>1</em>.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;seed&gt;</code> can contain simple and conditional attribute\n          references.\n</p>\n</li>\n<li>\n<p>\nThe <em>counter</em> system attribute will not be executed if the\n          containing line is dropped by the prior evaluation of an\n          undefined attribute.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>{counter2:&lt;attrname&gt;[:&lt;seed&gt;]}</code>\n</dt>\n<dd>\n<p>\n        Same as <code>counter</code> except the it always returns a blank string.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{eval:&lt;expression&gt;}</code>\n</dt>\n<dd>\n<p>\n        Substitutes the result of the Python <code>&lt;expression&gt;</code>.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf <code>&lt;expression&gt;</code> evaluates to <code>None</code> or <code>False</code> the\n          reference is deemed undefined and the line containing the\n          reference is dropped from the output.\n</p>\n</li>\n<li>\n<p>\nIf the expression evaluates to <code>True</code> the attribute\n          evaluates to an empty string.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;expression&gt;</code> can contain simple and conditional attribute\n          references.\n</p>\n</li>\n<li>\n<p>\nThe <em>eval</em> system attribute can be nested inside other\n          system attributes.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>{eval3:&lt;command&gt;}</code>\n</dt>\n<dd>\n<p>\n        Passthrough version of <code>{eval:&lt;expression&gt;}</code> — the generated\n        output is written directly to the output without any further\n        substitutions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{include:&lt;filename&gt;}</code>\n</dt>\n<dd>\n<p>\n        Substitutes contents of the file named <code>&lt;filename&gt;</code>.\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe included file is read at the time of attribute\n          substitution.\n</p>\n</li>\n<li>\n<p>\nIf the file does not exist a warning is emitted and the line\n          containing the reference is dropped from the output file.\n</p>\n</li>\n<li>\n<p>\nTabs are expanded based on the current <em>tabsize</em> attribute\n          value.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>{set:&lt;attrname&gt;[!][:&lt;value&gt;]}</code>\n</dt>\n<dd>\n<p>\n        Sets or unsets document attribute. Normally only used in\n        configuration file markup templates (use\n        <a href=\"#X18\">AttributeEntries</a> in AsciiDoc documents).\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf the attribute name is followed by an exclamation mark\n          the attribute becomes undefined.\n</p>\n</li>\n<li>\n<p>\nIf <code>&lt;value&gt;</code> is omitted the attribute is set to a blank\n          string.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;value&gt;</code> can contain simple and conditional attribute\n          references.\n</p>\n</li>\n<li>\n<p>\nReturns a blank string unless the attribute is undefined in\n          which case the return value is undefined and the enclosing\n          line will be dropped.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\n<code>{set2:&lt;attrname&gt;[!][:&lt;value&gt;]}</code>\n</dt>\n<dd>\n<p>\n        Same as <code>set</code> except that the attribute scope is local to the\n        template.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{sys:&lt;command&gt;}</code>\n</dt>\n<dd>\n<p>\n        Substitutes the stdout generated by the execution of the shell\n        <code>&lt;command&gt;</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{sys2:&lt;command&gt;}</code>\n</dt>\n<dd>\n<p>\n        Substitutes the stdout and stderr generated by the execution\n        of the shell <code>&lt;command&gt;</code>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{sys3:&lt;command&gt;}</code>\n</dt>\n<dd>\n<p>\n        Passthrough version of <code>{sys:&lt;command&gt;}</code> — the generated\n        output is written directly to the output without any further\n        substitutions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<code>{template:&lt;template&gt;}</code>\n</dt>\n<dd>\n<p>\n        Substitutes the contents of the configuration file section\n        named <code>&lt;template&gt;</code>. Attribute references contained in the\n        template are substituted.\n</p>\n</dd>\n</dl></div>\n<div class=\"ulist\"><div class=\"title\">System reference behavior</div><ul>\n<li>\n<p>\nSystem attribute arguments can contain non-system attribute\n  references.\n</p>\n</li>\n<li>\n<p>\nClosing brace characters inside system attribute arguments must be\n  escaped with a backslash.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X60\">31. Intrinsic Attributes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Intrinsic attributes are simple attributes that are created\nautomatically from: AsciiDoc document header parameters; <code>asciidoc(1)</code>\ncommand-line arguments; attributes defined in the default\nconfiguration files; the execution context.  Here’s the list of\npredefined intrinsic attributes:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>{amp}                 ampersand (&amp;) character entity\n{asciidoc-args}       used to pass inherited arguments to asciidoc filters\n{asciidoc-confdir}    the asciidoc(1) global configuration directory\n{asciidoc-dir}        the asciidoc(1) application directory\n{asciidoc-file}       the full path name of the asciidoc(1) script\n{asciidoc-version}    the version of asciidoc(1)\n{author}              author's full name\n{authored}            empty string '' if {author} or {email} defined,\n{authorinitials}      author initials (from document header)\n{backend-&lt;backend&gt;}   empty string ''\n{&lt;backend&gt;-&lt;doctype&gt;} empty string ''\n{backend}             document backend specified by `-b` option\n{backend-confdir}     the directory containing the &lt;backend&gt;.conf file\n{backslash}           backslash character\n{basebackend-&lt;base&gt;}  empty string ''\n{basebackend}         html or docbook\n{blockname}           current block name (note 8).\n{brvbar}              broken vertical bar (|) character\n{docdate}             document last modified date\n{docdir}              document input directory name  (note 5)\n{docfile}             document file name  (note 5)\n{docname}             document file name without extension (note 6)\n{doctime}             document last modified time\n{doctitle}            document title (from document header)\n{doctype-&lt;doctype&gt;}   empty string ''\n{doctype}             document type specified by `-d` option\n{email}               author's email address (from document header)\n{empty}               empty string ''\n{encoding}            specifies input and output encoding\n{filetype-&lt;fileext&gt;}  empty string ''\n{filetype}            output file name file extension\n{firstname}           author first name (from document header)\n{gt}                  greater than (&gt;) character entity\n{id}                  running block id generated by BlockId elements\n{indir}               input file directory name (note 2,5)\n{infile}              input file name (note 2,5)\n{lastname}            author last name (from document header)\n{ldquo}               Left double quote character (note 7)\n{level}               title level 1..4 (in section titles)\n{listindex}           the list index (1..) of the most recent list item\n{localdate}           the current date\n{localtime}           the current time\n{lsquo}               Left single quote character (note 7)\n{lt}                  less than (&lt;) character entity\n{manname}             manpage name (defined in NAME section)\n{manpurpose}          manpage (defined in NAME section)\n{mantitle}            document title minus the manpage volume number\n{manvolnum}           manpage volume number (1..8) (from document header)\n{middlename}          author middle name (from document header)\n{nbsp}                non-breaking space character entity\n{notitle}             do not display the document title\n{outdir}              document output directory name (note 2)\n{outfile}             output file name (note 2)\n{python}              the full path name of the Python interpreter executable\n{rdquo}               Right double quote character (note 7)\n{reftext}             running block xreflabel generated by BlockId elements\n{revdate}             document revision date (from document header)\n{revnumber}           document revision number (from document header)\n{rsquo}               Right single quote character (note 7)\n{sectnum}             formatted section number (in section titles)\n{sp}                  space character\n{showcomments}        send comment lines to the output\n{title}               section title (in titled elements)\n{two-colons}          Two colon characters\n{two-semicolons}      Two semicolon characters\n{user-dir}            the ~/.asciidoc directory (if it exists)\n{verbose}             defined as '' if --verbose command option specified\n{wj}                  Word-joiner\n{zwsp}                Zero-width space character entity</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nIntrinsic attributes are global so avoid defining custom attributes\n   with the same names.\n</p>\n</li>\n<li>\n<p>\n<code>{outfile}</code>, <code>{outdir}</code>, <code>{infile}</code>, <code>{indir}</code> attributes are\n   effectively read-only (you can set them but it won’t affect the\n   input or output file paths).\n</p>\n</li>\n<li>\n<p>\nSee also the <a href=\"#X88\">Backend Attributes</a> section for attributes\n    that relate to AsciiDoc XHTML file generation.\n</p>\n</li>\n<li>\n<p>\nThe entries that translate to blank strings are designed to be used\n   for conditional text inclusion. You can also use the <code>ifdef</code>,\n   <code>ifndef</code> and <code>endif</code> System macros for conditional inclusion.\n   <span class=\"footnote\"><br>[Conditional inclusion using <code>ifdef</code> and <code>ifndef</code> macros\n   differs from attribute conditional inclusion in that the former\n   occurs when the file is read while the latter occurs when the\n   contents are written.]<br></span>\n</p>\n</li>\n<li>\n<p>\n<code>{docfile}</code> and <code>{docdir}</code> refer to root document specified on the\n   <code>asciidoc(1)</code> command-line; <code>{infile}</code> and <code>{indir}</code> refer to the\n   current input file which may be the root document or an included\n   file. When the input is being read from the standard input\n   (<code>stdin</code>) these attributes are undefined.\n</p>\n</li>\n<li>\n<p>\nIf the input file is the standard input and the output file is not\n   the standard output then <code>{docname}</code> is the output file name sans\n   file extension.\n</p>\n</li>\n<li>\n<p>\nSee\n   <a href=\"http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks\">non-English\n   usage of quotation marks</a>.\n</p>\n</li>\n<li>\n<p>\nThe <code>{blockname}</code> attribute identifies the style of the current\n   block. It applies to delimited blocks, lists and tables. Here is a\n   list of <code>{blockname}</code> values (does not include filters or custom\n   block and style names):\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndelimited blocks\n</dt>\n<dd>\n<p>\ncomment, sidebar, open, pass, literal, verse,\n   listing, quote, example, note, tip, important, caution, warning,\n   abstract, partintro\n</p>\n</dd>\n<dt class=\"hdlist1\">\nlists\n</dt>\n<dd>\n<p>\narabic, loweralpha, upperalpha, lowerroman, upperroman,\n   labeled, labeled3, labeled4, qanda, horizontal, bibliography,\n   glossary\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntables\n</dt>\n<dd>\n<p>\ntable\n</p>\n</dd>\n</dl></div>\n</li>\n</ol></div>\n</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X73\">32. Block Element Definitions</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The syntax and behavior of Paragraph, DelimitedBlock, List and Table\nblock elements is determined by block definitions contained in\n<a href=\"#X7\">AsciiDoc configuration file</a> sections.</p></div>\n<div class=\"paragraph\"><p>Each definition consists of a section title followed by one or more\nsection entries. Each entry defines a block parameter controlling some\naspect of the block’s behavior. Here’s an example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[blockdef-listing]\ndelimiter=^-{4,}$\ntemplate=listingblock\npresubs=specialcharacters,callouts</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Configuration file block definition sections are processed\nincrementally after each configuration file is loaded. Block\ndefinition section entries are merged into the block definition, this\nallows block parameters to be overridden and extended by later\n<a href=\"#X27\">loading configuration files</a>.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc Paragraph, DelimitedBlock, List and Table block elements\nshare a common subset of configuration file parameters:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndelimiter\n</dt>\n<dd>\n<p>\n  A Python regular expression that matches the first line of a block\n  element — in the case of DelimitedBlocks and Tables it also matches\n  the last line.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntemplate\n</dt>\n<dd>\n<p>\n  The name of the configuration file markup template section that will\n  envelope the block contents. The pipe (<em>|</em>) character is substituted\n  for the block contents. List elements use a set of (list specific)\n  tag parameters instead of a single template. The template name can\n  contain attribute references allowing dynamic template selection a\n  the time of template substitution.\n</p>\n</dd>\n<dt class=\"hdlist1\">\noptions\n</dt>\n<dd>\n<p>\n  A comma delimited list of element specific option names. In addition\n  to being used internally, options are available during markup tag\n  and template substitution as attributes with an empty string value\n  named like <code>&lt;option&gt;-option</code> (where <code>&lt;option&gt;</code> is the option name).\n  See <a href=\"#X74\">attribute options</a> for a complete list of available\n  options.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nsubs, presubs, postsubs\n</dt>\n<dd>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<em>presubs</em> and <em>postsubs</em> are lists of comma separated substitutions that are\n    performed on the block contents. <em>presubs</em> is applied first,\n    <em>postsubs</em> (if specified) second.\n</p>\n</li>\n<li>\n<p>\n<em>subs</em> is an alias for <em>presubs</em>.\n</p>\n</li>\n<li>\n<p>\nIf a <em>filter</em> is allowed (Paragraphs, DelimitedBlocks and Tables)\n    and has been specified then <em>presubs</em> and <em>postsubs</em> substitutions\n    are performed before and after the filter is run respectively.\n</p>\n</li>\n<li>\n<p>\nAllowed values: <em>specialcharacters</em>, <em>quotes</em>, <em>specialwords</em>,\n    <em>replacements</em>, <em>macros</em>, <em>attributes</em>, <em>callouts</em>.\n</p>\n</li>\n<li>\n<p>\n<a id=\"X102\"></a>The following composite values are also allowed:\n</p>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\n<em>none</em>\n</dt>\n<dd>\n<p>\n        No substitutions.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<em>normal</em>\n</dt>\n<dd>\n<p>\n        The following substitutions in the following order:\n        <em>specialcharacters</em>, <em>quotes</em>, <em>attributes</em>, <em>specialwords</em>,\n        <em>replacements</em>, <em>macros</em>, <em>replacements2</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n<em>verbatim</em>\n</dt>\n<dd>\n<p>\n        The following substitutions in the following order:\n        <em>specialcharacters</em> and <em>callouts</em>.\n</p>\n</dd>\n</dl></div>\n</li>\n<li>\n<p>\n<em>normal</em> and <em>verbatim</em> substitutions can be redefined by with\n    <code>subsnormal</code> and <code>subsverbatim</code> entries in a configuration file\n    <code>[miscellaneous]</code> section.\n</p>\n</li>\n<li>\n<p>\nThe substitutions are processed in the order in which they are\n    listed and can appear more than once.\n</p>\n</li>\n</ul></div>\n</dd>\n<dt class=\"hdlist1\">\nfilter\n</dt>\n<dd>\n<p>\n  This optional entry specifies an executable shell command for\n  processing block content (Paragraphs, DelimitedBlocks and Tables).\n  The filter command can contain attribute references.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nposattrs\n</dt>\n<dd>\n<p>\n  Optional comma separated list of positional attribute names. This\n  list maps positional attributes (in the block’s <a href=\"#X21\">attribute   list</a>) to named block attributes. The following example, from the\n  QuoteBlock definition, maps the first and section positional\n  attributes:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>posattrs=attribution,citetitle</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nstyle\n</dt>\n<dd>\n<p>\n  This optional parameter specifies the default style name.\n</p>\n</dd>\n<dt class=\"hdlist1\">\n&lt;stylename&gt;-style\n</dt>\n<dd>\n<p>\n  Optional style definition (see <a href=\"#X23\">Styles</a> below).\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>The following block parameters behave like document attributes and can\nbe set in block attribute lists and style definitions: <em>template</em>,\n<em>options</em>, <em>subs</em>, <em>presubs</em>, <em>postsubs</em>, <em>filter</em>.</p></div>\n<div class=\"sect2\">\n<h3 id=\"X23\">32.1. Styles</h3>\n<div class=\"paragraph\"><p>A style is a set of block parameter bundled as a single named\nparameter. The following example defines a style named <em>verbatim</em>:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>verbatim-style=template=\"literalblock\",subs=\"verbatim\"</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>If a block’s <a href=\"#X21\">attribute list</a> contains a <em>style</em> attribute then\nthe corresponding style parameters are be merged into the default\nblock definition parameters.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAll style parameter names must be suffixed with <code>-style</code> and the\n  style parameter value is in the form of a list of <a href=\"#X21\">named   attributes</a>.\n</p>\n</li>\n<li>\n<p>\nThe <em>template</em> style parameter is mandatory, other parameters can be\n  omitted in which case they inherit their values from the default\n  block definition parameters.\n</p>\n</li>\n<li>\n<p>\nMulti-item style parameters (<em>subs</em>,<em>presubs</em>,<em>postsubs</em>,<em>posattrs</em>)\n  must be specified using Python tuple syntax (rather than a simple\n  list of values as they in separate entries) e.g.\n  <code>postsubs=(\"callouts\",)</code> not <code>postsubs=\"callouts\"</code>.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_paragraphs_2\">32.2. Paragraphs</h3>\n<div class=\"paragraph\"><p>Paragraph translation is controlled by <code>[paradef-*]</code> configuration\nfile section entries. Users can define new types of paragraphs and\nmodify the behavior of existing types by editing AsciiDoc\nconfiguration files.</p></div>\n<div class=\"paragraph\"><p>Here is the shipped Default paragraph definition:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>[paradef-default]\ndelimiter=(?P&lt;text&gt;\\S.*)\ntemplate=paragraph</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The normal paragraph definition has a couple of special properties:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nIt must exist and be defined in a configuration file section named\n   <code>[paradef-default]</code>.\n</p>\n</li>\n<li>\n<p>\nIrrespective of its position in the configuration files default\n   paragraph document matches are attempted only after trying all\n   other paragraph types.\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>Paragraph specific block parameter notes:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndelimiter\n</dt>\n<dd>\n<p>\n  This regular expression must contain the named group <em>text</em> which\n  matches the text on the first line.  Paragraphs are terminated by a\n  blank line, the end of file, or the start of a DelimitedBlock.\n</p>\n</dd>\n<dt class=\"hdlist1\">\noptions\n</dt>\n<dd>\n<p>\n  The <em>listelement</em> option specifies that paragraphs of this type will\n  automatically be considered part of immediately preceding list\n  items.  The <em>skip</em> option causes the paragraph to be treated as a\n  comment (see <a href=\"#X26\">CommentBlocks</a>).\n</p>\n</dd>\n</dl></div>\n<div class=\"olist arabic\"><div class=\"title\">Paragraph processing proceeds as follows:</div><ol class=\"arabic\">\n<li>\n<p>\nThe paragraph text is aligned to the left margin.\n</p>\n</li>\n<li>\n<p>\nOptional <em>presubs</em> inline substitutions are performed on the\n   paragraph text.\n</p>\n</li>\n<li>\n<p>\nIf a filter command is specified it is executed and the paragraph\n   text piped to its standard input; the filter output replaces the\n   paragraph text.\n</p>\n</li>\n<li>\n<p>\nOptional <em>postsubs</em> inline substitutions are performed on the\n   paragraph text.\n</p>\n</li>\n<li>\n<p>\nThe paragraph text is enveloped by the paragraph’s markup template\n   and written to the output file.\n</p>\n</li>\n</ol></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_delimited_blocks\">32.3. Delimited Blocks</h3>\n<div class=\"paragraph\"><p>DelimitedBlock <em>options</em> values are:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nsectionbody\n</dt>\n<dd>\n<p>\n    The block contents are processed as a SectionBody.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nskip\n</dt>\n<dd>\n<p>\n    The block is treated as a comment (see <a href=\"#X26\">CommentBlocks</a>).\n    Preceding <a href=\"#X21\">attribute lists</a> and <a href=\"#X42\">block titles</a> are not\n    consumed.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p><em>presubs</em>, <em>postsubs</em> and <em>filter</em> entries are ignored when\n<em>sectionbody</em> or <em>skip</em> options are set.</p></div>\n<div class=\"paragraph\"><p>DelimitedBlock processing proceeds as follows:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nOptional <em>presubs</em> substitutions are performed on the block\n   contents.\n</p>\n</li>\n<li>\n<p>\nIf a filter is specified it is executed and the block’s contents\n   piped to its standard input. The filter output replaces the block\n   contents.\n</p>\n</li>\n<li>\n<p>\nOptional <em>postsubs</em> substitutions are performed on the block\n   contents.\n</p>\n</li>\n<li>\n<p>\nThe block contents is enveloped by the block’s markup template and\n   written to the output file.\n</p>\n</li>\n</ol></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">Attribute expansion is performed on the block filter command\nbefore it is executed, this is useful for passing arguments to the\nfilter.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_lists\">32.4. Lists</h3>\n<div class=\"paragraph\"><p>List behavior and syntax is determined by <code>[listdef-*]</code> configuration\nfile sections. The user can change existing list behavior and add new\nlist types by editing configuration files.</p></div>\n<div class=\"paragraph\"><p>List specific block definition notes:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ntype\n</dt>\n<dd>\n<p>\n  This is either <em>bulleted</em>,<em>numbered</em>,<em>labeled</em> or <em>callout</em>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\ndelimiter\n</dt>\n<dd>\n<p>\n  A Python regular expression that matches the first line of a\n  list element entry. This expression can contain the named groups\n  <em>text</em> (bulleted groups), <em>index</em> and <em>text</em> (numbered lists),\n  <em>label</em> and <em>text</em> (labeled lists).\n</p>\n</dd>\n<dt class=\"hdlist1\">\ntags\n</dt>\n<dd>\n<p>\n  The <code>&lt;name&gt;</code> of the <code>[listtags-&lt;name&gt;]</code> configuration file section\n  containing list markup tag definitions.  The tag entries (<em>list</em>,\n  <em>entry</em>, <em>label</em>, <em>term</em>, <em>text</em>) map the AsciiDoc list structure to\n  backend markup; see the <em>listtags</em> sections in the AsciiDoc\n  distributed backend <code>.conf</code> configuration files for examples.\n</p>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_tables_2\">32.5. Tables</h3>\n<div class=\"paragraph\"><p>Table behavior and syntax is determined by <code>[tabledef-*]</code> and\n<code>[tabletags-*]</code> configuration file sections. The user can change\nexisting table behavior and add new table types by editing\nconfiguration files.  The following <code>[tabledef-*]</code> section entries\ngenerate table output markup elements:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ncolspec\n</dt>\n<dd>\n<p>\n  The table <em>colspec</em> tag definition.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nheadrow, footrow, bodyrow\n</dt>\n<dd>\n<p>\n  Table header, footer and body row tag definitions. <em>headrow</em> and\n  <em>footrow</em> table definition entries default to <em>bodyrow</em> if\n  they are undefined.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nheaddata, footdata, bodydata\n</dt>\n<dd>\n<p>\n  Table header, footer and body data tag definitions. <em>headdata</em> and\n  <em>footdata</em> table definition entries default to <em>bodydata</em> if they\n  are undefined.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nparagraph\n</dt>\n<dd>\n<p>\n  If the <em>paragraph</em> tag is specified then blank lines in the cell\n  data are treated as paragraph delimiters and marked up using this\n  tag.\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\" id=\"X4\"><p>Table behavior is also influenced by the following <code>[miscellaneous]</code>\nconfiguration file entries:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\npagewidth\n</dt>\n<dd>\n<p>\n  This integer value is the printable width of the output media. See\n  <a href=\"#X69\">table attributes</a>.\n</p>\n</dd>\n<dt class=\"hdlist1\">\npageunits\n</dt>\n<dd>\n<p>\n  The units of width in output markup width attribute values.\n</p>\n</dd>\n</dl></div>\n<div class=\"ulist\"><div class=\"title\">Table definition behavior</div><ul>\n<li>\n<p>\nThe output markup generation is specifically designed to work with\n  the HTML and CALS (DocBook) table models, but should be adaptable to\n  most XML table schema.\n</p>\n</li>\n<li>\n<p>\nTable definitions can be “mixed in” from multiple cascading\n  configuration files.\n</p>\n</li>\n<li>\n<p>\nNew table definitions inherit the default table and table tags\n  definitions (<code>[tabledef-default]</code> and <code>[tabletags-default]</code>) so you\n  only need to override those conf file entries that require\n  modification.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X59\">33. Filters</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc filters allow external commands to process AsciiDoc\n<em>Paragraphs</em>, <em>DelimitedBlocks</em> and <em>Table</em> content. Filters are\nprimarily an extension mechanism for generating specialized outputs.\nFilters are implemented using external commands which are specified in\nconfiguration file definitions.</p></div>\n<div class=\"paragraph\"><p>There’s nothing special about the filters, they’re just standard UNIX\nfilters: they read text from the standard input, process it, and write\nto the standard output.</p></div>\n<div class=\"paragraph\"><p>The <code>asciidoc(1)</code> command <code>--filter</code> option can be used to install and\nremove filters. The same option is used to unconditionally load a\nfilter.</p></div>\n<div class=\"paragraph\"><p>Attribute substitution is performed on the filter command prior to\nexecution — attributes can be used to pass parameters from the\nAsciiDoc source document to the filter.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/warning.png\" alt=\"Warning\">\n</td>\n<td class=\"content\">Filters sometimes included executable code. Before installing\na filter you should verify that it is from a trusted source.</td>\n</tr></tbody></table>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_filter_search_paths\">33.1. Filter Search Paths</h3>\n<div class=\"paragraph\"><p>If the filter command does not specify a directory path then\n<code>asciidoc(1)</code> recursively searches for the executable filter command:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nFirst it looks in the user’s <code>$HOME/.asciidoc/filters</code> directory.\n</p>\n</li>\n<li>\n<p>\nNext the global filters directory (usually <code>/etc/asciidoc/filters</code>\n  or <code>/usr/local/etc/asciidoc</code>) directory is searched.\n</p>\n</li>\n<li>\n<p>\nThen it looks in the <code>asciidoc(1)</code> <code>./filters</code> directory.\n</p>\n</li>\n<li>\n<p>\nFinally it relies on the executing shell to search the environment\n  search path (<code>$PATH</code>).\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Standard practice is to install each filter in it’s own sub-directory\nwith the same name as the filter’s style definition. For example the\nmusic filter’s style name is <em>music</em> so it’s configuration and filter\nfiles are stored in the <code>filters/music</code> directory.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_filter_configuration_files\">33.2. Filter Configuration Files</h3>\n<div class=\"paragraph\"><p>Filters are normally accompanied by a configuration file containing a\nParagraph or DelimitedBlock definition along with corresponding markup\ntemplates.</p></div>\n<div class=\"paragraph\"><p>While it is possible to create new <em>Paragraph</em> or <em>DelimitedBlock</em>\ndefinitions the preferred way to implement a filter is to add a\n<a href=\"#X23\">style</a> to the existing Paragraph and ListingBlock definitions\n(all filters shipped with AsciiDoc use this technique). The filter is\napplied to the paragraph or delimited block by preceding it with an\nattribute list: the first positional attribute is the style name,\nremaining attributes are normally filter specific parameters.</p></div>\n<div class=\"paragraph\"><p><code>asciidoc(1)</code> auto-loads all <code>.conf</code> files found in the filter search\npaths unless the container directory also contains a file named\n<code>__noautoload__</code> (see previous section). The <code>__noautoload__</code> feature\nis used for filters that will be loaded manually using the <code>--filter</code>\noption.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X56\">33.3. Example Filter</h3>\n<div class=\"paragraph\"><p>AsciiDoc comes with a toy filter for highlighting source code keywords\nand comments.  See also the <code>./filters/code/code-filter-readme.txt</code>\nfile.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">The purpose of this toy filter is to demonstrate how to write a\nfilter — it’s much to simplistic to be passed off as a code syntax\nhighlighter.  If you want a full featured multi-language highlighter\nuse the <a href=\"http://www.methods.co.nz/asciidoc/source-highlight-filter.html\">source code highlighter\nfilter</a>.</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_built_in_filters\">33.4. Built-in filters</h3>\n<div class=\"paragraph\"><p>The AsciiDoc distribution includes <em>source</em>, <em>music</em>, <em>latex</em> and\n<em>graphviz</em> filters, details are on the\n<a href=\"http://www.methods.co.nz/asciidoc/index.html#_filters\">AsciiDoc website</a>.</p></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<caption class=\"title\">Table 11. Built-in filters list</caption>\n<colgroup><col width=\"16%\">\n<col width=\"83%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Filter name </th>\n<th valign=\"top\" align=\"left\">Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>music</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A <a href=\"http://www.methods.co.nz/asciidoc/music-filter.html\">music filter</a> is included in the\ndistribution <code>./filters/</code> directory. It translates music in\n<a href=\"http://lilypond.org/\">LilyPond</a> or <a href=\"http://abcnotation.org.uk/\">ABC</a>\nnotation to standard classical notation.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>source</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">A <a href=\"http://www.methods.co.nz/asciidoc/source-highlight-filter.html\">source code highlight filter</a>\nis included in the distribution <code>./filters/</code> directory.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>latex</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The <a href=\"http://www.methods.co.nz/asciidoc/latex-filter.html\">AsciiDoc LaTeX filter</a> translates\nLaTeX source to a PNG image that is automatically inserted into the\nAsciiDoc output documents.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>graphviz</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Gouichi Iisaka has written a <a href=\"http://www.graphviz.org/\">Graphviz</a>\nfilter for AsciiDoc.  Graphviz generates diagrams from a textual\nspecification. Gouichi Iisaka’s Graphviz filter is included in the\nAsciiDoc distribution. Here are some\n<a href=\"http://www.methods.co.nz/asciidoc/asciidoc-graphviz-sample.html\">AsciiDoc Graphviz examples</a>.</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X58\">33.5. Filter plugins</h3>\n<div class=\"paragraph\"><p>Filter <a href=\"#X101\">plugins</a> are a mechanism for distributing AsciiDoc\nfilters.  A filter plugin is a Zip file containing the files that\nconstitute a filter.  The <code>asciidoc(1)</code> <code>--filter</code> option is used to\nload and manage filer <a href=\"#X101\">plugins</a>.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nFilter plugins <a href=\"#X27\">take precedence</a> over built-in filters with\n  the same name.\n</p>\n</li>\n<li>\n<p>\nBy default filter plugins are installed in\n  <code>$HOME/.asciidoc/filters/&lt;filter&gt;</code> where <code>&lt;filter&gt;</code> is the filter\n  name.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X101\">34. Plugins</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The AsciiDoc plugin architecture is an extension mechanism that allows\nadditional <a href=\"#X100\">backends</a>, <a href=\"#X58\">filters</a> and <a href=\"#X99\">themes</a> to be\nadded to AsciiDoc.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nA plugin is a Zip file containing an AsciiDoc backend, filter or\n  theme (configuration files, stylesheets, scripts, images).\n</p>\n</li>\n<li>\n<p>\nThe <code>asciidoc(1)</code> <code>--backend</code>, <code>--filter</code> and <code>--theme</code> command-line\n  options are used to load and manage plugins. Each of these options\n  responds to the plugin management <em>install</em>, <em>list</em>, <em>remove</em> and\n  <em>build</em> commands.\n</p>\n</li>\n<li>\n<p>\nThe plugin management command names are reserved and cannot be used\n  for filter, backend or theme names.\n</p>\n</li>\n<li>\n<p>\nThe plugin Zip file name always begins with the backend, filter or\n  theme name.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Plugin commands and conventions are documented in the <code>asciidoc(1)</code> man\npage.  You can find lists of plugins on the\n<a href=\"http://www.methods.co.nz/asciidoc/plugins.html\">AsciiDoc website</a>.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X36\">35. Help Commands</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <code>asciidoc(1)</code> command has a <code>--help</code> option which prints help topics\nto stdout. The default topic summarizes <code>asciidoc(1)</code> usage:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc --help</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>To print a help topic specify the topic name as a command argument.\nHelp topic names can be shortened so long as they are not ambiguous.\nExamples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc --help manpage\n$ asciidoc -h m              # Short version of previous example.\n$ asciidoc --help syntax\n$ asciidoc -h s              # Short version of previous example.</code></pre>\n</div></div>\n<div class=\"sect2\">\n<h3 id=\"_customizing_help\">35.1. Customizing Help</h3>\n<div class=\"paragraph\"><p>To change, delete or add your own help topics edit a help\nconfiguration file.  The help file name <code>help-&lt;lang&gt;.conf</code> is based on\nthe setting of the <code>lang</code> attribute, it defaults to <code>help.conf</code>\n(English).  The <a href=\"#X27\">help file location</a> will depend on whether you\nwant the topics to apply to all users or just the current user.</p></div>\n<div class=\"paragraph\"><p>The help topic files have the same named section format as other\n<a href=\"#X7\">configuration files</a>. The <code>help.conf</code> files are stored in the\nsame locations and loaded in the same order as other configuration\nfiles.</p></div>\n<div class=\"paragraph\"><p>When the <code>--help</code> command-line option is specified AsciiDoc loads the\nappropriate help files and then prints the contents of the section\nwhose name matches the help topic name.  If a topic name is not\nspecified <code>default</code> is used. You don’t need to specify the whole help\ntopic name on the command-line, just enough letters to ensure it’s not\nambiguous. If a matching help file section is not found a list of\navailable topics is printed.</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_tips_and_tricks\">36. Tips and Tricks</h2>\n<div class=\"sectionbody\">\n<div class=\"sect2\">\n<h3 id=\"_know_your_editor\">36.1. Know Your Editor</h3>\n<div class=\"paragraph\"><p>Writing AsciiDoc documents will be a whole lot more pleasant if you\nknow your favorite text editor. Learn how to indent and reformat text\nblocks, paragraphs, lists and sentences. <a href=\"#X20\">Tips for <em>vim</em> users</a>\nfollow.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X20\">36.2. Vim Commands for Formatting AsciiDoc</h3>\n<div class=\"sect3\">\n<h4 id=\"_text_wrap_paragraphs\">36.2.1. Text Wrap Paragraphs</h4>\n<div class=\"paragraph\"><p>Use the vim <code>:gq</code> command to reformat paragraphs. Setting the\n<em>textwidth</em> sets the right text wrap margin; for example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:set textwidth=70</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>To reformat a paragraph:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nPosition the cursor at the start of the paragraph.\n</p>\n</li>\n<li>\n<p>\nType <code>gq}</code>.\n</p>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>Execute <code>:help gq</code> command to read about the vim gq command.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">\n<div class=\"ulist\"><ul>\n<li>\n<p>\nAssign the <code>gq}</code> command to the Q key with the <code>nnoremap Q gq}</code>\n  command or put it in your <code>~/.vimrc</code> file to so it’s always\n  available (see the <a href=\"#X61\">Example <code>~/.vimrc</code> file</a>).\n</p>\n</li>\n<li>\n<p>\nPut <code>set</code> commands in your <code>~/.vimrc</code> file so you don’t have to\n  enter them manually.\n</p>\n</li>\n<li>\n<p>\nThe Vim website (<a href=\"http://www.vim.org/\">http://www.vim.org</a>) has a wealth of resources,\n  including scripts for automated spell checking and ASCII Art\n  drawing.\n</p>\n</li>\n</ul></div>\n</td>\n</tr></tbody></table>\n</div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_format_lists\">36.2.2. Format Lists</h4>\n<div class=\"paragraph\"><p>The <code>gq</code> command can also be used to format bulleted, numbered and\ncallout lists. First you need to set the <code>comments</code>, <code>formatoptions</code>\nand <code>formatlistpat</code> (see the <a href=\"#X61\">Example <code>~/.vimrc</code> file</a>).</p></div>\n<div class=\"paragraph\"><p>Now you can format simple lists that use dash, asterisk, period and\nplus bullets along with numbered ordered lists:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nPosition the cursor at the start of the list.\n</p>\n</li>\n<li>\n<p>\nType <code>gq}</code>.\n</p>\n</li>\n</ol></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"_indent_paragraphs\">36.2.3. Indent Paragraphs</h4>\n<div class=\"paragraph\"><p>Indent whole paragraphs by indenting the fist line with the desired\nindent and then executing the <code>gq}</code> command.</p></div>\n</div>\n<div class=\"sect3\">\n<h4 id=\"X61\">36.2.4. Example <code>~/.vimrc</code> File</h4>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>\" Use bold bright fonts.\nset background=dark\n\n\" Show tabs and trailing characters.\nset listchars=tab:»·,trail:·\nset list\n\n\" Don't highlight searched text.\nhighlight clear Search\n\n\" Don't move to matched text while search pattern is being entered.\nset noincsearch\n\n\" Reformat paragraphs and list.\nnnoremap R gq}\n\n\" Delete trailing white space and Dos-returns and to expand tabs to spaces.\nnnoremap S :set et&lt;CR&gt;:retab!&lt;CR&gt;:%s/[\\r \\t]\\+$//&lt;CR&gt;\n\nautocmd BufRead,BufNewFile *.txt,README,TODO,CHANGELOG,NOTES\n        \\ setlocal autoindent expandtab tabstop=8 softtabstop=2 shiftwidth=2 filetype=asciidoc\n        \\ textwidth=70 wrap formatoptions=tcqn\n        \\ formatlistpat=^\\\\s*\\\\d\\\\+\\\\.\\\\s\\\\+\\\\\\\\|^\\\\s*&lt;\\\\d\\\\+&gt;\\\\s\\\\+\\\\\\\\|^\\\\s*[a-zA-Z.]\\\\.\\\\s\\\\+\\\\\\\\|^\\\\s*[ivxIVX]\\\\+\\\\.\\\\s\\\\+\n        \\ comments=s1:/*,ex:*/,://,b:#,:%,:XCOMM,fb:-,fb:*,fb:+,fb:.,fb:&gt;</code></pre>\n</div></div>\n</div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_troubleshooting\">36.3. Troubleshooting</h3>\n<div class=\"paragraph\"><p>AsciiDoc diagnostic features are detailed in the <a href=\"#X82\">Diagnostics appendix</a>.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_gotchas\">36.4. Gotchas</h3>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\nIncorrect character encoding\n</dt>\n<dd>\n<p>\n    If you get an error message like <code>'UTF-8' codec can't decode ...</code>\n    then you source file contains invalid UTF-8 characters — set the\n    AsciiDoc <a href=\"#X54\">encoding attribute</a> for the correct character set\n    (typically ISO-8859-1 (Latin-1) for European languages).\n</p>\n</dd>\n<dt class=\"hdlist1\">\nInvalid output\n</dt>\n<dd>\n<p>\n    AsciiDoc attempts to validate the input AsciiDoc source but makes\n    no attempt to validate the output markup, it leaves that to\n    external tools such as <code>xmllint(1)</code> (integrated into <code>a2x(1)</code>).\n    Backend validation cannot be hardcoded into AsciiDoc because\n    backends are dynamically configured. The following example\n    generates valid HTML but invalid DocBook (the DocBook <code>literal</code>\n    element cannot contain an <code>emphasis</code> element):\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>+monospaced text with an _emphasized_ word+</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nMisinterpreted text formatting\n</dt>\n<dd>\n<p>\n    You can suppress markup expansion by placing a backslash character\n    immediately in front of the element. The following example\n    suppresses inline monospaced formatting:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>\\+1 for C++.</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nOverlapping text formatting\n</dt>\n<dd>\n<p>\n    Overlapping text formatting will generate illegal overlapping\n    markup tags which will result in downstream XML parsing errors.\n    Here’s an example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>Some *strong markup _that overlaps* emphasized markup_.</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nAmbiguous underlines\n</dt>\n<dd>\n<p>\n    A DelimitedBlock can immediately follow a paragraph without an\n    intervening blank line, but be careful, a single line paragraph\n    underline may be misinterpreted as a section title underline\n    resulting in a “closing block delimiter expected” error.\n</p>\n</dd>\n<dt class=\"hdlist1\">\nAmbiguous ordered list items\n</dt>\n<dd>\n<p>\n    Lines beginning with numbers at the end of sentences will be\n    interpreted as ordered list items.  The following example\n    (incorrectly) begins a new list with item number 1999:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>He was last sighted in\n1999. Since then things have moved on.</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <em>list item out of sequence</em> warning makes it unlikely that this\nproblem will go unnoticed.</p></div>\n</dd>\n<dt class=\"hdlist1\">\nSpecial characters in attribute values\n</dt>\n<dd>\n<p>\n    Special character substitution precedes attribute substitution so\n    if attribute values contain special characters you may, depending\n    on the substitution context, need to escape the special characters\n    yourself. For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a 'orgname=Bill &amp;amp; Ben Inc.' mydoc.txt</code></pre>\n</div></div>\n</dd>\n<dt class=\"hdlist1\">\nAttribute lists\n</dt>\n<dd>\n<p>\n    If any named attribute entries are present then all string\n    attribute values must be quoted.  For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>[\"Desktop screenshot\",width=32]</code></pre>\n</div></div>\n</dd>\n</dl></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"X90\">36.5. Combining separate documents</h3>\n<div class=\"paragraph\"><p>You have a number of stand-alone AsciiDoc documents that you want to\nprocess as a single document. Simply processing them with a series of\n<code>include</code> macros won’t work because the documents contain (level 0)\ndocument titles.  The solution is to create a top level wrapper\ndocument and use the <code>leveloffset</code> attribute to push them all down one\nlevel. For example:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>Combined Document Title\n=======================\n\n// Push titles down one level.\n:leveloffset: 1\n\ninclude::document1.txt[]\n\n// Return to normal title levels.\n:leveloffset: 0\n\nA Top Level Section\n-------------------\nLorum ipsum.\n\n// Push titles down one level.\n:leveloffset: 1\n\ninclude::document2.txt[]\n\ninclude::document3.txt[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The document titles in the included documents will now be processed as\nlevel 1 section titles, level 1 sections as level 2 sections and so\non.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nPut a blank line between the <code>include</code> macro lines to ensure the\n  title of the included document is not seen as part of the last\n  paragraph of the previous document.\n</p>\n</li>\n<li>\n<p>\nYou won’t want non-title document header lines (for example, Author\n  and Revision lines) in the included files — conditionally exclude\n  them if they are necessary for stand-alone processing.\n</p>\n</li>\n</ul></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_processing_document_sections_separately\">36.6. Processing document sections separately</h3>\n<div class=\"paragraph\"><p>You have divided your AsciiDoc document into separate files (one per\ntop level section) which are combined and processed with the following\ntop level document:</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>Combined Document Title\n=======================\nJoe Bloggs\nv1.0, 12-Aug-03\n\ninclude::section1.txt[]\n\ninclude::section2.txt[]\n\ninclude::section3.txt[]</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You also want to process the section files as separate documents.\nThis is easy because <code>asciidoc(1)</code> will quite happily process\n<code>section1.txt</code>, <code>section2.txt</code> and <code>section3.txt</code> separately — the\nresulting output documents contain the section but have no document\ntitle.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_processing_document_snippets\">36.7. Processing document snippets</h3>\n<div class=\"paragraph\"><p>Use the <code>-s</code> (<code>--no-header-footer</code>) command-line option to suppress\nheader and footer output, this is useful if the processed output is to\nbe included in another file. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -sb docbook section1.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p><code>asciidoc(1)</code> can be used as a filter, so you can pipe chunks of text\nthrough it. For example:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ echo 'Hello *World!*' | asciidoc -s -\n&lt;div class=\"paragraph\"&gt;&lt;p&gt;Hello &lt;strong&gt;World!&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_badges_in_html_page_footers\">36.8. Badges in HTML page footers</h3>\n<div class=\"paragraph\"><p>See the <code>[footer]</code> section in the AsciiDoc distribution <code>xhtml11.conf</code>\nconfiguration file.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_pretty_printing_asciidoc_output\">36.9. Pretty printing AsciiDoc output</h3>\n<div class=\"paragraph\"><p>If the indentation and layout of the <code>asciidoc(1)</code> output is not to your\nliking you can:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nChange the indentation and layout of configuration file markup\n   template sections. The <code>{empty}</code> attribute is useful for outputting\n   trailing blank lines in markup templates.\n</p>\n</li>\n<li>\n<p>\nUse Dave Raggett’s <a href=\"http://tidy.sourceforge.net/\">HTML Tidy</a> program\n   to tidy <code>asciidoc(1)</code> output. Example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -b docbook -o - mydoc.txt | tidy -indent -xml &gt;mydoc.xml</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nUse the <code>xmllint(1)</code> format option. Example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ xmllint --format mydoc.xml</code></pre>\n</div></div>\n</li>\n</ol></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_supporting_minor_docbook_dtd_variations\">36.10. Supporting minor DocBook DTD variations</h3>\n<div class=\"paragraph\"><p>The conditional inclusion of DocBook SGML markup at the end of the\ndistribution <code>docbook45.conf</code> file illustrates how to support minor\nDTD variations. The included sections override corresponding entries\nfrom preceding sections.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_creating_stand_alone_html_documents\">36.11. Creating stand-alone HTML documents</h3>\n<div class=\"paragraph\"><p>If you’ve ever tried to send someone an HTML document that includes\nstylesheets and images you’ll know that it’s not as straight-forward\nas exchanging a single file.  AsciiDoc has options to create\nstand-alone documents containing embedded images, stylesheets and\nscripts.  The following AsciiDoc command creates a single file\ncontaining <a href=\"#X66\">embedded images</a>, CSS stylesheets, and JavaScript\n(for table of contents and footnotes):</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a data-uri -a icons -a toc -a max-width=55em article.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>You can view the HTML file here: <a href=\"http://www.methods.co.nz/asciidoc/article-standalone.html\">http://www.methods.co.nz/asciidoc/article-standalone.html</a></p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_shipping_stand_alone_asciidoc_source\">36.12. Shipping stand-alone AsciiDoc source</h3>\n<div class=\"paragraph\"><p>Reproducing presentation documents from someone else’s source has one\nmajor problem: unless your configuration files are the same as the\ncreator’s you won’t get the same output.</p></div>\n<div class=\"paragraph\"><p>The solution is to create a single backend specific configuration file\nusing the <code>asciidoc(1)</code> <code>-c</code> (<code>--dump-conf</code>) command-line option. You\nthen ship this file along with the AsciiDoc source document plus the\n<code>asciidoc.py</code> script. The only end user requirement is that they have\nPython installed (and that they consider you a trusted source). This\nexample creates a composite HTML configuration file for <code>mydoc.txt</code>:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -cb xhtml11 mydoc.txt &gt; mydoc-xhtml11.conf</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Ship <code>mydoc.txt</code>, <code>mydoc-html.conf</code>, and <code>asciidoc.py</code>. With\nthese three files (and a Python interpreter) the recipient can\nregenerate the HMTL output:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ ./asciidoc.py -eb xhtml11 mydoc.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>-e</code> (<code>--no-conf</code>) option excludes the use of implicit\nconfiguration files, ensuring that only entries from the\n<code>mydoc-html.conf</code> configuration are used.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_inserting_blank_space\">36.13. Inserting blank space</h3>\n<div class=\"paragraph\"><p>Adjust your style sheets to add the correct separation between block\nelements. Inserting blank paragraphs containing a single non-breaking\nspace character <code>{nbsp}</code> works but is an ad hoc solution compared\nto using style sheets.</p></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_closing_open_sections\">36.14. Closing open sections</h3>\n<div class=\"paragraph\"><p>You can close off section tags up to level <code>N</code> by calling the\n<code>eval::[Section.setlevel(N)]</code> system macro. This is useful if you\nwant to include a section composed of raw markup. The following\nexample includes a DocBook glossary division at the top section level\n(level 0):</p></div>\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>ifdef::basebackend-docbook[]\n\neval::[Section.setlevel(0)]\n\n+++++++++++++++++++++++++++++++\n&lt;glossary&gt;\n  &lt;title&gt;Glossary&lt;/title&gt;\n  &lt;glossdiv&gt;\n  ...\n  &lt;/glossdiv&gt;\n&lt;/glossary&gt;\n+++++++++++++++++++++++++++++++\nendif::basebackend-docbook[]</code></pre>\n</div></div>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_validating_output_files\">36.15. Validating output files</h3>\n<div class=\"paragraph\"><p>Use <code>xmllint(1)</code> to check the AsciiDoc generated markup is both well\nformed and valid. Here are some examples:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ xmllint --nonet --noout --valid docbook-file.xml\n$ xmllint --nonet --noout --valid xhtml11-file.html\n$ xmllint --nonet --noout --valid --html html4-file.html</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>The <code>--valid</code> option checks the file is valid against the document\ntype’s DTD, if the DTD is not installed in your system’s catalog then\nit will be fetched from its Internet location. If you omit the\n<code>--valid</code> option the document will only be checked that it is well\nformed.</p></div>\n<div class=\"paragraph\"><p>The online <a href=\"http://validator.w3.org/#validate_by_uri+with_options\">W3C\nMarkup Validation Service</a> is the defacto standard when it comes to\nvalidating HTML (it validates all HTML standards including HTML5).</p></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_glossary\">Glossary</h2>\n<div class=\"sectionbody\">\n<div class=\"dlist glossary\"><dl>\n<dt>\n<a id=\"X8\"></a> Block element\n</dt>\n<dd>\n<p>\n    An AsciiDoc block element is a document entity composed of one or\n    more whole lines of text.\n</p>\n</dd>\n<dt>\n<a id=\"X34\"></a> Inline element\n</dt>\n<dd>\n<p>\n    AsciiDoc inline elements occur within block element textual\n    content, they perform formatting and substitution tasks.\n</p>\n</dd>\n<dt>\nFormal element\n</dt>\n<dd>\n<p>\n    An AsciiDoc block element that has a BlockTitle. Formal elements\n    are normally listed in front or back matter, for example lists of\n    tables, examples and figures.\n</p>\n</dd>\n<dt>\nVerbatim element\n</dt>\n<dd>\n<p>\n    The word verbatim indicates that white space and line breaks in\n    the source document are to be preserved in the output document.\n</p>\n</dd>\n</dl></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_migration_notes\">Appendix A: Migration Notes</h2>\n<div class=\"sectionbody\">\n<div class=\"sect2\">\n<h3 id=\"X53\">Version 7 to version 8</h3>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nA new set of quotes has been introduced which may match inline text\n  in existing documents — if they do you’ll need to escape the\n  matched text with backslashes.\n</p>\n</li>\n<li>\n<p>\nThe index entry inline macro syntax has changed — if your documents\n  include indexes you may need to edit them.\n</p>\n</li>\n<li>\n<p>\nReplaced <code>a2x(1)</code> <code>--no-icons</code> and <code>--no-copy</code> options with their\n  negated equivalents: <code>--icons</code> and <code>--copy</code> respectively. The\n  default behavior has also changed — the use of icons and copying of\n  icon and CSS files must be specified explicitly with the <code>--icons</code>\n  and <code>--copy</code> options.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>The rationale for the changes can be found in the AsciiDoc\n<code>CHANGELOG</code>.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">If you want to disable unconstrained quotes, the new alternative\nconstrained quotes syntax and the new index entry syntax then you can\ndefine the attribute <code>asciidoc7compatible</code> (for example by using the\n<code>-a asciidoc7compatible</code> command-line option).</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X38\">Appendix B: Packager Notes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Read the <code>README</code> and <code>INSTALL</code> files (in the distribution root\ndirectory) for install prerequisites and procedures.  The distribution\n<code>Makefile.in</code> (used by <code>configure</code> to generate the <code>Makefile</code>) is the\ncanonical installation procedure.</p></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X39\">Appendix C: AsciiDoc Safe Mode</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc <em>safe mode</em> skips potentially dangerous scripted sections in\nAsciiDoc source files by inhibiting the execution of arbitrary code or\nthe inclusion of arbitrary files.</p></div>\n<div class=\"paragraph\"><p>The safe mode is disabled by default, it can be enabled with the\n<code>asciidoc(1)</code> <code>--safe</code> command-line option.</p></div>\n<div class=\"ulist\"><div class=\"title\">Safe mode constraints</div><ul>\n<li>\n<p>\n<code>eval</code>, <code>sys</code> and <code>sys2</code> executable attributes and block macros are\n  not executed.\n</p>\n</li>\n<li>\n<p>\n<code>include::&lt;filename&gt;[]</code> and <code>include1::&lt;filename&gt;[]</code> block macro\n  files must reside inside the parent file’s directory.\n</p>\n</li>\n<li>\n<p>\n<code>{include:&lt;filename&gt;}</code> executable attribute files must reside\n  inside the source document directory.\n</p>\n</li>\n<li>\n<p>\nPassthrough Blocks are dropped.\n</p>\n</li>\n</ul></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/warning.png\" alt=\"Warning\">\n</td>\n<td class=\"content\">\n<div class=\"paragraph\"><p>The safe mode is not designed to protect against unsafe AsciiDoc\nconfiguration files. Be especially careful when:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nImplementing filters.\n</p>\n</li>\n<li>\n<p>\nImplementing elements that don’t escape special characters.\n</p>\n</li>\n<li>\n<p>\nAccepting configuration files from untrusted sources.\n</p>\n</li>\n</ol></div>\n</td>\n</tr></tbody></table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_using_asciidoc_with_non_english_languages\">Appendix D: Using AsciiDoc with non-English Languages</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc can process UTF-8 character sets but there are some things\nyou need to be aware of:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nIf you are generating output documents using a DocBook toolchain\n  then you should set the AsciiDoc <code>lang</code> attribute to the appropriate\n  language (it defaults to <code>en</code> (English)). This will ensure things\n  like table of contents, figure and table captions and admonition\n  captions are output in the specified language.  For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ a2x -a lang=es doc/article.txt</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nIf you are outputting HTML directly from <code>asciidoc(1)</code> you’ll\n  need to set the various <code>*_caption</code> attributes to match your target\n  language (see the list of captions and titles in the <code>[attributes]</code>\n  section of the distribution <code>lang-*.conf</code> files). The easiest way is\n  to create a language <code>.conf</code> file (see the AsciiDoc’s <code>lang-en.conf</code>\n  file).\n</p>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">You still use the <em>NOTE</em>, <em>CAUTION</em>, <em>TIP</em>, <em>WARNING</em>,\n<em>IMPORTANT</em> captions in the AsciiDoc source, they get translated in\nthe HTML output file.</td>\n</tr></tbody></table>\n</div>\n</li>\n<li>\n<p>\n<code>asciidoc(1)</code> automatically loads configuration files named like\n  <code>lang-&lt;lang&gt;.conf</code> where <code>&lt;lang&gt;</code> is a two letter language code that\n  matches the current AsciiDoc <code>lang</code> attribute. See also\n  <a href=\"#X27\">Configuration File Names and Locations</a>.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_vim_syntax_highlighter\">Appendix E: Vim Syntax Highlighter</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Syntax highlighting is incredibly useful, in addition to making\nreading AsciiDoc documents much easier syntax highlighting also helps\nyou catch AsciiDoc syntax errors as you write your documents.</p></div>\n<div class=\"paragraph\"><p>The AsciiDoc <code>./vim/</code> distribution directory contains Vim syntax\nhighlighter and filetype detection scripts for AsciiDoc.  Syntax\nhighlighting makes it much easier to spot AsciiDoc syntax errors.</p></div>\n<div class=\"paragraph\"><p>If Vim is installed on your system the AsciiDoc installer\n(<code>install.sh</code>) will automatically install the vim scripts in the Vim\nglobal configuration directory (<code>/etc/vim</code>).</p></div>\n<div class=\"paragraph\"><p>You can also turn on syntax highlighting by adding the following line\nto the end of you AsciiDoc source files:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>// vim: set syntax=asciidoc:</code></pre>\n</div></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/tip.png\" alt=\"Tip\">\n</td>\n<td class=\"content\">Bold fonts are often easier to read, use the Vim <code>:set\nbackground=dark</code> command to set bold bright fonts.</td>\n</tr></tbody></table>\n</div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">There are a number of alternative syntax highlighters for\nvarious editors listed on the <a href=\"http://www.methods.co.nz/asciidoc/\">AsciiDoc website</a>.</td>\n</tr></tbody></table>\n</div>\n<div class=\"sect2\">\n<h3 id=\"_limitations\">Limitations</h3>\n<div class=\"paragraph\"><p>The current implementation does a reasonable job but on occasions gets\nthings wrong:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nNested quoted text formatting is highlighted according to the outer\n  format.\n</p>\n</li>\n<li>\n<p>\nIf a closing Example Block delimiter is sometimes mistaken for a\n  title underline. A workaround is to insert a blank line before the\n  closing delimiter.\n</p>\n</li>\n<li>\n<p>\nLines within a paragraph starting with equals characters may be\n  highlighted as single-line titles.\n</p>\n</li>\n<li>\n<p>\nLines within a paragraph beginning with a period may be highlighted\n  as block titles.\n</p>\n</li>\n</ul></div>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X74\">Appendix F: Attribute Options</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>Here is the list of predefined <a href=\"#X75\">attribute list options</a>:</p></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<colgroup><col width=\"18%\">\n<col width=\"18%\">\n<col width=\"18%\">\n<col width=\"45%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Option</th>\n<th valign=\"top\" align=\"left\">Backends</th>\n<th valign=\"top\" align=\"left\">AsciiDoc Elements</th>\n<th valign=\"top\" align=\"left\">Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>autowidth</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5, html4</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">table</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The column widths are determined by the browser, not the AsciiDoc\n<em>cols</em> attribute. If there is no <em>width</em> attribute the table width is\nalso left up to the browser.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>unbreakable</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">block elements</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>unbreakable</em> attempts to keep the block element together on a single\nprinted page c.f. the <em>breakable</em> and <em>unbreakable</em> docbook (XSL/FO)\noptions below.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>breakable, unbreakable</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook (XSL/FO)</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">table, example, block image</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The <em>breakable</em> options allows block elements to break across page\nboundaries; <em>unbreakable</em> attempts to keep the block element together\non a single page. If neither option is specified the default XSL\nstylesheet behavior prevails.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>compact</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook, xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">bulleted list, numbered list</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Minimizes vertical space in the list</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>footer</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook, xhtml11, html5, html4</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">table</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The last row of the table is rendered as a footer.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>header</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook, xhtml11, html5, html4</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">table</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">The first row of the table is rendered as a header.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>pgwide</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook (XSL/FO)</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">table, block image, horizontal labeled list</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Specifies that the element should be rendered across the full text\nwidth of the page irrespective of the current indentation.</p></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>strong</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5, html4</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">labeled lists</p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">Emboldens label text.</p></td>\n</tr>\n</tbody>\n</table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X82\">Appendix G: Diagnostics</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>The <code>asciidoc(1)</code> <code>--verbose</code> command-line option prints additional\ninformation to stderr: files processed, filters processed, warnings,\nsystem attribute evaluation.</p></div>\n<div class=\"paragraph\"><p>A special attribute named <em>trace</em> enables the output of\nelement-by-element diagnostic messages detailing output markup\ngeneration to stderr.  The <em>trace</em> attribute can be set on the\ncommand-line or from within the document using <a href=\"#X18\">Attribute Entries</a> (the latter allows tracing to be confined to specific\nportions of the document).</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nTrace messages print the source file name and line number and the\n  trace name followed by related markup.\n</p>\n</li>\n<li>\n<p>\n<em>trace names</em> are normally the names of AsciiDoc elements (see the\n  list below).\n</p>\n</li>\n<li>\n<p>\nThe trace message is only printed if the <em>trace</em> attribute value\n  matches the start of a <em>trace name</em>. The <em>trace</em> attribute value can\n  be any Python regular expression.  If a trace value is not specified\n  all trace messages will be printed (this can result in large amounts\n  of output if applied to the whole document).\n</p>\n</li>\n<li>\n<p>\nIn the case of inline substitutions:\n</p>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe text before and after the substitution is printed; the before\n    text is preceded by a line containing <code>&lt;&lt;&lt;</code> and the after text by\n    a line containing <code>&gt;&gt;&gt;</code>.\n</p>\n</li>\n<li>\n<p>\nThe <em>subs</em> trace value is an alias for all inline substitutions.\n</p>\n</li>\n</ul></div>\n</li>\n</ul></div>\n<div class=\"literalblock\">\n<div class=\"title\">Trace names</div>\n<div class=\"content\">\n<pre><code>&lt;blockname&gt; block close\n&lt;blockname&gt; block open\n&lt;subs&gt;\ndropped line (a line containing an undefined attribute reference).\nfloating title\nfooter\nheader\nlist close\nlist entry close\nlist entry open\nlist item close\nlist item open\nlist label close\nlist label open\nlist open\nmacro block (a block macro)\nname (man page NAME section)\nparagraph\npreamble close\npreamble open\npush blockname\npop blockname\nsection close\nsection open: level &lt;level&gt;\nsubs (all inline substitutions)\ntable</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>Where:</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\n<code>&lt;level&gt;</code> is section level number <em>0…4</em>.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;blockname&gt;</code> is a delimited block name: <em>comment</em>, <em>sidebar</em>,\n  <em>open</em>, <em>pass</em>, <em>listing</em>, <em>literal</em>, <em>quote</em>, <em>example</em>.\n</p>\n</li>\n<li>\n<p>\n<code>&lt;subs&gt;</code> is an inline substitution type:\n  <em>specialcharacters</em>,<em>quotes</em>,<em>specialwords</em>, <em>replacements</em>,\n  <em>attributes</em>,<em>macros</em>,<em>callouts</em>, <em>replacements2</em>, <em>replacements3</em>.\n</p>\n</li>\n</ul></div>\n<div class=\"paragraph\"><p>Command-line examples:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nTrace the entire document.\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a trace mydoc.txt</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nTrace messages whose names start with <code>quotes</code> or <code>macros</code>:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a 'trace=quotes|macros'  mydoc.txt</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nPrint the first line of each trace message:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a trace mydoc.txt 2&gt;&amp;1 | grep ^TRACE:</code></pre>\n</div></div>\n</li>\n</ol></div>\n<div class=\"paragraph\"><p>Attribute Entry examples:</p></div>\n<div class=\"olist arabic\"><ol class=\"arabic\">\n<li>\n<p>\nBegin printing all trace messages:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:trace:</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nPrint only matched trace messages:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:trace: quotes|macros</code></pre>\n</div></div>\n</li>\n<li>\n<p>\nTurn trace messages off:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:trace!:</code></pre>\n</div></div>\n</li>\n</ol></div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"X88\">Appendix H: Backend Attributes</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>This table contains a list of optional attributes that influence the\ngenerated outputs.</p></div>\n<div class=\"tableblock\">\n<table rules=\"all\" frame=\"hsides\" cellpadding=\"4\" cellspacing=\"0\" width=\"100%\">\n<colgroup><col width=\"14%\">\n<col width=\"14%\">\n<col width=\"71%\">\n</colgroup><thead>\n<tr>\n<th valign=\"top\" align=\"left\">Name </th>\n<th valign=\"top\" align=\"left\">Backends </th>\n<th valign=\"top\" align=\"left\">Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>badges</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Link badges (<em>XHTML 1.1</em> and <em>CSS</em>) in document footers. By default\nbadges are omitted (<em>badges</em> is undefined).</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">The path names of images, icons and scripts are relative path\nnames to the output document not the source document.</td>\n</tr></tbody></table>\n</div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>data-uri</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Embed images using the <a href=\"#X66\">data: uri scheme</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>css-signature</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Set a <em>CSS signature</em> for the document (sets the <em>id</em> attribute of the\nHTML <em>body</em> element). CSS signatures provide a mechanism that allows\nusers to personalize the document appearance. The term <em>CSS signature</em>\nwas <a href=\"http://archivist.incutio.com/viewlist/css-discuss/13291\">coined by\nEric Meyer</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>disable-javascript</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>If the <code>disable-javascript</code> attribute is defined the <code>asciidoc.js</code>\nJavaScript is not embedded or linked to the output document.  By\ndefault AsciiDoc automatically embeds or links the <code>asciidoc.js</code>\nJavaScript to the output document. The script dynamically generates\n<a href=\"#X91\">table of contents</a> and <a href=\"#X92\">footnotes</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X97\"></a> docinfo, docinfo1, docinfo2</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">All backends</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>These three attributes control which <a href=\"#X87\">document information files</a> will be included in the the header of the output file:</p></div>\n<div class=\"dlist\"><dl>\n<dt class=\"hdlist1\">\ndocinfo\n</dt>\n<dd>\n<p>\nInclude <code>&lt;filename&gt;-docinfo.&lt;ext&gt;</code>\n</p>\n</dd>\n<dt class=\"hdlist1\">\ndocinfo1\n</dt>\n<dd>\n<p>\nInclude <code>docinfo.&lt;ext&gt;</code>\n</p>\n</dd>\n<dt class=\"hdlist1\">\ndocinfo2\n</dt>\n<dd>\n<p>\nInclude <code>docinfo.&lt;ext&gt;</code> and <code>&lt;filename&gt;-docinfo.&lt;ext&gt;</code>\n</p>\n</dd>\n</dl></div>\n<div class=\"paragraph\"><p>Where <code>&lt;filename&gt;</code> is the file name (sans extension) of the AsciiDoc\ninput file and <code>&lt;ext&gt;</code> is <code>.html</code> for HTML outputs or <code>.xml</code> for\nDocBook outputs. If the input file is the standard input then the\noutput file name is used. The following example will include the\n<code>mydoc-docinfo.xml</code> docinfo file in the DocBook <code>mydoc.xml</code> output\nfile:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a docinfo -b docbook mydoc.txt</code></pre>\n</div></div>\n<div class=\"paragraph\"><p>This next example will include <code>docinfo.html</code> and <code>mydoc-docinfo.html</code>\ndocinfo files in the HTML output file:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a docinfo2 -b html4 mydoc.txt</code></pre>\n</div></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X54\"></a>encoding</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Set the input and output document character set encoding. For example\nthe <code>--attribute encoding=ISO-8859-1</code> command-line option will set the\ncharacter set encoding to <code>ISO-8859-1</code>.</p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nThe default encoding is UTF-8.\n</p>\n</li>\n<li>\n<p>\nThis attribute specifies the character set in the output document.\n</p>\n</li>\n<li>\n<p>\nThe encoding name must correspond to a Python codec name or alias.\n</p>\n</li>\n<li>\n<p>\nThe <em>encoding</em> attribute can be set using an AttributeEntry inside\n  the document header. For example:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>:encoding: ISO-8859-1</code></pre>\n</div></div>\n</li>\n</ul></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X45\"></a>icons</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11, html5</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Link admonition paragraph and admonition block icon images and badge\nimages. By default <em>icons</em> is undefined and text is used in place of\nicon images.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X44\"></a>iconsdir</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The name of the directory containing linked admonition icons,\nnavigation icons and the <code>callouts</code> sub-directory (the <code>callouts</code>\nsub-directory contains <a href=\"#X105\">callout</a> number images). <em>iconsdir</em>\ndefaults to <code>./images/icons</code>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>imagesdir</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>If this attribute is defined it is prepended to the target image file\nname paths in inline and block image macros.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>keywords, description, title</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The <em>keywords</em> and <em>description</em> attributes set the correspondingly\nnamed HTML meta tag contents; the <em>title</em> attribute sets the HTML\ntitle tag contents.  Their principle use is for SEO (Search Engine\nOptimisation).  All three are optional, but if they are used they must\nappear in the document header (or on the command-line). If <em>title</em> is\nnot specified the AsciiDoc document title is used.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>linkcss</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Link CSS stylesheets and JavaScripts. By default <em>linkcss</em> is\nundefined in which case stylesheets and scripts are automatically\nembedded in the output document.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X103\"></a>max-width</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Set the document maximum display width (sets the <em>body</em> element CSS\n<em>max-width</em> property).</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>numbered</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html4, html5, xhtml11, docbook (XSL Stylesheets)</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Adds section numbers to section titles. The <em>docbook</em> backend ignores\n<em>numbered</em> attribute entries after the document header.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>plaintext</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">All backends</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>If this global attribute is defined all inline substitutions are\nsuppressed and block indents are retained.  This option is useful when\ndealing with large amounts of imported plain text.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>quirks</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Include the <code>xhtml11-quirks.conf</code> configuration file and\n<code>xhtml11-quirks.css</code> <a href=\"#X35\">stylesheet</a> to work around IE6 browser\nincompatibilities. This feature is deprecated and its use is\ndiscouraged — documents are still viewable in IE6 without it.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>revremark</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>A short summary of changes in this document revision. Must be defined\nprior to the first document section. The document also needs to be\ndated to output this attribute.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>scriptsdir</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The name of the directory containing linked JavaScripts.\nSee <a href=\"#X35\">HTML stylesheets and JavaScript locations</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>sgml</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">docbook45</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The <code>--backend=docbook45</code> command-line option produces DocBook 4.5\nXML.  You can produce the older DocBook SGML format using the\n<code>--attribute sgml</code> command-line option.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>stylesdir</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The name of the directory containing linked or embedded\n<a href=\"#X35\">stylesheets</a>.\nSee <a href=\"#X35\">HTML stylesheets and JavaScript locations</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>stylesheet</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>The file name of an optional additional CSS <a href=\"#X35\">stylesheet</a>.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>theme</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Use alternative stylesheet (see <a href=\"#X35\">Stylesheets</a>).</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em><a id=\"X91\"></a>toc</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11, docbook (XSL Stylesheets)</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Adds a table of contents to the start of an article or book document.\nThe <code>toc</code> attribute can be specified using the <code>--attribute toc</code>\ncommand-line option or a <code>:toc:</code> attribute entry in the document\nheader. The <em>toc</em> attribute is defined by default when the <em>docbook</em>\nbackend is used. To disable table of contents generation undefine the\n<em>toc</em> attribute by putting a <code>:toc!:</code> attribute entry in the document\nheader or from the command-line with an <code>--attribute toc!</code> option.</p></div>\n<div class=\"paragraph\"><p><strong>xhtml11 and html5 backends</strong></p></div>\n<div class=\"ulist\"><ul>\n<li>\n<p>\nJavaScript needs to be enabled in your browser.\n</p>\n</li>\n<li>\n<p>\nThe following example generates a numbered table of contents using a\n  JavaScript embedded in the <code>mydoc.html</code> output document:\n</p>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a toc -a numbered mydoc.txt</code></pre>\n</div></div>\n</li>\n</ul></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>toc2</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Adds a scrollable table of contents in the left hand margin of an\narticle or book document. Use the <em>max-width</em> attribute to change the\ncontent width. In all other respects behaves the same as the <em>toc</em>\nattribute.</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>toc-placement</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>When set to <em>auto</em> (the default value) <code>asciidoc(1)</code> will place the\ntable of contents in the document header. When <em>toc-placement</em> is set\nto <em>manual</em> the TOC can be positioned anywhere in the document by\nplacing the <code>toc::[]</code> block macro at the point you want the TOC to\nappear.</p></div>\n<div class=\"admonitionblock\">\n<table><tbody><tr>\n<td class=\"icon\">\n<img src=\"asciidoc-userguide_files/note.png\" alt=\"Note\">\n</td>\n<td class=\"content\">If you use <em>toc-placement</em> then you also have to define the\n<a href=\"#X91\">toc</a> attribute.</td>\n</tr></tbody></table>\n</div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>toc-title</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Sets the table of contents title (defaults to <em>Table of Contents</em>).</p></div></div></td>\n</tr>\n<tr>\n<td valign=\"top\" align=\"left\"><p class=\"table\"><em>toclevels</em></p></td>\n<td valign=\"top\" align=\"left\"><p class=\"table\">html5, xhtml11</p></td>\n<td valign=\"top\" align=\"left\"><div><div class=\"paragraph\"><p>Sets the number of title levels (1..4) reported in the table of\ncontents (see the <em>toc</em> attribute above). Defaults to 2 and must be\nused with the <em>toc</em> attribute. Example usage:</p></div>\n<div class=\"literalblock\">\n<div class=\"content\">\n<pre><code>$ asciidoc -a toc -a toclevels=3 doc/asciidoc.txt</code></pre>\n</div></div></div></td>\n</tr>\n</tbody>\n</table>\n</div>\n</div>\n</div>\n<div class=\"sect1\">\n<h2 id=\"_license\">Appendix I: License</h2>\n<div class=\"sectionbody\">\n<div class=\"paragraph\"><p>AsciiDoc is free software; you can redistribute it and/or modify it\nunder the terms of the <em>GNU General Public License version 2</em> (GPLv2)\nas published by the Free Software Foundation.</p></div>\n<div class=\"paragraph\"><p>AsciiDoc is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\nGeneral Public License version 2 for more details.</p></div>\n<div class=\"paragraph\"><p>Copyright © 2002-2011 Stuart Rackham.</p></div>\n</div>\n</div>\n</div>\n<div id=\"footnotes\"><hr></div>\n<div id=\"footer\">\n<div id=\"footer-text\">\nVersion 8.6.8<br>\nLast updated 2012-07-17 09:16:34 NZST\n</div>\n<div id=\"footer-badges\">\n<a href=\"http://validator.w3.org/check?uri=referer\">\n  <img style=\"border:0;width:88px;height:31px\" src=\"asciidoc-userguide_files/valid-xhtml11-blue.png\" alt=\"Valid XHTML 1.1\" height=\"31\" width=\"88\">\n</a>\n<a href=\"http://jigsaw.w3.org/css-validator/\">\n  <img style=\"border:0;width:88px;height:31px\" src=\"asciidoc-userguide_files/vcss-blue.gif\" alt=\"Valid CSS!\">\n</a>\n</div>\n</div>\n</div>\n</div>\n\n\n</body></html>"
  },
  {
    "path": "docs/asciidoc-userguide_files/Content.css",
    "content": "/*\nShareMeNot is licensed under the MIT license:\nhttp://www.opensource.org/licenses/mit-license.php\n\n\nCopyright (c) 2012 University of Washington\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n/* \n * Every property is !important to prevent any styles declared on the web page\n * from overriding ours.\n */\n\n.sharemenotReplacementButton {\n\tborder: none !important;\n\tcursor: pointer !important;\n\theight: auto !important;\n\twidth: auto !important;\n}\n\n.sharemenotOriginalButton {\n\tborder: none !important;\n\theight: 1.5em !important;\n}"
  },
  {
    "path": "docs/asciidoc-userguide_files/asciidoc.css",
    "content": "/* Shared CSS for AsciiDoc xhtml11 and html5 backends */\n\n/* Default font. */\nbody {\n  font-family: Georgia,serif;\n}\n\n/* Title font. */\nh1, h2, h3, h4, h5, h6,\ndiv.title, caption.title,\nthead, p.table.header,\n#toctitle,\n#author, #revnumber, #revdate, #revremark,\n#footer {\n  font-family: Arial,Helvetica,sans-serif;\n}\n\nbody {\n  margin: 1em 5% 1em 5%;\n}\n\na {\n  color: blue;\n  text-decoration: underline;\n}\na:visited {\n  color: fuchsia;\n}\n\nem {\n  font-style: italic;\n  color: navy;\n}\n\nstrong {\n  font-weight: bold;\n  color: #083194;\n}\n\nh1, h2, h3, h4, h5, h6 {\n  color: #527bbd;\n  margin-top: 1.2em;\n  margin-bottom: 0.5em;\n  line-height: 1.3;\n}\n\nh1, h2, h3 {\n  border-bottom: 2px solid silver;\n}\nh2 {\n  padding-top: 0.5em;\n}\nh3 {\n  float: left;\n}\nh3 + * {\n  clear: left;\n}\nh5 {\n  font-size: 1.0em;\n}\n\ndiv.sectionbody {\n  margin-left: 0;\n}\n\nhr {\n  border: 1px solid silver;\n}\n\np {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n\nul, ol, li > p {\n  margin-top: 0;\n}\nul > li     { color: #aaa; }\nul > li > * { color: black; }\n\n.monospaced, code, pre {\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: inherit;\n  color: navy;\n  padding: 0;\n  margin: 0;\n}\n\n\n#author {\n  color: #527bbd;\n  font-weight: bold;\n  font-size: 1.1em;\n}\n#email {\n}\n#revnumber, #revdate, #revremark {\n}\n\n#footer {\n  font-size: small;\n  border-top: 2px solid silver;\n  padding-top: 0.5em;\n  margin-top: 4.0em;\n}\n#footer-text {\n  float: left;\n  padding-bottom: 0.5em;\n}\n#footer-badges {\n  float: right;\n  padding-bottom: 0.5em;\n}\n\n#preamble {\n  margin-top: 1.5em;\n  margin-bottom: 1.5em;\n}\ndiv.imageblock, div.exampleblock, div.verseblock,\ndiv.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,\ndiv.admonitionblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\ndiv.admonitionblock {\n  margin-top: 2.0em;\n  margin-bottom: 2.0em;\n  margin-right: 10%;\n  color: #606060;\n}\n\ndiv.content { /* Block element content. */\n  padding: 0;\n}\n\n/* Block element titles. */\ndiv.title, caption.title {\n  color: #527bbd;\n  font-weight: bold;\n  text-align: left;\n  margin-top: 1.0em;\n  margin-bottom: 0.5em;\n}\ndiv.title + * {\n  margin-top: 0;\n}\n\ntd div.title:first-child {\n  margin-top: 0.0em;\n}\ndiv.content div.title:first-child {\n  margin-top: 0.0em;\n}\ndiv.content + div.title {\n  margin-top: 0.0em;\n}\n\ndiv.sidebarblock > div.content {\n  background: #ffffee;\n  border: 1px solid #dddddd;\n  border-left: 4px solid #f0f0f0;\n  padding: 0.5em;\n}\n\ndiv.listingblock > div.content {\n  border: 1px solid #dddddd;\n  border-left: 5px solid #f0f0f0;\n  background: #f8f8f8;\n  padding: 0.5em;\n}\n\ndiv.quoteblock, div.verseblock {\n  padding-left: 1.0em;\n  margin-left: 1.0em;\n  margin-right: 10%;\n  border-left: 5px solid #f0f0f0;\n  color: #888;\n}\n\ndiv.quoteblock > div.attribution {\n  padding-top: 0.5em;\n  text-align: right;\n}\n\ndiv.verseblock > pre.content {\n  font-family: inherit;\n  font-size: inherit;\n}\ndiv.verseblock > div.attribution {\n  padding-top: 0.75em;\n  text-align: left;\n}\n/* DEPRECATED: Pre version 8.2.7 verse style literal block. */\ndiv.verseblock + div.attribution {\n  text-align: left;\n}\n\ndiv.admonitionblock .icon {\n  vertical-align: top;\n  font-size: 1.1em;\n  font-weight: bold;\n  text-decoration: underline;\n  color: #527bbd;\n  padding-right: 0.5em;\n}\ndiv.admonitionblock td.content {\n  padding-left: 0.5em;\n  border-left: 3px solid #dddddd;\n}\n\ndiv.exampleblock > div.content {\n  border-left: 3px solid #dddddd;\n  padding-left: 0.5em;\n}\n\ndiv.imageblock div.content { padding-left: 0; }\nspan.image img { border-style: none; }\na.image:visited { color: white; }\n\ndl {\n  margin-top: 0.8em;\n  margin-bottom: 0.8em;\n}\ndt {\n  margin-top: 0.5em;\n  margin-bottom: 0;\n  font-style: normal;\n  color: navy;\n}\ndd > *:first-child {\n  margin-top: 0.1em;\n}\n\nul, ol {\n    list-style-position: outside;\n}\nol.arabic {\n  list-style-type: decimal;\n}\nol.loweralpha {\n  list-style-type: lower-alpha;\n}\nol.upperalpha {\n  list-style-type: upper-alpha;\n}\nol.lowerroman {\n  list-style-type: lower-roman;\n}\nol.upperroman {\n  list-style-type: upper-roman;\n}\n\ndiv.compact ul, div.compact ol,\ndiv.compact p, div.compact p,\ndiv.compact div, div.compact div {\n  margin-top: 0.1em;\n  margin-bottom: 0.1em;\n}\n\ntfoot {\n  font-weight: bold;\n}\ntd > div.verse {\n  white-space: pre;\n}\n\ndiv.hdlist {\n  margin-top: 0.8em;\n  margin-bottom: 0.8em;\n}\ndiv.hdlist tr {\n  padding-bottom: 15px;\n}\ndt.hdlist1.strong, td.hdlist1.strong {\n  font-weight: bold;\n}\ntd.hdlist1 {\n  vertical-align: top;\n  font-style: normal;\n  padding-right: 0.8em;\n  color: navy;\n}\ntd.hdlist2 {\n  vertical-align: top;\n}\ndiv.hdlist.compact tr {\n  margin: 0;\n  padding-bottom: 0;\n}\n\n.comment {\n  background: yellow;\n}\n\n.footnote, .footnoteref {\n  font-size: 0.8em;\n}\n\nspan.footnote, span.footnoteref {\n  vertical-align: super;\n}\n\n#footnotes {\n  margin: 20px 0 20px 0;\n  padding: 7px 0 0 0;\n}\n\n#footnotes div.footnote {\n  margin: 0 0 5px 0;\n}\n\n#footnotes hr {\n  border: none;\n  border-top: 1px solid silver;\n  height: 1px;\n  text-align: left;\n  margin-left: 0;\n  width: 20%;\n  min-width: 100px;\n}\n\ndiv.colist td {\n  padding-right: 0.5em;\n  padding-bottom: 0.3em;\n  vertical-align: top;\n}\ndiv.colist td img {\n  margin-top: 0.3em;\n}\n\n@media print {\n  #footer-badges { display: none; }\n}\n\n#toc {\n  margin-bottom: 2.5em;\n}\n\n#toctitle {\n  color: #527bbd;\n  font-size: 1.1em;\n  font-weight: bold;\n  margin-top: 1.0em;\n  margin-bottom: 0.1em;\n}\n\ndiv.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {\n  margin-top: 0;\n  margin-bottom: 0;\n}\ndiv.toclevel2 {\n  margin-left: 2em;\n  font-size: 0.9em;\n}\ndiv.toclevel3 {\n  margin-left: 4em;\n  font-size: 0.9em;\n}\ndiv.toclevel4 {\n  margin-left: 6em;\n  font-size: 0.9em;\n}\n\nspan.aqua { color: aqua; }\nspan.black { color: black; }\nspan.blue { color: blue; }\nspan.fuchsia { color: fuchsia; }\nspan.gray { color: gray; }\nspan.green { color: green; }\nspan.lime { color: lime; }\nspan.maroon { color: maroon; }\nspan.navy { color: navy; }\nspan.olive { color: olive; }\nspan.purple { color: purple; }\nspan.red { color: red; }\nspan.silver { color: silver; }\nspan.teal { color: teal; }\nspan.white { color: white; }\nspan.yellow { color: yellow; }\n\nspan.aqua-background { background: aqua; }\nspan.black-background { background: black; }\nspan.blue-background { background: blue; }\nspan.fuchsia-background { background: fuchsia; }\nspan.gray-background { background: gray; }\nspan.green-background { background: green; }\nspan.lime-background { background: lime; }\nspan.maroon-background { background: maroon; }\nspan.navy-background { background: navy; }\nspan.olive-background { background: olive; }\nspan.purple-background { background: purple; }\nspan.red-background { background: red; }\nspan.silver-background { background: silver; }\nspan.teal-background { background: teal; }\nspan.white-background { background: white; }\nspan.yellow-background { background: yellow; }\n\nspan.big { font-size: 2em; }\nspan.small { font-size: 0.6em; }\n\nspan.underline { text-decoration: underline; }\nspan.overline { text-decoration: overline; }\nspan.line-through { text-decoration: line-through; }\n\ndiv.unbreakable { page-break-inside: avoid; }\n\n\n/*\n * xhtml11 specific\n *\n * */\n\ndiv.tableblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\ndiv.tableblock > table {\n  border: 3px solid #527bbd;\n}\nthead, p.table.header {\n  font-weight: bold;\n  color: #527bbd;\n}\np.table {\n  margin-top: 0;\n}\n/* Because the table frame attribute is overriden by CSS in most browsers. */\ndiv.tableblock > table[frame=\"void\"] {\n  border-style: none;\n}\ndiv.tableblock > table[frame=\"hsides\"] {\n  border-left-style: none;\n  border-right-style: none;\n}\ndiv.tableblock > table[frame=\"vsides\"] {\n  border-top-style: none;\n  border-bottom-style: none;\n}\n\n\n/*\n * html5 specific\n *\n * */\n\ntable.tableblock {\n  margin-top: 1.0em;\n  margin-bottom: 1.5em;\n}\nthead, p.tableblock.header {\n  font-weight: bold;\n  color: #527bbd;\n}\np.tableblock {\n  margin-top: 0;\n}\ntable.tableblock {\n  border-width: 3px;\n  border-spacing: 0px;\n  border-style: solid;\n  border-color: #527bbd;\n  border-collapse: collapse;\n}\nth.tableblock, td.tableblock {\n  border-width: 1px;\n  padding: 4px;\n  border-style: solid;\n  border-color: #527bbd;\n}\n\ntable.tableblock.frame-topbot {\n  border-left-style: hidden;\n  border-right-style: hidden;\n}\ntable.tableblock.frame-sides {\n  border-top-style: hidden;\n  border-bottom-style: hidden;\n}\ntable.tableblock.frame-none {\n  border-style: hidden;\n}\n\nth.tableblock.halign-left, td.tableblock.halign-left {\n  text-align: left;\n}\nth.tableblock.halign-center, td.tableblock.halign-center {\n  text-align: center;\n}\nth.tableblock.halign-right, td.tableblock.halign-right {\n  text-align: right;\n}\n\nth.tableblock.valign-top, td.tableblock.valign-top {\n  vertical-align: top;\n}\nth.tableblock.valign-middle, td.tableblock.valign-middle {\n  vertical-align: middle;\n}\nth.tableblock.valign-bottom, td.tableblock.valign-bottom {\n  vertical-align: bottom;\n}\n\n\n/*\n * manpage specific\n *\n * */\n\nbody.manpage h1 {\n  padding-top: 0.5em;\n  padding-bottom: 0.5em;\n  border-top: 2px solid silver;\n  border-bottom: 2px solid silver;\n}\nbody.manpage h2 {\n  border-style: none;\n}\nbody.manpage div.sectionbody {\n  margin-left: 3em;\n}\n\n@media print {\n  body.manpage div#toc { display: none; }\n}\n"
  },
  {
    "path": "docs/asciidoc-userguide_files/asciidoc.js",
    "content": "var asciidoc = {  // Namespace.\n\n/////////////////////////////////////////////////////////////////////\n// Table Of Contents generator\n/////////////////////////////////////////////////////////////////////\n\n/* Author: Mihai Bazon, September 2002\n * http://students.infoiasi.ro/~mishoo\n *\n * Table Of Content generator\n * Version: 0.4\n *\n * Feel free to use this script under the terms of the GNU General Public\n * License, as long as you do not remove or alter this notice.\n */\n\n /* modified by Troy D. Hanson, September 2006. License: GPL */\n /* modified by Stuart Rackham, 2006, 2009. License: GPL */\n\n// toclevels = 1..4.\ntoc: function (toclevels) {\n\n  function getText(el) {\n    var text = \"\";\n    for (var i = el.firstChild; i != null; i = i.nextSibling) {\n      if (i.nodeType == 3 /* Node.TEXT_NODE */) // IE doesn't speak constants.\n        text += i.data;\n      else if (i.firstChild != null)\n        text += getText(i);\n    }\n    return text;\n  }\n\n  function TocEntry(el, text, toclevel) {\n    this.element = el;\n    this.text = text;\n    this.toclevel = toclevel;\n  }\n\n  function tocEntries(el, toclevels) {\n    var result = new Array;\n    var re = new RegExp('[hH]([1-'+(toclevels+1)+'])');\n    // Function that scans the DOM tree for header elements (the DOM2\n    // nodeIterator API would be a better technique but not supported by all\n    // browsers).\n    var iterate = function (el) {\n      for (var i = el.firstChild; i != null; i = i.nextSibling) {\n        if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {\n          var mo = re.exec(i.tagName);\n          if (mo && (i.getAttribute(\"class\") || i.getAttribute(\"className\")) != \"float\") {\n            result[result.length] = new TocEntry(i, getText(i), mo[1]-1);\n          }\n          iterate(i);\n        }\n      }\n    }\n    iterate(el);\n    return result;\n  }\n\n  var toc = document.getElementById(\"toc\");\n  if (!toc) {\n    return;\n  }\n\n  // Delete existing TOC entries in case we're reloading the TOC.\n  var tocEntriesToRemove = [];\n  var i;\n  for (i = 0; i < toc.childNodes.length; i++) {\n    var entry = toc.childNodes[i];\n    if (entry.nodeName.toLowerCase() == 'div'\n     && entry.getAttribute(\"class\")\n     && entry.getAttribute(\"class\").match(/^toclevel/))\n      tocEntriesToRemove.push(entry);\n  }\n  for (i = 0; i < tocEntriesToRemove.length; i++) {\n    toc.removeChild(tocEntriesToRemove[i]);\n  }\n  \n  // Rebuild TOC entries.\n  var entries = tocEntries(document.getElementById(\"content\"), toclevels);\n  for (var i = 0; i < entries.length; ++i) {\n    var entry = entries[i];\n    if (entry.element.id == \"\")\n      entry.element.id = \"_toc_\" + i;\n    var a = document.createElement(\"a\");\n    a.href = \"#\" + entry.element.id;\n    a.appendChild(document.createTextNode(entry.text));\n    var div = document.createElement(\"div\");\n    div.appendChild(a);\n    div.className = \"toclevel\" + entry.toclevel;\n    toc.appendChild(div);\n  }\n  if (entries.length == 0)\n    toc.parentNode.removeChild(toc);\n},\n\n\n/////////////////////////////////////////////////////////////////////\n// Footnotes generator\n/////////////////////////////////////////////////////////////////////\n\n/* Based on footnote generation code from:\n * http://www.brandspankingnew.net/archive/2005/07/format_footnote.html\n */\n\nfootnotes: function () {\n  // Delete existing footnote entries in case we're reloading the footnodes.\n  var i;\n  var noteholder = document.getElementById(\"footnotes\");\n  if (!noteholder) {\n    return;\n  }\n  var entriesToRemove = [];\n  for (i = 0; i < noteholder.childNodes.length; i++) {\n    var entry = noteholder.childNodes[i];\n    if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute(\"class\") == \"footnote\")\n      entriesToRemove.push(entry);\n  }\n  for (i = 0; i < entriesToRemove.length; i++) {\n    noteholder.removeChild(entriesToRemove[i]);\n  }\n\n  // Rebuild footnote entries.\n  var cont = document.getElementById(\"content\");\n  var spans = cont.getElementsByTagName(\"span\");\n  var refs = {};\n  var n = 0;\n  for (i=0; i<spans.length; i++) {\n    if (spans[i].className == \"footnote\") {\n      n++;\n      var note = spans[i].getAttribute(\"data-note\");\n      if (!note) {\n        // Use [\\s\\S] in place of . so multi-line matches work.\n        // Because JavaScript has no s (dotall) regex flag.\n        note = spans[i].innerHTML.match(/\\s*\\[([\\s\\S]*)]\\s*/)[1];\n        spans[i].innerHTML =\n          \"[<a id='_footnoteref_\" + n + \"' href='#_footnote_\" + n +\n          \"' title='View footnote' class='footnote'>\" + n + \"</a>]\";\n        spans[i].setAttribute(\"data-note\", note);\n      }\n      noteholder.innerHTML +=\n        \"<div class='footnote' id='_footnote_\" + n + \"'>\" +\n        \"<a href='#_footnoteref_\" + n + \"' title='Return to text'>\" +\n        n + \"</a>. \" + note + \"</div>\";\n      var id =spans[i].getAttribute(\"id\");\n      if (id != null) refs[\"#\"+id] = n;\n    }\n  }\n  if (n == 0)\n    noteholder.parentNode.removeChild(noteholder);\n  else {\n    // Process footnoterefs.\n    for (i=0; i<spans.length; i++) {\n      if (spans[i].className == \"footnoteref\") {\n        var href = spans[i].getElementsByTagName(\"a\")[0].getAttribute(\"href\");\n        href = href.match(/#.*/)[0];  // Because IE return full URL.\n        n = refs[href];\n        spans[i].innerHTML =\n          \"[<a href='#_footnote_\" + n +\n          \"' title='View footnote' class='footnote'>\" + n + \"</a>]\";\n      }\n    }\n  }\n},\n\ninstall: function(toclevels) {\n  var timerId;\n\n  function reinstall() {\n    asciidoc.footnotes();\n    if (toclevels) {\n      asciidoc.toc(toclevels);\n    }\n  }\n\n  function reinstallAndRemoveTimer() {\n    clearInterval(timerId);\n    reinstall();\n  }\n\n  timerId = setInterval(reinstall, 500);\n  if (document.addEventListener)\n    document.addEventListener(\"DOMContentLoaded\", reinstallAndRemoveTimer, false);\n  else\n    window.onload = reinstallAndRemoveTimer;\n}\n\n}\n"
  },
  {
    "path": "docs/asciidoc-userguide_files/layout2.css",
    "content": "body {\n  margin: 0;\n}\n\n#layout-menu-box {\n  position: fixed;\n  left: 0px;\n  top: 0px;\n  width: 160px;\n  height: 100%;\n  z-index: 1;\n  background-color: #f4f4f4;\n}\n\n#layout-content-box {\n  position: relative;\n  margin-left: 160px;\n  background-color: white;\n}\n\nh1 {\n  margin-top: 0.5em;\n}\n\n#layout-banner {\n  color: white;\n  background-color: #73a0c5;\n  font-family: Arial,Helvetica,sans-serif;\n  text-align: left;\n  padding: 0.8em 20px;\n}\n\n#layout-title {\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: 3.5em;\n  font-weight: bold;\n  letter-spacing: 0.2em;\n  margin: 0;\n}\n\n#layout-description {\n  font-size: 1.2em;\n  letter-spacing: 0.1em;\n}\n\n#layout-menu {\n  height: 100%;\n  border-right: 3px solid #eeeeee;\n  padding-top: 0.8em;\n  padding-left: 15px;\n  padding-right: 0.8em;\n  font-size: 1.0em;\n  font-family: Arial,Helvetica,sans-serif;\n  font-weight: bold;\n}\n#layout-menu a {\n  line-height: 2em;\n  margin-left: 0.5em;\n}\n#layout-menu a:link, #layout-menu a:visited, #layout-menu a:hover {\n  color: #527bbd;\n  text-decoration: none;\n}\n#layout-menu a:hover {\n  color: navy;\n  text-decoration: none;\n}\n#layout-menu #page-source {\n  border-top: 2px solid silver;\n  margin-top: 0.2em;\n}\n\n#layout-content {\n  padding-top: 0.2em;\n  padding-left: 1.0em;\n  padding-right: 0.4em;\n}\n\n@media print {\n  #layout-banner-box { display: none; }\n  #layout-menu-box { display: none; }\n  #layout-content-box { margin-top: 0; margin-left: 0; }\n}\n"
  },
  {
    "path": "docs/example_book.txt",
    "content": "Book Title Goes Here\n====================\nAuthor's Name\nv1.0, 2003-12\n:doctype: book\n\n\n[dedication]\nExample Dedication\n------------------\nOptional dedication.\n\nThis document is an AsciiDoc book skeleton containing briefly\nannotated example elements plus a couple of example index entries and\nfootnotes.\n\nBooks are normally used to generate DocBook markup and the titles of\nthe preface, appendix, bibliography, glossary and index sections are\nsignificant ('specialsections').\n\n\n[preface]\nExample Preface\n---------------\nOptional preface.\n\nPreface Sub-section\n~~~~~~~~~~~~~~~~~~~\nPreface sub-section body.\n\n\nThe First Chapter\n-----------------\nChapters can contain sub-sections nested up to three deep.\nfootnote:[An example footnote.]\nindexterm:[Example index entry]\n\nChapters can have their own bibliography, glossary and index.\n\nAnd now for something completely different: ((monkeys)), lions and\ntigers (Bengal and Siberian) using the alternative syntax index\nentries.\n(((Big cats,Lions)))\n(((Big cats,Tigers,Bengal Tiger)))\n(((Big cats,Tigers,Siberian Tiger)))\nNote that multi-entry terms generate separate index entries.\n\nHere are a couple of image examples: an image:images/smallnew.png[]\nexample inline image followed by an example block image:\n\n.Tiger block image\nimage::images/tiger.png[Tiger image]\n\nFollowed by an example table:\n\n.An example table\n[width=\"60%\",options=\"header\"]\n|==============================================\n| Option          | Description\n| -a 'USER GROUP' | Add 'USER' to 'GROUP'.\n| -R 'GROUP'      | Disables access to 'GROUP'.\n|==============================================\n\n.An example example\n===============================================\nLorum ipum...\n===============================================\n\n[[X1]]\nSub-section with Anchor\n~~~~~~~~~~~~~~~~~~~~~~~\nSub-section at level 2.\n\nChapter Sub-section\n^^^^^^^^^^^^^^^^^^^\nSub-section at level 3.\n\nChapter Sub-section\n+++++++++++++++++++\nSub-section at level 4.\n\nThis is the maximum sub-section depth supported by the distributed\nAsciiDoc configuration.\nfootnote:[A second example footnote.]\n\n\nThe Second Chapter\n------------------\nAn example link to anchor at start of the <<X1,first sub-section>>.\nindexterm:[Second example index entry]\n\nAn example link to a bibliography entry <<taoup>>.\n\n\nThe Third Chapter\n-----------------\nBook chapters are at level 1 and can contain sub-sections.\n\n\n:numbered!:\n\n[appendix]\nExample Appendix\n----------------\nOne or more optional appendixes go here at section level 1.\n\nAppendix Sub-section\n~~~~~~~~~~~~~~~~~~~\nSub-section body.\n\n\n[bibliography]\nExample Bibliography\n--------------------\nThe bibliography list is a style of AsciiDoc bulleted list.\n\n[bibliography]\n.Books\n- [[[taoup]]] Eric Steven Raymond. 'The Art of Unix\n  Programming'. Addison-Wesley. ISBN 0-13-142901-9.\n- [[[walsh-muellner]]] Norman Walsh & Leonard Muellner.\n  'DocBook - The Definitive Guide'. O'Reilly & Associates. 1999.\n  ISBN 1-56592-580-7.\n\n[bibliography]\n.Articles\n- [[[abc2003]]] Gall Anonim. 'An article', Whatever. 2003.\n\n\n[glossary]\nExample Glossary\n----------------\nGlossaries are optional. Glossaries entries are an example of a style\nof AsciiDoc labeled lists.\n\n[glossary]\nA glossary term::\n  The corresponding (indented) definition.\n\nA second glossary term::\n  The corresponding (indented) definition.\n\n\n[colophon]\nExample Colophon\n----------------\nText at the end of a book describing facts about its production.\n\n\n[index]\nExample Index\n-------------\n////////////////////////////////////////////////////////////////\nThe index is normally left completely empty, it's contents being\ngenerated automatically by the DocBook toolchain.\n////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "epilogue.asciidoc",
    "content": "[appendix]\n[role=\"afterword\"]\n== Obey the Testing Goat!\n\nLet's get back to the Testing Goat.\n\n\"Groan\", I hear you say—\"Harry, the Testing Goat stopped being funny about 17 chapters ago\".\nBear with me; I'm going to use it to make a serious point.\n\n=== Testing Is Hard\n\n(((\"Testing Goat\", \"philosophy of\")))\nI think the reason the phrase \"Obey the Testing Goat\" first grabbed me\nwhen I saw it was that it spoke to the fact that testing is hard--not hard\nto do in and of itself, but hard to _stick to_, and hard to keep doing.\n\nIt always feels easier to cut corners and skip a few tests.\nAnd it's doubly hard psychologically because the payoff is so disconnected\nfrom the point at which you put in the effort.\nA test you spend time writing now doesn't reward you immediately;\nit only helps much later--perhaps months later\nwhen it saves you from introducing a bug while refactoring,\nor catches a regression when you upgrade a dependency.\nOr, perhaps it pays you back in a way that's hard to measure,\nby encouraging you to write better-designed code,\nbut you convince yourself you could have written it\njust as elegantly without tests.\n\nI myself started slipping when I was writing the\nhttps://github.com/hjwp/Book-TDD-Web-Dev-Python/tree/master/tests[test\nframework for this book].\nBeing quite a complex beast, it has tests of its own,\nbut I cut several corners. So, coverage isn't perfect, and I now regret it\nbecause it's turned out quite unwieldy and ugly\n(go on; I've open sourced it now, so you can all point and laugh).\n\n[role=\"pagebreak-before less_space\"]\n==== Keep Your CI Builds Green\n\n(((\"continuous integration (CI)\", \"tips\")))\nAnother area that takes real hard work is continuous integration.\nYou saw in <<chapter_25_CI>> that strange and unpredictable bugs\nsometimes occur in CI.\nWhen you're looking at these and thinking \"it works fine on my machine\",\nthere's a strong temptation to just ignore them...but, if you're not careful,\nyou start to tolerate a failing test suite in CI,\nand pretty soon your CI build is actually useless,\nand it feels like too much work to get it going again.\nDon't fall into that trap.\nPersist, and you'll find the reason that your test is failing,\nand you'll find a way to lock it down and make it deterministic,\nand green, again.\n\n\n==== Take Pride in Your Tests, as You Do in Your Code\n\nOne of the things that helps is(((\"tests\", \"taking pride in as in code\")))\nto stop thinking of your tests as being an incidental add-on to the \"real\" code,\nand to start thinking of them as being a part of the finished product\nthat you're building--a part that should be\njust as finely polished and just as aesthetically pleasing,\nand a part you can be justly proud of delivering...\n\nSo, do it because the Testing Goat says so.\nDo it because you know the payoff will be worth it,\neven if it's not immediate.\nDo it out of a sense of duty, or professionalism, or perfectionism,\nor sheer bloody-mindedness.\nDo it because it's a good thing to practice.\nAnd, eventually, do it because it makes software development more fun.\n\n//something about pairing?\n\n\n==== Remember to Tip the Bar Staff\n\nThis book wouldn't have been possible without the backing of my publisher,\nthe wonderful O'Reilly Media.\nIf you're reading the free edition online, I hope you'll consider\nbuying a real copy...if\nyou don't need one for yourself, then maybe as a gift for a friend?\n\n// TODO: add amazon link back in above\n\n\n=== Don't Be a Stranger!\n\nI hope you enjoyed the book.  Do get in touch and tell me what you thought!\n\nHarry\n\n* https://fosstodon.org/@hjwp\n* obeythetestinggoat@gmail.com\n\n"
  },
  {
    "path": "index.txt",
    "content": "Index\n\nA\nacceptance test (see functional tests/testing\n(FT))\nacceptance tests, 397\naesthetics (see layout and style)\nagile movement in software development, 79\nAjax, 249, 269\nALLOWED_HOSTS, 151\nAnderson, Ross, 51\nAnsible, 166, 423–426\narchitectural solutions to test problems, 402\nassertion messages, 264\nAssertionError, 14, 44, 55\nassertRegex, 83\nassertTemplateUsed, 88\nassertTrue function, 44\nasynchronous JavaScript, 272–275\nauthentication\n    backend, 285–293\n    customising, 245–247, 277\n    in Django, 282\n    login view, 281–284\n    minimum custom user model, 295–299\n    mocking (see mocks/mocking)\n    Mozilla Persona, 242\n    pre-authentication, 303–306\n    testing logout, 300\n    testing view, 278\n    tests as documentation, 297\nautomation, in deployment, 132, 157–166\n(see also deployment)\nautomation, in provisioning, 166\nB\nBash, 141\nBehavior-Driven Development (BDD) tools,\n428\nBernhardt, Gary, 399, 404\nbest practices in testing, 397\nBig Design Up Front, 79\nblack box test (see functional tests/testing (FT))\nBoolean comparisons, 290\nBootstrap, 116–125\n    jumbotron, 123\n    large inputs, 124\n    rows and columns in, 120\n    table styling, 124\nboundaries, 403\nbrowsers, 428\nbrowsers, headless, 372\n\nC\ncaching, 429\ncapture group, 101\nCI server (see continuous integration (CI))\nclass-based generic views, 413–421\nclass-based views, 413\nclean architecture, 404\ncode smell, 193, 302\ncollectstatic, 126–128\ncomments, 13, 84\ncommits, 16, 22, 27, 108\nconfiguration management tools, 167\n(see also Fabric)\ncontext managers, 177\ncontinuous integration (CI), 365–385, cdvii\n    adding required plugins, 368\n    best practices, 385\n    configuring Jenkins, 367\n    debugging with screenshots, 374–378\n    first build, 371\n    installing Jenkins, 365\n    JavaScript tests, 381–384\n    project setup, 369\n    Selenium race conditions, 378–381\n    for staging server test automation, 384\n    virtual display setup, 372–374\ncontracts, implicit, 355\ncookies, 282, 304\nCross-Site Request Forgery (CSRF) error, 51\nCSS (Cascading Style Sheets) framework, 114,\n116\n(see also Bootstrap)\nwhere Bootstrap won’t work, 124\ncutting corners, cdvii\nD\ndata migrations, 432–435\ndatabase deployment issues, 132\ndatabase location, 141\nDe-spiking, 251, 285–293\ndebugging, 19, 50, 249\nAjax, 249\nDjango debug screen, 146\nimproving error messages, 55\nin continuous integration, 374–378\nin JavaScript, 262\nstaging for, 306–310\nswitching DEBUG to false, 151\nscreenshots, for debugging, 374–378\ndependencies, and deployment, 132\ndeployment, 411\nadjusting database location, 141\nautomating, 153–155, 157–166\ndependencies and, 132\ndeploying to live, 163\nfurther reading on, 166\nkey points, 155\nto live, 225\nmigrate, 147\nNginx, 144–147\noverview, 153\nproduction-ready, 148–152\nvs. provisioning, 140\nsample script, 158–161\nsaving progress, 156\nstaging, 225, 431\nvirtualenvs, 142–144\ndeployment testing, 131–156\ndanger areas, 132\ndomain name for, 135\nmanual provisioning for hosting, 136–140\noverview, 133\ndesign (see layout and style)\nDjango, 4\nadmin site, 428\napps in, 20\nauthentication in, 245–247, 282\nclass-based views, 413–421\n(see also class-based views)\ncollectstatic, 126–128\ncustom user model, 295–299\ndebugging screen, 146, 151\nfield types, 61\nforeign key relationship, 97\nforms in (see forms)\nFormView, 414functional tests (FT) in (see functional tests/\ntesting (FT))\nand Gunicorn, 148\nLiveServerTestCase, 75\nmanagement commands, 311–314, 320\nmigrations, 60–62, 69–71, 225\nmodel adjustment in, 95\nmodel-layer validation, 175–187\nModel-View-Controller (MVC), 22\nnotifications, 427\nObject-Relational Mapper (ORM), 58–62\nPOST requests (see POST requests)\nas PythonAnywhere app, 410\nrunning, 6\nstatic files in, 121\nstatic live server case, 122\ntemplate inheritance, 118–119\ntemplates, 67–68, 88\ntest class in, 91\ntest client, 86, 91\ntest fixtures, 304\nunit testing in, 21\nURLs in, 22–27, 86, 92, 94, 100, 104, 106\nvalidation quirk, 178\nview functions in, 22, 87, 92, 103–106, 326\nand virtualenvs, 142–144\nDjango-BrowserID, 243\ndocumentation, tests as, 297\ndomain configuration, 139\ndomain names, 135\nDon’t Test Constants rule, 38\ndouble-loop TDD, 45, 323\nDRY (don’t repeat yourself), 57, 396\nduplicates, eliminating, 56, 211–221\nE\nencryption, 430\nend-to-end test (see functional tests/testing\n(FT))\nerror messages, 429\nerror pages, 428\nexception handling, 293\nexpected failure, 14, 17\nexplicit waits, 254\nexploratory coding, 195, 242\n(see also spiking)\nF\nFabric, 166, 314, 426\nconfiguration, 163\ninstalling, 157\nsample deployment script, 158–161\nFake XMLHttpRequest, 269\nFixtures Div, 231–233\nforeign key relationship, 97\nforms\nadvanced, 211–223\nautogeneration, 195\ncustomising form field input, 194\nexperimenting with, 194\nfind and replace in, 201\nModelForm, 195\nsave methods, 208\nsimple, 193–210\nthin views, 210\ntips for, 210\nusing in views, 198–207\nvalidation testing and customising, 196\nFuctional Core, Imperative Shell architecture,\n404\nfunctional tests/testing (FT), 5, 397\nautomation of (see continuous integration\n(CI))\nblank items, 169–175\ncleanup, 75–78, 94, 387\nde-duplication, 320\ndefining, 11\nfor de-spiking, 251\nand developer stupidity, 213\nfor duplicate items, 211–221\nfor evaluating third-party systems, 253\nisolation in, 75–78, 109\nin JavaScript, 234–236\nfor layout and style, 113–116, 149, 173\nmultiple users, 387, 393–396\npros and cons, 363\nin provisioning, 139\nrunning unit tests only, 78\nsafeguards with, 317\nsplitting, 171\nfor staging sites, 132, 133\nunittest model, 11–17\nvs. unit tests, 20, 303\nin views, 223\nIndex\n|\n441G\ngenerator expression, 37\nGET requests, 198, 205\nget_user, 292, 293\nGit\nrepository setup, 7–10\nreset --hard, 116\ntags, 166, 226\nglobal variables, 230\ngreedy regular expressions, 104\nGunicorn, 148–155, 165, 307, 425\nH\nheadless browsers, 372\nhelper functions/methods, 57, 172, 175, 206,\n228, 350, 390–393\nhexagonal architecture, 404\nhosting options, 136\nhosting, manual provisioning, 136–140\nI\nIdempotency, 167\nimplicit waits, 16\nin-memory model objects, 352\nintegrated tests, 351–363, 403\nvs. integration test, 342\nvs. isolated tests, 362, 397\npros and cons, 363\nvs. unit tests, 59\nintegration tests, 342, 397\nintegrity errors, 217\nisolated tests, 337, 403\n(see also test isolation)\nvs. integrated tests, 362, 397\nproblems with, 400\npros and cons, 363\nJ\nJavaScript, 227\nde-spiking in, 251\ndebug console, 262\nfunctional test (FT) building in, 234–236\njQuery and Fixtures Div, 231–233\nlinters, 230\nMVC frameworks, 429\nonload boilerplate and namespacing, 236\nQUnit, 229\n442\n| Index\nrunning tests in continuous integration,\n381–384\nspiking with, 242–256\n(see also spiking)\nin TDD Cycle, 236\ntest runner setup, 228\ntesting notes, 237\nJenkins Security, 365–384\n(see also continuous integration (CI))\nadding required plugins, 368\nconfiguring, 367\ninstalling, 365\njQuery, 231–233, 236, 237\nJSON fixtures, 304, 320\njumbotron, 123\nL\nlarge inputs, 124\nlayout and style, 113–128\nBootstrap for (see Bootstrap)\nfunctional tests (FT) for, 173\nlarge inputs, 124\noverview, 128\nrows and columns, 120\nstatic files, 121, 126–128\ntable styling, 124\nusing a CSS framework for, 116\n(see also Bootstrap)\nusing our own CSS in, 124\nwhat to functionally test for, 113\nlist comprehension, 37\nLiveServerTestCase, 75\nlog messages, 320\nlogging, 307, 320\nlogging configuration, 318–320\nM\nmanage, 6\nMeta, 196\nmeta-comments, 84\nmigrate, 147\nmigrations, 60–62, 69–71, 225, 226\n(see also data migrations)\ndatabase, 431–435\ndeleting, 97\ntesting, 431–435\nminimum viable application, 11–14, 79\nmocking library, 265MockMyID, 252\nmocks/mocking\nin Boolean comparisons, 290\ncallbacks, 272–275\nchecking call arguments, 268\nDjango ORM, 292, 302\nimplicit contracts, 355\nin JavaScript, 241, 258–275\ninitialize function test, 259–264\nInternet requests, 285–295\nfor isolation, 338–341\nmock library, 302\nMock side_effects, 339\nnamespacing, 258\nin Outside-In TDD, 331\nin Python, 278–284\nrisks, 354\nsinon.js, 265\ntesting Django login, 284\nmodel adjustments, 95–99\nmodel-layer validation, 175–187\nchanges to test, 216\nenforcing, 186\nerrors in View, 178–182\nintegrity errors, 217\nPOST requests, 183–187\npreventing duplicates, 212\nrefactoring, 175, 184–186\nunit testing, 177–178\nat views level, 218\nModel-View-Controller (MVC), 22, 429\nModelForm, 195\nMozilla Persona, 242\nMVC frameworks, 22, 429\nN\nnamespacing, 236, 258\nNginx, 138, 144–147, 149, 165, 424\nnonroot user creation, 137\nnotifications, 427\nO\nORM (Object-Relational Mapper), 58–62\nORM code, 347–350, 363\nOutside-In TDD, 323–335\nadvantages, 323\ncontroller layer, 326\ndefined, 335\nvs. Inside-Out, 323\nmodel layer, 330–333\npitfalls, 335\npresentation layer, 325\ntemplate hierarchy, 327–329\nviews layer, 326–330, 333\nP\nPaaS (Platform-as-a-Service), 136\nPage pattern, 390–393, 396\nparameters, capture group, 101\npatch decorator, 279, 290, 302\npatching, 287\npayment systems, testing for, 253\nperformance testing, 429\nPersona, 242, 252, 308–310, 429\nPhantomJS, 381–384, 428\nPlatform-as-a-Service (PaaS), 136\nPOST requests, 203\nprocessing, 52, 183–187\nredirect after, 65\nsaving to database, 62–65\nsending, 49–52, 90\nPostgres, 427\nprivate key authentication, 137\nprogramming by wishful thinking, 328, 335\n(see also Outside-In TDD)\nproperty Decorator, 334\nprovisioning, 136–140\nwith Ansible, 423–426\nautomation in, 166\nfunctional tests (FT) in, 139\noverview, 153\nvs. deployment, 140\npure unit tests (see isolated tests)\npy.test, 430\nPython\nadding to Jenkins, 369\nPythonAnywhere, 136, 409\nQ\nQuerySet, 59, 214–216\nQUnit, 229, 237, 264, 269\nR\nrace conditions, 374, 389\nRed, Green, Refactor, 56, 87, 170\nIndex\n|\n443redirects, 65, 188\nredundant code, 359\nrefactoring\nat application level, 183–186\nRed, Green, Refactor, 56, 87, 170\nremoving hard-coded URLs, 187\nand test isolation, 341, 362\ntips, 190\nunit tests, 175\nwith templates, 38–42\nRefactoring Cat, 42, 109\nrelative import, 161, 173\nrender to string, 54\nREST (Representational Site Transfer), 80\nS\nscreenshots, 411\nscripts, automated, 132\nsecret key, 160\nSecurity Engineering (Anderson), 51\nsecurity tests, 429\nsed (stream editor), 165\nSelenium, 4\nand JavaScript, 237\nbest practices, 385\nin continuous integration, 378–381\nin continuous integration, 372\nrace conditions, 389\nrace conditions in, 378–381\nupgrading, 84\nfor user interaction testing, 35–38\nwait patterns, 16, 254, 387, 389\nwaits in, 379–381, 385\nserver configuration, 155\nserver options, 137\nservers, 136–140\n(see also staging server)\nsession key, 304\nsessions, 282\nShining Panda, 369\nsinon.js, 265, 269, 272\nskips, 170\nspiking, 242–256, 275\nbrowser-ID protocol, 244\nde-spiking, 251\nfrontend and JavaScript code, 243\nlogging, 250\nserver-side authentication, 245–247\nwith JavaScript, 242\n444\n|\nIndex\nSQLite, 427\nstaging server\ncreating sessions, 311\ndebugging in, 306–310\npre-creating a session, 303–306\ntest automation with CI, 384\ntest database on, 311–317\nstaging sites, 132, 133, 135\nstatic directories, 126–128\nstatic files, 114, 121, 132, 149\nstatic folder, site-wide, 256\nstatic live server case, 122\nstring representation, 215\nstring substitutions, 101\nstyle (see layout and style)\nsuperlists, 6, 8, 20, 108\nsuperusers, 70\nsystem boundaries, 403\nsystem tests, 397\nT\ntable styling, 124\ntemplate inheritance, 118–119\ntemplate inheritance hierarchy, 327\ntemplate tag, 51\ntemplates\nfor refactoring, 38–42\nPython variables in, 53–56\nrendering items in, 67–68\nseparate, 88\ntest fixtures, 304, 320\ntest isolation, 109, 337–363\ncleanup after, 359–361\ncollaborators, 343–345\ncomplexity in, 362\nforms layer, 347–350\nfull isolation, 342\ninteractions between layers, 355\nisolated vs. integrated tests, 362\nmocks/mocking for, 338–341\nmodels layer, 351–353\nORM code, 347–350, 363\nrefactoring in, 341, 362\nviews layer, 337, 338–346, 353\ntest methods, 15\ntest organisation, 190\ntest skips, 170\ntest types, 363, 397test-driven development (TDD)\nadvanced considerations in, 397–405\ndouble-loop, 45, 323\nfurther reading on, 405\nInside-Out, 323\niterating towards new design, 84\nJava testing in, 236\nnew design implementation with, 81–84\nOutside-In, 323–335\n(see also Outside-In TDD)\nprocess flowchart, 81\nprocess recap, 45–47\ntrivialities of, 33–35\nTestCase, in Django, 21\ntesting best practices, 397\nTesting Goat, 3, 108, 109, cdvii\ntests, as documentation, 297\nthin views, 210\ntime.sleep, 50\ntracebacks, 24, 54\ntriangulation, 56\nU\nUbuntu, 137\nunit tests\narchitectural solutions for, 402\ncontext manager, 177\ndesired features of, 401\nin Django, 21\nfor simple home page, 19–31\nvs. functional tests, 303\nvs. functional tests (FT), 20\nvs. integrated tests, 59\npros and cons of, 398–401\nrefactoring, 175\nunit-test/code cycle, 29–31\nunittest, 134\nunittest model, 11–17\nUnix sockets, 150\nUpstart, 151\nURLs\ncapturing parameters in, 101\ndistinct, 100\nin Django, 22–27, 86, 92, 94, 100, 104, 106\npointing forms to, 94\nurls.py, 25–27\nuser authentication, 241\nuser creation, 291\nuser input, saving, 49–72\nuser interaction testing, 35–38\nuser stories, 17, 170\nV\nVagrant, 426\nvalidation, 169\n(see also functional tests/testing (FT))\nblank items, 169–175\nmodel-layer, 175–187\n(see also model-layer validation)\nVCS (version control system), 7–10\nview functions, in Django, 22, 87, 92, 103–106\nviews layer, 337, 338–346, 353\nmodel validation errors in, 178–182\nviews, what to test in, 223\nvirtual displays, 372\nVirtualbox, 426\nvirtualenvs, 132, 142–144\nW\nwaits, 16, 254, 379–381, 385, 387, 389\nwarnings, 15\nwatch function, 265\nwebsockets, 429\nwidgets, 194, 196\nX\nXvfb, 369, 373, 410\nY\nYAGNI, 80\nIndex\n|\n445\n"
  },
  {
    "path": "ix.html",
    "content": "<section data-type=\"index\"/>"
  },
  {
    "path": "load_toc.js",
    "content": "var httpRequest = new XMLHttpRequest();\nhttpRequest.onreadystatechange = function() {\n  if (httpRequest.readyState === XMLHttpRequest.DONE) {\n    if (httpRequest.status === 200) {\n      document.getElementById('header').innerHTML += httpRequest.responseText;\n      var subheaders = document.getElementsByClassName('sectlevel2');\n      var section;\n      for (var i=0; i<subheaders.length; i++) {\n        section = subheaders[i];\n        if (section.innerHTML.indexOf(window.location.pathname) === -1) {\n          section.style.display = 'none';\n        } else {\n          section.scrollIntoView && section.scrollIntoView();\n        }\n      }\n\n    }\n  }\n};\nhttpRequest.open('GET', 'toc.html');\nhttpRequest.send();\n\n"
  },
  {
    "path": "misc/chapters.rst",
    "content": "===================================================\r\nPART 1 - An introduction to Test-Driven Development\r\n===================================================\r\n\r\n1: What is TDD?  \r\n---------------\r\n\r\n* Some evangelism\r\n* XP and Kent Beck, Agile methods\r\n* TDD versus testing afterwards, \r\n* Outline methodology\r\n\r\ncurrent tutorial : 800 words, no illustrations. this shouldn't be more than 3-5\r\npages really\r\n\r\n--> 5 pages\r\n\r\n\r\n2: A simple example (?)\r\n-----------------------\r\n\r\neg Roman numerals, as per \"Dive Into Python.\". Slightly boring topic, everyone\r\nuses it, but it will do for now. \r\n\r\n* basic test - convert I\r\n* I-III\r\n* V + X\r\n* VI, VII, VIII\r\n* IV and IX \r\n* ...\r\n\r\n* Start simple, include exceptions. Aim to show:\r\n* unittest, how to run tests, possibly setUp, tearDown\r\n* red / green / refactor:  the test/code cycle\r\n* making the minimal change\r\n* show how design grows organically, but stays neat\r\n* the psychological effect\r\n* as per Beck. \"Am I saying you should always code like this? No.  I'm saying you should always *be able to*.\r\n\r\n\r\nIn DIP, this is pp 183-205, so 22 pages. I'd aim for half that.\r\n\r\n--> 10 pages  (maybe split this into multiple chapters?)\r\n\r\n===> total for part 1: 15 pages\r\n\r\n===================================================\r\nPART 2 - Using TDD to build a basic web application\r\n===================================================\r\n\r\n3: Our first functional test with Selenium\r\n------------------------------------------\r\n\r\n\r\n* Briefly discuss difference between functional testing (AKA acceptance\r\n  testing, integration testing, whatever) and unit testing\r\n* Write first test - Introduce Selenium, `setUp`, `tearDown`\r\n* Demonstrate we can get it to open a web browser, and navigate to a web page\r\n  eg - google.com\r\n\r\ncurrently maybe 228 lines, 1200 words.\r\n--> 5 pages\r\n\r\n\r\n\r\n4: Getting Django set-up and running\r\n------------------------------------\r\n\r\n* Change our test to look for the test server\r\n* Switch to Django LiveServerTestCase. Explain\r\n* Get the first test running and failing for a sensible reason\r\n* Create django project `django-admin.py startproject`\r\n* It worked!\r\n\r\n--> 3 pages\r\n\r\n\r\n5: A static front page\r\n----------------------\r\n\r\n* Look for \"Welcome to the Forums\", or similar\r\n* `urls.py`, `direct_to_template` ?\r\n\r\n--> 3 pages\r\n\r\n\r\n6: Super-users and the Django admin site\r\n----------------------------------------\r\n\r\n* Extend FT to try and log in\r\n* Explain the admin site\r\n* Database setup, `settings.py`, `syncdb`, `admin.py`\r\n* `runserver` to show login code\r\n* Explain difference between test database and real database\r\n* Fixtures\r\n\r\n--> 7 pages\r\n\r\n\r\n7: First unit tests and Database model \r\n--------------------------------------\r\n\r\n* Distinction between unit tests and functional tests\r\n* Extend FT to try and create a new topic\r\n* new app\r\n* `models.py`\r\n* test/code cycle\r\n\r\n--> 7 pages\r\n\r\n\r\n\r\n8: Testing a view\r\n-----------------\r\n\r\n* urls.py again\r\n* Test view as a function\r\n* assert on string contents\r\n\r\n--> 4 pages\r\n\r\n9: Django's template system\r\n----------------------------\r\n\r\n* Introduce template syntax\r\n* Keep testing as a function\r\n* The, introduce the Django Test Client\r\n\r\n--> 6 pages\r\n\r\n\r\n\r\n10: Reflections: what to test, what not to test\r\n-----------------------------------------------\r\n\r\n* \"Don't test constants\"\r\n* Test logic\r\n* Tests for simple stuff should be simple, so not much effort\r\n\r\n--> 3 pages\r\n\r\n\r\n11: Simple Forms\r\n----------------\r\n\r\n* Manually coded HTML\r\n* Refactor test classes\r\n\r\n--> 5 pages\r\n\r\n\r\n12: User Authentication\r\n-----------------------\r\n\r\n* Sign up, login/logout\r\n* Email?\r\n\r\n--> 5 pages\r\n\r\n\r\n13: More advanced forms\r\n-----------------------\r\n\r\n* Use Django Forms classes\r\n\r\n--> 6 pages\r\n\r\n\r\n14: On Refactoring\r\n------------------\r\n\r\n* Martin Fowler\r\n* Tests critical\r\n* Methodical process - explain step by step\r\n\r\n--> 4 pages\r\n\r\n\r\n15: Pagination\r\n--------------\r\n\r\n* Extend various old unit tests and FTs\r\n\r\n--> 3 pages\r\n\r\n\r\n===> total for part 2: 60 pages\r\n\r\n\r\n\r\n======================================================\r\nPART 3: More advanced testing for a more advanced site\r\n======================================================\r\n\r\n15: Notifications\r\n------------------------------\r\n\r\n* Django Notifications, for post edits\r\n--> 5 pages\r\n\r\n\r\n16: Adding style with MarkDown\r\n------------------------------\r\n\r\n* Using an external library\r\n\r\n--> 5 pages\r\n\r\n\r\n17: Switching to OAuth: Mocking\r\n-------------------------------\r\n\r\n* \"Don't store passwords\"\r\n* Discuss challenges of external dependencies\r\n\r\n--> 7 pages\r\n\r\n\r\n18: Getting Dynamic: Testing Javascript part 1\r\n----------------------------------------------\r\n\r\n* Simple input validation\r\n* Choose JS unit testing framework (probably Qunit, or YUI)\r\n\r\n--> 6 pages\r\n\r\n\r\n19: Testing Javascript part 2 - Ajax\r\n------------------------------------\r\n\r\n* Dynamic previews of post input\r\n\r\n--> 5 pages\r\n\r\n\r\n20: Getting pretty: Bootstrap\r\n-----------------------------\r\n\r\n* Bring in nicer UI elements\r\n\r\n--> 4 pages\r\n\r\n\r\n21: Getting pretty: Gravatar\r\n----------------------------\r\n\r\n* pictures for users\r\n\r\n--> 4 pages\r\n\r\n\r\n22: The bottomless web page\r\n---------------------------\r\n\r\n* More javascript bells and whistles\r\n\r\n--> 3 pages\r\n\r\n===> total for part 3: 39 pages\r\n\r\n\r\n==============================\r\nPART 4: Getting seriously sexy\r\n==============================\r\n\r\n24: Switching to a proper Database: PostgreSQL\r\n----------------------------------------------\r\n\r\n* show how Django makes this easy\r\n\r\n--> 10 pages\r\n\r\n\r\n21: Websockets and Async on the server-side\r\n-------------------------------------------\r\n\r\n* we want dynamic notifications of when new posts appear on a thread we're\r\n  looking at\r\n* Need to spin up, Tornado/Twisted/Gevent as well as Django LiveServerTestCase\r\n* FT opens multiple browser tabs in parallel\r\n* Big change!\r\n\r\n--> 20 pages\r\n\r\n\r\n\r\n22: Continuous Integration \r\n--------------------------\r\n\r\n* Need to build 3 server types\r\n* Jenkins (or maybe buildbot)\r\n* Need to adapt Fts, maybe rely less on LiveServerTestCase\r\n\r\n--> 15 pages\r\n\r\n\r\n23: Caching for screamingly fast performance\r\n--------------------------------------------\r\n\r\n* unit testing `memcached`\r\n* Functionally testing performance\r\n* Apache `ab` testing\r\n\r\n--> 15 pages\r\n\r\n\r\n===> total for part 4: 60 pages\r\n\r\n\r\n"
  },
  {
    "path": "misc/chapters_v2.rst",
    "content": "===================================================\r\nPART 1 - An introduction to Test-Driven Development\r\n===================================================\r\n\r\n1: What is TDD?  \r\n---------------\r\n\r\n* Some evangelism\r\n* XP and Kent Beck, Agile methods\r\n* TDD versus testing afterwards, \r\n* Outline methodology\r\n\r\ncurrent tutorial : 800 words, no illustrations. this shouldn't be more than 3-5\r\npages really\r\n\r\n--> 5 pages\r\n\r\n\r\n2: A simple example (?)\r\n-----------------------\r\n\r\neg Roman numerals, as per \"Dive Into Python.\". Slightly boring topic, everyone\r\nuses it, but it will do for now. \r\n\r\n* basic test - convert I\r\n* I-III\r\n* V + X\r\n* VI, VII, VIII\r\n* IV and IX \r\n* ...\r\n\r\n* Start simple, include exceptions. Aim to show:\r\n* unittest, how to run tests, possibly setUp, tearDown\r\n* red / green / refactor:  the test/code cycle\r\n* making the minimal change\r\n* show how design grows organically, but stays neat\r\n* the psychological effect\r\n* as per Beck. \"Am I saying you should always code like this? No.  I'm saying you should always *be able to*.\r\n\r\n\r\nIn DIP, this is pp 183-205, so 22 pages. I'd aim for half that.\r\n\r\n--> 10 pages  (maybe split this into multiple chapters?)\r\n\r\n===> total for part 1: 15 pages\r\n\r\n===================================================\r\nPART 2 - Using TDD to build a basic web application\r\n===================================================\r\n\r\n3: Our first functional test with Selenium\r\n------------------------------------------\r\n\r\n\r\n* Briefly discuss difference between functional testing (AKA acceptance\r\n  testing, integration testing, whatever) and unit testing\r\n* Write first test - Introduce Selenium, `setUp`, `tearDown`\r\n* Demonstrate we can get it to open a web browser, and navigate to a web page\r\n  eg - google.com\r\n\r\ncurrently maybe 228 lines, 1200 words.\r\n--> 5 pages\r\n\r\n\r\n\r\n4: Getting Django set-up and running\r\n------------------------------------\r\n\r\n* Change our test to look for the test server\r\n* Switch to Django LiveServerTestCase. Explain\r\n* Get the first test running and failing for a sensible reason\r\n* Create django project `django-admin.py startproject`\r\n* It worked!\r\n\r\n--> 3 pages\r\n\r\n\r\n5: A static front page\r\n----------------------\r\n\r\n* Look for \"Welcome to the Forums\", or similar\r\n* `urls.py`, `direct_to_template` ?\r\n\r\n--> 3 pages\r\n\r\n\r\n6: Super-users and the Django admin site\r\n----------------------------------------\r\n\r\n* Extend FT to try and log in\r\n* Explain the admin site\r\n* Database setup, `settings.py`, `syncdb`, `admin.py`\r\n* `runserver` to show login code\r\n* Explain difference between test database and real database\r\n* Fixtures\r\n\r\n--> 7 pages\r\n\r\n\r\n7: First unit tests and Database model \r\n--------------------------------------\r\n\r\n* Distinction between unit tests and functional tests\r\n* Extend FT to try and create a new topic\r\n* new app\r\n* `models.py`\r\n* test/code cycle\r\n\r\n--> 7 pages\r\n\r\n\r\n\r\n8: Testing a view\r\n-----------------\r\n\r\n* urls.py again\r\n* Test view as a function\r\n* assert on string contents\r\n\r\n--> 4 pages\r\n\r\n9: Django's template system\r\n----------------------------\r\n\r\n* Introduce template syntax\r\n* Keep testing as a function\r\n* The, introduce the Django Test Client\r\n\r\n--> 6 pages\r\n\r\n\r\n\r\n10: Reflections: what to test, what not to test\r\n-----------------------------------------------\r\n\r\n* \"Don't test constants\"\r\n* Test logic\r\n* Tests for simple stuff should be simple, so not much effort\r\n\r\n--> 3 pages\r\n\r\n\r\n11: Simple Forms\r\n----------------\r\n\r\n* Manually coded HTML\r\n* Refactor test classes\r\n\r\n--> 5 pages\r\n\r\n\r\n12: User Authentication\r\n-----------------------\r\n\r\n* Sign up, login/logout\r\n* Email?\r\n\r\n--> 5 pages\r\n\r\n\r\n13: More advanced forms\r\n-----------------------\r\n\r\n* Use Django Forms classes\r\n\r\n--> 6 pages\r\n\r\n\r\n14: On Refactoring\r\n------------------\r\n\r\n* Martin Fowler\r\n* Tests critical\r\n* Methodical process - explain step by step\r\n\r\n--> 4 pages\r\n\r\n\r\n15: Pagination\r\n--------------\r\n\r\n* Extend various old unit tests and FTs\r\n\r\n--> 3 pages\r\n\r\n\r\n===> total for part 2: 60 pages\r\n\r\n\r\n\r\n======================================================\r\nPART 3: More advanced testing for a more advanced site\r\n======================================================\r\n\r\n15: Notifications\r\n------------------------------\r\n\r\n* Django Notifications, for post edits\r\n--> 5 pages\r\n\r\n\r\n16: Adding style with MarkDown\r\n------------------------------\r\n\r\n* Using an external library\r\n\r\n--> 5 pages\r\n\r\n\r\n17: Switching to OAuth: Mocking\r\n-------------------------------\r\n\r\n* \"Don't store passwords\"\r\n* Discuss challenges of external dependencies\r\n\r\n--> 7 pages\r\n\r\n\r\n18: Getting Dynamic: Testing Javascript part 1\r\n----------------------------------------------\r\n\r\n* Simple input validation\r\n* Choose JS unit testing framework (probably Qunit, or YUI)\r\n\r\n--> 6 pages\r\n\r\n\r\n19: Testing Javascript part 2 - Ajax\r\n------------------------------------\r\n\r\n* Dynamic previews of post input\r\n\r\n--> 5 pages\r\n\r\n\r\n20: Getting pretty: Bootstrap\r\n-----------------------------\r\n\r\n* Bring in nicer UI elements\r\n\r\n--> 4 pages\r\n\r\n\r\n21: Getting pretty: Gravatar\r\n----------------------------\r\n\r\n* pictures for users\r\n\r\n--> 4 pages\r\n\r\n\r\n22: The bottomless web page\r\n---------------------------\r\n\r\n* More javascript bells and whistles\r\n\r\n--> 3 pages\r\n\r\n===> total for part 3: 39 pages\r\n\r\n\r\n==============================\r\nPART 4: Getting seriously sexy\r\n==============================\r\n\r\n23: Switching Databases 1: PostgreSQL\r\n----------------------------------------------\r\n\r\n* show how Django makes this easy\r\n\r\n--> 10 pages\r\n\r\n\r\n\r\n25: Websockets and Async on the server-side\r\n-------------------------------------------\r\n\r\n* we want dynamic notifications of when new posts appear on a thread we're\r\n  looking at\r\n* Need to spin up, Tornado/Twisted/Gevent as well as Django LiveServerTestCase\r\n* FT opens multiple browser tabs in parallel\r\n* Big change!\r\n\r\n--> 20 pages\r\n\r\n24: Switching Databases 2: NoSQL and MongoDB\r\n----------------------------------------------\r\n\r\n* obligatory discussion of NoSQL and MongoDB\r\n* descrine installation, particularities of testing\r\n\r\n--> 10 pages\r\n\r\n\r\n26: Continuous Integration \r\n--------------------------\r\n\r\n* Need to build 3 server types\r\n* Jenkins (or maybe buildbot)\r\n* Need to adapt Fts, maybe rely less on LiveServerTestCase\r\n\r\n--> 15 pages\r\n\r\n\r\n27: Caching for screamingly fast performance\r\n--------------------------------------------\r\n\r\n* unit testing `memcached`\r\n* Functionally testing performance\r\n* Apache `ab` testing\r\n\r\n--> 15 pages\r\n\r\n\r\n===> total for part 4: 60 pages\r\n\r\n\r\n"
  },
  {
    "path": "misc/chimera_comments_scraper.py",
    "content": "from __future__ import print_function\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support import expected_conditions\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.common.exceptions import TimeoutException\nimport re\n\nURLS = [\n    'http://chimera.labs.oreilly.com/books/1234000000754/pr01.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/pr02.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch01.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch02.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch03.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch04.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch05.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch06.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch07.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch08.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch09.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch10.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch11.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch12.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch13.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch14.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch15.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch16.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch17.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch18.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch19.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch20.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch21.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ch22.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/pr03.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/apa.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/apb.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/apc.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/apd.html',\n    'http://chimera.labs.oreilly.com/books/1234000000754/ape.html',\n]\n\nMETADATA_PARSER = re.compile(r'Comment by (.+) (\\d+) (.+ ago)')\nbrowser = webdriver.Firefox()\nwait = WebDriverWait(browser, 3)\ntry:\n    for url in URLS:\n        page = url.partition('1234000000754/')[2]\n        browser.get(url)\n\n        browser.find_element_by_css_selector('#comments-link a').click()\n        try:\n            wait.until(expected_conditions.presence_of_element_located(\n                (By.CLASS_NAME, 'comment')\n            ))\n        except TimeoutException:\n            print(\"No comments on page %s\" % (url,))\n\n        elements = browser.find_elements_by_css_selector('.comment')\n        for element in elements:\n            metadata = element.find_element_by_css_selector('.comment-body-top').text.strip()\n            # print(repr(metadata))\n            parsed_metadata = METADATA_PARSER.search(metadata).groups()\n            # print(parsed_metadata)\n            by = parsed_metadata[0]\n            date = parsed_metadata[1] + parsed_metadata[2]\n            # if 'months' not in date and 'year' not in date:\n            if 'year' not in date:\n                comment = element.find_element_by_css_selector('.comment-body-bottom').text\n                print('%s\\t%s\\t%s\\t%s' % (page, by, date, comment))\n\nfinally:\n    browser.quit()\n\n"
  },
  {
    "path": "misc/get_stats.py",
    "content": "#!/usr/bin/env python3\nfrom collections import namedtuple\nimport csv\nfrom datetime import datetime\nimport os\nimport re\nimport subprocess\n\nCommit = namedtuple('Commit', ['hash', 'subject', 'date'])\nWordCount = namedtuple('WordCount', ['filename', 'lines', 'words'])\nFileWordCount = namedtuple('FileWordCount', ['date', 'subject', 'hash', 'lines', 'words'])\nBOOK_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))\n\ndef get_log():\n    commits = []\n    log = subprocess.check_output(['git', 'log', '--format=%h|%s|%ai']).decode('utf8')\n    for line in log.split('\\n'):\n        if line:\n            hash, subject, datestring = line.split('|')\n            date = datetime.strptime(datestring[:16], '%Y-%m-%d %H:%M')\n            commits.append(Commit(hash=hash, subject=subject, date=date))\n    return commits\n\n\ndef checkout_commit(hash):\n    subprocess.check_call(['git', 'checkout', hash])\n\n\ndef get_wordcounts():\n    docs = [f for f in os.listdir(BOOK_ROOT) if f.endswith('.asciidoc')]\n    wordcounts = []\n    for filename in docs:\n        with open(os.path.join(BOOK_ROOT, filename)) as f:\n            contents = f.read()\n        lines = len(contents.split('\\n'))\n        words = len(contents.split())\n        filename = re.sub(r'_(\\d)\\.asciidoc', r'_0\\1.asciidoc', filename)\n        filename = re.sub(r'chapter(\\d\\d)\\.asciidoc', r'chapter_\\1.asciidoc', filename)\n        wordcounts.append(WordCount(filename, lines=lines, words=words))\n    return wordcounts\n\n\ndef main():\n    commits = get_log()\n    all_wordcounts = {}\n    filenames = set()\n    try:\n        for commit in commits:\n            checkout_commit(commit.hash)\n            all_wordcounts[commit] = get_wordcounts()\n            filenames.update(set(wc.filename for wc in all_wordcounts[commit]))\n\n        with open(os.path.join(BOOK_ROOT, 'wordcounts.tsv'), 'w') as csvfile:\n            fieldnames = ['date.{}'.format(thing) for thing in ['year', 'month', 'day', 'hour']]\n            fieldnames += ['subject', 'hash', 'date']\n            fieldnames.extend(sorted(filename + \" (words)\" for filename in filenames))\n            fieldnames.extend(sorted(filename + \" (lines)\" for filename in filenames))\n            writer = csv.DictWriter(csvfile, fieldnames, dialect=\"excel-tab\")\n            writer.writeheader()\n            for commit, wordcounts in all_wordcounts.items():\n                row = {}\n                row['hash'] = commit.hash\n                row['subject'] = commit.subject\n                row['date'] = ''\n                row['date.year'] = commit.date.year\n                row['date.month'] = commit.date.month\n                row['date.day'] = commit.date.day\n                row['date.hour'] = commit.date.hour\n                for wordcount in wordcounts:\n                    row[wordcount.filename + \" (words)\"] = wordcount.words\n                    row[wordcount.filename + \" (lines)\"] = wordcount.lines\n                writer.writerow(row)\n\n    finally:\n        checkout_commit('master')\n\n\n\nif __name__ == '__main__':\n    main()\n\n"
  },
  {
    "path": "misc/get_stats.sh",
    "content": "dropbox stop\npython3 get_stats.py\ndropbox start\n"
  },
  {
    "path": "misc/isolation-talks/djangoisland.md",
    "content": "Outside-In TDD, Test Isolation, and Mocking\n===========================================\n\n* Harry Percival, @hjwp, www.obeythetestinggoat.com\n\n* Outside-in TDD?\n\n* DHH \"TDD is Dead\":  agree? disagree?\n\n* No idea?\n\n* Who doesn't know what a mock is?\n\n\n\n\n\n # PS if you're coming to my tutorial tomorrow:  **INSTALL STUFF**\n\n * Python 3.3+\n * Django 1.7 (from 1.7.x stable branch on gh)\n * Selenium\n * Firefox\n\n -- instructions in preface of my book, available online,\n    www.obeythetestinggoat.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nConclusion\n==========\n\n*Listen to your tests*\n\n- ppl complain about \"too many mocks\": are there architectural solutions that\n  would solve your problem?\n\n- Ports & Adapters Hexagonal / Clean architecture\n- Functional Core Imperative Shell\n\n- are you testing at the right level?\n\n*Further reading*\n\n- see chapter 22 in my book (available free online) for a reading list.\n  www.obeythetestinggoat.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# What do we want from our tests anyway?\n\n* Correctness\n* Clean, maintainable code\n* Productive workflow\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# On the Pros and Cons of Different Types of Test\n\nFunctional tests::\n\n    * Provide the best guarantee that your application really works correctly,\n      from the point of view of the user.\n    * But: it's a slower feedback cycle,\n    * And they don't necessarily help you write clean code.\n\nIntegrated tests (reliant on, eg, the ORM or the Django Test Client)::\n\n    * Are quick to write,\n    * Easy to understand,\n    * Will warn you of any integration issues,\n    * But may not always drive good design (that's up to you!).\n    * And are usually slower than isolated tests\n\nIsolated (\"mocky\") tests::\n\n    * These involve the most hard work.\n    * They can be harder to read and understand,\n    * But: these are the best ones for guiding you towards better design.\n    * And they run the fastest.\n\n\n\n\n\n\n\n\n\n\n\n\n\n# PS If you're coming to my tutorial tomorrow:  **INSTALL STUFF**\n\n* Python 3.3+\n* Django 1.7 (from 1.7.x stable branch on gh)\n* Selenium\n* Firefox\n\n-- instructions in preface of my book, available online, \n   www.obeythetestinggoat.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "misc/isolation-talks/djangoisland.py",
    "content": "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n# models.py\r\nfrom django import models\r\n\r\nclass List(models.Model):\r\n    pass\r\n\r\n\r\nclass Item(models.Model):\r\n    text = models.TextField(default='')\r\n    list = models.ForeignKey(List, default=None)\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"
  },
  {
    "path": "misc/isolation-talks/extra_styling_for_djangoisland.css",
    "content": "body, a, em  {\n  color: white;\n}\nbody div.listingblock {\n    margin-bottom: 100ex;\n}\np code, table code {\n    color: white;\n}\ndiv.admonitionblock {\n    display: none;\n}\n"
  },
  {
    "path": "misc/isolation-talks/outline.txt",
    "content": "\n* 787a587 First real draft of My Lists FT. --ch18l001--\n* 9a0b007 do-nothing my lists link in navbar. --ch18l002-1--\n* 82ad580 add actual URL for my_lists to template --ch18l002-2--\n* 785c527 test for my lists url and template. --ch18l003--\n* b29a119 URL for my lists. --ch18l004--\n* 7347a49 minimal view for my_lists, just renders template. --ch18l005--\n* 9e2a450 minimal my_lists.html template. --ch18l006--\n* 00ca8c4 Add block for list_form. --ch18l007-1--\n* 735a457 Add block for extra_content inside bs row + col. --ch18l007-2--\n* b769dcb flesh out my_lists.html. --ch18l010--\n* 3f20d8b test passes owner to my_lists template. --ch18l011--\n* c9b24e9 view passes owner to my_lists template. --ch18l012--\n* 51a85da add user to other view test. --ch18l013--\n* e0dc807 test that new list view saves owner. --ch18l014--\n* 3d98981 (tag: revisit_this_point_with_isolated_tests) Attempt saving list owner in view. --ch18l01\n* 58c5f5f test lists can have owners. --ch18l018--\n* 5d18abd extra test that list owner is optional. --ch18l020--\n* a6f70ab optional owner field on list model. --ch18l021--\n* 8349ba0 Migration for list owner. --ch18l022--\n* f0dba4f only save list owner if user is logged in. --ch18l023--\n* c0890b8 test .name attribute of list model. --ch18l024--\n* b08c8c4 lists have a .name attribute. --ch18l025--\n* bdbf725 fixup key ordering in list owner migration.\n* 04ef40a add another dunderfuture import in 18\n* 456b99e (origin/chapter_18, chapter_18) fix encodingey thing in migraiton\n\n* 81a4ca9 first attempt at a mocky test for list owner saveage. --ch19l003--\n* 871347c deliberately break by assigning owner after save. --ch19l004--\n* fc78466 use side_effect to check ordering. --ch19l005--\n* ec2ae28 revert views.py back to correctish owner saving. --ch19l006--\n* 96249c7 revert test back to non-mocky one with a skip. rename test class. --ch19l008--\n* 4240536 placeholder new_list2 view. --ch19l009--\n* 4e98e70 new isolated test for new view. --ch19l010--\n* 4e8ba73 import NewListForm in views.py. --ch19l011--\n* a670e27 placeholder NewListForm. --ch19l012--\n* 6be5ea9 start on using form in view. --ch19l012-2--\n* e9088b4 new test that form is saved. --ch19l013--\n* 3e5c30a save form with owner. --ch19l014--\n* 4953532 test we redirect if valid. --ch19l015--\n* 7752d0a redirect to saved form object. --ch19l016--\n* abcd3cc test that we render home template if form invalid. --ch19l017--\n* 4a87c03 deliberately slightly broken view. --ch19l018--\n* 4b5ccd7 test form not saved in invalid case. --ch19l019--\n* 954f352 fix logic error, view now done. --ch19l020--\n* 09020a8 First isolated test for form save. --ch19l021--\n* 680b7ce second isolated test for forms authenticated user case. --ch19l022--\n* 3ad4afd add import into forms. --ch19l023--\n* d51d93b placeholder for List.create_new. --ch19l024--\n* 32d344e First cut of form save. --ch19l025--\n* 9b904f3 test create_new at models layer. --ch19l026--\n* 730497f first cut of create_new staticmethod. --ch19l027--\n* 043c609 test create_new with owner. --ch19l028--\n* cc52837 test lists can have optional owner. --ch19l029--\n* e0ea36a owner field on List. --ch19l030-1--\n* f4499ad migration for list owner --ch19l030-2--\n* faeadf0 create_new now saves owner --ch19l030-3--\n* 7638d7b fix old view so that it conditionally saves user. --ch19l031--\n* f7c0596 switch out new list function in urls. --ch19l032--\n* b18d2cd switch out integrated test to use new view. --ch19l033--\n* 466957d placeholder test that form save returns the new list. --ch19l038-1--\n* 925a74c placeholder test for create_new returning new list --ch19l038-2--\n* 3af89e0 return list from form save. --ch19l039-1--\n* 09d53cc flesh out test that create_new should return list. --ch19l039-2--\n* f90648c return newly created list in List.create_new --ch19l039-3--\n* d78dd00 test list.name is first item text. --ch19l040--\n* 55a283e list.name is first item text. --ch19l041--\n* 86ab021 remove unused code from forms\n* 10e0449 remove old new_lists view from test_views. --ch19l045--\n* 7a175c6 rename to new_list from new_list2 in urls.py. --ch19l046--\n* 656c3e6 delete old new_list from views.py. --ch19l047--\n* d952cee (origin/chapter_19) Strip out irrelephant integrated view tests. --ch19l048--\n"
  },
  {
    "path": "misc/isolation-talks/webcast-commits.hist",
    "content": "\n* 787a587 First real draft of My Lists FT. --ch18l001--\n* 9a0b007 do-nothing my lists link in navbar. --ch18l002-1--\n* 82ad580 add actual URL for my_lists to template --ch18l002-2--\n* 785c527 test for my lists url and template. --ch18l003--\n* b29a119 URL for my lists. --ch18l004--\n* 7347a49 minimal view for my_lists, just renders template. --ch18l005--\n* 9e2a450 minimal my_lists.html template. --ch18l006--\n* 00ca8c4 Add block for list_form. --ch18l007-1--\n* 735a457 Add block for extra_content inside bs row + col. --ch18l007-2--\n* b769dcb flesh out my_lists.html. --ch18l010--\n* 3f20d8b test passes owner to my_lists template. --ch18l011--\n* c9b24e9 view passes owner to my_lists template. --ch18l012--\n* 51a85da add user to other view test. --ch18l013--\n* e0dc807 test that new list view saves owner. --ch18l014--\n* 3d98981 (tag: revisit_this_point_with_isolated_tests) Attempt saving list owner in view. --ch18l015--\n* 58c5f5f test lists can have owners. --ch18l018--\n* 5d18abd extra test that list owner is optional. --ch18l020--\n* a6f70ab optional owner field on list model. --ch18l021--\n* 8349ba0 Migration for list owner. --ch18l022--\n* f0dba4f only save list owner if user is logged in. --ch18l023--\n* c0890b8 test .name attribute of list model. --ch18l024--\n* b08c8c4 lists have a .name attribute. --ch18l025--\n* bdbf725 fixup key ordering in list owner migration.\n* 04ef40a add another dunderfuture import in 18\n* 456b99e (HEAD, origin/chapter_18, chapter_18) fix encodingey thing in migraiton\n\n\n* 3d98981 (tag: revisit_this_point_with_isolated_tests) Attempt saving list owner in view. --ch18l015--\n* 81a4ca9 first attempt at a mocky test for list owner saveage. --ch19l003--\n* 871347c deliberately break by assigning owner after save. --ch19l004--\n* fc78466 use side_effect to check ordering. --ch19l005--\n* ec2ae28 revert views.py back to correctish owner saving. --ch19l006--\n* 96249c7 revert test back to non-mocky one with a skip. rename test class. --ch19l008--\n* 4240536 placeholder new_list2 view. --ch19l009--\n* 4e98e70 new isolated test for new view. --ch19l010--\n* 4e8ba73 import NewListForm in views.py. --ch19l011--\n* a670e27 placeholder NewListForm. --ch19l012--\n* 6be5ea9 start on using form in view. --ch19l012-2--\n* e9088b4 new test that form is saved. --ch19l013--\n* 3e5c30a save form with owner. --ch19l014--\n* 4953532 test we redirect if valid. --ch19l015--\n* 7752d0a redirect to saved form object. --ch19l016--\n* abcd3cc test that we render home template if form invalid. --ch19l017--\n* 4a87c03 deliberately slightly broken view. --ch19l018--\n* 4b5ccd7 test form not saved in invalid case. --ch19l019--\n* 954f352 fix logic error, view now done. --ch19l020--\n* 09020a8 First isolated test for form save. --ch19l021--\n* 680b7ce second isolated test for forms authenticated user case. --ch19l022--\n* 3ad4afd add import into forms. --ch19l023--\n* d51d93b placeholder for List.create_new. --ch19l024--\n* 32d344e First cut of form save. --ch19l025--\n* 9b904f3 test create_new at models layer. --ch19l026--\n* 730497f first cut of create_new staticmethod. --ch19l027--\n* 043c609 test create_new with owner. --ch19l028--\n* cc52837 test lists can have optional owner. --ch19l029--\n* e0ea36a owner field on List. --ch19l030-1--\n* f4499ad migration for list owner --ch19l030-2--\n* faeadf0 create_new now saves owner --ch19l030-3--\n* 7638d7b fix old view so that it conditionally saves user. --ch19l031--\n* f7c0596 switch out new list function in urls. --ch19l032--\n* b18d2cd switch out integrated test to use new view. --ch19l033--\n* 466957d placeholder test that form save returns the new list. --ch19l038-1--\n* 925a74c placeholder test for create_new returning new list --ch19l038-2--\n* 3af89e0 return list from form save. --ch19l039-1--\n* 09d53cc flesh out test that create_new should return list. --ch19l039-2--\n* f90648c return newly created list in List.create_new --ch19l039-3--\n* d78dd00 test list.name is first item text. --ch19l040--\n* 55a283e list.name is first item text. --ch19l041--\n* 86ab021 remove unused code from forms\n* 10e0449 remove old new_lists view from test_views. --ch19l045--\n* 7a175c6 rename to new_list from new_list2 in urls.py. --ch19l046--\n* 656c3e6 delete old new_list from views.py. --ch19l047--\n* d952cee (HEAD, origin/chapter_19, chapter_19) Strip out irrelephant integrated view tests. --ch19l048--\n\n"
  },
  {
    "path": "misc/plot.py",
    "content": "from datetime import datetime\nimport numpy\nfrom matplotlib import pyplot\nimport csv\n\ndef get_data_from_csv():\n    with open('wordcounts.tsv') as f:\n        reader = csv.DictReader(f, dialect=\"excel-tab\")\n        data = []\n        for ix, row in enumerate(reader):\n            fixed_row = {}\n            if ix > 4:\n                break\n            for field in reader.fieldnames:\n                if 'words' in field:\n                    val = row[field]\n                    if val:\n                        fixed_row[field] = val\n                    else:\n                        fixed_row[field] = 0\n            date = datetime(int(row['date.year']), int(row['date.month']), int(row['date.day']), int(row['date.hour']),)\n            fixed_row['date'] = date\n            data.append(fixed_row)\n    return data\n\n\n# print(len(data))\ndata = {}\ndata['date'] = [1, 4, 3, 2]\ndata['words1'] = [0, 3, 3, 5]\ndata['words2'] = [4, 6, 0, 2]\narray = [data['date'], data['words1'], data['words2']]\nnumpy.sort(array, 0)\n\ndata = get_data_from_csv()\ndata.sort(key=lambda d: d['date'])\nx = [d['date'] for d in data]\ny = [\n    [d[key] for d in data]\n    for key in data[0].keys() if 'words' in key\n]\npyplot.stackplot(x, y)\n\n# pyplot.stackplot(data['date'], [values for (field, values) in data.items() if 'words' in field])\n# for (field, values) in data.items():\n    # if 'words' in field:\n        # pyplot.plot(data['date'], values)\n\npyplot.show()\n"
  },
  {
    "path": "misc/reddit_post.md",
    "content": "Hi Python-Redditors!,\n\nI'm somehow writing a book on TDD and Python for O'Reilly, despite feeling massively underqualified.  Please help me to instill the book with more wisdom than I can muster on my own!\n\nHere's a link where you can take a look at what I have so far (8 chapters):\n\nhttp://www.obeythetestinggoat.com\n\n**My background:**\n\nI've only been a professional programmer for about 4 years, but I was lucky enough to fall in with a bunch of XP fanatics at Resolver Systems, now [PythonAnywhere](http://www.pythonanywhere.com).  Over the past few years I've learned an awful lot, and I'm trying to share what I've learned with other people who are starting out. Someone described learning TDD as being a step on the journey from amateur coder to professional, maybe that's a good way of putting it.\n\nFor the past couple of years I've been running beginners' TDD / Django workshops at EuroPython and Pycons, and they've been well-received. They're probably what must have fooled O'Reilly into thinking I could write a book!\n\n**The book**\n\nThe concept is to take the user through building a web app from scratch, but using full TDD all along the way.  That involves:\n\n* Functional tests using Selenium\n* \"unit\" tests using the Django test runner\n* All Django goodness including views, models, templates, forms, admin etc\n* Unit testing javascript\n* Tips on deployment, and how to test against  a staging site\n\n...and lots more.  You'll find a [proposed chapter outline](http://chimera.labs.oreilly.com/books/1234000000754/ch09.html) at the end of the book.\n\nI've made a good start, 8 chapters, taking us all the way to deploying a minimum viable app, but I really need some more feedback.\n\n* Am I covering the right stuff?  Would you add / remove any topics?\n* Am I teaching what you consider to be best practice?  Some people, for example, think that touching the database in a thing you call a unit test is an absolute no-no (cf [past discussion on reddit](http://www.reddit.com/r/django/comments/1c67rl/is_tddjangotutorial_truly_a_good_resource_i_want/)).  Do you think that's important?  Should I acknowledge the controversy, and maybe refer people to an appendix which discusses how to avoid hitting the db in unit tests?  Or should I throw out the Django test runner?\n* I'm also telling people to use a very belt & braces technique, where we write functional tests *and* unit tests for pretty much every single line of code.  Is that overkill? Or do you think that it's a good thing to learn, even if you later relax the rules when you get a bit more experience (I make this argument in chapter 4)\n* My latest chapter is [all about deployment](http://chimera.labs.oreilly.com/books/1234000000754/ch08.html) -- lots of people do different things in this area, we had a good discussion of it on the Python-UK mailing list. What do you think of what I have so far?\n\n\n**Why help?**\n\nPerhaps you're thinking \"Why should I help this guy to write a better book and make more money, despite the blatant massive lacunae in his knowledge?\". \n\nWell, one thing that I hope will sway your cold, hard heart is that I am pledging to make the book available for free under a CC license, alongside the official paid-for printed and ebook versions. I insisted on that as part of my contract, and O'Reilly were totally happy with it (props to them for being forward-thinking).  I really hope that this book can distill something of the sum total of all the knowledge in the Python testing community, over and above my own, and be a useful resource for that whole community, and not just those that can afford to pay for a book...\n\nIf that's not enough, how about I promise I'll buy you a beer one day?\n\n\nThanks in advance, \nHarry\n\n"
  },
  {
    "path": "misc/redditnotesresponse.txt",
    "content": "Thanks again for your detailed and very helpful comments!\n\nFirst off, on your general suggestion that Git and Deployment are 'orthogonal' to TDD -- I tend to agree, especially on the former.\n\nI guess it's about the picture I have in my head of my target audience -- people who are relatively new to programming, who've maybe hacked together a few programs, or maybe just finished a degree.  Basically, it's me 4 years ago.  If I look back on what I've learned over the past few years, things I think are absolutely indispensable skills, TDD is one, but how Git fits into my development process is also a huge part of it.  Experienced developers will probably already know git, so they can just skip over my explanations (which are pretty much confined to chapter 1), but my hope is that beginners will get a lot out of it.  It's all part of the flow of a more mature development process -- test, edit, commit, as someone put it in another thread.\n\nSame thing with deployment -- although I think I've made a case for how testing *does* interact with deployment and staging sites, much more so than it does with VCS.  But the point is that you're not a web developer until you've deployed your app, so, again, my hope is that beginners will be getting something out of that chapter.\n\nRe showing non-best-practice first, my aim is to start with the simplest possible thing as a way of introducing the core concept. Then I can bring in the best practice once I feel the reader is ready for them.  I mean, I can't throw in git, virtualenvs, south, postgres, gunicorn, CDNs, selenium, unittest, django, memcached and all the rest into the very first chapter!  Perhaps I do sometimes take it a little too far though...  I'll have another read-through.\n\nThanks for the suggestion re the FT comments spec too.  I guess some of that is a matter of style, or what you're used to... The comments are consciously quite vague, whilst the selenium assertions are specific, precisely because the former represent human-language requirements and the latter start to be implementation-dependent. You could imagine changing the implementation without changing the implementation. I'm thinking of including a chapter or appendix on BDD though, so I may be able to introduce a different style there.  I really don't know enough about it yet, so if you 're interested in a collaboration, do get in touch.\n\nRe chapter 7, I know I'm in a danger area there.  I certainly don't think you should test cosmetics, and as you diagnosed, the main point of the test is as a smoke test that the CSS has loaded correctly.  So, would some kind of non-functional integration test be better?  Maybe... The FT I guess adds the check that not only is the CSS available, but also that it was loaded correctly by the page... I guess I should just make it clearer about what the objective of that test is, and be more explicit about saying \"don't test cosmetics\".  With that said, there's definitely a place for testing layout using functional tests... JavaScript-driven resize behaviour an example that springs to mind from PythonAnywhere\n\nThanks for the validation re my belt + braces, FT + unit approach.\n\nFinally, onto the random notes!\n\nI've never used RequestFactory.  What does it give you that instantiating a regular HttpRequest() doesn't?\n\nAnd, wow, thanks for telling me about addCleanup() -- I can't believe I'd never come across that (maybe it's because we used to write within the constraints of Python 2.6 unittest).  Love it, will definitely be using it.\n\nRe starting + stopping firefox for each test, I just defaulted to that because it's what we do at PA. I think our intention there was about isolating FTs completely, making sure that cookies  + sessions are clean each time, etc. That may not always be necessary, but at least once I do it very deliberately...  I'll make a mention of it as a possible optimisation.\n\nFinally, on the overall dev process, I'm just alt-tabbing between a console and editor, and that's pretty much what I'm telling people to do.  Automated file watchers are cool -- I think my ex-colleague JB wrote one called rerun -- but I think I'll leave them for people to discover on their own. CI I hope to cover in a later chapter,  cf the outline...\n\nOnce again, thanks again for your detailed comments.   It's nice to have some validation, to learn a couple of things, and also to have a couple of decisions challenged.  I'll go away and think about it all.  And do get in touch if you want to work together on a BDD chapter -- if nothing else, I'd love to know what you use, and what your experience is with your tools, what you've ruled out, what you've preferred and why, possible pitfalls...\n\n\n"
  },
  {
    "path": "misc/tdd-flowchart.dot",
    "content": "digraph g {\n\nwrite_test [label=\"Write a test\" shape=box]\nif_passes [label=\"Run the test.\\nDoes it pass?\" shape=diamond]\ncode [label=\"Write minimal code\" shape=box]\nrefactor [label=\"Refactor (optional)\" shape=box]\n\nsubgraph along {\n    rank = same;\n    write_test -> if_passes ;\n    if_passes -> refactor [label=\"Yes\" color = green] ;\n    refactor -> write_test;\n}\nsubgraph down {\n    rankdir = UD;\n    if_passes -> code:w [label=\"No\" color = red] ;\n}\n\ncode:e -> if_passes\n\n} \n"
  },
  {
    "path": "outline_and_future_chapters.asciidoc",
    "content": "Outline to date & future chapters plan\n--------------------------------------\n\nThanks for reading this far!  I'd really like your input on this too:  What do\nyou think of the book so far, and what do you think about the topics I'm\nproposing to cover in the list below?  Email me at\nobeythetestinggoat@gmail.com!\n\nNB - when I say \"book\" below, they're all going to be parts of this book. I\nguess I should say \"part\" instead, but for some reason I've decided book sounds\ncooler.  Like Lord of the Rings.\n\n\n* Preface (Chapter 0) - intro + pre-requisites\n\nBOOK 1: Building a minimum viable app with TDD\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDone:\n\n* Chapter 1 - Getting Django set up using a Functional Test\n* Chapter 2 - Extending our FT using the unittest module\n* Chapter 3 - Testing a simple home page with unit tests\n* Chapter 4 - What are we doing with all these tests?\n* Chapter 5 - Saving form submissions to the database\n* Chapter 6 - Getting to the minimum viable site\n* Chapter 7 - Prettification\n* Chapter 8 - Deploy!\n\n\nBOOK 2: Growing the site\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nDone:\n\n* Chapter 9 - Input validation and test organisation\n* Chapter 10 - A simple form\n* Chapter 11 - More advanced Forms \n* Chapter 12 - Database migrations\n* Chapter 13 - Dipping our toes, very tentatively, into JavaScript\n* Chapter 14 - User authentication, integrating 3rd party plugins, and Mocking\n               with JavaScript\n* Chapter 15 - Mocking 3rd party web services with Python mock\n* Chapter 16 - Server-side test database management\n* Chapter 17 - Continuous Integration (CI) with Jenkins\n\nPlanned:\n\n\nChapter 18: sharing lists\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* email notifications\n* django notifications (?)\n* \"claim\" an existing list (?)\n* URLs would need to be less guessable\n\n\nMore/Other possible contents\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n*Django stuff:*\n\n* switch database to eg postgres\n* uploading files?\n\n\n*Testing topics to work in*\n\n* more how to read a traceback (ref step 4 in chapter 3)\n* simplify the model test down to minimal/best practice.\n* talk about the \"purist\" approach to unit testing vs the django test client.\n* AKA \"Functional core imperative shell\"\n* selenium page pattern\n* coverage\n* alternative test runners -- py.test, nose (lots of ppl mentioned latter)\n* addCleanup\n* PhantomJS for faster Fts?\n* fixtures (factory boy?)\n* JS: mocking external web service to simulate errors\n* Splinter\n* difference between unittest.TestCase, django.test.TestCase, LiveServerTestCase\n* general troubleshooting tips (appendix, collecting all notes etc?)\n* LiveServerTestCase does not flush staging server DB. fix in CI chapter?\n* How to stop and start (expand on bit in ch4. refer to stop+start book)\n* some kind of indicator of where in the tdd cycle we are, in the margin?\n\n\n*Deployment stuff*\n\n* FT for 404 and 500 pages?\n* email integration\n\n\n\nBOOK 3: Trendy stuff\n~~~~~~~~~~~~~~~~~~~~\n\nChapter 19 & 20: Javascript MVC frameworks\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* MVC tool (backbone / angular)\n* single page website (?) or bottomless web page?\n* switching to a full REST API\n* HTML5, eg LocalStorage\n* Encryption - client-side decrypt lists, for privacy?\n\n\nChapter 21: Async\n^^^^^^^^^^^^^^^^^\n\n* websockets\n* tornado/gevent (or sthing based on Python 3 async??)\n* how to get django to talk to tornado: redis? (just for fun?)\n* for collaborative lists??\n\n\nChapter 22: Caching\n^^^^^^^^^^^^^^^^^^^\n\n* unit testing `memcached`\n* Functionally testing performance\n* Apache `ab` testing\n\n5/6 chapters?\n\n\nAppendices\n~~~~~~~~~~\n\n\nPossible appendix topics\n^^^^^^^^^^^^^^^^^^^^^^^^\n\n* BDD  (+2 from reddit)\n* Mobile (use selenium, link to using bootstrap?)\n* Payments... Some kind of shopping cart?\n* unit testing fabric scripts\n* testing tools pros & cons, eg django test client vs mocks, liverservertestcase vs roll-your-own\n* NoSQL / Redis / MongoDB?\n\n\n\nA PythonAnywhere\n^^^^^^^^^^^^^^^^^\n\n* Running Firefox Selenium sessions with pyVirtualDisplay\n* Setting up Django as a PythonAnywhere web app\n* Cleaning up /tmp\n* Screenshots\n\n\nB: Django class-based views\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n* refactoring, proving usefulness of view tests.\n\nC: Automated provisioning and configuration management with Ansible\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n* light appendix.\n\n"
  },
  {
    "path": "part1.asciidoc",
    "content": "[[part1]]\n[part]\n[role=\"pagenumrestart\"]\n== The Basics of TDD and Django\n\n[partintro]\n--\nIn this first part, I'm going to introduce the basics of 'test-driven\ndevelopment' (TDD). We'll build a real web application from scratch, writing tests first at every stage.\n\nWe'll cover functional testing with Selenium, as well as unit testing,\nand see the difference between the two.\nI'll introduce the TDD workflow, red/green/refactor.\n \nI'll also be using a version control system (Git).\nWe'll discuss how and when to do commits and integrate them with the TDD and web development workflow.\n\nWe'll be using Django, the Python world's most popular web framework (probably).\nI've tried to introduce the Django concepts slowly and one at a time,\nand provide lots of links to further reading.\nIf you're a total beginner to Django, I thoroughly recommend taking the time to read them.\nIf you find yourself feeling a bit lost,\ntake a couple of hours to go through the https://docs.djangoproject.com/en/5.2/intro[official Django tutorial]\nand then come back to the book.\n\nIn <<part1>>, you'll also get to meet the Testing Goat...\n\n\n[WARNING]\n====\nBe careful with copy and paste. If you're working from a digital version of the book,\nit's natural to want to copy and paste code listings from the book as you're working through it.\nIt's much better if you don't: typing things in by hand gets them into your muscle memory,\nand just feels much more real.\nYou also inevitably make the occasional typo, and debugging is an important thing to learn.\n\nQuite apart from that, you'll find that the quirks of the PDF format\nmean that weird stuff often happens when you try to copy/paste from it...\n====\n\n--\n"
  },
  {
    "path": "part2.asciidoc",
    "content": "[[part2]]\n[part]\n== Going to Production\n\n[partintro]\n[quote, 'https://oreil.ly/Q7UDe[DevOps Borat]']\n______________________________________________________________\nIs all fun and game until you are need of put it in production.\n______________________________________________________________\n\nIt's time to deploy the first version of our site and make it public.\nThey say that if you wait until you feel _ready_ to ship,\nthen you've waited too long.\n\nIs our site usable? Is it better than nothing? Can we make lists on it?\nYes, yes, yes.\n\nNo, you can't log in yet.\nNo, you can't mark tasks as completed.\nBut do we really need any of that stuff?\nNot really--and you can never be sure\nwhat your users are _actually_ going to do with your site\nonce they get their hands on it.\nWe think our users want to use the site for to-do lists,\nbut maybe they actually want to use it\nto make \"top 10 best fly-fishing spots\" lists,\nfor which you don't _need_ any kind of \"mark completed\" function.\nWe won't know until we put it out there.\n\nOver the next couple of chapters we're going to go through\nand actually deploy our site to a real, live web server.\n\nYou might be tempted to skip this bit--there's lots of daunting stuff in it,\nand maybe you think this isn't what you signed up for.\nBut I _strongly_ urge you to give it a go.\nThis is one of the sections of the book I'm most pleased with,\nand it's one that people often write to me about\nsaying they were really glad they stuck through it.\n\nIf you've never done a server deployment before,\nit will demystify a whole world for you,\nand there's nothing like the feeling of seeing your site live\non the actual internet.\nGive it a buzzword name like \"DevOps\"\nif that's what it takes to convince you it's worth it.\n\n[role=\"notoc\"]\n=== The Danger Areas of Deployment\n\nDeploying a site to a live web server can be a tricky topic.\nOft heard is the forlorn cry, \"but it works on my machine!\"\n\n(((\"deployment\", \"danger areas of\")))\nSome of the danger areas of deployment include:\n\nNetworking::\n    Once we're off our own machine, networking issues come in:\n    making sure that DNS is routing our domain to the correct IP address for our server,\n    making sure our server is configured to listen to traffic coming in from the world,\n    making sure it's using the right ports,\n    and making sure any firewalls in the way are configured to let traffic through.\n\nDependencies::\n    We need to make sure that the packages our software relies on\n    (Python, Django, and so on) are installed on the server\n    and have the correct versions.\n\nThe database::\n    There can be permissions and path issues,\n    and we need to be careful about preserving data between deploys.\n\nStatic files (CSS, JavaScript, images, etc.)::\n    Web servers usually need special configuration for serving these.\n    (((\"static files\", \"challenges of\")))\n\nSecurity and configuration::\n    Once we're on the public internet,\n    we need to worry more about security.\n    Various settings that are really useful for local development\n    (like the Django debug page)\n    become dangerous in production\n    (because they expose our source code in tracebacks).\n\nReproducibility and divergence between local dev and prod::\n    All of the above add up to differences between your local development environment\n    and the way code runs in production.\n    We want to be able to reproduce the way things work on our machine,\n    as closely as possible, in production (and vice versa)\n    to give us as much confidence as possible that\n    \"it works on my machine\" means \"it's going to work in production\".\n\n\nOne way to approach the problem is to get a server\nand start manually configuring and installing everything,\nhacking about until it works,\nand maybe think about automating things later.footnote:[\nThis was, more or less, the approach I took in earlier editions of the book.\nWith a fair bit of testing thrown in, of course.]\n\nBut if there's one thing we've learned\nin the world of Agile/Lean software development,\nit's that taking smaller steps usually pays off.\n\nHow can we take smaller, safer steps towards a production deployment?\nCan we _simulate_ the process of moving to a server\nso that we can iron out all the bugs\nbefore we actually take the plunge?\nCan we then make small changes one at a time,\nsolving problems one by one,\nrather than having to bite off everything in one mouthful?\nCan we use our existing test suite to make sure things\nwork on the server, as well as locally?\n\nAbsolutely we can.  And if you've looked at the table of contents,\nI'm sure you're already guessing that Docker is going\nto be part of the answer.\n\n[role=\"notoc\"]\n=== An Overview of Our Deployment Procedure\n\nOver the next three chapters, I'm going to go through a deployment procedure.\nIt isn't meant to be the _perfect_ deployment procedure,\nso please don't take it as being best practice\nor a recommendation--it's meant to be an illustration,\nto show the kinds of issues involved in putting code into production,\nand where testing fits in.\n\n\n<<chapter_09_docker>>::\n  * Adapt our functional tests (FTs) so they can run against a container.\n  * Build a minimal Dockerfile with everything we need to run our site.\n  * Learn how to build and run a container on our machine.\n  * Get a first cut of our code up and running inside Docker,\n    with passing tests.\n\n\n<<chapter_10_production_readiness>>::\n  * Gradually, incrementally change the container configuration\n    to make it production-ready.\n  * Regularly rerun the FTs to check we didn't break anything.\n  * Address issues to do with the database, static files, secrets, and so on.\n\n\n<<chapter_11_server_prep>>::\n  * Set up a \"staging\" server,footnote:[\n  Some people prefer the term pre-prod or test environment.\n  It's all the same idea.] using the same infrastructure that we plan to use for production.\n  * Set up a real domain name and point it at this server.\n  * Install Ansible and flush out any networking issues.\n\n[role=\"pagebreak-before less_space\"]\n<<chapter_12_ansible>>::\n  * Gradually build up an Ansible playbook to deploy our containers on a real server.\n  * Again, use our FTs to check for any problems.\n  * Learn how to SSH (Secure Shell) into the server to debug things,\n    locate logs, and find other useful information.\n  * Confidently deploy to production once we have a working deployment script for staging.\n\n[role=\"notoc\"]\n=== TDD and Docker Versus the Danger Areas of Deployment\n\nHopefully you can start to see how the combination of TDD, Docker, staging,\nand automation are going to help minimise the risk of the various \"danger areas\":\n\nContainers as mini servers:: Containers will act as mini servers\n  letting us flush out issues with dependencies, static files, and so on.\n  A key advantage is that they'll give us a way of getting faster feedback cycles;\n  because we can spin them up locally almost instaneously,\n  we can very quicly see the effect of any changes.\n\nPackaging Python and system dependencies:: Our containers will package up both our Python and system dependencies,\n  including a production-ready web server and static files system,\n  as well as many production settings and configuration differences.\n  This minimises the difference between what we can test locally,\n  and what we will have on our servers.\n  As we'll see, it will give us a reliable way to reproduce bugs we see in production,\n  on our local machine.\n\nFully automated FTs:: Our FTs mean that we'll have a fully automated way of checking\n  that everything works.\n\nRunning FTs on staging server:: Later, when we deploy our containers to a staging server,\n  we can run the FTs against that too.\n  It'll be slightly slower and might involve some fiddly compromises,\n  but it'll give us one more layer of reassurance.\n\nAutomating build and deployment:: Finally, by fully automating container creation and deployment,\n  and by testing the end results of both these things,\n  we maximise reproducibility, thus minimising the risk of deployment to production.\n\nOh, but there's lots of fun stuff coming up!  Just you wait!\n\n"
  },
  {
    "path": "part3.asciidoc",
    "content": "[[part3]]\n[part]\n== Forms and Validation\n\n[partintro]\n--\nNow that we've got things into production,\nwe'll spend a bit of time on validation,\na core topic in web development.\n\nThere's quite a lot of Django-specific content in this part,\nso if you weren't familiar with Django before starting on the book,\nyou may find that taking a little time to run through the \nhttps://docs.djangoproject.com/en/5.2/intro/tutorial01/#creating-models[official Django tutorial]\nwill complement the next few chapters nicely.\n\nWith that said, there are lots of good lessons about test-driven development (TDD) in general in here too!\nSo, alternatively, if you're not that interested in Django itself,\ndon't worry too much about the details; instead, look out for the more general principles of testing.\n\nHere's a little preview of what we'll cover:\n\n* Splitting tests out across multiple files\n\n* Using a decorator for Selenium waits/polling\n\n* Database-layer validation and constraints\n\n* HTML5 form validation in the frontend\n\n* The Django forms framework\n\n* The trade-offs of frameworks in general, and when to stop using them\n\n* How far to go when testing for possible coding errors\n\n* An overview of all the typical tests for Django views\n\n--\n"
  },
  {
    "path": "part4.asciidoc",
    "content": "[[part4]]\n[part]\n== More Advanced Topics in Testing\n\n[partintro]\n--\n\n\"Oh my gosh, what?  Another section?  Harry, I'm exhausted. It's already \nbeen four hundred pages; I don't think I can handle a whole nother section\nof the book.  Particularly not if it's called 'Advanced&rsquo;...maybe I can\nget away with just skipping it?\"\n\nOh no, you can't! This may be called the \"advanced\" section,\nbut it's full of really important topics for test-driven development (TDD) and web development.\nNo way can you skip it.\nIf anything, it's 'even more important' than the first two sections.\n\nFirst off, we'll get into that sine qua non of web development: JavaScript.\nSeeing how TDD works in another language can give you a whole new perspective.\n\nWe'll be talking about a key technique, \"spiking\",\nwhich is where you relax the strict rules of TDD\nand allow yourself a bit of exploratory hacking.\n\nTIP: A common objection to TDD is \"how can I write tests if I don't even know what I'm doing?\"\n  Spiking is the bit where you get to play around and figure things out,\n  so you can come back and do it test-first later.\n\nWe'll be talking about how to integrate third-party systems, and how to test them. We'll cover mocking, which is hard to avoid in the world of Python testing.footnote:[Although not impossible! Check out the book https://www.cosmicpython.com[_Cosmic Python_], which has tips on testing without mocks. I happen to know that at least one of the two authors is incredibly wise.]\n\nWe'll talk about test fixtures and server-side debugging, and how to set up a continuous integration (CI) environment.\nNone of these things are take-it-or-leave-it, optional, luxury extras for your project--they're all\nvital!\n\n\nInevitably, the learning curve does get a little steeper in this section.\nYou may find yourself having to read things a couple of times before they sink in,\nor you may find that things don't work on the first go,\nand that you need to do a bit of debugging on your own.\n\nBut I encourage you to persist with it!\nThe harder it is, the more rewarding it is, right?\nAnd, remember, I'm always happy to help if you're stuck;\njust drop me an email at obeythetestinggoat@gmail.com.\n\nCome on; I promise the best is yet to come!\n--\n"
  },
  {
    "path": "praise.forbook.asciidoc",
    "content": "[\"dedication\", role=\"praise\"]\r\n== Praise for 'Test-Driven Development with Python'\r\n\r\n[quote, Michael Foord, Python Core Developer and Maintainer of unittest]\r\n____\r\n“In this book, Harry takes us on an adventure of discovery with Python and testing. It’s an excellent book, fun to read and full of vital information. It has my highest recommendations for anyone interested in testing with Python, learning Django or wanting to use Selenium. Testing is essential for developer sanity and it's a notoriously difficult field, full of trade-offs. Harry does a fantastic job of holding our attention whilst exploring real world testing practices.”\r\n____\r\n\r\n[quote, Kenneth Reitz, Fellow at Python Software Foundation]\r\n____\r\n“This book is far more than an introduction to Test Driven Development—it’s a complete best-practices crash course, from start to finish, into modern web application development with Python. Every web developer needs this book.” \r\n____\r\n\r\n[quote, Daniel and Audrey Roy Greenfeld, authors of \"Two Scoops of Django\" (Two Scoops Press)]\r\n____\r\n“Harry’s book is what we wish existed when we were learning Django. At a pace that’s achievable and yet delightfully challenging, it provides excellent instruction for Django and various test practices. The material on Selenium alone makes the book worth purchasing, but there's so much more!”\r\n____\r\n"
  },
  {
    "path": "praise.html",
    "content": "<section data-type=\"dedication\" xmlns=\"http://www.w3.org/1999/xhtml\" class=\"praise\">\n<h1>Praise for <em>Test-Driven Development with Python</em></h1>\n<blockquote>\n  <p>In this book, Harry takes us on an adventure of discovery with Python and testing. <span class=\"keep-together\">It’s an excellent</span> book, fun to read, and full of vital information. It has my highest recommendations for anyone interested in testing with Python, learning Django, or wanting to use Selenium. Testing is essential for developer sanity and it's a notoriously difficult field, full of trade-offs. Harry does a fantastic job of <span class=\"keep-together\">holding our attention whilst exploring</span> real-world testing practices.</p>\n  <p data-type=\"attribution\">Michael Foord, <span class=\"keep-together\">Python Core Developer and Maintainer of unittest</span></p>\n</blockquote>\n<blockquote>\n  <p>This book is far more than an introduction to test-driven development—it’s a complete best-practices crash course, from start to finish, into modern web <span class=\"keep-together\">application development</span> with Python. Every web developer needs this book.</p>\n  <p data-type=\"attribution\">Kenneth Reitz, <br/>Fellow at Python Software Foundation</p>\n</blockquote>\n<blockquote>\n  <p>Harry’s book is what we wish existed when we were learning Django. At a pace that’s achievable and yet delightfully challenging, it provides excellent instruction for Django and various test practices. The material on Selenium alone <span class=\"keep-together\">makes the book worth purchasing,</span> but there's so much more!</p>\n  <p data-type=\"attribution\">Daniel and Audrey Roy Greenfeld, <span class=\"keep-together\">authors of <em>Two Scoops of Django</em> (Two Scoops Press)</span></p>\n</blockquote>\n</section>\n"
  },
  {
    "path": "pre-requisite-installations.asciidoc",
    "content": "[[pre-requisites]]\n[preface]\n== Prerequisites and Assumptions\n\n(((\"prerequisite knowledge\", id=\"prereq00\")))\n(((\"Test-Driven Development (TDD)\", \"prerequisite knowledge assumed\", id=\"TDDprereq00\")))\nHere's an outline of what I'm assuming about you and what you already know,\nas well as what software you'll need ready and installed on your computer.\n\n\n=== Python 3 and Programming\n\n\n(((\"Python 3\", \"introductory books on\")))\nI've tried to write this book with beginners in mind,\nbut if you're new to programming, I'm assuming that you've already learned the basics of Python.\nSo if you haven't already, do run through a Python beginner's tutorial\nor get an introductory book like https://www.manning.com/books/the-quick-python-book-third-edition[_The Quick Python Book_]\nor https://oreil.ly/think-python-3e[_Think Python_],\nor (just for fun) https://inventwithpython.com/invent4thed[_Invent Your Own Computer Games with Python_]—all of which are excellent introductions.\n\nIf you're an experienced programmer but new to Python, you should get along just fine.\nPython is joyously simple to understand.\n\nYou should be able to follow this book on Mac, Windows, or Linux.\nDetailed installation instructions for each OS follow.\n\nTIP: This book was tested against Python 3.14.\n    If you're on an earlier version, you will find minor differences\n    in the way things look in my command output listings\n    (tracebacks won't have the `^^^^^^` carets marking error locations, for example),\n    so you're best off upgrading, ideally, if you can.\n\nIn any case, I expect you to have access to Python,\nto know how to launch it from a command line,\nand to know how to edit a Python file and run it.\nAgain, have a look at the three books I recommended previously if you're in any doubt.\n\n\n\n=== How HTML Works\n\n(((\"HTML\", \"tutorials\")))I'm\nalso assuming you have a basic grasp of how the web works—what HTML is,\nwhat a POST request is, and so on.  If you're not sure about those, you'll need to\nfind a basic HTML tutorial; there are a few at https://developer.mozilla.org/Learn_web_development.  If\nyou can figure out how to create an HTML page on your PC and look at it in your\nbrowser, and understand what a form is and how it might work, then you're\nprobably OK.\n\n\n=== Django\n\n\n(((\"Django framework\", \"tutorials\")))The\nbook uses the Django framework, which is (probably) the most well-established web framework\nin the Python world.\nI've written this book assuming that the reader has no prior knowledge of Django,\nbut if you're new to Python _and_ new to web development _and_ new to testing,\n you may occasionally find that there's just one too many topics and sets of concepts\nto try and take on board.\n If that's the case, I recommend taking a break from the book,\nand taking a look at a Django tutorial.\nhttps://tutorial.djangogirls.org[DjangoGirls] is the best, most beginner-friendly tutorial I know of.\nDjango's https://docs.djangoproject.com/en/5.2/intro/tutorial01[official tutorial]\nis also excellent for more experienced programmers.\n\n\n=== JavaScript\n\n\nThere's a little bit of JavaScript in the second half of the book.  If you\ndon't know JavaScript, don't worry about it until then. And if you find\nyourself a little confused, I'll recommend a couple of guides at that point.\n\n\nRead on for installation instructions.\n\n\n=== Required Software Installations\n\n(((\"software requirements\", id=\"soft00\")))\nAside from Python, you'll need:\n\nThe Firefox web browser::\n    Selenium can actually drive any of the major browsers,\n    but I chose Firefox because it's the least in hock to corporate interests.\n    (((\"Firefox\", \"benefits of\")))(((\"web browsers\", \"Firefox\")))\n\n\nThe Git version control system::\n    This is available for any platform, at http://git-scm.com.\n    On Windows, it comes with the _bash_ command line, which is needed for the book.\n    See <<windows-notes>>.\n    (((\"Git\", \"downloading\")))\n\n\nA virtualenv with Python 3.14, Django 5.2, and Selenium 4 in it::\n    Python's virtualenv and pip tools now come bundled with Python (they\n    didn't always used to, so this is a big hooray). (((\"virtualenv (virtual environment)\"))) Detailed instructions for\n    preparing your virtualenv follow.\n\n\n\n\n.MacOS Notes\n*******************************************************************************\n\n(((\"MacOS\")))\n(((\"Python 3\", \"installation and setup\", \"MacOS installation\")))\nMacOS installations for Python and Git are relatively straightforward:\n\n* Python 3.14 should install without a fuss from its\n  http://www.python.org[downloadable installer].  It will automatically install\n  `pip`, too.\n\n* Git's installer should also \"just work\".\n\n* You might also want to check out http://brew.sh[Homebrew].\n  It's a fairly reliable way of installing common Unix tools on a Mac.footnote:[I wouldn't recommend\n  installing Firefox via Homebrew though:\n  `brew` puts the Firefox binary in a strange location,\n  and it confuses Selenium.\n  You can work around it, but it's simpler to just install Firefox in the normal way.]\n  Although the normal Python installer is now fine, you may find Homebrew\n  useful in future. It does require you to download all 1.1 GB of Xcode, but\n  that also gives you a C compiler, which is a useful side effect.\n\n* If you want to run multiple different versions of Python on your Mac,\n  tools like https://docs.astral.sh/uv/guides/install-python[uv]\n  or https://github.com/pyenv/pyenv[pyenv] can help.\n  The downside is that each is one more fiddly tool to have to learn. But the key is to make sure, when creating your virtualenv,\n  that you use the right version of Python.\n  From then on, you shouldn't need to worry about it,\n  at least not when following this book.\n\nSimilarly to Windows, the test for all this is that you should be able to open\na terminal and just run `git`, `python3`, or `pip` from anywhere.  If you run\ninto any trouble, the search terms \"system path\" and \"command not found\" should\nprovide good troubleshooting resources.\n*******************************************************************************\n\n[role=\"pagebreak-before less_space\"]\n.Linux Notes\n*******************************************************************************\n\n(((\"Linux\")))\n(((\"Python 3\", \"installation and setup\", \"Linux installation\")))\nIf you're on Linux, I'm assuming you're already a glutton for punishment,\nso you don't need detailed installation instructions.\nBut in brief, if Python 3.14 isn't available directly from your package manager, you can try the following:\n\n* On Ubuntu, you can install the\n  https://oreil.ly/fHrpG[deadsnakes PPA].\n  Make sure you `apt install python3.14-venv` as well as just `python3.14` to\n  un-break the default Debian version of Python.\n\n* Alternatively, https://docs.astral.sh/uv/guides/install-python[uv]\n  and https://github.com/pyenv/pyenv[pyenv] both let you\n  manage multiple Python versions on the same machine,\n  but it is yet another thing to have to learn and remember.\n\n* Alternatively, compiling Python from source\n  is actually surprisingly easy!\n\nHowever you install it, make sure you can run Python 3.14 from a terminal.\n*******************************************************************************\n\n\n[[windows-notes]]\n.Windows Notes\n*******************************************************************************\n\n(((\"Windows\", \"tips\")))\n(((\"Python 3\", \"installation and setup\", \"Windows installation\")))\nWindows users can sometimes feel a little neglected in the open source world.\nAs macOS and Linux are so prevalent, it's easy to forget there's a world outside the Unix paradigm.\nBackslashes as directory separators?  Drive letters?  What?\nStill, it is absolutely possible to follow along with this book on Windows.\nHere are a few tips:\n\n* When you install Git for Windows, it will include \"Git Bash\".\n    Use this as your main command prompt throughout the book,\n    and you'll get all the useful GNU command-line tools\n    like `ls`, `touch`, and `grep`, plus forward-slash directory [.keep-together]#separators#.\n\n* During the Git installation,\n    you'll get the option to choose the default editor used by Git.\n    Unless you're already a Vim user (or are desperate to learn),\n    I'd suggest using a more familiar editor—even just Notepad!\n    See <<git-windows-default-editor>>.\n\n* Also in the Git installer, choose \"Use Windows' default console\";\n    otherwise, Python won't work properly in the Git Bash window.\n\n* When you install Python, tick the option that says \"Add python.exe to PATH\"\n    as in <<add-python-to-path>>,\n    so that you can easily run Python from the command line.\n\nThe test for all this is that you should be able to go to a Git Bash command prompt\nand just run `python` or `pip` from any folder.\n\n[role=\"width-95\"]\n[[git-windows-default-editor]]\n.Choose a nice default editor for Git\nimage::images/tdd3_0001.png[\"Screenshot of Git installer\"]\n\n[role=\"width-95\"]\n[[add-python-to-path]]\n.Add Python to the system path from the installer\nimage::images/tdd3_0002.png[\"Screenshot of python installer\"]\n\n// TODO: update screenshot above for 3.14\n\n*******************************************************************************\n\n[[firefox_gecko]]\n==== Installing Firefox\n\n\n(((\"Firefox\", \"installing\")))\nFirefox is available as a download for Windows and macOS from pass:[<a class=\"orm:hideurl\" href=\"https://www.firefox.com/\"><em>firefox.com</em></a>].\nOn Linux, you probably already have it installed,\nbut otherwise your package manager will have it.\n\n(((\"geckodriver\")))\nMake sure you have the latest version,\nso that the \"geckodriver\" browser automation module is available.\n\n\n=== Setting Up Your Virtualenv\n\n(((\"Python 3\", \"installation and setup\", \"virtualenv set up and activation\", id=\"P3installvirt00\")))\n(((\"virtualenv (virtual environment)\", \"installation and setup\", id=\"VEinstall00\")))\n(((\"\", startref=\"soft00\")))\nA Python virtualenv (short for virtual environment) is how you set up your\nenvironment for different Python projects.  It enables you to use different\npackages (e.g., different versions of Django, and even different versions of\nPython) in each project.  And because you're not installing things\nsystem-wide, it means you don't need root [keep-together]#permissions#.\n\nLet's create a virtualenv. I'm assuming you're working in a folder\ncalled _goat-book_, but you can name your work folder whatever you like.\nStick to the name \".venv\" for the virtualenv, though:\n\n[subs=quotes]\n.on Windows:\n----\n$ *cd goat-book*\n$ *py -3.14 -m venv .venv*\n----\n\nOn Windows, the `py` executable is a shortcut for different Python versions.  On\nMac or Linux, we use `python3.14`:\n\n\n[subs=quotes]\n.on Mac/Linux:\n----\n$ *cd goat-book*\n$ *python3.14 -m venv .venv*\n----\n\n\n\n==== Activating and Deactivating the Virtualenv\n\nWhenever you're working through the book,\nyou'll want to make sure your virtualenv has been \"activated\".\nYou can always tell when your virtualenv is active\nbecause, in your prompt, you'll see `(.venv)` in parentheses.\nBut you can also check by running `which python`\nto check whether Python is currently the system-installed one or the virtualenv one.\n\nThe command to activate the virtualenv is `source .venv/Scripts/activate` on Windows\nand `source .venv/bin/activate` on Mac/Linux.\nThe command to deactivate is just `deactivate`.\n\n[role=\"pagebreak-before\"]\nTry it out like this, on Windows:\n\n\n[subs=quotes]\n----\n$ *source .venv/Scripts/activate*\n(.venv)$\n(.venv)$ *which python*\n/C/Users/harry/goat-book/.venv/Scripts/python\n(.venv)$ *deactivate*\n$\n$ *which python*\n/c/Users/harry/AppData/Local/Programs/Python/Python312-32/python\n----\n\nOr like this, on Mac/Linux:\n\n[subs=quotes]\n----\n$ *source .venv/bin/activate*\n(.venv)$\n(.venv)$ *which python*\n/home/harry/goat-book/.venv/bin/python\n(.venv)$ *deactivate*\n$\n$ *which python*\n/usr/bin/python\n----\n\n\nTIP: Always make sure your virtualenv is active when working on the book. Look\n    out for the `(.venv)` in your prompt, or run `which python` to check.\n\n.Virtualenvs and IDEs\n*******************************************************************************\nIf you're using an IDE like PyCharm or Visual Studio Code,\nyou should be able to configure them to use the virtualenv\nas the default Python interpreter for the project.\n\nYou should then be able to launch a terminal inside the IDE\nwith the virtualenv already activated.\n*******************************************************************************\n\n\n==== Installing Django and Selenium\n\n(((\"Django framework\", \"installation\")))\n(((\"Selenium\", \"installation\")))\nWe'll install Django 5.2 and the latest Selenium.footnote:[\nYou might be wondering why I'm not mentioning a specific version of Selenium.\nIt's because Selenium is constantly being updated\nto keep up with changes in web browsers,\nand as we can't really pin our browser to a specific version,\nwe're best off using the latest Selenium.\nIt was version 4.24 at the time of writing.\n] Remember to make sure your virtualenv is active first!\n\n[subs=\"specialcharacters,quotes\"]\n----\n(.venv) $ *pip install \"django<6\" \"selenium\"*\nCollecting django<6\n  Downloading Django-5.2.3-py3-none-any.whl (8.0 MB)\n     ---------------------------------------- 8.1/8.1 MB 7.6 MB/s eta 0:00:00\nCollecting selenium\n  Downloading selenium-4.24.0-py3-none-any.whl (6.5 MB)\n     ---------------------------------------- 6.5/6.5 MB 6.3 MB/s eta 0:00:00\nCollecting asgiref>=3.8.1 (from django<6)\n  Downloading asgiref-3.8.1-py3-none-any.whl.metadata (9.3 kB)\nCollecting sqlparse>=0.3.1 (from django<6)Collecting sqlparse>=0.3.1 (from \ndjango<6)\n  [...]\nInstalling collected packages: sortedcontainers, websocket-client, urllib3,\ntyping_extensions, sqlparse, sniffio, pysocks, idna, h11, certifi, attrs,\nasgiref, wsproto, outcome, django, trio, trio-websocket, selenium\nSuccessfully installed asgiref-3.8.1 attrs-25.3.0 certifi-2025.4.26\ndjango-5.2.3 [...]\nselenium-4.32.0 [...]\n----\n\n\nCheck that it works:\n\n\n[subs=\"specialcharacters,quotes\"]\n----\n(.venv) $ *python -c \"from selenium import webdriver; webdriver.Firefox()\"*\n----\n\nThis should pop open a Firefox web browser,\nwhich you'll then need to close.\n\nTIP: If you see an error, you'll need to debug it before you go further.\n    On Linux/Ubuntu, I ran into https://github.com/mozilla/geckodriver/issues/2010[a bug],\n    which needs to be fixed by setting an environment variable called `TMPDIR`.\n\n\n==== Some Error Messages You're Likely to See When You Inevitably Fail to Activate Your Virtualenv\n\n(((\"troubleshooting\", \"virtualenv activation\")))\nIf you're new to virtualenvs--or\neven if you're not, to be honest--at some point\nyou're 'guaranteed' to forget to activate it,\nand then you'll be staring at an error message.\nHappens to me all the time.\nHere are some of the things to look out for:\n\n----\nModuleNotFoundError: No module named 'selenium'\n----\n\nOr:\n\n----\nModuleNotFoundError: No module named 'django'\n[...]\nImportError: Couldn't import Django. Are you sure it's installed and available\non your PYTHONPATH environment variable? Did you forget to activate a virtual\nenvironment?\n----\n\nAs always, look out for that `(.venv)` in your command prompt,\nand a quick\n`source .venv/Scripts/activate`\nor\n`source .venv/bin/activate`\nis probably what you need to get it working again.\n\n\n\nHere's another, for good measure:\n\n----\nbash: .venv/Scripts/activate: No such file or directory\n----\n\nThis means you're not currently in the right directory for working on the\nproject.  Try a `cd goat-book`, or similar.\n\nAlternatively, if you're sure you're in the right place, you may have run into\na bug from an older version of Python, where it wouldn't install\nan activate script that was compatible with Git Bash.  Reinstall Python 3, and\nmake sure you have version 3.6.3 or later, and then delete and re-create your\nvirtualenv.\n\nIf you see something like this, it's probably the same issue and you need to\nupgrade Python:\n\n----\nbash: @echo: command not found\nbash: .venv/Scripts/activate.bat: line 4:\n      syntax error near unexpected token `(\nbash: .venv/Scripts/activate.bat: line 4: `if not defined PROMPT ('\n----\n\n\nFinal one! Consider this:\n\n----\n'source' is not recognized as an internal or external command,\noperable program or batch file.\n----\n\nIf you see this, it's because you've launched the default Windows command prompt, +cmd+,\ninstead of Git Bash.  Close it and open the latter.\n\n\n.On Anaconda\n*******************************************************************************\n\nAnaconda is another tool for managing different Python environments.\nIt's particularly popular on Windows and for scientific computing,\nwhere it can be hard to get some of the compiled libraries to install.\n\nIn the world of web programming, it's much less necessary,\nso _I recommend you do not use Anaconda for this book_.\n\n*******************************************************************************\n\nHappy coding!\n(((\"\", startref=\"prereq00\")))\n(((\"\", startref=\"TDDprereq00\")))\n(((\"\", startref=\"P3installvirt00\")))\n(((\"\", startref=\"VEinstall00\")))\n\n\nNOTE: Did these instructions not work for you? Or have you got better ones? Get\n    in touch: obeythetestinggoat@gmail.com!\n"
  },
  {
    "path": "preface.asciidoc",
    "content": "[[preface]]\n[preface]\n== Preface\n\nThis book has been my attempt to share with the world the journey\nI took from \"hacking\" to \"software engineering\".\nIt's mainly about testing,\nbut there's a lot more to it, as you'll soon see.\n\nI want to thank you for reading it.\n\nIf you bought a copy, then I'm very grateful.\nIf you're reading the free online version,\nthen I'm _still_ grateful\nthat you've decided it's worth spending your time on.\nWho knows; perhaps once you get to the end,\nyou'll decide it's good enough to buy a physical copy for yourself or a friend.\n\n(((\"contact information\")))\n(((\"questions and comments\")))\n(((\"comments and questions\")))\n(((\"feedback\")))\nIf you have any comments, questions, or suggestions,\nI'd love to hear from you.\nYou can reach me directly via obeythetestinggoat@gmail.com,\nor on Mastodon https://fosstodon.org/@hjwp[@hjwp].\nYou can also check out\nhttp://www.obeythetestinggoat.com[the website and my blog].\n\nI hope you'll enjoy reading this book as much as I enjoyed writing it.\n\n//////////////////////////////////////////\n=== Third Edition Early Release History\n\ntbc\n\n\n\n.Third Edition Early Release Information\n*******************************************************************************\nIf you can see this, you are reading an early release of the third edition,\neither via www.obeythetestinggoat.com, or via the O'Reilly Learning site.\nCongratulations!\n\nAt the time of writing, all of the code listings\nin the main book (the chapters up to 25, but not the appendices)\nhave been updated to Python 3.14 and Django 5.\n\nWe're still in tech review, and many chapters still need a little work,\nbut the core of the book is there.\n\nThanks for reading, and please do send any and all feedback!\nAt this early release stage, feedback is more important than ever.\nYou can reach me via obeythetestinggoat@gmail.com\n\n*******************************************************************************\n//////////////////////////////////////////\n\n=== Why I Wrote a Book About Test-Driven Development\n\n_“Who are you, why have you written this book, and why should I\nread it?”_ I hear you ask.\n\n//IDEA: tighten up this section\n\n(((\"Test-Driven Development (TDD)\", \"need for\", id=\"TDDneed00\")))\nI was lucky enough early on in my career\nto fall in with a bunch of test-driven development (TDD) fanatics,\nand it made such a big impact on my programming\nthat I was burning to share it with everyone.\nYou might say I had the enthusiasm of a recent convert,\nand the learning experience was still a recent memory for me,\nso that's what led to the first edition, back in 2014.\n\nWhen I first learned Python\n(from Mark Pilgrim's excellent\nhttps://diveintopython3.net[_Dive Into Python_]),\nI came across the concept of TDD,\nand thought, \"Yes. I can definitely see the sense in that\".\nPerhaps you had a similar reaction when you first heard about TDD?\nIt seemed like a really sensible approach,\na really good habit to get into--like regularly flossing your teeth.\n\nThen came my first big project,\nand you can guess what happened--there was a client,\nthere were deadlines, there was lots to do,\nand any good intentions about TDD went straight out of the window.\n\nAnd, actually, it was fine.  I was fine.\n\nAt first.\n\nAt first I thought I didn't really need TDD because the website was small,\nand I could easily test whether things worked\nby just manually checking it out. Click\nthis link _here_, choose that drop-down item _there_,\nand _this_ should happen.\nEasy.\nThis whole \"writing tests\" thing sounded like it would have taken _ages_.\nAnd besides, I fancied myself,\nfrom the full height of my three weeks of adult coding experience,\nas being a pretty good programmer.\nI could handle it.\nEasy.\n\nThen came the fearful goddess Complexity.\nShe soon showed me the limits of my experience.\n\nThe project grew. Parts of the system started to depend on other parts.\nI did my best to follow good principles like DRY (don't repeat yourself),\nbut that just led to some pretty dangerous territory.\nSoon, I was playing with multiple inheritance.\nClass hierarchies eight levels deep. `eval` statements.\n\n\nI became scared of making changes to my code.\nI was no longer sure what depended on what,\nand what might happen if I changed this code _over here_...oh gosh, I think that bit over there inherits from it...no,\nit doesn't; it's overridden.\nOh, but it depends on that class variable.\nRight, well, as long as I override the override it should be fine.\nI'll just check--but checking was getting much harder.\nThere were lots of sections for the site now,\nand clicking through them all manually was starting to get impractical.\nBetter to leave well enough alone. Forget refactoring. Just make do.\n\n\nSoon I had a hideous, ugly mess of code. New development became painful.\n\nNot too long after this, I was lucky enough to get a job\nwith a company called Resolver Systems\n(now https://www.pythonanywhere.com[PythonAnywhere]),\nwhere\nhttps://martinfowler.com/bliki/ExtremeProgramming.html[extreme programming (XP)]\nwas the norm.\nThe people there introduced me to rigorous TDD.\n\nAlthough my previous experience had certainly opened my mind\nto the possible benefits of automated testing,\nI still dragged my feet at every stage.\n``I mean, testing in general might be a good idea, but 'really'?  All these tests?\nSome of them seem like a total waste of time...what? Functional tests _as well as_ unit tests?\nCome on, that's overdoing it! And this TDD test/minimal-code-change/test cycle?\nThis is just silly! We don't need all these baby steps!\nCome on—we can see what the right answer is; why don't we just skip to the end?''\n\nBelieve me, I second-guessed every rule, I suggested every shortcut,\nI demanded justifications for every seemingly pointless aspect of TDD—and I still came out seeing the wisdom of it all.\nI've lost count of the number of times I've thought, ``Thanks, tests'',footnote:[\nhttps://oreil.ly/LGP3g[Thests].]\nas a functional test uncovers a regression we would never have predicted,\nor a unit test saves me from making a really silly logic error.\nPsychologically, it's made development a much less stressful process.\nIt produces code that's a pleasure to work with.(((\"\", startref=\"TDDneed00\")))\n\nSo, let me tell you _all_ about it!\n\n\n\n=== Aims of This Book\n\nMy main aim is to impart a methodology--a way of doing web development, which\nI think makes for better web apps and happier developers. There's not much\npoint in a book that just covers material you could find by googling, so this\nbook isn't a guide to Python syntax, nor a tutorial on web development per se.\nInstead, I hope to teach you how to use TDD to get more reliably to our shared,\nholy goal: _clean code that works._\n\nWith that said: I will constantly refer to a real practical example, by\nbuilding a web app from scratch using tools like Django, Selenium, jQuery,\nand mocks. I'm not assuming any prior knowledge of any of these, so you\nshould come out the other end of this book with a decent introduction to\nthose tools, as well as the discipline of TDD.\n\nIn extreme programming we always pair-program, so I've imagined writing this\nbook as if I was pairing with my previous self, having to explain how the\ntools work and answer questions about why we code in this particular way. So,\nif I ever take a bit of a patronising tone, it's because I'm not all that\nsmart, and I have to be very patient with myself. And if I ever sound\ndefensive, it's because I'm the kind of annoying person that systematically\ndisagrees with whatever anyone else says, so sometimes it takes a lot of\njustifying to convince myself of anything.\n\n\n[role=\"pagebreak-before less_space\"]\n=== Outline\n\nI've split this book into four parts.\n\n<<part1>> (Chapters <<chapter_01,1>> to <<chapter_08_prettification,8>>): The Basics of TDD and Django::\n    We dive straight into building a simple web app using TDD.\n    We start by writing a functional test (with Selenium),\n    and then we go through the basics of Django--models, views, templates--with\n    rigorous unit testing at every stage.\n    I also introduce the Testing Goat.\n\n\n<<part2>> (Chapters <<chapter_09_docker,9>> to <<chapter_12_ansible,12>>): Going to Production::\n    These chapters are all about deploying your web app to an actual server.\n    We discuss how our tests, and the TDD practice of working incrementally,\n    can take a lot of the pain and risk out of what is normally quite a fraught process.\n\n\n<<part3>> (Chapters <<chapter_13_organising_test_files,13>> to <<chapter_16_advanced_forms,16>>): Forms and Validation::\n    Here, we get into some of the details of the Django Forms framework,\n    implementing validation, and data integrity using database constraints.\n    We discuss using tests to explore unfamiliar APIs,\n    and the limits of frameworks.\n\n\n<<part4>> (Chapters <<chapter_17_javascript,17>> to <<chapter_27_hot_lava,27>>): Advanced Topics in Testing::\n    Covers some of the more advanced topics in TDD,\n    including spiking (where we relax the rules of TDD temporarily),\n    mocking, working outside-in, and continuous integration (CI).\n\n\nNow, onto a little housekeeping...\n\n=== Conventions Used in This Book\n\n(((\"typographical conventions\")))The\nfollowing typographical conventions are used in this book:\n\n_Italic_:: Indicates new terms, URLs, email addresses, filenames, and file\nextensions\n\n`Constant width`:: Used for program listings and within paragraphs to\nrefer to program elements such as variable or function names, databases, data\ntypes, environment variables, statements, and keywords\n\n+*Constant width bold*+:: Shows commands or other text that should be typed\nliterally by the user\n\n[role=\"pagebreak-before\"]\nOccasionally I will use the symbol:\n\n[subs=\"specialcharacters,quotes\"]\n----\n[...]\n----\n\nto signify that some of the content has been skipped, to shorten long bits of\noutput, or to skip down to a relevant section. You will also encounter the following callouts:\n\n\n\nTIP: This element signifies a tip or suggestion.\n\nNOTE: This element signifies a general note or aside.\n\nWARNING: This element indicates a warning or caution.\n\n\n=== Submitting Errata\n\n(((\"errata\")))Spotted\na mistake or a typo?  The sources for this book are available on\nGitHub, and I'm always very happy to receive issues and pull requests:\nhttps://github.com/hjwp/Book-TDD-Web-Dev-Python[].\n\n=== Using Code Examples\n\n(((\"code examples, obtaining and using\")))Code\nexamples are available at https://github.com/hjwp/book-example/[]; you'll\nfind branches for each chapter there (e.g.,\nhttps://github.com/hjwp/book-example/tree/chapter_03_unit_test_first_view[]).\nYou can find a full list\nand some suggestions on ways of working with this repository\nin <<appendix_github_links>>.\n\nThis book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.\n\nWe appreciate, but do not require, attribution. An attribution usually includes\nthe title, author, publisher, and ISBN. For example: “_Test-Driven Development with Python_, 3rd edition, by Harry J.W. Percival (O’Reilly). Copyright 2025 Harry Percival, 978-1-098-14871-3”.\n\nIf you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at pass:[<a class=\"email\"\nhref=\"mailto:permissions@oreilly.com\"><em>permissions@oreilly.com</em></a>].\n\n=== O'Reilly Online Learning\n\n[role = \"ormenabled\"]\n[NOTE]\n====\nFor more than 40 years, pass:[<a href=\"https://oreilly.com\" class=\"orm:hideurl\"><em class=\"hyperlink\">O’Reilly Media</em></a>] has provided technology and business training, knowledge, and insight to help companies succeed.\n====\n\nOur unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O'Reilly and 200+ other publishers. For more information, visit pass:[<a href=\"https://oreilly.com\" class=\"orm:hideurl\"><em>https://oreilly.com</em></a>].\n\n=== How to Contact Us\n\nPlease address comments and questions concerning this book to the publisher:\n\n++++\n<ul class=\"simplelist\">\n  <li>O’Reilly Media, Inc.</li>\n  <li>141 Stony Circle, Suite 195</li>\n  <li>Santa Rosa, CA 95401</li>\n  <li>800-889-8969 (in the United States or Canada)</li>\n  <li>707-827-7019 (international or local)</li>\n  <li>707-829-0104 (fax)</li>\n  <li><a class=\"email\" href=\"mailto:support@oreilly.com\"><em>support@oreilly.com</em></a></li>\n  <li><a href=\"https://oreilly.com/about/contact.html\"><em>https://oreilly.com/about/contact.html</em></a></li>\n</ul>\n++++\n\nWe have a web page for this book, where we list errata, examples, and any additional information. You can access this page at link:$$https://oreil.ly/TDD-with-python-3e$$[].\n\n++++\n<!--Don't forget to update the link above.-->\n++++\n\nFor news and information about our books and courses, visit link:$$https://oreilly.com$$[].\n\nFind us on LinkedIn: link:$$https://linkedin.com/company/oreilly-media$$[].\n\nWatch us on YouTube: link:$$https://youtube.com/oreillymedia$$[].\n\n[role=\"pagebreak-before less_space\"]\n[[video_plug]]\n=== Companion Video\n\n(((\"companion video\")))(((\"video-based instruction\")))(((\"Test-Driven Development (TDD)\", \"video-based instruction\")))\nI've recorded a \nhttps://learning.oreilly.com/videos/test-driven-development/9781491919163[10-part video series\nto accompany this book].footnote:[The video has not been updated for the third edition,\nbut the content is all mostly the same.]\nIt covers the content of <<part1>>.\nIf you find that you learn well from video-based material, then I encourage you to check it out.\nOver and above what's in the book,\nit should give you a feel for what the \"flow\" of TDD is like,\nflicking between tests and code and explaining the thought process as we go.\n\nPlus, I'm wearing a delightful yellow t-shirt.\n\n[[video-screengrab]]\nimage::images/tdd3_00in01.png[screengrab from video]\n\n[role=\"pagebreak-before less_space\"]\n=== License for the Free Edition\n\nIf you're reading the free edition of this book hosted at http://www.obeythetestinggoat.com,\nthen the license is\nhttps://creativecommons.org/licenses/by-nc-nd/4.0/legalcode[Creative Commons Attribution-NonCommercial-NoDerivatives].footnote:[The no-derivs clause is there\nbecause O'Reilly wants to maintain some control over derivative works,\nbut it often does grant permissions for things,\nso don't hesitate to get in touch if you want to build something\nbased on this book.]\nI want to thank O'Reilly for its fantastic attitude towards\nlicensing; most publishers aren't so forward-thinking.\n\nI see this as a \"try before you buy\" scheme really.\nIf you're reading this book it's for professional reasons,\nso I hope that if you like it, you'll buy a copy--if not for yourself,\nthen for a friend!\nO'Reilly has been great, it deserves your support.\nYou'll find http://www.obeythetestinggoat.com[links to buy back on the home page].\n"
  },
  {
    "path": "pygments-default.css",
    "content": "pre.pygments .hll { background-color: #ffffcc }\npre.pygments { background: #f8f8f8; }\npre.pygments .tok-c { color: #3D7B7B; font-style: italic } /* Comment */\npre.pygments .tok-err { border: 1px solid #FF0000 } /* Error */\npre.pygments .tok-k { color: #008000; font-weight: bold } /* Keyword */\npre.pygments .tok-o { color: #666666 } /* Operator */\npre.pygments .tok-ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */\npre.pygments .tok-cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */\npre.pygments .tok-cp { color: #9C6500 } /* Comment.Preproc */\npre.pygments .tok-cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */\npre.pygments .tok-c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */\npre.pygments .tok-cs { color: #3D7B7B; font-style: italic } /* Comment.Special */\npre.pygments .tok-gd { color: #A00000 } /* Generic.Deleted */\npre.pygments .tok-ge { font-style: italic } /* Generic.Emph */\npre.pygments .tok-ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */\npre.pygments .tok-gr { color: #E40000 } /* Generic.Error */\npre.pygments .tok-gh { color: #000080; font-weight: bold } /* Generic.Heading */\npre.pygments .tok-gi { color: #008400 } /* Generic.Inserted */\npre.pygments .tok-go { color: #717171 } /* Generic.Output */\npre.pygments .tok-gp { color: #000080; font-weight: bold } /* Generic.Prompt */\npre.pygments .tok-gs { font-weight: bold } /* Generic.Strong */\npre.pygments .tok-gu { color: #800080; font-weight: bold } /* Generic.Subheading */\npre.pygments .tok-gt { color: #0044DD } /* Generic.Traceback */\npre.pygments .tok-kc { color: #008000; font-weight: bold } /* Keyword.Constant */\npre.pygments .tok-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\npre.pygments .tok-kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\npre.pygments .tok-kp { color: #008000 } /* Keyword.Pseudo */\npre.pygments .tok-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\npre.pygments .tok-kt { color: #B00040 } /* Keyword.Type */\npre.pygments .tok-m { color: #666666 } /* Literal.Number */\npre.pygments .tok-s { color: #BA2121 } /* Literal.String */\npre.pygments .tok-na { color: #687822 } /* Name.Attribute */\npre.pygments .tok-nb { color: #008000 } /* Name.Builtin */\npre.pygments .tok-nc { color: #0000FF; font-weight: bold } /* Name.Class */\npre.pygments .tok-no { color: #880000 } /* Name.Constant */\npre.pygments .tok-nd { color: #AA22FF } /* Name.Decorator */\npre.pygments .tok-ni { color: #717171; font-weight: bold } /* Name.Entity */\npre.pygments .tok-ne { color: #CB3F38; font-weight: bold } /* Name.Exception */\npre.pygments .tok-nf { color: #0000FF } /* Name.Function */\npre.pygments .tok-nl { color: #767600 } /* Name.Label */\npre.pygments .tok-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\npre.pygments .tok-nt { color: #008000; font-weight: bold } /* Name.Tag */\npre.pygments .tok-nv { color: #19177C } /* Name.Variable */\npre.pygments .tok-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\npre.pygments .tok-w { color: #bbbbbb } /* Text.Whitespace */\npre.pygments .tok-mb { color: #666666 } /* Literal.Number.Bin */\npre.pygments .tok-mf { color: #666666 } /* Literal.Number.Float */\npre.pygments .tok-mh { color: #666666 } /* Literal.Number.Hex */\npre.pygments .tok-mi { color: #666666 } /* Literal.Number.Integer */\npre.pygments .tok-mo { color: #666666 } /* Literal.Number.Oct */\npre.pygments .tok-sa { color: #BA2121 } /* Literal.String.Affix */\npre.pygments .tok-sb { color: #BA2121 } /* Literal.String.Backtick */\npre.pygments .tok-sc { color: #BA2121 } /* Literal.String.Char */\npre.pygments .tok-dl { color: #BA2121 } /* Literal.String.Delimiter */\npre.pygments .tok-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\npre.pygments .tok-s2 { color: #BA2121 } /* Literal.String.Double */\npre.pygments .tok-se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */\npre.pygments .tok-sh { color: #BA2121 } /* Literal.String.Heredoc */\npre.pygments .tok-si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */\npre.pygments .tok-sx { color: #008000 } /* Literal.String.Other */\npre.pygments .tok-sr { color: #A45A77 } /* Literal.String.Regex */\npre.pygments .tok-s1 { color: #BA2121 } /* Literal.String.Single */\npre.pygments .tok-ss { color: #19177C } /* Literal.String.Symbol */\npre.pygments .tok-bp { color: #008000 } /* Name.Builtin.Pseudo */\npre.pygments .tok-fm { color: #0000FF } /* Name.Function.Magic */\npre.pygments .tok-vc { color: #19177C } /* Name.Variable.Class */\npre.pygments .tok-vg { color: #19177C } /* Name.Variable.Global */\npre.pygments .tok-vi { color: #19177C } /* Name.Variable.Instance */\npre.pygments .tok-vm { color: #19177C } /* Name.Variable.Magic */\npre.pygments .tok-il { color: #666666 } /* Literal.Number.Integer.Long */"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\nrequires-python = \">=3.14\"\n\n[project]\nname = \"goat-book\"\nversion = \"0\"\n\n# most requts are deliberately unpinned so we stay up to date with deps,\n# and CI will warn when things change.\ndependencies = [\n    \"requests\",\n    \"lxml\",\n    \"lxml-stubs\",\n    \"cssselect\",\n    \"django<6\",\n    \"django-types\",\n    \"pygments\",\n    \"docopt\",\n    \"requests\",\n    \"selenium<5\",\n    \"pytest\",\n    # \"pytest-xdist\",\n    \"ruff\",\n    \"black\",  # needed as a marker to tell django to use black\n    \"whitenoise\", # from chap 10 on\n]\n\n[tool.setuptools]\npackages = []\n\n[tool.ruff.lint]\nselect = [\n    # pycodestyle\n    \"E\",\n    # Pyflakes\n    \"F\",\n    # pyupgrade\n    \"UP\",\n    # flake8-bugbear\n    \"B\",\n    # flake8-simplify\n    \"SIM\",\n    # isort\n    \"I\",\n    # pylint\n    \"PL\",\n]\nignore = [\n    \"E741\",  # single-letter variable\n    \"E731\",  # allow lambdas\n    \"PLR2004\",  # magic values\n]\n\n[tool.pyright]\n# these help pyright in neovide to find its way around\nvenvPath = \".\"\nvenv = \".venv\"\n# most of the source for the book itself is untyped\ntypeCheckingMode = \"standard\"\n\n[tool.pytest.ini_options]\n# -r N disables the post-test summary\naddopts = [\"--tb=short\", \"-r N\", \"--color=yes\"]\n"
  },
  {
    "path": "rename-chapter.sh",
    "content": "#!/bin/bash\nset -eux\nset -o pipefail\n\nOLD_CHAPTER=$1\nNEW_NAME=$2\n\ngit mv \"$OLD_CHAPTER.asciidoc\" \"$NEW_NAME.asciidoc\"\nmv \"$OLD_CHAPTER.html\" \"$NEW_NAME.html\" || touch \"$NEW_NAME.html\"\n\nif [ -e \"tests/test_$OLD_CHAPTER.py\" ]; then\n    git mv \"tests/test_$OLD_CHAPTER.py\" \"tests/test_$NEW_NAME.py\"\nfi\n\ngit mv \"source/$OLD_CHAPTER\" \"source/$NEW_NAME\"\n\ncd \"source/$NEW_NAME/superlists\" \ngit switch \"$OLD_CHAPTER\"\ngit switch -c \"$NEW_NAME\"\ngit push -u local\ngit push -u origin\ncd ../../..\n\ngit grep -l \"$OLD_CHAPTER\" | xargs sed -i '' \"s/$OLD_CHAPTER/$NEW_NAME/g\"\n\n# make \"test_$NEW_NAME\" || echo -e \"\\a\"\n\necho git commit -am \\'rename \"$OLD_CHAPTER\" to \"$NEW_NAME\".\\'\n"
  },
  {
    "path": "research/js-testing.rst",
    "content": "Options:\n\nQunit -- weird syntax, but seems popular\nYUI -- familiar\njasmine - seems sane\njs-test-driver: by google, but seems to be server-only\n\nSinon - for mocking\n\nhttp://www.letscodejavascript.com/\n"
  },
  {
    "path": "run_test_tests.sh",
    "content": "#!/bin/bash\nPYTHONHASHSEED=0 pytest \\\n    --failed-first \\\n    --tb=short \\\n    -k 'not test_listings_and_commands_and_output' \\\n    \"$@\" \\\n    tests/\n# py.test --tb=short `ls tests/test_* | grep -v test_chapter | grep -v test_server`\n"
  },
  {
    "path": "server-quickstart.md",
    "content": "# Ultra-brief instructions for how to get a Linux server\n\nThese instructions are meant as companion to the \n[server prep chapter of my book](https://www.obeythetestinggoat.com/book/chapter_11_server_prep.html).\nThey're almost telegraphic in style, but I hope they're better than nothing!\n\n\n## Use Digital Ocean\n\nI didn't want to make a specific recommendation in the book itself,\nbut I'll make one here.\nI like [Digital Ocean](https://m.do.co/c/876844cd6b2e).\nGood value for money, fast servers,\nand you can get a couple of months' worth of free credit\nby following [my referral link](https://m.do.co/c/876844cd6b2e).\n\n\n## Generate an SSH key\n\nSSH aka \"secure shell\" is a protocol for running a terminal\nsession on a remote server, across the network.\nIt involves authentication, as you might expect,\nand different types of it.\nYou can use usernames and passwords,\nbut public/private key authentication is more convenient,\nand (as always, arguably) more secure.\n\nIf you've never created one before, the command is\n\n```bash\nssh-keygen\n```\n\n**NOTE** _If you're on Windows,\nyou need to be using Git-Bash for `ssh-keygen` and `ssh` to work.\nThere's more info in the\n[installation instructions chapter](https://www.obeythetestinggoat.com/book/pre-requisite-installations.html)_\n\nJust accept all the defaults if you really want to just get started in a hurry,\nand no passphrase.\n\nLater on, you'll want to re-create a key with a passphrase for extra security,\nbut that means you have to figure out how to save that passphrase in such a way\nthat Ansible won't ask for it later, and I don't have time to write instructions\nfor that now!\n\nMake a note of your \"public key\"\n\n```bash\ncat ~/.ssh/id_rsa.pub\n```\n\nMore info on public key authentication [here](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/)\nand [here](https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/)\n\n\n## Start a Server aka VM aka Droplet\n\nA \"droplet\" is Digital Ocean's name for a server.\nPick the default Ubuntu, the cheapest type,\nand whichever region is closest to you.\nYou won't need access to the ancillary services that are available\n(Block storage, a VPC network, IPv6, User-Data, Monitoring, Back-Ups, etc,\ndon't worry about those)\n\n* Choose **New SSH Key** and upload your public key from above\n\nMake a note of your server's IP address once it's started\n\n\n## Log in for the first time\n\n\n```bash\nssh root@your-server-ip-address-here\n```\n\nIt should just magically find your SSH key and log you in\nwithout any need for a password.  Hooray!\n\n\n## Create a non-root user\n\nIt's good security practice to avoid using the root user directly.\nLet's create a non-root user with super-user (\"sudo\") privileges,\na bit like you have on your own machine.\n\n```bash\nuseradd -m -s /bin/bash elspeth # add user named elspeth \n# -m creates a home folder, -s sets elspeth to use bash by default\n\nusermod -a -G sudo elspeth # add elspeth to the sudoers group\n\n# allow elspeth to sudo without retyping password\necho 'elspeth ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/elspeth\n\n # set password for elspeth (you'll need to type one in)\npasswd elspeth\n\nsu - elspeth # switch-user to being elspeth!\n```\n\n\n## Add your public key to the non-root user as well.\n\n* Copy your public key to your clipboard, and then\n\n\n```bash\n# as user elspeth\nmkdir -p ~/.ssh\necho 'PASTE\nYOUR\nPUBLIC\nKEY\nHERE' >> ~/.ssh/authorized_keys\n```\n\nNow log out from the server,\nand verify you can SSH in as elspeth from your laptop\n\n\n```bash\nssh elspeth@your-server-ip-address-here\n```\n\nAlso check you can use \"sudo\" as elspeth\n\n```bash\nsudo echo hi\n```\n\n\n## Registering a domain name\n\nThere's one more thing you need to do in the book,\nwhich is to map a domain name to your server's IP address.\n\nIf you don't already own a domain name you can use\n(you don't have to use the *www.* subdomain, you could use *superlists.yourdomain.com*),\nthen you'll need to get one from a \"domain registrar\".\nThere are loads out there, I quite like Gandi or the slightly-more-friendly 123-reg.\n\nThere aren't any registrars offering free domain names any more,\nbut the cheapest registrar I've found is https://www.ionos.co.uk/,\nwhere last I checked you could get a domain for one pound, like $1.50, for a year.\nBut I haven't used them myself personally.\n\n\n# Pull requests and suggestions accepted!\n\nI literally threw these instructions together in 10 minutes flat, so I'm \nsure they could do with improvements.  Please send in suggestions, typos,\nfixes, any common \"gotchas\" you ran into that you think I should mention.\n\n\n"
  },
  {
    "path": "source/blackify-chap.sh",
    "content": "#!/bin/bash\nset -e\n\nPREV=$1\nCHAP=$2\n\n# assumes a git remote local pointing at a local bare repo...\nREPO=local\n\ncd $CHAP/superlists\n\ngit fetch $REPO\n\nSTARTCOMMIT=\"$(git rev-parse $PREV)\"\nENDCOMMIT=\"$(git rev-parse $CHAP)\"\n\ngit switch $CHAP\ngit reset --hard $REPO/$PREV\nruff format .\ngit commit -am\"initial black commit\" --allow-empty\n\ngit rev-list $STARTCOMMIT^..$ENDCOMMIT| tail -r | xargs -n1 sh -c 'git co $0 -- . && ruff format . && git add . && git stwdiff && git commit -am \"$(git show -s --format=%B $0)\"'\n\ngit diff -w $REPO/$CHAP\n\n\ncd ../..\n"
  },
  {
    "path": "source/feed-thru-cherry-picks.sh",
    "content": "#!/bin/bash\n# replay all the commits for a given chapter ($CHAP)\n# onto the latest version of the previous chapter ($PREV)\nset -e\n\nPREV=$1\nCHAP=$2\n\n# assumes a git remote called \"local\" (eg pointing at a local bare repo...)\n# rather than using origin/github,\n# this allows us to feed through changes without pushing to github\nREPO=local\n\ncd \"$CHAP/superlists\"\n\n# determine the commit we want to start from,\n# which is the last commit of the $PREV branch as it was *before* our new version.\n# We assume that the repo version in $CHAP/superlists has *not* got this latest version yet.\n# (so that's why we don't do the git fetch until after this step)\nSTART_COMMIT=\"$(git rev-list -n 1 \"$REPO/$PREV\")\"\nCHAP_COMMIT_LIST=\"$START_COMMIT..$REPO/$CHAP\"\n\n# check START_COMMIT exists in $CHAP branch's history\n# https://stackoverflow.com/a/4129070/366221\nif [ \"$(git merge-base \"$START_COMMIT\" \"$CHAP\")\" != \"$START_COMMIT\" ]; then\n    echo \"Error: $START_COMMIT is not in the history of $CHAP\"\n    exit 1\nfi\n\n# now we pull down the latest version of $PREV\ngit fetch \"$REPO\"\n\n# reset our chapter to the new version of the end of $PREV\ngit switch \"$CHAP\"\ngit reset --hard \"$REPO/$PREV\"\n\n# now cherry pick all the old commits from $CHAP onto this new base.\n# (we can't just use rebase because it does the wrong thing with trying to find common history)\ngit cherry-pick -Xrename-threshold=20%  \"$CHAP_COMMIT_LIST\"\n\n\n# display a little diff to sanity-check what we've done.\ngit diff -w \"$REPO/$CHAP\"\n\ncd ../..\n"
  },
  {
    "path": "source/fix-commit-numbers.py",
    "content": "#!/usr/bin/env python\n\n# use with\n# FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f --msg-filter $PWD/../../fix-commit-numbers.py 3fc31f1b..\n\nimport re\nimport sys\n\nwhile incoming := sys.stdin.readline():\n    # if m := re.match(r\"(.+) --ch(\\d+)l(\\d+)(-?)(\\d?)--\", incoming.rstrip()):\n    if m := re.match(r\"(.+) (\\(|--)ch(\\d+)l(\\d+)(-?)(\\d?)(\\)|--)\", incoming.rstrip()):\n        prefix, sep1, chap_num, listing_num, extra_dash, suffix, sep2 = m.groups()\n        chap_num = int(chap_num)\n        listing_num = int(listing_num)\n        suffix = int(suffix) if suffix else None\n        if chap_num == 14:\n            pass\n\n        elif suffix and listing_num == 30:\n            listing_num = listing_num - 13 + suffix\n\n        if listing_num == 30:\n            assert suffix is None\n            listing_num = 21\n        elif listing_num == 31:\n            assert suffix is None\n            listing_num = 22\n        elif listing_num == 32 and suffix == \"1\":\n            listing_num = 23\n        elif listing_num == 32 and suffix == \"2\":\n            listing_num = 24\n        elif listing_num == 33 and suffix is None:\n            listing_num = 25\n        elif listing_num == 33 and suffix:\n            listing_num = 26\n        elif listing_num == 34:\n            listing_num = 27\n        elif listing_num == 35:\n            listing_num = 28\n        elif listing_num == 36:\n            assert suffix\n            listing_num = 28 + suffix\n\n        print(f\"{prefix} {sep1}ch{chap_num}l{listing_num:03d}{sep2}\")\n    else:\n        print(incoming, end=\"\")\n"
  },
  {
    "path": "source/push-back.sh",
    "content": "#!/bin/bash\nset -e\n\nCHAP=$1\n\ncd \"$CHAP/superlists\"\ngit push --force-with-lease local \"$CHAP\"\ngit push --force-with-lease origin \"$CHAP\"\n\ncd ../..\n"
  },
  {
    "path": "tests/actual_manage_py_test.output",
    "content": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE..s.............EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE..............................EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE...............................EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE.EEEEEEEEEEEEEEEEEEEEE\n======================================================================\nERROR: test_get_all_permissions (django.contrib.auth.tests.auth_backends.AnonymousUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_module_perms (django.contrib.auth.tests.auth_backends.AnonymousUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.AnonymousUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perms (django.contrib.auth.tests.auth_backends.AnonymousUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_message_attrs (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_perm_in_perms_attrs (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_perms_attrs (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_is_accessed (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_not_accessed (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_attrs (django.contrib.auth.tests.context_processors.AuthContextProcessorTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_named_urls (django.contrib.auth.tests.views.AuthViewNamedURLTests)\nNamed URLs should be reversible\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_inactive_user (django.contrib.auth.tests.forms.AuthenticationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_inactive_user_i18n (django.contrib.auth.tests.forms.AuthenticationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_username (django.contrib.auth.tests.forms.AuthenticationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_success (django.contrib.auth.tests.forms.AuthenticationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_username_field_label (django.contrib.auth.tests.forms.AuthenticationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_anonymous_user (django.contrib.auth.tests.basic.BasicTestCase)\nCheck the properties of the anonymous user\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_createsuperuser_management_command (django.contrib.auth.tests.basic.BasicTestCase)\nCheck the operation of the createsuperuser management command\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_createsuperuser_nolocale (django.contrib.auth.tests.basic.BasicTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_createsuperuser_non_ascii_verbose_name (django.contrib.auth.tests.basic.BasicTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_user_model (django.contrib.auth.tests.basic.BasicTestCase)\nThe current user model can be retrieved\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_superuser (django.contrib.auth.tests.basic.BasicTestCase)\nCheck the creation and properties of a superuser\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_swappable_user (django.contrib.auth.tests.basic.BasicTestCase)\nThe current user model can be swapped out for another\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_swappable_user_bad_setting (django.contrib.auth.tests.basic.BasicTestCase)\nThe alternate user setting must point to something in the format app.model\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_swappable_user_nonexistent_model (django.contrib.auth.tests.basic.BasicTestCase)\nThe current user model must point to an installed model\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user (django.contrib.auth.tests.basic.BasicTestCase)\nCheck that users can be created and can set their password\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_no_email (django.contrib.auth.tests.basic.BasicTestCase)\nCheck that users can be created without an email\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_change_done_fails (django.contrib.auth.tests.views.ChangePasswordTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_change_done_succeeds (django.contrib.auth.tests.views.ChangePasswordTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_change_fails_with_invalid_old_password (django.contrib.auth.tests.views.ChangePasswordTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_change_fails_with_mismatched_passwords (django.contrib.auth.tests.views.ChangePasswordTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_change_succeeds (django.contrib.auth.tests.views.ChangePasswordTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_changelist_disallows_password_lookups (django.contrib.auth.tests.views.ChangelistTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_that_changepassword_command_changes_joes_password (django.contrib.auth.tests.management.ChangepasswordManagementCommandTestCase)\nExecuting the changepassword management command should change joe's password\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_that_max_tries_exits_1 (django.contrib.auth.tests.management.ChangepasswordManagementCommandTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_createsuperuser (django.contrib.auth.tests.management.CreatesuperuserManagementCommandTestCase)\nCheck the operation of the createsuperuser management command\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_email_in_username (django.contrib.auth.tests.management.CreatesuperuserManagementCommandTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_swappable_user (django.contrib.auth.tests.management.CreatesuperuserManagementCommandTestCase)\nA superuser can be created when a custom User model is in use\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_swappable_user_missing_required_field (django.contrib.auth.tests.management.CreatesuperuserManagementCommandTestCase)\nA Custom superuser won't be created when a required field isn't provided\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_verbosity_zero (django.contrib.auth.tests.management.CreatesuperuserManagementCommandTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_perms (django.contrib.auth.tests.auth_backends.CustomPermissionsUserModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_all_superuser_permissions (django.contrib.auth.tests.auth_backends.CustomPermissionsUserModelBackendTest)\nA superuser has all permissions. Refs #14795\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_no_object_perm (django.contrib.auth.tests.auth_backends.CustomPermissionsUserModelBackendTest)\nRegressiontest for #12462\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.CustomPermissionsUserModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_authenticate (django.contrib.auth.tests.auth_backends.CustomUserModelBackendAuthenticateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_username_non_unique (django.contrib.auth.tests.management.CustomUserModelValidationTestCase)\nA non-unique USERNAME_FIELD should raise a model validation error.\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_username_not_in_required_fields (django.contrib.auth.tests.management.CustomUserModelValidationTestCase)\nUSERNAME_FIELD should not appear in REQUIRED_FIELDS.\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_valid_custom_user (django.contrib.auth.tests.views.CustomUserPasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_perms (django.contrib.auth.tests.auth_backends.ExtensionUserModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_all_superuser_permissions (django.contrib.auth.tests.auth_backends.ExtensionUserModelBackendTest)\nA superuser has all permissions. Refs #14795\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_no_object_perm (django.contrib.auth.tests.auth_backends.ExtensionUserModelBackendTest)\nRegressiontest for #12462\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.ExtensionUserModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_implementation (django.contrib.auth.tests.management.GetDefaultUsernameTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing (django.contrib.auth.tests.management.GetDefaultUsernameTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_i18n (django.contrib.auth.tests.management.GetDefaultUsernameTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_simple (django.contrib.auth.tests.management.GetDefaultUsernameTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_module_perms (django.contrib.auth.tests.auth_backends.InActiveUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.InActiveUserBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_builtin_user_isactive (django.contrib.auth.tests.models.IsActiveTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_is_active_field_default (django.contrib.auth.tests.models.IsActiveTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_is_created_and_added_to_group (django.contrib.auth.tests.models.LoadDataWithNaturalKeysTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_is_created_and_added_to_group (django.contrib.auth.tests.models.LoadDataWithoutNaturalKeysTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: testCallable (django.contrib.auth.tests.decorators.LoginRequiredTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: testLoginRequired (django.contrib.auth.tests.decorators.LoginRequiredTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: testLoginRequiredNextUrl (django.contrib.auth.tests.decorators.LoginRequiredTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: testView (django.contrib.auth.tests.decorators.LoginRequiredTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_current_site_in_context_after_login (django.contrib.auth.tests.views.LoginTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_security_check (django.contrib.auth.tests.views.LoginTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_https_login_url (django.contrib.auth.tests.views.LoginURLSettings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_login_url_with_querystring (django.contrib.auth.tests.views.LoginURLSettings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_remote_login_url (django.contrib.auth.tests.views.LoginURLSettings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_remote_login_url_with_next_querystring (django.contrib.auth.tests.views.LoginURLSettings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_standard_login_url (django.contrib.auth.tests.views.LoginURLSettings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_14377 (django.contrib.auth.tests.views.LogoutTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_default (django.contrib.auth.tests.views.LogoutTest)\nLogout without next_page option renders the default template\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_with_custom_redirect_argument (django.contrib.auth.tests.views.LogoutTest)\nLogout with custom query string redirects to specified resource\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_with_next_page_specified (django.contrib.auth.tests.views.LogoutTest)\nLogout with next_page option given redirects to specified resource\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_with_overridden_redirect_url (django.contrib.auth.tests.views.LogoutTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_with_redirect_argument (django.contrib.auth.tests.views.LogoutTest)\nLogout with query string redirects to specified resource\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_security_check (django.contrib.auth.tests.views.LogoutTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_check_password (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/auth/tests/handlers.py\", line 21, in test_check_password\n    User.objects.create_user('test', 'test@example.com', 'test')\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/manager.py\", line 256, in __get__\n    self.model._meta.object_name, self.model._meta.swapped\nAttributeError: Manager isn't available; User has been swapped for 'auth.ExtensionUser'\n\n======================================================================\nERROR: test_check_password (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 268, in __call__\n    self._post_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 533, in _post_teardown\n    self._fixture_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 553, in _fixture_teardown\n    skip_validation=True, reset_sequences=False)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/__init__.py\", line 161, in call_command\n    return klass.execute(*args, **defaults)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 255, in execute\n    output = self.handle(*args, **options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 385, in handle\n    return self.handle_noargs(**options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/commands/flush.py\", line 46, in handle_noargs\n    sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/sql.py\", line 113, in sql_flush\n    tables = connection.introspection.django_table_names(only_existing=True)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 995, in django_table_names\n    existing_tables = self.table_names()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 965, in table_names\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_check_password_custom_user (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 220, in inner\n    return test_func(*args, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/auth/tests/handlers.py\", line 45, in test_check_password_custom_user\n    CustomUser._default_manager.create_user('test@example.com', '1990-01-01', 'test')\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/auth/tests/custom_user.py\", line 29, in create_user\n    user.save(using=self._db)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/base.py\", line 546, in save\n    force_update=force_update, update_fields=update_fields)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/base.py\", line 650, in save_base\n    result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/manager.py\", line 215, in _insert\n    return insert_query(self.model, objs, fields, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 1661, in insert_query\n    return query.get_compiler(using=using).execute_sql(return_id)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 935, in execute_sql\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_check_password_custom_user (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 268, in __call__\n    self._post_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 533, in _post_teardown\n    self._fixture_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 553, in _fixture_teardown\n    skip_validation=True, reset_sequences=False)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/__init__.py\", line 161, in call_command\n    return klass.execute(*args, **defaults)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 255, in execute\n    output = self.handle(*args, **options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 385, in handle\n    return self.handle_noargs(**options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/commands/flush.py\", line 46, in handle_noargs\n    sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/sql.py\", line 113, in sql_flush\n    tables = connection.introspection.django_table_names(only_existing=True)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 995, in django_table_names\n    existing_tables = self.table_names()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 965, in table_names\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_groups_for_user (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/auth/tests/handlers.py\", line 62, in test_groups_for_user\n    user1 = User.objects.create_user('test', 'test@example.com', 'test')\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/auth/models.py\", line 186, in create_user\n    user.save(using=self._db)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/base.py\", line 546, in save\n    force_update=force_update, update_fields=update_fields)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/base.py\", line 650, in save_base\n    result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/manager.py\", line 215, in _insert\n    return insert_query(self.model, objs, fields, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 1661, in insert_query\n    return query.get_compiler(using=using).execute_sql(return_id)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 935, in execute_sql\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_groups_for_user (django.contrib.auth.tests.handlers.ModWsgiHandlerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 268, in __call__\n    self._post_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 533, in _post_teardown\n    self._fixture_teardown()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 553, in _fixture_teardown\n    skip_validation=True, reset_sequences=False)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/__init__.py\", line 161, in call_command\n    return klass.execute(*args, **defaults)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 255, in execute\n    output = self.handle(*args, **options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/base.py\", line 385, in handle\n    return self.handle_noargs(**options)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/commands/flush.py\", line 46, in handle_noargs\n    sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)\n  File \"/usr/local/lib/python2.7/dist-packages/django/core/management/sql.py\", line 113, in sql_flush\n    tables = connection.introspection.django_table_names(only_existing=True)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 995, in django_table_names\n    existing_tables = self.table_names()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 965, in table_names\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_perms (django.contrib.auth.tests.auth_backends.ModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_all_superuser_permissions (django.contrib.auth.tests.auth_backends.ModelBackendTest)\nA superuser has all permissions. Refs #14795\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_no_object_perm (django.contrib.auth.tests.auth_backends.ModelBackendTest)\nRegressiontest for #12462\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.ModelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_group_natural_key (django.contrib.auth.tests.models.NaturalKeysTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_natural_key (django.contrib.auth.tests.models.NaturalKeysTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_raises_exception (django.contrib.auth.tests.auth_backends.NoBackendsTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_field_order (django.contrib.auth.tests.forms.PasswordChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_incorrect_password (django.contrib.auth.tests.forms.PasswordChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_verification (django.contrib.auth.tests.forms.PasswordChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_success (django.contrib.auth.tests.forms.PasswordChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_5605 (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cleaned_data (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_email_subject (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_inactive_user (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_email (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_nonexistant_email (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_unusable_password (django.contrib.auth.tests.forms.PasswordResetFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_admin_reset (django.contrib.auth.tests.views.PasswordResetTest)\nIf the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override.\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_complete (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_different_passwords (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_invalid (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_invalid_post (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_invalid_user (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_overflow_user (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_confirm_valid (django.contrib.auth.tests.views.PasswordResetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_email_found (django.contrib.auth.tests.views.PasswordResetTest)\nEmail is sent if a valid email address is provided for password reset\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_email_found_custom_from (django.contrib.auth.tests.views.PasswordResetTest)\nEmail is sent if a valid email address is provided for password reset when a custom from_email is provided.\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_email_not_found (django.contrib.auth.tests.views.PasswordResetTest)\nError is raised if the provided email address isn't currently registered\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_poisoned_http_host (django.contrib.auth.tests.views.PasswordResetTest)\nPoisoned HTTP_HOST headers can't be used for reset emails\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_poisoned_http_host_admin_site (django.contrib.auth.tests.views.PasswordResetTest)\nPoisoned HTTP_HOST headers can't be used for reset emails on admin views\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_permlookupdict_in (django.contrib.auth.tests.context_processors.PermWrapperTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_permwrapper_in (django.contrib.auth.tests.context_processors.PermWrapperTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_duplicated_permissions (django.contrib.auth.tests.management.PermissionDuplicationTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_site_profile_not_available (django.contrib.auth.tests.models.ProfileTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_19349_render_with_none_value (django.contrib.auth.tests.forms.ReadOnlyPasswordHashWidgetTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_header_disappears (django.contrib.auth.tests.remote_user.RemoteUserCustomTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_known_user (django.contrib.auth.tests.remote_user.RemoteUserCustomTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_last_login (django.contrib.auth.tests.remote_user.RemoteUserCustomTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_remote_user (django.contrib.auth.tests.remote_user.RemoteUserCustomTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_unknown_user (django.contrib.auth.tests.remote_user.RemoteUserCustomTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_header_disappears (django.contrib.auth.tests.remote_user.RemoteUserNoCreateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_known_user (django.contrib.auth.tests.remote_user.RemoteUserNoCreateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_last_login (django.contrib.auth.tests.remote_user.RemoteUserNoCreateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_remote_user (django.contrib.auth.tests.remote_user.RemoteUserNoCreateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_unknown_user (django.contrib.auth.tests.remote_user.RemoteUserNoCreateTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_header_disappears (django.contrib.auth.tests.remote_user.RemoteUserTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_known_user (django.contrib.auth.tests.remote_user.RemoteUserTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_last_login (django.contrib.auth.tests.remote_user.RemoteUserTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_remote_user (django.contrib.auth.tests.remote_user.RemoteUserTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_unknown_user (django.contrib.auth.tests.remote_user.RemoteUserTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_all_permissions (django.contrib.auth.tests.auth_backends.RowlevelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_group_permissions (django.contrib.auth.tests.auth_backends.RowlevelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_perm (django.contrib.auth.tests.auth_backends.RowlevelBackendTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_verification (django.contrib.auth.tests.forms.SetPasswordFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_success (django.contrib.auth.tests.forms.SetPasswordFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_login (django.contrib.auth.tests.signals.SignalTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout (django.contrib.auth.tests.signals.SignalTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_logout_anonymous (django.contrib.auth.tests.signals.SignalTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update_last_login (django.contrib.auth.tests.signals.SignalTestCase)\nEnsure that only `last_login` is updated in `update_last_login`\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_10265 (django.contrib.auth.tests.tokens.TokenGeneratorTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_date_length (django.contrib.auth.tests.tokens.TokenGeneratorTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_make_token (django.contrib.auth.tests.tokens.TokenGeneratorTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_timeout (django.contrib.auth.tests.tokens.TokenGeneratorTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_14242 (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_17944_empty_password (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_17944_unknown_password_algorithm (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_17944_unmanageable_password (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_19133 (django.contrib.auth.tests.forms.UserChangeFormTest)\nThe change form does not return the password value\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_bug_19349_bound_password_field (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_unsuable_password (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_username_validity (django.contrib.auth.tests.forms.UserChangeFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_both_passwords (django.contrib.auth.tests.forms.UserCreationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_data (django.contrib.auth.tests.forms.UserCreationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_password_verification (django.contrib.auth.tests.forms.UserCreationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_success (django.contrib.auth.tests.forms.UserCreationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_user_already_exists (django.contrib.auth.tests.forms.UserCreationFormTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_create_user (django.contrib.auth.tests.models.UserManagerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_create_user_email_domain_normalize (django.contrib.auth.tests.models.UserManagerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_create_user_email_domain_normalize_rfc3696 (django.contrib.auth.tests.models.UserManagerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_create_user_email_domain_normalize_with_whitespace (django.contrib.auth.tests.models.UserManagerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_empty_username (django.contrib.auth.tests.models.UserManagerTestCase)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_for_concrete_model (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_for_concrete_models (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_for_models_empty_cache (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_for_models_full_cache (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_for_models_partial_cache (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_lookup_cache (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_missing_model (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_shortcut_view (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_shortcut_view_with_broken_get_absolute_url (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_shortcut_view_without_get_absolute_url (django.contrib.contenttypes.tests.ContentTypesTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_expiry (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clear (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_datetime (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_reset (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_seconds (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_timedelta (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cycle (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_decode (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_expiry (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_delete (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_exists_searches_cache_first (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_expire_at_browser_close (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_key (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_key (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iteritems (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iterkeys (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_itervalues (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_load_overlong_key (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_new_session (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop_default (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_key_is_read_only (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_setdefault (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_store (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_values (django.contrib.sessions.tests.CacheDBSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_expiry (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clear (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_datetime (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_reset (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_seconds (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_timedelta (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cycle (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_decode (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_expiry (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_delete (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_exists_searches_cache_first (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_expire_at_browser_close (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_key (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_key (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iteritems (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iterkeys (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_itervalues (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_load_overlong_key (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_new_session (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop_default (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_key_is_read_only (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_setdefault (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_store (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_values (django.contrib.sessions.tests.CacheDBSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_expiry (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clear (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_datetime (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_reset (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_seconds (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_timedelta (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cycle (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_decode (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_expiry (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_delete (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_expire_at_browser_close (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_key (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_key (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iteritems (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iterkeys (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_itervalues (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_new_session (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop_default (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_key_is_read_only (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_setdefault (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_store (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_values (django.contrib.sessions.tests.CookieSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_expiry (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clear (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clearsessions_command (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_datetime (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_reset (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_seconds (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_timedelta (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cycle (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_decode (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_expiry (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_delete (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_expire_at_browser_close (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_key (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_key (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iteritems (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iterkeys (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_itervalues (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_new_session (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop_default (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_get_decoded (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_key_is_read_only (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_sessionmanager_save (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_setdefault (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_store (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_values (django.contrib.sessions.tests.DatabaseSessionTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_actual_expiry (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clear (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_clearsessions_command (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_datetime (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_reset (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_seconds (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_expiry_timedelta (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_cycle (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_decode (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_expiry (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_delete (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_expire_at_browser_close (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_has_key (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_invalid_key (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iteritems (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_iterkeys (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_itervalues (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_new_session (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_pop_default (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_get_decoded (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_key_is_read_only (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_sessionmanager_save (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_setdefault (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_store (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_update (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_values (django.contrib.sessions.tests.DatabaseSessionWithTimeZoneTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_httponly_session_cookie (django.contrib.sessions.tests.SessionMiddlewareTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 220, in inner\n    return test_func(*args, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/tests.py\", line 504, in test_httponly_session_cookie\n    response = middleware.process_response(request, response)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/middleware.py\", line 38, in process_response\n    request.session.save()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 50, in save\n    session_key=self._get_or_create_session_key(),\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 148, in _get_or_create_session_key\n    self._session_key = self._get_new_session_key()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 142, in _get_new_session_key\n    if not self.exists(session_key):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 26, in exists\n    return Session.objects.filter(session_key=session_key).exists()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 596, in exists\n    return self.query.has_results(using=self.db)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/query.py\", line 442, in has_results\n    return bool(compiler.execute_sql(SINGLE))\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 830, in execute_sql\n    sql, params = self.as_sql()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 74, in as_sql\n    out_cols = self.get_columns(with_col_aliases)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 174, in get_columns\n    result = ['(%s) AS %s' % (col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)]\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_httponly_session_cookie (django.contrib.sessions.tests.SessionMiddlewareTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 220, in inner\n    return test_func(*args, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/tests.py\", line 521, in test_no_httponly_session_cookie\n    response = middleware.process_response(request, response)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/middleware.py\", line 38, in process_response\n    request.session.save()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 50, in save\n    session_key=self._get_or_create_session_key(),\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 148, in _get_or_create_session_key\n    self._session_key = self._get_new_session_key()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 142, in _get_new_session_key\n    if not self.exists(session_key):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 26, in exists\n    return Session.objects.filter(session_key=session_key).exists()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 596, in exists\n    return self.query.has_results(using=self.db)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/query.py\", line 442, in has_results\n    return bool(compiler.execute_sql(SINGLE))\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 830, in execute_sql\n    sql, params = self.as_sql()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 74, in as_sql\n    out_cols = self.get_columns(with_col_aliases)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 174, in get_columns\n    result = ['(%s) AS %s' % (col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)]\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_secure_session_cookie (django.contrib.sessions.tests.SessionMiddlewareTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 220, in inner\n    return test_func(*args, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/tests.py\", line 489, in test_secure_session_cookie\n    response = middleware.process_response(request, response)\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/middleware.py\", line 38, in process_response\n    request.session.save()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 50, in save\n    session_key=self._get_or_create_session_key(),\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 148, in _get_or_create_session_key\n    self._session_key = self._get_new_session_key()\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/base.py\", line 142, in _get_new_session_key\n    if not self.exists(session_key):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 26, in exists\n    return Session.objects.filter(session_key=session_key).exists()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 596, in exists\n    return self.query.has_results(using=self.db)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/query.py\", line 442, in has_results\n    return bool(compiler.execute_sql(SINGLE))\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 830, in execute_sql\n    sql, params = self.as_sql()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 74, in as_sql\n    out_cols = self.get_columns(with_col_aliases)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 174, in get_columns\n    result = ['(%s) AS %s' % (col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)]\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_save_on_500 (django.contrib.sessions.tests.SessionMiddlewareTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/tests.py\", line 541, in test_session_save_on_500\n    self.assertNotIn('hello', request.session.load())\n  File \"/usr/local/lib/python2.7/dist-packages/django/contrib/sessions/backends/db.py\", line 18, in load\n    expire_date__gt=timezone.now()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/manager.py\", line 143, in get\n    return self.get_query_set().get(*args, **kwargs)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 382, in get\n    num = len(clone)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 90, in __len__\n    self._result_cache = list(self.iterator())\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/query.py\", line 301, in iterator\n    for row in compiler.results_iter():\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 775, in results_iter\n    for rows in self.execute_sql(MULTI):\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 830, in execute_sql\n    sql, params = self.as_sql()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 74, in as_sql\n    out_cols = self.get_columns(with_col_aliases)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 212, in get_columns\n    col_aliases)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 299, in get_default_columns\n    r = '%s.%s' % (qn(alias), qn2(field.column))\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/models/sql/compiler.py\", line 52, in quote_name_unless_alias\n    r = self.connection.ops.quote_name(name)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_current_site (django.contrib.sites.tests.SitesFrameworkTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_save_another (django.contrib.sites.tests.SitesFrameworkTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_site_cache (django.contrib.sites.tests.SitesFrameworkTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_site_manager (django.contrib.sites.tests.SitesFrameworkTests)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_lazy_translation (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_update (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_tags (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_level (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_domain (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add_read_update (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read_add_update (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_full_request_response_cycle (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_bad_cookie (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_high_level (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_json_encoder_decoder (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_low_level (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_max_cookie_length (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled_fail_silently (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_multiple_posts (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_update (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_safedata (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_settings_level (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_tags (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_with_template_response (django.contrib.messages.tests.cookie.CookieTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/utils.py\", line 208, in _pre_setup\n    original_pre_setup(innerself)\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_lazy_translation (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_update (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_tags (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_level (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add_read_update (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read_add_update (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_flush_used_backends (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_full_request_response_cycle (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_empty (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_fallback (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get_fallback_only (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_high_level (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_low_level (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled_fail_silently (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_multiple_posts (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_fallback (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_update (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_fallback (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_session_fallback_only (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_settings_level (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_tags (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_with_template_response (django.contrib.messages.tests.fallback.FallbackTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_lazy_translation (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_add_update (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_custom_tags (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_default_level (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_add_read_update (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_existing_read_add_update (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_full_request_response_cycle (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_get (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_high_level (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_low_level (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_middleware_disabled_fail_silently (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_multiple_posts (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_no_update (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_safedata (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_settings_level (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_tags (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n======================================================================\nERROR: test_with_template_response (django.contrib.messages.tests.session.SessionTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 259, in __call__\n    self._pre_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 479, in _pre_setup\n    self._fixture_setup()\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 829, in _fixture_setup\n    if not connections_support_transactions():\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in connections_support_transactions\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/test/testcases.py\", line 816, in <genexpr>\n    for conn in connections.all())\n  File \"/usr/local/lib/python2.7/dist-packages/django/utils/functional.py\", line 43, in __get__\n    res = instance.__dict__[self.func.__name__] = self.func(instance)\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n    self.connection.enter_transaction_management()\n  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/dummy/base.py\", line 15, in complain\n    raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\nImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.\n\n----------------------------------------------------------------------\nRan 85 tests in 0.785s\n\nFAILED (errors=404, skipped=1)\nAttributeError: _original_allowed_hosts\n"
  },
  {
    "path": "tests/book_parser.py",
    "content": "#!/usr/bin/env python3\nimport re\n\nCOMMIT_REF_FINDER = r\"ch\\d\\dl\\d\\d\\d-?\\d?\"\n\n\nclass CodeListing:\n    COMMIT_REF_FINDER = r\"^(.+) \\((\" + COMMIT_REF_FINDER + r\")\\)$\"\n\n    def __init__(self, filename, contents):\n        self.is_server_listing = False\n        if re.match(CodeListing.COMMIT_REF_FINDER, filename):\n            self.filename = re.match(CodeListing.COMMIT_REF_FINDER, filename).group(1)\n            self.commit_ref = re.match(CodeListing.COMMIT_REF_FINDER, filename).group(2)\n        elif filename.startswith(\"server: \"):\n            self.filename = filename.replace(\"server: \", \"\")\n            self.commit_ref = None\n            self.is_server_listing = True\n        else:\n            self.filename = filename\n            self.commit_ref = None\n        self.contents = contents\n        self.was_written = False\n        self.skip = False\n        self.currentcontents = False\n        self.against_server = False\n        self.pause_first = False\n\n    def is_diff(self):\n        lines = self.contents.split(\"\\n\")\n        if any(l.count(\"@@\") > 1 for l in lines):\n            return True\n        if len([l for l in lines if l.startswith(\"+\") or l.startswith(\"-\")]) > 2:\n            return True\n\n    @property\n    def type(self):\n        if self.is_server_listing:\n            return \"server code listing\"\n        elif self.currentcontents:\n            return \"code listing currentcontents\"\n        elif self.commit_ref:\n            return \"code listing with git ref\"\n        elif self.is_diff():\n            return \"diff\"\n        else:\n            return \"code listing\"\n\n    def __repr__(self):\n        return \"<CodeListing %s: %s...>\" % (self.filename, self.contents.split(\"\\n\")[0])\n\n\nclass Command(str):\n    def __init__(self, a_string):\n        self.was_run = False\n        self.skip = False\n        self.ignore_errors = False\n        self.server_command = False\n        self.against_server = False\n        self.pause_first = False\n        self.dofirst = None\n        str.__init__(a_string)\n\n    @property\n    def type(self):\n        if self.server_command:\n            return \"server command\"\n        for git_cmd in (\"git diff\", \"git status\", \"git commit\"):\n            if git_cmd in self:\n                return git_cmd\n        if self.startswith(\"python\") and \"test\" in self:\n            return \"test\"\n        if self == \"python manage.py behave\":\n            return \"bdd test\"\n        if self == \"python manage.py migrate\":\n            return \"interactive manage.py\"\n        if self == \"python manage.py makemigrations\":\n            return \"interactive manage.py\"\n        if self == \"python manage.py collectstatic\":\n            return \"interactive manage.py\"\n        if \"docker run\" in self and \"-it\" in self:\n            return \"docker run tty\"\n        if \"docker exec\" in self and \"container-id-or-name\" in self:\n            return \"docker exec\"\n        if \"ottg.co.uk\" in self:\n            return \"against staging\"\n        return \"other command\"\n\n    def __repr__(self):\n        return f\"<Command {str.__repr__(self)}>\"\n\n\nclass Output(str):\n    def __init__(self, a_string):\n        self.was_checked = False\n        self.skip = False\n        self.dofirst = None\n        self.jasmine_output = False\n        self.against_server = False\n        self.pause_first = False\n        str.__init__(a_string)\n\n    @property\n    def type(self) -> str:\n        if self.jasmine_output:\n            return \"jasmine output\"\n        if \"├\" in self:\n            return \"tree\"\n        else:\n            return \"output\"\n\n\ndef fix_newlines(text):\n    if text is None:\n        return \"\"\n    return text.replace(\"\\r\\n\", \"\\n\").replace(\"\\\\\\n\", \"\").strip(\"\\n\")\n\n\ndef parse_output(listing):\n    text = fix_newlines(listing.text_content().strip())\n\n    commands = listing.cssselect(\"pre strong\")\n    if not commands:\n        return [Output(text)]\n\n    outputs = []\n    output_before = fix_newlines(listing.text.strip()) if listing.text else \"\"\n\n    for command in commands:\n        if \"$\" in output_before and \"\\n\" in output_before:\n            last_cr = output_before.rfind(\"\\n\")\n            previous_lines = output_before[:last_cr]\n            if previous_lines:\n                outputs.append(Output(previous_lines))\n        elif output_before and \"$\" not in output_before:\n            outputs.append(Output(output_before))\n\n        command_text = fix_newlines(command.text)\n        if output_before.strip().startswith(\"(virtualenv)\"):\n            command_text = \"source ./.venv/bin/activate && \" + command_text\n        outputs.append(Command(command_text))\n\n        output_before = fix_newlines(command.tail)\n\n    if output_before:\n        outputs.append(Output(output_before))\n\n    return outputs\n\n\ndef _strip_callouts(content):\n    callout_at_end = r\"\\s+\\(\\d+\\)$\"\n    counts = 0\n    while re.search(callout_at_end, content, re.MULTILINE):\n        content = re.sub(callout_at_end, \"\", content, flags=re.MULTILINE)\n        counts += 1\n    return content\n\n\ndef parse_listing(listing):  # noqa: PLR0912\n    classes = listing.get(\"class\").split()\n    skip = \"skipme\" in classes\n    dofirst_classes = [c for c in classes if c.startswith(\"dofirst\")]\n    if dofirst_classes:\n        dofirst = re.findall(COMMIT_REF_FINDER, dofirst_classes[0])[0]\n    else:\n        dofirst = None\n\n    if \"sourcecode\" in classes:\n        try:\n            filename = listing.cssselect(\".title\")[0].text_content().strip()\n        except IndexError:\n            raise Exception(\n                f\"could not find title for listing {listing.text_content()}\"\n            )\n        contents = (\n            listing.cssselect(\".content\")[0]\n            .text_content()\n            .replace(\"\\r\\n\", \"\\n\")\n            .strip(\"\\n\")\n        )\n        contents = _strip_callouts(contents)\n        listing = CodeListing(filename, contents)\n        listing.skip = skip\n        listing.dofirst = dofirst\n        if \"currentcontents\" in classes:\n            listing.currentcontents = True\n        return [listing]\n\n    elif \"jasmine-output\" in classes:\n        contents = (\n            listing.cssselect(\".content\")[0]\n            .text_content()\n            .replace(\"\\r\\n\", \"\\n\")\n            .strip(\"\\n\")\n        )\n        output = Output(contents)\n        output.jasmine_output = True\n        output.skip = skip\n        output.dofirst = dofirst\n        return [output]\n\n    if \"server-commands\" in classes:\n        listing = listing.cssselect(\"div.content\")[0]\n\n    outputs = parse_output(listing)\n    if skip:\n        for listing in outputs:\n            listing.skip = True\n    if dofirst:\n        outputs[0].dofirst = dofirst\n    if \"ignore-errors\" in classes:\n        for listing in outputs:\n            if isinstance(listing, Command):\n                listing.ignore_errors = True\n    if \"server-commands\" in classes:\n        for listing in outputs:\n            if isinstance(listing, Command):\n                listing.server_command = True\n    if \"against-server\" in classes:\n        for listing in outputs:\n            listing.against_server = True\n\n    if \"pause-first\" in classes:\n        for listing in outputs:\n            listing.pause_first = True\n\n    return outputs\n\n\ndef get_commands(node):\n    return [\n        el.text_content().replace(\"\\\\\\n\", \"\")\n        for el in node.cssselect(\"pre code strong\")\n    ]\n"
  },
  {
    "path": "tests/book_tester.py",
    "content": "import os\nimport re\nimport subprocess\nimport sys\nimport tempfile\nimport time\nimport unittest\nfrom pathlib import Path\nfrom textwrap import wrap\n\nfrom book_parser import (\n    CodeListing,\n    Command,\n    Output,\n    parse_listing,\n)\nfrom lxml import html\nfrom sourcetree import Commit, SourceTree\nfrom update_source_repo import update_sources_for_chapter\nfrom write_to_file import write_to_file\n\nJASMINE_RUNNER = Path(__file__).parent / \"run-js-spec.py\"\n# DO_SERVER_COMMANDS = True\n# if os.environ.get(\"CI\") or os.environ.get(\"NO_SERVER_COMMANDS\"):\nDO_SERVER_COMMANDS = False\n\n\ndef contains(inseq, subseq):\n    return any(\n        inseq[pos : pos + len(subseq)] == subseq\n        for pos in range(0, len(inseq) - len(subseq) + 1)\n    )\n\n\ndef wrap_long_lines(text):\n    paragraphs = text.split(\"\\n\")\n    return \"\\n\".join(\n        \"\\n\".join(wrap(p, 79, break_long_words=True, break_on_hyphens=False))\n        for p in paragraphs\n    )\n\n\ndef split_blocks(text):\n    return [\n        block.strip()\n        for block in re.split(r\"\\n\\n+|^.*\\[\\.\\.\\..*$\", text, flags=re.MULTILINE)\n    ]\n\n\ndef fix_test_dashes(output):\n    return output.replace(\" \" + \"-\" * 69, \"-\" * 70)\n\n\ndef strip_mock_ids(output):\n    strip_mocks_with_names = re.sub(\n        r\"Mock name='(.+)' id='(\\d+)'>\",\n        r\"Mock name='\\1' id='XX'>\",\n        output,\n    )\n    strip_all_mocks = re.sub(\n        r\"Mock id='(\\d+)'>\",\n        r\"Mock id='XX'>\",\n        strip_mocks_with_names,\n    )\n    return strip_all_mocks\n\n\ndef strip_object_ids(output):\n    return re.sub(\"0x([0-9a-f]+)>\", \"0x123abc22>\", output)\n\n\ndef strip_migration_timestamps(output):\n    return re.sub(r\"00(\\d\\d)_auto_20\\d{6}_\\d{4}\", r\"00\\1_auto_20XXXXXX_XXXX\", output)\n\n\ndef strip_localhost_port(output):\n    lh_fixed = re.sub(r\"localhost:\\d\\d\\d\\d\\d?\", r\"localhost:XXXX\", output)\n    ipaddr_fixed = re.sub(r\"127.0.0.1:\\d\\d\\d\\d\\d?\", r\"127.0.0.1:XXXX\", lh_fixed)\n    return ipaddr_fixed\n\n\ndef strip_selenium_trace_ids(output):\n    fixing = re.sub(\n        r\"\\d{13}(\\s+)geckodriver\",\n        r\"1234567890111\\1geckodriver\",\n        output,\n    )\n    fixing = re.sub(\n        r\"\\d{13}(\\s+)webdriver\",\n        r\"1234567890112\\1webdriver\",\n        fixing,\n    )\n    fixing = re.sub(\n        r\"\\d{13}(\\s+)mozrunner\",\n        r\"1234567890113\\1mozrunner\",\n        fixing,\n    )\n    fixing = re.sub(\n        r\"\\d{13}(\\s+)Marionette\",\n        r\"1234567890114\\1Marionette\",\n        fixing,\n    )\n    return fixing\n\n\ndef fix_firefox_esr_version(output):\n    return re.sub(r\"(\\d\\d\\d\\.\\d+\\.?\\d*)esr\", r\"128.10.1esr\", output)\n\n\ndef strip_session_ids(output):\n    return re.sub(r\"^[a-z0-9]{32}$\", r\"xxx_session_id_xxx\", output)\n\n\ndef standardise_assertionerror_none(output):\n    return output.replace(\"AssertionError: None\", \"AssertionError\")\n\n\ndef standardise_git_init_msg(output):\n    return output.replace(\n        \"Initialized empty Git repository\", \"Initialised empty Git repository\"\n    )\n\n\ndef strip_git_hashes(output):\n    fixed_indexes = re.sub(\n        r\"index .......\\.\\........ 100644\",\n        r\"index XXXXXXX\\.\\.XXXXXXX 100644\",\n        output,\n    )\n    fixed_diff_commits = re.sub(\n        r\"^[a-f0-9]{7} \",\n        r\"XXXXXXX \",\n        fixed_indexes,\n        flags=re.MULTILINE,\n    )\n    fixed_git_log_commits = re.sub(\n        r\"\\* [a-f0-9]{7} \",\n        r\"* abc123d \",\n        fixed_diff_commits,\n        flags=re.MULTILINE,\n    )\n    return fixed_git_log_commits\n\n\ndef strip_callouts(output):\n    minus_old_callouts = re.sub(\n        r\"^(.+)  <\\d+>$\",\n        r\"\\1\",\n        output,\n        flags=re.MULTILINE,\n    )\n    minus_new_callouts = re.sub(\n        r\"^(.+)  \\(\\d+\\)$\",\n        r\"\\1\",\n        minus_old_callouts,\n        flags=re.MULTILINE,\n    )\n    return minus_new_callouts\n\n\ndef standardise_library_paths(output):\n    return re.sub(\n        r'(File \").+packages/',\n        r\"\\1.../\",\n        output,\n        flags=re.MULTILINE,\n    )\n\n\ndef standardise_geckodriver_tracebacks(output):\n    return re.sub(\n        r\"@chrome://remote/(.+):(\\d+:\\d+)$\",\n        r\"@chrome://\\1:XXX:XXX\",\n        output,\n        flags=re.MULTILINE,\n    )\n\n\ndef strip_test_speed(output):\n    return re.sub(\n        r\"Ran (\\d+) tests? in \\d+\\.\\d\\d\\ds\",\n        r\"Ran \\1 tests in X.Xs\",\n        output,\n    )\n\n\ndef strip_js_test_speed(output):\n    return re.sub(\n        r\"Took \\d+ms to run (\\d+) tests. (\\d+) passed, (\\d+) failed.\",\n        r\"Took XXms to run \\1 tests. \\2 passed, \\3 failed.\",\n        output,\n    )\n\n\ndef strip_bdd_test_speed(output):\n    return re.sub(\n        r\"features/steps/(\\w+).py:(\\d+) \\d+.\\d\\d\\ds\",\n        r\"features/steps/\\1.py:\\2 XX.XXXs\",\n        output,\n    )\n\n\ndef strip_screenshot_timestamps(output):\n    fixed = re.sub(\n        r\"-(20\\d\\d-\\d\\d-\\d\\dT\\d\\d\\.\\d\\d\\.\\d?\\d?)\",\n        r\"-20XX-XX-XXTXX.XX\",\n        output,\n    )\n    # this last is very specific to one listing in 19...\n    fixed = re.sub(r\"^\\d\\d\\.html$\", \"XX.html\", fixed, flags=re.MULTILINE)\n    return fixed\n\n\ndef strip_docker_image_ids_and_creation_times(output):\n    fixed = re.sub(\n        r\"superlists\\s+latest\\s+\\w+\\s+\\d+ \\w+ ago\\s+164MB\",\n        r\"superlists   latest   someidorother   X time ago   164MB\",\n        output,\n    )\n    return fixed\n\n\ndef fix_curl_stuff(output):\n    fixed = re.sub(\n        r\"User-Agent: curl/\\d\\.\\d+\\.\\d*\",\n        r\"User-Agent: curl/8.6.0\",\n        output,\n    )\n    fixed = re.sub(\n        r\"Trying \\[::1\\]:(\\d\\d\\d\\d)...\",\n        r\"Trying ::1:\\1...\",\n        fixed,\n    )\n    fixed = re.sub(\n        r\"Closing connection 0\",\n        r\"Closing connection\",\n        fixed,\n    )\n    fixed = re.sub(\n        r\"Connected to localhost \\(127.0.0.1\\) port (\\d\\d\\d\\d) \\(#0\\)\",\n        r\"Connected to localhost (127.0.0.1) port \\1\",\n        fixed,\n    )\n    return fixed\n\n\ndef fix_curl_linebreak_after_download(output):\n    return re.sub(\n        r\"\\*(\\s+)Trying\",\n        \"\\n*\\\\1Trying\",\n        output,\n    )\n\n\nSQLITE_MESSAGES = {\n    \"django.db.utils.IntegrityError: lists_item.list_id may not be NULL\": \"django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id\",\n    \"django.db.utils.IntegrityError: columns list_id, text are not unique\": \"django.db.utils.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\\nlists_item.text\",\n    \"sqlite3.IntegrityError: columns list_id, text are not unique\": \"sqlite3.IntegrityError: UNIQUE constraint failed: lists_item.list_id,\\nlists_item.text\",\n}\n\n\ndef fix_sqlite_messages(actual_text):\n    fixed_text = actual_text\n    for old_version, new_version in SQLITE_MESSAGES.items():\n        fixed_text = fixed_text.replace(old_version, new_version)\n    return fixed_text\n\n\ndef standardize_layout_test_pixelsize(actual_text):\n    return re.sub(\n        r\"10\\d.\\d+ != 512 within 10 delta \\(40\\d.\\d+\",\n        r\"102.5 != 512 within 10 delta (409.5\",\n        actual_text,\n    )\n\n\ndef fix_creating_database_line(actual_text):\n    creating_db = \"Creating test database for alias 'default'...\"\n    actual_lines = actual_text.split(\"\\n\")\n    if creating_db in actual_lines:\n        actual_lines.remove(creating_db)\n        actual_lines.insert(0, creating_db)\n        actual_text = \"\\n\".join(actual_lines)\n    return actual_text\n\n\ndef fix_interactive_managepy_stuff(actual_text):\n    return actual_text.replace(\n        \"Select an option: \",\n        \"Select an option:\\n\",\n    ).replace(\n        \">>> \",\n        \">>>\\n\",\n    )\n\n\nclass ChapterTest(unittest.TestCase):\n    chapter_name = \"override me\"\n    maxDiff = None\n\n    def setUp(self):\n        self.sourcetree = SourceTree()\n        self.tempdir = self.sourcetree.tempdir\n        self.processes = []\n        self.pos = 0\n        self.dev_server_running = False\n        self.current_server_cd = None\n        self.current_server_exports = {}\n\n    def tearDown(self):\n        print(f\"finished running test in {self.sourcetree.tempdir}\")\n        print(\"writing tmpdir out to\", f\".tmpdir.test_{self.chapter_name}\")\n        with open(f\".tmpdir.test_{self.chapter_name}\", \"w\") as f:\n            f.write(str(self.sourcetree.tempdir))\n        self.sourcetree.cleanup()\n\n    def parse_listings(self):\n        base_dir = os.path.split(os.path.abspath(os.path.dirname(__file__)))[0]\n        filename = self.chapter_name + \".html\"\n        with open(os.path.join(base_dir, filename), encoding=\"utf-8\") as f:\n            raw_html = f.read()\n        parsed_html = html.fromstring(raw_html)\n        all_nodes = parsed_html.cssselect(\n            \".exampleblock.sourcecode, div:not(.sourcecode) div.listingblock\"\n        )\n        listing_nodes = []\n        for ix, node in enumerate(all_nodes):\n            prev = all_nodes[ix - 1]\n            if node not in list(prev.iterdescendants()):\n                listing_nodes.append(node)\n\n        self.listings = [p for n in listing_nodes for p in parse_listing(n)]\n\n    def check_final_diff(self, ignore=None, diff=None):\n        if diff is None:\n            diff = self.run_command(Command(f\"git diff -w repo/{self.chapter_name}\"))\n        print(\"checking final diff\", diff)\n        self.assertNotIn(\"fatal:\", diff)\n        start_marker = \"diff --git a/\\n\"\n        commit = Commit.from_diff(start_marker + diff)\n\n        if ignore is None:\n            if commit.lines_to_add:\n                self.fail(f\"Found lines to add in diff:\\n{commit.lines_to_add}\")\n            if commit.lines_to_remove:\n                self.fail(f\"Found lines to remove in diff:\\n{commit.lines_to_remove}\")\n            return\n\n        if \"moves\" in ignore:\n            ignore.remove(\"moves\")\n            difference_lines = commit.deleted_lines + commit.new_lines\n        else:\n            difference_lines = commit.lines_to_add + commit.lines_to_remove\n\n        for line in difference_lines:\n            if any(ignorable in line for ignorable in ignore):\n                continue\n            self.fail(f\"Found divergent line in diff:\\n{line}\")\n\n    def start_with_checkout(self):\n        update_sources_for_chapter(self.chapter_name, self.previous_chapter)\n        self.sourcetree.start_with_checkout(self.chapter_name, self.previous_chapter)\n\n    def write_to_file(self, codelisting):\n        self.assertEqual(\n            type(codelisting),\n            CodeListing,\n            \"passed a non-Codelisting to write_to_file:\\n%s\" % (codelisting,),\n        )\n        print(\"writing to file\", codelisting.filename)\n        write_to_file(codelisting, self.tempdir)\n\n    def apply_patch(self, codelisting):\n        tf = tempfile.NamedTemporaryFile(delete=False)\n        tf.write(codelisting.contents.encode(\"utf8\"))\n        tf.write(b\"\\n\")\n        tf.close()\n        print(\"patch:\\n\", codelisting.contents)\n        patch_output = self.run_command(\n            Command(\n                \"patch --fuzz=3 --no-backup-if-mismatch %s %s\"\n                % (codelisting.filename, tf.name)\n            )\n        )\n        print(patch_output)\n        self.assertNotIn(\"malformed\", patch_output)\n        self.assertNotIn(\"failed\", patch_output.lower())\n        codelisting.was_checked = True\n        with open(os.path.join(self.tempdir, codelisting.filename)) as f:\n            print(f.read())\n        os.remove(tf.name)\n        self.pos += 1\n        codelisting.was_written = True\n\n    def run_command(self, command, cwd=None, user_input=None, ignore_errors=False):\n        assert isinstance(command, Command), (\n            f\"passed a non-Command to run-command:\\n{command}\"\n        )\n        if command == \"git push\":\n            command.was_run = True\n            return\n\n        if command.startswith(\"curl\"):\n            cmd_to_run = command.replace(\"curl\", \"curl --silent --show-error\")\n        if command.startswith(\"grep\") and sys.platform == \"darwin\":\n            cmd_to_run = command.replace(\"grep\", \"ggrep\")\n        else:\n            cmd_to_run = str(command)\n\n        print(f\"running command {cmd_to_run} with {ignore_errors=}\")\n        output = self.sourcetree.run_command(\n            cmd_to_run, cwd=cwd, user_input=user_input, ignore_errors=ignore_errors\n        )\n        command.was_run = True\n        return output\n\n    def prep_virtualenv(self):\n        virtualenv_path = self.tempdir / \".venv\"\n        if not virtualenv_path.exists():\n            print(\"preparing virtualenv\")\n            self.sourcetree.run_command(\"uv venv -p 3.14 .venv\")\n        os.environ[\"VIRTUAL_ENV\"] = str(virtualenv_path)\n        os.environ[\"PATH\"] = \":\".join(\n            [f\"{virtualenv_path}/bin\"] + os.environ[\"PATH\"].split(\":\")\n        )\n        if (self.tempdir / \"requirements.txt\").exists():\n            self.sourcetree.run_command(\"uv pip install -r requirements.txt\")\n        else:\n            self.sourcetree.run_command('uv pip install \"django<6\" selenium')\n        self.sourcetree.run_command(\"uv pip install pip\")\n\n    def prep_database(self):\n        self.sourcetree.run_command(f\"python {self._manage_py()} migrate --noinput\")\n\n    def assertLineIn(self, line, lines):\n        if \"\\t\" in line or \"\\t\" in \"\\n\".join(lines):\n            print(\"tabz\")\n        if line not in lines:\n            raise AssertionError(\n                f\"{repr(line)} not found in:\\n\" + \"\\n\".join(repr(l) for l in lines)\n            )\n\n    def assert_console_output_correct(self, actual, expected, ls=False):\n        print(\"checking expected output\\n\", expected)\n        print(\"against actual\\n\", actual)\n        self.assertEqual(\n            type(expected),\n            Output,\n            f\"passed a non-Output to run-command:\\n{expected}\",\n        )\n\n        if str(self.tempdir) in actual:\n            actual = actual.replace(str(self.tempdir), \"...goat-book\")\n            if sys.platform == \"darwin\":\n                # for some reason macos does full paths to virtualenvs\n                # when linux doesnt\n                actual = actual.replace(\"...goat-book/.venv\", \"./.venv\")\n            actual = actual.replace(\"/private\", \"\")  # macos thing\n\n        if ls:\n            actual = actual.strip()\n            self.assertCountEqual(actual.split(\"\\n\"), expected.split())\n            expected.was_checked = True\n            return\n\n        actual_fixed = standardise_library_paths(actual)\n        actual_fixed = standardise_geckodriver_tracebacks(actual_fixed)\n        actual_fixed = standardize_layout_test_pixelsize(actual_fixed)\n        actual_fixed = strip_test_speed(actual_fixed)\n        actual_fixed = strip_js_test_speed(actual_fixed)\n        actual_fixed = strip_bdd_test_speed(actual_fixed)\n        actual_fixed = strip_git_hashes(actual_fixed)\n        actual_fixed = strip_mock_ids(actual_fixed)\n        actual_fixed = strip_object_ids(actual_fixed)\n        actual_fixed = strip_migration_timestamps(actual_fixed)\n        actual_fixed = strip_session_ids(actual_fixed)\n        actual_fixed = strip_docker_image_ids_and_creation_times(actual_fixed)\n        actual_fixed = fix_curl_stuff(actual_fixed)\n        actual_fixed = fix_curl_linebreak_after_download(actual_fixed)\n        actual_fixed = strip_localhost_port(actual_fixed)\n        actual_fixed = strip_selenium_trace_ids(actual_fixed)\n        actual_fixed = fix_firefox_esr_version(actual_fixed)\n        actual_fixed = strip_screenshot_timestamps(actual_fixed)\n        actual_fixed = fix_sqlite_messages(actual_fixed)\n        actual_fixed = fix_creating_database_line(actual_fixed)\n        actual_fixed = fix_interactive_managepy_stuff(actual_fixed)\n        actual_fixed = standardise_assertionerror_none(actual_fixed)\n        actual_fixed = standardise_git_init_msg(actual_fixed)\n        actual_fixed = wrap_long_lines(actual_fixed)\n\n        expected_fixed = standardise_library_paths(expected)\n        expected_fixed = standardise_geckodriver_tracebacks(expected_fixed)\n        expected_fixed = fix_test_dashes(expected_fixed)\n        expected_fixed = strip_test_speed(expected_fixed)\n        expected_fixed = strip_js_test_speed(expected_fixed)\n        expected_fixed = strip_bdd_test_speed(expected_fixed)\n        expected_fixed = strip_git_hashes(expected_fixed)\n        expected_fixed = strip_mock_ids(expected_fixed)\n        expected_fixed = strip_docker_image_ids_and_creation_times(expected_fixed)\n        expected_fixed = fix_curl_stuff(expected_fixed)\n        expected_fixed = strip_object_ids(expected_fixed)\n        expected_fixed = strip_migration_timestamps(expected_fixed)\n        expected_fixed = strip_session_ids(expected_fixed)\n        expected_fixed = strip_localhost_port(expected_fixed)\n        expected_fixed = strip_selenium_trace_ids(expected_fixed)\n        expected_fixed = fix_firefox_esr_version(expected_fixed)\n        expected_fixed = strip_screenshot_timestamps(expected_fixed)\n        expected_fixed = strip_callouts(expected_fixed)\n        expected_fixed = standardise_assertionerror_none(expected_fixed)\n\n        actual_fixed = actual_fixed.replace(\"\\xa0\", \" \")\n        expected_fixed = expected_fixed.replace(\"\\xa0\", \" \")\n        if \"\\t\" in actual_fixed:\n            print(\"fixing tabs\")\n            actual_fixed = re.sub(r\"\\s+\", \" \", actual_fixed)\n            expected_fixed = re.sub(r\"\\s+\", \" \", expected_fixed)\n\n        actual_lines = actual_fixed.split(\"\\n\")\n        expected_lines = expected_fixed.split(\"\\n\")\n\n        for line in expected_lines:\n            if line.startswith(\"[...\"):\n                continue\n            if line.endswith(\"[...]\"):\n                line = line.rsplit(\"[...]\")[0].rstrip()\n                self.assertLineIn(line, [l[: len(line)] for l in actual_lines])\n            elif line.startswith(\" \"):\n                self.assertLineIn(line, actual_lines)\n            else:\n                self.assertLineIn(line.rstrip(), [l.strip() for l in actual_lines])\n\n        if (\n            len(expected_lines) > 4\n            and \"[...\" not in expected_fixed\n            and expected.type != \"qunit output\"\n        ):\n            self.assertMultiLineEqual(actual_fixed.strip(), expected_fixed.strip())\n\n        expected.was_checked = True\n\n    def find_with_check(self, pos, expected_content):\n        listing = self.listings[pos]\n        listing_text = lambda l: getattr(l, \"contents\", l)\n        first_match = next(\n            (\n                (ix, l)\n                for ix, l in enumerate(self.listings)\n                if expected_content in listing_text(l)\n            ),\n            None,\n        )\n        all_listings = \"\\n\".join(str(t) for t in enumerate(self.listings))\n        error = f'Could not find {expected_content} at pos {pos}: (\"{listing}\"). ' + (\n            f\"Did you mean {first_match}?\"\n            if first_match\n            else f\"Listings were:\\n{all_listings}\"\n        )\n        if hasattr(listing, \"contents\"):\n            if expected_content not in listing.contents:\n                raise Exception(error)\n        elif expected_content not in listing:\n            raise Exception(error)\n        return listing\n\n    def skip_with_check(self, pos, expected_content):\n        listing = self.find_with_check(pos, expected_content)\n        listing.skip = True\n\n    def replace_command_with_check(self, pos, old, new):\n        listing = self.listings[pos]\n        all_listings = \"\\n\".join(str(t) for t in enumerate(self.listings))\n        error = f'Could not find {old} at pos {pos}: \"{listing}\". Listings were:\\n{all_listings}'\n        if old not in listing:\n            raise Exception(error)\n        assert type(listing) == Command\n\n        new_listing = Command(listing.replace(old, new))\n        for attr, val in vars(listing).items():\n            setattr(new_listing, attr, val)\n        self.listings[pos] = new_listing\n\n    def skip_forward_if_skipto_set(self) -> None:\n        if target_listing := os.environ.get(\"SKIPTO\"):\n            self.sourcetree.run_command(\"uv pip install gunicorn whitenoise\")\n            commit_spec = self.sourcetree.get_commit_spec(target_listing)\n            while True:\n                listing = self.listings[self.pos]\n                found = False\n                self.pos += 1\n                if getattr(listing, \"commit_ref\", None) == target_listing:\n                    found = True\n                    print(\"Skipping to pos\", self.pos)\n                    self.sourcetree.run_command(f\"git checkout {commit_spec}\")\n                    break\n            if not found:\n                raise Exception(f\"Could not find {target_listing}\")\n\n    def _run_tree(self, target=\"\", no_report=False):\n        return self.sourcetree.run_command(\n            f\"tree -v -I __pycache__ {'--noreport' if no_report else ''} {target}\"\n        )\n\n    def assert_directory_tree_correct(self, expected_tree):\n        actual_tree = self._run_tree(no_report=True)\n        self.assert_console_output_correct(actual_tree, expected_tree)\n\n    def assert_all_listings_checked(self, listings, exceptions=[]):\n        for i, listing in enumerate(listings):\n            if i in exceptions:\n                continue\n            if listing.skip:\n                continue\n\n            if type(listing) == CodeListing:\n                self.assertTrue(\n                    listing.was_written, \"Listing %d not written:\\n%s\" % (i, listing)\n                )\n            if type(listing) == Command:\n                self.assertTrue(\n                    listing.was_run, \"Command %d not run:\\n%s\" % (i, listing)\n                )\n            if type(listing) == Output:\n                self.assertTrue(\n                    listing.was_checked, \"Output %d not checked:\\n%s\" % (i, listing)\n                )\n\n    def check_test_code_cycle(self, pos, test_command_in_listings=True, ft=False):\n        self.write_to_file(self.listings[pos])\n        if test_command_in_listings:\n            pos += 1\n            self.assertIn(\"test\", self.listings[pos])\n            test_run = self.run_command(self.listings[pos])\n        elif ft:\n            test_run = self.run_command(Command(\"python functional_tests.py\"))\n        else:\n            test_run = self.run_command(\n                Command(f\"python {self._manage_py()} test lists\")\n            )\n        pos += 1\n        self.assert_console_output_correct(test_run, self.listings[pos])\n\n    def unset_PYTHONDONTWRITEBYTECODE(self):\n        # so any references to  __pycache__ in the book work\n        if \"PYTHONDONTWRITEBYTECODE\" in os.environ:\n            del os.environ[\"PYTHONDONTWRITEBYTECODE\"]\n\n    def run_test_and_check_result(self, bdd=False):\n        if bdd:\n            self.assertIn(\"behave\", self.listings[self.pos])\n        else:\n            self.assertIn(\"test\", self.listings[self.pos])\n        if bdd:\n            test_run = self.run_command(self.listings[self.pos], ignore_errors=True)\n        else:\n            test_run = self.run_command(self.listings[self.pos])\n        self.assert_console_output_correct(test_run, self.listings[self.pos + 1])\n        self.pos += 2\n\n    def run_js_tests(self, tests_path: Path):\n        p = subprocess.run(\n            [\"python\", str(JASMINE_RUNNER), str(tests_path)],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            # env={**os.environ, \"OPENSSL_CONF\": \"/dev/null\"},\n            check=False,\n        )\n        return p.stdout.decode()\n\n    def check_jasmine_output(self, expected_output):\n        lists_tests = Path(self.tempdir) / \"src/lists/static/tests/SpecRunner.html\"\n        assert lists_tests.exists()\n        lists_run = self.run_js_tests(lists_tests)\n        self.assert_console_output_correct(lists_run, expected_output)\n\n    def check_current_contents(self, listing, actual_contents):\n        print(\"CHECK CURRENT CONTENTS\")\n        stripped_actual_lines = [l.strip() for l in actual_contents.split(\"\\n\")]\n        listing_contents = re.sub(r\" +#$\", \"\", listing.contents, flags=re.MULTILINE)\n        for block in split_blocks(listing_contents):\n            stripped_block = [line.strip() for line in block.strip().split(\"\\n\")]\n            for line in stripped_block:\n                self.assertIn(\n                    line,\n                    stripped_actual_lines,\n                    f\"{line!r} not found in\\n\"\n                    + \"\\n\".join(repr(l) for l in stripped_actual_lines),\n                )\n            self.assertTrue(\n                contains(stripped_actual_lines, stripped_block),\n                \"\\n{}\\n\\nnot found in\\n\\n{}\".format(\n                    \"\\n\".join(stripped_block), \"\\n\".join(stripped_actual_lines)\n                ),\n            )\n        listing.was_written = True\n\n    def check_commit(self, pos):\n        if self.listings[pos].endswith(\"commit -a\"):\n            self.listings[pos] = Command(\n                self.listings[pos] + 'm \"commit for listing %d\"' % (self.pos,)\n            )\n        elif self.listings[pos].endswith(\"commit\"):\n            self.listings[pos] = Command(\n                self.listings[pos] + ' -am \"commit for listing %d\"' % (self.pos,)\n            )\n\n        commit = self.run_command(self.listings[pos])\n        assert \"insertion\" in commit or \"changed\" in commit\n        self.pos += 1\n\n    def check_diff_or_status(self, pos):\n        LIKELY_FILES = [\n            \"urls.py\",\n            \"tests.py\",\n            \"views.py\",\n            \"functional_tests.py\",\n            \"settings.py\",\n            \"home.html\",\n            \"list.html\",\n            \"base.html\",\n            \"test_\",\n            \"base.py\",\n            \"test_my_lists.py\",\n            \"deploy-playbook.yaml\",\n        ]\n        self.assertTrue(\"diff\" in self.listings[pos] or \"status\" in self.listings[pos])\n        git_output = self.run_command(self.listings[pos])\n        self.pos += 1\n        comment = self.listings[pos + 1]\n        if comment.skip:\n            comment.was_checked = True\n            self.pos += 1\n            return\n        if comment.type != \"output\":\n            return\n        if not any(\"/\" + l in git_output for l in LIKELY_FILES):\n            if not any(f in git_output for f in (\"lists/\", \"functional_tests.py\")):\n                self.fail(\"no likely files in diff output %s\" % (git_output,))\n        for expected_file in LIKELY_FILES:\n            if \"/\" + expected_file in git_output:\n                if expected_file not in comment:\n                    self.fail(\n                        \"could not find %s in comment %r given git output\\n%s\"\n                        % (expected_file, comment, git_output)\n                    )\n                self.listings[pos + 1].was_checked = True\n        comment.was_checked = True\n        self.pos += 1\n\n    def _manage_py(self):\n        if (self.tempdir / \"src/manage.py\").exists():\n            # if we're later in the book,\n            # we've moved everything into an src folder\n            return \"src/manage.py\"\n        return \"manage.py\"\n\n    def start_dev_server(self):\n        self.run_command(Command(\"python manage.py runserver\"))\n        self.dev_server_running = True\n        time.sleep(1)\n\n    def restart_dev_server(self):\n        print(\"restarting dev server\")\n        self.run_command(Command(\"pkill -f runserver\"))\n        time.sleep(1)\n        self.start_dev_server()\n        time.sleep(1)\n\n    def run_unit_tests(self):\n        if (self.tempdir / \"src/accounts/tests\").exists():\n            return self.run_command(\n                Command(f\"python {self._manage_py()} test lists accounts\")\n            )\n        else:\n            return self.run_command(Command(f\"python {self._manage_py()} test lists\"))\n\n    def run_fts(self):\n        if (self.tempdir / \"functional_tests\").exists():\n            return self.run_command(\n                Command(f\"python {self._manage_py()} test functional_tests\")\n            )\n        if (self.tempdir / \"src/functional_tests\").exists():\n            return self.run_command(\n                Command(f\"python {self._manage_py()} test functional_tests\")\n            )\n        else:\n            return self.run_command(Command(\"python functional_tests.py\"))\n\n    def run_interactive_manage_py(self, listing):\n        output_before = self.listings[self.pos + 1]\n        assert isinstance(output_before, Output)\n\n        LIKELY_INPUTS = (\"yes\", \"no\", \"1\", \"2\", \"''\")\n        user_input = self.listings[self.pos + 2]\n        if isinstance(user_input, Command) and user_input in LIKELY_INPUTS:\n            if user_input == \"yes\":\n                print(\"yes case\")\n                # in this case there is moar output after the yes\n                output_after = self.listings[self.pos + 3]\n                assert isinstance(output_after, Output)\n                expected_output = Output(\n                    wrap_long_lines(output_before + \" \" + output_after.lstrip())\n                )\n                next_output = None\n            elif user_input == \"1\":\n                print(\"migrations 1 case\")\n                # in this case there is another hop\n                output_after = self.listings[self.pos + 3]\n                assert isinstance(output_after, Output)\n                first_input = user_input\n                next_input = self.listings[self.pos + 4]\n                assert isinstance(next_input, Command)\n                next_output = self.listings[self.pos + 5]\n                expected_output = Output(\n                    wrap_long_lines(\n                        output_before + \"\\n\" + output_after + \"\\n\" + next_output\n                    )\n                )\n                user_input = Command(first_input + \"\\n\" + next_input)\n            else:\n                expected_output = output_before\n                output_after = None\n                next_output = None\n            if user_input == \"2\":\n                ignore_errors = True\n            else:\n                ignore_errors = False\n\n        else:\n            user_input = None\n            expected_output = output_before\n            output_after = None\n            ignore_errors = True\n            next_output = None\n\n        output = self.run_command(\n            listing, user_input=user_input, ignore_errors=ignore_errors\n        )\n        self.assert_console_output_correct(output, expected_output)\n\n        listing.was_checked = True\n        output_before.was_checked = True\n        self.pos += 2\n        if user_input is not None:\n            user_input.was_run = True\n            self.pos += 1\n        if output_after is not None:\n            output_after.was_checked = True\n            self.pos += 1\n        if next_output is not None:\n            self.pos += 2\n            next_output.was_checked = True\n            first_input.was_run = True\n            next_input.was_run = True\n\n    def recognise_listing_and_process_it(self):\n        listing = self.listings[self.pos]\n        if listing.pause_first:\n            print(\"pausing first\")\n            time.sleep(2)\n        if listing.dofirst:\n            print(\"DOFIRST\", listing.dofirst)\n            self.sourcetree.patch_from_commit(\n                listing.dofirst,\n            )\n        if listing.skip:\n            print(\"SKIP\")\n            listing.was_checked = True\n            listing.was_written = True\n            self.pos += 1\n        elif listing.against_server and not DO_SERVER_COMMANDS:\n            print(\"SKIP AGAINST SERVER\")\n            listing.was_checked = True\n            listing.was_run = True\n            self.pos += 1\n        elif listing.type == \"test\":\n            print(\"TEST RUN\")\n            self.run_test_and_check_result()\n        elif listing.type == \"bdd test\":\n            print(\"BDD TEST RUN\")\n            self.run_test_and_check_result(bdd=True)\n        elif listing.type == \"git diff\":\n            print(\"GIT DIFF\")\n            self.check_diff_or_status(self.pos)\n        elif listing.type == \"git status\":\n            print(\"STATUS\")\n            self.check_diff_or_status(self.pos)\n        elif listing.type == \"git commit\":\n            print(\"COMMIT\")\n            self.check_commit(self.pos)\n        elif listing.type == \"interactive manage.py\":\n            print(\"INTERACTIVE MANAGE.PY\")\n            self.run_interactive_manage_py(listing)\n        elif listing.type == \"tree\":\n            print(\"TREE\")\n            self.assert_directory_tree_correct(listing)\n            self.pos += 1\n\n        elif listing.type == \"server command\":\n            if DO_SERVER_COMMANDS:\n                assert 0, \"re-implement\"\n                server_output = self.run_server_command(listing)\n            listing.was_run = True\n            self.pos += 1\n            next_listing = self.listings[self.pos]\n            if next_listing.type == \"output\" and not next_listing.skip:\n                if DO_SERVER_COMMANDS:\n                    for line in next_listing.split(\"\\n\"):\n                        line = line.split(\"[...]\")[0].strip()\n                        line = re.sub(r\"\\s+\", \" \", line)\n                        server_output = re.sub(r\"\\s+\", \" \", server_output)\n                        self.assertIn(line, server_output)\n                next_listing.was_checked = True\n                self.pos += 1\n\n        elif listing.type == \"against staging\":\n            print(\"AGAINST STAGING\")\n            next_listing = self.listings[self.pos + 1]\n            if DO_SERVER_COMMANDS:\n                output = self.run_command(listing, ignore_errors=listing.ignore_errors)\n                listing.was_checked = True\n            else:\n                listing.skip = True\n            if next_listing.type == \"output\" and not next_listing.skip:\n                if DO_SERVER_COMMANDS:\n                    self.assert_console_output_correct(output, next_listing)\n                    next_listing.was_checked = True\n                else:\n                    next_listing.skip = True\n                self.pos += 2\n\n        elif listing.type == \"docker run tty\":\n            self.sourcetree.run_command(\n                \"docker kill $(docker ps -q)\", ignore_errors=True, silent=True\n            )\n            fixed = Command(listing.replace(\" -it \", \" -t \"))\n            if \"docker run --platform=linux/amd64 -t debug-ci\" in fixed:\n                fixed = Command(\n                    fixed.replace(\n                        \"docker run --platform=linux/amd64 -t debug-ci\",\n                        \"docker run -e PYTHON_COLORS=0 --platform=linux/amd64 -t debug-ci\",\n                    )\n                )\n            next_listing = self.listings[self.pos + 1]\n            if next_listing.type == \"output\" and not next_listing.skip:\n                output = self.run_command(fixed, ignore_errors=listing.ignore_errors)\n                listing.was_run = True\n                self.assert_console_output_correct(output, next_listing)\n                next_listing.was_checked = True\n                self.pos += 2\n            else:\n                self.run_command(fixed, ignore_errors=listing.ignore_errors)\n                listing.was_run = True\n                listing.was_checked = True\n                self.pos += 1\n\n        elif listing.type == \"docker exec\":\n            container_id = self.sourcetree.run_command(\n                \"docker ps --filter=ancestor=superlists -q\"\n            ).strip()\n            fixed = Command(listing.replace(\"container-id-or-name\", container_id))\n            next_listing = self.listings[self.pos + 1]\n            if next_listing.type == \"output\" and not next_listing.skip:\n                output = self.run_command(fixed, ignore_errors=listing.ignore_errors)\n                listing.was_run = True\n                self.assert_console_output_correct(output, next_listing)\n                next_listing.was_checked = True\n                self.pos += 2\n            else:\n                self.run_command(fixed, ignore_errors=listing.ignore_errors)\n                listing.was_run = True\n                listing.was_checked = True\n                self.pos += 1\n\n        elif listing.type == \"other command\":\n            print(\"A COMMAND\")\n            next_listing = self.listings[self.pos + 1]\n            if next_listing.type == \"output\" and not next_listing.skip:\n                output = self.run_command(listing, ignore_errors=listing.ignore_errors)\n                ls = listing.startswith(\"ls\")\n                self.assert_console_output_correct(output, next_listing, ls=ls)\n                next_listing.was_checked = True\n                self.pos += 2\n            elif \"tree\" in listing and next_listing.type == \"tree\":\n                assert listing.startswith(\"tree\")\n                _, _, target = listing.partition(\"tree\")\n                output = self._run_tree(target=target)\n                listing.was_run = True\n                self.assert_console_output_correct(output, next_listing)\n                next_listing.was_checked = True\n                self.pos += 2\n            else:\n                self.run_command(listing, ignore_errors=listing.ignore_errors)\n                listing.was_checked = True\n                self.pos += 1\n\n        elif listing.type == \"diff\":\n            print(\"DIFF\")\n            self.apply_patch(listing)\n\n        elif listing.type == \"code listing currentcontents\":\n            actual_contents = self.sourcetree.get_contents(listing.filename)\n            self.check_current_contents(listing, actual_contents)\n            self.pos += 1\n\n        elif listing.type == \"code listing\":\n            print(\"CODE\")\n            self.write_to_file(listing)\n            self.pos += 1\n\n        elif listing.type == \"code listing with git ref\":\n            print(\"CODE FROM GIT REF\")\n            self.sourcetree.apply_listing_from_commit(listing)\n            self.pos += 1\n\n        elif listing.type == \"server code listing\":\n            assert 0, \"reimplement\"\n\n        elif listing.type == \"jasmine output\":\n            self.check_jasmine_output(listing)\n            self.pos += 1\n\n        elif listing.type == \"output\":\n            test_run = self.run_unit_tests()\n            if \"OK\" in test_run.splitlines() and \"OK\" not in listing.splitlines():\n                print(\"unit tests pass, must be an FT:\\n\", test_run)\n                test_run = self.run_fts()\n            try:\n                self.assert_console_output_correct(test_run, listing)\n            except AssertionError as e:\n                if \"OK\" in test_run.splitlines() and \"OK\" in listing.splitlines():\n                    print(\"got error when checking unit tests\", e)\n                    test_run = self.run_fts()\n                    self.assert_console_output_correct(test_run, listing)\n                else:\n                    raise\n\n            self.pos += 1\n\n        else:\n            self.fail(\"not implemented for \" + str(listing))\n"
  },
  {
    "path": "tests/chapters.py",
    "content": "CHAPTERS = [\n    # part 1\n    \"chapter_01\",\n    \"chapter_02_unittest\",\n    \"chapter_03_unit_test_first_view\",\n    \"chapter_04_philosophy_and_refactoring\",\n    \"chapter_05_post_and_database\",\n    \"chapter_06_explicit_waits_1\",\n    \"chapter_07_working_incrementally\",\n\n    # part 2: deploy\n    \"chapter_08_prettification\",\n    \"chapter_09_docker\",\n    \"chapter_10_production_readiness\",\n    \"chapter_11_server_prep\",\n    \"chapter_12_ansible\",\n\n    # part 3: validation\n    \"chapter_13_organising_test_files\",\n    \"chapter_14_database_layer_validation\",\n    \"chapter_15_simple_form\",\n    \"chapter_16_advanced_forms\",\n\n    # part 4: spiking and mocking\n    \"chapter_17_javascript\",\n    \"chapter_18_second_deploy\",\n    \"chapter_19_spiking_custom_auth\",\n    \"chapter_20_mocking_1\",\n    \"chapter_21_mocking_2\",\n    \"chapter_22_fixtures_and_wait_decorator\",\n    \"chapter_23_debugging_prod\",\n    \"chapter_24_outside_in\",\n    \"chapter_25_CI\",\n    \"chapter_26_page_pattern\",\n\n    \"chapter_27_hot_lava\",\n\n]\n"
  },
  {
    "path": "tests/check_links.py",
    "content": "from lxml import html\nimport requests\n\nwith open('book.html') as f:\n    node = html.fromstring(f.read())\n\nall_hrefs = [e.get('href') for e in node.cssselect('a')]\nurls = [l for l in all_hrefs if l and l.startswith('h')]\n\nfor l in urls:\n    try:\n        response = requests.get(l)\n        if response.status_code != 200:\n            print(l)\n        else:\n            print('.', end=\"\", flush=True)\n    except requests.RequestException:\n        print(l)\n\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\npytest.register_assert_rewrite('sourcetree')\n"
  },
  {
    "path": "tests/examples.py",
    "content": "CODE_LISTING_WITH_CAPTION = \"\"\"<div class=\"exampleblock sourcecode\">\n<div class=\"title\">functional_tests.py</div>\n<div class=\"content\">\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre class=\"CodeRay highlight\"><code data-lang=\"python\"><span class=\"keyword\">from</span> <span class=\"include\">selenium</span> <span class=\"keyword\">import</span> <span class=\"include\">webdriver</span>\n\nbrowser = webdriver.Firefox()\nbrowser.get(<span class=\"string\"><span class=\"delimiter\">'</span><span class=\"content\">http://localhost:8000</span><span class=\"delimiter\">'</span></span>)\n\n<span class=\"keyword\">assert</span> <span class=\"string\"><span class=\"delimiter\">'</span><span class=\"content\">Django</span><span class=\"delimiter\">'</span></span> <span class=\"keyword\">in</span> browser.title</code></pre>\n</div>\n</div>\n</div>\n</div>\"\"\"\n\nCODE_LISTING_WITH_CAPTION_AND_GIT_COMMIT_REF = \"\"\"<div class=\"exampleblock sourcecode\">\n<div class=\"title\">functional_tests/tests.py (ch06l001)</div>\n<div class=\"content\">\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre class=\"CodeRay highlight\"><code data-lang=\"python\"><span class=\"keyword\">from</span> <span class=\"include\">django.test</span> <span class=\"keyword\">import</span> <span class=\"include\">LiveServerTestCase</span>\n<span class=\"keyword\">from</span> <span class=\"include\">selenium</span> <span class=\"keyword\">import</span> <span class=\"include\">webdriver</span>\n<span class=\"keyword\">from</span> <span class=\"include\">selenium.webdriver.common.keys</span> <span class=\"keyword\">import</span> <span class=\"include\">Keys</span>\n<span class=\"keyword\">import</span> <span class=\"include\">time</span>\n\n\n<span class=\"keyword\">class</span> <span class=\"class\">NewVisitorTest</span>(LiveServerTestCase):\n\n    <span class=\"keyword\">def</span> <span class=\"function\">setUp</span>(<span class=\"predefined-constant\">self</span>):\n        [...]</code></pre>\n</div>\n</div>\n</div>\n</div>\"\"\"\n\n\nSERVER_COMMAND = \"\"\"<div class=\"listingblock server-commands\">\n<div class=\"content\">\n<pre><code>elspeth@server:$ <strong>sudo do stuff</strong></code></pre>\n</div></div>\"\"\"\n\n\nCOMMANDS_WITH_VIRTUALENV = \"\"\"<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>$ <strong>source ../.venv/bin/activate</strong>\n(virtualenv)$ <strong>python manage.py test lists</strong>\n[...]\nImportError: No module named django</code></pre>\n</div></div>\"\"\"\n\n\nCODE_LISTING_WITH_DIFF_FORMATING_AND_COMMIT_REF = \"\"\"<div class=\"listingblock sourcecode\">\n<div class=\"title\">lists/tests/test_models.py (ch09l010)</div>\n<div class=\"content\"><div class=\"highlight\"><pre><span class=\"gh\">diff --git a/lists/tests/test_views.py b/lists/tests/test_views.py</span>\n<span class=\"gh\">index fc1eb64..9305bf8 100644</span>\n<span class=\"gd\">--- a/lists/tests/test_views.py</span>\n<span class=\"gi\">+++ b/lists/tests/test_views.py</span>\n<span class=\"gu\">@@ -81,33 +81,3 @@ class ListViewTest(TestCase):</span>\n         self.assertTemplateUsed(response, 'list.html')\n         self.assertEqual(response.context['list'], list)\n\n<span class=\"gd\">-</span>\n<span class=\"gd\">-</span>\n<span class=\"gd\">-class ListAndItemModelsTest(TestCase):</span>\n<span class=\"gd\">-</span>\n<span class=\"gd\">-    def test_saving_and_retrieving_items(self):</span>\n[...]\n</pre></div></div></div>\"\"\"\n\n\nCOMMAND_MADE_WITH_ATS = \"\"\"\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>$ <strong>grep id_new_item functional_tests/tests/test*</strong></code></pre>\n</div></div>\n\"\"\"\n\nOUTPUT_WITH_SKIPME = \"\"\"\n<div class=\"listingblock skipme\">\n<div class=\"content\"><div class=\"highlight\"><pre><span class=\"k\">try</span><span class=\"p\">:</span>\n    <span class=\"n\">item</span><span class=\"o\">.</span><span class=\"n\">save</span><span class=\"p\">()</span>\n    <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">fail</span><span class=\"p\">(</span><span class=\"s\">'The full_clean should have raised an exception'</span><span class=\"p\">)</span>\n<span class=\"k\">except</span> <span class=\"n\">ValidationError</span><span class=\"p\">:</span>\n    <span class=\"k\">pass</span>\n</pre></div></div></div>\"\"\"\n\n\nCODE_LISTING_WITH_SKIPME = \"\"\"\n<div class=\"listingblock sourcecode skipme\">\n<div class=\"title\">lists/functional_tests/test_list_item_validation.py</div>\n<div class=\"content\"><div class=\"highlight\"><pre>    <span class=\"k\">def</span> <span class=\"nf\">DONTtest_cannot_add_empty_list_items</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">):</span>\n</pre></div></div></div>\"\"\"\n\nOUTPUTS_WITH_DOFIRST = \"\"\"\n<div class=\"listingblock dofirst-ch09l058\">\n<div class=\"content\">\n<pre><code>$ <strong>grep -r id_new_item lists/</strong>\n\nlists/static/base.css:#id_new_item {\nlists/templates/list.html:        &lt;input name=\"item_text\" id=\"id_new_item\"\nplaceholder=\"Enter a to-do item\" /&gt;</code></pre>\n</div></div>\"\"\"\n\nOUTPUTS_WITH_CURRENTCONTENTS = \"\"\"\n<div class=\"listingblock sourcecode currentcontents\">\n<div class=\"title\">superlists/urls.py</div>\n<div class=\"content\"><div class=\"highlight\"><pre><span class=\"kn\">from</span> <span class=\"nn\">django.conf.urls</span> <span class=\"kn\">import</span> <span class=\"n\">patterns</span><span class=\"p\">,</span> <span class=\"n\">include</span><span class=\"p\">,</span> <span class=\"n\">url</span>\n\n<span class=\"kn\">from</span> <span class=\"nn\">django.contrib</span> <span class=\"kn\">import</span> <span class=\"n\">admin</span>\n<span class=\"n\">admin</span><span class=\"o\">.</span><span class=\"n\">autodiscover</span><span class=\"p\">()</span>\n\n<span class=\"n\">urlpatterns</span> <span class=\"o\">=</span> <span class=\"n\">patterns</span><span class=\"p\">(</span><span class=\"s\">''</span><span class=\"p\">,</span>\n    <span class=\"c\"># Examples:</span>\n    <span class=\"c\"># url(r'^$', 'superlists.views.home', name='home'),</span>\n    <span class=\"c\"># url(r'^blog/', include('blog.urls')),</span>\n\n    <span class=\"n\">url</span><span class=\"p\">(</span><span class=\"s\">r'^admin/'</span><span class=\"p\">,</span> <span class=\"n\">include</span><span class=\"p\">(</span><span class=\"n\">admin</span><span class=\"o\">.</span><span class=\"n\">site</span><span class=\"o\">.</span><span class=\"n\">urls</span><span class=\"p\">)),</span>\n<span class=\"p\">)</span>\n<span class=\"kn\">from</span> <span class=\"nn\">django.conf.urls</span> <span class=\"kn\">import</span> <span class=\"n\">patterns</span><span class=\"p\">,</span> <span class=\"n\">include</span><span class=\"p\">,</span> <span class=\"n\">url</span>\n</pre></div></div></div>\"\"\"\n\nJASMINE_OUTPUT = \"\"\"\n<div class=\"listingblock jasmine-output\">\n<div class=\"content\">\n<pre>2 specs, 0 failures, randomized with seed 12345        finished in 0.01s\n\nSuperlists tests\n  * check we know how to hide things\n  * sense check our html fixture</pre>\n</div>\n\"\"\"\n\nOUTPUT_WITH_CONTINUATION = \"\"\"\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>$ <strong>wget -O bootstrap.zip https://github.com/twbs/bootstrap/releases/download/\\\nv3.1.0/bootstrap-3.1.0-dist.zip</strong>\n$ <strong>unzip bootstrap.zip</strong>\n$ <strong>mkdir lists/static</strong>\n$ <strong>mv dist lists/static/bootstrap</strong>\n$ <strong>rm bootstrap.zip</strong></code></pre>\n</div></div>\n\"\"\"\n\n\nOUTPUT_WITH_COMMANDS_INLINE = \"\"\"\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre><code>$ <strong>python manage.py makemigrations</strong>\nYou are trying to add a non-nullable field 'list' to item without a default;\nwe can't do that (the database needs something to populate existing rows).\nPlease select a fix:\n 1) Provide a one-off default now (will be set on all existing rows)\n 2) Quit, and let me add a default in models.py\nSelect an option: <strong>1</strong>\nPlease enter the default value now, as valid Python\nThe datetime module is available, so you can do e.g. datetime.date.today()\n&gt;&gt;&gt; <strong>''</strong>\nMigrations for 'lists':\n  0003_item_list.py:\n    - Add field list to item</code></pre>\n</div></div>\n\"\"\"\n\n\nCODE_LISTING_WITH_ASCIIDOCTOR_CALLOUTS = \"\"\"\n<div class=\"exampleblock sourcecode\">\n<div class=\"title\">src/lists/templates/base.html (ch16l004)</div>\n<div class=\"content\">\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre class=\"pygments highlight\"><code data-lang=\"html\"><span></span>    <span class=\"tok-p\">&lt;/</span><span class=\"tok-nt\">div</span><span class=\"tok-p\">&gt;</span>\n\n    <span class=\"tok-p\">&lt;</span><span class=\"tok-nt\">script</span><span class=\"tok-p\">&gt;</span>\n<span class=\"tok-w\">      </span><span class=\"tok-kd\">const</span><span class=\"tok-w\"> </span><span class=\"tok-nx\">textInput</span><span class=\"tok-w\"> </span><span class=\"tok-o\">=</span><span class=\"tok-w\"> </span><span class=\"tok-nb\">document</span><span class=\"tok-p\">.</span><span class=\"tok-nx\">querySelector</span><span class=\"tok-p\">(</span><span class=\"tok-s2\">&quot;#id_text&quot;</span><span class=\"tok-p\">);</span><span class=\"tok-w\">  </span><i class=\"conum\" data-value=\"1\"></i><b>(1)</b>\n<span class=\"tok-w\">      </span><span class=\"tok-nx\">textInput</span><span class=\"tok-p\">.</span><span class=\"tok-nx\">oninput</span><span class=\"tok-w\"> </span><span class=\"tok-o\">=</span><span class=\"tok-w\"> </span><span class=\"tok-p\">()</span><span class=\"tok-w\"> </span><span class=\"tok-p\">=&gt;</span><span class=\"tok-w\"> </span><span class=\"tok-p\">{</span><span class=\"tok-w\">  </span><i class=\"conum\" data-value=\"2\"></i><b>(2)</b> <i class=\"conum\" data-value=\"3\"></i><b>(3)</b>\n<span class=\"tok-w\">        </span><span class=\"tok-kd\">const</span><span class=\"tok-w\"> </span><span class=\"tok-nx\">errorMsg</span><span class=\"tok-w\"> </span><span class=\"tok-o\">=</span><span class=\"tok-w\"> </span><span class=\"tok-nb\">document</span><span class=\"tok-p\">.</span><span class=\"tok-nx\">querySelector</span><span class=\"tok-p\">(</span><span class=\"tok-s2\">&quot;.invalid-feedback&quot;</span><span class=\"tok-p\">);</span>\n<span class=\"tok-w\">        </span><span class=\"tok-nx\">errorMsg</span><span class=\"tok-p\">.</span><span class=\"tok-nx\">style</span><span class=\"tok-p\">.</span><span class=\"tok-nx\">display</span><span class=\"tok-w\"> </span><span class=\"tok-o\">=</span><span class=\"tok-w\"> </span><span class=\"tok-s2\">&quot;none&quot;</span><span class=\"tok-p\">;</span><span class=\"tok-w\">  </span><i class=\"conum\" data-value=\"4\"></i><b>(4)</b>\n<span class=\"tok-w\">      </span><span class=\"tok-p\">}</span>\n<span class=\"tok-w\">    </span><span class=\"tok-p\">&lt;/</span><span class=\"tok-nt\">script</span><span class=\"tok-p\">&gt;</span></code></pre>\n</div>\n</div>\n</div>\n</div>\n\"\"\"\n\n\nOUTPUT_WITH_CALLOUTS = \"\"\"<div class=\"listingblock\">\n<div class=\"content\">\n<pre>$ <strong>python manage.py test functional_tests.test_list_item_validation</strong>\nCreating test database for alias 'default'...\nE\n======================================================================\nERROR: test_cannot_add_empty_list_items\n(functional_tests.test_list_item_validation.ItemValidationTest)\n ---------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"...goat-book/functional_tests/test_list_item_validation.py\", line\n15, in test_cannot_add_empty_list_items\n    self.wait_for(lambda: self.assertEqual(  <i class=\"conum\" data-value=\"1\"></i><b>(1)</b>\n  File \"...goat-book/functional_tests/base.py\", line 37, in wait_for\n    raise e  <i class=\"conum\" data-value=\"2\"></i><b>(2)</b>\n  File \"...goat-book/functional_tests/base.py\", line 34, in wait_for\n    return fn()  <i class=\"conum\" data-value=\"2\"></i><b>(2)</b>\n  File \"...goat-book/functional_tests/test_list_item_validation.py\", line\n16, in &lt;lambda&gt;  <i class=\"conum\" data-value=\"3\"></i><b>(3)</b>\n    self.browser.find_element_by_css_selector('.has-error').text,  <i class=\"conum\" data-value=\"3\"></i><b>(3)</b>\n[...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: .has-error\n\n\n ---------------------------------------------------------------------\nRan 1 test in 10.575s\n\nFAILED (errors=1)</pre>\n</div>\n</div>\"\"\"\n\nEXAMPLE_DIFF_LISTING = \"\"\"\n<div class=\"exampleblock sourcecode small-code\">\n<div class=\"title\">lists/templates/home.html (ch07l018)</div>\n<div class=\"content\">\n<div class=\"listingblock\">\n<div class=\"content\">\n<pre class=\"CodeRay highlight\"><code data-lang=\"diff\">   &lt;body&gt;\n<span class=\"line delete\"><span class=\"delete\">-</span>    &lt;h1&gt;<span class=\"eyecatcher\">Your</span> To-Do list&lt;/h1&gt;</span>\n<span class=\"line insert\"><span class=\"insert\">+</span>    &lt;h1&gt;<span class=\"eyecatcher\">Start a new</span> To-Do list&lt;/h1&gt;</span>\n     &lt;form method=&quot;POST&quot; action=&quot;/&quot;&gt;\n       &lt;input name=&quot;item_text&quot; id=&quot;id_new_item&quot; placeholder=&quot;Enter a to-do item&quot; /&gt;\n       {% csrf_token %}\n     &lt;/form&gt;\n<span class=\"line delete\"><span class=\"delete\">-</span>    &lt;table id=&quot;id_list_table&quot;&gt;</span>\n<span class=\"line delete\"><span class=\"delete\">-</span>      {% for item in items %}</span>\n<span class=\"line delete\"><span class=\"delete\">-</span>        &lt;tr&gt;&lt;td&gt;{{ forloop.counter }}: {{ item.text }}&lt;/td&gt;&lt;/tr&gt;</span>\n<span class=\"line delete\"><span class=\"delete\">-</span>      {% endfor %}</span>\n<span class=\"line delete\"><span class=\"delete\">-</span>    &lt;/table&gt;</span>\n   &lt;/body&gt;</code></pre>\n</div>\n</div>\n</div>\n</div>\n\"\"\"\n"
  },
  {
    "path": "tests/my-phantomjs-qunit-runner.js",
    "content": "/*global require, phantom */\nvar system = require('system');\n\nif (!system.args[1]){\n    console.log('Pass path to test file as second arg');\n    phantom.exit();\n}\n\nvar path = system.args[1];\nif (path.indexOf('/') !== 0) {\n    path = system.env.PWD + '/' + path;\n}\n\nvar page = require('webpage').create();\nvar logs = '';\n\npage.onConsoleMessage = function (msg) {\n  logs += msg + '\\n';\n};\n\npage.open('file://' + path, function () {\n    setTimeout(function() {\n        var output = page.evaluate( function () {\n            var results = '';\n\n            var headline = $('#qunit-testresult').text().split('.')[1] + '.';\n            results += headline + '\\n';\n\n            var testCounter = 0;\n            $('#qunit-tests li').each(function() {\n                var li = $(this);\n                if (li.prop('id').indexOf('qunit-test-output') !== -1){\n                    testCounter += 1;\n                    var resultLine = '';\n                    resultLine += testCounter + '. ';\n                    if (li.find('.module-name').length > 0) {\n                        resultLine += li.find('.module-name').text() + ': ';\n                    }\n                    resultLine += li.find('.test-name').text();\n                    resultLine += ' ' + li.find('.counts').text();\n                    resultLine = resultLine.replace('Rerun', '');\n                    var fails = li.find('.fail');\n                    if (fails.text()) {\n                        li.find('.qunit-assert-list li').each(function (assertCounter) {\n                            var assert = $(this);\n                            resultLine += '\\n';\n                            resultLine += '    ' + (assertCounter + 1) + '. ';\n                            resultLine += assert.find('.test-message').text();\n                            if (assert.find('.fail')) {\n                                assert.find('tr').each(function () {\n                                    resultLine += '\\n';\n                                    resultLine += '        ' + $(this).text();\n                                });\n                            }\n                        });\n                    }\n                    results += resultLine + '\\n';\n                }\n            });\n\n            return results;\n        });\n        console.log(output);\n        console.log(logs);\n        phantom.exit();\n\n    }, 100);\n});\n\n"
  },
  {
    "path": "tests/run-js-spec.py",
    "content": "#!python\nimport re\nimport sys\nimport time\nfrom pathlib import Path\n\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.remote.webelement import WebElement\n\n\ndef sub_book_path(text: str) -> str:\n    return re.sub(\n        r\"@file://(.+)/superlists/src/\",\n        \".../goat-book/src/\",\n        text,\n    )\n\n\ndef run(path: Path):\n    assert path.exists()\n\n    options = webdriver.FirefoxOptions()\n    options.add_argument(\"--headless\")\n    browser = webdriver.Firefox(options=options)\n    # options = webdriver.ChromeOptions()\n    # options.add_argument(\"--headless=new\")\n    # options.binary_location = \"/Applications/Vivaldi.app/Contents/MacOS/Vivaldi\"\n    # browser = webdriver.Chrome(options=options)\n    failed = False\n\n    def _el_text(sel, node: webdriver.Remote | WebElement = browser):\n        raw = \"\\n\".join(el.text for el in node.find_elements(By.CSS_SELECTOR, sel))\n        return re.sub(r\" 0\\.\\d+s\", \" 0.005s\", raw)\n\n    try:\n        browser.get(f\"file:///{path}?seed=12345\")\n        time.sleep(0.2)\n\n        # for entry in browser.get_log('browser'):\n        #     print(entry)\n\n        print(\n            f\"{_el_text('.jasmine-overall-result')}      {_el_text('.jasmine-duration')}\"\n        )\n\n        print(_el_text(\".jasmine-bar.jasmine-errored\"))\n\n        print(_el_text(\".jasmine-menu.jasmine-failure-list\"))\n        for failures_el in browser.find_elements(By.CSS_SELECTOR, \".jasmine-failures\"):\n            for spec_failure in failures_el.find_elements(\n                By.CSS_SELECTOR, \".jasmine-spec-detail.jasmine-failed\"\n            ):\n                failed = True\n                print()\n                print(_el_text(\".jasmine-description\", spec_failure))\n                print(_el_text(\".jasmine-messages\", spec_failure))\n\n        for success_el in browser.find_elements(By.CSS_SELECTOR, \".jasmine-summary\"):\n            for suite_el in success_el.find_elements(By.CSS_SELECTOR, \".jasmine-suite\"):\n                if suite_el.is_displayed():\n                    print(_el_text(\"li.jasmine-suite-detail\", suite_el))\n                    for spec_el in suite_el.find_elements(\n                        By.CSS_SELECTOR, \"ul.jasmine-specs li.jasmine-passed\"\n                    ):\n                        print(\"  * \" + spec_el.text)\n\n    finally:\n        browser.quit()\n    return failed\n\n\nif __name__ == \"__main__\":\n    _, fn, *__ = sys.argv\n    if fn.endswith(\"Spec.js\"):\n        fn = fn.replace(\"Spec.js\", \"SpecRunner.html\")\n    failed = run(Path(fn).resolve())\n    sys.exit(1 if failed is True else 0)\n"
  },
  {
    "path": "tests/slimerjs-0.9.0/LICENSE",
    "content": "The files SlimerJS are licensed under the MPL 2.0 (http://mozilla.org/MPL/2.0/),\nwith the exception of the sources file listed below (zipped into the omni.ja file in the\ndistributed version of SlimerJS), which are made available by\ntheir authors under the licenses listed alongside.\n\n* modules/addon-sdk/*\n    are released under the Mozilla Public License, v. 2.0\n    see http://mozilla.org/MPL/2.0/\n    These files comes from Firefox source code and Mozilla Addon-SDK source code\n    http://mxr.mozilla.org/mozilla-central/source/toolkit/addon-sdk/\n    https://github.com/laurentj/addon-sdk/tree/master/lib/sdk/io\n\n* components/httpd.js\n* modules/httpUtils.jsm\n    is released under the Mozilla Public License, v. 2.0\n    see http://mozilla.org/MPL/2.0/\n    These files comes from Mozilla source code\n    http://mxr.mozilla.org/mozilla-central/source/netwerk/test/httpserver/\n\n* components/ConsoleAPI.js\n* components/nsPrompter.js\n    is released under the Mozilla Public License, v. 2.0\n    see http://mozilla.org/MPL/2.0/\n    This file comes originally from the source code of Firefox\n    http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js\n    http://mxr.mozilla.org/mozilla-central/source/toolkit/components/prompts/src/nsPrompter.js\n\n* modules/slimer-sdk/net-log.js\n    is released under the Mozilla Public License, v. 2.0\n    see http://mozilla.org/MPL/2.0/\n    The author is Olivier Meunier and Laurent Jouanneau\n    and this file comes from https://github.com/olivier-m/jetpack-net-log/\n\n* modules/slimer-sdk/webpage.js\n    is released under the Mozilla Public License, v. 2.0\n    see http://mozilla.org/MPL/2.0/\n    Some pieces of code come from webpage.js of the\n    project https://github.com/olivier-m/jetpack-webpage/\n    The author of these pieces of code is Olivier Meunier\n\n* modules/slPhantomJSKeyCode.jsm\n\n  content of this file is a part of webpage.js from the PhantomJS\n  project from Ofi Labs.\n\n  Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>\n  Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com>\n  Copyright (C) 2011 James Roe <roejames12@hotmail.com>\n  Copyright (C) 2011 execjosh, http://execjosh.blogspot.com\n  Copyright (C) 2012 James M. Greene <james.m.greene@gmail.com>\n\n  Redistribution and use in source and binary forms, with or without\n  modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the <organization> nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\n  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\n  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "tests/slimerjs-0.9.0/README.md",
    "content": "# SlimerJS\n\nSlimerJS is a scriptable browser. It allows you to manipulate a web page\nwith a Javascript script: opening a webpage, clicking on links, modifying the content...\nIt is useful to do functional tests, page automaton, network monitoring, screen capture etc.\n\nGo to [http://slimerjs.org] to know more and to access to the documentation\n\n\n# Install\n\n- Install [Firefox](http://getfirefox.com),\n  or [XulRunner](http://ftp.mozilla.org/pub/mozilla.org/xulrunner/releases/19.0.2/runtimes/) (both version 18 or more)\n- [Download the latest package](http://download.slimerjs.org/slimerjs-0.5RC1.zip) or\n  [the source code of SlimerJS](https://github.com/laurentj/slimerjs/archive/master.zip) if you didn't it yet\n- On windows, a .bat is provided, but you can also launch slimer from a \"true\" console. In this case, you should install\n  [Cygwin](http://www.cygwin.com/) or any other unix environment to launch slimerjs.\n- SlimerJS needs to know where Firefox or XulRunner is stored. It tries to discover\n  itself the path but can fail. You must then set the environment variable\n  SLIMERJSLAUNCHER, which should contain the full path to the firefox binary:\n   - On linux: ```export SLIMERJSLAUNCHER=/usr/bin/firefox```\n   - on Windows: ```SET SLIMERJSLAUNCHER=\"c:\\Program Files\\Mozilla Firefox\\firefox.exe```\n   - On windows with cygwin : ```export SLIMERJSLAUNCHER=\"/cygdrive/c/program files/mozilla firefox/firefox.exe\"```\n   - On MacOS: ```export SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox```\n- You can of course set this variable in your .bashrc, .profile or in the computer\n   properties on Windows.\n\n# Launching SlimerJS\n\nOpen a terminal and go to the directory of SlimerJS (src/ if you downloaded the source code). Then launch:\n\n```\n    ./slimerjs myscript.js\n```\n\nIn the Windows commands console:\n\n```\n    slimerjs.bat myscript.js\n```\n\n\nThe given script myscripts.js is then executed in a window. If your script is\nshort, you probably won't see this window.\n\nYou can for example launch some tests if you execute SlimerJS from the source code:\n\n```\n    ./slimerjs ../test/initial-tests.js\n```\n\n# Launching a headless SlimerJS\n\nThere is a tool called xvfb, available on Linux and MacOS. It allows to launch\nany \"graphical\" programs without the need of X-Windows environment. Windows of\nthe application won't be shown and will be drawn only in memory.\n\nInstall it from your prefered repository (```sudo apt-get install xvfb```\nwith debian/ubuntu).\n\nThen launch SlimerJS like this:\n\n```\n    xvfb-run ./slimerjs myscript.js\n```\n\nYou won't see any windows. If you have any problems with xvfb, see its\ndocumentation.\n\n# Getting help\n\n- Ask your questions on the dedicated [mailing list](https://groups.google.com/forum/#!forum/slimerjs).\n- Discuss with us on IRC: channel #slimerjs on irc.mozilla.org.\n- Read the faq [on the website](http://slimerjs.org/faq.html).\n- Read [the documentation](http://docs.slimerjs.org/current/)\n"
  },
  {
    "path": "tests/slimerjs-0.9.0/application.ini",
    "content": "[App]\nVendor=Innophi\nName=SlimerJS\nVersion=0.9.0\nBuildID=20131211\nID=slimerjs@slimerjs.org\nCopyright=Copyright 2012-2013 Laurent Jouanneau & Innophi\n\n[Gecko]\nMinVersion=17.0.0\nMaxVersion=27.*\n"
  },
  {
    "path": "tests/slimerjs-0.9.0/slimerjs",
    "content": "#!/bin/bash\n\n#retrieve full path of the current script\n# symlinks are resolved, so application.ini could be found\n# this code comes from http://stackoverflow.com/questions/59895/\nSOURCE=\"${BASH_SOURCE[0]}\"\nwhile [ -h \"$SOURCE\" ]; do # resolve $SOURCE until the file is no longer a symlink\n  SLIMERDIR=\"$( cd -P \"$( dirname \"$SOURCE\" )\" && pwd )\"\n  SOURCE=\"$(readlink \"$SOURCE\")\"\n  [[ $SOURCE != /* ]] && SOURCE=\"$SLIMERDIR/$SOURCE\" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located\ndone\nSLIMERDIR=\"$( cd -P \"$( dirname \"$SOURCE\" )\" && pwd )\"\n\nSYSTEM=`uname -o 2>&1`\nif [ \"$SYSTEM\" == \"Cygwin\" ]\nthen\n   IN_CYGWIN=1\n   SLIMERDIR=`cygpath -w $SLIMERDIR`\nelse\n   IN_CYGWIN=\nfi\n\n# retrieve the path of a gecko launcher\nif [ \"$SLIMERJSLAUNCHER\" == \"\" ]\nthen\n    if [ -f \"$SLIMERDIR/xulrunner/xulrunner\" ]\n    then\n        SLIMERJSLAUNCHER=\"$SLIMERDIR/xulrunner/xulrunner\"\n    else\n        if [ -f \"$SLIMERDIR/xulrunner/xulrunner.exe\" ]\n        then\n            SLIMERJSLAUNCHER=\"$SLIMERDIR/xulrunner/xulrunner.exe\"\n        else\n            SLIMERJSLAUNCHER=`command -v firefox`\n            if [ \"$SLIMERJSLAUNCHER\" == \"\" ]\n            then\n                SLIMERJSLAUNCHER=`command -v xulrunner`\n                if [ \"$SLIMERJSLAUNCHER\" == \"\" ]\n                then\n                    echo \"SLIMERJSLAUNCHER environment variable is missing. Set it with the path to Firefox or XulRunner\"\n                    exit 1\n                fi\n            fi\n        fi\n    fi\nfi\n\nif [ ! -x \"$SLIMERJSLAUNCHER\" ]\nthen\n    echo \"SLIMERJSLAUNCHER environment variable does not contain an executable path. Set it with the path to Firefox\"\n    exit 1\nfi\n\nfunction showHelp() {\n\n    echo \"  --config=<file>                    Load the given configuration file\"\n    echo \"                                     (JSON formated)\"\n    echo \"  --debug=[yes|no]                   Prints additional warning and debug message\"\n    echo \"                                     (default is no)\"\n    echo \"  --disk-cache=[yes|no]              Enables disk cache (default is no).\"\n    echo \"  --help or -h                       Show this help\"\n    #echo \"  --ignore-ssl-errors=[yes|no]       Ignores SSL errors (default is no).\"\n    echo \"  --load-images=[yes|no]             Loads all inlined images (default is yes)\"\n    echo \"  --local-storage-quota=<number>     Sets the maximum size of the offline\"\n    echo \"                                     local storage (in KB)\"\n    #echo \"  --local-to-remote-url-access=[yes|no] Allows local content to access remote\"\n    #echo \"                                        URL (default is no)\"\n    echo \"  --max-disk-cache-size=<number>     Limits the size of the disk cache (in KB)\"\n    #echo \"  --output-encoding=<enc>            Sets the encoding for the terminal output\"\n    #echo \"                                     (default is 'utf8')\"\n    #echo \"  --remote-debugger-port=<number>    Starts the script in a debug harness and\"\n    #echo \"                                     listens on the specified port\"\n    #echo \"  --remote-debugger-autorun=[yes|no] Runs the script in the debugger immediately\"\n    #echo \"                                     (default is no)\"\n    echo \"  --proxy=<proxy url>                Sets the proxy server\"\n    echo \"  --proxy-auth=<username:password>   Provides authentication information for the\"\n    echo \"                                     proxy\"\n    echo \"  --proxy-type=[http|socks5|none|auto|system|config-url]    Specifies the proxy type (default is http)\"\n    #echo \"  --script-encoding=<enc>            Sets the encoding used for the starting\"\n    #echo \"                                     script (default is utf8)\"\n    #echo \"  --web-security=[yes|no]            Enables web security (default is yes)\"\n    echo \"  --version or v                     Prints out SlimerJS version\"\n    #echo \"  --webdriver or --wd or -w          Starts in 'Remote WebDriver mode' (embedded\"\n    #echo \"                                     GhostDriver) '127.0.0.1:8910'\"\n    #echo \"  --webdriver=[<IP>:]<PORT>          Starts in 'Remote WebDriver mode' in the\"\n    #echo \"                                     specified network interface\"\n    #echo \"  --webdriver-logfile=<file>         File where to write the WebDriver's Log \"\n    #echo \"                                     (default 'none') (NOTE: needs '--webdriver')\"\n    #echo \"  --webdriver-loglevel=[ERROR|WARN|INFO|DEBUG] WebDriver Logging Level \"\n    #echo \"                                 (default is 'INFO') (NOTE: needs '--webdriver')\"\n    #echo \"  --webdriver-selenium-grid-hub=<url> URL to the Selenium Grid HUB (default is\"\n    #echo \"                                      'none') (NOTE: needs '--webdriver') \"\n    echo \"  --error-log-file=<file>            Log all javascript errors in a file\"\n    echo \"  -jsconsole                         Open a window to view all javascript errors\"\n    echo \"                                       during the execution\"\n    echo \"\"\n    echo \"*** About profiles: see details of these Mozilla options at\"\n    echo \"https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile\"\n    echo \"\"\n    echo \"  --createprofile name               Create a new profile and exit\"\n    echo \"  -P name                            Use the specified profile to execute the script\"\n    echo \"  -profile path                      Use the profile stored in the specified\"\n    echo \"                                     directory, to execute the script\"\n    echo \"By default, SlimerJS use a temporary profile\"\n    echo \"\"\n}\n\n# retrieve list of existing environment variable, because Mozilla doesn't provide an API to get this\n# list\nLISTVAR=\"\"\nENVVAR=`env`;\nfor v in $ENVVAR; do\n    IFS='=' read -a var <<< \"$v\"\n    LISTVAR=\"$LISTVAR,${var[0]}\"\ndone\n\n# check arguments.\nCREATE_TEMP='Y'\nHIDE_ERRORS='Y'\nshopt -s nocasematch\nfor i in $*; do\n    case \"$i\" in\n        --help|-h)\n           showHelp\n           exit 0\n           ;;\n        -reset-profile|-profile|-p|-createprofile|-profilemanager)\n            CREATE_TEMP=''\n            ;;\n        --reset-profile|--profile|--p|--createprofile|--profilemanager)\n            CREATE_TEMP=''\n            ;;\n        \"--debug=true\")\n            HIDE_ERRORS='N'\n    esac\n    if [[ $i == --debug* ]] && [[ \"$i\" == *errors* ]]; then\n        HIDE_ERRORS='N'\n    fi\ndone\nshopt -u nocasematch\n\n# If profile parameters, don't create a temporary profile\nPROFILE=\"\"\nPROFILE_DIR=\"\"\nif [ \"$CREATE_TEMP\" == \"Y\" ]\nthen\n    PROFILE_DIR=`mktemp -d -q /tmp/slimerjs.XXXXXXXX`\n    if [ \"$PROFILE_DIR\" == \"\" ]; then\n        echo \"Error: cannot generate temp profile\"\n        exit 1\n    fi\n    if [ \"$IN_CYGWIN\" == 1 ]; then\n        PROFILE_DIR=`cygpath -w $PROFILE_DIR`\n    fi\n    PROFILE=\"--profile $PROFILE_DIR\"\nelse\n    PROFILE=\"-purgecaches\"\nfi\n\n# put all arguments in a variable, to have original arguments before their transformation\n# by Mozilla\nexport __SLIMER_ARGS=\"$@\"\nexport __SLIMER_ENV=\"$LISTVAR\"\n\n# launch slimerjs with firefox/xulrunner\nif [ \"$HIDE_ERRORS\" == \"Y\" ]; then\n    \"$SLIMERJSLAUNCHER\" -app $SLIMERDIR/application.ini $PROFILE -no-remote \"$@\" 2> /dev/null\nelse\n    \"$SLIMERJSLAUNCHER\" -app $SLIMERDIR/application.ini $PROFILE -no-remote \"$@\"\nfi\n\nEXITCODE=$?\n\nif [ \"$PROFILE_DIR\" != \"\" ]; then\n    rm -rf $PROFILE_DIR\nfi\n\nexit $EXITCODE"
  },
  {
    "path": "tests/slimerjs-0.9.0/slimerjs.bat",
    "content": "@echo off\r\n\r\nSET SLIMERJSLAUNCHER=\"%SLIMERJSLAUNCHER%\"\r\nREM % ~ d[rive] p[ath] 0[script name] is the absolute path to this bat file, without quotes, always.\r\nREM ~ strips quotes from the argument\r\nSET SLIMERDIR=%~dp0\r\nREM %* is every argument passed to this script.\r\nSET __SLIMER_ARGS=%*\r\nSET __SLIMER_ENV=\r\n\r\nSET CREATETEMP=Y\r\nSET HIDE_ERRORS=Y\r\n\r\nREM check arguments\r\nFOR %%A IN (%*) DO (\r\n\r\n    if [\"%%A\"]==[\"/?\"] (\r\n        call :helpMessage\r\n        goto :eof\r\n    )\r\n    if /I [\"%%A\"]==[\"--help\"] (\r\n        call :helpMessage\r\n        goto :eof\r\n    )\r\n    if /I [\"%%A\"]==[\"-h\"] (\r\n        call :helpMessage\r\n        goto :eof\r\n    )\r\n    if /I [\"%%A\"]==[\"/h\"] (\r\n        call :helpMessage\r\n        goto :eof\r\n    )\r\n    if /I [\"%%A\"]==[\"-reset-profile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"-profile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"-p\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"-createprofile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"-profilemanager\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"--reset-profile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"--profile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"--p\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"--createprofile\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"--profilemanager\"] (\r\n        SET CREATETEMP=\r\n    )\r\n    if /I [\"%%A\"]==[\"/debug\"] (\r\n        SET HIDE_ERRORS=\r\n    )\r\n)\r\n\r\nif not exist %SLIMERJSLAUNCHER% (\r\n    if exist \"%SLIMERDIR%\\xulrunner\\xulrunner.exe\" (\r\n        SET SLIMERJSLAUNCHER=\"%SLIMERDIR%\\xulrunner\\xulrunner.exe\"\r\n    )\r\n)\r\nif not exist %SLIMERJSLAUNCHER% (\r\n    call :findFirefox\r\n)\r\nif not exist %SLIMERJSLAUNCHER% (\r\n    echo SLIMERJSLAUNCHER environment variable is missing or the path is invalid.\r\n    echo Set it with the path to Firefox or xulrunner.\r\n    echo The current value of SLIMERJSLAUNCHER is: %SLIMERJSLAUNCHER%\r\n    REM %% escapes the percent sign so it displays literally\r\n    echo SET SLIMERJSLAUNCHER=\"%%programfiles%%\\Mozilla Firefox\\firefox.exe\"\r\n    echo SET SLIMERJSLAUNCHER=\"%%programfiles%%\\XULRunner\\xulrunner.exe\"\r\n    pause\r\n    exit 1\r\n)\r\n\r\n\r\nSETLOCAL EnableDelayedExpansion\r\n\r\nREM store environment variable into __SLIMER_ENV for SlimerJS\r\nFOR /F \"usebackq delims==\" %%i IN (`set`) DO set __SLIMER_ENV=!__SLIMER_ENV!,%%i\r\n\r\nREM let's create a temporary dir for the profile, if needed\r\nif [\"%CREATETEMP%\"]==[\"\"] (\r\n   SET PROFILEDIR=\r\n   SET PROFILE=-purgecaches\r\n   goto callexec\r\n)\r\n:createdirname\r\nSET PROFILEDIR=%Temp%\\slimerjs-!Random!!Random!!Random!\r\nIF EXIST \"%PROFILEDIR%\" (\r\n    GOTO createdirname\r\n)\r\nmkdir %PROFILEDIR%\r\n\r\nSET PROFILE=-profile %PROFILEDIR%\r\n\r\n:callexec\r\nif [\"%HIDE_ERRORS%\"]==[\"\"] (\r\n    %SLIMERJSLAUNCHER% -app \"%SLIMERDIR%application.ini\" %PROFILE% -attach-console -no-remote %__SLIMER_ARGS%\r\n) ELSE (\r\n    %SLIMERJSLAUNCHER% -app \"%SLIMERDIR%application.ini\" %PROFILE% -attach-console -no-remote %__SLIMER_ARGS% 2>NUL\r\n)\r\n\r\nif [\"%CREATETEMP%\"]==[\"Y\"] (\r\n     rmdir /S /Q %PROFILEDIR%\r\n)\r\nENDLOCAL\r\n\r\ngoto :eof\r\n\r\n\r\n:helpMessage\r\nREM in echo statements the escape character is ^\r\nREM escape < > | and &\r\nREM the character % is escaped by doubling it to %%\r\nREM if delayed variable expansion is turned on then the character ! needs to be escaped as ^^!\r\n\techo   Available options are:\r\n\techo.\r\n    echo   --config=^<filename^>                Load the given configuration file\r\n    echo                                      (JSON formated)\r\n    echo   --debug=[yes^|no]                   Prints additional warning and debug message\r\n    echo                                      (default is no)\r\n    echo   --disk-cache=[yes^|no]              Enables disk cache (default is no).\r\n    echo   --help or -h                       Show this help\r\nREM    echo   --ignore-ssl-errors=[yes^|no]       Ignores SSL errors (default is no).\r\n    echo   --load-images=[yes^|no]            Loads all inlined images (default is yes)\r\n    echo   --local-storage-quota=^<number^>   Sets the maximum size of the offline\r\n    echo                                      local storage (in KB)\r\nREM    echo   --local-to-remote-url-access=[yes^|no] Allows local content to access remote\r\nREM    echo                                         URL (default is no)\r\n    echo   --max-disk-cache-size=^<number^>     Limits the size of the disk cache (in KB)\r\nREM    echo   --output-encoding=^<enc^>            Sets the encoding for the terminal output\r\nREM    echo                                      (default is 'utf8')\r\nREM    echo   --remote-debugger-port=^<number^>    Starts the script in a debug harness and\r\nREM    echo                                      listens on the specified port\r\nREM    echo   --remote-debugger-autorun=[yes^|no] Runs the script in the debugger immediately\r\nREM    echo                                      (default is no)\r\n    echo   --proxy=^<proxy url^>                Sets the proxy server\r\n    echo   --proxy-auth=^<username:password^>   Provides authentication information for the\r\n    echo                                      proxy\r\n    echo   --proxy-type=[http^|socks5^|none^|auto^|system^|config-url]    Specifies the proxy type (default is http)\r\nREM    echo   --script-encoding=^<enc^>            Sets the encoding used for the starting\r\nREM    echo                                      script (default is utf8)\r\nREM    echo   --web-security=[yes^|no]            Enables web security (default is yes)\r\n    echo   --version or v                     Prints out SlimerJS version\r\nREM    echo   --webdriver or --wd or -w          Starts in 'Remote WebDriver mode' (embedded\r\nREM    echo                                      GhostDriver) '127.0.0.1:8910'\r\nREM    echo   --webdriver=[^<IP^>:]^<PORT^>          Starts in 'Remote WebDriver mode' in the\r\nREM    echo                                      specified network interface\r\nREM    echo   --webdriver-logfile=^<file^>         File where to write the WebDriver's Log\r\nREM    echo                                      (default 'none') (NOTE: needs '--webdriver')\r\nREM    echo   --webdriver-loglevel=[ERROR^|WARN^|INFO^|DEBUG^|] WebDriver Logging Level\r\nREM    echo                                  (default is 'INFO') (NOTE: needs '--webdriver')\r\nREM    echo   --webdriver-selenium-grid-hub=^<url^> URL to the Selenium Grid HUB (default is\r\nREM    echo                                       'none') (NOTE: needs '--webdriver')\r\n    echo   --error-log-file=<file>            Log all javascript errors in a file\r\n    echo   -jsconsole                         Open a window to view all javascript errors\r\n    echo                                        during the execution\r\n    echo.\r\n    echo *** About profiles: see details of these Mozilla options at\r\n    echo https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile\r\n    echo.\r\n    echo   --createprofile name               Create a new profile and exit\r\n    echo   -P name                            Use the specified profile to execute the script\r\n    echo   -profile path                      Use the profile stored in the specified\r\n    echo                                      directory, to execute the script\r\n    echo By default, SlimerJS use a temporary profile\r\n    echo.\r\ngoto :eof\r\n\r\n\r\n:findFirefox\r\nif exist \"%programfiles%\\Mozilla Firefox\\firefox.exe\" (\r\n    SET SLIMERJSLAUNCHER=\"%programfiles%\\Mozilla Firefox\\firefox.exe\"\r\n)\r\nif exist \"%programfiles% (x86)\\Mozilla Firefox\\firefox.exe\" (\r\n    SET SLIMERJSLAUNCHER=\"%programfiles% (x86)\\Mozilla Firefox\\firefox.exe\"\r\n)\r\necho SLIMERJSLAUNCHER is set to %SLIMERJSLAUNCHER%\r\ngoto :eof\r\n"
  },
  {
    "path": "tests/slimerjs-0.9.0/slimerjs.py",
    "content": "#!/usr/bin/env python\n\nimport os\nimport sys\nimport tempfile\nimport shutil\nimport string\nimport subprocess\n\ndef resolve(path):\n    if os.path.islink(path):\n        path = os.path.join(os.path.dirname(path), os.readlink(path))\n        return resolve(path)\n    return path\n\ndef is_exe(fpath):\n    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)\n\ndef which(program):\n    fpath, fname = os.path.split(program)\n    if fpath:\n        if is_exe(program):\n            return program\n    else:\n        for path in os.environ[\"PATH\"].split(os.pathsep):\n            path = path.strip('\"')\n            exe_file = os.path.join(path, program)\n            if is_exe(exe_file):\n                return exe_file\n    return None\n\n# retrieve full path of the current script\n# symlinks are resolved, so application.ini could be found\nSLIMERJS_PATH = os.path.abspath(os.path.dirname(resolve(__file__)))\nSYS_ARGS = sys.argv[1:]\n\nSLIMERJSLAUNCHER = os.environ.get(\"SLIMERJSLAUNCHER\", \"\");\n\nif SLIMERJSLAUNCHER == \"\":\n    POSSIBLE_PATH = []\n\n    if sys.platform == \"linux\" or sys.platform == \"linux2\" or sys.platform == \"darwin\":\n        POSSIBLE_PATH.append(os.path.join(SLIMERJS_PATH, \"xulrunner\", \"xulrunner\"))\n        path = which('firefox')\n        if path != None:\n            POSSIBLE_PATH.append(path)\n        path = which('xulrunner')\n        if path != None:\n            POSSIBLE_PATH.append(path)\n    elif sys.platform == \"win32\":\n        POSSIBLE_PATH.append(os.path.join(SLIMERJS_PATH, \"xulrunner\", \"xulrunner.exe\"))\n        path = which('firefox.exe')\n        if path != None:\n            POSSIBLE_PATH.append(path)\n        path = which('xulrunner.exe')\n        if path != None:\n            POSSIBLE_PATH.append(path)\n        POSSIBLE_PATH.append(os.path.join(os.environ.get('programfiles'), \"Mozilla Firefox\", \"firefox.exe\"))\n        POSSIBLE_PATH.append(\"%s (x86)\" % os.path.join(os.environ.get('programfiles'), \"Mozilla Firefox\", \"firefox.exe\"))\n\n    for path in POSSIBLE_PATH:\n        if is_exe(path):\n            SLIMERJSLAUNCHER = path\n            break\n    if SLIMERJSLAUNCHER == \"\":\n        print('SLIMERJSLAUNCHER environment variable is missing and I don\\'t find XulRunner or Firefox')\n        print('Set SLIMERJSLAUNCHER with the path to Firefox or XulRunner')\n        sys.exit(1)\nelse:\n    if not os.path.exists(SLIMERJSLAUNCHER):\n        print(\"SLIMERJSLAUNCHER environment variable does not contain an executable path: %s. Set it with the path to Firefox\" % SLIMERJSLAUNCHER)\n        sys.exit(1)\n\ndef showHelp():\n    print(\"  --config=<file>                    Load the given configuration file\")\n    print(\"                                     (JSON formated)\")\n    print(\"  --debug=[yes|no]                   Prints additional warning and debug message\")\n    print(\"                                     (default is no)\")\n    print(\"  --disk-cache=[yes|no]              Enables disk cache (default is no).\")\n    print(\"  --help or -h                       Show this help\")\n    #print(\"  --ignore-ssl-errors=[yes|no]       Ignores SSL errors (default is no).\")\n    print(\"  --load-images=[yes|no]             Loads all inlined images (default is yes)\")\n    print(\"  --local-storage-quota=<number>     Sets the maximum size of the offline\")\n    print(\"                                     local storage (in KB)\")\n    #print(\"  --local-to-remote-url-access=[yes|no] Allows local content to access remote\")\n    #print(\"                                        URL (default is no)\")\n    print(\"  --max-disk-cache-size=<number>     Limits the size of the disk cache (in KB)\")\n    #print(\"  --output-encoding=<enc>            Sets the encoding for the terminal output\")\n    #print(\"                                     (default is 'utf8')\")\n    #print(\"  --remote-debugger-port=<number>    Starts the script in a debug harness and\")\n    #print(\"                                     listens on the specified port\")\n    #print(\"  --remote-debugger-autorun=[yes|no] Runs the script in the debugger immediately\")\n    #print(\"                                     (default is no)\")\n    print(\"  --proxy=<proxy url>                Sets the proxy server\")\n    print(\"  --proxy-auth=<username:password>   Provides authentication information for the\")\n    print(\"                                     proxy\")\n    print(\"  --proxy-type=[http|socks5|none|auto|system|config-url]    Specifies the proxy type (default is http)\")\n    #print(\"  --script-encoding=<enc>            Sets the encoding used for the starting\")\n    #print(\"                                     script (default is utf8)\")\n    #print(\"  --web-security=[yes|no]            Enables web security (default is yes)\")\n    print(\"  --version or v                     Prints out SlimerJS version\")\n    #print(\"  --webdriver or --wd or -w          Starts in 'Remote WebDriver mode' (embedded\")\n    #print(\"                                     GhostDriver) '127.0.0.1:8910'\")\n    #print(\"  --webdriver=[<IP>:]<PORT>          Starts in 'Remote WebDriver mode' in the\")\n    #print(\"                                     specified network interface\")\n    #print(\"  --webdriver-logfile=<file>         File where to write the WebDriver's Log \")\n    #print(\"                                     (default 'none') (NOTE: needs '--webdriver')\")\n    #print(\"  --webdriver-loglevel=[ERROR|WARN|INFO|DEBUG] WebDriver Logging Level \")\n    #print(\"                                 (default is 'INFO') (NOTE: needs '--webdriver')\")\n    #print(\"  --webdriver-selenium-grid-hub=<url> URL to the Selenium Grid HUB (default is\")\n    #print(\"                                      'none') (NOTE: needs '--webdriver') \")\n    print(\"  --error-log-file=<file>            Log all javascript errors in a file\")\n    print(\"  -jsconsole                         Open a window to view all javascript errors\")\n    print(\"                                       during the execution\")\n    print(\"\")\n    print(\"*** About profiles: see details of these Mozilla options at\")\n    print(\"https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile\")\n    print(\"\")\n    print(\"  --createprofile name               Create a new profile and exit\")\n    print(\"  -P name                            Use the specified profile to execute the script\")\n    print(\"  -profile path                      Use the profile stored in the specified\")\n    print(\"                                     directory, to execute the script\")\n    print(\"By default, SlimerJS use a temporary profile\")\n    print(\"\")\n\n\n# retrieve list of existing environment variable,\n#because Mozilla doesn't provide an API to get this\n# list\nLISTVAR=\"\"\nfor env in os.environ.data:\n    LISTVAR = \"%s,%s\" % (LISTVAR, env)\n\n# check arguments.\nHIDE_ERRORS=True\nCREATE_TEMP=True\nNO_TEMP_PROFILE_OPTIONS = [\n    \"-reset-profile\",\"-profile\",\"-p\",\"-createprofile\",\"-profilemanager\",\n    \"--reset-profile\",\"--profile\",\"--p\",\"--createprofile\",\"--profilemanager\",\n]\nfor arg in SYS_ARGS:\n    if arg == '--help' or arg == \"-h\":\n        showHelp()\n        sys.exit(0)\n\n    # If profile parameters, don't create a temporary profile\n    if arg.lower() in NO_TEMP_PROFILE_OPTIONS:\n        CREATE_TEMP=False\n\n    if arg == '--debug=true' or (arg.startswith('--debug=') and arg.find(\"errors\") != -1):\n        HIDE_ERRORS=False\n\nPROFILE=[]\nPROFILE_DIR=\"\"\nif CREATE_TEMP:\n    PROFILE_DIR = tempfile.mkdtemp('', 'slimerjs.')\n    PROFILE=['--profile', PROFILE_DIR]\nelse:\n    PROFILE=[\"-purgecaches\"]\n\n\n# put all arguments in a variable, to have original arguments before their transformation\n# by Mozilla\nos.environ.data['__SLIMER_ENV'] = LISTVAR\nos.environ.data['__SLIMER_ARGS'] = string.join(SYS_ARGS,' ')\n\n# launch slimerjs with firefox/xulrunner\nSLCMD = [ SLIMERJSLAUNCHER ]\nSLCMD.extend([\"-app\", os.path.join(SLIMERJS_PATH, \"application.ini\"), \"-no-remote\"])\nif sys.platform == \"win32\":\n    SLCMD.extend([\"-attach-console\"])\nSLCMD.extend(PROFILE)\nSLCMD.extend(SYS_ARGS)\n\nexitCode = 0\ntry:\n    if HIDE_ERRORS:\n        try:\n            from subprocess import DEVNULL # py3k\n        except ImportError:\n            DEVNULL = open(os.devnull, 'wb')\n\n        exitCode = subprocess.call(SLCMD, stderr=DEVNULL)\n    else:\n        exitCode = subprocess.call(SLCMD)\n\nexcept OSError as err:\n    print('Fatal: %s. Are you sure %s exists?' % (err, SLIMERJSLAUNCHER))\n    sys.exit(1)\n\nif CREATE_TEMP:\n    shutil.rmtree(PROFILE_DIR)\n    \nsys.exit(exitCode)\n"
  },
  {
    "path": "tests/source_updater.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport ast\nfrom collections import OrderedDict\nimport os\nimport re\nfrom textwrap import dedent\n\nVIEW_FINDER = re.compile(r'^def (\\w+)\\(request.*\\):$')\n\n\nclass SourceUpdateError(Exception):\n    pass\n\n\ndef get_indent(line):\n    return (len(line) - len(line.lstrip())) * \" \"\n\n\nclass Block(object):\n\n    def __init__(self, node, source):\n        self.name = node.name\n        self.node = node\n        self.full_source = source\n        self.start_line = self.node.lineno - 1\n        self.full_line = self.full_source.split('\\n')[self.start_line]\n        self.source = '\\n'.join(\n            self.full_source.split('\\n')[self.start_line:self.last_line + 1]\n        )\n\n\n    @property\n    def is_view(self):\n        return bool(VIEW_FINDER.match(self.full_line))\n\n\n    @property\n    def last_line(self):\n        last_line_no = max(\n            getattr(n, 'lineno', -1) for n in ast.walk(self.node)\n        )\n        lines = self.full_source.split('\\n')\n        if len(lines) > last_line_no:\n            for line in lines[last_line_no:]:\n                if line.strip() == '':\n                    break\n                last_line_no += 1\n        return last_line_no - 1\n\n\n\nclass Source(object):\n\n    def __init__(self):\n        self.contents = ''\n\n    @classmethod\n    def from_path(kls, path):\n        source = Source()\n        if os.path.exists(path):\n            with open(path) as f:\n                source.contents = f.read()\n        source.path = path\n        return source\n\n\n    @classmethod\n    def _from_contents(kls, contents):\n        source = Source()\n        source.contents = contents\n        return source\n\n\n    @property\n    def lines(self):\n        return self.contents.split('\\n')\n\n\n    @property\n    def functions(self):\n        if not hasattr(self, '_functions'):\n            self._functions = OrderedDict()\n            for node in self.ast:\n                if isinstance(node, ast.FunctionDef):\n                    block = Block(node, self.contents)\n                    self._functions[block.name] = block\n        return self._functions\n\n\n    @property\n    def views(self):\n        return OrderedDict((f.name, f) for f in self.functions.values() if f.is_view)\n\n\n    @property\n    def ast(self):\n        try:\n            return list(ast.walk(ast.parse(self.contents)))\n        except SyntaxError:\n            return []\n\n\n    @property\n    def classes(self):\n        if not hasattr(self, '_classes'):\n            self._classes = OrderedDict()\n            for node in self.ast:\n                if isinstance(node, ast.ClassDef):\n                    block = Block(node, self.contents)\n                    self._classes[block.name] = block\n        return self._classes\n\n\n    @property\n    def _import_nodes(self):\n        for node in self.ast:\n            if isinstance(node, (ast.Import, ast.ImportFrom)):\n                node.full_line = self.lines[node.lineno - 1]\n                yield node\n\n    @property\n    def _deduped_import_nodes(self):\n        from_imports = {}\n        other_imports = []\n        for node in self._import_nodes:\n            if isinstance(node, ast.Import):\n                other_imports.append(node)\n            else:\n                if node.module in from_imports:\n                    if len(node.names) > len(from_imports[node.module].names):\n                        from_imports[node.module] = node\n                else:\n                    from_imports[node.module] = node\n        return other_imports + list(from_imports.values())\n\n\n    @property\n    def imports(self):\n        for node in self._deduped_import_nodes:\n            yield node.full_line\n\n    @property\n    def django_imports(self):\n        return [i for i in self.imports if i.startswith('from django')]\n\n    @property\n    def project_imports(self):\n        return [i for i in self.imports if i.startswith('from lists')]\n\n    @property\n    def general_imports(self):\n        return [i for i in self.imports if i not in self.django_imports and i not in self.project_imports]\n\n    @property\n    def fixed_imports(self):\n        import_sections = []\n        if self.general_imports:\n            import_sections.append('\\n'.join(sorted(self.general_imports)))\n        if self.django_imports:\n            import_sections.append('\\n'.join(sorted(self.django_imports)))\n        if self.project_imports:\n            import_sections.append('\\n'.join(sorted(self.project_imports)))\n\n        fixed_imports = '\\n\\n'.join(import_sections)\n        if fixed_imports and not fixed_imports.endswith('\\n'):\n            fixed_imports += '\\n'\n        return fixed_imports\n\n\n    def find_first_nonimport_line(self):\n        try:\n            first_nonimport = next(l for l in self.lines if l and l not in self.imports)\n        except StopIteration:\n            return len(self.lines)\n        pos = self.lines.index(first_nonimport)\n        if self._import_nodes:\n            if pos < max(n.lineno for n in self._import_nodes):\n                raise SourceUpdateError('first nonimport (%s) was before end of imports (%s)' % (\n                    first_nonimport, max(n.lineno for n in self._import_nodes))\n                )\n        return pos\n\n\n    def replace_function(self, new_lines):\n        function_name = re.search(r'def (\\w+)\\(.*\\):', new_lines[0].strip()).group(1)\n        print('replacing function', function_name)\n        old_function = self.functions[function_name]\n        indent = get_indent(old_function.full_line)\n        self.contents = '\\n'.join(\n            self.lines[:old_function.start_line] +\n            [indent + l for l in new_lines] +\n            self.lines[old_function.last_line + 1:]\n        )\n        return self.contents\n\n\n    def remove_function(self, function_name):\n        print('removing function %s' % (function_name,))\n        function = self.functions[function_name]\n        self.contents = '\\n'.join(\n            self.lines[:function.start_line] +\n            self.lines[function.last_line + 1:]\n        )\n        self.contents = re.sub(r'\\n\\n\\n\\n+', r'\\n\\n\\n', self.contents)\n        return self.contents\n\n\n    def find_start_line(self, new_lines):\n        if not new_lines:\n            raise SourceUpdateError()\n        start_line = new_lines[0].strip()\n        if start_line == '':\n            raise SourceUpdateError()\n\n        try:\n            return [l.strip() for l in self.lines].index(start_line.strip())\n        except ValueError:\n            print('no start line match for', start_line)\n\n\n    def add_to_class(self, classname, new_lines):\n        new_lines = dedent('\\n'.join(new_lines)).strip().split('\\n')\n        klass = self.classes[classname]\n        lines_before_class = '\\n'.join(self.lines[:klass.start_line])\n        print('lines before\\n', lines_before_class)\n        lines_after_class = '\\n'.join(self.lines[klass.last_line + 1:])\n        print('lines after\\n', lines_after_class)\n        new_class = klass.source + '\\n\\n\\n' + '\\n'.join(\n            '    ' + l for l in new_lines\n        )\n        print('new class\\n', new_class)\n        self.contents = lines_before_class + '\\n' + new_class + '\\n' + lines_after_class\n\n\n    def find_end_line(self, new_lines):\n        if not new_lines:\n            raise SourceUpdateError()\n        end_line = new_lines[-1].strip()\n        if end_line == '':\n            raise SourceUpdateError()\n        start_line = self.find_start_line(new_lines)\n\n        try:\n            from_start = [l.strip() for l in self.lines[start_line:]].index(end_line.strip())\n            return start_line + from_start\n        except ValueError:\n            print('no end line match for', end_line)\n\n\n    def add_imports(self, imports):\n        post_import_lines = self.lines[self.find_first_nonimport_line():]\n        self.contents = '\\n'.join(imports + self.lines)\n        self.contents = (\n            self.fixed_imports + '\\n' +\n            '\\n'.join(post_import_lines)\n        )\n\n\n    def update(self, new_contents):\n        self.contents = new_contents\n\n\n    def get_updated_contents(self):\n        if not self.contents.endswith('\\n'):\n            self.contents += '\\n'\n        return self.contents\n\n\n    def write(self):\n        with open(self.path, 'w') as f:\n            f.write(self.get_updated_contents())\n\n"
  },
  {
    "path": "tests/sourcetree.py",
    "content": "import io\nimport os\nimport re\nimport shutil\nimport signal\nimport subprocess\nimport tempfile\nimport time\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\n\ndef strip_comments(line):\n    match_python = re.match(r\"^(.+\\S) +#$\", line)\n    if match_python:\n        print(\"match python\")\n        return match_python.group(1)\n    match_js = re.match(r\"^(.+\\S) +//$\", line)\n    if match_js:\n        return match_js.group(1)\n    return line\n\n\nBOOTSTRAP_WGET = \"wget -O bootstrap.zip https://github.com/twbs/bootstrap/releases/download/v3.3.4/bootstrap-3.3.4-dist.zip\"\n\n\n@dataclass\nclass Commit:\n    info: str\n\n    @staticmethod\n    def from_diff(commit_info):\n        return Commit(info=commit_info)\n\n    @property\n    def all_lines(self):\n        return self.info.split(\"\\n\")\n\n    @property\n    def lines_to_add(self):\n        return [\n            l[1:]\n            for l in self.all_lines\n            if l.startswith(\"+\") and l[1:].strip() and l[1] != \"+\"\n        ]\n\n    @property\n    def lines_to_remove(self):\n        return [\n            l[1:]\n            for l in self.all_lines\n            if l.startswith(\"-\") and l[1:].strip() and l[1] != \"-\"\n        ]\n\n    @property\n    def moved_lines(self):\n        return [l for l in self.lines_to_add if l in self.lines_to_remove]\n\n    @property\n    def deleted_lines(self):\n        return [l for l in self.lines_to_remove if l not in self.lines_to_add]\n\n    @property\n    def new_lines(self):\n        return [l for l in self.lines_to_add if l not in self.lines_to_remove]\n\n    @property\n    def stripped_lines_to_add(self):\n        return [l.strip() for l in self.lines_to_add]\n\n\nclass ApplyCommitException(Exception):\n    pass\n\n\nclass SourceTree:\n    def __init__(self):\n        self.tempdir = Path(tempfile.mkdtemp())\n        self.processes = []\n        self.dev_server_running = False\n\n    def get_contents(self, path):\n        with open(os.path.join(self.tempdir, path)) as f:\n            return f.read()\n\n    def cleanup(self):\n        for process in self.processes:\n            try:\n                os.killpg(process.pid, signal.SIGTERM)\n            except OSError:\n                pass\n        if os.environ.get(\"TMPDIR_CLEANUP\") not in (\"0\", \"false\"):\n            shutil.rmtree(self.tempdir)\n\n    def run_command(\n        self, command, cwd=None, user_input=None, ignore_errors=False, silent=False\n    ):\n        if cwd is None:\n            cwd = self.tempdir\n\n        env = os.environ.copy()\n        if \"manage.py test\" in command:\n            # prevent stdout and stderr from appearing to come out in wrong order\n            env[\"PYTHONUNBUFFERED\"] = \"1\"\n\n        process = subprocess.Popen(\n            command,\n            shell=True,\n            cwd=cwd,\n            executable=\"/bin/bash\",\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            stdin=subprocess.PIPE,\n            # preexec_fn=os.setsid,  # disabled to get passwordless sudo to work\n            universal_newlines=True,\n            env=env,\n        )\n        process._command = command\n        self.processes.append(process)\n        if \"runserver\" in command:\n            # can't read output, stdout.read just hangs.\n            # TODO: readline?  see below.\n            # TODO: could also try UNBUFFERED?\n            return\n        if \"docker run\" in command and \"superlists\" in command and not ignore_errors:\n            output = \"\"\n            while True:\n                line = process.stdout.readline()\n                print(f\"\\t{line}\", end=\"\")\n                output += line\n                if \"Quit the server with CONTROL-C.\" in output:\n                    # go any further and we hang.\n                    print(\"brief sleep to allow docker server to become available\")\n                    time.sleep(2)\n                    return output\n                if \"Booting worker with pid\" in output:\n                    # gunicorn startup, also hangs:\n                    print(\"brief sleep to allow docker server to become available\")\n                    time.sleep(2)\n                    return output\n                if \"ERROR: failed to solve\" in output:\n                    # docker build error, bail out\n                    break\n\n        if user_input and not user_input.endswith(\"\\n\"):\n            user_input += \"\\n\"\n        if user_input:\n            print(f\"sending user input: {user_input}\")\n        output, _ = process.communicate(user_input)\n        if process.returncode and not ignore_errors:\n            if (\n                \" test\" in command\n                or \"functional_tests\" in command\n                or \"diff\" in command\n                or \"migrate\" in command\n            ):\n                return output\n            print(\n                \"process %s return a non-zero code (%s)\" % (command, process.returncode)\n            )\n            print(\"output:\\n\", output)\n            raise Exception(\n                \"process %s return a non-zero code (%s)\" % (command, process.returncode)\n            )\n        if not silent:\n            try:\n                print(output)\n            except io.BlockingIOError as e:\n                print(e)\n                pass\n        return output\n\n    def get_local_repo_path(self, chapter_name):\n        return os.path.abspath(\n            os.path.join(\n                os.path.dirname(__file__),\n                \"../source/{}/superlists\".format(chapter_name),\n            )\n        )\n\n    def start_with_checkout(self, chapter, previous_chapter):\n        print(\"starting with checkout\")\n        self.run_command(\"git init .\")\n        self.run_command(f'git remote add repo \"{self.get_local_repo_path(chapter)}\"')\n        self.run_command(\"git fetch repo\")\n        # NB - relies on previous_chapter existing as a branch in the chapter local_repo_path\n        self.run_command(\"git reset --hard repo/{}\".format(previous_chapter))\n        print(self.run_command(\"git status\"))\n        self.chapter = chapter\n\n    def get_commit_spec(self, commit_ref):\n        return f\"repo/{self.chapter}^{{/--{commit_ref}--}}\"\n\n    def get_files_from_commit_spec(self, commit_spec):\n        return self.run_command(\n            f\"git diff-tree --no-commit-id --name-only --find-renames -r {commit_spec}\"\n        ).split()\n\n    def show_future_version(self, commit_spec, path):\n        return self.run_command(\"git show {}:{}\".format(commit_spec, path), silent=True)\n\n    def patch_from_commit(self, commit_ref, path=None):\n        commit_spec = self.get_commit_spec(commit_ref)\n        self.run_command(\n            #'git diff {commit}^ {commit} | patch'.format(commit=commit_spec)\n            \"git show -M {commit} | patch -p1 --fuzz=3 --no-backup-if-mismatch\".format(\n                commit=commit_spec\n            )\n        )\n        # self.run_command('git reset')\n\n    def tidy_up_after_patches(self):\n        # tidy up any .origs from patches\n        self.run_command('find . -name \"*.orig\" -exec rm {} \\\\;')\n\n    def apply_listing_from_commit(self, listing):\n        commit_spec = self.get_commit_spec(listing.commit_ref)\n        print(\"Applying listing from commit.\\nListing:\\n\" + listing.contents)\n\n        files = self.get_files_from_commit_spec(commit_spec)\n        if files != [listing.filename]:\n            raise ApplyCommitException(\n                f\"wrong files in listing {listing.commit_ref}: {listing.filename!r} should have been {files}\"\n            )\n        future_contents = self.show_future_version(commit_spec, listing.filename)\n\n        self._check_listing_matches_commit(listing, commit_spec, future_contents)\n\n        self.patch_from_commit(listing.commit_ref, listing.filename)\n        listing.was_written = True\n        print(\"applied commit.\")\n\n    def _check_listing_matches_commit(self, listing, commit_spec, future_contents):\n        commit = Commit.from_diff(self.run_command(f\"git show -w {commit_spec}\"))\n        if listing.is_diff():\n            diff = Commit.from_diff(listing.contents)\n            if diff.new_lines == commit.new_lines:\n                return\n            commit_withwhitespace = Commit.from_diff(\n                self.run_command(f\"git show {commit_spec}\")\n            )\n            if diff.new_lines == commit_withwhitespace.new_lines:\n                return\n            raise ApplyCommitException(\n                f\"diff new lines did not match.\\n\"\n                f\"{diff.new_lines}\\n!=\\n{commit.new_lines}\"\n            )\n\n        listing_lines = [strip_comments(l) for l in listing.contents.split(\"\\n\")]\n        stripped_listing_lines = [l.strip() for l in listing_lines]\n        for new_line in commit.new_lines:\n            if new_line.strip() not in stripped_listing_lines:\n                # print('stripped_listing_lines', stripped_listing_lines)\n                raise ApplyCommitException(\n                    f\"could not find commit new line {new_line!r} in listing {listing.commit_ref}:\\n{listing.contents}\"\n                )\n\n        check_chunks_against_future_contents(\"\\n\".join(listing_lines), future_contents)\n\n\ndef check_chunks_against_future_contents(listing_contents, future_contents):\n    future_lines = future_contents.split(\"\\n\")\n\n    for chunk in split_into_chunks(listing_contents):\n        reindented_chunk = reindent_to_match(chunk, future_lines)\n        if reindented_chunk not in future_contents:\n            missing_lines = [\n                l for l in reindented_chunk.splitlines() if l not in future_lines\n            ]\n            if missing_lines:\n                print(\"missing lines:\\n\" + \"\\n\".join(repr(l) for l in missing_lines))\n                raise ApplyCommitException(\n                    f\"{len(missing_lines)} lines did not match future contents\"\n                )\n            else:\n                print(\"reindented listing\")\n                print(\"\\n\".join(repr(l) for l in reindented_chunk.splitlines()))\n                print(\"future contents\")\n                print(\"\\n\".join(repr(l) for l in future_contents.splitlines()))\n                tdir = Path(tempfile.mkdtemp())\n                print(\"saving to\", tdir)\n                (tdir / \"listing.txt\").write_text(reindented_chunk)\n                (tdir / \"future.txt\").write_text(future_contents)\n                raise ApplyCommitException(\n                    \"Commit lines in wrong order, or listing is missing a [...] (?)\"\n                )\n\n\ndef get_offset(lines, future_lines):\n    for line in lines:\n        if line == \"\":\n            continue\n        if line in future_lines:\n            return \"\"\n        else:\n            for future_line in future_lines:\n                if future_line.endswith(line):\n                    return future_line[: -len(line)]\n\n    raise Exception(f\"not match found to determine offset {lines[0]!r}\")\n\n\ndef reindent_to_match(code, future_lines):\n    offset = get_offset(code.splitlines(), future_lines)\n    return \"\\n\".join((offset + line) if line else \"\" for line in code.splitlines())\n\n\ndef split_into_chunks(code):\n    chunk = \"\"\n    for line in code.splitlines():\n        if line.strip() == \"[...]\":\n            if chunk:\n                yield chunk\n                chunk = \"\"\n        elif line.startswith(\"[...]\"):\n            if chunk:\n                yield chunk\n                chunk = \"\"\n        elif line.endswith(\"[...]\"):\n            linestart, _, _ = line.partition(\"[...]\")\n            chunk += linestart\n            yield chunk\n            chunk = \"\"\n        else:\n            if chunk:\n                chunk = f\"{chunk}\\n{line}\"\n            else:\n                chunk = line\n\n    if chunk:\n        yield chunk\n"
  },
  {
    "path": "tests/test_appendix_DjangoRestFramework.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass AppendixVIITest(ChapterTest):\n    chapter_name = 'appendix_DjangoRestFramework'\n    previous_chapter = 'appendix_rest_api'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, 'other command')\n        self.assertEqual(self.listings[1].type, 'code listing')\n\n        # skips\n        #self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        skip = False\n        if skip:\n            self.pos = 40\n            self.sourcetree.run_command('git switch {}'.format(\n                self.sourcetree.get_commit_spec('ch36l027')\n            ))\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        # TODO:\n        # self.sourcetree.patch_from_commit('ch37l015')\n        # self.sourcetree.patch_from_commit('ch37l017')\n        # self.sourcetree.run_command(\n        #    'git add . && git commit -m\"final commit in rest api chapter\"'\n        #)\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_appendix_Django_Class-Based_Views.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass AppendixIITest(ChapterTest):\n    chapter_name = 'appendix_Django_Class-Based_Views'\n    previous_chapter = 'chapter_16_advanced_forms'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        #self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, 'code listing currentcontents')\n        self.assertEqual(self.listings[1].type, 'code listing with git ref')\n        self.assertEqual(self.listings[2].type, 'code listing with git ref')\n\n        # skips\n        #self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        skip = False\n        if skip:\n            self.pos = 27\n            self.sourcetree.run_command('git switch {0}'.format(\n                self.sourcetree.get_commit_spec('ch20l015')\n            ))\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_appendix_bdd.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass AppendixVTest(ChapterTest):\n    chapter_name = 'appendix_bdd'\n    previous_chapter = 'chapter_23_debugging_prod'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        #self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, 'other command')\n        self.assertEqual(self.listings[4].type, 'tree')\n        self.assertEqual(self.listings[6].type, 'diff')\n        self.assertEqual(self.listings[7].type, 'bdd test')\n\n        # skips\n        #self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        skip = False\n        if skip:\n            self.pos = 27\n            self.sourcetree.run_command('git switch {0}'.format(\n                self.sourcetree.get_commit_spec('ch20l015')\n            ))\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.sourcetree.run_command('git add . && git commit -m\"final commit in bdd chapter\"')\n        self.check_final_diff()\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_appendix_purist_unit_tests.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter20Test(ChapterTest):\n    chapter_name = 'appendix_purist_unit_tests'\n    previous_chapter = 'chapter_24_outside_in'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, 'other command')\n        self.assertEqual(self.listings[1].type, 'output')\n        self.assertEqual(self.listings[4].type, 'code listing currentcontents')\n\n        # skips\n        self.skip_with_check(1, '# a branch')  # comment\n        self.skip_with_check(109, '# optional backup')  # comment\n        self.skip_with_check(112, '# reset master')  # comment\n\n        # prep\n        self.start_with_checkout()\n\n        # hack fast-forward\n        skip = False\n        if skip:\n            self.pos = 75\n            self.sourcetree.run_command('git switch {0}'.format(\n                self.sourcetree.get_commit_spec('ch19l041')\n            ))\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\"])\n\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_appendix_rest_api.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass AppendixVITest(ChapterTest):\n    chapter_name = 'appendix_rest_api'\n    previous_chapter = 'chapter_26_page_pattern'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        # self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, 'code listing')\n        self.assertEqual(self.listings[1].type, 'code listing')\n\n        # skips\n        #self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        skip = False\n        if skip:\n            self.pos = 40\n            self.sourcetree.run_command('git switch {}'.format(\n                self.sourcetree.get_commit_spec('ch36l027')\n            ))\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.sourcetree.run_command(\n            'git add . && git commit -m\"final commit in rest api chapter\"'\n        )\n        self.check_final_diff()\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_book_parser.py",
    "content": "#!/usr/bin/env python\nfrom lxml import html\nimport re\nfrom textwrap import dedent\nimport unittest\n\nfrom book_parser import (\n    COMMIT_REF_FINDER,\n    CodeListing,\n    Command,\n    Output,\n    get_commands,\n    parse_listing,\n    _strip_callouts,\n)\nimport examples\n\n\nclass CodeListingTest(unittest.TestCase):\n    def test_stringify(self):\n        c = CodeListing(filename=\"a.py\", contents=\"abc\\ndef\")\n        assert \"abc\" in str(c)\n        assert \"a.py\" in str(c)\n        assert c.is_server_listing is False\n\n    def test_server_codelisting(self):\n        c = CodeListing(filename=\"server: a_filename.py\", contents=\"foo\")\n        assert c.contents == \"foo\"\n        assert c.filename == \"a_filename.py\"\n        assert c.is_server_listing is True\n\n\nclass CommitRefFinderTest(unittest.TestCase):\n    def test_base_finder(self):\n        assert re.search(COMMIT_REF_FINDER, \"bla bla ch09l027-2\")\n        assert re.findall(COMMIT_REF_FINDER, \"bla bla ch09l027-2\") == [\"ch09l027-2\"]\n        assert not re.search(COMMIT_REF_FINDER, \"bla bla 09l6666\")\n\n    def test_finder_on_codelisting(self):\n        matches = re.match(\n            CodeListing.COMMIT_REF_FINDER, \"some_filename.txt (ch09l027-2)\"\n        )\n        assert matches.group(1) == \"some_filename.txt\"\n        assert matches.group(2) == \"ch09l027-2\"\n\n\nclass ParseCodeListingTest(unittest.TestCase):\n    def test_recognises_code_listings(self):\n        code_html = examples.CODE_LISTING_WITH_CAPTION.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(type(listing), CodeListing)\n        self.assertEqual(listing.filename, \"functional_tests.py\")\n        self.assertEqual(\n            listing.contents,\n            dedent(\n                \"\"\"\n                from selenium import webdriver\n\n                browser = webdriver.Firefox()\n                browser.get('http://localhost:8000')\n\n                assert 'Django' in browser.title\n                \"\"\"\n            ).strip(),\n        )\n        self.assertFalse(\"\\r\" in listing.contents)\n        self.assertEqual(listing.commit_ref, None)\n\n    def test_recognises_git_commit_refs(self):\n        code_html = examples.CODE_LISTING_WITH_CAPTION_AND_GIT_COMMIT_REF.replace(\n            \"\\n\", \"\\r\\n\"\n        )\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(type(listing), CodeListing)\n        self.assertEqual(listing.filename, \"functional_tests/tests.py\")\n        self.assertEqual(listing.commit_ref, \"ch06l001\")\n        self.assertEqual(listing.type, \"code listing with git ref\")\n\n    def test_recognises_git_commit_refs_even_if_formatted_as_diffs(self):\n        code_html = examples.CODE_LISTING_WITH_DIFF_FORMATING_AND_COMMIT_REF.replace(\n            \"\\n\", \"\\r\\n\"\n        )\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(type(listing), CodeListing)\n        self.assertEqual(listing.filename, \"lists/tests/test_models.py\")\n        self.assertEqual(listing.commit_ref, \"ch09l010\")\n        self.assertEqual(listing.type, \"code listing with git ref\")\n        self.assertTrue(listing.is_diff())\n\n    def test_recognises_diffs_even_if_they_dont_have_atat(self):\n        code_html = examples.EXAMPLE_DIFF_LISTING.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        [listing] = parse_listing(node)\n        self.assertEqual(listing.type, \"code listing with git ref\")\n        self.assertTrue(listing.is_diff())\n\n    def test_recognises_skipme_tag_on_unmarked_code_listing(self):\n        code_html = examples.OUTPUT_WITH_SKIPME.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(listing.skip, True)\n\n    def test_recognises_skipme_tag_on_code_listing(self):\n        code_html = examples.CODE_LISTING_WITH_SKIPME.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(listing.skip, True)\n\n    def test_recognises_currentcontents_tag(self):\n        code_html = examples.OUTPUTS_WITH_CURRENTCONTENTS.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        assert listing.currentcontents is True\n        assert listing.type == \"code listing currentcontents\"\n\n    def test_recognises_dofirst_tag(self):\n        code_html = examples.OUTPUTS_WITH_DOFIRST.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        listing = listings[0]\n        assert listing.dofirst == \"ch09l058\"\n        self.assertEqual(len(listings), 2)\n\n    def test_recognises_jasmine_tag(self):\n        code_html = examples.JASMINE_OUTPUT.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        assert listing.type == \"jasmine output\"\n        assert (\n            listing\n            == dedent(\n                \"\"\"\n            2 specs, 0 failures, randomized with seed 12345        finished in 0.01s\n\n            Superlists tests\n              * check we know how to hide things\n              * sense check our html fixture\n            \"\"\"\n            ).strip()\n        )\n\n    def test_recognises_server_commands(self):\n        code_html = examples.SERVER_COMMAND.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        print(listings)\n        self.assertEqual(len(listings), 1)\n        listing = listings[0]\n        self.assertEqual(listing.type, \"server command\")\n        self.assertEqual(listing, \"sudo do stuff\")\n\n    def test_recognises_virtualenv_commands(self):\n        code_html = examples.COMMANDS_WITH_VIRTUALENV.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        print(listings)\n        virtualenv_command = listings[1]\n        self.assertEqual(\n            virtualenv_command,\n            \"source ./.venv/bin/activate && python manage.py test lists\",\n        )\n        self.assertEqual(len(listings), 3)\n\n    def test_recognises_command_with_ats(self):\n        code_html = examples.COMMAND_MADE_WITH_ATS.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        print(listings)\n        self.assertEqual(len(listings), 1)\n        command = listings[0]\n        self.assertEqual(command, \"grep id_new_item functional_tests/tests/test*\")\n        self.assertEqual(command.type, \"other command\")\n\n    def test_can_extract_one_command_and_its_output(self):\n        listing = html.fromstring(\n            '<div class=\"listingblock\">\\r\\n'\n            '<div class=\"content\">\\r\\n'\n            \"<pre><code>$ <strong>python functional_tests.py</strong>\\r\\n\"\n            \"Traceback (most recent call last):\\r\\n\"\n            '  File \"functional_tests.py\", line 6, in &lt;module&gt;\\r\\n'\n            \"    assert 'Django' in browser.title\\r\\n\"\n            \"AssertionError</code></pre>\\r\\n\"\n            \"</div></div>&#13;\\n\"\n        )\n        parsed_listings = parse_listing(listing)\n        self.assertEqual(\n            parsed_listings,\n            [\n                \"python functional_tests.py\",\n                \"Traceback (most recent call last):\\n\"\n                '  File \"functional_tests.py\", line 6, in <module>\\n'\n                \"    assert 'Django' in browser.title\\n\"\n                \"AssertionError\",\n            ],\n        )\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(type(parsed_listings[1]), Output)\n\n    def test_extracting_multiple(self):\n        listing = html.fromstring(\n            '<div class=\"listingblock\">\\r\\n'\n            '<div class=\"content\">\\r\\n'\n            \"<pre><code>$ <strong>ls</strong>\\r\\n\"\n            \"superlists          functional_tests.py\\r\\n\"\n            \"$ <strong>mv functional_tests.py superlists/</strong>\\r\\n\"\n            \"$ <strong>cd superlists</strong>\\r\\n\"\n            \"$ <strong>git init .</strong>\\r\\n\"\n            \"Initialized empty Git repository in /chapter_1/superlists/.git/</code></pre>\\r\\n\"\n            \"</div></div>&#13;\\n\"\n        )\n        parsed_listings = parse_listing(listing)\n        self.assertEqual(\n            parsed_listings,\n            [\n                \"ls\",\n                \"superlists          functional_tests.py\",\n                \"mv functional_tests.py superlists/\",\n                \"cd superlists\",\n                \"git init .\",\n                \"Initialized empty Git repository in /chapter_1/superlists/.git/\",\n            ],\n        )\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(type(parsed_listings[1]), Output)\n        self.assertEqual(type(parsed_listings[2]), Command)\n        self.assertEqual(type(parsed_listings[3]), Command)\n        self.assertEqual(type(parsed_listings[4]), Command)\n        self.assertEqual(type(parsed_listings[5]), Output)\n\n    def test_post_command_comment_with_multiple_spaces(self):\n        listing = html.fromstring(\n            '<div class=\"listingblock\">'\n            '<div class=\"content\">'\n            \"<pre><code>$ <strong>git diff</strong>  # should show changes to functional_tests.py\\n\"\n            '$ <strong>git commit -am \"Functional test now checks we can input a to-do item\"</strong></code></pre>'\n            \"</div></div>&#13;\"\n        )\n        commands = get_commands(listing)\n        self.assertEqual(\n            commands,\n            [\n                \"git diff\",\n                'git commit -am \"Functional test now checks we can input a to-do item\"',\n            ],\n        )\n\n        parsed_listings = parse_listing(listing)\n        self.assertEqual(\n            parsed_listings,\n            [\n                \"git diff\",\n                \"  # should show changes to functional_tests.py\",\n                'git commit -am \"Functional test now checks we can input a to-do item\"',\n            ],\n        )\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(type(parsed_listings[1]), Output)\n        self.assertEqual(type(parsed_listings[2]), Command)\n\n    def test_catches_command_with_trailing_comment(self):\n        listing = html.fromstring(\n            dedent(\"\"\"\n                <div class=\"listingblock\">\n                    <div class=\"content\">\n                        <pre><code>$ <strong>git diff --staged</strong> # will show you the diff that you're about to commit\n                </code></pre>\n                </div></div>\n                \"\"\")\n        )\n        parsed_listings = parse_listing(listing)\n        self.assertEqual(\n            parsed_listings,\n            [\n                \"git diff --staged\",\n                \" # will show you the diff that you're about to commit\",\n            ],\n        )\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(type(parsed_listings[1]), Output)\n\n    def test_handles_multiline_commands(self):\n        listing = html.fromstring(\n            dedent(\n                \"\"\"\n            <div class=\"listingblock\">\n            <div class=\"content\">\n            <pre><code>$ <strong>do something\\\\\n            that continues on this line</strong>\n            OK\n            </code></pre>\n            </div></div>\n                \"\"\"\n            )\n        )\n        commands = get_commands(listing)\n        assert len(commands) == 1\n        # assert commands[0] == 'do something\\\\\\nthat continues on this line'\n        assert commands[0] == \"do somethingthat continues on this line\"\n\n        # too hard for now\n        parsed_listings = parse_listing(listing)\n        print(parsed_listings)\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(parsed_listings[0], commands[0])\n\n    def test_handles_inline_inputs(self):\n        listing = html.fromstring(examples.OUTPUT_WITH_COMMANDS_INLINE)\n        commands = get_commands(listing)\n        self.assertEqual(\n            [str(c) for c in commands],\n            [\n                \"python manage.py makemigrations\",\n                \"1\",\n                \"''\",\n            ],\n        )\n\n        # too hard for now\n        parsed_listings = parse_listing(listing)\n        print(parsed_listings)\n        self.assertEqual(type(parsed_listings[0]), Command)\n        self.assertEqual(parsed_listings[0], commands[0])\n\n        print(parsed_listings[1])\n        self.assertIn(\"Select an option:\", parsed_listings[1])\n        self.assertTrue(parsed_listings[1].endswith(\"Select an option: \"))\n\n    def test_strips_asciidoctor_callouts_from_code(self):\n        code_html = examples.CODE_LISTING_WITH_ASCIIDOCTOR_CALLOUTS.replace(\n            \"\\n\", \"\\r\\n\"\n        )\n        node = html.fromstring(code_html)\n        listings = parse_listing(node)\n        listing = listings[0]\n        self.assertEqual(type(listing), CodeListing)\n        self.assertNotIn(\"(1)\", listing.contents)\n        self.assertNotIn(\"(2)\", listing.contents)\n        self.assertNotIn(\"(3)\", listing.contents)\n        self.assertNotIn(\"(4)\", listing.contents)\n        self.assertNotIn(\"(7)\", listing.contents)\n\n    def test_strips_asciidoctor_callouts_from_output(self):\n        listing_html = examples.OUTPUT_WITH_CALLOUTS.replace(\"\\n\", \"\\r\\n\")\n        node = html.fromstring(listing_html)\n        listings = parse_listing(node)\n        output = listings[1]\n        self.assertEqual(type(output), Output)\n        self.assertNotIn(\"(1)\", output)\n        # self.assertIn('assertEqual(\\n', output)  ## TODO: re-enable\n\n    def test_strip_callouts_helper(self):\n        self.assertEqual(_strip_callouts(\"foo  (1)\"), \"foo\")\n        self.assertEqual(_strip_callouts(\"foo (1)\"), \"foo\")\n        self.assertEqual(_strip_callouts(\"foo (112)\"), \"foo\")\n        self.assertEqual(\n            _strip_callouts(\"line1\\nline2 (2)\\nline3\"), \"line1\\nline2\\nline3\"\n        )\n        self.assertEqual(_strip_callouts(\"foo  (ya know)  (2)\"), \"foo  (ya know)\")\n        self.assertEqual(_strip_callouts(\"foo  (1)\\n  bar  (7)\"), \"foo\\n  bar\")\n        self.assertEqual(_strip_callouts(\"foo  (1)\\n  bar  (7)\\n\"), \"foo\\n  bar\\n\")\n\n        self.assertEqual(\n            _strip_callouts(\"foo  (hi)\"),\n            \"foo  (hi)\",\n        )\n        self.assertEqual(\n            _strip_callouts(\"this  (4) foo\"),\n            \"this  (4) foo\",\n        )\n        self.assertEqual(\n            _strip_callouts(\"foo(1)\"),\n            \"foo(1)\",\n        )\n        self.assertEqual(_strip_callouts(\"foo  (1) (2)\"), \"foo\")\n        self.assertEqual(_strip_callouts(\"<form>  (1)\"), \"<form>\")\n\n\nclass GetCommandsTest(unittest.TestCase):\n    def test_extracting_one_command(self):\n        listing = html.fromstring(\n            '<div class=\"listingblock\">\\r\\n<div class=\"content\">\\r\\n<pre><code>$ <strong>python functional_tests.py</strong>\\r\\nTraceback (most recent call last):\\r\\n  File \"functional_tests.py\", line 6, in &lt;module&gt;\\r\\n    assert \\'Django\\' in browser.title\\r\\nAssertionError</code></pre>\\r\\n</div></div>&#13;\\n'  # noqa\n        )\n        self.assertEqual(get_commands(listing), [\"python functional_tests.py\"])\n\n    def test_extracting_multiple(self):\n        listing = html.fromstring(\n            '<div class=\"listingblock\">\\r\\n<div class=\"content\">\\r\\n<pre><code>$ <strong>ls</strong>\\r\\nsuperlists          functional_tests.py\\r\\n$ <strong>mv functional_tests.py superlists/</strong>\\r\\n$ <strong>cd superlists</strong>\\r\\n$ <strong>git init .</strong>\\r\\nInitialized empty Git repository in /chapter_1/superlists/.git/</code></pre>\\r\\n</div></div>&#13;\\n'  # noqa\n        )\n        self.assertEqual(\n            get_commands(listing),\n            [\n                \"ls\",\n                \"mv functional_tests.py superlists/\",\n                \"cd superlists\",\n                \"git init .\",\n            ],\n        )\n"
  },
  {
    "path": "tests/test_book_tester.py",
    "content": "import os\nimport shutil\nimport subprocess\nimport sys\nimport unittest\nfrom textwrap import dedent\nfrom unittest.mock import Mock\n\nimport pytest\nfrom book_parser import (\n    CodeListing,\n    Command,\n    Output,\n)\nfrom book_tester import (\n    JASMINE_RUNNER,\n    ChapterTest,\n    contains,\n    split_blocks,\n    wrap_long_lines,\n)\n\n\nclass WrapLongLineTest(unittest.TestCase):\n    def test_wrap_long_lines_with_words(self):\n        self.assertEqual(wrap_long_lines(\"normal line\"), \"normal line\")\n        text = (\n            \"This is a short line\\n\"\n            \"This is a long line which should wrap just before the word that \"\n            \"takes it over 79 chars in length\\n\"\n            \"This line is fine though.\"\n        )\n        expected_text = (\n            \"This is a short line\\n\"\n            \"This is a long line which should wrap just before the word that \"\n            \"takes it over\\n\"\n            \"79 chars in length\\n\"\n            \"This line is fine though.\"\n        )\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_with_words_2(self):\n        text = \"ViewDoesNotExist: Could not import superlists.views.home. Parent module superlists.views does not exist.\"\n        expected_text = \"ViewDoesNotExist: Could not import superlists.views.home. Parent module\\nsuperlists.views does not exist.\"\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_with_words_3(self):\n        text = '  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\", line 442, in supports_transactions'\n        expected_text = '  File \"/usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.py\",\\nline 442, in supports_transactions'\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_doesnt_swallow_spaces(self):\n        text = \"A  really  long  line  that  uses  multiple  spaces  to  go  over  80  chars  by  a  country  mile\"\n        expected_text = \"A  really  long  line  that  uses  multiple  spaces  to  go  over  80  chars\\nby  a  country  mile\"\n        # TODO: handle trailing space corner case?\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_with_unbroken_chars(self):\n        text = \".\" * 479\n        # fmt: off\n        expected_text = (\n            \".\" * 79 + \"\\n\" +\n            \".\" * 79 + \"\\n\" +\n            \".\" * 79 + \"\\n\" +\n            \".\" * 79 + \"\\n\" +\n            \".\" * 79 + \"\\n\" +\n            \".\" * 79 + \"\\n\" +\n            \".....\"\n        )\n        # fmt: on\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_with_unbroken_chars_2(self):\n        text = (\n            \"E\\n\"\n            \"======================================================================\\n\"\n            \"ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)\"\n        )\n        expected_text = (\n            \"E\\n\"\n            \"======================================================================\\n\"\n            \"ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)\"\n        )\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n    def test_wrap_long_lines_with_indent(self):\n        text = (\n            \"This is a short line\\n\"\n            \"   This is a long line with an indent which should wrap just \"\n            \"before the word that takes it over 79 chars in length\\n\"\n            \"   This is a short indented line\\n\"\n            \"This is a long line which should wrap just before the word that \"\n            \"takes it over 79 chars in length\"\n        )\n        expected_text = (\n            \"This is a short line\\n\"\n            \"   This is a long line with an indent which should wrap just \"\n            \"before the word\\n\"\n            \"that takes it over 79 chars in length\\n\"\n            \"   This is a short indented line\\n\"\n            \"This is a long line which should wrap just before the word that \"\n            \"takes it over\\n\"\n            \"79 chars in length\"\n        )\n        self.assertMultiLineEqual(wrap_long_lines(text), expected_text)\n\n\nclass RunCommandTest(ChapterTest):\n    def test_calls_sourcetree_run_command_and_marks_as_run(self):\n        self.sourcetree.run_command = Mock()\n        cmd = Command(\"foo\")\n        output = self.run_command(cmd, cwd=\"bar\", user_input=\"thing\")\n        assert output == self.sourcetree.run_command.return_value\n        self.sourcetree.run_command.assert_called_with(\n            \"foo\",\n            cwd=\"bar\",\n            user_input=\"thing\",\n            ignore_errors=False,\n        )\n        assert cmd.was_run\n\n    def test_raises_if_not_command(self):\n        with self.assertRaises(AssertionError):\n            self.run_command(\"foo\")\n\n\nclass GetListingsTest(ChapterTest):\n    chapter_name = \"chapter_01\"\n\n    def test_get_listings_gets_exampleblock_code_listings_and_regular_listings(self):\n        self.parse_listings()\n        self.assertEqual(self.listings[0].type, \"code listing\")\n        self.assertEqual(\n            self.listings[0].contents.split()[:3], [\"from\", \"selenium\", \"import\"]\n        )\n        self.assertEqual(self.listings[1], \"python functional_tests.py\")\n        self.assertEqual(self.listings[1].type, \"test\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n\nclass AssertConsoleOutputCorrectTest(ChapterTest):\n    def test_simple_case(self):\n        actual = \"foo\"\n        expected = Output(\"foo\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_test_run_times_and_test_dashes(self):\n        actual = dedent(\n            \"\"\"\n            bla bla bla\n\n            ----------------------------------------------------------------------\n            Ran 1 test in 1.343s\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bla bla bla\n\n             ---------------------------------------------------------------------\n            Ran 1 test in 1.456s\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_handles_elipsis(self):\n        actual = dedent(\n            \"\"\"\n            bla\n            bla bla\n            loads more stuff\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bla\n            bla bla\n            [...]\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_handles_elipsis_at_end_of_line_where_theres_actually_a_linebreak(self):\n        actual = dedent(\n            \"\"\"\n            bla bla bla\n            loads more stuff\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bla bla bla [...]\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_with_start_elipsis_and_OK(self):\n        actual = dedent(\n            \"\"\"\n            bla\n\n            OK\n\n            and some epilogue\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            [...]\n            OK\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_with_elipsis_finds_assertionerrors(self):\n        actual = dedent(\n            \"\"\"\n            bla\n            bla bla\n                self.assertSomething(burgle)\n            AssertionError: nope\n\n            and then there's some stuff afterwards we don't care about\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            [...]\n                self.assertSomething(burgle)\n            AssertionError: nope\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_with_start_elipsis_and_end_longline_elipsis(self):\n        actual = dedent(\n            \"\"\"\n            bla\n            bla bla\n            loads more stuff\n                raise MyException('eek')\n            MyException: a really long exception, which will eventually wrap into multiple lines, so much so that it just gets boring after a while and we just stop caring...\n\n            and then there's some stuff afterwards we don't care about\n            \"\"\"\n        ).strip()  # noqa\n        expected = Output(\n            dedent(\n                \"\"\"\n            [...]\n            MyException: a really long exception, which will eventually wrap into multiple\n            lines, so much so that it just gets boring after a while and [...]\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_with_start_elipsis_and_end_longline_elipsis_with_assertionerror(self):\n        actual = dedent(\n            \"\"\"\n            bla\n                self.assertSomething(bla)\n            AssertionError: a really long exception, which will eventually wrap into multiple lines, so much so that it gets boring after a while...\n\n            and then there's some stuff afterwards we don't care about\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            [...]\n            AssertionError: a really long exception, which will eventually [...]\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_for_short_expected_with_trailing_elipsis(self):\n        actual = dedent(\n            \"\"\"\n            bla\n            bla bla\n                self.assertSomething(burgle)\n            AssertionError: a long assertion error which ends up wrapping so we have to have it across two lines but then it really goes on and on and on, so much so that it gets boring and we chop it off\n            \"\"\"  # noqa\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            AssertionError: a long assertion error which ends up wrapping so we have to\n            have it across two lines but then it really goes on and on [...]\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_elipsis_lines_still_checked(self):\n        actual = dedent(\n            \"\"\"\n            AssertionError: a long assertion error which ends up wrapping so we have to have it across two lines but then it changes and ends up saying something different from what was expected so we shoulf fail\n            \"\"\"  # noqa\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            AssertionError: a long assertion error which ends up wrapping so we have to\n            have it across two lines but then it really goes on and on [...]\n            \"\"\"\n            ).strip()\n        )\n\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(actual, expected)\n\n    def test_with_middle_elipsis(self):\n        actual = dedent(\n            \"\"\"\n            bla\n            bla bla\n            ERROR: the first line\n\n            some more blurg\n                something else\n                an indented penultimate line\n            KeyError: something\n            more stuff happens later\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            ERROR: the first line\n            [...]\n                an indented penultimate line\n            KeyError: something\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ls(self):\n        expected = Output(\"superlists          functional_tests.py\")\n        actual = \"functional_tests.py\\nsuperlists\\n\"\n        self.assert_console_output_correct(actual, expected, ls=True)\n        self.assertTrue(expected.was_checked)\n\n    def test_working_directory_substitution(self):\n        expected = Output(\"bla bla ...goat-book/foo stuff\")\n        actual = f\"bla bla {self.tempdir}/foo stuff\"\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_tabs(self):\n        expected = Output(\"#       bla bla\")\n        actual = \"#\\tbla bla\"\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_diff_indexes(self):\n        actual = dedent(\n            \"\"\"\n            diff --git a/functional_tests.py b/functional_tests.py\n            index d333591..1f55409 100644\n            --- a/functional_tests.py\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            diff --git a/functional_tests.py b/functional_tests.py\n            index d333591..b0f22dc 100644\n            --- a/functional_tests.py\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_callouts(self):\n        actual = dedent(\n            \"\"\"\n            bla bla\n            stuff\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bla bla  <12>\n            stuff\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_asciidoctor_callouts(self):\n        actual = dedent(\n            \"\"\"\n            bla bla\n            stuff\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bla bla  (12)\n            stuff\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_git_commit_numers_in_logs(self):\n        actual = dedent(\n            \"\"\"\n            ea82222 Basic view now returns minimal HTML\n            7159049 First unit test and url mapping, dummy view\n            edba758 Add app for lists, with deliberately failing unit test\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            a6e6cc9 Basic view now returns minimal HTML\n            450c0f3 First unit test and url mapping, dummy view\n            ea2b037 Add app for lists, with deliberately failing unit test\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n        actual = dedent(\n            \"\"\"\n            abc Basic view now returns minimal HTML\n            123 First unit test and url mapping, dummy view\n            \"\"\"\n        ).strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            bad Basic view now returns minimal HTML\n            456 First unit test and url mapping, dummy view\n            \"\"\"\n            ).strip()\n        )\n\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(actual, expected)\n\n    def test_ignores_geckodriver_stacktrace_line_numbers(self):\n        actual = dedent(\n            \"\"\"\n            Stacktrace:\n            RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8\n            WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:188:3\n            \"\"\"\n        ).rstrip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            Stacktrace:\n            RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:9:8\n            WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:180:6\n            \"\"\"\n            ).rstrip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n\n    def test_ignores_mock_ids(self):\n        actual = dedent(\n            \"\"\"\n                self.assertEqual(user, mock_user)\n            AssertionError: None != <Mock name='mock()' id='46962183546064'>\n            \"\"\"\n        ).rstrip()\n        expected = Output(\n            dedent(\n                \"\"\"\n                self.assertEqual(user, mock_user)\n            AssertionError: None != <Mock name='mock()' id='139758452629392'>\n            \"\"\"\n            ).rstrip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_mock_ids_when_they_dont_have_names(self):\n        actual = dedent(\n            \"\"\"\n                self.assertEqual(user, mock_user)\n            AssertionError: None != <Mock id='46962183546064'>\n            \"\"\"\n        ).rstrip()\n        expected = Output(\n            dedent(\n                \"\"\"\n                self.assertEqual(user, mock_user)\n            AssertionError: None != <Mock id='139758452629392'>\n            \"\"\"\n            ).rstrip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_phantomjs_run_times(self):\n        actual = \"Took 24ms to run 2 tests. 2 passed, 0 failed.\"\n        expected = Output(\"Took 15ms to run 2 tests. 2 passed, 0 failed.\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_bdd_run_times(self):\n        actual = \"features/steps/my_lists.py:19 0.187s\"\n        expected = Output(\"features/steps/my_lists.py:19 0.261s\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_object_ids(self):\n        actual = \"<AnonymousUser object at 0x2b3629047150>\"\n        expected = Output(\"<AnonymousUser object at 0x7f364795ef90>\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_migration_timestamps(self):\n        actual = \"  0005_auto_20140414_2038.py:\"\n        expected = Output(\"  0005_auto_20140414_2108.py:\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_session_ids(self):\n        actual = \"qnslckvp2aga7tm6xuivyb0ob1akzzwl\"\n        expected = Output(\"jvhzc8kj2mkh06xooqq9iciptead20qq\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_3_5_x_AssertionError_None_thing(self):\n        actual = \"AssertionError\"\n        expected = Output(\"AssertionError: None\")\n        self.assert_console_output_correct(actual, expected)\n        actual2 = \"AssertionError: something\"\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(actual2, expected)\n\n    def test_ignores_localhost_server_port_4digits(self):\n        actual = \"//localhost:2021/my-url is a thing\"\n        expected = Output(\"//localhost:3339/my-url is a thing\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_localhost_server_port_5_digits(self):\n        actual = \"//localhost:40433/my-url is a thing\"\n        expected = Output(\"//localhost:8081/my-url is a thing\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_ignores_127_0_0_1_server_port_4digits(self):\n        actual = \"//127.0.0.1:2021/my-url is a thing\"\n        expected = Output(\"//127.0.0.1:3339/my-url is a thing\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_only_ignores_exactly_32_char_strings_no_whitespace(self):\n        actual = \"qnslckvp2aga7tm6xuivyb0ob1akzzwl\"\n        expected = Output(\"jvhzc8kj2mkh06xooqq9iciptead20qq\")\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(actual[:-1], expected[:-1])\n            self.assert_console_output_correct(actual + \"1\", expected + \"a\")\n            self.assert_console_output_correct(\" \" + actual, \" \" + expected)\n\n    def test_ignores_selenium_trace_log_ids(self):\n        actual = dedent(\n            \"\"\"\n            1739977878464    geckodriver    INFO    Listening on 127.0.0.1:59905\n            1739977878481    webdriver::server    DEBUG    -> POST /session\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            1739977878465    geckodriver    INFO    Listening on 127.0.0.1:59905\n            1739987878488    webdriver::server    DEBUG    -> POST /session\n            \"\"\"\n        )\n        self.assert_console_output_correct(actual, Output(expected))\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(\n                actual.replace(\"geckodriver\", \"foo\"),\n                expected.replace(\"geckodriver\", \"foo\"),\n            )\n            self.assert_console_output_correct(\n                actual.replace(\"webdriver\", \"foo\"),\n                expected.replace(\"webdriver\", \"foo\"),\n            )\n\n    def test_ignores_firefox_esr_version(self):\n        expected = \"1234567890111   geckodriver::capabilities       DEBUG   Found version\\n128.10esr\"\n        actual = \"1747863999574\tgeckodriver::capabilities\tDEBUG\tFound version 128.10.1esr\"\n        self.assert_console_output_correct(actual, Output(expected))\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(\n                actual.replace(\"128.10.1esr\", \"1234abc\"),\n                Output(expected),\n            )\n        actual2 = \"1234567890111   geckodriver::capabilities       DEBUG   Found version 140.3esr\"\n        self.assert_console_output_correct(actual2, Output(expected))\n\n    def test_ignores_docker_image_ids_and_creation_time(self):\n        actual = \"superlists   latest    522824a399de   2 weeks ago     164MB\"\n        expected = Output(\"superlists   latest    522824a399de   2 minutes ago   164MB\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"geoff   latest    522824a399de   2 weeks ago     164MB\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_minor_differences_in_curl_output1(self):\n        actual = \"*   Trying ::1:8888...\"\n        expected = Output(\"*   Trying [::1]:8888...\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"*   Trying ::1:9999...\"\n            self.assert_console_output_correct(bad_actual, expected)\n            bad_actual = \"*   Trying [::]1:9999...\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_minor_differences_in_curl_output2(self):\n        actual = \"* Closing connection\"\n        expected = Output(\"* Closing connection 0\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"Closing Geoff\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_minor_differences_in_curl_output3(self):\n        actual = \"* Connected to localhost (127.0.0.1) port 8888 (#0)\"\n        expected = Output(\"* Connected to localhost (127.0.0.1) port 8888\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"* Connected to localhost (127.0.0.1) port 8889 (#0)\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_minor_differences_in_curl_output4(self):\n        actual = \"*> User-Agent: curl/7.81.0\"\n        expected = Output(\"*> User-Agent: curl/8.6.0\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"Closing Geoff\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_minor_differences_in_curl_output5(self):\n        actual = \"0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying [::1]:8888...\"\n        expected = Output(\"*   Trying ::1:8888...\")\n        self.assert_console_output_correct(actual, expected)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"10* Hi\"\n            expected = Output(\"*Hi\")\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_git_localisation_uk_vs_usa(self):\n        actual = \"Initialized empty Git repository in somewhere/.git/\"\n        expected = Output(\"Initialised empty Git repository in somewhere/.git/\")\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n        with self.assertRaises(AssertionError):\n            bad_actual = \"Error initializing Git repo\"\n            self.assert_console_output_correct(bad_actual, expected)\n\n    def test_ignores_screenshot_times(self):\n        actual = (\n            \"screenshotting to ...goat-book/functional_tests/screendumps/MyListsTes\\n\"\n            \"t.test_logged_in_users_lists_are_saved_as_my_lists-window0-2024-03-09T11.39.38.\\n\"\n            \"png\\n\"\n            \"dumping page HTML to ...goat-book/functional_tests/screendumps/MyLists\\n\"\n            \"Test.test_logged_in_users_lists_are_saved_as_my_lists-window0-2024-03-09T11.39.\\n\"\n            \"38.html\\n\"\n        )\n        expected = Output(\n            \"screenshotting to ...goat-book/functional_tests/screendumps/MyListsTes\\n\"\n            \"t.test_logged_in_users_lists_are_saved_as_my_lists-window0-2013-04-09T13.40.39.\\n\"\n            \"png\\n\"\n            \"dumping page HTML to ...goat-book/functional_tests/screendumps/MyLists\\n\"\n            \"Test.test_logged_in_users_lists_are_saved_as_my_lists-window0-2024-04-04T12.43.\\n\"\n            \"42.html\\n\"\n        )\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_matches_system_vs_virtualenv_install_paths(self):\n        actual = dedent(\n            \"\"\"\n              File \"/home/harry/.virtualenvs/Book/lib/python3.4/site-packages/django/core/urlresolvers.py\", line 521, in resolve\n                return get_resolver(urlconf).resolve(path)\n            \"\"\"\n        ).rstrip()\n        expected = Output(\n            dedent(\n                \"\"\"\n              File \"...-packages/django/core/urlresolvers.py\", line 521, in resolve\n                return get_resolver(urlconf).resolve(path)\n            \"\"\"\n            ).rstrip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n        incorrect_actual = dedent(\n            \"\"\"\n              File \"/home/harry/.virtualenvs/Book/lib/python3.4/site-packages/django/core/urlresolvers.py\", line 522, in resolve\n                return get_resolver(urlconf).resolve(path)\n            \"\"\"\n        ).rstrip()\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(incorrect_actual, expected)\n        incorrect_actual = dedent(\n            \"\"\"\n              File \"/home/harry/.virtualenvs/Book/lib/python3.4/site-packages/django/core/another_file.py\", line 521, in resolve\n                return get_resolver(urlconf).resolve(path)\n            \"\"\"\n        ).rstrip()\n        with self.assertRaises(AssertionError):\n            self.assert_console_output_correct(incorrect_actual, expected)\n\n    def test_fixes_stdout_stderr_for_creating_db(self):\n        actual = dedent(\n            \"\"\"\n            ======================================================================\n            FAIL: test_basic_addition (lists.tests.SimpleTest)\n            ----------------------------------------------------------------------\n            Traceback etc\n\n            ----------------------------------------------------------------------\n            Ran 1 tests in X.Xs\n\n            FAILED (failures=1)\n            Creating test database for alias 'default'...\n            Destroying test database for alias 'default'\n            \"\"\"\n        ).strip()\n\n        expected = Output(\n            dedent(\n                \"\"\"\n            Creating test database for alias 'default'...\n            ======================================================================\n            FAIL: test_basic_addition (lists.tests.SimpleTest)\n            ----------------------------------------------------------------------\n            Traceback etc\n\n            ----------------------------------------------------------------------\n            Ran 1 tests in X.Xs\n\n            FAILED (failures=1)\n            Destroying test database for alias 'default'\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_handles_long_lines(self):\n        actual = dedent(\n            \"\"\"\n            A normal line\n                An indented line, that's longer than 80 chars. it goes on for a while you see.\n                a normal indented line\n            \"\"\"\n        ).strip()\n\n        expected = Output(\n            dedent(\n                \"\"\"\n            A normal line\n                An indented line, that's longer than 80 chars. it goes on for a while you\n            see.\n                a normal indented line\n            \"\"\"\n            ).strip()\n        )\n\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_for_minimal_expected(self):\n        actual = dedent(\n            \"\"\"\n            Creating test database for alias 'default'...\n            E\n            ======================================================================\n            ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)\n            ----------------------------------------------------------------------\n            Traceback (most recent call last):\n              File \"...goat-book/lists/tests.py\", line 8, in test_root_url_resolves_to_home_page_view\n                found = resolve('/')\n              File \"/usr/local/lib/python2.7/dist-packages/django/core/urlresolvers.py\", line 440, in resolve\n                return get_resolver(urlconf).resolve(path)\n              File \"/usr/local/lib/python2.7/dist-packages/django/core/urlresolvers.py\", line 104, in get_callable\n                (lookup_view, mod_name))\n            ViewDoesNotExist: Could not import superlists.views.home. Parent module superlists.views does not exist.\n            ----------------------------------------------------------------------\n            Ran 1 tests in X.Xs\n\n            FAILED (errors=1)\n            Destroying test database for alias 'default'...\n            \"\"\"\n        ).strip()\n\n        expected = Output(\n            dedent(\n                \"\"\"\n            ViewDoesNotExist: Could not import superlists.views.home. Parent module\n            superlists.views does not exist.\n            \"\"\"\n            ).strip()\n        )\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n    def test_for_long_traceback(self):\n        with open(\n            os.path.join(os.path.dirname(__file__), \"actual_manage_py_test.output\")\n        ) as f:\n            actual = f.read().strip()\n        expected = Output(\n            dedent(\n                \"\"\"\n            [... lots and lots of traceback]\n\n            Traceback (most recent call last):\n              File \"[...]-packages/django/test/testcases.py\", line 259, in __call__\n                self._pre_setup()\n              File \"[...]-packages/django/test/testcases.py\", line 479, in _pre_setup\n                self._fixture_setup()\n              File \"[...]-packages/django/test/testcases.py\", line 829, in _fixture_setup\n                if not connections_support_transactions():\n              File \"[...]-packages/django/test/testcases.py\", line 816, in\n            connections_support_transactions\n                for conn in connections.all())\n              File \"[...]-packages/django/test/testcases.py\", line 816, in <genexpr>\n                for conn in connections.all())\n              File \"[...]-packages/django/utils/functional.py\", line 43, in __get__\n                res = instance.__dict__[self.func.__name__] = self.func(instance)\n              File \"[...]-packages/django/db/backends/__init__.py\", line 442, in supports_transactions\n                self.connection.enter_transaction_management()\n              File \"[...]-packages/django/db/backends/dummy/base.py\", line 15, in complain\n                raise ImproperlyConfigured(\"settings.DATABASES is improperly configured. \"\n            ImproperlyConfigured: settings.DATABASES is improperly configured. Please\n            supply the ENGINE value. Check settings documentation for more details.\n\n             ---------------------------------------------------------------------\n            Ran 85 tests in 0.788s\n\n            FAILED (errors=404, skipped=1)\n            AttributeError: _original_allowed_hosts\n            \"\"\"\n            ).strip()\n        )\n        self.assert_console_output_correct(actual, expected)\n        self.assertTrue(expected.was_checked)\n\n\nclass CurrentContentsTest(ChapterTest):\n    def test_ok_for_correct_current_contents(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 0\n            line 1\n            line 2\n            line 3\n            line 4\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 2\n            line 3\n            \"\"\"\n            ).lstrip(),\n        )\n        self.check_current_contents(listing, actual_contents)  # should not raise\n\n    def test_raises_for_any_line_not_in_actual_contents(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 0\n            line 1\n            line 2\n            line 3\n            line 4\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 3\n            line 4\n            line 5\n            \"\"\"\n            ).lstrip(),\n        )\n        with self.assertRaises(AssertionError):\n            self.check_current_contents(listing, actual_contents)\n\n    def test_indentation_is_ignored(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 0\n                line 1\n            line 2\n            line 3\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 2\n            line 3\n            \"\"\"\n            ).lstrip(),\n        )\n        self.check_current_contents(listing, actual_contents)\n\n    def test_raises_if_lines_not_in_order(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 1\n            line 2\n            line 3\n            line 4\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 3\n            line 2\n            \"\"\"\n            ).lstrip(),\n        )\n        listing.currentcontents = True\n\n        with self.assertRaises(AssertionError):\n            self.check_current_contents(listing, actual_contents)\n\n    def test_checks_elipsis_blocks_separately(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 1\n            line 2\n            line 3\n            line 4\n            line 5\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 2\n            [...]\n            line 4\n            \"\"\"\n            ).lstrip(),\n        )\n        listing.currentcontents = True\n        self.check_current_contents(listing, actual_contents)  # should not raise\n\n    def test_checks_ignores_blank_lines(self):\n        actual_contents = dedent(\n            \"\"\"\n            line 1\n            line 2\n\n\n            line 3\n\n\n            line 4\n            line 5\n            \"\"\"\n        )\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 2\n\n            line 3\n\n            line 4\n            \"\"\"\n            ).lstrip(),\n        )\n        listing.currentcontents = True\n        self.check_current_contents(listing, actual_contents)  # should not raise\n\n        listing2 = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n            line 1\n            line 2\n            line 3\n\n            line 4\n            \"\"\"\n            ).lstrip(),\n        )\n        with self.assertRaises(AssertionError):\n            self.check_current_contents(listing2, actual_contents)\n\n\nclass SplitBlocksTest(unittest.TestCase):\n    def test_splits_on_multi_newlines(self):\n        assert split_blocks(\n            dedent(\n                \"\"\"\n            this\n            is block 1\n\n            this is block 2\n            \"\"\"\n            )\n        ) == [\"this\\nis block 1\", \"this is block 2\"]\n\n    def test_splits_on_elipsis(self):\n        assert split_blocks(\n            dedent(\n                \"\"\"\n            this\n            is block 1\n            [...]\n            this is block 2\n            \"\"\"\n            )\n        ) == [\"this\\nis block 1\", \"this is block 2\"]\n\n\nclass TestContains:\n    def test_smoketest(self):\n        assert contains([1, 2, 3, 4], [1, 2])\n\n    def test_contains_end_seq(self):\n        assert contains([1, 2, 3, 4], [3, 4])\n\n    def test_contains_middle_seq(self):\n        assert contains([1, 2, 3, 4, 5], [3, 4])\n\n    def test_contains_oversized_seq(self):\n        assert contains([1, 2, 3, 4, 4], [1, 2, 3, 4])\n\n    def test_contains_iteslf(self):\n        assert contains([1, 2, 3], [1, 2, 3])\n\n\n@pytest.mark.skipif(not shutil.which(\"phantomjs\"), reason=\"PhantomJS not available\")\nclass CheckQunitOuptutTest(ChapterTest):\n    def test_partial_listing_passes(self):\n        self.chapter_name = \"chapter_17_javascript\"\n        self.sourcetree.start_with_checkout(\n            \"chapter_18_second_deploy\", \"chapter_17_javascript\"\n        )\n        expected = Output(\"2 assertions of 2 passed, 0 failed.\")\n        self.check_qunit_output(expected)  # should pass\n        assert expected.was_checked\n\n    def test_fails_if_lists_fail_and_no_accounts(self):\n        self.chapter_name = \"chapter_17_javascript\"\n        self.sourcetree.start_with_checkout(\n            \"chapter_18_second_deploy\", \"chapter_17_javascript\"\n        )\n        with self.assertRaises(AssertionError):\n            self.check_qunit_output(Output(\"arg\"))\n\n    def TODOtest_runs_phantomjs_runner_against_lists_tests(self):\n        self.chapter_name = \"chapter_17_javascript\"\n        self.sourcetree.start_with_checkout(\n            \"chapter_18_second_deploy\", \"chapter_17_javascript\"\n        )\n        lists_tests = os.path.join(\n            os.path.abspath(os.path.dirname(__file__)),\n            \"../source/chapter_17_javascript/superlists/lists/static/tests/tests.html\",\n        )\n\n        manual_run = subprocess.check_output(\n            [\"python\", JASMINE_RUNNER, lists_tests],\n        )\n        expected = Output(manual_run.strip().decode())\n        self.check_jasmine_output(expected)  # should pass\n\n\nclass CheckFinalDiffTest(ChapterTest):\n    chapter_name = \"chapter_01\"\n\n    def test_empty_passes(self):\n        self.run_command = lambda _: \"\"\n        self.check_final_diff()  # should pass\n\n    def test_diff_fails(self):\n        diff = dedent(\n            \"\"\"\n            + a missing line\n            - a line that was wrong\n            bla\n            \"\"\"\n        )\n        self.run_command = lambda _: diff\n        with self.assertRaises(AssertionError):\n            self.check_final_diff()\n\n    def test_blank_lines_ignored(self):\n        diff = dedent(\n            \"\"\"\n            +\n            -\n            bla\n            \"\"\"\n        )\n        self.run_command = lambda _: diff\n        self.check_final_diff()  # should pass\n\n    def test_ignore_moves(self):\n        diff = dedent(\n            \"\"\"\n            + some\n            + block\n            stuff\n            - some\n            - block\n\n            bla\n            \"\"\"\n        )\n        self.run_command = lambda _: diff\n        with self.assertRaises(AssertionError):\n            self.check_final_diff()\n        self.check_final_diff(ignore=[\"moves\"])  # should pass\n        with self.assertRaises(AssertionError):\n            diff += \"\\n+a genuinely different line\"\n            self.check_final_diff(ignore=[\"moves\"])\n\n    def test_ignore_secret_key_and_generated_by_django(self):\n        diff = dedent(\n            \"\"\"\n            diff --git a/superlists/settings.py b/superlists/settings.py\n            index 7463a4c..6eb4bde 100644\n            --- a/superlists/settings.py\n            +++ b/superlists/settings.py\n            @@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))\n             # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/\n\n             # SECURITY WARNING: keep the secret key used in production secret!\n            -SECRET_KEY = '!x8-9w9o%s#c8u(4^zb9n2g(xy4q*@c^$9axl2o48wkz(v%_!*'\n            +SECRET_KEY = 'y)exet(6z6z6)(b!v1m8it$a0q^e=b^#*r8a2o5er1u(=sl=7f'\n\n             # SECURITY WARNING: don't run with debug turned on in production!\n\n            -# Generated by Django 1.10.3 on 2016-12-01 21:11\n            +# Generated by Django 1.10.3 on 2016-12-02 10:19\n             from __future__ import unicode_literals\n            \"\"\"\n        )\n        self.run_command = lambda _: diff\n        with self.assertRaises(AssertionError):\n            self.check_final_diff()\n        self.check_final_diff(\n            ignore=[\"SECRET_KEY\", \"Generated by Django 1.10\"]\n        )  # should pass\n        with self.assertRaises(AssertionError):\n            diff += \"\\n+a genuinely different line\"\n            self.check_final_diff(ignore=[\"SECRET_KEY\", \"Generated by Django 1.10\"])\n\n    def test_ignore_moves_and_custom(self):\n        diff = dedent(\n            \"\"\"\n            + some\n            + block\n            stuff\n            - some\n            - block\n\n            bla\n            + ignore me\n            \"\"\"\n        )\n        self.run_command = lambda _: diff\n        with self.assertRaises(AssertionError):\n            self.check_final_diff()\n        self.check_final_diff(ignore=[\"moves\", \"ignore me\"])  # should pass\n        with self.assertRaises(AssertionError):\n            diff += \"\\n+a genuinely different line\"\n            self.check_final_diff(ignore=[\"moves\", \"ignore me\"])\n"
  },
  {
    "path": "tests/test_chapter_01.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport os\nimport unittest\n\nfrom book_parser import Output\nfrom book_tester import ChapterTest, CodeListing, write_to_file\nfrom update_source_repo import update_sources_for_chapter\n\nos.environ[\"LC_ALL\"] = \"en_GB.UTF-8\"\nos.environ[\"LANG\"] = \"en_GB.UTF-8\"\nos.environ[\"LANGUAGE\"] = \"en_GB.UTF-8\"\n\n\nclass Chapter1Test(ChapterTest):\n    chapter_name = \"chapter_01\"\n\n    def write_to_file(self, codelisting):\n        # override write to file, in this chapter cwd is root tempdir\n        print(\"writing to file\", codelisting.filename)\n        write_to_file(codelisting, os.path.join(self.tempdir))\n        print(\"wrote\", open(os.path.join(self.tempdir, codelisting.filename)).read())\n\n    def test_listings_and_commands_and_output(self):\n        update_sources_for_chapter(self.chapter_name, previous_chapter=None)\n        self.parse_listings()\n        # self.fail('\\n'.join(f'{l.type}: {l}' for l in self.listings))\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), CodeListing)\n\n        self.skip_with_check(6, \"Performing system checks...\")  # after runserver\n        self.listings[8] = Output(str(self.listings[8]).replace(\"$\", \"\"))\n\n        # prep folder as it would be\n        self.sourcetree.run_command(\"mkdir -p .venv/bin\")\n        self.sourcetree.run_command(\"mkdir -p .venv/lib\")\n\n        self.unset_PYTHONDONTWRITEBYTECODE()\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        # manually add repo, we didn't do it at the beginning\n        local_repo_path = os.path.abspath(\n            os.path.join(os.path.dirname(__file__), \"../source/chapter_01/superlists\")\n        )\n        self.sourcetree.run_command('git remote add repo \"{}\"'.format(local_repo_path))\n        self.sourcetree.run_command(\"git fetch repo\")\n\n        self.check_final_diff(\n            ignore=[\n                \"SECRET_KEY\",\n                \"Generated by 'django-admin startproject' using Django 5.2.\",\n            ]\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_02_unittest.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport unittest\n\nfrom book_tester import (\n    ChapterTest,\n    CodeListing,\n    Command,\n)\n\nclass Chapter2Test(ChapterTest):\n    chapter_name = 'chapter_02_unittest'\n    previous_chapter = 'chapter_01'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), CodeListing)\n        self.assertEqual(type(self.listings[2]), Command)\n\n        self.start_with_checkout()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_03_unit_test_first_view.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport unittest\nimport time\n\nfrom book_tester import (\n    ChapterTest,\n    CodeListing,\n    Command,\n    Output,\n)\n\nclass Chapter3Test(ChapterTest):\n    chapter_name = 'chapter_03_unit_test_first_view'\n    previous_chapter = 'chapter_02_unittest'\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), Command)\n        self.assertEqual(type(self.listings[1]), Output)\n        self.assertEqual(type(self.listings[2]), CodeListing)\n\n        self.skip_with_check(10, 'will show you')\n        final_ft = 43\n        self.assertIn('Finish the test', self.listings[final_ft + 1])\n\n        self.start_with_checkout()\n        self.start_dev_server()\n        self.unset_PYTHONDONTWRITEBYTECODE()\n\n        print(self.pos)\n        assert 'manage.py startapp lists' in self.listings[self.pos]\n        self.recognise_listing_and_process_it()\n        time.sleep(1)  # voodoo sleep, otherwise db.sqlite3 doesnt appear in CI sometimes\n\n        while self.pos < final_ft:\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n        self.restart_dev_server()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_04_philosophy_and_refactoring.py",
    "content": "#!/usr/bin/env python3\nimport time\nimport unittest\n\nfrom book_tester import (\n    ChapterTest,\n    CodeListing,\n    Command,\n    Output,\n)\n\n\nclass Chapter4Test(ChapterTest):\n    chapter_name = \"chapter_04_philosophy_and_refactoring\"\n    previous_chapter = \"chapter_03_unit_test_first_view\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), Command)\n        self.assertEqual(type(self.listings[1]), Output)\n        self.assertEqual(type(self.listings[2]), CodeListing)\n\n        self.start_with_checkout()\n        self.start_dev_server()\n\n        self.skip_with_check(38, \"add the untracked templates folder\")\n        self.skip_with_check(40, \"review the changes\")\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            time.sleep(0.5)  # let runserver fs watcher catch up\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_05_post_and_database.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import (\n    ChapterTest,\n    CodeListing,\n    Command,\n    Output,\n)\n\n\nclass Chapter5Test(ChapterTest):\n    chapter_name = \"chapter_05_post_and_database\"\n    previous_chapter = \"chapter_04_philosophy_and_refactoring\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), Output)\n        self.assertEqual(type(self.listings[1]), CodeListing)\n        self.assertEqual(type(self.listings[3]), Command)\n\n        views_pos = 21\n        self.find_with_check(views_pos, \"def home_page\")\n\n        nutemplate_pos = 95\n        nl = self.find_with_check(nutemplate_pos, '{\"items\": items}')\n        print(nl)\n\n        migrate_pos = 99\n        ml = self.find_with_check(migrate_pos, \"migrate\")\n        assert ml.type == \"interactive manage.py\"\n\n        self.start_with_checkout()\n        self.start_dev_server()\n        self.unset_PYTHONDONTWRITEBYTECODE()\n\n        restarted_after_views = False\n        restarted_after_migrate = False\n        restarted_after_nutemplate = False\n        while self.pos < len(self.listings):\n            print(self.pos)\n            if self.pos > views_pos and not restarted_after_views:\n                self.restart_dev_server()\n                restarted_after_views = True\n            if self.pos > migrate_pos and not restarted_after_migrate:\n                self.restart_dev_server()\n                restarted_after_migrate = True\n            if self.pos > nutemplate_pos and not restarted_after_nutemplate:\n                self.restart_dev_server()\n                restarted_after_nutemplate = True\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(\n            ignore=[\n                \"moves\",\n                \"Generated by Django 5.\",\n            ]\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_06_explicit_waits_1.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import (\n    ChapterTest,\n    Command,\n)\n\n\nclass Chapter6Test(ChapterTest):\n    chapter_name = \"chapter_06_explicit_waits_1\"\n    previous_chapter = \"chapter_05_post_and_database\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(type(self.listings[0]), Command)\n        self.assertEqual(type(self.listings[1]), Command)\n        self.assertEqual(type(self.listings[2]), Command)\n\n        # skips\n        self.skip_with_check(15, \"msg eg\")  # git\n\n        # other prep\n        self.start_with_checkout()\n        self.unset_PYTHONDONTWRITEBYTECODE()\n        self.run_command(Command(\"python3 manage.py migrate --noinput\"))\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_07_working_incrementally.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import (\n    ChapterTest,\n    Command,\n)\n\n\nclass Chapter7Test(ChapterTest):\n    chapter_name = \"chapter_07_working_incrementally\"\n    previous_chapter = \"chapter_06_explicit_waits_1\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"output\")\n        self.assertEqual(self.listings[1].type, \"output\")\n\n        # skips\n        self.skip_with_check(61, \"should show 4 changed files\")  # git\n        self.skip_with_check(66, \"add a message summarising\")  # git\n        self.skip_with_check(87, \"5 changed files\")  # git\n        self.skip_with_check(89, \"forms x2\")  # git\n        self.skip_with_check(116, \"3 changed files\")  # git\n\n        # other prep\n        self.start_with_checkout()\n        self.run_command(Command(\"python3 manage.py migrate --noinput\"))\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.check_final_diff(ignore=[\"moves\", \"Generated by Django 5.\"])\n        self.assert_all_listings_checked(self.listings)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_08_prettification.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_parser import Command, Output\nfrom book_tester import ChapterTest\n\n\nclass Chapter8Test(ChapterTest):\n    chapter_name = \"chapter_08_prettification\"\n    previous_chapter = \"chapter_07_working_incrementally\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(type(self.listings[1]), Command)\n        self.assertEqual(type(self.listings[2]), Output)\n\n        self.start_with_checkout()\n        # other prep\n        self.sourcetree.run_command(\"python3 manage.py migrate --noinput\")\n        # self.unset_PYTHONDONTWRITEBYTECODE()\n        self.prep_virtualenv()\n        self.sourcetree.run_command(\"uv pip install pip\")\n\n        # skips\n        self.skip_with_check(24, \"the -w means ignore whitespace\")\n        self.skip_with_check(27, \"leave static, for now\")\n        self.skip_with_check(52, \"will now show all the bootstrap\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_09_docker.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter9Test(ChapterTest):\n    chapter_name = \"chapter_09_docker\"\n    previous_chapter = \"chapter_08_prettification\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"test\")\n\n        # skips:\n\n        # docker build output, we want to run the 'docker build'\n        # but not check output\n        self.skip_with_check(29, \"naming to docker.io/library/superlists\")\n        self.skip_with_check(36, \"naming to docker.io/library/superlists\")\n        self.skip_with_check(36, \"naming to docker.io/library/superlists\")\n        # normal git one\n        self.skip_with_check(82, \"add Dockerfile, .dockerignore, .gitignore\")\n\n        self.start_with_checkout()\n        # simulate having a db.sqlite3 and a static folder from previous chaps\n        self.sourcetree.run_command(\"./manage.py migrate --noinput\")\n        self.sourcetree.run_command(\"./manage.py collectstatic --noinput\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n\n        while self.pos < len(self.listings):\n            listing = self.listings[self.pos]\n            print(self.pos, listing.type, repr(listing))\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_10_production_readiness.py",
    "content": "#!/usr/bin/env python3\nimport unittest\nfrom pathlib import Path\n\nfrom book_tester import ChapterTest\n\nTHIS_DIR = Path(__file__).parent\n\n\nclass Chapter10Test(ChapterTest):\n    chapter_name = \"chapter_10_production_readiness\"\n    previous_chapter = \"chapter_09_docker\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"other command\")\n        self.assertEqual(self.listings[3].type, \"docker run tty\")\n\n        self.start_with_checkout()\n        self.prep_virtualenv()\n        self.prep_database()\n\n        # skips\n        self.skip_with_check(47, \"should show dockerfile\")\n        self.skip_with_check(50, \"should now be clean\")\n        self.skip_with_check(55, \"Change the owner\")\n        self.skip_with_check(57, \"Change the file to be group-writeable as well\")\n        self.skip_with_check(61, \"note container id\")\n\n        # hack fast-forward, nu way\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            listing = self.listings[self.pos]\n            print(self.pos, listing.type, repr(listing))\n\n            self.recognise_listing_and_process_it()\n\n        self.check_final_diff(\n            ignore=[\n                \"Django==5.2\",\n                \"gunicorn==2\",\n                \"whitenoise==6.\",\n            ]\n        )\n        self.assert_all_listings_checked(self.listings)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_11_server_prep.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter11Test(ChapterTest):\n    chapter_name = \"chapter_11_server_prep\"\n    previous_chapter = \"chapter_10_production_readiness\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"server command\")\n        self.assertEqual(self.listings[1].type, \"server command\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n        self.start_with_checkout()\n        self.prep_virtualenv()\n        # self.sourcetree.run_command('mkdir -p static/stuff')\n\n        # skips\n        # self.skip_with_check(13, \"we also need the Docker\")\n\n        # vm_restore = 'MANUAL_END'\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.check_call(['vagrant', 'snapshot', 'restore', vm_restore])\n        #\n        # self.current_server_cd = '~/sites/$SITENAME'\n\n        while self.pos < len(self.listings):\n            listing = self.listings[self.pos]\n            print(self.pos, listing.type, repr(listing))\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.sourcetree.run_command(\"git add . && git commit -m ch11\")\n        self.check_final_diff(ignore=[\"gunicorn==19\"])\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.run(['vagrant', 'snapshot', 'delete', 'MAKING_END'], check=False)\n        #     subprocess.run(['vagrant', 'snapshot', 'save', 'MAKING_END'], check=True)\n        #\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_12_ansible.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter12Test(ChapterTest):\n    chapter_name = \"chapter_12_ansible\"\n    previous_chapter = \"chapter_11_server_prep\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"against staging\")\n\n        self.start_with_checkout()\n        self.prep_virtualenv()\n        self.prep_database()\n        # self.sourcetree.run_command('mkdir -p static/stuff')\n\n        # skips\n        self.skip_with_check(62, \"git diff\")\n        self.skip_with_check(63, \"should show our changes\")\n\n        # vm_restore = 'MANUAL_END'\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.check_call(['vagrant', 'snapshot', 'restore', vm_restore])\n        #\n        # self.current_server_cd = '~/sites/$SITENAME'\n\n        while self.pos < len(self.listings):\n            listing = self.listings[self.pos]\n            print(self.pos, listing.type, repr(listing))\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"gunicorn==19\"])\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.run(['vagrant', 'snapshot', 'delete', 'MAKING_END'], check=False)\n        #     subprocess.run(['vagrant', 'snapshot', 'save', 'MAKING_END'], check=True)\n        #\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_13_organising_test_files.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter13Test(ChapterTest):\n    chapter_name = \"chapter_13_organising_test_files\"\n    previous_chapter = \"chapter_12_ansible\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[2].type, \"test\")\n\n        # other prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(\n            ignore=[\n                # \"django==1.11\"\n            ]\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_14_database_layer_validation.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter13Test(ChapterTest):\n    chapter_name = \"chapter_14_database_layer_validation\"\n    previous_chapter = \"chapter_13_organising_test_files\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"test\")\n        self.assertEqual(self.listings[1].type, \"output\")\n        self.assertEqual(self.listings[2].type, \"code listing with git ref\")\n\n        # other prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # self.skip_with_check(5, \"equivalent to running sqlite3\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_15_simple_form.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter15Test(ChapterTest):\n    chapter_name = \"chapter_15_simple_form\"\n    previous_chapter = \"chapter_14_database_layer_validation\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n        # skips\n        # self.skip_with_check(31, \"# review changes\")  # diff\n\n        # prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_16_advanced_forms.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter16Test(ChapterTest):\n    chapter_name = \"chapter_16_advanced_forms\"\n    previous_chapter = \"chapter_15_simple_form\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"test\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n        # prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # skips\n        self.skip_with_check(29, \"# should show changes\")  # diff\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"Generated by Django 5.\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_17_javascript.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter16Test(ChapterTest):\n    chapter_name = \"chapter_17_javascript\"\n    previous_chapter = \"chapter_16_advanced_forms\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"test\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n        # skip some inline bash comments\n        self.skip_with_check(15, \"if you're on Windows\")\n        self.skip_with_check(17, \"delete all the other stuff\")\n        self.skip_with_check(74, \"all our js\")\n        self.skip_with_check(76, \"changes to the base template\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.sourcetree.run_command('git add . && git commit -m\"final commit\"')\n        self.check_final_diff()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_19_spiking_custom_auth.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter19Test(ChapterTest):\n    chapter_name = \"chapter_19_spiking_custom_auth\"\n    previous_chapter = \"chapter_18_second_deploy\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        # self.assertEqual(self.listings[0].type, 'other command')\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[2].type, \"other command\")\n        # self.assertTrue(self.listings[88].dofirst)\n\n        # skips\n        self.skip_with_check(34, \"switch back to main\")  # comment\n        self.skip_with_check(36, \"remove any trace\")  # comment\n\n        # prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        # and do a final commit\n        self.sourcetree.run_command('git add . && git commit -m\"final commit\"')\n        self.check_final_diff(ignore=[\"Generated by Django 5.\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_20_mocking_1.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter20Test(ChapterTest):\n    chapter_name = \"chapter_20_mocking_1\"\n    previous_chapter = \"chapter_19_spiking_custom_auth\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[2].type, \"test\")\n\n        # skips\n        # self.skip_with_check(22, 'switch back to master') # comment\n\n        self.prep_database()\n        # self.sourcetree.run_command(\"rm src/accounts/tests.py\")\n        self.sourcetree.run_command(\"mkdir -p src/static\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        self.sourcetree.run_command(\n            'git add . && git commit -m\"final commit in chap 19\"'\n        )\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_21_mocking_2.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter21Test(ChapterTest):\n    chapter_name = \"chapter_21_mocking_2\"\n    previous_chapter = \"chapter_20_mocking_1\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing\")\n        self.assertEqual(self.listings[1].skip, True)\n        self.assertEqual(self.listings[2].type, \"code listing with git ref\")\n\n        # skips\n        # self.skip_with_check(22, 'switch back to master') # comment\n\n        self.prep_database()\n        # self.sourcetree.run_command(\"rm src/accounts/tests.py\")\n        self.sourcetree.run_command(\"mkdir -p src/static\")\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_22_fixtures_and_wait_decorator.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter22Test(ChapterTest):\n    chapter_name = \"chapter_22_fixtures_and_wait_decorator\"\n    previous_chapter = \"chapter_21_mocking_2\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n\n        # skips\n        # self.skip_with_check(22, 'switch back to master') # comment\n\n        # prep\n        self.start_with_checkout()\n        self.prep_database()\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        self.sourcetree.tidy_up_after_patches()\n        self.sourcetree.run_command('git add . && git commit -m\"final commit ch17\"')\n        self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_23_debugging_prod.py",
    "content": "#!/usr/bin/env python3.7\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter18Test(ChapterTest):\n    chapter_name = \"chapter_23_debugging_prod\"\n    previous_chapter = \"chapter_22_fixtures_and_wait_decorator\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"docker run tty\")\n        self.assertEqual(self.listings[1].type, \"output\")\n\n        # skips\n        self.skip_with_check(1, \"naming to docker\")\n\n        # self.replace_command_with_check(\n        #     13,\n        #     \"EMAIL_PASSWORD=yoursekritpasswordhere\",\n        #     \"EMAIL_PASSWORD=\" + os.environ[\"EMAIL_PASSWORD\"],\n        # )\n\n        # deploy_pos = 49\n        # assert \"ansible-playbook\" in self.listings[deploy_pos]\n\n        # prep\n        self.start_with_checkout()\n        self.prep_database()\n        self.sourcetree.run_command(\"touch container.db.sqlite3\")\n        self.sourcetree.run_command(\"sudo chown 1234 container.db.sqlite3\")\n        # for macos, see chap 10\n        self.sourcetree.run_command(\"sudo chmod g+rw container.db.sqlite3\")\n\n        # vm_restore = \"FABRIC_END\"\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.check_call([\"vagrant\", \"snapshot\", \"restore\", vm_restore])\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        self.sourcetree.tidy_up_after_patches()\n        self.sourcetree.run_command('git add . && git commit -m\"final commit ch17\"')\n        self.check_final_diff(ignore=[\"moves\", \"YAHOO_PASSWORD\"])\n        # if DO_SERVER_COMMANDS:\n        #     subprocess.check_call([\"vagrant\", \"snapshot\", \"save\", \"SERVER_DEBUGGED\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_24_outside_in.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter24Test(ChapterTest):\n    chapter_name = \"chapter_24_outside_in\"\n    previous_chapter = \"chapter_23_debugging_prod\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        # self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"code listing with git ref\")\n\n        # skips\n        self.skip_with_check(42, 'views.py, templates')\n\n        self.start_with_checkout()\n        self.prep_database()\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos, self.listings[self.pos].type)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.check_final_diff(ignore=[\"moves\", \"Generated by Django 5\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_25_CI.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter25Test(ChapterTest):\n    chapter_name = \"chapter_25_CI\"\n    previous_chapter = \"chapter_24_outside_in\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        # self.prep_virtualenv()\n\n        # sanity checks\n        self.assertEqual(self.listings[0].skip, True)\n        self.assertEqual(self.listings[1].skip, True)\n        self.assertEqual(self.listings[10].type, \"code listing with git ref\")\n\n        # skips\n        # self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n        self.sourcetree.run_command(\n            \"git add .gitlab-ci.yml\",\n        )\n        # TODO: test package.json\n        # self.sourcetree.run_command(\n        #     \"git add src/lists/static/package.json src/lists/static/tests\"\n        # )\n        self.sourcetree.run_command(\"git commit -m'final commit'\")\n        # self.check_final_diff(ignore=[\"moves\"])\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_chapter_26_page_pattern.py",
    "content": "#!/usr/bin/env python3\nimport unittest\n\nfrom book_tester import ChapterTest\n\n\nclass Chapter26Test(ChapterTest):\n    chapter_name = \"chapter_26_page_pattern\"\n    previous_chapter = \"chapter_25_CI\"\n\n    def test_listings_and_commands_and_output(self):\n        self.parse_listings()\n        self.start_with_checkout()\n        # self.prep_virtualenv()\n\n        # sanity checks\n        # self.assertEqual(self.listings[0].type, 'code listing')\n        self.assertEqual(self.listings[0].type, \"code listing with git ref\")\n        self.assertEqual(self.listings[1].type, \"test\")\n        self.assertEqual(self.listings[2].type, \"output\")\n\n        # skips\n        # self.skip_with_check(22, 'switch back to master') # comment\n\n        # hack fast-forward\n        self.skip_forward_if_skipto_set()\n\n        while self.pos < len(self.listings):\n            print(self.pos)\n            self.recognise_listing_and_process_it()\n\n        self.assert_all_listings_checked(self.listings)\n\n        self.sourcetree.tidy_up_after_patches()\n        # final branch includes a suggested implementation...\n        # so just check diff up to the last listing\n        commit = self.sourcetree.get_commit_spec(\"ch26l013\")\n        diff = self.sourcetree.run_command(f\"git diff -b {commit}\")\n        self.check_final_diff(ignore=[\"moves\"], diff=diff)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_source_updater.py",
    "content": "#!/usr/bin/env python3\nimport unittest\nimport tempfile\nfrom textwrap import dedent\n\n\nfrom source_updater import Source, SourceUpdateError\n\n\nclass SourceTest(unittest.TestCase):\n\n    def test_from_path_constructor_with_existing_file(self):\n        tf = tempfile.NamedTemporaryFile(delete=False)\n        self.addCleanup(tf.close)\n        tf.write('stuff'.encode('utf8'))\n        tf.flush()\n\n        s = Source.from_path(tf.name)\n        self.assertIsInstance(s, Source)\n        self.assertEqual(s.path, tf.name)\n        self.assertEqual(s.contents, 'stuff')\n\n\n    def test_from_path_constructor_with_nonexistent_file(self):\n        s = Source.from_path('no.such.path')\n        self.assertIsInstance(s, Source)\n        self.assertEqual(s.path, 'no.such.path')\n        self.assertEqual(s.contents, '')\n\n\n    def test_lines(self):\n        s = Source()\n        s.contents = 'abc\\ndef'\n        self.assertEqual(s.lines, ['abc', 'def'])\n\n\n    def test_write_writes_new_content_to_path(self):\n        s = Source()\n        tf = tempfile.NamedTemporaryFile()\n        s.get_updated_contents = lambda: 'abc\\ndef'\n        s.path = tf.name\n        s.write()\n        with open(tf.name) as f:\n            self.assertEqual(f.read(), s.get_updated_contents())\n\n\n\nclass FunctionFinderTest(unittest.TestCase):\n\n    def test_function_object(self):\n        s = Source._from_contents(dedent(\n            \"\"\"\n            def a_function(stuff, args):\n                pass\n            \"\"\"\n        ))\n        f = s.functions['a_function']\n        self.assertEqual(f.name, 'a_function')\n        self.assertEqual(f.full_line, 'def a_function(stuff, args):')\n\n\n    def test_finds_functions(self):\n        s = Source._from_contents(dedent(\n            \"\"\"\n            def firstfn(stuff, args):\n                pass\n\n            # stuff\n\n            def second_fn():\n                pass\n            \"\"\"\n        ))\n        assert list(s.functions) == ['firstfn', 'second_fn']\n\n\n    def test_finds_views(self):\n        s = Source._from_contents(dedent(\n            \"\"\"\n            def firstfn(stuff, args):\n                pass\n\n            # stuff\n            def a_view(request):\n                pass\n\n            def second_fn():\n                pass\n\n            def another_view(request, stuff):\n                pass\n            \"\"\"\n        ))\n        assert list(s.functions) == ['firstfn', 'a_view', 'second_fn', 'another_view']\n        assert list(s.views) == ['a_view', 'another_view']\n\n\n    def test_finds_classes(self):\n        s = Source._from_contents(dedent(\n            \"\"\"\n            import thingy\n\n            class Jimbob(object):\n                pass\n\n            # stuff\n            class Harlequin(thingy.Thing):\n                pass\n            \"\"\"\n        ))\n        assert list(s.classes) == ['Jimbob', 'Harlequin']\n\n\n\nclass ReplaceFunctionTest(unittest.TestCase):\n\n    def test_finding_last_line_in_function(self):\n        source = Source._from_contents(dedent(\"\"\"\n            def myfn():\n                a += 1\n                return b\n            \"\"\").strip()\n        )\n        assert source.functions['myfn'].last_line ==  2\n\n\n    def test_finding_last_line_in_function_with_brackets(self):\n        source = Source._from_contents(dedent(\"\"\"\n            def myfn():\n                a += 1\n                return (\n                    '2'\n                )\n            \"\"\").strip()\n        )\n        assert source.functions['myfn'].last_line ==  4\n\n\n    def test_finding_last_line_in_function_with_brackets_before_another(self):\n        source = Source._from_contents(dedent(\"\"\"\n            def myfn():\n                a += 1\n                return (\n                    '2'\n                )\n\n            # bla\n\n            def anotherfn():\n                pass\n            \"\"\").strip()\n        )\n        assert source.functions['myfn'].last_line ==  4\n\n\n    def test_changing_the_end_of_a_method(self):\n        source = Source._from_contents(dedent(\"\"\"\n            class A(object):\n                def method1(self, stuff):\n                    # do step 1\n                    # do step 2\n                    # do step 3\n                    # do step 4\n                    return (\n                        'something'\n                    )\n\n\n                def method2(self):\n                    # do stuff\n                    pass\n            \"\"\").lstrip()\n        )\n        new = dedent(\"\"\"\n            def method1(self, stuff):\n                # do step 1\n                # do step 2\n                # do step A\n                return (\n                    'something else'\n                )\n            \"\"\"\n        ).strip()\n        expected = dedent(\"\"\"\n            class A(object):\n                def method1(self, stuff):\n                    # do step 1\n                    # do step 2\n                    # do step A\n                    return (\n                        'something else'\n                    )\n\n\n                def method2(self):\n                    # do stuff\n                    pass\n            \"\"\"\n        ).lstrip()\n        to_write = source.replace_function(new.split('\\n'))\n        assert to_write == expected\n        assert source.get_updated_contents() == expected\n\n\nclass RemoveFunctionTest(unittest.TestCase):\n\n    def test_removing_a_function(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            def fn1(args):\n                # do stuff\n                pass\n\n\n            def fn2(arg2, arg3):\n                # do things\n                return 2\n\n\n            def fn3():\n                # do nothing\n                # really\n                pass\n            \"\"\").lstrip()\n        )\n\n        expected = dedent(\n            \"\"\"\n            def fn1(args):\n                # do stuff\n                pass\n\n\n            def fn3():\n                # do nothing\n                # really\n                pass\n            \"\"\"\n        ).lstrip()\n\n        assert source.remove_function('fn2') == expected\n        assert source.get_updated_contents() == expected\n\n\nclass AddToClassTest(unittest.TestCase):\n\n    def test_finding_class_info(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import topline\n\n            class ClassA(object):\n                def metha(self):\n                    pass\n\n                def metha2(self):\n                    pass\n\n            class ClassB(object):\n                def methb(self):\n                    pass\n            \"\"\").lstrip()\n        )\n\n        assert source.classes['ClassA'].start_line == 2\n        assert source.classes['ClassA'].last_line == 7\n        assert source.classes['ClassA'].source == dedent(\n            \"\"\"\n            class ClassA(object):\n                def metha(self):\n                    pass\n\n                def metha2(self):\n                    pass\n            \"\"\").strip()\n\n        assert source.classes['ClassB'].last_line == 11\n        assert source.classes['ClassB'].source == dedent(\n            \"\"\"\n            class ClassB(object):\n                def methb(self):\n                    pass\n            \"\"\").strip()\n\n\n    def test_addding_to_class(self):\n        source = Source._from_contents(dedent(\"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n\n\n\n            class B(object):\n                def methb(self):\n                    pass\n            \"\"\").lstrip()\n        )\n        source.add_to_class('A', dedent(\n            \"\"\"\n            def metha2(self):\n                pass\n            \"\"\").strip().split('\\n')\n        )\n\n        expected = dedent(\"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n\n\n                def metha2(self):\n                    pass\n\n\n\n            class B(object):\n                def methb(self):\n                    pass\n            \"\"\"\n        ).lstrip()\n        assert source.contents == expected\n\n\n    def test_addding_to_class_fixes_indents_and_superfluous_lines(self):\n        source = Source._from_contents(dedent(\"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n            \"\"\").lstrip()\n        )\n        source.add_to_class('A', [\n            \"\",\n            \"    def metha2(self):\",\n            \"        pass\",\n        ])\n\n        expected = dedent(\"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n\n\n                def metha2(self):\n                    pass\n            \"\"\"\n        ).lstrip()\n        assert source.contents == expected\n\n\n\nclass ImportsTest(unittest.TestCase):\n\n    def test_finding_different_types_of_import(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import trees\n            from django.core.widgets import things, more_things\n            import cars\n            from datetime import datetime\n\n            from django.monkeys import banana_eating\n\n            from lists.views import Thing\n\n            not_an_import = 'import things'\n\n            def foo():\n                # normal code\n                pass\n            \"\"\"\n        ))\n\n        assert set(source.imports) == {\n            \"import trees\",\n            \"from django.core.widgets import things, more_things\",\n            \"import cars\",\n            \"from datetime import datetime\",\n            \"from django.monkeys import banana_eating\",\n            \"from lists.views import Thing\",\n        }\n        assert set(source.django_imports) == {\n            \"from django.core.widgets import things, more_things\",\n            \"from django.monkeys import banana_eating\",\n        }\n        assert set(source.project_imports) == {\n            \"from lists.views import Thing\",\n        }\n        assert set(source.general_imports) == {\n            \"import trees\",\n            \"from datetime import datetime\",\n            \"import cars\",\n        }\n\n\n    def test_find_first_nonimport_line(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import trees\n            from django.core.widgets import things, more_things\n            from django.monkeys import banana_eating\n            from lists.views import Thing\n\n            not_an_import = 'bla'\n            # the end\n            \"\"\").lstrip()\n        )\n\n        assert source.find_first_nonimport_line() == 5\n\n\n    def test_find_first_nonimport_line_raises_if_imports_in_a_mess(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import trees\n            def foo():\n                return 'monkeys'\n            import monkeys\n            \"\"\").lstrip()\n        )\n        with self.assertRaises(SourceUpdateError):\n            source.find_first_nonimport_line()\n\n    def test_fixed_imports(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import btopline\n            import atopline\n            \"\"\").lstrip()\n        )\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n            import btopline\n            \"\"\").lstrip()\n\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import atopline\n\n            from django.monkeys import monkeys\n            from django.chickens import chickens\n            \"\"\").lstrip()\n        )\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n\n            from django.chickens import chickens\n            from django.monkeys import monkeys\n            \"\"\").lstrip()\n\n        source = Source._from_contents(dedent(\n            \"\"\"\n            from lists.views import thing\n            import atopline\n            \"\"\").lstrip()\n        )\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n\n            from lists.views import thing\n            \"\"\").lstrip()\n\n        source = Source._from_contents(dedent(\n            \"\"\"\n            from lists.views import thing\n\n            from django.db import models\n            import atopline\n\n            from django.aardvarks import Wilbur\n\n            \"\"\").lstrip()\n        )\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n\n            from django.aardvarks import Wilbur\n            from django.db import models\n\n            from lists.views import thing\n            \"\"\").lstrip()\n\n\n    def test_add_import(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import atopline\n\n            from django.monkeys import monkeys\n            from django.chickens import chickens\n\n            from lists.views import thing\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\").lstrip()\n        )\n        source.add_imports([\n            \"import btopline\"\n        ])\n\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n            import btopline\n\n            from django.chickens import chickens\n            from django.monkeys import monkeys\n\n            from lists.views import thing\n            \"\"\"\n        ).lstrip()\n\n        source.add_imports([\n            \"from django.dickens import ChuzzleWit\"\n        ])\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n            import btopline\n\n            from django.chickens import chickens\n            from django.dickens import ChuzzleWit\n            from django.monkeys import monkeys\n\n            from lists.views import thing\n            \"\"\"\n        ).lstrip()\n\n\n    def test_add_import_chooses_longer_lines(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import atopline\n            from django.chickens import chickens\n            from lists.views import thing\n            # some stuff\n            \"\"\").lstrip()\n        )\n        source.add_imports([\n            \"from django.chickens import chickens, eggs\"\n        ])\n\n        assert source.fixed_imports == dedent(\n            \"\"\"\n            import atopline\n\n            from django.chickens import chickens, eggs\n\n            from lists.views import thing\n            \"\"\"\n        ).lstrip()\n\n\n    def test_add_import_ends_up_in_updated_contents_when_appending(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import atopline\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\").lstrip()\n        )\n        source.add_imports([\n            \"from django.db import models\"\n        ])\n\n        assert source.contents == dedent(\n            \"\"\"\n            import atopline\n\n            from django.db import models\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\"\n        ).lstrip()\n\n\n    def test_add_import_ends_up_in_updated_contents_when_prepending(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            import btopline\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\").lstrip()\n        )\n        source.add_imports([\n            \"import atopline\"\n        ])\n\n        assert source.contents == dedent(\n            \"\"\"\n            import atopline\n            import btopline\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\"\n        ).lstrip()\n\n\n\n\nclass LineFindingTests(unittest.TestCase):\n\n    def test_finding_start_line(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            stuff\n            things\n            bla\n            bla bla\n                indented\n            more\n            then end\n            \"\"\").lstrip()\n        )\n\n        assert source.find_start_line(['stuff', 'whatever']) == 0\n        assert source.find_start_line(['bla bla', 'whatever']) == 3\n        assert source.find_start_line(['indented', 'whatever']) == 4\n        assert source.find_start_line(['    indented', 'whatever']) == 4\n        assert source.find_start_line(['no such line', 'whatever']) == None\n        with self.assertRaises(SourceUpdateError):\n            source.find_start_line([''])\n        with self.assertRaises(SourceUpdateError):\n            source.find_start_line([])\n\n\n    def test_finding_end_line(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            stuff\n            things\n            bla\n                bla bla\n                indented\n                more\n            then end\n            \"\"\").lstrip()\n        )\n\n        assert source.find_end_line(['stuff', 'things']) == 1\n        assert source.find_end_line(['bla bla', 'whatever', 'more']) == 5\n        assert source.find_end_line(['bla bla', 'whatever']) == None\n        assert source.find_end_line(['no such line', 'whatever']) == None\n        with self.assertRaises(SourceUpdateError):\n            source.find_end_line([])\n        with self.assertRaises(SourceUpdateError):\n            source.find_end_line(['whatever',''])\n\n\n    def test_finding_end_line_depends_on_start(self):\n        source = Source._from_contents(dedent(\n            \"\"\"\n            stuff\n            things\n            bla\n\n            more stuff\n            things\n            bla\n            then end\n            \"\"\").lstrip()\n        )\n\n        assert source.find_end_line(['more stuff', 'things', 'bla']) == 6\n\n\n\nclass SourceUpdateTest(unittest.TestCase):\n\n    def test_update_with_empty_contents(self):\n        s = Source()\n        s.update('new stuff\\n')\n        self.assertEqual(s.get_updated_contents(), 'new stuff\\n')\n\n    def test_adds_final_newline_if_necessary(self):\n        s = Source()\n        s.update('new stuff')\n        self.assertEqual(s.get_updated_contents(), 'new stuff\\n')\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_sourcetree.py",
    "content": "import os\nimport subprocess\nimport unittest\nfrom textwrap import dedent\n\nimport pytest\nfrom book_parser import CodeListing\nfrom sourcetree import (\n    ApplyCommitException,\n    Commit,\n    SourceTree,\n    check_chunks_against_future_contents,\n    get_offset,\n    strip_comments,\n)\n\n\nclass GetFileTest(unittest.TestCase):\n    def test_get_contents(self):\n        sourcetree = SourceTree()\n        (sourcetree.tempdir / \"foo.txt\").write_text(\"bla bla\")\n        assert sourcetree.get_contents(\"foo.txt\") == \"bla bla\"\n\n\nclass StripCommentTest(unittest.TestCase):\n    def test_strips_python_comments(self):\n        assert strip_comments(\"foo #\") == \"foo\"\n        assert strip_comments(\"foo  #\") == \"foo\"\n\n    def test_strips_js_comments(self):\n        assert strip_comments(\"foo //\") == \"foo\"\n        assert strip_comments(\"foo  //\") == \"foo\"\n\n    def test_doesnt_break_comment_lines(self):\n        assert strip_comments(\"# normal comment\") == \"# normal comment\"\n\n    def test_doesnt_break_trailing_slashes(self):\n        assert strip_comments(\"a_url/\") == \"a_url/\"\n\n\nclass StartWithCheckoutTest(unittest.TestCase):\n    def test_get_local_repo_path(self):\n        sourcetree = SourceTree()\n        assert sourcetree.get_local_repo_path(\"chapter_name\") == os.path.abspath(\n            os.path.join(os.path.dirname(__file__), \"../source/chapter_name/superlists\")\n        )\n\n    def test_checks_out_repo_chapter_as_main(self):\n        sourcetree = SourceTree()\n        sourcetree.get_local_repo_path = lambda c: os.path.abspath(\n            os.path.join(os.path.dirname(__file__), \"testrepo\")\n        )\n        sourcetree.start_with_checkout(\"chapter_17\", \"chapter_16\")\n        remotes = sourcetree.run_command(\"git remote\").split()\n        assert remotes == [\"repo\"]\n        branch = sourcetree.run_command(\"git branch\").strip()\n        assert branch == \"* main\"\n        diff = sourcetree.run_command(\"git diff repo/chapter_16\").strip()\n        assert diff == \"\"\n\n\nclass ApplyFromGitRefTest(unittest.TestCase):\n    def setUp(self):\n        self.sourcetree = SourceTree()\n        self.sourcetree.get_local_repo_path = lambda c: os.path.abspath(\n            os.path.join(os.path.dirname(__file__), \"testrepo\")\n        )\n        self.sourcetree.start_with_checkout(\"chapter_17\", \"chapter_16\")\n        self.sourcetree.run_command(\"git switch test-start\")\n        self.sourcetree.run_command(\"git reset\")\n\n    def test_from_real_git_stuff(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n        assert (self.sourcetree.tempdir / \"file1.txt\").read_text() == dedent(\n            \"\"\"\n            file 1 line 1\n            file 1 line 2 amended\n            file 1 line 3\n            \"\"\"\n        ).lstrip()\n\n        assert listing.was_written\n\n    def test_leaves_staging_empty(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n        staged = self.sourcetree.run_command(\"git diff --staged\")\n        assert staged == \"\"\n\n    def test_raises_if_wrong_file(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 1\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def _checkout_commit(self, commit):\n        commit_spec = self.sourcetree.get_commit_spec(commit)\n        self.sourcetree.run_command(\"git checkout \" + commit_spec)\n        self.sourcetree.run_command(\"git reset\")\n\n    def test_raises_if_too_many_files_in_commit(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 1\n                file 1 line 2\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l023\"\n\n        self._checkout_commit(\"ch17l022\")\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_raises_if_listing_doesnt_show_all_new_lines_in_diff(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_raises_if_listing_lines_in_wrong_order(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 3\n                file 1 line 2 amended\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_line_ordering_check_isnt_confused_by_dupe_lines(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                another line changed\n                some duplicate lines coming up...\n\n                hello\n                goodbye\n                hello\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l027\"\n        self._checkout_commit(\"ch17l026\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_line_ordering_check_isnt_confused_by_new_lines_that_dupe_existing(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                some duplicate lines coming up...\n\n                hello\n\n                one more line at end\n                add a line with a dupe of existing\n                hello\n                goodbye\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l031\"\n        self._checkout_commit(\"ch17l030\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def DONTtest_non_dupes_are_still_order_checked(self):\n        # TODO: get this working\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                some duplicate lines coming up...\n\n                hello\n\n                one more line at end\n                add a line with a dupe of existing\n                goodbye\n                hello\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l031\"\n        self._checkout_commit(\"ch17l030\")\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_raises_if_any_other_listing_lines_not_in_before_version(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                what is this?\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_lines_in_before_and_after_version(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                file 2 line 1 changed\n                [...]\n\n                hello\n                hello\n\n                one more line at end\n            \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l028\"\n        self._checkout_commit(\"ch17l027\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_raises_if_listing_line_not_in_after_version(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                hello\n                goodbye\n                hello\n\n                one more line at end\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l028\"\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_lines_from_just_before_diff(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                file 1 line 1\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_listings_showing_a_move_mean_can_ignore_commit_lines_added_and_removed(\n        self,\n    ):\n        listing = CodeListing(\n            filename=\"pythonfile.py\",\n            contents=dedent(\n                \"\"\"\n                class NuKlass(object):\n\n                    def method1(self):\n                        [...]\n                        a = a + 3\n                        [...]\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l029\"\n        self._checkout_commit(\"ch17l028-1\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_listings_showing_a_move_mean_can_ignore_commit_lines_added_and_removed_2(\n        self,\n    ):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                hello\n\n                one more line at end\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l030\"\n        self._checkout_commit(\"ch17l029\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_elipsis(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                [...]\n                file 1 line 2 amended\n                file 1 line 3\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def DONTtest_listings_must_use_elipsis_to_indicate_skipped_lines(self):\n        # TODO!\n        lines = [\n            \"file 1 line 1\",\n            \"file 1 line 2 amended\",\n            \"file 1 line 3\",\n            \"file 1 line 4 inserted\",\n            \"another line\",\n        ]\n        listing = CodeListing(filename=\"file1.txt\", contents=\"\")\n        listing.commit_ref = \"ch17l022\"\n        self._checkout_commit(\"ch17l021\")\n\n        listing.contents = \"\\n\".join(lines)\n        self.sourcetree.apply_listing_from_commit(listing)  # should not raise\n\n        lines[1] = \"[...]\"\n        listing.contents = \"\\n\".join(lines)\n        self.sourcetree.apply_listing_from_commit(listing)  # should not raise\n\n        lines.pop(1)\n        listing.contents = \"\\n\".join(lines)\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_python_callouts(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                [...]\n                file 1 line 2 amended  #\n                file 1 line 3  #\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_js_callouts(self):\n        listing = CodeListing(\n            filename=\"file1.txt\",\n            contents=dedent(\n                \"\"\"\n                [...]\n                file 1 line 2 amended  //\n                file 1 line 3  //\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l021\"\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_happy_with_blank_lines(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                file 2 line 1 changed\n\n                another line changed\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l024\"\n        self._checkout_commit(\"ch17l023\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_handles_indents(self):\n        listing = CodeListing(\n            filename=\"pythonfile.py\",\n            contents=dedent(\n                \"\"\"\n                def method1(self):\n                    # amend method 1\n                    return 2\n\n                [...]\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l026\"\n        self._checkout_commit(\"ch17l025\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_over_indentation_differences_are_picked_up(self):\n        listing = CodeListing(\n            filename=\"pythonfile.py\",\n            contents=dedent(\n                \"\"\"\n                def method1(self):\n                        # amend method 1\n                    return 2\n\n                [...]\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l026\"\n        self._checkout_commit(\"ch17l025\")\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_under_indentation_differences_are_picked_up(self):\n        listing = CodeListing(\n            filename=\"pythonfile.py\",\n            contents=dedent(\n                \"\"\"\n                def method1(self):\n                # amend method 1\n                return 2\n\n                [...]\n                \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l026\"\n        self._checkout_commit(\"ch17l025\")\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_with_diff_listing_passing_case(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                diff --git a/file2.txt b/file2.txt\n                index 93f054e..519d518 100644\n                --- a/file2.txt\n                +++ b/file2.txt\n                @@ -4,6 +4,5 @@ another line changed\n                 some duplicate lines coming up...\n\n                 hello\n                -hello\n\n                 one more line at end\n                 \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l030\"\n        self._checkout_commit(\"ch17l029\")\n\n        self.sourcetree.apply_listing_from_commit(listing)\n\n    def test_with_diff_listing_failure_case(self):\n        listing = CodeListing(\n            filename=\"file2.txt\",\n            contents=dedent(\n                \"\"\"\n                diff --git a/file2.txt b/file2.txt\n                index 93f054e..519d518 100644\n                --- a/file2.txt\n                +++ b/file2.txt\n                @@ -4,6 +4,5 @@ another line changed\n                 some duplicate lines coming up...\n\n                 hello\n                -hello\n                +something else\n\n                 one more line at end\n                 \"\"\"\n            ).lstrip(),\n        )\n        listing.commit_ref = \"ch17l030\"\n        self._checkout_commit(\"ch17l029\")\n\n        with self.assertRaises(ApplyCommitException):\n            self.sourcetree.apply_listing_from_commit(listing)\n\n\nclass SourceTreeRunCommandTest(unittest.TestCase):\n    def test_running_simple_command(self):\n        sourcetree = SourceTree()\n        sourcetree.run_command(\"touch foo\", cwd=sourcetree.tempdir)\n        assert os.path.exists(os.path.join(sourcetree.tempdir, \"foo\"))\n\n    def test_default_directory_is_tempdir(self):\n        sourcetree = SourceTree()\n        sourcetree.run_command(\"touch foo\")\n        assert os.path.exists(os.path.join(sourcetree.tempdir, \"foo\"))\n\n    def test_returns_output(self):\n        sourcetree = SourceTree()\n        output = sourcetree.run_command(\"echo hello\", cwd=sourcetree.tempdir)\n        assert output == \"hello\\n\"\n\n    def test_raises_on_errors(self):\n        sourcetree = SourceTree()\n        with self.assertRaises(Exception):\n            sourcetree.run_command(\"synt!tax error\", cwd=sourcetree.tempdir)\n        sourcetree.run_command(\n            \"synt!tax error\", cwd=sourcetree.tempdir, ignore_errors=True\n        )\n\n    def test_environment_variables(self):\n        sourcetree = SourceTree()\n        os.environ[\"TEHFOO\"] = \"baz\"\n        output = sourcetree.run_command(\"echo $TEHFOO\", cwd=sourcetree.tempdir)\n        assert output.strip() == \"baz\"\n\n    def test_doesnt_raise_for_some_things_where_a_return_code_is_ok(self):\n        sourcetree = SourceTree()\n        sourcetree.run_command(\"diff foo bar\", cwd=sourcetree.tempdir)\n        sourcetree.run_command(\"python test.py\", cwd=sourcetree.tempdir)\n\n    @pytest.mark.xfail(\n        reason=\"disabled to allow passwordless sudo, see preexec fn in sourcetree.py\"\n    )\n    def test_cleanup_kills_backgrounded_processes_and_rmdirs(self):\n        sourcetree = SourceTree()\n        sourcetree.run_command(\n            'python -c\"import time; time.sleep(5)\" & #runserver', cwd=sourcetree.tempdir\n        )\n        assert len(sourcetree.processes) == 1\n        sourcetree_pid = sourcetree.processes[0].pid\n        pids = (\n            subprocess.check_output(\"pgrep -f time.sleep\", shell=True)\n            .decode(\"utf8\")\n            .split()\n        )\n        print(\"sourcetree_pid\", sourcetree_pid)\n        print(\"pids\", pids)\n        sids = []\n        for pid in reversed(pids):\n            print(\"checking\", pid)\n            cmd = \"ps -o sid --no-header -p %s\" % (pid,)\n            print(cmd)\n            try:\n                sid = subprocess.check_output(cmd, shell=True)\n                print(\"sid\", sid)\n                sids.append(sid)\n                assert sourcetree_pid == int(sid)\n            except subprocess.CalledProcessError:\n                pass\n        assert sids\n\n        sourcetree.cleanup()\n        assert \"time.sleep\" not in subprocess.check_output(\"ps aux\", shell=True).decode(\n            \"utf8\"\n        )\n        # assert not os.path.exists(sourcetree.tempdir)\n\n    def test_running_interactive_command(self):\n        sourcetree = SourceTree()\n\n        command = \"python3 -c \\\"print('input please?'); a = input();print('OK' if a=='yes' else 'NO')\\\"\"\n        output = sourcetree.run_command(command, user_input=\"no\")\n        assert \"NO\" in output\n        output = sourcetree.run_command(command, user_input=\"yes\")\n        assert \"OK\" in output\n\n\nclass CommitTest(unittest.TestCase):\n    def test_init_from_example(self):\n        example = dedent(\n            \"\"\"\n            commit 9ecbb2c2222b9b31ab21e51e42ed8179ec79b273\n            Author: Harry <hjwp2@cantab.net>\n            Date:   Thu Aug 22 20:26:09 2013 +0100\n\n                Some comment text. --ch09l021--\n\n                Conflicts:\n                    lists/tests/test_views.py\n\n            diff --git a/lists/tests/test_views.py b/lists/tests/test_views.py\n            index 8e18d77..03fc675 100644\n            --- a/lists/tests/test_views.py\n            +++ b/lists/tests/test_views.py\n            @@ -55,36 +55,6 @@ class NewListTest(TestCase):\n\n\n\n            -class NewItemTest(TestCase):\n            -\n            -    def test_can_save_a_POST_request_to_an_existing_list(self):\n            -        other_list = List.objects.create()\n            -        correct_list = List.objects.create()\n            -        self.assertEqual(new_item.list, correct_list)\n            -\n            -\n            -    def test_redirects_to_list_view(self):\n            -        other_list = List.objects.create()\n            -        correct_list = List.objects.create()\n            -        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))\n            -\n            -\n            -\n             class ListViewTest(TestCase):\n\n                 def test_list_view_passes_list_to_list_template(self):\n            @@ -112,3 +82,29 @@ class ListViewTest(TestCase):\n                     self.assertNotContains(response, 'other list item 1')\n                     self.assertNotContains(response, 'other list item 2')\n\n            +\n            +    def test_can_save_a_POST_request_to_an_existing_list(self):\n            +        other_list = List.objects.create()\n            +        correct_list = List.objects.create()\n            +        self.assertEqual(new_item.list, correct_list)\n            +\n            +\n            +    def test_POST_redirects_to_list_view(self):\n            +        other_list = List.objects.create()\n            +        correct_list = List.objects.create()\n            +        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))\n            + \"\"\"\n        )\n\n        commit = Commit.from_diff(example)\n\n        assert commit.info == example\n\n        assert commit.lines_to_add == [\n            \"    def test_can_save_a_POST_request_to_an_existing_list(self):\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertEqual(new_item.list, correct_list)\",\n            \"    def test_POST_redirects_to_list_view(self):\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))\",\n        ]\n\n        assert commit.lines_to_remove == [\n            \"class NewItemTest(TestCase):\",\n            \"    def test_can_save_a_POST_request_to_an_existing_list(self):\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertEqual(new_item.list, correct_list)\",\n            \"    def test_redirects_to_list_view(self):\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))\",\n        ]\n\n        assert commit.moved_lines == [\n            \"    def test_can_save_a_POST_request_to_an_existing_list(self):\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertEqual(new_item.list, correct_list)\",\n            \"        other_list = List.objects.create()\",\n            \"        correct_list = List.objects.create()\",\n            \"        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))\",\n        ]\n\n        assert commit.deleted_lines == [\n            \"class NewItemTest(TestCase):\",\n            \"    def test_redirects_to_list_view(self):\",\n        ]\n\n        assert commit.new_lines == [\n            \"    def test_POST_redirects_to_list_view(self):\",\n        ]\n\n\nclass CheckChunksTest(unittest.TestCase):\n    def test_get_offset_when_none(self):\n        lines = [\n            \"def method1(self):\",\n            \"    return 2\",\n        ]\n        future_lines = [\n            \"def method1(self):\",\n            \"    return 2\",\n        ]\n        assert get_offset(lines, future_lines) == \"\"\n\n    def test_get_offset_when_some(self):\n        lines = [\n            \"\",\n            \"def method1(self):\",\n            \"    return 2\",\n        ]\n        future_lines = [\n            \"    \",\n            \"    def method1(self):\",\n            \"        return 2\",\n        ]\n        assert get_offset(lines, future_lines) == \"    \"\n\n    def test_passing_case(self):\n        code = dedent(\n            \"\"\"\n            def method1(self):\n                # amend method 1\n                return 2\n            \"\"\"\n        ).strip()\n        future_contents = dedent(\n            \"\"\"\n            class Thing:\n                def method1(self):\n                    # amend method 1\n                    return 2\n            \"\"\"\n        ).strip()\n        check_chunks_against_future_contents(code, future_contents)  # should not raise\n\n    def test_over_indentation_differences_are_picked_up(self):\n        code = dedent(\n            \"\"\"\n            def method1(self):\n                    # amend method 1\n                return 2\n            \"\"\"\n        ).strip()\n        future_contents = dedent(\n            \"\"\"\n            class Thing:\n                def method1(self):\n                    # amend method 1\n                    return 2\n            \"\"\"\n        ).strip()\n        with self.assertRaises(ApplyCommitException):\n            check_chunks_against_future_contents(code, future_contents)\n\n    def test_under_indentation_differences_are_picked_up(self):\n        code = dedent(\n            \"\"\"\n            def method1(self):\n            # amend method 1\n                return 2\n            \"\"\"\n        ).strip()\n        future_contents = dedent(\n            \"\"\"\n            class Thing:\n                def method1(self):\n                    # amend method 1\n                    return 2\n            \"\"\"\n        ).strip()\n        with self.assertRaises(ApplyCommitException):\n            check_chunks_against_future_contents(code, future_contents)\n\n    def test_leading_blank_lines_in_listing_are_ignored(self):\n        code = dedent(\n            \"\"\"\n            def method1(self):\n                # amend method 1\n                return 2\n\n            \"\"\"\n        )\n        future_contents = dedent(\n            \"\"\"\n            class Thing:\n                def method1(self):\n                    # amend method 1\n                    return 2\n            \"\"\"\n        ).strip()\n        check_chunks_against_future_contents(code, future_contents)  # should not raise\n\n    def test_thing(self):\n        code = dedent(\n            \"\"\"\n            [...]\n            item.save()\n\n            return render(\n                request,\n                \"home.html\",\n                {\"new_item_text\": item.text},\n            )\n            \"\"\"\n        )\n        future_contents = dedent(\n            \"\"\"\n            from django.shortcuts import render\n            from lists.models import Item\n\n\n            def home_page(request):\n                item = Item()\n                item.text = request.POST.get(\"item_text\", \"\")\n                item.save()\n\n                return render(\n                    request,\n                    \"home.html\",\n                    {\"new_item_text\": item.text},\n                )\n            \"\"\"\n        ).strip()\n        check_chunks_against_future_contents(code, future_contents)  # should not raise\n\n    def test_trailing_blank_lines_in_listing_are_ignored(self):\n        code = dedent(\n            \"\"\"\n            def method1(self):\n                # amend method 1\n                return 2\n\n            \"\"\"\n        ).lstrip()\n        future_contents = dedent(\n            \"\"\"\n            class Thing:\n                def method1(self):\n                    # amend method 1\n                    return 2\n            \"\"\"\n        ).strip()\n        check_chunks_against_future_contents(code, future_contents)  # should not raise\n\n    def test_elipsis_lines_are_ignored(self):\n        lines = dedent(\n            \"\"\"\n            def method1(self):\n                # amend method 1\n                return 2\n            [...]\n            \"\"\"\n        ).strip()\n        future_lines = dedent(\n            \"\"\"\n                def method1(self):\n                    # amend method 1\n                    return 2\n            stuff\n            \"\"\"\n        ).rstrip()\n        check_chunks_against_future_contents(lines, future_lines)  # should not raise\n"
  },
  {
    "path": "tests/test_write_to_file.py",
    "content": "#!/usr/bin/env python3\nimport unittest\nimport os\nimport shutil\nfrom textwrap import dedent\nimport tempfile\n\nfrom book_tester import CodeListing\n\nfrom write_to_file import (\n    _find_last_line_for_class,\n    number_of_identical_chars,\n    write_to_file,\n)\n\n\nclass ClassFinderTest(unittest.TestCase):\n\n    def test_find_last_line_for_class(self):\n        source = dedent(\n            \"\"\"\n            import topline\n\n            class ClassA(object):\n                def metha(self):\n                    pass\n\n                def metha2(self):\n                    pass\n\n            class ClassB(object):\n                def methb(self):\n                    pass\n            \"\"\"\n        )\n\n        lineno = _find_last_line_for_class(source, 'ClassA')\n        self.assertEqual(lineno, 9)\n        # sanity-check\n        self.assertEqual(source.split('\\n')[lineno - 1].strip(), 'pass')\n\n        lineno = _find_last_line_for_class(source, 'ClassB')\n        self.assertEqual(lineno, 13)\n\n\n\n\nclass LineFinderTest(unittest.TestCase):\n\n    def test_number_of_identical_chars(self):\n        self.assertEqual(\n            number_of_identical_chars('1234', '5678'),\n            0\n        )\n        self.assertEqual(\n            number_of_identical_chars('1234', '1235'),\n            3\n        )\n        self.assertEqual(\n            number_of_identical_chars('1234', '1243'),\n            2\n        )\n        self.assertEqual(\n            number_of_identical_chars('12345', '123WHATEVER45'),\n            5\n        )\n\n\n\nclass WriteToFileTest(unittest.TestCase):\n    maxDiff = None\n\n    def setUp(self):\n        self.tempdir = tempfile.mkdtemp()\n\n    def tearDown(self):\n        shutil.rmtree(self.tempdir)\n\n    def test_simple_case(self):\n        #done\n        listing = CodeListing(filename='foo.py', contents='abc\\ndef')\n        write_to_file(listing, self.tempdir)\n        with open(os.path.join(self.tempdir, listing.filename)) as f:\n            self.assertEqual(f.read(), listing.contents + '\\n')\n        self.assertTrue(listing.was_written)\n\n    def test_multiple_files(self):\n        listing = CodeListing(filename='foo.py, bar.py', contents='abc\\ndef')\n        write_to_file(listing, self.tempdir)\n        with open(os.path.join(self.tempdir, 'foo.py')) as f:\n            self.assertEqual(f.read(), listing.contents + '\\n')\n        with open(os.path.join(self.tempdir, 'bar.py')) as f:\n            self.assertEqual(f.read(), listing.contents + '\\n')\n        self.assertTrue(listing.was_written)\n\n\n    def assert_write_to_file_gives(\n        self, old_contents, new_contents, expected_contents\n    ):\n        listing = CodeListing(filename='foo.py', contents=new_contents)\n        with open(os.path.join(self.tempdir, 'foo.py'), 'w') as f:\n            f.write(old_contents)\n\n        write_to_file(listing, self.tempdir)\n\n        with open(os.path.join(self.tempdir, listing.filename)) as f:\n            actual = f.read()\n            self.assertMultiLineEqual(actual, expected_contents)\n\n\n    def test_strips_python_line_callouts_one_space(self):\n        contents = 'hello\\nbla #\\nstuff'\n        self.assert_write_to_file_gives('', contents, 'hello\\nbla\\nstuff\\n')\n\n    def test_strips_python_line_callouts_two_spaces(self):\n        contents = 'hello\\nbla  #\\nstuff'\n        self.assert_write_to_file_gives('', contents, 'hello\\nbla\\nstuff\\n')\n\n\n    def test_strips_js_line_callouts(self):\n        contents = 'hello\\nbla //\\nstuff'\n        self.assert_write_to_file_gives('', contents, 'hello\\nbla\\nstuff\\n')\n        contents = 'hello\\nbla  //'\n        self.assert_write_to_file_gives('', contents, 'hello\\nbla\\n')\n\n\n    def test_doesnt_mess_with_multiple_newlines(self):\n        contents = 'hello\\n\\n\\nbla'\n        self.assert_write_to_file_gives('', contents, 'hello\\n\\n\\nbla\\n')\n\n\n    def test_existing_file_bears_no_relation_means_replaced(self):\n        old = '#abc\\n#def\\n#ghi\\n#jkl\\n'\n        new = '#mno\\n#pqr\\n#stu\\n#vvv\\n'\n        expected = new\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_existing_file_has_views_means_apppend(self):\n        old = dedent(\n            \"\"\"\n            from django.stuff import things\n\n            def a_view(request, param):\n                pass\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            def another_view(request):\n                pass\n            \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            from django.stuff import things\n\n            def a_view(request, param):\n                pass\n\n\n            def another_view(request):\n                pass\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_existing_file_has_single_class_means_replace(self):\n        old = dedent(\n            \"\"\"\n            class Jimmy(object):\n                pass\n            \"\"\").lstrip()\n        new = dedent(\n            \"\"\"\n            class Carruthers(object):\n                pass\n            \"\"\").strip()\n\n        expected = dedent(\n            \"\"\"\n            class Carruthers(object):\n                pass\n            \"\"\").lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_existing_file_has_multiple_classes_means_append(self):\n        old = dedent(\n            \"\"\"\n            import apples\n            import pears\n\n            class Jimmy(object):\n                pass\n\n\n\n            class Bob(object):\n                pass\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            class Carruthers(object):\n                pass\n            \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            import apples\n            import pears\n\n            class Jimmy(object):\n                pass\n\n\n\n            class Bob(object):\n                pass\n\n\n\n            class Carruthers(object):\n                pass\n            \"\"\").lstrip()\n\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n\n    def test_leading_elipsis_is_ignored(self):\n        old = dedent(\n            \"\"\"\n            class C():\n                def foo():\n                    # bla 1\n                    # bla 2\n                    # bla 3\n                    # bla 4\n                    return 1\n\n            # the end\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            [...]\n            # bla 2\n            # bla 3b\n            # bla 4b\n            return 1\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            class C():\n                def foo():\n                    # bla 1\n                    # bla 2\n                    # bla 3b\n                    # bla 4b\n                    return 1\n\n            # the end\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_adding_import_at_top_then_elipsis_then_modified_stuff(self):\n        old = dedent(\n            \"\"\"\n            import topline\n            # some stuff\n\n            class C():\n                def foo():\n                    return 1\n            \"\"\"\n        )\n        new = dedent(\n            \"\"\"\n            import newtopline\n            [...]\n\n                def foo():\n                    return 2\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import newtopline\n            import topline\n\n            # some stuff\n\n            class C():\n                def foo():\n                    return 2\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def DONTtest_adding_import_at_top_without_elipsis_then_modified_stuff(self):\n        old = dedent(\n            \"\"\"\n            import anoldthing\n            import bthing\n            import cthing\n\n            class C(cthing.Bar):\n                def foo():\n                    return 1\n\n                    # more stuff...\n            \"\"\"\n        )\n        new = dedent(\n            \"\"\"\n            import anewthing\n            import bthing\n            import cthing\n\n            class C(anewthing.Baz):\n                def foo():\n                    [...]\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import anewthing\n            import bthing\n            import cthing\n\n            class C(anewthing.Baz):\n                def foo():\n                    return 1\n\n                    # more stuff...\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_adding_import_at_top_then_elipsis_then_totally_new_stuff(self):\n        old = dedent(\n            \"\"\"\n            import topline\n\n            # some stuff\n            class C():\n                pass\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            import newtopline\n            [...]\n\n            class Nu():\n                pass\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import newtopline\n            import topline\n\n            # some stuff\n            class C():\n                pass\n\n\n\n            class Nu():\n                pass\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_elipsis_indicating_which_class_to_add_new_method_to(self):\n        old = dedent(\n            \"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n\n            class B(object):\n                def methb(self):\n                    pass\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            class A(object):\n                [...]\n\n                def metha2(self):\n                    pass\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import topline\n\n            class A(object):\n                def metha(self):\n                    pass\n\n\n                def metha2(self):\n                    pass\n\n            class B(object):\n                def methb(self):\n                    pass\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_adding_import_at_top_sorts_alphabetically_respecting_django_and_locals(self):\n        old = dedent(\n            \"\"\"\n            import atopline\n\n            from django.monkeys import monkeys\n            from django.chickens import chickens\n\n            from lists.views import thing\n\n            # some stuff\n            class C():\n                def foo():\n                    return 1\n            \"\"\")\n        new = dedent(\n            \"\"\"\n            import btopline\n            [...]\n\n                def foo():\n                    return 2\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import atopline\n            import btopline\n\n            from django.chickens import chickens\n            from django.monkeys import monkeys\n\n            from lists.views import thing\n\n            # some stuff\n            class C():\n                def foo():\n                    return 2\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n        new = dedent(\n            \"\"\"\n            from django.dickens import dickens\n            [...]\n\n                def foo():\n                    return 2\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import atopline\n\n            from django.chickens import chickens\n            from django.dickens import dickens\n            from django.monkeys import monkeys\n\n            from lists.views import thing\n\n            # some stuff\n            class C():\n                def foo():\n                    return 2\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n        new = dedent(\n            \"\"\"\n            from lists.zoos import thing\n            [...]\n\n                def foo():\n                    return 2\n            \"\"\"\n        )\n        expected = dedent(\n            \"\"\"\n            import atopline\n\n            from django.chickens import chickens\n            from django.monkeys import monkeys\n\n            from lists.views import thing\n            from lists.zoos import thing\n\n            # some stuff\n            class C():\n                def foo():\n                    return 2\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_new_contents_then_indented_elipsis_then_appendix(self):\n        old = '#abc\\n#def\\n#ghi\\n#jkl\\n'\n        new = (\n            '#abc\\n'\n            'def foo(v):\\n'\n            '    return v + 1\\n'\n            '    #def\\n'\n            '    [... old stuff as before]\\n'\n            '# then add this'\n        )\n        expected = (\n            '#abc\\n'\n            'def foo(v):\\n'\n            '    return v + 1\\n'\n            '    #def\\n'\n            '    #ghi\\n'\n            '    #jkl\\n'\n            '\\n'\n            '# then add this\\n'\n        )\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_for_existing_file_replaces_matching_lines(self):\n        old = dedent(\n            \"\"\"\n            class Foo(object):\n                def method_1(self):\n                    return 1\n\n                def method_2(self):\n                    # two\n                    return 2\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n                def method_2(self):\n                    # two\n                    return 'two'\n                \"\"\"\n        ).strip()\n        expected = dedent(\n            \"\"\"\n            class Foo(object):\n                def method_1(self):\n                    return 1\n\n                def method_2(self):\n                    # two\n                    return 'two'\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_for_existing_file_doesnt_swallow_whitespace(self):\n        old = dedent(\n            \"\"\"\n            one = (\n                1,\n            )\n\n            two = (\n                2,\n            )\n\n            three = (\n                3,\n            )\n            \"\"\").lstrip()\n        new = dedent(\n            \"\"\"\n            two = (\n                2,\n                #two\n            )\n            \"\"\"\n        ).strip()\n\n\n        expected = dedent(\n            \"\"\"\n            one = (\n                1,\n            )\n\n            two = (\n                2,\n                #two\n            )\n\n            three = (\n                3,\n            )\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_longer_new_file_starts_replacing_from_first_different_line(self):\n        old = dedent(\n            \"\"\"\n            # line 1\n            # line 2\n            # line 3\n\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            # line 1\n            # line 2\n\n            # line 3\n\n            # line 4\n            \"\"\"\n        ).strip()\n        expected = dedent(\n            \"\"\"\n            # line 1\n            # line 2\n\n            # line 3\n\n            # line 4\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_changing_the_end_of_a_method(self):\n        old = dedent(\n            \"\"\"\n            class A(object):\n                def method1(self):\n                    # do step 1\n                    # do step 2\n                    # do step 3\n                    # do step 4\n                    # do step 5\n                    pass\n\n                def method2(self):\n                    # do stuff\n                    pass\n            \"\"\"\n        ).lstrip()\n        new = dedent(\n            \"\"\"\n            def method1(self):\n                # do step 1\n                # do step 2\n                # do step A\n                # do step B\n            \"\"\"\n        ).strip()\n        expected = dedent(\n            \"\"\"\n            class A(object):\n                def method1(self):\n                    # do step 1\n                    # do step 2\n                    # do step A\n                    # do step B\n\n                def method2(self):\n                    # do stuff\n                    pass\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_for_existing_file_inserting_new_lines_between_comments(self):\n        old = dedent(\n            \"\"\"\n            # test 1\n            a = foo()\n            assert  a == 1\n\n            if a:\n                # test 2\n                self.fail('finish me')\n\n                # test 3\n\n                # the end\n            # is here\n            \"\"\").lstrip()\n        new = dedent(\n            \"\"\"\n            # test 2\n            b = bar()\n            assert b == 2\n\n            # test 3\n            assert True\n            self.fail('finish me')\n\n            # the end\n            [...]\n            \"\"\"\n        ).lstrip()\n\n        expected = dedent(\n            \"\"\"\n            # test 1\n            a = foo()\n            assert  a == 1\n\n            if a:\n                # test 2\n                b = bar()\n                assert b == 2\n\n                # test 3\n                assert True\n                self.fail('finish me')\n\n                # the end\n            # is here\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_single_line_replacement(self):\n        old = dedent(\n            \"\"\"\n            def wiggle():\n                abc def\n                abcd fghi\n                jkl mno\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n            abcd abcd\n            \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            def wiggle():\n                abc def\n                abcd abcd\n                jkl mno\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_single_line_replacement_finds_most_probable_line(self):\n        old = dedent(\n            \"\"\"\n            abc\n            abc daf ghi\n            abc dex xyz\n            jkl mno\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n            abc deFFF ghi\n            \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            abc\n            abc deFFF ghi\n            abc dex xyz\n            jkl mno\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_single_line_assertion_replacement(self):\n        old = dedent(\n            \"\"\"\n            class Wibble(unittest.TestCase):\n\n                def test_number_1(self):\n                    self.assertEqual(1 + 1, 2)\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n                self.assertEqual(1 + 1, 3)\n                \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            class Wibble(unittest.TestCase):\n\n                def test_number_1(self):\n                    self.assertEqual(1 + 1, 3)\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_single_line_assertion_replacement_finds_right_one(self):\n        old = dedent(\n            \"\"\"\n            class Wibble(unittest.TestCase):\n\n                def test_number_1(self):\n                    self.assertEqual(1 + 1, 2)\n\n                def test_number_2(self):\n                    self.assertEqual(1 + 2, 3)\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n                self.assertEqual(1 + 2, 4)\n                \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            class Wibble(unittest.TestCase):\n\n                def test_number_1(self):\n                    self.assertEqual(1 + 1, 2)\n\n                def test_number_2(self):\n                    self.assertEqual(1 + 2, 4)\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_single_line_assertion_replacement_real_views_example(self):\n        old = dedent(\n            \"\"\"\n            from lists.models import Item, List\n\n            def home_page(request):\n                return render(request, 'home.html')\n\n\n            def view_list(request, list_id):\n                list = List.objects.get(id=list_id)\n                items = Item.objects.filter(list=list)\n                return render(request, 'list.html', {'items': items})\n\n\n            def new_list(request):\n                list = List.objects.create()\n                Item.objects.create(text=request.POST['item_text'], list=list)\n                return redirect('/lists/%d/' % (list.id,))\n\n\n            def add_item(request):\n                pass\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n            def add_item(request, list_id):\n                pass\n            \"\"\"\n        )\n\n        expected = dedent(\n            \"\"\"\n            from lists.models import Item, List\n\n            def home_page(request):\n                return render(request, 'home.html')\n\n\n            def view_list(request, list_id):\n                list = List.objects.get(id=list_id)\n                items = Item.objects.filter(list=list)\n                return render(request, 'list.html', {'items': items})\n\n\n            def new_list(request):\n                list = List.objects.create()\n                Item.objects.create(text=request.POST['item_text'], list=list)\n                return redirect('/lists/%d/' % (list.id,))\n\n\n            def add_item(request, list_id):\n                pass\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_changing_function_signature_and_stripping_comment(self):\n        old = dedent(\n            \"\"\"\n            # stuff\n\n            def foo():\n                pass\n            \"\"\"\n        ).lstrip()\n\n        new = dedent(\n            \"\"\"\n            def foo(bar):\n                pass\n            \"\"\"\n        ).strip()\n\n        expected = new + '\\n'\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_with_two_elipsis_dedented_change(self):\n        old = dedent(\n            \"\"\"\n            class Wibble(object):\n\n                def foo(self):\n                    return 2\n\n                def bar(self):\n                    return 3\n            \"\"\").lstrip()\n\n        new = dedent(\n            \"\"\"\n                [...]\n                def foo(self):\n                    return 4\n\n                def bar(self):\n                [...]\n                \"\"\"\n        ).strip()\n\n        expected = dedent(\n            \"\"\"\n            class Wibble(object):\n\n                def foo(self):\n                    return 4\n\n                def bar(self):\n                    return 3\n            \"\"\"\n        ).lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n    def test_indents_in_new_dont_confuse_things(self):\n        old = dedent(\n            \"\"\"\n            class Wibble():\n                def foo(self):\n                    # comment 1\n                    do something\n                    # comment 2\n                    do something else\n                    and keep going\n            \"\"\").lstrip()\n\n        new = (\n            \"    # comment 2\\n\"\n            \"    time.sleep(2)\\n\"\n            \"    do something else\\n\"\n        )\n\n        expected = dedent(\n            \"\"\"\n            class Wibble():\n                def foo(self):\n                    # comment 1\n                    do something\n                    # comment 2\n                    time.sleep(2)\n                    do something else\n                    and keep going\n            \"\"\").lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n    def test_double_indents_in_new_dont_confuse_things(self):\n        old = dedent(\n            \"\"\"\n            class Wibble():\n                def foo(self):\n                    if something:\n                        do something\n                # end of class\n            \"\"\").lstrip()\n\n        new = dedent(\n            \"\"\"\n                if something:\n                    do something else\n            # end of class\n            \"\"\")\n\n        expected = dedent(\n            \"\"\"\n            class Wibble():\n                def foo(self):\n                    if something:\n                        do something else\n                # end of class\n            \"\"\").lstrip()\n        self.assert_write_to_file_gives(old, new, expected)\n\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/update_source_repo.py",
    "content": "#!/usr/bin/env python\nimport getpass\nimport os\nimport subprocess\nfrom pathlib import Path\n\nfrom chapters import CHAPTERS\n\nREMOTE = \"local\" if \"harry\" in getpass.getuser() else \"origin\"\nBASE_FOLDER = Path(__file__).parent.parent\n\n\ndef fetch_if_possible(target_dir: Path):\n    fetch = subprocess.Popen(\n        [\"git\", \"fetch\", REMOTE],\n        cwd=target_dir,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n    )\n    stdout, stderr = fetch.communicate()\n    print(stdout.decode(), stderr.decode())\n    if fetch.returncode:\n        if (\n            \"Name or service not known\" in stderr.decode()\n            or \"Could not resolve\" in stderr.decode()\n            or \"github.com port 22: Undefined error\" in stderr.decode()\n        ):\n            # no internet\n            print(\"No Internet\")\n            return False\n        raise Exception(\"Error running git fetch\")\n    return True\n\n\ndef update_sources_for_chapter(chapter, previous_chapter=None):\n    source_dir = BASE_FOLDER / \"source\" / chapter / \"superlists\"\n    if not source_dir.exists():\n        print(\"No folder at\", source_dir, \"skipping...\")\n        return\n    print(\"updating\", source_dir)\n    fetch_if_possible(source_dir)\n\n    subprocess.check_output([\"git\", \"submodule\", \"update\", str(source_dir)])\n    commit_specified_by_submodule = (\n        subprocess.check_output([\"git\", \"log\", \"-n 1\", \"--format=%H\"], cwd=source_dir)\n        .decode()\n        .strip()\n    )\n\n    if previous_chapter is not None:\n        # make sure branch for previous chapter is available to start tests\n        prev_chap_source_dir = BASE_FOLDER / \"source\" / previous_chapter / \"superlists\"\n\n        subprocess.check_output(\n            [\"git\", \"checkout\", str(previous_chapter)], cwd=source_dir\n        )\n        # we use the submodule commit,\n        # as specfified in the previous chapter source/x dir\n        prev_chap_commit_specified_by_submodule = (\n            subprocess.check_output(\n                [\"git\", \"log\", \"-n 1\", \"--format=%H\"], cwd=prev_chap_source_dir\n            )\n            .decode()\n            .strip()\n        )\n        print(\n            f\"resetting {previous_chapter} branch to {prev_chap_commit_specified_by_submodule}\"\n        )\n        subprocess.check_output([\"git\", \"checkout\", previous_chapter], cwd=source_dir)\n        subprocess.check_output(\n            [\"git\", \"reset\", \"--hard\", prev_chap_commit_specified_by_submodule],\n            cwd=source_dir,\n        )\n\n    # check out current branch, local version, for final diff\n    subprocess.check_output([\"git\", \"checkout\", chapter], cwd=source_dir)\n    if os.environ.get(\"CI\"):\n        # if in CI, we use the submodule commit, to check that the submodule\n        # config is up to date\n        print(f\"resetting {chapter} branch to {commit_specified_by_submodule}\")\n        subprocess.check_output(\n            [\"git\", \"reset\", \"--hard\", commit_specified_by_submodule], cwd=source_dir\n        )\n\n\ndef checkout_testrepo_branches():\n    testrepo_dir = BASE_FOLDER / \"tests/testrepo\"\n    for branchname in [\"chapter_16\", \"master\", \"chapter_20\", \"chapter_17\"]:\n        subprocess.check_output([\"git\", \"checkout\", str(branchname)], cwd=testrepo_dir)\n\n\ndef main():\n    \"\"\"\n    update submodule folders for all chapters,\n    making sure previous and current branches are locally checked out\n    \"\"\"\n    if \"SKIP_CHAPTER_SUBMODULES\" not in os.environ:\n        for chapter, previous_chapter in zip(CHAPTERS, [None, *CHAPTERS]):\n            update_sources_for_chapter(chapter, previous_chapter=previous_chapter)\n\n    checkout_testrepo_branches()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tests/write_to_file.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport ast\nimport os\nimport re\nfrom textwrap import dedent\n\nfrom source_updater import (\n    VIEW_FINDER,\n    get_indent,\n    Source,\n)\n\ndef _replace_lines_from_to(old_lines, new_lines, start_pos, end_pos):\n    print('replace lines from line', start_pos, 'to line', end_pos)\n    old_indent = get_indent(old_lines[start_pos])\n    new_indent = get_indent(new_lines[0])\n    if new_indent:\n        missing_indent = old_indent[:-len(new_indent)]\n    else:\n        missing_indent = old_indent\n    indented_new_lines = [missing_indent + l for l in new_lines]\n    return '\\n'.join(\n        old_lines[:start_pos] +\n        indented_new_lines +\n        old_lines[end_pos + 1:]\n    )\n\n\ndef _get_function(source, function_name):\n    functions = [\n        n for n in ast.walk(ast.parse(source))\n        if isinstance(n, ast.FunctionDef)\n    ]\n    try:\n        return next(c for c in functions if c.name == function_name)\n    except StopIteration:\n        raise Exception('Could not find function named %s' % (function_name,))\n\n\n\ndef _replace_lines_from(old_lines, new_lines, start_pos):\n    print('replace lines from line', start_pos)\n    start_line_in_old = old_lines[start_pos]\n    indent = get_indent(start_line_in_old)\n    for ix, new_line in enumerate(new_lines):\n        if len(old_lines) > start_pos + ix:\n            old_lines[start_pos + ix] = indent + new_line\n        else:\n            old_lines.append(indent + new_line)\n    return '\\n'.join(old_lines)\n\n\ndef _number_of_identical_chars_at_beginning(string1, string2):\n    n = 0\n    for char1, char2 in zip(string1, string2):\n        if char1 != char2:\n            return n\n        n += 1\n    return n\n\ndef number_of_identical_chars(string1, string2):\n    string1, string2 = string1.strip(), string2.strip()\n    start_num = _number_of_identical_chars_at_beginning(string1, string2)\n    end_num = _number_of_identical_chars_at_beginning(\n        reversed(string1), reversed(string2)\n    )\n    return min(len(string1), start_num + end_num)\n\n\ndef _replace_single_line(old_lines, new_lines):\n    print('replace single line')\n    new_line = new_lines[0]\n    line_finder = lambda l: number_of_identical_chars(l, new_line)\n    likely_line = sorted(old_lines, key=line_finder)[-1]\n    new_line = get_indent(likely_line) + new_line\n    new_content = '\\n'.join(old_lines).replace(likely_line, new_line)\n    return new_content\n\n\ndef _replace_lines_in(old_lines, new_lines):\n    source = Source._from_contents('\\n'.join(old_lines))\n    if new_lines[0].strip() == '':\n        new_lines.pop(0)\n    new_lines = dedent('\\n'.join(new_lines)).split('\\n')\n    if len(new_lines) == 1:\n        return _replace_single_line(old_lines, new_lines)\n\n    start_pos = source.find_start_line(new_lines)\n    if start_pos is None:\n        print('no start line found')\n        if 'import' in new_lines[0] and 'import' in old_lines[0]:\n            new_contents = new_lines[0] + '\\n'\n            return new_contents + _replace_lines_in(old_lines[1:], new_lines[1:])\n\n        if VIEW_FINDER.match(new_lines[0]):\n            if source.views:\n                view_name = VIEW_FINDER.search(new_lines[0]).group(1)\n                if view_name in source.views:\n                    return source.replace_function(new_lines)\n                return '\\n'.join(old_lines) + '\\n\\n' + '\\n'.join(new_lines)\n\n        class_finder = re.compile(r'^class \\w+\\(.+\\):$', re.MULTILINE)\n        if class_finder.match(new_lines[0]):\n            print('found class in input')\n            if len(source.classes) > 1:\n                print('found classes')\n                return '\\n'.join(old_lines) + '\\n\\n\\n' + '\\n'.join(new_lines)\n\n        return '\\n'.join(new_lines)\n\n    end_pos = source.find_end_line(new_lines)\n    if end_pos is None:\n        if new_lines[0].strip().startswith('def '):\n            return source.replace_function(new_lines)\n\n        else:\n            #TODO: can we get rid of this?\n            return _replace_lines_from(old_lines, new_lines, start_pos)\n\n    else:\n        return _replace_lines_from_to(old_lines, new_lines, start_pos, end_pos)\n\n\n\ndef add_import_and_new_lines(new_lines, old_lines):\n    source = Source._from_contents('\\n'.join(old_lines))\n    print('add import and new lines')\n    source.add_imports(new_lines[:1])\n    lines_with_import = source.get_updated_contents().split('\\n')\n    new_lines_remaining = '\\n'.join(new_lines[2:]).strip('\\n').split('\\n')\n    start_pos = source.find_start_line(new_lines_remaining)\n    if start_pos is None:\n        return '\\n'.join(lines_with_import) + '\\n\\n\\n' + '\\n'.join(new_lines_remaining)\n    else:\n        return _replace_lines_in(lines_with_import, new_lines_remaining)\n\n\ndef _find_last_line_for_class(source, classname):\n    all_nodes = list(ast.walk(ast.parse(source)))\n    classes = [n for n in all_nodes if isinstance(n, ast.ClassDef)]\n    our_class = next(c for c in classes if c.name == classname)\n    last_line_in_our_class = max(\n        getattr(thing, 'lineno', 0) for thing in ast.walk(our_class)\n    )\n    return last_line_in_our_class\n\n\ndef add_to_class(new_lines, old_lines):\n    print('adding to class')\n    source = Source._from_contents('\\n'.join(old_lines))\n    classname = re.search(r'class (\\w+)\\(\\w+\\):', new_lines[0]).group(1)\n    source.add_to_class(classname, new_lines[2:])\n    return source.get_updated_contents()\n\n\n\ndef write_to_file(codelisting, cwd):\n    if ',' in codelisting.filename:\n        files = codelisting.filename.split(', ')\n    else:\n        files = [codelisting.filename]\n    new_contents = codelisting.contents\n    for filename in files:\n        path = os.path.join(cwd, filename)\n        _write_to_file(path, new_contents)\n        #with open(os.path.join(path)) as f:\n        #    print(f.read())\n    codelisting.was_written = True\n\n\ndef _write_to_file(path, new_contents):\n    source = Source.from_path(path)\n    # strip callouts\n    new_contents = re.sub(r' +#$', '', new_contents, flags=re.MULTILINE)\n    new_contents = re.sub(r' +//$', '', new_contents, flags=re.MULTILINE)\n\n    if not os.path.exists(path):\n        dir = os.path.dirname(path)\n        if not os.path.exists(dir):\n            os.makedirs(dir)\n\n    else:\n        old_lines = source.lines\n        new_lines = new_contents.strip('\\n').split('\\n')\n\n        if \"[...\" not in new_contents:\n            new_contents = _replace_lines_in(old_lines, new_lines)\n\n        else:\n            if new_contents.count(\"[...\") == 1:\n                split_line = [l for l in new_lines if \"[...\" in l][0]\n                split_line_pos = new_lines.index(split_line)\n\n                if split_line_pos == 0:\n                    new_contents = _replace_lines_in(old_lines, new_lines[1:])\n\n                elif split_line == new_lines[-1]:\n                    new_contents = _replace_lines_in(old_lines, new_lines[:-1])\n\n                elif split_line_pos == 1:\n                    if 'import' in new_lines[0]:\n                        new_contents = add_import_and_new_lines(new_lines, old_lines)\n                    elif 'class' in new_lines[0]:\n                        new_contents = add_to_class(new_lines, old_lines)\n\n                else:\n                    lines_before = new_lines[:split_line_pos]\n                    last_line_before = lines_before[-1]\n                    lines_after = new_lines[split_line_pos + 1:]\n\n                    last_old_line = [\n                        l for l in old_lines if l.strip() == last_line_before.strip()\n                    ][0]\n                    last_old_line_pos = old_lines.index(last_old_line)\n                    old_lines_after = old_lines[last_old_line_pos + 1:]\n\n                    # special-case: stray browser.quit in chap. 2\n                    if 'rest of comments' in split_line:\n                        assert 'browser.quit()' in [l.strip() for l in old_lines_after]\n                        assert old_lines_after[-2] == 'browser.quit()'\n                        old_lines_after = old_lines_after[:-2]\n\n                    new_contents = '\\n'.join(\n                        lines_before +\n                        [get_indent(split_line) + l for l in old_lines_after] +\n                        lines_after\n                    )\n\n            elif new_contents.strip().startswith(\"[...]\") and new_contents.endswith(\"[...]\"):\n                new_contents = _replace_lines_in(old_lines, new_lines[1:-1])\n            else:\n                raise Exception(\"I don't know how to deal with this\")\n\n    # strip trailing whitespace\n    new_contents = re.sub(r'^ +$', '', new_contents, flags=re.MULTILINE)\n    source.update(new_contents)\n    source.write()\n\n"
  },
  {
    "path": "theme/epub/epub.css",
    "content": "/* Styling for custom captions on code blocks */\n.sourcecode p {\n  text-align: right;\n  display: block;\n  margin-bottom: -3pt;\n  font-style: italic;\n  hyphens: none;\n}\n\ndiv[data-type=\"example\"].sourcecode pre {\n  margin: 25px 0 25px 25px;\n}\n\ndiv[data-type=\"example\"].sourcecode {\n  margin-bottom: 0;\n}\n\n"
  },
  {
    "path": "theme/epub/epub.xsl",
    "content": "<xsl:stylesheet version=\"1.0\"\n                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n                xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                xmlns=\"http://www.w3.org/1999/xhtml\"\n                exclude-result-prefixes=\"h\">\n\n  <!-- Add title heading elements for different admonition types that do not already have headings in markup -->\n  <xsl:param name=\"add.title.heading.for.admonitions\" select=\"1\"/>  \n\n  <!-- Override to print example captions without labels-->\n  <xsl:template match=\"h:div[@data-type='example' and contains(@class, 'sourcecode')]/h:h5\" mode=\"process-heading\">\n    <p>\n      <xsl:apply-templates/>\n    </p>\n  </xsl:template>\n\n  <!-- Insert SCRATCHPAD: heading for all sidebars with 'scratchpad' class -->\n  <xsl:template match=\"h:aside[@data-type='sidebar' and contains(@class, 'scratchpad')]//h:h5\">\n    <h5>SCRATCHPAD:</h5>\n    <xsl:apply-templates/>\n  </xsl:template> \n\n  <!-- Fix for TOC having empty ol. Matches sections that contain only descendants with the notoc class. We do this by setting the TOC section depth to 1. -->\n<!-- Match divs that contain only sect1s with notoc class -->\n<xsl:template match=\"h:div[descendant::h:section[contains(@data-type, 'sect1')] and \n                         not(descendant::h:section[contains(@data-type, 'sect1') and not(contains(@class, 'notoc'))])]\" \n              mode=\"tocgen\">\n    <xsl:param name=\"toc.section.depth\"/>\n    <xsl:param name=\"inline.markup.in.toc\" select=\"$inline.markup.in.toc\"/>\n    <xsl:choose>\n      <xsl:when test=\"contains(@class, 'notoc')\"/>\n      <xsl:otherwise>\n        <xsl:element name=\"li\">\n          <xsl:attribute name=\"data-type\">\n            <xsl:value-of select=\"@data-type\"/>\n          </xsl:attribute>\n          <a>\n            <xsl:attribute name=\"href\">\n              <xsl:call-template name=\"href.target\">\n                <xsl:with-param name=\"object\" select=\".\"/>\n              </xsl:call-template>\n            </xsl:attribute>\n            <xsl:if test=\"$toc-include-labels = 1\">\n              <xsl:variable name=\"toc-entry-label\">\n                <xsl:apply-templates select=\".\" mode=\"label.markup\"/>\n              </xsl:variable>\n              <xsl:value-of select=\"normalize-space($toc-entry-label)\"/>\n              <xsl:if test=\"$toc-entry-label != ''\">\n                <xsl:value-of select=\"$label.and.title.separator\"/>\n              </xsl:if>\n            </xsl:if>\n            <xsl:choose>\n              <xsl:when test=\"$inline.markup.in.toc = 1\">\n                <xsl:apply-templates select=\".\" mode=\"title.markup\"/>\n              </xsl:when>\n              <xsl:otherwise>\n                <xsl:variable name=\"title.markup\">\n                  <xsl:apply-templates select=\".\" mode=\"title.markup\"/>\n                </xsl:variable>\n                <xsl:value-of select=\"$title.markup\"/>\n              </xsl:otherwise>\n            </xsl:choose>    \n          </a>\n          <!-- Deliberately not including the nested ol here -->\n        </xsl:element>\n      </xsl:otherwise>\n    </xsl:choose>\n  </xsl:template>\n                                                                              \n</xsl:stylesheet>\n"
  },
  {
    "path": "theme/epub/layout.html",
    "content": "{{ doctype }}\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"publisher\" content=\"O'Reilly Media, Inc.\"/>\n    <meta name=\"author\" content=\"Harry Percival\"/>\n    <meta name=\"date\" content=\"2023-07-18\"/>\n    <meta name=\"description\" content=\"The third edition of this trusted guide demonstrates the practical advantages of test-driven development (TDD) with Python and describes how to develop a real web application. You'll learn how to write and run tests before building each part of your app and then develop the minimum amount of code required to pass those tests. The result? Clean code that works. Author Harry Percival teaches software and web developers the basics of Django, Selenium, Git, JavaScript, and Mock libraries, along with current web development techniques.\"/>\n    <meta name=\"identifier\" content=\"9781098148713\"/>\n    <title>{{ title }}</title>\n  </head>\n  <body data-type=\"book\">\n    {{ content }}\n  </body>\n</html>\n"
  },
  {
    "path": "theme/html/html.xsl",
    "content": "<xsl:stylesheet version=\"1.0\"\n                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n                xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                xmlns=\"http://www.w3.org/1999/xhtml\"\n                exclude-result-prefixes=\"h\">\n\n  <!-- Do add border div for figure images in animal series -->\n  <xsl:param name=\"figure.border.div\" select=\"1\"/>\n\n  <!-- This param is required for animal_theme_sass, but not the old animal_theme -->\n  <!-- Generate separate footnote-call markers, so that we don't\n       need to rely on AH counters to do footnote numbering -->\n  <xsl:param name=\"process.footnote.callouts.only\" select=\"1\"/>\n\n  <xsl:template name=\"string-replace-all\">\n    <xsl:param name=\"text\"/>\n    <xsl:param name=\"replace\"/>\n    <xsl:param name=\"by\"/>\n    <xsl:choose>\n      <xsl:when test=\"contains($text, $replace)\">\n        <xsl:value-of select=\"substring-before($text,$replace)\"/>\n        <xsl:value-of select=\"$by\"/>\n        <xsl:call-template name=\"string-replace-all\">\n          <xsl:with-param name=\"text\" select=\"substring-after($text,$replace)\"/>\n          <xsl:with-param name=\"replace\" select=\"$replace\"/>\n          <xsl:with-param name=\"by\" select=\"$by\"/>\n        </xsl:call-template>\n      </xsl:when>\n      <xsl:otherwise>\n        <xsl:value-of select=\"$text\"/>\n      </xsl:otherwise>\n    </xsl:choose>\n  </xsl:template>\n  \n  <xsl:template match=\"h:img/@src\">\n    <xsl:choose>\n    <xsl:when test=\"contains(., 'callouts/')\">\n      <xsl:variable name=\"newtext\">\n        <xsl:call-template name=\"string-replace-all\">\n          <xsl:with-param name=\"text\" select=\".\"/>\n          <xsl:with-param name=\"replace\" select=\"'png'\"/>\n          <xsl:with-param name=\"by\" select=\"'pdf'\"/>\n        </xsl:call-template>\n      </xsl:variable>\n       <xsl:attribute name=\"src\">\n          <xsl:value-of select=\"$newtext\"/>\n       </xsl:attribute>\n    </xsl:when>\n    <xsl:otherwise>\n      <xsl:copy>\n        <xsl:apply-templates select=\"@*|node()\"/>\n      </xsl:copy>\n     </xsl:otherwise>\n    </xsl:choose>\n  </xsl:template>\n\n  <!-- Override to print example captions without labels-->\n  <xsl:template match=\"h:div[@data-type='example' and contains(@class, 'sourcecode')]/h:h5\" mode=\"process-heading\">\n    <xsl:apply-templates/>\n  </xsl:template>\n\n  <!-- Will need to fix numbering for formal examples in template override -->\n \n</xsl:stylesheet>\n\n\n"
  },
  {
    "path": "theme/mobi/layout.html",
    "content": "{{ doctype }}\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"publisher\" content=\"O'Reilly Media, Inc.\"/>\n    <meta name=\"author\" content=\"Harry Percival\"/>\n    <meta name=\"date\" content=\"2017-08-18\"/>\n    <meta name=\"description\" content=\"By taking you through the development of a real web application from beginning to end, the second edition of this hands-on guide demonstrates the practical advantages of test-driven development (TDD) with Python. You’ll learn how to write and run tests *before* building each part of your app, and then develop the minimum amount of code required to pass those tests. The result? Clean code that works.\"/>\n    <meta name=\"identifier\" content=\"9781491958704\"/>\n    <title>{{ title }}</title>\n  </head>\n  <body data-type=\"book\">\n    {{ content }}\n  </body>\n</html>\n"
  },
  {
    "path": "theme/mobi/mobi.css",
    "content": "/* Styling for custom captions on code blocks */\n.sourcecode p {\n  text-align: right;\n  display: block;\n  margin-bottom: -3pt;\n  font-style: italic;\n  hyphens: none;\n}\n\ndiv[data-type=\"example\"].sourcecode pre {\n  margin: 25px 0 25px 25px;\n}\n\ndiv[data-type=\"example\"].sourcecode {\n  margin-bottom: 0;\n}\n\n"
  },
  {
    "path": "theme/mobi/mobi.xsl",
    "content": "<xsl:stylesheet version=\"1.0\"\n                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n                xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                xmlns=\"http://www.w3.org/1999/xhtml\"\n                exclude-result-prefixes=\"h\">\n\n  <!-- Add title heading elements for different admonition types that do not already have headings in markup -->\n  <xsl:param name=\"add.title.heading.for.admonitions\" select=\"1\"/>  \n\n  <!-- Override to print example captions without labels-->\n  <xsl:template match=\"h:div[@data-type='example' and contains(@class, 'sourcecode')]/h:h5\" mode=\"process-heading\">\n    <p>\n      <xsl:apply-templates/>\n    </p>\n  </xsl:template>\n\n  <!-- Insert SCRATCHPAD: heading for all sidebars with 'scratchpad' class -->\n  <xsl:template match=\"h:aside[@data-type='sidebar' and contains(@class, 'scratchpad')]//h:h5\">\n    <h5>SCRATCHPAD:</h5>\n    <xsl:apply-templates/>\n  </xsl:template> \n\n  \n                                                                              \n</xsl:stylesheet>\n"
  },
  {
    "path": "theme/pdf/pdf.css",
    "content": "@charset \"UTF-8\";\n/*----Rendering for special role=\"caption\" lines below code blocks, per AU request; see RT #151714----*/\n/*----Amended by author in may 2013 to use role=\"sourcecode\" and be the official title for the codeblock...----*/\n\n/*--Adjusting padding in TOC to avoid bad break--*/\n@page toc:first { /* first page */\n   padding-bottom: 0.5in;\n}\n\n@page toc: { \n   padding-bottom: 0.5in;\n}\n\n/* Custom widths */\n.width-10 img { width: 10% !important; }\n.width-20 img { width: 20% !important; }\n.width-30 img { width: 30% !important; }\n.width-40 img { width: 40% !important; }\n.width-50 img { width: 50% !important; }\n.width-60 img { width: 60% !important; }\n.width-70 img { width: 70% !important; }\n.width-80 img { width: 80% !important; }\n.width-90 img { width: 90% !important; }\n.width-95 img { width: 95% !important; }\n\n/* less space for sidebar pagebreaks*/\n.less_space {margin-top: 0 !important;}\n\n/* tighten tracking for paragraphs */\n.fix_tracking { letter-spacing: -0.1pt; }\n\n/* Adding font fallback for characters not represented in standard text font */\nbody[data-type=\"book\"] { font-family: MinionPro, Symbola, ArialUnicodeMS }\n\n/* Globally preventing code blocks from breaking across pages */\ndiv[data-type=\"example\"], pre { page-break-inside: avoid; }\n\n/* Removing labels from formal code blocks */\nsection[data-type=\"chapter\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\nsection[data-type=\"appendix\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\nsection[data-type=\"preface\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\ndiv[data-type=\"part\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\ndiv[data-type=\"part\"] section[data-type=\"chapter\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\ndiv[data-type=\"part\"] section[data-type=\"appendix\"] div[data-type=\"example\"] h5:before {\n  content: none;\n}\n\n/* Styling the file name captions on code blocks */\ndiv.sourcecode h5 {\n  text-align: right;\n  display: block;\n  margin-bottom: 1pt;\n  font-style: italic;\n  hyphens: none;\n  font-size: 9.3pt;\n}\n\n/*Splitting a list into two columns*/\nul.two-col { columns: 2; }\n\n\n/* Styling formal code blocks with file name captions\n   like informal code blocks */\ndiv.sourcecode pre {\n  margin-left: 17pt;\n}\n\n/* Add some space below sourcecode code blocks (STYL-991) */\ndiv.sourcecode {\n  margin-bottom: 1.5em;\n}\n\n/* Not sure what this custom formatting is for, was blindly ported from \n   first edition */\ndiv.small-code pre,\npre.small-code {\n  font-size: 75%;\n}\n\naside[data-type=\"sidebar\"] pre.small-code code {\n  font-size: 95%;\n}\n\nblockquote.blockquote{\nfont-style:italic;\n}\n\n.blockquote p{\ntext-align:right;\n}\n\n\n/*---custom formatting for scratchpad items---*/\n\n\n\naside[data-type=\"sidebar\"].scratchpad { \n  overflow: auto;\n  margin-top: 0.625in;\n\n}\n\naside[data-type=\"sidebar\"].scratchpad:before {\n  content: \" \";\n  background-image: url('../../images/papertop.png');\n  position: relative;\n  background-repeat: no-repeat;\n  background-size: 4in auto;\n  display: block;\n  height: 0.386in;\n  margin-bottom: 0;\n  padding-bottom: 0;\n  width: 4in;\n  margin-left: -25px;\n  top: -0.386in;\n}\n\naside[data-type=\"sidebar\"].scratchpad {\n  background-image: url('../../images/papermiddle.png');\n  background-position: left top;\n  background-repeat: repeat-y; \n  background-size: 4in auto;\n  padding-top: 0;\n  padding-bottom: 0;\n  padding-left: 25px;\n  width: 4in;\n  page-break-inside: avoid;\n  border: none;\n}\n\naside[data-type=\"sidebar\"].scratchpad:after {\n  content: \" \";\n  background-image: url('../../images/paperbottom.png');\n  background-repeat: no-repeat; \n  background-size: 4in auto;\n  display: block;\n  height: 0.377in;\n  margin-top: 0;\n  width: 4in;\n  margin-left: -25px;\n  margin-top: 3pt;\n  \n}\n\n\naside[data-type=\"sidebar\"].scratchpad ul  {\n  margin-top: -28pt;\n  margin-left: 10pt;\n  list-style-type: none;\n  padding-right: 1.2in;\n}\n\naside[data-type=\"sidebar\"].scratchpad ul li  {\n  line-height: 0.165in;\n  margin-top: 0;\n  margin-bottom: 1pt;\n}\n\naside[data-type=\"sidebar\"].scratchpad ul li p {  \n  font-family: \"ORAHand-Medium\", ArialUnicodeMS;\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n/* Mock H3 */\nspan.fake-h3 {\nfont-family: \"MyriadPro-SemiboldCond\";\nfont-weight: 600;\nfont-size: 11.56pt;\n}\n\n\n/*----Uncomment to change the TOC start page (set \nthe number to one page _after_ the one you want; \nso 6 to start on v, 8 to start on vii, etc.)\n----*/\n/* handling for elements to keep them from breaking across pages */\n\n.nobreakinside {page-break-inside: avoid; page-break-before: auto;}\n\n/*----Uncomment to change the TOC start page (set \nthe number to one page _after_ the one you want; \nso 6 to start on v, 8 to start on vii, etc.)\n----*/\n/* handling for elements to keep them from breaking across pages */\n\n.nobreakinside {page-break-inside: avoid; page-break-before: auto;}\n\n\n\n/*--------Put Your Custom CSS Rules Below--------*/\n/*--- This oneoff overrides the code in https://github.com/oreillymedia/<name_of_theme>/blob/master/pdf/pdf.css---*/\n\n/*----Uncomment to temporarily turn on code-eyballer highlighting (make sure to recomment after you build)\n\npre {\n background-color: yellow;\n}---*/\n\n\n/*----Uncomment to turn on automatic code wrapping\n\npre {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n----*/\n\n/*----Uncomment to change the TOC start page (set \nthe number to one page _after_ the one you want; \nso 6 to start on v, 8 to start on vii, etc.)\n\n@page toc:first {\n  counter-reset: page 6;\n}\n----*/\n\n/*----Uncomment to fix a bad break in the title \n      (increase padding value to push down, decrease \n      value to pull up)\n\nsection[data-type=\"titlepage\"] h1 {\n  padding-left: 1.5in;\n}\n----*/\n\n/*----Uncomment to fix a bad break in the subtitle\n      (increase padding value to push down, decrease\n      value to pull up)\n\nsection[data-type=\"titlepage\"] h2 {\n  padding-left: 1in;\n}\n----*/\n\n/*----Uncomment to fix a bad break in the author names \n      (increase padding value to push down, decrease \n      value to pull up)\n\nsection[data-type=\"titlepage\"] p.author {\n  padding-left: 3in;\n}\n----*/\n"
  },
  {
    "path": "theme/pdf/pdf.xsl",
    "content": "<xsl:stylesheet version=\"1.0\"\n                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n                xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                xmlns=\"http://www.w3.org/1999/xhtml\"\n                exclude-result-prefixes=\"h\">\n\n  <!-- Do add border div for figure images in animal series -->\n  <xsl:param name=\"figure.border.div\" select=\"1\"/>\n\n  <!-- This param is required for animal_theme_sass, but not the old animal_theme -->\n  <!-- Generate separate footnote-call markers, so that we don't\n       need to rely on AH counters to do footnote numbering -->\n  <xsl:param name=\"process.footnote.callouts.only\" select=\"1\"/>\n\n  <xsl:template name=\"string-replace-all\">\n    <xsl:param name=\"text\"/>\n    <xsl:param name=\"replace\"/>\n    <xsl:param name=\"by\"/>\n    <xsl:choose>\n      <xsl:when test=\"contains($text, $replace)\">\n        <xsl:value-of select=\"substring-before($text,$replace)\"/>\n        <xsl:value-of select=\"$by\"/>\n        <xsl:call-template name=\"string-replace-all\">\n          <xsl:with-param name=\"text\" select=\"substring-after($text,$replace)\"/>\n          <xsl:with-param name=\"replace\" select=\"$replace\"/>\n          <xsl:with-param name=\"by\" select=\"$by\"/>\n        </xsl:call-template>\n      </xsl:when>\n      <xsl:otherwise>\n        <xsl:value-of select=\"$text\"/>\n      </xsl:otherwise>\n    </xsl:choose>\n  </xsl:template>\n  \n  <xsl:template match=\"h:img/@src\">\n    <xsl:choose>\n    <xsl:when test=\"contains(., 'callouts/')\">\n      <xsl:variable name=\"newtext\">\n        <xsl:call-template name=\"string-replace-all\">\n          <xsl:with-param name=\"text\" select=\".\"/>\n          <xsl:with-param name=\"replace\" select=\"'png'\"/>\n          <xsl:with-param name=\"by\" select=\"'pdf'\"/>\n        </xsl:call-template>\n      </xsl:variable>\n       <xsl:attribute name=\"src\">\n          <xsl:value-of select=\"$newtext\"/>\n       </xsl:attribute>\n    </xsl:when>\n    <xsl:otherwise>\n      <xsl:copy>\n        <xsl:apply-templates select=\"@*|node()\"/>\n      </xsl:copy>\n     </xsl:otherwise>\n    </xsl:choose>\n  </xsl:template>\n\n</xsl:stylesheet>\n\n"
  },
  {
    "path": "titlepage.html",
    "content": "<section data-type=\"titlepage\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<h1>Test-Driven Development <span class=\"keep-together\">with Python</span></h1>\n\n<p class=\"edition\">Third Edition</p>\n\n<p class=\"subtitle\">Obey the Testing Goat: Using Django, <span class=\"keep-together\">Selenium, and JavaScript</span></p>\n\n<p class=\"author\">Harry J.W. Percival</p>\n</section>\n"
  },
  {
    "path": "toc.html",
    "content": "<nav data-type=\"toc\" xmlns=\"http://www.w3.org/1999/xhtml\"/>"
  },
  {
    "path": "todos.txt",
    "content": "# Today\n\n\n## to consider\n\n- remove virtualenvs from diagram\n- dev-requirements.txt\n- assertEqual(items.objects.all, []) in chap 4 and plus.\n    nice explanation of the importance of helplful error messages\n* shellscripts for all infra stuff\n- rewrite all test names in js chapter\n\n\n\n# Later\n\n* was lxml overkill?  can just use `assertContains(name=text)`\n  or `assertContains(action=/foo/bla)\n* add a new chapter 8 where we delete the multiple users FT??? 😱\n* item_set -> use related_name.\n* change URLs to be more restey\n* TEST_HOST\n* appendix on tradeoffs and testing\n* remove isinstance(form) from 15+?\n\n* run thru using docker+ansible on windows via wsl2.  needs non-macos host machine,\n    bc docker desktop needs hyper-v, and that needs nested virtualisation,\n    and that's not supported by parallels or virtualbox, on apple silicon\n\n* hash the tokens\n* investigate this flakiness in 15 https://github.com/hjwp/Book-TDD-Web-Dev-Python/actions/runs/13393304093/job/37405965975\n\n* switch to more rest-ful url structure in 7\n- consider splitting 23 into two chapters\n- figure out how to delete at least one FT?\n- spike chap:  start with test of login view.\n- pytest\n- \"switch to flask\" appendix\n- pytest appendix\n- run thru and test WSL\n\n## switch to postgres?\n\n- do it in deploy chaps\n- production-readiness really\n- install it locally?\n- or, put it in a docker container?\n- docker-compose??\n\n# Appendix ideas\n\n- new appendix on js modules\n- appendix where we get rid of model-layer tests?\n\n\n\n# OLD TODOS\nget rid of staging/live.\n\n\nmention pdb.set_trace()\n\nadd a note re double-checking the django version with django-admin.py --version\n\nbuild some tests for all the URLs, add to jenkins\n\n\n# DONE\n- count all TR notes per chapter\n- python 3.14\n- massive updates in 11\n- update chap 21 server debugging, do with docker instead?\n- requirements.txt chat into 10\n* remove all 'fab' handling from tests eg in sourcetree.py\n* mention https://testdesiderata.com/ somewhere\n"
  },
  {
    "path": "tools/figure_renaming_report.tsv",
    "content": "Original names\tNew names\nimages/git_windows_installer_choose_editor.png\timages/ttd3_0001.png\nimages/python_install_add_to_path.png\timages/ttd3_0002.png\nimages/twp2_00in01.png\timages/ttd3_00in01.png\nimages/twp2_0101.png\timages/ttd3_0101.png\nimages/firefox_upgrade_popup.png\timages/ttd3_0102.png\nimages/twp2_0102.png\timages/ttd3_0103.png\nimages/twp2_0103.png\timages/ttd3_0104.png\nimages/twp2_0401.png\timages/ttd3_0401.png\nimages/twp2_0402.png\timages/ttd3_0402.png\nimages/tdd-process-unit-tests-only-excalidraw.png\timages/ttd3_0403.png\nimages/red-green-refactor-excalidraw.png\timages/ttd3_0404.png\nimages/double-loop-tdd-simpler.png\timages/ttd3_0405.png\nimages/twp2_0501.png\timages/ttd3_0501.png\nimages/todo_list_table_disappeared.png\timages/ttd3_0502.png\nimages/django_operationalerror.png\timages/ttd3_0503.png\nimages/twp2_0503.png\timages/ttd3_0504.png\nimages/multiple-lists-users-and-urls.png\timages/ttd3_0701.png\nimages/double-loop-tdd-simpler.png\timages/ttd3_0702.png\nimages/devtools-post-request.png\timages/ttd3_0703.png\nimages/ugly-homepage.png\timages/ttd3_0801.png\nimages/prettified-1.png\timages/ttd3_0802.png\nimages/prettified-2.png\timages/ttd3_0803.png\nimages/prettified-dark.png\timages/ttd3_0804.png\nimages/prettified-final.png\timages/ttd3_0805.png\nimages/virtualenv-vs-vm.png\timages/ttd3_0901.png\nimages/containers-diagram.png\timages/ttd3_0902.png\nimages/firefox-unable-to-connect.png\timages/ttd3_0903.png\nimages/firefox-connection-reset.png\timages/ttd3_0904.png\nimages/orly-essential-googling-the-error-message.png\timages/ttd3_0905.png\nimages/google-results-with-stackoverflow.png\timages/ttd3_0906.png\nimages/site-in-docker-is-up.png\timages/ttd3_0907.png\nimages/twp2_0904.png\timages/ttd3_0908.png\nimages/homepage_no_css_8888.png\timages/ttd3_1001.png\nimages/django_400.png\timages/ttd3_1002.png\nimages/search-results-400-bad-request.png\timages/ttd3_1003.png\nimages/server_error_500.png\timages/ttd3_1004.png\nimages/gandi_add_dns_a_record.png\timages/ttd3_1101.png\nimages/ansible-and-ssh.png\timages/ttd3_1102.png\nimages/single-endpoint-for-forms.png\timages/ttd3_1401.png\nimages/html5-validation-popup.png\timages/ttd3_1501.png\nimages/integrity_error_unique_constraint.png\timages/ttd3_1601.png\nimages/devtools_error_div_hidden.png\timages/ttd3_1602.png\nimages/devtools_closeup_edit_html.png\timages/ttd3_1603.png\nimages/wrong_order_list.png\timages/ttd3_1604.png\nimages/duplicate_item_error.png\timages/ttd3_1701.png\nimages/error-gone-but-input-still-red.png\timages/ttd3_1702.png\nimages/editing-html-via-devtools.png\timages/ttd3_1703.png\nimages/jasmine-in-browser-green.png\timages/ttd3_1704.png\nimages/jasmine-in-browser-red.png\timages/ttd3_1705.png\nimages/typeerror_in_devools.png\timages/ttd3_1706.png\nimages/jasmine-console-logs.png\timages/ttd3_1707.png\nimages/double-loop-tdd-simpler.png\timages/ttd3_1708.png\nimages/magic-links-overview.png\timages/ttd3_1901.png\nimages/login-email-sent-page.png\timages/ttd3_1902.png\nimages/login-link-in-email.png\timages/ttd3_1903.png\nimages/spike-it-worked-windows.png\timages/ttd3_1904.png\nimages/check_your_email.png\timages/ttd3_2001.png\nimages/car-factory-illustration.png\timages/ttd3_2101.png\nimages/check_your_email.png\timages/ttd3_2102.png\nimages/cookies-in-debug-toolbar.png\timages/ttd3_2201.png\nimages/outside-in-layers.png\timages/ttd3_2401.png\nimages/my-lists-page-sketch.png\timages/ttd3_2402.png\nimages/empty_my_lists.png\timages/ttd3_2403.png\nimages/twp2_2201.png\timages/ttd3_2404.png\nimages/gitlab_new_blank_project.png\timages/ttd3_2501.png\nimages/gitlab_files_ui.png\timages/ttd3_2502.png\nimages/gitlab_first_build.png\timages/ttd3_2503.png\nimages/gitlab_ui_for_browse_artifacts.png\timages/ttd3_2504.png\nimages/gitlab_ui_show_screenshot.png\timages/ttd3_2505.png\nimages/gitlab_pipeline_success.png\timages/ttd3_2506.png\nimages/gitlab_pipeline_js_success.png\timages/ttd3_2507.png\nimages/gitlab_pipeline_overview_success.png\timages/ttd3_2508.png\nimages/list_with_sharing_options.png\timages/ttd3_2601.png\nimages/test_pyramid.png\timages/ttd3_2701.png\nimages/twp2_2601.png\timages/ttd3_2702.png\nimages/frog-in-a-pan-placeholder.png\timages/ttd3_2703.png\nimages/double-loop-tdd-simpler.png\timages/ttd3_aa01.png\n"
  },
  {
    "path": "tools/intake_report.txt",
    "content": "Title: Test-Driven Development with Python\nISBN: 9781098148713\nJIRA Ticket #: /DCPSPROD-10184\n\nStylesheet: animal_theme_sass\nToolchain: Atlas 2\n\nAtlas URL: https://atlas.oreilly.com/oreillymedia/test-driven-development-with-python-3e\n\nIncoming format: AsciiDoc\nOutgoing format: AsciiDoc\n\nPreliminary pagecount: 740\n\nIs this project in Early Release? Yes\n\nResources\n=================\n* Figs: Illustrations is still working on the figs.\n** 132 total. (There are 76 formal figures, 1 informal one in the front matter, and 55 informal \"scratchpad\" ones.)\n\n** Once the figs are processed on /work, you'll need to add them to the book's repo.\n** A report mapping original figure file names to their new names can be found in the tools folder for this project as figure_renaming_report.tsv.\n\n* Intake Report:(Atlas repo) tools/intakereport.txt\n\n* MS Snapshot: To view the submitted files, checkout the git tag named 'manuscript_to_prod' by running the following command:\n\n$ git checkout manuscript_to_prod\n\nThis will temporarily switch the files in your repo to the state they were in when the manuscript_to_prod tag was created. To switch the files back to the current state, run: \n\n$ git checkout main \n\n Notes from Tools:\n=================\n\n* PROD: Add any authors to the project that need to be added.\n\n* Syntax highlighting: applied to 534 out of 1201 code listings.\n\n* There are a number files in the repo that aren't in the build list (e.g some additional appendix files, notes files, some code files). Probably many can be deleted but I didn't remove any, in case they anything is pulled in with include statements or otherwise needed.\n\n* I replaced the sidebars with figure markup and copied the PNGs into the repo from the preintake figs folder on /work.\n\n* The acknowledgements are in a separate front matter file rather than in the preface.\n\n* I didn't update the Using Code Examples section of the preface with the latest boilerplate because the existing section has some custom text.\n\n* I replaced the \"scratchpad\" sidebars with informal figures.\n\n* The remaining error in the build log is (I think) due to the third code block in Chapter 20. It's missing the explanation text for the <2> callout. The second code block in the chapter also has an irregularity - it's got an extra <2> marker, but this doesn't seem to be causing an error. \n\n* Please check the bibliography formatting. I had to put the entries into an HTML passthrough list to get the cross-references (to one of the listed references) to work, but possibly this list should be formatted differently.\n\n* Please let Tools know ASAP if there are any other global problems for which we can help automate a fix.\n\nNotes at arrival in production:\n=================\n\nSome blank IDs and broken xrefs currently in the build log.\n\nKB took screenshots of all the scratchpad images (at least in Chs 1-8 so far), because those are currently created with code and appear like images in the book. We'll need to convert those to informal images, during intake?\n\nThis book is finally ready for Intake. We've already started CE and CE review, but the book's main content has been submitted to production—we were waiting for the author to finish writing all chapter content before starting Intake. The author is making edits to the AI Preface and App A and B in a separate branch, which we will then merge to main. His due date for those are 7/7, but this will probably slip a few days. I'll let you know when we're ready to do this. He knows to avoid the main branch starting today, but let me know if you see any activity there and I'll send a reminder.\n\nThere is CSS to keep code blocks from breaking across pages, please retain this for this edition. All CSS should be kept in, if you see something really questionable, let me know.\n\nThere are a lot of informal figures, which will have a different numbering system than the captioned ones. Just FYI that these \"scratchpad\" figures will end up being numbered like 05in01, 05in02, etc. See figure log for details:\n\n=================\n\nPlease let me know about any other issues.\n\nThanks,\nTheresa\n"
  },
  {
    "path": "tools/oneoffs/oneoff.css",
    "content": "@charset \"UTF-8\";\n\n@import \"DYNAMIC CSS PLACEHOLDER\";\n\n/*----Rendering for special role=\"caption\" lines below code blocks, per AU request; see RT #151714----*/\n/*----Amended by author in may 2013 to use role=\"sourcecode\" and be the official title for the codeblock...----*/\n\np.sourcecode {\n  text-align: right;\n  display: block;\n  margin-bottom: -3pt;\n  font-style: italic;\n  hyphens: none;\n  font-size: 9.3pt;\n}\n\npre.programlisting.small-code, pre.screen.small-code, pre.literallayout.small-code {\n  font-size: 75%;\n}\n\n.sidebar pre.programlisting.small-code code {\n  font-size: 95%;\n}\n\nblockquote.blockquote{\nfont-style:italic;\n}\n\n.blockquote p{\ntext-align:right;\n}\n\n\n/*---custom formatting for scratchpad items---*/\n\n\n\ndiv.sidebar.scratchpad { \n  overflow: auto;\n  margin-top: 0.625in;\n\n}\n\ndiv.sidebar.scratchpad:before {\n  content: \" \";\n  background-image: url('../../images/papertop.png');\n  position: relative;\n  background-repeat: no-repeat;\n  background-size: 4in auto;\n  display: block;\n  height: 0.386in;\n  margin-bottom: 0;\n  padding-bottom: 0;\n  width: 4in;\n  margin-left: -25px;\n  top: -0.386in;\n}\n\ndiv.sidebar.scratchpad {\n  background-image: url('../../images/papermiddle.png');\n  background-position: left top;\n  background-repeat: repeat-y; \n  background-size: 4in auto;\n  padding-top: 0;\n  padding-bottom: 0;\n  padding-left: 25px;\n  width: 4in;\n  page-break-inside: avoid;\n  border: none;\n}\n\ndiv.sidebar.scratchpad:after {\n  content: \" \";\n  background-image: url('../../images/paperbottom.png');\n  background-position: left bottom;\n  background-repeat: no-repeat; \n  background-size: 4in auto;\n  display: block;\n  height: 0.377in;\n  margin-top: 0;\n  width: 4in;\n  margin-left: -25px;\n  margin-top: 3pt;\n}\n\n\ndiv.sidebar.scratchpad ul  {\n  margin-top: -25pt;\n  margin-left: 10pt;\n  list-style-type: none;\n  padding-right: 1.2in;\n}\n\ndiv.sidebar.scratchpad ul li  {\n  line-height: 0.15in;\n}\n\ndiv.sidebar.scratchpad ul li p {  font-family: \"ORAHand-Medium\";\n}\n\n/*----Uncomment to change the TOC start page (set \nthe number to one page _after_ the one you want; \nso 6 to start on v, 8 to start on vii, etc.)\n----*/\n\n@page tableofcontents:first {\n  counter-reset: page 8;\n}\n\n/* handling for elements to keep them from breaking across pages */\n\n.nobreakinside {page-break-inside: avoid; page-break-before: auto;}\n\n"
  },
  {
    "path": "tools/oneoffs/oneoff.xsl",
    "content": "<?xml version=\"1.0\"?>\n\n<xsl:stylesheet version=\"1.0\"\n                xmlns=\"http://www.w3.org/1999/xhtml\"\n                xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n                xmlns:opf=\"http://www.idpf.org/2007/opf\"\n                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n                xmlns:exsl=\"http://exslt.org/common\"\n                exclude-result-prefixes=\"dc opf exsl\">\n\n<!-- Placeholder xsl:import instruction -->\n<xsl:import href=\"DYNAMICALLY_UPDATED_PLACEHOLDER\"/>\n\n<xsl:template match=\"programlisting|screen|synopsis\" mode=\"class.value\">\n  <xsl:param name=\"class\" select=\"local-name(.)\"/>\n  <xsl:choose>\n    <xsl:when test=\"@role\">\n      <xsl:value-of select=\"concat($class, ' ', @role)\"/>\n    </xsl:when>\n    <xsl:otherwise>\n      <xsl:value-of select=\"$class\"/>\n    </xsl:otherwise>\n  </xsl:choose>\n</xsl:template>\n\n\n<xsl:template match=\"figure|table|example\" mode=\"label.markup\">\n  <xsl:variable name=\"pchap\"\n                select=\"ancestor::chapter\n                        |ancestor::appendix\n                        |ancestor::article[ancestor::book]\"/>\n\n  <xsl:variable name=\"prefix\">\n    <xsl:if test=\"count($pchap) &gt; 0\">\n      <xsl:apply-templates select=\"$pchap\" mode=\"label.markup\"/>\n    </xsl:if>\n  </xsl:variable>\n\n  <xsl:choose>\n    <xsl:when test=\"@label\">\n      <xsl:value-of select=\"@label\"/>\n    </xsl:when>\n    <xsl:otherwise>\n      <xsl:choose>\n        <xsl:when test=\"$prefix != ''\">\n          <xsl:choose>\n            <xsl:when test=\"ancestor::chapter and count(/book//chapter) = 1\"/>\n            <xsl:otherwise>\n              <xsl:apply-templates select=\"$pchap\" mode=\"label.markup\"/>\n              <xsl:apply-templates select=\"$pchap\" mode=\"intralabel.punctuation\"/>\n            </xsl:otherwise>\n          </xsl:choose>\n          <xsl:number format=\"1\" from=\"chapter|appendix\" level=\"any\"/>\n        </xsl:when>\n\n\n <!-- Oneoff taken from Labels.xsl -->\n <!-- ORM: Use P- for Preface, ala Frame (or I- for Introduction in MMs); see RT #45135 -->\n        <xsl:when test=\"ancestor::preface\">\n          <xsl:variable name=\"thispreface\" select=\"ancestor::preface\"/>\n          <xsl:variable name=\"title\">\n            <xsl:value-of select=\"$thispreface/title\"/>\n          </xsl:variable>\n          <xsl:choose>\n            <xsl:when test=\"contains($title, 'Preface')\">\n              <xsl:text>P</xsl:text>\n              <xsl:apply-templates select=\"$thispreface\" mode=\"intralabel.punctuation\"/>\n              <xsl:number format=\"1\" from=\"preface\" level=\"any\"/>\n            </xsl:when>\n            <xsl:when test=\"contains($title, 'Prerequisites')\">\n              <xsl:text>P</xsl:text>\n              <xsl:apply-templates select=\"$thispreface\" mode=\"intralabel.punctuation\"/>\n              <xsl:number format=\"1\" from=\"preface\" level=\"any\"/>\n            </xsl:when>\n             <!-- Oneoff starts here - I'm just putting Setup there so that it registers instead of running the otherwise message and so that S is put in there. -->\n            <xsl:when test=\"contains($title, 'Setup')\">\n              <xsl:text>S</xsl:text>\n              <xsl:apply-templates select=\"$thispreface\" mode=\"intralabel.punctuation\"/>\n              <xsl:number format=\"1\" from=\"preface\" level=\"any\"/>\n            </xsl:when>\n            <xsl:otherwise>\n              <xsl:message>WARNING: Formal object (<xsl:value-of select=\"local-name()\"/>) encountered in nonstandard &lt;preface&gt; (title=<xsl:value-of select=\"$title\"/>, id=<xsl:value-of select=\"@id\"/>). Please change markup or contact toolsreq@oreilly.com to discuss desired caption labels.</xsl:message>\n              <xsl:text>**LABEL TBD**</xsl:text>\n              <xsl:apply-templates select=\"$thispreface\" mode=\"intralabel.punctuation\"/>\n              <xsl:number format=\"1\" from=\"preface\" level=\"any\"/>\n            </xsl:otherwise>\n          </xsl:choose>\n        </xsl:when>\n        \n  <!-- Use uppercase roman numerals for parts -->\n        <xsl:when test=\"ancestor::part\">\n          <xsl:variable name=\"ppart\"\n            select=\"ancestor::part\"/>\n          <xsl:variable name=\"part_prefix\">\n            <xsl:if test=\"count($ppart) &gt; 0\">\n              <xsl:apply-templates select=\"$ppart\" mode=\"label.markup\"/>\n            </xsl:if>\n          </xsl:variable>\n          <xsl:if test=\"$part_prefix != ''\">\n            <xsl:apply-templates select=\"$ppart\" mode=\"label.markup\"/>\n            <!-- This allows us to use hyphen as label separator in fig captions -->\n            <xsl:apply-templates select=\".\" mode=\"intralabel.punctuation\"/>\n          </xsl:if>\n          <xsl:number format=\"1\" from=\"part\" level=\"any\"/>\n        </xsl:when>\n        \n        <xsl:otherwise>\n          <xsl:number format=\"1\" from=\"book|article\" level=\"any\"/>\n        </xsl:otherwise>\n      </xsl:choose>\n    </xsl:otherwise>\n  </xsl:choose>\n</xsl:template>\n\n</xsl:stylesheet>\n"
  },
  {
    "path": "video_plug.asciidoc",
    "content": "[[video_plug]]\n[preface]\n== Companion Video\n\n(((\"companion video\")))(((\"video-based instruction\")))(((\"Test-Driven Development (TDD)\", \"video-based instruction\")))\nI've recorded a \nhttps://learning.oreilly.com/videos/test-driven-development/9781491919163[10-part video series\nto accompany this book].footnote:[The video has not been updated for the third edition,\nbut the content is all mostly the same.]\nIt covers the content of <<part1>>.\nIf you find that you learn well from video-based material, then I encourage you to check it out.\nOver and above what's in the book,\nit should give you a feel for what the \"flow\" of TDD is like,\nflicking between tests and code and explaining the thought process as we go.\n\nPlus, I'm wearing a delightful yellow t-shirt.\n\n[[video-screengrab]]\nimage::images/tdd3_00in01.png[screengrab from video]\n\n"
  },
  {
    "path": "wordcount",
    "content": "    76    721   4490 acknowledgments.asciidoc\n   606   2304  19065 appendix_II_Django_Class-Based_Views.asciidoc\n   213    791   6308 appendix_III_provisioning_with_ansible.asciidoc\n   141    759   5518 appendix_I_PythonAnywhere.asciidoc\n   197   1151   7268 appendix_IV_what_to_do_next.asciidoc\n   286   1140   9561 appendix_V_testing_migrations.asciidoc\n    24    116   1364 bibliography.asciidoc\n    50     46   1344 book.asciidoc\n   355   1919  13422 chapter_01.asciidoc\n   395   2193  15299 chapter_02.asciidoc\n   817   3697  27783 chapter_03.asciidoc\n   818   4259  30508 chapter_04.asciidoc\n  1468   6921  50299 chapter_05.asciidoc\n  2182   9153  71131 chapter_06.asciidoc\n   943   3816  30584 chapter_07.asciidoc\n  1419   6648  49120 chapter_08.asciidoc\n   620   3213  29044 chapter_09.asciidoc\n  1463   5595  45188 chapter_10.asciidoc\n  1225   4409  37409 chapter_11.asciidoc\n   931   3120  28931 chapter_12.asciidoc\n    93    344   2572 chapter_13.asciidoc\n   589   2537  19196 chapter_14.asciidoc\n  2094   8427  68819 chapter_15.asciidoc\n  1720   5375  49042 chapter_16.asciidoc\n  1166   4144  37969 chapter_17.asciidoc\n   818   3194  26288 chapter_18.asciidoc\n  1880   6451  54681 chapter_19.asciidoc\n   890   3711  31631 chapter_20.asciidoc\n   573   2508  20696 chapter_21.asciidoc\n   450   3017  20704 chapter_22.asciidoc\n   611   3352  25881 cheat_sheet.asciidoc\n     6     58    345 colo.asciidoc\n    90    596   3680 epilogue.asciidoc\n     1      2      8 index.asciidoc\n   160    670   4313 outline_and_future_chapters.asciidoc\n    40    301   1812 part1.asciidoc\n    39    265   1668 part2.asciidoc\n    35    263   1584 part3.asciidoc\n     6     19    144 praise.asciidoc\n   414   2851  19412 preface.asciidoc\n   250   1816  11850 pre-requisite-installations.asciidoc\n   115    337   2437 workshop.asciidoc\n 26269 112209 888368 total\n"
  },
  {
    "path": "workshops/intermediate_workshop_notes.md",
    "content": "# Outline\n\n> 1 day: Outside-in TDD with and without mocks (AKA - \"listen to your tests\").\n> This day will start with a discussion of the \"outside-in\" technique, a way of\n> deciding which tests to write, and what code to write, in what order, and how\n> high-level tests interact with lower-level tests.  We'll do one run with\n> familiar django/integration tests, and then we'll move on to working with\n> mocks.  Using a practical example we'll be able to see many of the common\n> problems with mocks, the difficulties they introduce, but also a real\n> demonstration of the possible benefits mocks (or \"purist\" unit testing) can\n> bring.  We'll conclude the day with a discussion of the pros and cons of\n> different types of tests: end-to-end/functional vs integration vs unit tests.\n\n\n* Intro and installations (10m, t=10)\n* Our example app - tour (2m, t=12)\n* Codebase tour (5m, t=17)\n* Target site tour (3m, t=20)\n* Coding challenge 1:  building the \"my lists\" feature (30m, t=50)\n* Outside-In TDD demo.  Examples + discussion (30m, t=1h20)\n* Break (10m, t=1h30)\n* Mocks demo: (10m, t=1h40)\n* Coding challenge 2: redo it with a more \"purist\" approach (30m, t=1h45)\n* Mocks and \"Listen to your tests\" demo/discussion. (25m, t=2h10)\n* Coding/debugging challenge 3: why doesn't it work? (20m, t=2h30)\n* Recap + discussion:  the pros and cons of different types of test (10m, t=2h25)\n* end (t=2h25)\n\n\n\n# notes from prep\n\n### live code demo 1:\ngit checkout intermediate-workshop-start\nadd url #, navbar-left, re-run ft\nmove down to test views\nclient.get /lists/my_lists/\nassert 200 as well as template\nadd url\ncreate placeholder view\nrender home instead of my lists\nadd my_lists template\ninherit from base\nadd block content, h1\n\n### live code demo 2:\ngit checkout end-of-live-code\nmy_lists.html\nuser.list_set.all\nmention other possibilities\nlist.name\nthen examples back in main prezzo\n\n\n### live code demo 3:\nmockListClass\nsee problem with form\nmockItemForm\npasses but not saving.  hand over\n\n\n\n# Welcome to the Intermediate TDD with Django Workshop\n\n```installation instructions:\ngit clone https://github.com/hjwp/book-example/ tdd-workshop\ncd tdd-workshop\ngit checkout intermediate-workshop-start\npython3.7 -m venv ./virtualenv  # or however you like to create virtualenvs\nsource ./.venv/bin/activate\npip install -r requirements.txt\n# you will also need Firefox and geckodrive. see installation instructions chapter of book\n\n# Take a look around the site  with:\npython manage.py migrate\npython manage.py runserver\n\n# Run the test suite:\npython manage.py test\n\n# you should see it run 53 tests and all but one should pass,\n# expected error = NoSuchElementException, \"Unable to locate element: My lists\"\n```\n\nIf the functional tests give you any trouble, You can try switching from\n`webdriver.Firefox()` to `webdriver.Chrome()`.  You will need to download a\nthing called \"chromedriver\" (google it) and have it on the path (in the main\nrepo folder might also work)\n\n\ncolor: apprentice? colorful? beachcomber? ironman?\n\n\n\n\n\n\n\n\n\n\n# Intro\n\n* pair up: beginners to sit next to more experienced people\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# Our example app - tour\n\n* Current state, demo\n* Desired state, demo\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# Codebase tour\n\n\n**Models**:  a list has many items:\n\n\nlists/models.py:\n```python\n\nclass List(models.Model):\n    pass\n\nclass Item(models.Model):\n    text = models.TextField()\n    list = models.ForeignKey(List)\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Views**:\n\nlists/views.py:\n```python\n\ndef home_page(request):\n    return render(request, 'home.html', {'form': ItemForm()})\n\ndef new_list(request):\n    # use form to recreate and redirect to a new list, or render error template\n\ndef view_list(request, list_id):\n    # retrieve list object\n    # display if GET, use form to add new items if POST\n```\n\n* forms live in *lists/forms.py*.  We don't need them for the first part of this workshop.\n\n* login/logout etc are handled by the accounts module, which we won't need to\n  look at today.  you can just use any email to log in\n\n\n\n\n\n# Double-loop TDD demo\n\n* running the FT\n* possible failure modes\n\n* write a unit test\n* make it pass\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# Coding challenge 1:  building the \"my lists\" feature, quick and dirty\n\nie: Get this FT to pass:\n\n    python manage.py test functional_tests.test_my_lists\n\n\nTips:\n\n* don't worry about tests for now\n* you'll probably need a foreign key from lists to the user model\n* `request.user` will be available if user is logged in\n* `request.user.is_authenticated` is False if user is not logged in\n* `list.get_absolute_url()` will give you a url you can use in an <a> tag for the lists page\n* you will probably want a new template at `lists/templates/my_lists.html`, and a new URL + view for it.  You can inherit from 'base.html'.  note the `extra_content` block will be useful\n* you will need to associate the creation of a new list with the current user, if they're logged in, in the `new_list` view\n* if you want to try manually logging in, you can just enter any email\n\n\n\n\n\n\n\n\n\n\n\n\n# Outside-In TDD.  Examples + discussion\n\n\nLive code demo - programming by wishful thinking in the template\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n* we can work incrementally, small steps\n\n* functional test\n  * drives templates layer ('programming by wishful thinking')\n    * drives views-layer tests\n      * drive views-layer code\n        * drive models-layer tests\n          * drive models-layer-code\n\n\nAdditional illustrations\n\n* next we want to associate owners with lists\n* at the views layer, we need to save owner relationship at new list creation\n* at the models layer, we need to implement saving owners for lists\n\n\n\n\n\n\n\n\n\n\n\n\nlists/tests/test_views.py:\n```python\n\n    def test_list_owner_is_saved_if_user_is_authenticated(self):\n        user = User.objects.create(email='a@b.com')\n        self.client.force_login(user)\n\n        self.client.post('/lists/new', data={'text': 'new item'})\n\n        list_ = List.objects.first()\n        self.assertEqual(list_.owner, user)\n```\n\n\nlists/views.py:\n```python\n\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List()\n        if request.user.is_authenticated():\n            list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n```\n\n\n\n\n\n\n\n\n\n\n\n\n```\n======================================================================\nERROR: test_list_owner_is_saved_if_user_is_authenticated (lists.tests.test_views.NewListTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/home/harry/workspace/book-example/lists/tests/test_views.py\", line 76, in test_list_owner_is_saved_if_user_is_authenticated\n    self.assertEqual(list_.owner, user)\nAttributeError: 'List' object has no attribute 'owner'\n\n----------------------------------------------------------------------\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nSo we move down to the models layer:\n\n\nlists/tests/test_models.py:\n``` python\n\n    def test_lists_can_have_owners(self):\n        user = User.objects.create(email='a@b.com')\n        list_ = List.objects.create(owner=user)\n        self.assertIn(list_, user.list_set.all())\n```\n\n\n\nlists/models.py:\n```python\n\nclass List(models.Model):\n    owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nMaybe we think everything should work now, we pop back up to run the FT:\n\n\n\n```\n======================================================================\nERROR: test_logged_in_users_lists_are_saved_as_my_lists\n(functional_tests.test_my_lists.MyListsTest)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/home/harry/workspace/book-example/functional_tests/test_my_lists.py\",\n  line 48, in test_logged_in_users_lists_are_saved_as_my_lists\n    self.browser.find_element_by_link_text('First list 1st item').click()\n    [...]\nselenium.common.exceptions.NoSuchElementException: Message: Unable to locate\nelement: {\"method\":\"link text\",\"selector\":\"First list 1st item\"}\n[...]\n----------------------------------------------------------------------\n```\n\n\nOh no!  what's happening?\n\n  firefox ~/Dropbox/Book/images/intermediate-ws-ft-fail-ss1.png \n\n\n\n\n\n\n\n\n\n\n\n\n\nlists/templates/my_lists.html:\n```html\n\n  <li><a href=\"{{ list.get_absolute_url }}\">{{ list.name }}</a></li>\n\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nLists need a name attribute (what we'd programmed by wishful thinking)\n\n\n\nlists/tests/test_models.py:\n```python\n\n    def test_list_name_is_first_item_text(self):\n        list_ = List.objects.create()\n        Item.objects.create(list=list_, text='first item')\n        Item.objects.create(list=list_, text='second item')\n        self.assertEqual(list_.name, 'first item')\n```\n\nlists/models.py:\n```python\n\nclass List(models.Model):\n    # ...\n\n    @property\n    def name(self):\n        return self.item_set.first().text\n```\n\n\n\n\n\nand we're done!\n\n\n\n\n\n\n\n\n\n# Discussion\n\n\n* benefits of outside-in vs bottom-up\n* when might it not work?\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# Break\n\n\n\n\n\n\n\n\n\n\n\n\n\n# Next challenge: redo it with a more \"purist\" approach\n\n* how many people have never used mocks?\n\n* live demo of mocks\n\n    git checkout intermediate-workshop-part2\n    python manage.py test lists\n\n* Objective: get this test to pass *before* we move onto the models layer\n\nTips:\n* Test will probably need re-writing to use mocks\n\n* `new_list` view has two \"collaborators\", \n  - `ItemForm` \n  - the `List` class\nyou will probably need to mock one or both of these\n  - you need to check a list object is created\n  - you need to check it has the owner assigned to it\n  - either inside the `objects.create()` call,\n  - or *before* calling `list_.save()`\n\n    self.assertEqual(mock_list.save.called, True)\n\n* No need to use mocks once you get to the models layer!\n\n(for bonus points: testing that things happen in a particular order involves an advanced mocking technique involving custom `side_effect` functions. Look these up in the docs and try and use them, if you finish early)\n\n\nThink about:\n- are these mocky tests nice to work with?\n- how are they driving the design, and the workflow?\n\n\n\n# Mocks and \"Listen to your tests\" discussion.\n\n\n\nDid you end up with a test like this?\n\nlists/tests/test_views:\n```python\n\n    @patch('lists.views.List')\n    def test_list_owner_is_saved_if_user_is_authenticated(self, mockListClass):\n        mock_list = List.objects.create()\n        mock_list.save = Mock()\n        mockListClass.return_value = mock_list\n        request = HttpRequest()\n        request.user = Mock()\n        request.user.is_authenticated.return_value = True\n        request.POST['text'] = 'new list item'\n\n        def check_owner_assigned():\n            self.assertEqual(mock_list.owner, request.user)\n        mock_list.save.side_effect = check_owner_assigned\n\n        new_list(request)\n\n        mock_list.save.assert_called_once_with()\n\n```\n\nyuck!\n\n\nWhy is this so hard? What are the tests trying to tell us?\n\n\nlists/views.py:\n```python\ndef new_list(request):\n    form = ItemForm(data=request.POST)\n    if form.is_valid():\n        list_ = List()\n        if request.user.is_authenticated():\n            list_.owner = request.user\n        list_.save()\n        form.save(for_list=list_)\n        return redirect(list_)\n    else:\n        return render(request, 'home.html', {\"form\": form})\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nWhat if it was easier?\n\n\n\nlists/views.py:\n```python\ndef new_list(request):\n    form = NewListForm(data=request.POST)\n    if form.is_valid():\n        list_ = form.save(owner=request.user)\n        return redirect(list_)\n    return render(request, 'home.html', {'form': form})\n```\n\n\n\nAnd then we could write a \"nice\" mocky test like this, rather than a nasty one...\n\n\nlists/tests/test_views.py:\n```python\n    @patch('lists.views.NewListForm')\n    def test_saves_form_with_owner_if_form_valid(self, mockNewListForm):\n        mock_form = mockNewListForm.return_value\n        mock_form.is_valid.return_value = True\n        new_list(self.request)\n        mock_form.save.assert_called_once_with(owner=self.request.user)\n```\n\n\n\n\n\n\nof course if we're going to go the whole way, we would rewrite all the tests:\n\nlists/tests/test_views.py:\n```python\n    def test_passes_POST_data_to_NewListForm(self, mockNewListForm):\n\n    def test_saves_form_with_owner_if_form_valid(self, mockNewListForm):\n\n    def test_does_not_save_if_form_invalid(self, mockNewListForm):\n\n    @patch('lists.views.redirect')\n    def test_redirects_to_form_returned_object_if_form_valid(\n\n    @patch('lists.views.render')\n    def test_renders_home_template_with_form_if_form_invalid(\n```\n\n\n\n\n\n\n\n\n\n\n\n\nSame story at the forms layer:\n\nlists/forms.py:\n```python\nclass NewListForm(models.Form):\n\n    def save(self, owner):\n        list_ = List()\n        if owner.is_authenticated():\n            list_.owner = owner\n        list_.save()\n        item = Item()\n        item.list = list_\n        item.text = self.cleaned_data['text']\n        item.save()\n```\n\n\nWhich leads to tests that look like this:\n\n\nlists/forms.py:\n```python\n\nclass NewListFormTest(unittest.TestCase):\n\n    @patch('lists.forms.List')  #1\n    @patch('lists.forms.Item')  #2\n    def test_save_creates_new_list_and_item_from_post_data(\n        self, mockItem, mockList  #3\n    ):\n        mock_item = mockItem.return_value\n        mock_list = mockList.return_value\n        user = Mock()\n        form = NewListForm(data={'text': 'new item text'})\n        form.is_valid() #4\n\n        def check_item_text_and_list():\n            self.assertEqual(mock_item.text, 'new item text')\n            self.assertEqual(mock_item.list, mock_list)\n            self.assertTrue(mock_list.save.called)\n        mock_item.save.side_effect = check_item_text_and_list  #5\n\n        form.save(owner=user)\n\n        self.assertTrue(mock_item.save.called)  #6\n\n```\n\n\nyuck!  again.\n\nBut, again, this is a call to \"listen to our tests\"\n\n\n\nlists/forms.py:\n```python\n\nclass NewListForm(ItemForm):\n\n    def save(self, owner):\n        if owner.is_authenticated():\n            List.create_new(first_item=self.cleaned_data['text'], owner=owner)\n        else:\n            List.create_new(first_item=self.cleaned_data['text'])\n```\n\n\nEnd result:\n\n* Cleaner code at each layer\n* views only handle extracting info from requests, choosing what kind of response to return\n* forms handle validation of that data, and then hands off to..\n* models layer is in charge of actually saving objects and relationships between them\n* we can write tests at the model layer without mocks\n\n\n\n\n# The pitfalls of mocking: debugging challenge!\n\ngit checkout intermediate-workshop-part3\n\n\nCan you figure out what went wrong?\n\n\n\n* lesson:  mocking requires clear identification of contracts, and testing same.\n\n* keeping: some mid-level integration tests around is a good idea.\n\n\n\n# Recap + discussion:  the pros and cons of different types of test\n\n\n\n**Functional tests:**\n    * Provide the best guarantee that your application really works correctly,\n    from the point of view of the user.\n    * But: it's a slower feedback cycle,\n    * And they don't necessarily help you write clean code.\n\n* **Integrated tests** (reliant on, e.g., the ORM or the Django Test Client):\n    * Are quick to write,\n    * Easy to understand,\n    * Will warn you of any integration issues,\n    * But may not always drive good design (that's up to you!).\n    * And are usually slower than isolated tests\n\n**Isolated (\"mocky\") tests:**\n    * These involve the most hard work.\n    * They can be harder to read and understand,\n    * But: these are the best ones for guiding you towards better design.\n    * And they run the fastest.\n\n\n\n\n\n\n\n\n\n\n\n\n\n# THE END!\n\n\nwww.obeythetestinggoat.com\n\nDISCOUNT CODE for oreilly.com: \"AUTHD\"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# misc notes\n\ncolor: apprentice? colorful? beachcomber?\n\n\n"
  },
  {
    "path": "workshops/js-testing-with-jasmine.asciidoc",
    "content": "== Dipping Our Toes, Very Tentatively, into JavaScript\r\n\r\n\r\n[quote, 'John Calvin (as portrayed in http://onemillionpoints.blogspot.co.uk/2008/08/calvin-and-chipmunks.html[Calvin and the Chipmunks])']\r\n______________________________________________________________\r\nIf the Good Lord had wanted us to enjoy ourselves, he wouldn't have granted us\r\nhis precious gift of relentless misery.\r\n______________________________________________________________\r\n\r\n\r\nOriginal description:\r\n\r\n'1/2 day: \"JavaScript testing\".  Working in Python is just too nice, so every\r\nso often we have to work with JavaScript just to remind ourselves of how\r\nlucky we are.  What to do when our javascript codebase starts to get more\r\ncomplex?  This course will cover the essentials of javascript testing, from\r\nsetting up a test runner, organising fixtures, ensuring isolation between\r\ntests, through testing AJAX calls and mocking with javascript.'\r\n\r\nInitial setup:\r\n~~~~~~~~~~~~~~\r\n\r\n----\r\ngit checkout -b js-workshop\r\ngit fetch --tags\r\ngit reset --hard js-workshop-start\r\n\r\npython manage.py test\r\n# should show one test failure:\r\n[...]\r\n    self.assertFalse(error.is_displayed())\r\nAssertionError: True is not false\r\n----\r\n\r\n\r\nIntro\r\n~~~~~\r\n\r\n* Not a Js expert\r\n* Who is familiar with: JavaScript (or never used?), jQuery, jasmine, angular,\r\n require.js\r\n\r\n\r\nObjectives\r\n~~~~~~~~~~\r\n\r\n* Get a basic js test environment going,\r\n* Meet some of the common js testing challenges\r\n* See how it fits in with a Python TDD cycle\r\n\r\n\r\n\r\nContext and site tour\r\n~~~~~~~~~~~~~~~~~~~~~\r\n\r\nSame site. New bits: \r\n\r\n* validation errors (not dynamic)\r\n* forms\r\n\r\nThen:\r\n\r\n* current test failure / today's goals.\r\n  'functional_tests/test_list_item_validation.py'\r\n\r\n\r\n\r\nExercise 1: Get it working!\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nNo need for tests, just hack in some javascript, and see if you can get our\r\nFT passing\r\n\r\nTo run just our test:\r\n\r\n----\r\npython3 manage.py test functional_tests.test_list_item_validation\r\n----\r\n\r\nThen run all the tests with `manage.py test` to make sure we didn't\r\nbreak anything.\r\n\r\nDo a `git stash` to reset your code when you're done.\r\n\r\ntips:\r\n\r\n    mkdir ../database  # to fix runserver/migrate errors. because reasons.\r\n\r\n* use 'lists/static' for, eg, jquery\r\n\r\nfor bonus points:\r\n- if you used jquery, do it without.  or vice-versa.\r\n\r\n\r\nSetting Up the Jasmine browser-based test runner\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nDiscussion:\r\n\r\n* Python's relatively small range of testing tools\r\n* vs Javascript: YUI, jsUnit, QUnit, Mocha, Chutzpah, Karma, Jasmine, \r\n* And then you need to choose an 'assertion framework' and a\r\n'reporter', and maybe a 'mocking library', and it never ends!\r\n* and it all changes every five minutes.\r\n\r\nWe'll use http://jasmine.github.io/[Jasmine] because it's already in\r\nuse here at the org, and is increasingly popular particularly in the\r\nAngular.js world.  In the book I use QUnit, which is relatively\r\nstraighforward and make jQuery easy, but it comes at the expense of\r\nflexibility.  And it's  a bit fuddy-duddy.\r\n\r\nMake a directory called 'tests' inside 'lists/static', download the jasmine\r\nrelease, unzip it, and tidy up the folder structure a little, removing the\r\nexample code.  Also create an empty file at 'lists/static/list.js', and\r\nanother one at 'lists/static/tests/listSpec.js'.\r\n\r\nYou should end up with something like this:\r\n\r\n\r\n----\r\nlists/static\r\n├── base.css\r\n├── bootstrap\r\n│   └── [...]\r\n├── list.js\r\n└── tests\r\n    ├── jasmine-2.5.2\r\n    │   ├── boot.js\r\n    │   ├── console.js\r\n    │   ├── jasmine.css\r\n    │   ├── jasmine_favicon.png\r\n    │   ├── jasmine-html.js\r\n    │   └── jasmine.js\r\n    ├── listSpec.js\r\n    └── SpecRunner.html\r\n----\r\n\r\nAnd then edit 'SpecRunner.html' to tweak the paths for our new folder\r\nstructure:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/SpecRunner.html\r\n[source,html]\r\n----\r\n<!DOCTYPE html>\r\n<html>\r\n<head>\r\n  <meta charset=\"utf-8\">\r\n  <title>JavaScript unit tests</title>\r\n\r\n  <link rel=\"shortcut icon\" type=\"image/png\" href=\"jasmine-2.5.2/jasmine_favicon.png\">\r\n  <link rel=\"stylesheet\" href=\"jasmine-2.5.2/jasmine.css\">\r\n  <script src=\"jasmine-2.5.2/jasmine.js\"></script>\r\n  <script src=\"jasmine-2.5.2/jasmine-html.js\"></script>\r\n  <script src=\"jasmine-2.5.2/boot.js\"></script>\r\n\r\n  <script src=\"../list.js\"></script>\r\n  <script src=\"listSpec.js\"></script>\r\n</head>\r\n\r\n<body>\r\n</body>\r\n</html>\r\n----\r\n\r\nBoth 'list.js' and 'listSpec.js' are empty, but we should still see something\r\nlike this if you load 'SpecRunner.html' up in a browser:\r\n\r\n.Basic Jasmine Spec runner with no specs\r\nimage::images/empty_jasmine_specrunner.png[\"Jasmine Spec Runner with no specs\"]\r\n\r\nNOTE: don't spend more than 5 mins or so fiddling around with the directory\r\n    structure, especially if you already have a jasmine testing setup that\r\n    you use every day.  This part is mainly useful for people who've never\r\n    seen jasmine.  Here's your \"cheat\" checkout:\r\n    `git reset --hard jasmine-workshop-ready`\r\n\r\n\r\nSmoke test\r\n^^^^^^^^^^\r\n\r\nEdit 'listSpec.js' and create a \"smoke test\"\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\ndescribe(\"list js\", function() {\r\n  it(\"should have working maths\", function() {\r\n    expect(1 + 1).toEqual(2);\r\n  });\r\n});\r\n----\r\n\r\n// harry to live-code this based on copy-paste example from jasmine site?\r\n// explain as we go\r\n\r\n\r\nAnd you should see something like this:\r\n\r\n\r\n.Maths works\r\nimage::images/maths_works.png[\"Jasmine with 1 passing spec\"]\r\n\r\n\r\nAnd if you deliberately break the test you should get this:\r\n\r\n.Maths is broken\r\nimage::images/maths_broken.png[\"Jasmine with 1 failing spec\"]\r\n\r\n\r\n\r\nConcepts recap\r\n^^^^^^^^^^^^^^\r\n\r\n* \"spec files\" aka tests\r\n* \"source files\" ie your real javascript\r\n* \"SpecRunner.html\" ie the browser-based test runner\r\n    - as we'll see later you can also have a command-line test runner\r\n* smoke test is always a nice way to try out any testing framework.\r\n* Jasmine \"BDD\" tests:\r\n    * `describe` = test class\r\n    * `it` = test\r\n    * `expect` + `toEqual` = assert\r\n\r\n\r\n\r\n.Basic Jasmine setup: advanced exercises\r\n****************************************\r\n\r\n1. Browse the http://jasmine.github.io/2.5/introduction.html[Jasmine docs] to\r\n  learn more about Jasmine\r\n\r\n2. Checkout out https://github.com/jasmine/jasmine-py[Jasmine-Py], \r\n  `pip install jasmine`, and see if you can get its alternative test runner\r\n  working\r\n\r\n****************************************\r\n\r\n\r\nAdding jQuery\r\n~~~~~~~~~~~~~\r\n\r\nDownload from jquery.com and put it in 'lists/static/jquery-3.1.1.min.js' \r\n(don't worry if you get a slightly different version)\r\n\r\n\r\nAdd it to the SpecRunner\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/SpecRunner.html\r\n[source,diff]\r\n----\r\n@@ -10,6 +10,7 @@\r\n   <script src=\"jasmine-2.5.2/jasmine-html.js\"></script>\r\n   <script src=\"jasmine-2.5.2/boot.js\"></script>\r\n \r\n+  <script src=\"../jquery-3.1.1.min.js\"></script>\r\n   <script src=\"../list.js\"></script>\r\n   <script src=\"listSpec.js\"></script>\r\n </head>\r\n----\r\n\r\n\r\nAnd also add a bit of HTML to represent the form\r\nand its error div which we want to hide:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/SpecRunner.html\r\n[source,diff]\r\n----\r\n@@ -16,5 +16,11 @@\r\n </head>\r\n \r\n <body>\r\n+\r\n+  <form>\r\n+    <input name=\"text\" />\r\n+    <div class=\"has-error\"></div>\r\n+  </form>\r\n+\r\n </body>\r\n </html>\r\n----\r\n\r\n\r\nNow let's use jQuery in our test:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\n  it(\"should be able to use jquery to create and hide things\", function() {\r\n    expect( $('.has-error').is(':visible') ).toBe(true);\r\n    $('.has-error').hide();\r\n    expect( $('.has-error').is(':visible') ).toBe(false);\r\n  });\r\n----\r\n\r\n\r\nGlobal state:  the key challenge of js testing. Lesson 1: HTML fixtures\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\n* what happens if we dupe the test?\r\n\r\n----\r\n2 specs, 1 failure\r\n\r\nlist js should be able to run the same test twice\r\n  Expected false to be true.\r\n----\r\n\r\n\r\n* need some way of re-setting the DOM before each test?\r\n* or we only do things that are non-destructive\r\n\r\n--> use `beforeEach` and `afterEach` and jQuery append/remove\r\n--> not only solution!\r\n\r\n\r\n.HTML fixtures advanced exercise\r\n****************************************\r\n\r\nCheck out \"jasmine-jquery\" and \"jasmine-fixtures\" as alternative ways\r\nof loading fixtures.\r\n\r\n****************************************\r\n\r\n\r\n\r\n\r\nTesting our actual intended behaviour\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nReplace our tests with what we actually want to test:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\n  it(\"should hide errors on keypress\", function() {\r\n    $('#testform input').trigger('keypress');\r\n    expect( $('.has-error').is(':visible') ).toBe(false);\r\n  });\r\n----\r\n\r\nAnd maybe this too?  Always check the negative case!\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\n  it(\"should not hide errors unnecessarily\", function() {\r\n    expect( $('.has-error').is(':visible') ).toBe(true);\r\n  });\r\n----\r\n\r\n\r\nThe key challenge of js testing. Lesson 2: Execution order\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n* What's the simplest thing we can do?\r\n* Does your hacked-in implementation from earlier work?\r\n* If not, why not?  Debug with some `console.logs`\r\n* If you didn't already, try an 'jquery-wait-for-document-ready' invocation.\r\n  Does that help?\r\n\r\nTips:\r\n\r\n* Be clear on what gets executed when:\r\n* When does our HTML fixture get added?  When do we attach our event listeners?\r\n* Want to ask jQuery what event listeners are attached to an element?\r\n\r\n[source,javascript]\r\n----\r\n$._data($('selector')[0], 'events')\r\n----\r\n\r\n\r\n\r\nBuilding a solution that works\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nWe'll have to bite the bullet and use an initialization function.  It's\r\na common pattern.\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\n  it(\"should hide errors on keypress\", function() {\r\n    initialize();\r\n    $('#testform input').trigger('keypress');\r\n    expect( $('.has-error').is(':visible') ).toBe(false);\r\n  });\r\n----\r\n\r\n* Get this working\r\n* How can we improve on it?\r\n\r\n\r\n.JavaScript testing: more advanced challenges\r\n**********************************************\r\n\r\n1. Add onclick handler, with fts and unit tests\r\n2. Install jslint or jshint into your editor and get it to check your code.\r\n3. Rewrite everything to not use jQuery\r\n4. Require.js?\r\n\r\n**********************************************\r\n\r\n\r\nFinal discussion\r\n~~~~~~~~~~~~~~~~\r\n\r\n----\r\ngit stash show -p\r\n# vs\r\ngit diff js-workshop-start\r\n----\r\n\r\nCompare our finalised JavaScript with our first hacked-in solution.  Was it\r\nworth it?  If not in the immediate, how might it be worth it in the longer run?\r\n\r\n\r\n\r\nRecap: JavaScript Testing Notes\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\n* One of the great advantages of Selenium is that it allows you to test that\r\n  your JavaScript really works, just as it tests your Python code.\r\n\r\n* There are many JavaScript test running libraries out there.  Jasmine is\r\n  popular with the Angular.js crowd.  QUnit is a nice simple one if you're\r\n  only using jQuery\r\n\r\n* No matter which testing library you use, you'll always need to find solutions\r\n  to the main challenge of JavaScript testing, which is about 'managing global\r\n  state'.  That includes:\r\n    - the DOM / HTML fixtures\r\n    - namespacing\r\n    - understanding and controlling execution order.\r\n\r\n* I don't really mean it when I say that JavaScript is awful. It can actually\r\n  be quite fun.  But I'll say it again: make sure you've read\r\n  <<jsgoodparts,'JavaScript: The Good Parts'>>.\r\n\r\n\r\n\r\nBonus round: Ajax and Mocking\r\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nStill with me?  Glutton for punishment?  OK, let's do a little more.\r\n\r\n    git checkout js-workshop-bonus-start\r\n\r\nHere's a new file:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/api.py\r\n[source,html]\r\n----\r\ndef list(request, list_id):\r\n    list_ = List.objects.get(id=list_id)\r\n    if request.method == 'POST':\r\n        form = ExistingListItemForm(for_list=list_, data=request.POST)\r\n        if form.is_valid():\r\n            form.save()\r\n            return HttpResponse(status=201)\r\n        else:\r\n            return HttpResponse(\r\n                json.dumps({'error': form.errors['text'][0]}),\r\n                content_type='application/json',\r\n                status=400\r\n            )\r\n    item_dicts = [\r\n        {'id': item.id, 'text': item.text}\r\n        for item in list_.item_set.all()\r\n    ]\r\n    return HttpResponse(\r\n        json.dumps(item_dicts),\r\n        content_type='application/json'\r\n    )\r\n----\r\n\r\nYou now have a new API view you can use, for an existing list, to do retrieving\r\nand adding list items via REST calls.  Can we test-drive the development\r\nof our Ajax in 'list.js'?\r\n\r\nWe won't worry about errors for now.\r\n\r\n\r\n\r\nTesting an Ajax get with jasmine-ajax-mock\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nHere's our first test, for retrieving list items via ajax GET and populating\r\nthe list table:\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/listSpec.js\r\n[source,javascript]\r\n----\r\n  it(\"should retrieve items via ajax and fill in lists table on page load\", function () {\r\n    var url = '/listitemsapi/';\r\n    window.Superlists.startAjax(url);\r\n\r\n    expect(jasmine.Ajax.requests.mostRecent().url).toBe(url);\r\n\r\n    var rowsJson = JSON.stringify([\r\n        {'id': 101, 'text': 'item 1 text'},\r\n        {'id': 102, 'text': 'item 2 text'},\r\n    ]);\r\n\r\n    jasmine.Ajax.requests.mostRecent().respondWith({\r\n      \"status\": 200,\r\n      \"contentType\": 'application/json',\r\n      \"responseText\": rowsJson\r\n    });\r\n\r\n    var rows = $('#id_list_table tr');\r\n    expect(rows.length).toEqual(2);\r\n    var row1 = $('#id_list_table tr:first-child td');\r\n    expect(row1.text()).toEqual('1: item 1 text');\r\n    var row2 = $('#id_list_table tr:last-child td');\r\n    expect(row2.text()).toEqual('2: item 2 text');\r\n  });\r\n----\r\n\r\nLet's get it passing!\r\n\r\n\r\nCheating and working backwards: test a POST that already exists\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\n    git checkout js-workshop-bonus-part2\r\n\r\nFunctional Tests should pass.\r\n\r\n\r\n[role=\"sourcecode\"]\r\n.lists/static/tests/list.js\r\n[source,javascript]\r\n----\r\nwindow.Superlists.startAjax = function (url) {\r\n  getListItems(url);\r\n  var form = $('input[name=\"text\"]').parent('form');\r\n  form.on('submit', function (event) {\r\n    event.preventDefault();\r\n    $.post(url, {\r\n      'text': form.find('input[name=\"text\"]').val(),\r\n      'csrfmiddlewaretoken': form.find('input[name=\"csrfmiddlewaretoken\"]').val(),\r\n    }).done(function () {\r\n      getListItems(url);\r\n    });\r\n  });\r\n};\r\n----\r\n\r\nCan we reverse-engineer  some js tests for that?\r\n\r\nWhat about for repopulating the table?\r\n\r\n//TODO: add placeholder code for second test\r\n\r\nTips:\r\n\r\n* use `toEqual` not `toBe`.\r\n\r\n\r\nFinally: Mocking with JavaScript\r\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n\r\nWhat about our `getListItems` helper function?  Could we de-duplicate our tests\r\nby using some mocks!  Yes indeed we could!  (whether it's actually worth it is\r\ndebatable, but we may as well learn the technique).\r\n\r\n* use spyOn(window, 'getListItems') (or add it to Superlists)\r\n* refactor two tests\r\n* or, debatably, leave one sanity-check test, add a second.\r\n* don't forget to deliberately break things to see them fail.\r\n\r\n    git checkout js-workshop-bonus-part3 if you need to\r\n\r\n"
  },
  {
    "path": "workshops/pycon.uk.2015.dirigible-talk.md",
    "content": "Title\nCategory\nDuration\nDescription\nIf your talk is accepted this will be made public and printed in the program. Should be one paragraph, maximum 400 characters.\n\nAudience\nWho is the intended audience for your talk? (Be specific; \"Python programmers\" is not a good answer to this question.)\n\nPython level\nLevel of audience expertise assumed in Python.\n\nObjectives\nWhat will attendees get out of your talk? When they leave the room, what will they know that they didn't know before?\n\nDetailed Abstract\nDetailed description. Will be made public if your talk is accepted.\n\nOutline\n\nAdditional notes\n\nAnything else you'd like the program committee to know when making their selection: your past speaking experience, open source community experience, etc.\n\n\nAdditional requirements\nPlease let us know if you have any specific needs (A/V requirements, multiple microphones, a table, etc). Note for example that 'audio out' is not provided for your computer unless you tell us in advance.\n\n"
  },
  {
    "path": "workshops/pycon.uk.2015.tutorial-beginners.md",
    "content": "Title:\n    TDD with Django, from scratch: a beginners intro to testing and web development\n\nCategory:\n    Testing\n\nPython level:\nLevel of audience expertise assumed in Python.\n    Novice\n\nDomain level:\nLevel of audience expertise assumed in the presentation's domain.\n    Novice\n\n\nDescription\nIf your talk is accepted this will be made public and printed in the program. Should be one paragraph, maximum 400 characters.\n    A beginner's introduction to testing and web development with Django. We'll build a simple web app, from scratch, but with full TDD, including functional testing with Selenium and unit testing Django's views, templates, and models. Some familiarity with Python is desirable, but no prior knowledge of Django or testing is assumed.\n\nAudience\nWho is the intended audience for your talk? (Be specific; \"Python programmers\" is not a good answer to this question.)\n    The target is anyone interested in learning TDD, and how it is applied for a more rigorous and structured approach to web development.  It doesn't assume any prior knowledge of testing at all, or even of Django/web development. I've even had total beginners attend and enjoy it, although for them the learning curve is quite steep.  I usually ask people to pair up for the tutorial, and I tried to make sure that beginners are sat next to someone more experienced\n\n\nObjectives\nWhat will attendees get out of your talk? When they leave the room, what will they know that they didn't know before?\n    Attendees will get a practical, hands-on introduction to TDD and Django, including how to drive a real web browser with selenium, for functional/ end-to-end testing of test UI and user interactions, as well as how to write unit tests using Django's testing tools and the standard library's unittest.  We'll also cover the basics of django and web development, including views, templates, and the Django ORM.\n\nDetailed Abstract\n\nThe aim is to cover the basics of setting up a simple Django site, but using full, rigorous TDD at every step along the way.\n\nThe tutorial is based on the first few chapters of my book, which is available (even for free!) online for you to follow up with after the session, so that you can embed what you've learned.  [www.obeythetestinggoat.com](http://www.obeythetestinggoat.com)\n\nWe'll learn how to set up functional tests with Selenium, how to set up Django, how to run Django unit tests, how TDD actually works in practice, the unit test / code cycle where we re-run the tests after each tiny, incremental change to the code, as well as all the basics of Django like views, models and templates. We'll talk about what to test, what not to test, what the point of all this testing is anyway, and I promise to make it all at least moderately entertaining.\n\nPlus it's all in Python 3!\n\n\nOutline\n\n* (help all the people who've, inevitably, failed to install the required software.  5-10 mins)\n* Intro and welcome.  A brief TDD story.  Propose pair programming, help beginners find more experience ppl to pair with. 5-10 mins\n* First FT. Selenium, django runserver. 10-15 mins\n* move to unittest. 10 mins\n* First django view and unit test. 20 mins\n* Refactoring, using templates. 20-30 mins\n* Models and the database. (whatever time is left!)\n\n\nMore info\n\n*Pre-requisites!*\n\nIt is absolutely vital that you come with the required software pre-installed on your PC. It's not fair on everyone else to be sitting there while we wait for you to download stuff from PyPI over the conference network. So, make sure you have the following installed on your PC:\n\nPython 3.4\nGit\nFirefox\nSelenium\nDjango\n\nHave a look at the preface section in my book for [detailed installation instructions](http://chimera.labs.oreilly.com/books/1234000000754/pr02.html#_required_software_installations), and just [email me](mailto:obeythetestinggoat@gmail.com) if you need help.\n\nAdditional notes\nOther than a few minor tweaks, this would be the same tutorial as last year.  Trusty classic!\n\nAdditional requirements\n\nStudent Handout\n\n\n"
  },
  {
    "path": "workshops/pycon.us.2015.study-group.md",
    "content": "# Title:\nTDD study group: Goat herding for beginner and intermediate testers\n\n# Category:\nTesting\n\n# Python level:\nNovice\n\n# Domain level:\nNovice\n\n\n# Description\nA study group based on the book \"TDD with Python\" (available at obeythetestinggoat.com).  Attendees will be able to go through the book alone or in pairs, at their own pace.  Three suggested \"tracks\" are provided (Beginner, Intermediate, and \"Devops\") to help pick a path through the book's 22 chapters.  The author and other experienced TDDers will be on hand to facilitate and answer questions.\n\n# Audience\nBeginners or Intermediate-level testers looking to deepen their TDD skills.\n\n# Objectives\n\n    What will attendees get out of your talk? When they leave the room, what will they know that they didn't know before?*\n\nBeginners should be able to get a solid understanding of TDD, unit testing and functional testing, and some of the basics of web development too.  Intermediate-level testers should be able to experiment with more advanced techniques, like mocking, testing against servers,  using tests to verify server deployments, testing javascript, testing 3rd party API integrations, setting up CI, and a hands-on investigation of the pros and cons of isolated unit tests against more integrated tests.\n\n# Detailed Abstract\n\n[The book](http://www.obeythetestinggoat.com) has 22 chapters in total, and can take a reader from complete TDD beginner up through a wide range of intermediate techniques, like mocking, javascript testing, outside-in TDD, test isolation, test-driven server deployment automation, continuous integration, 3rd-party API integration, and more.  Rather than have to follow a fixed path through a subset of these topics, and find yourself waiting for those less experienced to catch up, or lost when the rest of the class pulls ahead of you, come along and choose your own path, and progress at your own speed.\n\nWhy come and do this in a tutorial session, rather than just go through it at home in my own time, I hear you ask?  Several possible reasons\n\n* The author, and several other experienced TDD'ers will be on hand to answer your questions and help you if you get stuck.\n\n* Several different tracks will be offered, to help you find the content you're interested in, and save you from wasting time on material that's too simple or too advanced for you.\n\n* You will have the option of pairing up with someone else in your track, to be able to help each other through the chapters (nothing helps you learn like having to explain what you know to someone else!)\n\n* Plus, will you, *actually* find the time to go through this stuff on your own?  Booking a slot in a tutorial is a wonderful way of overcoming prevarication, and, no matter how much you get through in the session, there'll be plenty more of the book for you to go through when you get back home...\n\n\n# Outline\n\nAfter 10 minutes of intro and welcome, introducing myself and the teaching assistants, I'll give a brief overview of the whole book, and then outline the suggested tracks.  Once people have chosen a track, I'll encourage them to pair-program within their track\n\n## Beginner track\n\n* Start from beginning!\n* pre-requs to get stuff installed\n* Then as far as you can get!  1-5 is good progress, then 6, 7, then skip to 10 if you don't want the brain-damage of server deploys\n\n\n## Intermediate TDD skills track\n\n* Chapter 7, layout and styling\n* Chapter 13, Testing JavaScript\n* Chapter 18, Outside-In TDD\n* Chapter 19, \"Listen to your tests\"\n* Chapter 22, Fast Test Slow test\n* Appendix E: investigate a BDD tool (hint: use behave and django-behave)\n\n\n## Devops track\n\n* Chapter 7, layout and styling\n* Chapter 8, manual provisioning\n* Chapter 9, automated deployments\n* Chapter 17, Fixtures, Logging and server-side debugging\n* Appendix C, Ansible\n\n\n## Mocking Overload Track\n\n* Chapter 15, Mocking in JavaScript\n* Chapter 16, Mocking in Python\n* Chapter 18, Outside-In TDD\n* Chapter 19, \"Listen to your tests\"\n\n\n# More info\n\nThere are no absolute pre-requisites for this tutorial, as the book contains \n[detailed installation instructions](http://chimera.labs.oreilly.com/books/1234000000754/pr02.html#_required_software_installations) for all the software\nyou'll need.  With that said, if you want to save some time sitting around waiting to download software over the saturated conference wifi, you might find it worthwhile going through said [instructions](http://chimera.labs.oreilly.com/books/1234000000754/pr02.html#_required_software_installations) before you show up!\n\nAll the source code for the book can be found at \n\n    https://github.com/hjwp/book-example/\n\nAnd each chapter has its own branch, eg `chapter_04_philosophy_and_refactoring`, `chapter_15`, `appendix_III` etc\n\n\n# Additional notes\n\nThe \"study group\" format is a reaction to one of the criticisms of the general tutorial format, which is that not everyone learns at the same speed, so you usually find that some people are bored and some people are a bit lost when you try and teach the same materials to a whole group.\n\nHowever, I gather that \"study-group\" style tutorials don't always go down well -- people complain that they're not structured enough, or that there's no additional value compared to just doing it alone at home.  So that's why I tried to anticipate that possible criticism, by\n\n* Providing a structure, by outlining different \"tracks\"\n* Encouraging people to pair-program\n* Making sure that I'm on hand, and other TDD experts too, to help people when they get stuck\n\nI'm experimenting with this format for the first time at PyconUK which is next weekend (19th-21st September), so I'll be able to write up a few experience notes after that, and include them here.\n\n\nStudent Handout\n\n\n"
  },
  {
    "path": "workshops/study-group.md",
    "content": "www.obeythetestinggoat.com\ngithub.com/hjwp/book-example/  # branches chapter\\_XX\n\n# Beginner track\n* Start from beginning!\n* pre-requs to get stuff installed\n* Then as far as you can get!  1-5 is good progress, then 6, 7, then skip to 10 if you don't want the brain-damage of server deploys\n\n\n# Intermediate TDD skills track\n* Chapter 13, Testing JavaScript\n* Chapter 18, Outside-In TDD\n* Chapter 19, \"Listen to your tests\"\n* Chapter 21, The Selenium Page pattern\n* Chapter 22, Fast Test Slow test\n* Appendix E: investigate a BDD tool (hint: use behave and django-behave)\n\n\n# Devops track\n* Chapter 7, layout and styling\n* Chapter 8, manual provisioning\n* Chapter 9, automated deployments\n* Chapter 17, Fixtures, Logging and server-side debugging\n* Appendix C, Ansible\n\n"
  },
  {
    "path": "workshops/working-incrementally-handout.md",
    "content": "# Day 1 morning: Working incrementally\n\n> 1/2 day: \"working incrementally\".  Writing tests and getting them to pass is\n> all very well, but it's still easy to get into trouble.  Have you ever set\n> off to make some changes to your code, and found yourself several hours into\n> the effort, with changes to half a dozen files, everything is broken, many\n> tests failing, and starting to worry about how you're ever going to get\n> things working again?  This course is aimed at teaching the technique of\n> working incrementally -- how to make changes to a codebase in small steps,\n> going from \"working state to working state\", based on a series of practical\n> examples.\n\n## Prep (which naturally everyone has already done, naturally)\n\n```\nmkvirtualenv tdd-workshop --python=python3\npip install 'django<1.11' 'selenium<3'\ngit clone https://github.com/hjwp/book-example.git tdd-workshop\ncd tdd-workshop\ngit checkout chapter_06\npython manage.py test # should all pass.\n```\n\n\n## Intro\n\nCheck out the code:\n\n    git checkout incremental-workshop-start\n    git checkout -b working-incrementally\n\n\nHarry to give a quick tour of the site, outline what works now,\nwhat we want to achieve.\n\n\n## First exercise: hack it together as quickly as poss!\n\nGet multiple lists for multiple people working!  Each should have its own URL\n\n* Try to get the FT to pass\n* And, ideally, with a set of passing unit tests too (but worry about them\n  later if you like)\n\n\nTips:\n\n* to run just the FT:\n\n    python manage.py test functional_tests\n\n* manage.py makemigrations after any models.py changes\n  - don't worry about default values, just enter 1, we don't care about existing db data\n\n* urls.py entries don't need a leading \"/\", the \"^\" does that for you\n\n\n\n## Discussion: on working incrementally\n\n* How many people got it working?\n* Was it hard?  What steps did you go through?\n* Obv this is a simple example.\n\n\n## Quick overview: what we want to achieve.  Breaking it down.\n\nNow let's do it the incremental way.\n\n  - BDUF vs lean discussion\n  - start todo list\n  - model layer\n  - brief REST discussion\n\n\nPlanned future URLS:\n\n    / \n    /lists/new\n    /lists/<list identifier>/\n    /lists/<list identifier>/add_item\n\n\n## Harry demo session 1, on working incrementally\n\nHere's our to-do list:\n\n* Adjust model so that items are associated with different lists\n* Add unique URLs for each list\n* Add a URL for creating a new list via POST\n* Add URLs for adding a new item to an existing list via POST\n\nWhat can we pick off as a small, achievable task that will move\nus towards our solution without breaking anything, getting back\nto a working state asap?\n\n\n-> Harry live-code demo.\n\n\n## Break\n\nFika!\n\n\n## Second exercise:  another small chunk.\n\nWhat next?\n\n* Adjust model so that items are associated with different lists\n* *Add unique URLs for each list* (started)\n* Add a URL for creating a new list via POST (done)\n* Add URLs for adding a new item to an existing list via POST\n\n\n  git stash # or commit, or new branch, or whatever you like\n  git reset --hard incremental-workshop-step-2\n\n\ntips/useful commmands:\n\n  delete the database: rm db.sqlite3\n  re-create it:        python manage.py migrate\n  run the unit tests:  python manage.py test lists\n  run the FT:          python manage.py test functional_tests\n\nThis tag includes a couple of failing unit tests for you to get passing.\n\n  * create new url, for view that doesnt exist yet\n  * create dummy view\n  * make the unit tests pass one by one, redirect first, then saving data\n  * point the form at the new URL\n  * re-run the FT and confirm things still work\n  * red/green/refactor:\n    - remove redundant unit tests for `home_page`\n    - strip out redundant code from `home_page`\n\nIf in doubt, follow along with the book, here:\n[http://www.obeythetestinggoat.com/book/chapter_06.html#_another_url_and_view_for_adding_list_items](chapter 6) \n\n\n## Discussion of first two small steps (11:25)\n\n* How did y'all get on?\n* Any reactions?\n\n\n## Final exercise:  try and finish!\n\nOption 1: follow the book\n\n- models change first\n- unbreak all unit tests\n- regression / partial credit: now have many lists of maximum one item.\n- create new view+url for adding to existing list\n\nOption 2: Harry's alternative (untested) plan\n\n- create new url for adding to existing list first, based on only-list-in-world\n- use {% url %} entries\n- switch from hardcoded -only-list to a param in add_item and view_list\n- models change, \n- but use get_or_create in homepage to create the only list in world, and\n  redirect.\n- now switch to multiple lists\n\n\nOption 3: choose your own adventure!\n\n* always try to get back to a working state as quickly as you can\n\n* and do lots of commits!\n\n\n## Wrap-up\n\nIf I haven't mentioned them, ask about:\n\n* refactoring cat\n* the kata analogy\n* the bucket of water and the well\n\n\n"
  },
  {
    "path": "workshops/working-incrementally-notes.md",
    "content": "\n# Day 1 morning: Working incrementally\n\n> 1/2 day: \"working incrementally\".  writing tests and getting them to pass is\n> all very well, but it's still easy to get into trouble.  Have you ever set\n> off to make some changes to your code, and found yourself several hours into\n> the effort, with changes to half a dozen files, everything is broken, many\n> tests failing, and starting to worry about how you're ever going to get\n> things working again?  This course is aimed at teaching the technique of\n> working incrementally -- how to make changes to a codebase in small steps,\n> going from \"working state to working state\", based on a series of practical\n> examples.\n\n## Prep (which naturally everyone has already done, naturally)\n\n```\nmkvirtualenv tdd-workshop --python=python3\npip install 'django<1.10' selenium\ngit clone https://github.com/hjwp/book-example.git tdd-workshop\ncd tdd-workshop\ngit checkout chapter_06\npython manage.py test # should all pass.\n```\n\nAlso, do this\n\n```\ngit fetch --tags\npip install chromedriver_installer\n```\n\n\n* Intro (9.20)\n  - Start in chapter 6\n  - demo site as at end of 5, single to-do list\n  - code tour\n  - demo site as desired at end of 6, multiple to-do lists\n  - get ppl to check out code, including modified FT\n  - show how to run unit tests and FTs\n  - explain the FT\n  - give more hints about the desired solution - each list gets an id\n\n\nInstructions:\n\n    git checkout incremental-workshop-start\n    git checkout -b working-incrementally\n\n\n\n* First live-code session (10.00)\n  - pairing? depends on numbers\n  - have a go!\n  - try to get to where you want to be\n  - ideally, with a set of unit tests (but worry about them later if you like)\n  - with a passing FT\n\n\n\n\n* Discussion: on working incrementally (10.15)\n  - how many people got it working?\n  - was it hard?  what steps did you go through?\n  - obv this is a simple example.\n  - now let's do it the incremental way\n  - BDUF vs lean discussion\n  - start todo list\n  - model layer\n  - brief REST discussion\n\n    * 'Adjust model so that items are associated with different lists'\n    * 'Add unique URLs for each list'\n    * 'Add a URL for creating a new list via POST'\n    * 'Add URLs for adding a new item to an existing list via POST'\n\n\nPlanned future URLS:\n\n    / \n    /lists/new\n    /lists/<list identifier>/\n    /lists/<list identifier>/add_item\n\n\n* Demo: starting with an easy, small change (10.30)\n  * off first task unique urls, and make it simpler\n  * change redirect\n  * new url, point at home page view\n  * unit tests pass\n  * FT to check progress\n  * note regression: cannot add second element\n  * fix in form, add action=\"/\"\n  * tests pass, commit.\n  * refactor: new view\n  * refactor to remove superfluous code\n  * refactor/improve to use a separate template\n  * we get a little further, now home page of francis has edith's stuff.\n  * get to end and re-run FT again.\n  * discussion - seems like a tiny step, but actually we've laid loads of groundwork\n\n* Break. (10.45)\n\n* Second live-coding session: new URL for creating a new list via POST (11:10)\n  * point to book if we get stuck\n  * checkout tag which includes failing unit test\n  * create new url, for view that doesnt exist yet\n  * create dummy view\n  * make the unit tests pass one by one, redirect first, then saving data\n  * point the form at the new URL\n  * re-run the FT and confirm things still work\n  * red/green/refactor:\n    - remove redundant unit tests for `home_page`\n    - strip out redundant code from `home_page`\n\n\n* Discussion of first two small steps (11:25)\n  - how did y'all get on?\n  - any reactions?\n\n* Final live-coding session:  try and finish! (12:00)\n  - point to book for help\n\n* Final discussion/show-and-tell (12:30)\n\n\n"
  },
  {
    "path": "workshops/workshop.asciidoc",
    "content": "= WELCOME TO THE TEST-DRIVEN-DJANGO WORKSHOP\n= Required installations\n - Python 3, Git, Firefox\n - A virtualenv\n - Django 1.10 (`pip install \"django<1.10\"`)\n - Selenium 2 (`pip install --upgrade \"selenium<3\"`)\nUSB keys have installers for everything.\n\n* Handout: http://www.obeythetestinggoat.com/static/tdd-workshop.zip (Also on USB key)\n* If you're a *beginner*, makes sure you sit next to someone more experienced\n\n= Checking it all works\n\n    (my-virtualenv) $ python\n    Python 3.x.x (default, Feb 28 2014, 00:52:16) \n    [...]\n    >>> import django\n    >>> print( django.get_version() )\n    1.7.x\n    >>> from selenium import webdriver\n    >>> browser = webdriver.Firefox()  # will pop up a firefox window\n    >>> browser.get('http://www.google.com')\n    >>> print(browser.title)  # this should say \"Google\"\n    Google\n    >>> browser.quit()\n\n\n\n\n\n\n\n\nIntroduction\n============\n\nWho I am, and why should you listen to me?\n------------------------------------------\n\n    - recent convert, resolver, etc\n\n\nWho knows what?\n---------------\n\n    - Python - anyone v. new to it?\n\n    - Django - anyone never used it?\n\n    - TDD - unittest\n \n    - Selenium\n\n\nLaptops, tools and working\n--------------------------\n\n    - who is on Windows? Mac? Linux? VM? headless?? (the last is bad)\n\n    - who is using an IDE?\n\n\n\n\n\nThe Plan\n--------\n\n    - To run through the first few chapters of my book\n        - building a To-Do lists app\n        - but TDD all the way\n        - similar content to official Django tutorial\n\n    - Handout contains all the steps\n        - Code listings\n        - Commands\n        - Expected output\n        - I demo, then you have a go!\n\n    - Pair programming\n        - if you're up for it!\n\n    - Outline\n        - Chapter 1: First functional test with selenium, install Django\n        - Chapter 2: Switch to `unittest`\n        - Chapter 3: First unit test, url, view\n        - Chapter 4: **Refactor**: switch to using templates\n        - Chapter 5: Using the database\n        - Chapter 6: Iterate towards \"MVP\"\n\n\n\nSpeed and questions\n~~~~~~~~~~~~~~~~~~~\n\n    - We go at the speed of the slowest person\n    - Ask questions\n    - when I say \"does everyone get that\", don't just nod!\n\n\n>> all moved to main book repo\n\n\n"
  }
]