diff --git a/.gitignore b/.gitignore index d04dac0b..7b225ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,21 @@ svn-commit.tmp bin build include -lib \ No newline at end of file +lib +share +cast-offs +develop-eggs +development +*.db +*.sublime-project +*.sublime-workspace +.mr.developer.cfg +outline_improvements.txt +src +html +slides +new_mash +.buildinfo +pip-selfcheck.json +.ipynb_checkpoints +testenvs diff --git a/Makefile b/Makefile index 11bcf2d7..84b07bcc 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,17 @@ # # You can set these variables from the command line. +BINDIR = ./bin SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = $(BINDIR)/sphinx-build PAPER = BUILDDIR = build +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter @@ -29,17 +35,20 @@ help: @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: - -rm -rf $(BUILDDIR)/* + rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @@ -77,17 +86,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/InternetProgrammingwithPython.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonWebProgramming.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/InternetProgrammingwithPython.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonWebProgramming.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/InternetProgrammingwithPython" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/InternetProgrammingwithPython" + @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonWebProgramming" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonWebProgramming" @echo "# devhelp" epub: @@ -108,6 +117,12 @@ latexpdf: $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @@ -151,3 +166,19 @@ doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + + +slides: + $(SPHINXBUILD) -b slides $(ALLSPHINXOPTS) $(BUILDDIR)/slides + @echo "Build finished. The HTML slides are in $(BUILDDIR)/slides." + diff --git a/README.rst b/README.rst index 032b7338..e7769775 100644 --- a/README.rst +++ b/README.rst @@ -6,33 +6,82 @@ Introduction This package provides the source for all lecture materials for a 10-session course in Web Development using Python. -This version of the documentation is used for a one-week Python Web -Programming Workshop taught by `Cris Ewing`_. This workshop is being `offered -August 5-9, 2013`_ on the campus of the University of North Carolina in Chapel Hill, NC. +This package provides the source for all lecture materials used for the +`Internet Programming in Python`_ section of the `Certificate in Python +Programming`_ offered by the `University of Washington Professional & +Continuing Education`_ program. This version of the documentation is used for +the Winter 2016 instance of the course, Taught by `Cris Ewing`_ -.. _offered August 5-9, 2013: http://trizpug.org/boot-camp/pywebpw13/ +.. _Internet Programming in Python: http://www.pce.uw.edu/courses/internet-programming-python/downtown-seattle-winter-2016/ +.. _Certificate in Python Programming: http://www.pce.uw.edu/certificates/python-programming.html +.. _University of Washington Professional & Continuing Education: http://www.pce.uw.edu/ .. _Cris Ewing: http://www.linkedin.com/profile/view?id=19741495 +This course is taught using Python 3. + +This documentation builds both an HTML version of the course lectures (for the +students) and a set of slides (for the instructor). It uses the Python-based +documentation tool `Sphinx`_ and the `hieroglyph`_ sphinx extension. Shell +examples use `iPython` and tests are written for `pytest`. The build +environment is managed using `virtualenv` and `pip` + +.. _iPython: http://ipython.org/ +.. _Sphinx: http://sphinx-doc.org/ +.. _hieroglyph: http://docs.hieroglyph.io/en/latest/ +.. _pytest: http://pytest.org/latest/ +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ +.. _pip: https://pip.pypa.io/en/stable + Building The Documentation -------------------------- -This documentation is built using docutils and Sphinx. The package uses -`zc.buildout` to manage setup and dependencies. This package uses the v1 -`bootstrap.py` script. +To build the documentation locally, begin by cloning the project to your +machine: + +.. code-block:: bash + + $ git clone https://github.com/cewing/training.python_web.git + +Change directories into the repository, then create a virtualenv using Python +3: + +.. code-block:: bash + + $ cd training.python_web + $ virtualenv --python /path/to/bin/python3.5 . + Running virtualenv with interpreter /path/to/bin/python3.5 + New python executable in training.python_web/bin/python3.5 + Also creating executable in training.python_web/bin/python + Installing setuptools, pip...done. + +Install the requirements for the documentation using pip: + +.. code-block:: bash + + $ bin/pip install -r requirements.pip + ... + + Successfully installed Babel-2.0 Jinja2-2.8 MarkupSafe-0.23 Pygments-2.0.2 Sphinx-1.3.1 alabaster-0.7.6 appnope-0.1.0 decorator-4.0.2 docutils-0.12 gnureadline-6.3.3 hieroglyph-0.7.1 ipython-4.0.0 ipython-genutils-0.1.0 path.py-8.1 pexpect-3.3 pickleshare-0.5 py-1.4.30 pytest-2.7.2 pytz-2015.4 simplegeneric-0.8.1 six-1.9.0 snowballstemmer-1.2.0 sphinx-rtd-theme-0.1.8 traitlets-4.0.0 + +Once that has successfully completed, you should be able to build both the html +documentation and the slides using the included Makefile. + +.. code-block:: bash + + $ make html + ... + + Build finished. The HTML pages are in build/html. + + (webdocs)$ make slides + ... -After cloning this package from the repository, do the following:: + Build finished. The HTML slides are in build/slides. - $ cd training.python_web # the location of your local copy - $ python bootstrap.py # must be Python 2.6 or 2.7 - $ bin/buildout - $ bin/sphinx # to build the main documentation and course outline - $ bin/build_s5 # to build the class session presentations +.. note:: If you prefer to build your virtualenvs in other ways, you will need + to adjust the `BINDIR` variable in `Makefile` to fit your reality. -At the end of a successful build, you will find a ``build/html`` directory, -containing the completed documentation and presentations. -.. _zc.buildout: https://pypi.python.org/pypi/zc.buildout/ -.. _bootstrap.py: http://downloads.buildout.org/1/bootstrap.py Reading The Documentation ------------------------- diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 4d7aa17b..00000000 --- a/bootstrap.py +++ /dev/null @@ -1,170 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os -import shutil -import sys -import tempfile - -from optparse import OptionParser - -tmpeggs = tempfile.mkdtemp() - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --find-links to point to local resources, you can keep -this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", help="use a specific zc.buildout version") - -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", "--config-file", - help=("Specify the path to the buildout configuration " - "file to be used.")) -parser.add_option("-f", "--find-links", - help=("Specify a URL to search for buildout releases")) - - -options, args = parser.parse_args() - -###################################################################### -# load/install setuptools - -to_reload = False -try: - import pkg_resources - import setuptools -except ImportError: - ez = {} - - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - - # XXX use a more permanent ez_setup.py URL when available. - exec(urlopen('https://bitbucket.org/pypa/setuptools/raw/0.7.2/ez_setup.py' - ).read(), ez) - setup_args = dict(to_dir=tmpeggs, download_delay=0) - ez['use_setuptools'](**setup_args) - - if to_reload: - reload(pkg_resources) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -###################################################################### -# Install buildout - -ws = pkg_resources.working_set - -cmd = [sys.executable, '-c', - 'from setuptools.command.easy_install import main; main()', - '-mZqNxd', tmpeggs] - -find_links = os.environ.get( - 'bootstrap-testing-find-links', - options.find_links or - ('http://downloads.buildout.org/' - if options.accept_buildout_test_releases else None) - ) -if find_links: - cmd.extend(['-f', find_links]) - -setuptools_path = ws.find( - pkg_resources.Requirement.parse('setuptools')).location - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setuptools_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -import subprocess -if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: - raise Exception( - "Failed to execute command:\n%s", - repr(cmd)[1:-1]) - -###################################################################### -# Import and run buildout - -ws.add_entry(tmpeggs) -ws.require(requirement) -import zc.buildout.buildout - -if not [a for a in args if '=' not in a]: - args.append('bootstrap') - -# if -c was provided, we push it back into args for buildout' main function -if options.config_file is not None: - args[0:0] = ['-c', options.config_file] - -zc.buildout.buildout.main(args) -shutil.rmtree(tmpeggs) \ No newline at end of file diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 8d23d9ee..00000000 --- a/buildout.cfg +++ /dev/null @@ -1,91 +0,0 @@ -# -# Buildout to set-up Sphinx -# -[buildout] -parts = - venv - venv-pygments - build_s5 - executable - sphinx - -allow-picked-versions = true -show-picked-versions = true - -versions = versions - -script-in = ${buildout:directory}/commands/build.in - -[sphinx] -recipe = collective.recipe.sphinxbuilder -#doc-directory = . -outputs = - html -source = ${buildout:directory}/source/main -build = ${buildout:directory}/build -eggs = - Sphinx - docutils - roman - Pygments - -[venv] -recipe = rjm.recipe.venv -venv_options = --no-site-packages -distutils_urls = - http://pypi.python.org/packages/source/d/docutils/docutils-0.9.1.tar.gz - -[build_s5] -recipe = collective.recipe.template[genshi]:genshi -input = ${buildout:script-in} -output = ${buildout:directory}/bin/build_s5 -build-suffix = html -build-directory = ${buildout:directory}/build/html/presentations -build-cmd = ${buildout:directory}/bin/rst2s5.py - -[executable] -recipe = collective.recipe.cmd -on_install = true -on_update = true -cmds = - chmod 744 ${build_s5:output} - -# manually install Pygments into the docutils venv so it will be there for -# colorizing slide code examples. -[venv-pygments] -recipe = collective.recipe.cmd -on_install = true -on_update = false -cmds = - ${buildout:directory}/bin/easy_install Pygments - - -[versions] -# pin versions for continued sanity -Jinja2 = 2.6 -Pygments = 1.6 -Sphinx = 1.1.3 -collective.recipe.sphinxbuilder = 0.7.1 -roman = 1.4.0 - -#Required by: -#collective.recipe.sphinxbuilder 0.7.1 -docutils = 0.9.1 - -#Required by: -#collective.recipe.sphinxbuilder 0.7.1 -zc.buildout = 1.5.2 - -#Required by: -#collective.recipe.sphinxbuilder 0.7.1 -zc.recipe.egg = 1.3.2 - -Genshi = 0.6 -collective.recipe.cmd = 0.5 -collective.recipe.template = 1.9 -rjm.recipe.venv = 0.8 - - -#Required by: -#rjm.recipe.venv 0.8 -virtualenv = 1.10 diff --git a/commands/build.in b/commands/build.in deleted file mode 100644 index cc365279..00000000 --- a/commands/build.in +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -shopt -s nullglob -SRC=rst -DEST=${options['build-suffix']} - -cp -R ${parts.buildout.directory}/source/ui ${options['build-directory']}/ -cp -R ${parts.buildout.directory}/source/img ${options['build-directory']}/ - -for RST in ${parts.buildout.directory}/source/presentations/*.rst -do - BASE=`basename $$RST` - OUT=${options['build-directory']}/$${BASE%.$$SRC}.$$DEST - ${options['build-cmd']} $$RST $$OUT -done diff --git a/docutils.conf b/docutils.conf index c640184a..f36d1c9b 100644 --- a/docutils.conf +++ b/docutils.conf @@ -1,8 +1,11 @@ [general] source_url: http://github.com/cewing/training.python_web +[restructuredtext parser] +syntax_highlight = short + [s5_html writer] current_slide: True embed_stylesheet: false stylesheet: ui/uw_pce_theme/pretty.css -theme_url: ui/uw_pce_theme \ No newline at end of file +theme_url: ui/uw_pce_theme diff --git a/downloads/.gitignore b/downloads/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/downloads/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/notebooks/Networking & Sockets.ipynb b/notebooks/Networking & Sockets.ipynb new file mode 100644 index 00000000..09d64583 --- /dev/null +++ b/notebooks/Networking & Sockets.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import socket" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def get_constants(prefix):\n", + " \"\"\"mapping of socket module constants to their names\"\"\"\n", + " return {getattr(socket, n): n\n", + " for n in dir(socket)\n", + " if n.startswith(prefix)\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "families = get_constants('AF_')\n", + "types = get_constants('SOCK_')\n", + "protocols = get_constants('IPPROTO_')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{: 'AF_UNSPEC',\n", + " : 'AF_UNIX',\n", + " : 'AF_INET',\n", + " : 'AF_SNA',\n", + " 12: 'AF_DECnet',\n", + " : 'AF_APPLETALK',\n", + " : 'AF_ROUTE',\n", + " : 'AF_LINK',\n", + " : 'AF_IPX',\n", + " : 'AF_INET6',\n", + " : 'AF_SYSTEM'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "families" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{: 'SOCK_STREAM',\n", + " : 'SOCK_DGRAM',\n", + " : 'SOCK_RAW',\n", + " : 'SOCK_RDM',\n", + " : 'SOCK_SEQPACKET'}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "types" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'IPPROTO_IP',\n", + " 1: 'IPPROTO_ICMP',\n", + " 2: 'IPPROTO_IGMP',\n", + " 3: 'IPPROTO_GGP',\n", + " 4: 'IPPROTO_IPV4',\n", + " 6: 'IPPROTO_TCP',\n", + " 8: 'IPPROTO_EGP',\n", + " 12: 'IPPROTO_PUP',\n", + " 17: 'IPPROTO_UDP',\n", + " 22: 'IPPROTO_IDP',\n", + " 29: 'IPPROTO_TP',\n", + " 36: 'IPPROTO_XTP',\n", + " 41: 'IPPROTO_IPV6',\n", + " 43: 'IPPROTO_ROUTING',\n", + " 44: 'IPPROTO_FRAGMENT',\n", + " 46: 'IPPROTO_RSVP',\n", + " 47: 'IPPROTO_GRE',\n", + " 50: 'IPPROTO_ESP',\n", + " 51: 'IPPROTO_AH',\n", + " 58: 'IPPROTO_ICMPV6',\n", + " 59: 'IPPROTO_NONE',\n", + " 60: 'IPPROTO_DSTOPTS',\n", + " 63: 'IPPROTO_HELLO',\n", + " 77: 'IPPROTO_ND',\n", + " 80: 'IPPROTO_EON',\n", + " 103: 'IPPROTO_PIM',\n", + " 108: 'IPPROTO_IPCOMP',\n", + " 132: 'IPPROTO_SCTP',\n", + " 255: 'IPPROTO_RAW',\n", + " 256: 'IPPROTO_MAX'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "protocols" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "default_socket = socket.socket()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'AF_INET'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "families[default_socket.family]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'SOCK_STREAM'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "types[default_socket.type]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'IPPROTO_IP'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "protocols[default_socket.proto]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def get_address_info(host, port):\n", + " for response in socket.getaddrinfo(host, port):\n", + " fam, typ, pro, nam, add = response\n", + " print('family: {}'.format(families[fam]))\n", + " print('type: {}'.format(types[typ]))\n", + " print('protocol: {}'.format(protocols[pro]))\n", + " print('canonical name: {}'.format(nam))\n", + " print('socket address: {}'.format(add))\n", + " print('')" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "family: AF_INET\n", + "type: SOCK_DGRAM\n", + "protocol: IPPROTO_UDP\n", + "canonical name: \n", + "socket address: ('127.0.0.1', 80)\n", + "\n", + "family: AF_INET\n", + "type: SOCK_STREAM\n", + "protocol: IPPROTO_TCP\n", + "canonical name: \n", + "socket address: ('127.0.0.1', 80)\n", + "\n" + ] + } + ], + "source": [ + "get_address_info(socket.gethostname(), 'http')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.4.3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 00000000..7539549c --- /dev/null +++ b/requirements.pip @@ -0,0 +1,24 @@ +alabaster==0.7.6 +appnope==0.1.0 +Babel==2.0 +decorator==4.0.2 +docutils==0.12 +gnureadline==6.3.3 +hieroglyph==0.7.1 +ipython==4.0.0 +ipython-genutils==0.1.0 +Jinja2==2.8 +MarkupSafe==0.23 +path.py==8.1 +pexpect==3.3 +pickleshare==0.5 +py==1.4.30 +Pygments==2.0.2 +pytest==2.7.2 +pytz==2015.4 +simplegeneric==0.8.1 +six==1.9.0 +snowballstemmer==1.2.0 +Sphinx==1.3.1 +sphinx-rtd-theme==0.1.8 +traitlets==4.0.0 diff --git a/resources/common/virtualenv.py b/resources/common/virtualenv.py deleted file mode 100644 index ccb6eec9..00000000 --- a/resources/common/virtualenv.py +++ /dev/null @@ -1,2581 +0,0 @@ -#!/usr/bin/env python -"""Create a "virtual" Python installation -""" - -# If you change the version here, change it in setup.py -# and docs/conf.py as well. -__version__ = "1.9.1" # following best practices -virtualenv_version = __version__ # legacy, again - -import base64 -import sys -import os -import codecs -import optparse -import re -import shutil -import logging -import tempfile -import zlib -import errno -import glob -import distutils.sysconfig -from distutils.util import strtobool -import struct -import subprocess - -if sys.version_info < (2, 5): - print('ERROR: %s' % sys.exc_info()[1]) - print('ERROR: this script requires Python 2.5 or greater.') - sys.exit(101) - -try: - set -except NameError: - from sets import Set as set -try: - basestring -except NameError: - basestring = str - -try: - import ConfigParser -except ImportError: - import configparser as ConfigParser - -join = os.path.join -py_version = 'python%s.%s' % (sys.version_info[0], sys.version_info[1]) - -is_jython = sys.platform.startswith('java') -is_pypy = hasattr(sys, 'pypy_version_info') -is_win = (sys.platform == 'win32') -is_cygwin = (sys.platform == 'cygwin') -is_darwin = (sys.platform == 'darwin') -abiflags = getattr(sys, 'abiflags', '') - -user_dir = os.path.expanduser('~') -if is_win: - default_storage_dir = os.path.join(user_dir, 'virtualenv') -else: - default_storage_dir = os.path.join(user_dir, '.virtualenv') -default_config_file = os.path.join(default_storage_dir, 'virtualenv.ini') - -if is_pypy: - expected_exe = 'pypy' -elif is_jython: - expected_exe = 'jython' -else: - expected_exe = 'python' - - -REQUIRED_MODULES = ['os', 'posix', 'posixpath', 'nt', 'ntpath', 'genericpath', - 'fnmatch', 'locale', 'encodings', 'codecs', - 'stat', 'UserDict', 'readline', 'copy_reg', 'types', - 're', 'sre', 'sre_parse', 'sre_constants', 'sre_compile', - 'zlib'] - -REQUIRED_FILES = ['lib-dynload', 'config'] - -majver, minver = sys.version_info[:2] -if majver == 2: - if minver >= 6: - REQUIRED_MODULES.extend(['warnings', 'linecache', '_abcoll', 'abc']) - if minver >= 7: - REQUIRED_MODULES.extend(['_weakrefset']) - if minver <= 3: - REQUIRED_MODULES.extend(['sets', '__future__']) -elif majver == 3: - # Some extra modules are needed for Python 3, but different ones - # for different versions. - REQUIRED_MODULES.extend(['_abcoll', 'warnings', 'linecache', 'abc', 'io', - '_weakrefset', 'copyreg', 'tempfile', 'random', - '__future__', 'collections', 'keyword', 'tarfile', - 'shutil', 'struct', 'copy', 'tokenize', 'token', - 'functools', 'heapq', 'bisect', 'weakref', - 'reprlib']) - if minver >= 2: - REQUIRED_FILES[-1] = 'config-%s' % majver - if minver == 3: - import sysconfig - platdir = sysconfig.get_config_var('PLATDIR') - REQUIRED_FILES.append(platdir) - # The whole list of 3.3 modules is reproduced below - the current - # uncommented ones are required for 3.3 as of now, but more may be - # added as 3.3 development continues. - REQUIRED_MODULES.extend([ - #"aifc", - #"antigravity", - #"argparse", - #"ast", - #"asynchat", - #"asyncore", - "base64", - #"bdb", - #"binhex", - #"bisect", - #"calendar", - #"cgi", - #"cgitb", - #"chunk", - #"cmd", - #"codeop", - #"code", - #"colorsys", - #"_compat_pickle", - #"compileall", - #"concurrent", - #"configparser", - #"contextlib", - #"cProfile", - #"crypt", - #"csv", - #"ctypes", - #"curses", - #"datetime", - #"dbm", - #"decimal", - #"difflib", - #"dis", - #"doctest", - #"dummy_threading", - "_dummy_thread", - #"email", - #"filecmp", - #"fileinput", - #"formatter", - #"fractions", - #"ftplib", - #"functools", - #"getopt", - #"getpass", - #"gettext", - #"glob", - #"gzip", - "hashlib", - #"heapq", - "hmac", - #"html", - #"http", - #"idlelib", - #"imaplib", - #"imghdr", - "imp", - "importlib", - #"inspect", - #"json", - #"lib2to3", - #"logging", - #"macpath", - #"macurl2path", - #"mailbox", - #"mailcap", - #"_markupbase", - #"mimetypes", - #"modulefinder", - #"multiprocessing", - #"netrc", - #"nntplib", - #"nturl2path", - #"numbers", - #"opcode", - #"optparse", - #"os2emxpath", - #"pdb", - #"pickle", - #"pickletools", - #"pipes", - #"pkgutil", - #"platform", - #"plat-linux2", - #"plistlib", - #"poplib", - #"pprint", - #"profile", - #"pstats", - #"pty", - #"pyclbr", - #"py_compile", - #"pydoc_data", - #"pydoc", - #"_pyio", - #"queue", - #"quopri", - #"reprlib", - "rlcompleter", - #"runpy", - #"sched", - #"shelve", - #"shlex", - #"smtpd", - #"smtplib", - #"sndhdr", - #"socket", - #"socketserver", - #"sqlite3", - #"ssl", - #"stringprep", - #"string", - #"_strptime", - #"subprocess", - #"sunau", - #"symbol", - #"symtable", - #"sysconfig", - #"tabnanny", - #"telnetlib", - #"test", - #"textwrap", - #"this", - #"_threading_local", - #"threading", - #"timeit", - #"tkinter", - #"tokenize", - #"token", - #"traceback", - #"trace", - #"tty", - #"turtledemo", - #"turtle", - #"unittest", - #"urllib", - #"uuid", - #"uu", - #"wave", - #"weakref", - #"webbrowser", - #"wsgiref", - #"xdrlib", - #"xml", - #"xmlrpc", - #"zipfile", - ]) - -if is_pypy: - # these are needed to correctly display the exceptions that may happen - # during the bootstrap - REQUIRED_MODULES.extend(['traceback', 'linecache']) - -class Logger(object): - - """ - Logging object for use in command-line script. Allows ranges of - levels, to avoid some redundancy of displayed information. - """ - - DEBUG = logging.DEBUG - INFO = logging.INFO - NOTIFY = (logging.INFO+logging.WARN)/2 - WARN = WARNING = logging.WARN - ERROR = logging.ERROR - FATAL = logging.FATAL - - LEVELS = [DEBUG, INFO, NOTIFY, WARN, ERROR, FATAL] - - def __init__(self, consumers): - self.consumers = consumers - self.indent = 0 - self.in_progress = None - self.in_progress_hanging = False - - def debug(self, msg, *args, **kw): - self.log(self.DEBUG, msg, *args, **kw) - def info(self, msg, *args, **kw): - self.log(self.INFO, msg, *args, **kw) - def notify(self, msg, *args, **kw): - self.log(self.NOTIFY, msg, *args, **kw) - def warn(self, msg, *args, **kw): - self.log(self.WARN, msg, *args, **kw) - def error(self, msg, *args, **kw): - self.log(self.ERROR, msg, *args, **kw) - def fatal(self, msg, *args, **kw): - self.log(self.FATAL, msg, *args, **kw) - def log(self, level, msg, *args, **kw): - if args: - if kw: - raise TypeError( - "You may give positional or keyword arguments, not both") - args = args or kw - rendered = None - for consumer_level, consumer in self.consumers: - if self.level_matches(level, consumer_level): - if (self.in_progress_hanging - and consumer in (sys.stdout, sys.stderr)): - self.in_progress_hanging = False - sys.stdout.write('\n') - sys.stdout.flush() - if rendered is None: - if args: - rendered = msg % args - else: - rendered = msg - rendered = ' '*self.indent + rendered - if hasattr(consumer, 'write'): - consumer.write(rendered+'\n') - else: - consumer(rendered) - - def start_progress(self, msg): - assert not self.in_progress, ( - "Tried to start_progress(%r) while in_progress %r" - % (msg, self.in_progress)) - if self.level_matches(self.NOTIFY, self._stdout_level()): - sys.stdout.write(msg) - sys.stdout.flush() - self.in_progress_hanging = True - else: - self.in_progress_hanging = False - self.in_progress = msg - - def end_progress(self, msg='done.'): - assert self.in_progress, ( - "Tried to end_progress without start_progress") - if self.stdout_level_matches(self.NOTIFY): - if not self.in_progress_hanging: - # Some message has been printed out since start_progress - sys.stdout.write('...' + self.in_progress + msg + '\n') - sys.stdout.flush() - else: - sys.stdout.write(msg + '\n') - sys.stdout.flush() - self.in_progress = None - self.in_progress_hanging = False - - def show_progress(self): - """If we are in a progress scope, and no log messages have been - shown, write out another '.'""" - if self.in_progress_hanging: - sys.stdout.write('.') - sys.stdout.flush() - - def stdout_level_matches(self, level): - """Returns true if a message at this level will go to stdout""" - return self.level_matches(level, self._stdout_level()) - - def _stdout_level(self): - """Returns the level that stdout runs at""" - for level, consumer in self.consumers: - if consumer is sys.stdout: - return level - return self.FATAL - - def level_matches(self, level, consumer_level): - """ - >>> l = Logger([]) - >>> l.level_matches(3, 4) - False - >>> l.level_matches(3, 2) - True - >>> l.level_matches(slice(None, 3), 3) - False - >>> l.level_matches(slice(None, 3), 2) - True - >>> l.level_matches(slice(1, 3), 1) - True - >>> l.level_matches(slice(2, 3), 1) - False - """ - if isinstance(level, slice): - start, stop = level.start, level.stop - if start is not None and start > consumer_level: - return False - if stop is not None and stop <= consumer_level: - return False - return True - else: - return level >= consumer_level - - #@classmethod - def level_for_integer(cls, level): - levels = cls.LEVELS - if level < 0: - return levels[0] - if level >= len(levels): - return levels[-1] - return levels[level] - - level_for_integer = classmethod(level_for_integer) - -# create a silent logger just to prevent this from being undefined -# will be overridden with requested verbosity main() is called. -logger = Logger([(Logger.LEVELS[-1], sys.stdout)]) - -def mkdir(path): - if not os.path.exists(path): - logger.info('Creating %s', path) - os.makedirs(path) - else: - logger.info('Directory %s already exists', path) - -def copyfileordir(src, dest): - if os.path.isdir(src): - shutil.copytree(src, dest, True) - else: - shutil.copy2(src, dest) - -def copyfile(src, dest, symlink=True): - if not os.path.exists(src): - # Some bad symlink in the src - logger.warn('Cannot find file %s (bad symlink)', src) - return - if os.path.exists(dest): - logger.debug('File %s already exists', dest) - return - if not os.path.exists(os.path.dirname(dest)): - logger.info('Creating parent directories for %s' % os.path.dirname(dest)) - os.makedirs(os.path.dirname(dest)) - if not os.path.islink(src): - srcpath = os.path.abspath(src) - else: - srcpath = os.readlink(src) - if symlink and hasattr(os, 'symlink') and not is_win: - logger.info('Symlinking %s', dest) - try: - os.symlink(srcpath, dest) - except (OSError, NotImplementedError): - logger.info('Symlinking failed, copying to %s', dest) - copyfileordir(src, dest) - else: - logger.info('Copying to %s', dest) - copyfileordir(src, dest) - -def writefile(dest, content, overwrite=True): - if not os.path.exists(dest): - logger.info('Writing %s', dest) - f = open(dest, 'wb') - f.write(content.encode('utf-8')) - f.close() - return - else: - f = open(dest, 'rb') - c = f.read() - f.close() - if c != content.encode("utf-8"): - if not overwrite: - logger.notify('File %s exists with different content; not overwriting', dest) - return - logger.notify('Overwriting %s with new content', dest) - f = open(dest, 'wb') - f.write(content.encode('utf-8')) - f.close() - else: - logger.info('Content %s already in place', dest) - -def rmtree(dir): - if os.path.exists(dir): - logger.notify('Deleting tree %s', dir) - shutil.rmtree(dir) - else: - logger.info('Do not need to delete %s; already gone', dir) - -def make_exe(fn): - if hasattr(os, 'chmod'): - oldmode = os.stat(fn).st_mode & 0xFFF # 0o7777 - newmode = (oldmode | 0x16D) & 0xFFF # 0o555, 0o7777 - os.chmod(fn, newmode) - logger.info('Changed mode of %s to %s', fn, oct(newmode)) - -def _find_file(filename, dirs): - for dir in reversed(dirs): - files = glob.glob(os.path.join(dir, filename)) - if files and os.path.isfile(files[0]): - return True, files[0] - return False, filename - -def _install_req(py_executable, unzip=False, distribute=False, - search_dirs=None, never_download=False): - - if search_dirs is None: - search_dirs = file_search_dirs() - - if not distribute: - egg_path = 'setuptools-*-py%s.egg' % sys.version[:3] - found, egg_path = _find_file(egg_path, search_dirs) - project_name = 'setuptools' - bootstrap_script = EZ_SETUP_PY - tgz_path = None - else: - # Look for a distribute egg (these are not distributed by default, - # but can be made available by the user) - egg_path = 'distribute-*-py%s.egg' % sys.version[:3] - found, egg_path = _find_file(egg_path, search_dirs) - project_name = 'distribute' - if found: - tgz_path = None - bootstrap_script = DISTRIBUTE_FROM_EGG_PY - else: - # Fall back to sdist - # NB: egg_path is not None iff tgz_path is None - # iff bootstrap_script is a generic setup script accepting - # the standard arguments. - egg_path = None - tgz_path = 'distribute-*.tar.gz' - found, tgz_path = _find_file(tgz_path, search_dirs) - bootstrap_script = DISTRIBUTE_SETUP_PY - - if is_jython and os._name == 'nt': - # Jython's .bat sys.executable can't handle a command line - # argument with newlines - fd, ez_setup = tempfile.mkstemp('.py') - os.write(fd, bootstrap_script) - os.close(fd) - cmd = [py_executable, ez_setup] - else: - cmd = [py_executable, '-c', bootstrap_script] - if unzip and egg_path: - cmd.append('--always-unzip') - env = {} - remove_from_env = ['__PYVENV_LAUNCHER__'] - if logger.stdout_level_matches(logger.DEBUG) and egg_path: - cmd.append('-v') - - old_chdir = os.getcwd() - if egg_path is not None and os.path.exists(egg_path): - logger.info('Using existing %s egg: %s' % (project_name, egg_path)) - cmd.append(egg_path) - if os.environ.get('PYTHONPATH'): - env['PYTHONPATH'] = egg_path + os.path.pathsep + os.environ['PYTHONPATH'] - else: - env['PYTHONPATH'] = egg_path - elif tgz_path is not None and os.path.exists(tgz_path): - # Found a tgz source dist, let's chdir - logger.info('Using existing %s egg: %s' % (project_name, tgz_path)) - os.chdir(os.path.dirname(tgz_path)) - # in this case, we want to be sure that PYTHONPATH is unset (not - # just empty, really unset), else CPython tries to import the - # site.py that it's in virtualenv_support - remove_from_env.append('PYTHONPATH') - elif never_download: - logger.fatal("Can't find any local distributions of %s to install " - "and --never-download is set. Either re-run virtualenv " - "without the --never-download option, or place a %s " - "distribution (%s) in one of these " - "locations: %r" % (project_name, project_name, - egg_path or tgz_path, - search_dirs)) - sys.exit(1) - elif egg_path: - logger.info('No %s egg found; downloading' % project_name) - cmd.extend(['--always-copy', '-U', project_name]) - else: - logger.info('No %s tgz found; downloading' % project_name) - logger.start_progress('Installing %s...' % project_name) - logger.indent += 2 - cwd = None - if project_name == 'distribute': - env['DONT_PATCH_SETUPTOOLS'] = 'true' - - def _filter_ez_setup(line): - return filter_ez_setup(line, project_name) - - if not os.access(os.getcwd(), os.W_OK): - cwd = tempfile.mkdtemp() - if tgz_path is not None and os.path.exists(tgz_path): - # the current working dir is hostile, let's copy the - # tarball to a temp dir - target = os.path.join(cwd, os.path.split(tgz_path)[-1]) - shutil.copy(tgz_path, target) - try: - call_subprocess(cmd, show_stdout=False, - filter_stdout=_filter_ez_setup, - extra_env=env, - remove_from_env=remove_from_env, - cwd=cwd) - finally: - logger.indent -= 2 - logger.end_progress() - if cwd is not None: - shutil.rmtree(cwd) - if os.getcwd() != old_chdir: - os.chdir(old_chdir) - if is_jython and os._name == 'nt': - os.remove(ez_setup) - -def file_search_dirs(): - here = os.path.dirname(os.path.abspath(__file__)) - dirs = ['.', here, - join(here, 'virtualenv_support')] - if os.path.splitext(os.path.dirname(__file__))[0] != 'virtualenv': - # Probably some boot script; just in case virtualenv is installed... - try: - import virtualenv - except ImportError: - pass - else: - dirs.append(os.path.join(os.path.dirname(virtualenv.__file__), 'virtualenv_support')) - return [d for d in dirs if os.path.isdir(d)] - -def install_setuptools(py_executable, unzip=False, - search_dirs=None, never_download=False): - _install_req(py_executable, unzip, - search_dirs=search_dirs, never_download=never_download) - -def install_distribute(py_executable, unzip=False, - search_dirs=None, never_download=False): - _install_req(py_executable, unzip, distribute=True, - search_dirs=search_dirs, never_download=never_download) - -_pip_re = re.compile(r'^pip-.*(zip|tar.gz|tar.bz2|tgz|tbz)$', re.I) -def install_pip(py_executable, search_dirs=None, never_download=False): - if search_dirs is None: - search_dirs = file_search_dirs() - - filenames = [] - for dir in search_dirs: - filenames.extend([join(dir, fn) for fn in os.listdir(dir) - if _pip_re.search(fn)]) - filenames = [(os.path.basename(filename).lower(), i, filename) for i, filename in enumerate(filenames)] - filenames.sort() - filenames = [filename for basename, i, filename in filenames] - if not filenames: - filename = 'pip' - else: - filename = filenames[-1] - easy_install_script = 'easy_install' - if is_win: - easy_install_script = 'easy_install-script.py' - # There's two subtle issues here when invoking easy_install. - # 1. On unix-like systems the easy_install script can *only* be executed - # directly if its full filesystem path is no longer than 78 characters. - # 2. A work around to [1] is to use the `python path/to/easy_install foo` - # pattern, but that breaks if the path contains non-ASCII characters, as - # you can't put the file encoding declaration before the shebang line. - # The solution is to use Python's -x flag to skip the first line of the - # script (and any ASCII decoding errors that may have occurred in that line) - cmd = [py_executable, '-x', join(os.path.dirname(py_executable), easy_install_script), filename] - # jython and pypy don't yet support -x - if is_jython or is_pypy: - cmd.remove('-x') - if filename == 'pip': - if never_download: - logger.fatal("Can't find any local distributions of pip to install " - "and --never-download is set. Either re-run virtualenv " - "without the --never-download option, or place a pip " - "source distribution (zip/tar.gz/tar.bz2) in one of these " - "locations: %r" % search_dirs) - sys.exit(1) - logger.info('Installing pip from network...') - else: - logger.info('Installing existing %s distribution: %s' % ( - os.path.basename(filename), filename)) - logger.start_progress('Installing pip...') - logger.indent += 2 - def _filter_setup(line): - return filter_ez_setup(line, 'pip') - try: - call_subprocess(cmd, show_stdout=False, - filter_stdout=_filter_setup) - finally: - logger.indent -= 2 - logger.end_progress() - -def filter_ez_setup(line, project_name='setuptools'): - if not line.strip(): - return Logger.DEBUG - if project_name == 'distribute': - for prefix in ('Extracting', 'Now working', 'Installing', 'Before', - 'Scanning', 'Setuptools', 'Egg', 'Already', - 'running', 'writing', 'reading', 'installing', - 'creating', 'copying', 'byte-compiling', 'removing', - 'Processing'): - if line.startswith(prefix): - return Logger.DEBUG - return Logger.DEBUG - for prefix in ['Reading ', 'Best match', 'Processing setuptools', - 'Copying setuptools', 'Adding setuptools', - 'Installing ', 'Installed ']: - if line.startswith(prefix): - return Logger.DEBUG - return Logger.INFO - - -class UpdatingDefaultsHelpFormatter(optparse.IndentedHelpFormatter): - """ - Custom help formatter for use in ConfigOptionParser that updates - the defaults before expanding them, allowing them to show up correctly - in the help listing - """ - def expand_default(self, option): - if self.parser is not None: - self.parser.update_defaults(self.parser.defaults) - return optparse.IndentedHelpFormatter.expand_default(self, option) - - -class ConfigOptionParser(optparse.OptionParser): - """ - Custom option parser which updates its defaults by by checking the - configuration files and environmental variables - """ - def __init__(self, *args, **kwargs): - self.config = ConfigParser.RawConfigParser() - self.files = self.get_config_files() - self.config.read(self.files) - optparse.OptionParser.__init__(self, *args, **kwargs) - - def get_config_files(self): - config_file = os.environ.get('VIRTUALENV_CONFIG_FILE', False) - if config_file and os.path.exists(config_file): - return [config_file] - return [default_config_file] - - def update_defaults(self, defaults): - """ - Updates the given defaults with values from the config files and - the environ. Does a little special handling for certain types of - options (lists). - """ - # Then go and look for the other sources of configuration: - config = {} - # 1. config files - config.update(dict(self.get_config_section('virtualenv'))) - # 2. environmental variables - config.update(dict(self.get_environ_vars())) - # Then set the options with those values - for key, val in config.items(): - key = key.replace('_', '-') - if not key.startswith('--'): - key = '--%s' % key # only prefer long opts - option = self.get_option(key) - if option is not None: - # ignore empty values - if not val: - continue - # handle multiline configs - if option.action == 'append': - val = val.split() - else: - option.nargs = 1 - if option.action == 'store_false': - val = not strtobool(val) - elif option.action in ('store_true', 'count'): - val = strtobool(val) - try: - val = option.convert_value(key, val) - except optparse.OptionValueError: - e = sys.exc_info()[1] - print("An error occured during configuration: %s" % e) - sys.exit(3) - defaults[option.dest] = val - return defaults - - def get_config_section(self, name): - """ - Get a section of a configuration - """ - if self.config.has_section(name): - return self.config.items(name) - return [] - - def get_environ_vars(self, prefix='VIRTUALENV_'): - """ - Returns a generator with all environmental vars with prefix VIRTUALENV - """ - for key, val in os.environ.items(): - if key.startswith(prefix): - yield (key.replace(prefix, '').lower(), val) - - def get_default_values(self): - """ - Overridding to make updating the defaults after instantiation of - the option parser possible, update_defaults() does the dirty work. - """ - if not self.process_default_values: - # Old, pre-Optik 1.5 behaviour. - return optparse.Values(self.defaults) - - defaults = self.update_defaults(self.defaults.copy()) # ours - for option in self._get_all_options(): - default = defaults.get(option.dest) - if isinstance(default, basestring): - opt_str = option.get_opt_string() - defaults[option.dest] = option.check_value(opt_str, default) - return optparse.Values(defaults) - - -def main(): - parser = ConfigOptionParser( - version=virtualenv_version, - usage="%prog [OPTIONS] DEST_DIR", - formatter=UpdatingDefaultsHelpFormatter()) - - parser.add_option( - '-v', '--verbose', - action='count', - dest='verbose', - default=0, - help="Increase verbosity") - - parser.add_option( - '-q', '--quiet', - action='count', - dest='quiet', - default=0, - help='Decrease verbosity') - - parser.add_option( - '-p', '--python', - dest='python', - metavar='PYTHON_EXE', - help='The Python interpreter to use, e.g., --python=python2.5 will use the python2.5 ' - 'interpreter to create the new environment. The default is the interpreter that ' - 'virtualenv was installed with (%s)' % sys.executable) - - parser.add_option( - '--clear', - dest='clear', - action='store_true', - help="Clear out the non-root install and start from scratch") - - parser.set_defaults(system_site_packages=False) - parser.add_option( - '--no-site-packages', - dest='system_site_packages', - action='store_false', - help="Don't give access to the global site-packages dir to the " - "virtual environment (default)") - - parser.add_option( - '--system-site-packages', - dest='system_site_packages', - action='store_true', - help="Give access to the global site-packages dir to the " - "virtual environment") - - parser.add_option( - '--unzip-setuptools', - dest='unzip_setuptools', - action='store_true', - help="Unzip Setuptools or Distribute when installing it") - - parser.add_option( - '--relocatable', - dest='relocatable', - action='store_true', - help='Make an EXISTING virtualenv environment relocatable. ' - 'This fixes up scripts and makes all .pth files relative') - - parser.add_option( - '--distribute', '--use-distribute', # the second option is for legacy reasons here. Hi Kenneth! - dest='use_distribute', - action='store_true', - help='Use Distribute instead of Setuptools. Set environ variable ' - 'VIRTUALENV_DISTRIBUTE to make it the default ') - - parser.add_option( - '--no-setuptools', - dest='no_setuptools', - action='store_true', - help='Do not install distribute/setuptools (or pip) ' - 'in the new virtualenv.') - - parser.add_option( - '--no-pip', - dest='no_pip', - action='store_true', - help='Do not install pip in the new virtualenv.') - - parser.add_option( - '--setuptools', - dest='use_distribute', - action='store_false', - help='Use Setuptools instead of Distribute. Set environ variable ' - 'VIRTUALENV_SETUPTOOLS to make it the default ') - - # Set this to True to use distribute by default, even in Python 2. - parser.set_defaults(use_distribute=False) - - default_search_dirs = file_search_dirs() - parser.add_option( - '--extra-search-dir', - dest="search_dirs", - action="append", - default=default_search_dirs, - help="Directory to look for setuptools/distribute/pip distributions in. " - "You can add any number of additional --extra-search-dir paths.") - - parser.add_option( - '--never-download', - dest="never_download", - action="store_true", - help="Never download anything from the network. Instead, virtualenv will fail " - "if local distributions of setuptools/distribute/pip are not present.") - - parser.add_option( - '--prompt', - dest='prompt', - help='Provides an alternative prompt prefix for this environment') - - if 'extend_parser' in globals(): - extend_parser(parser) - - options, args = parser.parse_args() - - global logger - - if 'adjust_options' in globals(): - adjust_options(options, args) - - verbosity = options.verbose - options.quiet - logger = Logger([(Logger.level_for_integer(2 - verbosity), sys.stdout)]) - - if options.python and not os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): - env = os.environ.copy() - interpreter = resolve_interpreter(options.python) - if interpreter == sys.executable: - logger.warn('Already using interpreter %s' % interpreter) - else: - logger.notify('Running virtualenv with interpreter %s' % interpreter) - env['VIRTUALENV_INTERPRETER_RUNNING'] = 'true' - file = __file__ - if file.endswith('.pyc'): - file = file[:-1] - popen = subprocess.Popen([interpreter, file] + sys.argv[1:], env=env) - raise SystemExit(popen.wait()) - - # Force --distribute on Python 3, since setuptools is not available. - if majver > 2: - options.use_distribute = True - - if os.environ.get('PYTHONDONTWRITEBYTECODE') and not options.use_distribute: - print( - "The PYTHONDONTWRITEBYTECODE environment variable is " - "not compatible with setuptools. Either use --distribute " - "or unset PYTHONDONTWRITEBYTECODE.") - sys.exit(2) - if not args: - print('You must provide a DEST_DIR') - parser.print_help() - sys.exit(2) - if len(args) > 1: - print('There must be only one argument: DEST_DIR (you gave %s)' % ( - ' '.join(args))) - parser.print_help() - sys.exit(2) - - home_dir = args[0] - - if os.environ.get('WORKING_ENV'): - logger.fatal('ERROR: you cannot run virtualenv while in a workingenv') - logger.fatal('Please deactivate your workingenv, then re-run this script') - sys.exit(3) - - if 'PYTHONHOME' in os.environ: - logger.warn('PYTHONHOME is set. You *must* activate the virtualenv before using it') - del os.environ['PYTHONHOME'] - - if options.relocatable: - make_environment_relocatable(home_dir) - return - - create_environment(home_dir, - site_packages=options.system_site_packages, - clear=options.clear, - unzip_setuptools=options.unzip_setuptools, - use_distribute=options.use_distribute, - prompt=options.prompt, - search_dirs=options.search_dirs, - never_download=options.never_download, - no_setuptools=options.no_setuptools, - no_pip=options.no_pip) - if 'after_install' in globals(): - after_install(options, home_dir) - -def call_subprocess(cmd, show_stdout=True, - filter_stdout=None, cwd=None, - raise_on_returncode=True, extra_env=None, - remove_from_env=None): - cmd_parts = [] - for part in cmd: - if len(part) > 45: - part = part[:20]+"..."+part[-20:] - if ' ' in part or '\n' in part or '"' in part or "'" in part: - part = '"%s"' % part.replace('"', '\\"') - if hasattr(part, 'decode'): - try: - part = part.decode(sys.getdefaultencoding()) - except UnicodeDecodeError: - part = part.decode(sys.getfilesystemencoding()) - cmd_parts.append(part) - cmd_desc = ' '.join(cmd_parts) - if show_stdout: - stdout = None - else: - stdout = subprocess.PIPE - logger.debug("Running command %s" % cmd_desc) - if extra_env or remove_from_env: - env = os.environ.copy() - if extra_env: - env.update(extra_env) - if remove_from_env: - for varname in remove_from_env: - env.pop(varname, None) - else: - env = None - try: - proc = subprocess.Popen( - cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout, - cwd=cwd, env=env) - except Exception: - e = sys.exc_info()[1] - logger.fatal( - "Error %s while executing command %s" % (e, cmd_desc)) - raise - all_output = [] - if stdout is not None: - stdout = proc.stdout - encoding = sys.getdefaultencoding() - fs_encoding = sys.getfilesystemencoding() - while 1: - line = stdout.readline() - try: - line = line.decode(encoding) - except UnicodeDecodeError: - line = line.decode(fs_encoding) - if not line: - break - line = line.rstrip() - all_output.append(line) - if filter_stdout: - level = filter_stdout(line) - if isinstance(level, tuple): - level, line = level - logger.log(level, line) - if not logger.stdout_level_matches(level): - logger.show_progress() - else: - logger.info(line) - else: - proc.communicate() - proc.wait() - if proc.returncode: - if raise_on_returncode: - if all_output: - logger.notify('Complete output from command %s:' % cmd_desc) - logger.notify('\n'.join(all_output) + '\n----------------------------------------') - raise OSError( - "Command %s failed with error code %s" - % (cmd_desc, proc.returncode)) - else: - logger.warn( - "Command %s had error code %s" - % (cmd_desc, proc.returncode)) - - -def create_environment(home_dir, site_packages=False, clear=False, - unzip_setuptools=False, use_distribute=False, - prompt=None, search_dirs=None, never_download=False, - no_setuptools=False, no_pip=False): - """ - Creates a new environment in ``home_dir``. - - If ``site_packages`` is true, then the global ``site-packages/`` - directory will be on the path. - - If ``clear`` is true (default False) then the environment will - first be cleared. - """ - home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) - - py_executable = os.path.abspath(install_python( - home_dir, lib_dir, inc_dir, bin_dir, - site_packages=site_packages, clear=clear)) - - install_distutils(home_dir) - - if not no_setuptools: - if use_distribute: - install_distribute(py_executable, unzip=unzip_setuptools, - search_dirs=search_dirs, never_download=never_download) - else: - install_setuptools(py_executable, unzip=unzip_setuptools, - search_dirs=search_dirs, never_download=never_download) - - if not no_pip: - install_pip(py_executable, search_dirs=search_dirs, never_download=never_download) - - install_activate(home_dir, bin_dir, prompt) - -def is_executable_file(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - -def path_locations(home_dir): - """Return the path locations for the environment (where libraries are, - where scripts go, etc)""" - # XXX: We'd use distutils.sysconfig.get_python_inc/lib but its - # prefix arg is broken: http://bugs.python.org/issue3386 - if is_win: - # Windows has lots of problems with executables with spaces in - # the name; this function will remove them (using the ~1 - # format): - mkdir(home_dir) - if ' ' in home_dir: - import ctypes - GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW - size = max(len(home_dir)+1, 256) - buf = ctypes.create_unicode_buffer(size) - try: - u = unicode - except NameError: - u = str - ret = GetShortPathName(u(home_dir), buf, size) - if not ret: - print('Error: the path "%s" has a space in it' % home_dir) - print('We could not determine the short pathname for it.') - print('Exiting.') - sys.exit(3) - home_dir = str(buf.value) - lib_dir = join(home_dir, 'Lib') - inc_dir = join(home_dir, 'Include') - bin_dir = join(home_dir, 'Scripts') - if is_jython: - lib_dir = join(home_dir, 'Lib') - inc_dir = join(home_dir, 'Include') - bin_dir = join(home_dir, 'bin') - elif is_pypy: - lib_dir = home_dir - inc_dir = join(home_dir, 'include') - bin_dir = join(home_dir, 'bin') - elif not is_win: - lib_dir = join(home_dir, 'lib', py_version) - multiarch_exec = '/usr/bin/multiarch-platform' - if is_executable_file(multiarch_exec): - # In Mageia (2) and Mandriva distros the include dir must be like: - # virtualenv/include/multiarch-x86_64-linux/python2.7 - # instead of being virtualenv/include/python2.7 - p = subprocess.Popen(multiarch_exec, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - # stdout.strip is needed to remove newline character - inc_dir = join(home_dir, 'include', stdout.strip(), py_version + abiflags) - else: - inc_dir = join(home_dir, 'include', py_version + abiflags) - bin_dir = join(home_dir, 'bin') - return home_dir, lib_dir, inc_dir, bin_dir - - -def change_prefix(filename, dst_prefix): - prefixes = [sys.prefix] - - if is_darwin: - prefixes.extend(( - os.path.join("/Library/Python", sys.version[:3], "site-packages"), - os.path.join(sys.prefix, "Extras", "lib", "python"), - os.path.join("~", "Library", "Python", sys.version[:3], "site-packages"), - # Python 2.6 no-frameworks - os.path.join("~", ".local", "lib","python", sys.version[:3], "site-packages"), - # System Python 2.7 on OSX Mountain Lion - os.path.join("~", "Library", "Python", sys.version[:3], "lib", "python", "site-packages"))) - - if hasattr(sys, 'real_prefix'): - prefixes.append(sys.real_prefix) - if hasattr(sys, 'base_prefix'): - prefixes.append(sys.base_prefix) - prefixes = list(map(os.path.expanduser, prefixes)) - prefixes = list(map(os.path.abspath, prefixes)) - # Check longer prefixes first so we don't split in the middle of a filename - prefixes = sorted(prefixes, key=len, reverse=True) - filename = os.path.abspath(filename) - for src_prefix in prefixes: - if filename.startswith(src_prefix): - _, relpath = filename.split(src_prefix, 1) - if src_prefix != os.sep: # sys.prefix == "/" - assert relpath[0] == os.sep - relpath = relpath[1:] - return join(dst_prefix, relpath) - assert False, "Filename %s does not start with any of these prefixes: %s" % \ - (filename, prefixes) - -def copy_required_modules(dst_prefix): - import imp - # If we are running under -p, we need to remove the current - # directory from sys.path temporarily here, so that we - # definitely get the modules from the site directory of - # the interpreter we are running under, not the one - # virtualenv.py is installed under (which might lead to py2/py3 - # incompatibility issues) - _prev_sys_path = sys.path - if os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): - sys.path = sys.path[1:] - try: - for modname in REQUIRED_MODULES: - if modname in sys.builtin_module_names: - logger.info("Ignoring built-in bootstrap module: %s" % modname) - continue - try: - f, filename, _ = imp.find_module(modname) - except ImportError: - logger.info("Cannot import bootstrap module: %s" % modname) - else: - if f is not None: - f.close() - # special-case custom readline.so on OS X, but not for pypy: - if modname == 'readline' and sys.platform == 'darwin' and not ( - is_pypy or filename.endswith(join('lib-dynload', 'readline.so'))): - dst_filename = join(dst_prefix, 'lib', 'python%s' % sys.version[:3], 'readline.so') - else: - dst_filename = change_prefix(filename, dst_prefix) - copyfile(filename, dst_filename) - if filename.endswith('.pyc'): - pyfile = filename[:-1] - if os.path.exists(pyfile): - copyfile(pyfile, dst_filename[:-1]) - finally: - sys.path = _prev_sys_path - - -def subst_path(prefix_path, prefix, home_dir): - prefix_path = os.path.normpath(prefix_path) - prefix = os.path.normpath(prefix) - home_dir = os.path.normpath(home_dir) - if not prefix_path.startswith(prefix): - logger.warn('Path not in prefix %r %r', prefix_path, prefix) - return - return prefix_path.replace(prefix, home_dir, 1) - - -def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear): - """Install just the base environment, no distutils patches etc""" - if sys.executable.startswith(bin_dir): - print('Please use the *system* python to run this script') - return - - if clear: - rmtree(lib_dir) - ## FIXME: why not delete it? - ## Maybe it should delete everything with #!/path/to/venv/python in it - logger.notify('Not deleting %s', bin_dir) - - if hasattr(sys, 'real_prefix'): - logger.notify('Using real prefix %r' % sys.real_prefix) - prefix = sys.real_prefix - elif hasattr(sys, 'base_prefix'): - logger.notify('Using base prefix %r' % sys.base_prefix) - prefix = sys.base_prefix - else: - prefix = sys.prefix - mkdir(lib_dir) - fix_lib64(lib_dir) - stdlib_dirs = [os.path.dirname(os.__file__)] - if is_win: - stdlib_dirs.append(join(os.path.dirname(stdlib_dirs[0]), 'DLLs')) - elif is_darwin: - stdlib_dirs.append(join(stdlib_dirs[0], 'site-packages')) - if hasattr(os, 'symlink'): - logger.info('Symlinking Python bootstrap modules') - else: - logger.info('Copying Python bootstrap modules') - logger.indent += 2 - try: - # copy required files... - for stdlib_dir in stdlib_dirs: - if not os.path.isdir(stdlib_dir): - continue - for fn in os.listdir(stdlib_dir): - bn = os.path.splitext(fn)[0] - if fn != 'site-packages' and bn in REQUIRED_FILES: - copyfile(join(stdlib_dir, fn), join(lib_dir, fn)) - # ...and modules - copy_required_modules(home_dir) - finally: - logger.indent -= 2 - mkdir(join(lib_dir, 'site-packages')) - import site - site_filename = site.__file__ - if site_filename.endswith('.pyc'): - site_filename = site_filename[:-1] - elif site_filename.endswith('$py.class'): - site_filename = site_filename.replace('$py.class', '.py') - site_filename_dst = change_prefix(site_filename, home_dir) - site_dir = os.path.dirname(site_filename_dst) - writefile(site_filename_dst, SITE_PY) - writefile(join(site_dir, 'orig-prefix.txt'), prefix) - site_packages_filename = join(site_dir, 'no-global-site-packages.txt') - if not site_packages: - writefile(site_packages_filename, '') - - if is_pypy or is_win: - stdinc_dir = join(prefix, 'include') - else: - stdinc_dir = join(prefix, 'include', py_version + abiflags) - if os.path.exists(stdinc_dir): - copyfile(stdinc_dir, inc_dir) - else: - logger.debug('No include dir %s' % stdinc_dir) - - platinc_dir = distutils.sysconfig.get_python_inc(plat_specific=1) - if platinc_dir != stdinc_dir: - platinc_dest = distutils.sysconfig.get_python_inc( - plat_specific=1, prefix=home_dir) - if platinc_dir == platinc_dest: - # Do platinc_dest manually due to a CPython bug; - # not http://bugs.python.org/issue3386 but a close cousin - platinc_dest = subst_path(platinc_dir, prefix, home_dir) - if platinc_dest: - # PyPy's stdinc_dir and prefix are relative to the original binary - # (traversing virtualenvs), whereas the platinc_dir is relative to - # the inner virtualenv and ignores the prefix argument. - # This seems more evolved than designed. - copyfile(platinc_dir, platinc_dest) - - # pypy never uses exec_prefix, just ignore it - if sys.exec_prefix != prefix and not is_pypy: - if is_win: - exec_dir = join(sys.exec_prefix, 'lib') - elif is_jython: - exec_dir = join(sys.exec_prefix, 'Lib') - else: - exec_dir = join(sys.exec_prefix, 'lib', py_version) - for fn in os.listdir(exec_dir): - copyfile(join(exec_dir, fn), join(lib_dir, fn)) - - if is_jython: - # Jython has either jython-dev.jar and javalib/ dir, or just - # jython.jar - for name in 'jython-dev.jar', 'javalib', 'jython.jar': - src = join(prefix, name) - if os.path.exists(src): - copyfile(src, join(home_dir, name)) - # XXX: registry should always exist after Jython 2.5rc1 - src = join(prefix, 'registry') - if os.path.exists(src): - copyfile(src, join(home_dir, 'registry'), symlink=False) - copyfile(join(prefix, 'cachedir'), join(home_dir, 'cachedir'), - symlink=False) - - mkdir(bin_dir) - py_executable = join(bin_dir, os.path.basename(sys.executable)) - if 'Python.framework' in prefix: - # OS X framework builds cause validation to break - # https://github.com/pypa/virtualenv/issues/322 - if os.environ.get('__PYVENV_LAUNCHER__'): - os.unsetenv('__PYVENV_LAUNCHER__') - if re.search(r'/Python(?:-32|-64)*$', py_executable): - # The name of the python executable is not quite what - # we want, rename it. - py_executable = os.path.join( - os.path.dirname(py_executable), 'python') - - logger.notify('New %s executable in %s', expected_exe, py_executable) - pcbuild_dir = os.path.dirname(sys.executable) - pyd_pth = os.path.join(lib_dir, 'site-packages', 'virtualenv_builddir_pyd.pth') - if is_win and os.path.exists(os.path.join(pcbuild_dir, 'build.bat')): - logger.notify('Detected python running from build directory %s', pcbuild_dir) - logger.notify('Writing .pth file linking to build directory for *.pyd files') - writefile(pyd_pth, pcbuild_dir) - else: - pcbuild_dir = None - if os.path.exists(pyd_pth): - logger.info('Deleting %s (not Windows env or not build directory python)' % pyd_pth) - os.unlink(pyd_pth) - - if sys.executable != py_executable: - ## FIXME: could I just hard link? - executable = sys.executable - shutil.copyfile(executable, py_executable) - make_exe(py_executable) - if is_win or is_cygwin: - pythonw = os.path.join(os.path.dirname(sys.executable), 'pythonw.exe') - if os.path.exists(pythonw): - logger.info('Also created pythonw.exe') - shutil.copyfile(pythonw, os.path.join(os.path.dirname(py_executable), 'pythonw.exe')) - python_d = os.path.join(os.path.dirname(sys.executable), 'python_d.exe') - python_d_dest = os.path.join(os.path.dirname(py_executable), 'python_d.exe') - if os.path.exists(python_d): - logger.info('Also created python_d.exe') - shutil.copyfile(python_d, python_d_dest) - elif os.path.exists(python_d_dest): - logger.info('Removed python_d.exe as it is no longer at the source') - os.unlink(python_d_dest) - # we need to copy the DLL to enforce that windows will load the correct one. - # may not exist if we are cygwin. - py_executable_dll = 'python%s%s.dll' % ( - sys.version_info[0], sys.version_info[1]) - py_executable_dll_d = 'python%s%s_d.dll' % ( - sys.version_info[0], sys.version_info[1]) - pythondll = os.path.join(os.path.dirname(sys.executable), py_executable_dll) - pythondll_d = os.path.join(os.path.dirname(sys.executable), py_executable_dll_d) - pythondll_d_dest = os.path.join(os.path.dirname(py_executable), py_executable_dll_d) - if os.path.exists(pythondll): - logger.info('Also created %s' % py_executable_dll) - shutil.copyfile(pythondll, os.path.join(os.path.dirname(py_executable), py_executable_dll)) - if os.path.exists(pythondll_d): - logger.info('Also created %s' % py_executable_dll_d) - shutil.copyfile(pythondll_d, pythondll_d_dest) - elif os.path.exists(pythondll_d_dest): - logger.info('Removed %s as the source does not exist' % pythondll_d_dest) - os.unlink(pythondll_d_dest) - if is_pypy: - # make a symlink python --> pypy-c - python_executable = os.path.join(os.path.dirname(py_executable), 'python') - if sys.platform in ('win32', 'cygwin'): - python_executable += '.exe' - logger.info('Also created executable %s' % python_executable) - copyfile(py_executable, python_executable) - - if is_win: - for name in 'libexpat.dll', 'libpypy.dll', 'libpypy-c.dll', 'libeay32.dll', 'ssleay32.dll', 'sqlite.dll': - src = join(prefix, name) - if os.path.exists(src): - copyfile(src, join(bin_dir, name)) - - if os.path.splitext(os.path.basename(py_executable))[0] != expected_exe: - secondary_exe = os.path.join(os.path.dirname(py_executable), - expected_exe) - py_executable_ext = os.path.splitext(py_executable)[1] - if py_executable_ext == '.exe': - # python2.4 gives an extension of '.4' :P - secondary_exe += py_executable_ext - if os.path.exists(secondary_exe): - logger.warn('Not overwriting existing %s script %s (you must use %s)' - % (expected_exe, secondary_exe, py_executable)) - else: - logger.notify('Also creating executable in %s' % secondary_exe) - shutil.copyfile(sys.executable, secondary_exe) - make_exe(secondary_exe) - - if '.framework' in prefix: - if 'Python.framework' in prefix: - logger.debug('MacOSX Python framework detected') - # Make sure we use the the embedded interpreter inside - # the framework, even if sys.executable points to - # the stub executable in ${sys.prefix}/bin - # See http://groups.google.com/group/python-virtualenv/ - # browse_thread/thread/17cab2f85da75951 - original_python = os.path.join( - prefix, 'Resources/Python.app/Contents/MacOS/Python') - if 'EPD' in prefix: - logger.debug('EPD framework detected') - original_python = os.path.join(prefix, 'bin/python') - shutil.copy(original_python, py_executable) - - # Copy the framework's dylib into the virtual - # environment - virtual_lib = os.path.join(home_dir, '.Python') - - if os.path.exists(virtual_lib): - os.unlink(virtual_lib) - copyfile( - os.path.join(prefix, 'Python'), - virtual_lib) - - # And then change the install_name of the copied python executable - try: - mach_o_change(py_executable, - os.path.join(prefix, 'Python'), - '@executable_path/../.Python') - except: - e = sys.exc_info()[1] - logger.warn("Could not call mach_o_change: %s. " - "Trying to call install_name_tool instead." % e) - try: - call_subprocess( - ["install_name_tool", "-change", - os.path.join(prefix, 'Python'), - '@executable_path/../.Python', - py_executable]) - except: - logger.fatal("Could not call install_name_tool -- you must " - "have Apple's development tools installed") - raise - - if not is_win: - # Ensure that 'python', 'pythonX' and 'pythonX.Y' all exist - py_exe_version_major = 'python%s' % sys.version_info[0] - py_exe_version_major_minor = 'python%s.%s' % ( - sys.version_info[0], sys.version_info[1]) - py_exe_no_version = 'python' - required_symlinks = [ py_exe_no_version, py_exe_version_major, - py_exe_version_major_minor ] - - py_executable_base = os.path.basename(py_executable) - - if py_executable_base in required_symlinks: - # Don't try to symlink to yourself. - required_symlinks.remove(py_executable_base) - - for pth in required_symlinks: - full_pth = join(bin_dir, pth) - if os.path.exists(full_pth): - os.unlink(full_pth) - os.symlink(py_executable_base, full_pth) - - if is_win and ' ' in py_executable: - # There's a bug with subprocess on Windows when using a first - # argument that has a space in it. Instead we have to quote - # the value: - py_executable = '"%s"' % py_executable - # NOTE: keep this check as one line, cmd.exe doesn't cope with line breaks - cmd = [py_executable, '-c', 'import sys;out=sys.stdout;' - 'getattr(out, "buffer", out).write(sys.prefix.encode("utf-8"))'] - logger.info('Testing executable with %s %s "%s"' % tuple(cmd)) - try: - proc = subprocess.Popen(cmd, - stdout=subprocess.PIPE) - proc_stdout, proc_stderr = proc.communicate() - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.EACCES: - logger.fatal('ERROR: The executable %s could not be run: %s' % (py_executable, e)) - sys.exit(100) - else: - raise e - - proc_stdout = proc_stdout.strip().decode("utf-8") - proc_stdout = os.path.normcase(os.path.abspath(proc_stdout)) - norm_home_dir = os.path.normcase(os.path.abspath(home_dir)) - if hasattr(norm_home_dir, 'decode'): - norm_home_dir = norm_home_dir.decode(sys.getfilesystemencoding()) - if proc_stdout != norm_home_dir: - logger.fatal( - 'ERROR: The executable %s is not functioning' % py_executable) - logger.fatal( - 'ERROR: It thinks sys.prefix is %r (should be %r)' - % (proc_stdout, norm_home_dir)) - logger.fatal( - 'ERROR: virtualenv is not compatible with this system or executable') - if is_win: - logger.fatal( - 'Note: some Windows users have reported this error when they ' - 'installed Python for "Only this user" or have multiple ' - 'versions of Python installed. Copying the appropriate ' - 'PythonXX.dll to the virtualenv Scripts/ directory may fix ' - 'this problem.') - sys.exit(100) - else: - logger.info('Got sys.prefix result: %r' % proc_stdout) - - pydistutils = os.path.expanduser('~/.pydistutils.cfg') - if os.path.exists(pydistutils): - logger.notify('Please make sure you remove any previous custom paths from ' - 'your %s file.' % pydistutils) - ## FIXME: really this should be calculated earlier - - fix_local_scheme(home_dir) - - if site_packages: - if os.path.exists(site_packages_filename): - logger.info('Deleting %s' % site_packages_filename) - os.unlink(site_packages_filename) - - return py_executable - - -def install_activate(home_dir, bin_dir, prompt=None): - home_dir = os.path.abspath(home_dir) - if is_win or is_jython and os._name == 'nt': - files = { - 'activate.bat': ACTIVATE_BAT, - 'deactivate.bat': DEACTIVATE_BAT, - 'activate.ps1': ACTIVATE_PS, - } - - # MSYS needs paths of the form /c/path/to/file - drive, tail = os.path.splitdrive(home_dir.replace(os.sep, '/')) - home_dir_msys = (drive and "/%s%s" or "%s%s") % (drive[:1], tail) - - # Run-time conditional enables (basic) Cygwin compatibility - home_dir_sh = ("""$(if [ "$OSTYPE" "==" "cygwin" ]; then cygpath -u '%s'; else echo '%s'; fi;)""" % - (home_dir, home_dir_msys)) - files['activate'] = ACTIVATE_SH.replace('__VIRTUAL_ENV__', home_dir_sh) - - else: - files = {'activate': ACTIVATE_SH} - - # suppling activate.fish in addition to, not instead of, the - # bash script support. - files['activate.fish'] = ACTIVATE_FISH - - # same for csh/tcsh support... - files['activate.csh'] = ACTIVATE_CSH - - files['activate_this.py'] = ACTIVATE_THIS - if hasattr(home_dir, 'decode'): - home_dir = home_dir.decode(sys.getfilesystemencoding()) - vname = os.path.basename(home_dir) - for name, content in files.items(): - content = content.replace('__VIRTUAL_PROMPT__', prompt or '') - content = content.replace('__VIRTUAL_WINPROMPT__', prompt or '(%s)' % vname) - content = content.replace('__VIRTUAL_ENV__', home_dir) - content = content.replace('__VIRTUAL_NAME__', vname) - content = content.replace('__BIN_NAME__', os.path.basename(bin_dir)) - writefile(os.path.join(bin_dir, name), content) - -def install_distutils(home_dir): - distutils_path = change_prefix(distutils.__path__[0], home_dir) - mkdir(distutils_path) - ## FIXME: maybe this prefix setting should only be put in place if - ## there's a local distutils.cfg with a prefix setting? - home_dir = os.path.abspath(home_dir) - ## FIXME: this is breaking things, removing for now: - #distutils_cfg = DISTUTILS_CFG + "\n[install]\nprefix=%s\n" % home_dir - writefile(os.path.join(distutils_path, '__init__.py'), DISTUTILS_INIT) - writefile(os.path.join(distutils_path, 'distutils.cfg'), DISTUTILS_CFG, overwrite=False) - -def fix_local_scheme(home_dir): - """ - Platforms that use the "posix_local" install scheme (like Ubuntu with - Python 2.7) need to be given an additional "local" location, sigh. - """ - try: - import sysconfig - except ImportError: - pass - else: - if sysconfig._get_default_scheme() == 'posix_local': - local_path = os.path.join(home_dir, 'local') - if not os.path.exists(local_path): - os.mkdir(local_path) - for subdir_name in os.listdir(home_dir): - if subdir_name == 'local': - continue - os.symlink(os.path.abspath(os.path.join(home_dir, subdir_name)), \ - os.path.join(local_path, subdir_name)) - -def fix_lib64(lib_dir): - """ - Some platforms (particularly Gentoo on x64) put things in lib64/pythonX.Y - instead of lib/pythonX.Y. If this is such a platform we'll just create a - symlink so lib64 points to lib - """ - if [p for p in distutils.sysconfig.get_config_vars().values() - if isinstance(p, basestring) and 'lib64' in p]: - logger.debug('This system uses lib64; symlinking lib64 to lib') - assert os.path.basename(lib_dir) == 'python%s' % sys.version[:3], ( - "Unexpected python lib dir: %r" % lib_dir) - lib_parent = os.path.dirname(lib_dir) - top_level = os.path.dirname(lib_parent) - lib_dir = os.path.join(top_level, 'lib') - lib64_link = os.path.join(top_level, 'lib64') - assert os.path.basename(lib_parent) == 'lib', ( - "Unexpected parent dir: %r" % lib_parent) - if os.path.lexists(lib64_link): - return - os.symlink('lib', lib64_link) - -def resolve_interpreter(exe): - """ - If the executable given isn't an absolute path, search $PATH for the interpreter - """ - if os.path.abspath(exe) != exe: - paths = os.environ.get('PATH', '').split(os.pathsep) - for path in paths: - if os.path.exists(os.path.join(path, exe)): - exe = os.path.join(path, exe) - break - if not os.path.exists(exe): - logger.fatal('The executable %s (from --python=%s) does not exist' % (exe, exe)) - raise SystemExit(3) - if not is_executable(exe): - logger.fatal('The executable %s (from --python=%s) is not executable' % (exe, exe)) - raise SystemExit(3) - return exe - -def is_executable(exe): - """Checks a file is executable""" - return os.access(exe, os.X_OK) - -############################################################ -## Relocating the environment: - -def make_environment_relocatable(home_dir): - """ - Makes the already-existing environment use relative paths, and takes out - the #!-based environment selection in scripts. - """ - home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) - activate_this = os.path.join(bin_dir, 'activate_this.py') - if not os.path.exists(activate_this): - logger.fatal( - 'The environment doesn\'t have a file %s -- please re-run virtualenv ' - 'on this environment to update it' % activate_this) - fixup_scripts(home_dir) - fixup_pth_and_egg_link(home_dir) - ## FIXME: need to fix up distutils.cfg - -OK_ABS_SCRIPTS = ['python', 'python%s' % sys.version[:3], - 'activate', 'activate.bat', 'activate_this.py'] - -def fixup_scripts(home_dir): - # This is what we expect at the top of scripts: - shebang = '#!%s/bin/python' % os.path.normcase(os.path.abspath(home_dir)) - # This is what we'll put: - new_shebang = '#!/usr/bin/env python%s' % sys.version[:3] - if is_win: - bin_suffix = 'Scripts' - else: - bin_suffix = 'bin' - bin_dir = os.path.join(home_dir, bin_suffix) - home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) - for filename in os.listdir(bin_dir): - filename = os.path.join(bin_dir, filename) - if not os.path.isfile(filename): - # ignore subdirs, e.g. .svn ones. - continue - f = open(filename, 'rb') - try: - try: - lines = f.read().decode('utf-8').splitlines() - except UnicodeDecodeError: - # This is probably a binary program instead - # of a script, so just ignore it. - continue - finally: - f.close() - if not lines: - logger.warn('Script %s is an empty file' % filename) - continue - if not lines[0].strip().startswith(shebang): - if os.path.basename(filename) in OK_ABS_SCRIPTS: - logger.debug('Cannot make script %s relative' % filename) - elif lines[0].strip() == new_shebang: - logger.info('Script %s has already been made relative' % filename) - else: - logger.warn('Script %s cannot be made relative (it\'s not a normal script that starts with %s)' - % (filename, shebang)) - continue - logger.notify('Making script %s relative' % filename) - script = relative_script([new_shebang] + lines[1:]) - f = open(filename, 'wb') - f.write('\n'.join(script).encode('utf-8')) - f.close() - -def relative_script(lines): - "Return a script that'll work in a relocatable environment." - activate = "import os; activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); execfile(activate_this, dict(__file__=activate_this)); del os, activate_this" - # Find the last future statement in the script. If we insert the activation - # line before a future statement, Python will raise a SyntaxError. - activate_at = None - for idx, line in reversed(list(enumerate(lines))): - if line.split()[:3] == ['from', '__future__', 'import']: - activate_at = idx + 1 - break - if activate_at is None: - # Activate after the shebang. - activate_at = 1 - return lines[:activate_at] + ['', activate, ''] + lines[activate_at:] - -def fixup_pth_and_egg_link(home_dir, sys_path=None): - """Makes .pth and .egg-link files use relative paths""" - home_dir = os.path.normcase(os.path.abspath(home_dir)) - if sys_path is None: - sys_path = sys.path - for path in sys_path: - if not path: - path = '.' - if not os.path.isdir(path): - continue - path = os.path.normcase(os.path.abspath(path)) - if not path.startswith(home_dir): - logger.debug('Skipping system (non-environment) directory %s' % path) - continue - for filename in os.listdir(path): - filename = os.path.join(path, filename) - if filename.endswith('.pth'): - if not os.access(filename, os.W_OK): - logger.warn('Cannot write .pth file %s, skipping' % filename) - else: - fixup_pth_file(filename) - if filename.endswith('.egg-link'): - if not os.access(filename, os.W_OK): - logger.warn('Cannot write .egg-link file %s, skipping' % filename) - else: - fixup_egg_link(filename) - -def fixup_pth_file(filename): - lines = [] - prev_lines = [] - f = open(filename) - prev_lines = f.readlines() - f.close() - for line in prev_lines: - line = line.strip() - if (not line or line.startswith('#') or line.startswith('import ') - or os.path.abspath(line) != line): - lines.append(line) - else: - new_value = make_relative_path(filename, line) - if line != new_value: - logger.debug('Rewriting path %s as %s (in %s)' % (line, new_value, filename)) - lines.append(new_value) - if lines == prev_lines: - logger.info('No changes to .pth file %s' % filename) - return - logger.notify('Making paths in .pth file %s relative' % filename) - f = open(filename, 'w') - f.write('\n'.join(lines) + '\n') - f.close() - -def fixup_egg_link(filename): - f = open(filename) - link = f.readline().strip() - f.close() - if os.path.abspath(link) != link: - logger.debug('Link in %s already relative' % filename) - return - new_link = make_relative_path(filename, link) - logger.notify('Rewriting link %s in %s as %s' % (link, filename, new_link)) - f = open(filename, 'w') - f.write(new_link) - f.close() - -def make_relative_path(source, dest, dest_is_directory=True): - """ - Make a filename relative, where the filename is dest, and it is - being referred to from the filename source. - - >>> make_relative_path('/usr/share/something/a-file.pth', - ... '/usr/share/another-place/src/Directory') - '../another-place/src/Directory' - >>> make_relative_path('/usr/share/something/a-file.pth', - ... '/home/user/src/Directory') - '../../../home/user/src/Directory' - >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') - './' - """ - source = os.path.dirname(source) - if not dest_is_directory: - dest_filename = os.path.basename(dest) - dest = os.path.dirname(dest) - dest = os.path.normpath(os.path.abspath(dest)) - source = os.path.normpath(os.path.abspath(source)) - dest_parts = dest.strip(os.path.sep).split(os.path.sep) - source_parts = source.strip(os.path.sep).split(os.path.sep) - while dest_parts and source_parts and dest_parts[0] == source_parts[0]: - dest_parts.pop(0) - source_parts.pop(0) - full_parts = ['..']*len(source_parts) + dest_parts - if not dest_is_directory: - full_parts.append(dest_filename) - if not full_parts: - # Special case for the current directory (otherwise it'd be '') - return './' - return os.path.sep.join(full_parts) - - - -############################################################ -## Bootstrap script creation: - -def create_bootstrap_script(extra_text, python_version=''): - """ - Creates a bootstrap script, which is like this script but with - extend_parser, adjust_options, and after_install hooks. - - This returns a string that (written to disk of course) can be used - as a bootstrap script with your own customizations. The script - will be the standard virtualenv.py script, with your extra text - added (your extra text should be Python code). - - If you include these functions, they will be called: - - ``extend_parser(optparse_parser)``: - You can add or remove options from the parser here. - - ``adjust_options(options, args)``: - You can change options here, or change the args (if you accept - different kinds of arguments, be sure you modify ``args`` so it is - only ``[DEST_DIR]``). - - ``after_install(options, home_dir)``: - - After everything is installed, this function is called. This - is probably the function you are most likely to use. An - example would be:: - - def after_install(options, home_dir): - subprocess.call([join(home_dir, 'bin', 'easy_install'), - 'MyPackage']) - subprocess.call([join(home_dir, 'bin', 'my-package-script'), - 'setup', home_dir]) - - This example immediately installs a package, and runs a setup - script from that package. - - If you provide something like ``python_version='2.5'`` then the - script will start with ``#!/usr/bin/env python2.5`` instead of - ``#!/usr/bin/env python``. You can use this when the script must - be run with a particular Python version. - """ - filename = __file__ - if filename.endswith('.pyc'): - filename = filename[:-1] - f = codecs.open(filename, 'r', encoding='utf-8') - content = f.read() - f.close() - py_exe = 'python%s' % python_version - content = (('#!/usr/bin/env %s\n' % py_exe) - + '## WARNING: This file is generated\n' - + content) - return content.replace('##EXT' 'END##', extra_text) - -##EXTEND## - -def convert(s): - b = base64.b64decode(s.encode('ascii')) - return zlib.decompress(b).decode('utf-8') - -##file site.py -SITE_PY = convert(""" -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 -UM3ckC/Q8aP8VLy+mjYY5MxOtAdgjULwf2RtvCc= -""") - -##file ez_setup.py -EZ_SETUP_PY = convert(""" -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 -qL7+C1AkCcw= -""") - -##file distribute_from_egg.py -DISTRIBUTE_FROM_EGG_PY = convert(""" -eJw9j8tqAzEMRfcG/4MgmxQyptkGusonZBmGoGTUGYFfWPKE6dfXTkM3gqt7rh47OKP3NMF3SQFW -LlrRU1zhybpAxoKBlIqcrNnBdRjQP3GTocYfzmNrrCPQPN9iwzpxSQfQhWBi0cL3qtRtYIG/4Mv0 -KApY5hooqrOGQ05FQTaxptF9Fnx16Rq0XofjaE1XGXVxHIWK7j8P8EY/rHndLqQ1a0pe3COFgHFy -hLLdWkDbi/DeEpCjNb3u/zccT2Ob8gtnwVyI -""") - -##file distribute_setup.py -DISTRIBUTE_SETUP_PY = convert(""" -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 -""") - -##file activate.sh -ACTIVATE_SH = convert(""" -eJytVVFvokAQfudXTLEPtTlLeo9tvMSmJpq02hSvl7u2wRUG2QR2DSxSe7n/frOACEVNLlceRHa+ -nfl25pvZDswCnoDPQ4QoTRQsENIEPci4CsBMZBq7CAsuLOYqvmYKTTj3YxnBgiXBudGBjUzBZUJI -BXEqgCvweIyuCjeG4eF2F5x14bcB9KQiQQWrjSddI1/oQIx6SYYeoFjzWIoIhYI1izlbhJjkKO7D -M/QEmKfO9O7WeRo/zr4P7pyHwWxkwitcgwpQ5Ej96OX+PmiFwLeVjFUOrNYKaq1Nud3nR2n8nI2m -k9H0friPTGVsUdptaxGrTEfpNVFEskxpXtUkkCkl1UNF9cgLBkx48J4EXyALuBtAwNYIjF5kcmUU -abMKmMq1ULoiRbgsDEkTSsKSGFCJ6Z8vY/2xYiSacmtyAfCDdCNTVZoVF8vSTQOoEwSnOrngBkws -MYGMBMg8/bMBLSYKS7pYEXP0PqT+ZmBT0Xuy+Pplj5yn4aM9nk72JD8/Wi+Gr98sD9eWSMOwkapD -BbUv91XSvmyVkICt2tmXR4tWmrcUCsjWOpw87YidEC8i0gdTSOFhouJUNxR+4NYBG0MftoCTD9F7 -2rTtxG3oPwY1b2HncYwhrlmj6Wq924xtGDWqfdNxap+OYxplEurnMVo9RWks+rH8qKEtx7kZT5zJ -4H7oOFclrN6uFe+d+nW2aIUsSgs/42EIPuOhXq+jEo3S6tX6w2ilNkDnIpHCWdEQhFgwj9pkk7FN -l/y5eQvRSIQ5+TrL05lewxWpt/Lbhes5cJF3mLET1MGhcKCF+40tNWnUulxrpojwDo2sObdje3Bz -N3QeHqf3D7OjEXMVV8LN3ZlvuzoWHqiUcNKHtwNd0IbvPGKYYM31nPKCgkUILw3KL+Y8l7aO1ArS -Ad37nIU0fCj5NE5gQCuC5sOSu+UdI2NeXg/lFkQIlFpdWVaWZRfvqGiirC9o6liJ9FXGYrSY9mI1 -D/Ncozgn13vJvsznr7DnkJWXsyMH7e42ljdJ+aqNDF1bFnKWFLdj31xtaJYK6EXFgqmV/ymD/ROG -+n8O9H8f5vsGOWXsL1+1k3g= -""") - -##file activate.fish -ACTIVATE_FISH = convert(""" -eJyVVWFv2jAQ/c6vuBoqQVWC9nVSNVGVCaS2VC2rNLWVZZILWAs2s52wVvvxsyEJDrjbmgpK7PP5 -3bt3d22YLbmGlGcIq1wbmCPkGhPYcLMEEsGciwGLDS+YwSjlekngLFVyBe73GXSXxqw/DwbuTS8x -yyKpFr1WG15lDjETQhpQuQBuIOEKY5O9tlppLqxHKSDByjVAPwEy+mXtCq5MzjIUBTCRgEKTKwFG -gpBqxTLYXgN2myspVigMaYF92tZSowGZJf4mFExxNs9Qb614CgZtmH0BpEOn11f0cXI/+za8pnfD -2ZjA1sg9zlV/8QvcMhxbNu0QwgYokn/d+n02nt6Opzcjcnx1vXcIoN74O4ymWQXmHURfJw9jenc/ -vbmb0enj6P5+cuVhqlKm3S0u2XRtRbA2QQAhV7VhBF0rsgUX9Ur1rBUXJgVSy8O751k8mzY5OrKH -RW3eaQhYGTr8hrXO59ALhxQ83mCsDLAid3T72CCSdJhaFE+fXgicXAARUiR2WeVO37gH3oYHzFKo -9k7CaPZ1UeNwH1tWuXA4uFKYYcEa8vaKqXl7q1UpygMPhFLvlVKyNzsSM3S2km7UBOl4xweUXk5u -6e3wZmQ9leY1XE/Ili670tr9g/5POBBpGIJXCCF79L1siarl/dbESa8mD8PL61GpzqpzuMS7tqeB -1YkALrRBloBMbR9yLcVx7frQAgUqR7NZIuzkEu110gbNit1enNs82Rx5utq7Z3prU78HFRgulqNC -OTwbqJa9vkJFclQgZSjbKeBgSsUtCtt9D8OwAbIVJuewQdfvQRaoFE9wd1TmCuRG7OgJ1bVXGHc7 -z5WDL/WW36v2oi37CyVBak61+yPBA9C1qqGxzKQqZ0oPuocU9hpud0PIp8sDHkXR1HKkNlzjuUWA -a0enFUyzOWZA4yXGP+ZMI3Tdt2OuqU/SO4q64526cPE0A7ZyW2PMbWZiZ5HamIZ2RcCKLXhcDl2b -vXL+eccQoRzem80mekPDEiyiWK4GWqZmwxQOmPM0eIfgp1P9cqrBsewR2p/DPMtt+pfcYM+Ls2uh -hALufTAdmGl8B1H3VPd2af8fQAc4PgqjlIBL9cGQqNpXaAwe3LrtVn8AkZTUxg== -""") - -##file activate.csh -ACTIVATE_CSH = convert(""" -eJx9VG1P2zAQ/u5fcYQKNgTNPtN1WxlIQ4KCUEGaxuQ6yYVYSuzKdhqVX7+zk3bpy5YPUXL3PPfc -ne98DLNCWshliVDV1kGCUFvMoJGugMjq2qQIiVSxSJ1cCofD1BYRnOVGV0CfZ0N2DD91DalQSjsw -tQLpIJMGU1euvPe7QeJlkKzgWixlhnAt4aoUVsLnLBiy5NtbJWQ5THX1ZciYKKWwkOFaE04dUm6D -r/zh7pq/3D7Nnid3/HEy+wFHY/gEJydg0aFaQrBFgz1c5DG1IhTs+UZgsBC2GMFBlaeH+8dZXwcW -VPvCjXdlAvCfQsE7al0+07XjZvrSCUevR5dnkVeKlFYZmUztG4BdzL2u9KyLVabTU0bdfg7a0hgs -cSmUg6UwUiQl2iHrcbcVGNvPCiLOe7+cRwG13z9qRGgx2z6DHjfm/Op2yqeT+xvOLzs0PTKHDz2V -tkckFHoQfQRXoGJAj9el0FyJCmEMhzgMS4sB7KPOE2ExoLcSieYwDvR+cP8cg11gKkVJc2wRcm1g -QhYFlXiTaTfO2ki0fQoiFM4tLuO4aZrhOzqR4dIPcWx17hphMBY+Srwh7RTyN83XOWkcSPh1Pg/k -TXX/jbJTbMtUmcxZ+/bbqOsy82suFQg/BhdSOTRhMNBHlUarCpU7JzBhmkKmRejKOQzayQe6MWoa -n1wqWmuh6LZAaHxcdeqIlVLhIBJdO9/kbl0It2oEXQj+eGjJOuvOIR/YGRqvFhttUB2XTvLXYN2H -37CBdbW2W7j2r2+VsCn0doVWcFG1/4y1VwBjfwAyoZhD -""") - -##file activate.bat -ACTIVATE_BAT = convert(""" -eJx9UdEKgjAUfW6wfxjiIH+hEDKUFHSKLCMI7kNOEkIf9P9pTJ3OLJ/03HPPPed4Es9XS9qqwqgT -PbGKKOdXL4aAFS7A4gvAwgijuiKlqOpGlATS2NeMLE+TjJM9RkQ+SmqAXLrBo1LLIeLdiWlD6jZt -r7VNubWkndkXaxg5GO3UaOOKS6drO3luDDiO5my3iA0YAKGzPRV1ack8cOdhysI0CYzIPzjSiH5X -0QcvC8Lfaj0emsVKYF2rhL5L3fCkVjV76kShi59NHwDniAHzkgDgqBcwOgTMx+gDQQqXCw== -""") - -##file deactivate.bat -DEACTIVATE_BAT = convert(""" -eJxzSE3OyFfIT0vj4ipOLVEI8wwKCXX0iXf1C7Pl4spMU0hJTcvMS01RiPf3cYmHyQYE+fsGhCho -cCkAAUibEkTEVhWLMlUlLk6QGixStlyaeCyJDPHw9/Pw93VFsQguim4ZXAJoIUw5DhX47XUM8UCx -EchHtwsohN1bILUgw61c/Vy4AJYPYm4= -""") - -##file activate.ps1 -ACTIVATE_PS = convert(""" -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 -QastYw== -""") - -##file distutils-init.py -DISTUTILS_INIT = convert(""" -eJytV1uL4zYUfvevOE0ottuMW9q3gVDa3aUMXXbLMlDKMBiNrSTqOJKRlMxkf33PkXyRbGe7Dw2E -UXTu37lpxLFV2oIyifAncxmOL0xLIfcG+gv80x9VW6maw7o/CANSWWBwFtqeWMPlGY6qPjV8A0bB -C4eKSTgZ5LRgFeyErMEeOBhbN+Ipgeizhjtnhkn7DdyjuNLPoCS0l/ayQTG0djwZC08cLXozeMss -aG5EzQ0IScpnWtHSTXuxByV/QCmxE7y+eS0uxWeoheaVVfqSJHiU7Mhhi6gULbOHorshkrEnKxpT -0n3A8Y8SMpuwZx6aoix3ouFlmW8gHRSkeSJ2g7hU+kiHLDaQw3bmRDaTGfTnty7gPm0FHbIBg9U9 -oh1kZzAFLaue2R6htPCtAda2nGlDSUJ4PZBgCJBGVcwKTAMz/vJiLD+Oin5Z5QlvDPdulC6EsiyE -NFzb7McNTKJzbJqzphx92VKRFY1idenzmq3K0emRcbWBD0ryqc4NZGmKOOOX9Pz5x+/l27tP797c -f/z0d+4NruGNai8uAM0bfsYaw8itFk8ny41jsfpyO+BWlpqfhcG4yxLdi/0tQqoT4a8Vby382mt8 -p7XSo7aWGdPBc+b6utaBmCQ7rQKQoWtAuthQCiold2KfJIPTT8xwg9blPumc+YDZC/wYGdAyHpJk -vUbHbHWAp5No6pK/WhhLEWrFjUwtPEv1Agf8YmnsuXUQYkeZoHm8ogP16gt2uHoxcEMdf2C6pmbw -hUMsWGhanboh4IzzmsIpWs134jVPqD/c74bZHdY69UKKSn/+KfVhxLgUlToemayLMYQOqfEC61bh -cbhwaqoGUzIyZRFHPmau5juaWqwRn3mpWmoEA5nhzS5gog/5jbcFQqOZvmBasZtwYlG93k5GEiyw -buHhMWLjDarEGpMGB2LFs5nIJkhp/nUmZneFaRth++lieJtHepIvKgx6PJqIlD9X2j6pG1i9x3pZ -5bHuCPFiirGHeO7McvoXkz786GaKVzC9DSpnOxJdc4xm6NSVq7lNEnKdVlnpu9BNYoKX2Iq3wvgh -gGEUM66kK6j4NiyoneuPLSwaCWDxczgaolEWpiMyDVDb7dNuLAbriL8ig8mmeju31oNvQdpnvEPC -1vAXbWacGRVrGt/uXN/gU0CDDwgooKRrHfTBb1/s9lYZ8ZqOBU0yLvpuP6+K9hLFsvIjeNhBi0KL -MlOuWRn3FRwx5oHXjl0YImUx0+gLzjGchrgzca026ETmYJzPD+IpuKzNi8AFn048Thd63OdD86M6 -84zE8yQm0VqXdbbgvub2pKVnS76icBGdeTHHXTKspUmr4NYo/furFLKiMdQzFjHJNcdAnMhltBJK -0/IKX3DVFqvPJ2dLE7bDBkH0l/PJ29074+F0CsGYOxsb7U3myTUncYfXqnLLfa6sJybX4g+hmcjO -kMRBfA1JellfRRKJcyRpxdS4rIl6FdmQCWjo/o9Qz7yKffoP4JHjOvABcRn4CZIT2RH4jnxmfpVG -qgLaAvQBNfuO6X0/Ux02nb4FKx3vgP+XnkX0QW9pLy/NsXgdN24dD3LxO2Nwil7Zlc1dqtP3d7/h -kzp1/+7hGBuY4pk0XD/0Ao/oTe/XGrfyM773aB7iUhgkpy+dwAMalxMP0DrBcsVw/6p25+/hobP9 -GBknrWExDhLJ1bwt1NcCNblaFbMKCyvmX0PeRaQ= -""") - -##file distutils.cfg -DISTUTILS_CFG = convert(""" -eJxNj00KwkAMhfc9xYNuxe4Ft57AjYiUtDO1wXSmNJnK3N5pdSEEAu8nH6lxHVlRhtDHMPATA4uH -xJ4EFmGbvfJiicSHFRzUSISMY6hq3GLCRLnIvSTnEefN0FIjw5tF0Hkk9Q5dRunBsVoyFi24aaLg -9FDOlL0FPGluf4QjcInLlxd6f6rqkgPu/5nHLg0cXCscXoozRrP51DRT3j9QNl99AP53T2Q= -""") - -##file activate_this.py -ACTIVATE_THIS = convert(""" -eJyNU01v2zAMvetXEB4K21jmDOstQA4dMGCHbeihlyEIDMWmG62yJEiKE//7kXKdpN2KzYBt8euR -fKSyLPs8wiEo8wh4wqZTGou4V6Hm0wJa1cSiTkJdr8+GsoTRHuCotBayiWqQEYGtMCgfD1KjGYBe -5a3p0cRKiAe2NtLADikftnDco0ko/SFEVgEZ8aRC5GLux7i3BpSJ6J1H+i7A2CjiHq9z7JRZuuQq -siwTIvpxJYCeuWaBpwZdhB+yxy/eWz+ZvVSU8C4E9FFZkyxFsvCT/ZzL8gcz9aXVE14Yyp2M+2W0 -y7n5mp0qN+avKXvbsyyzUqjeWR8hjGE+2iCE1W1tQ82hsCZN9UzlJr+/e/iab8WfqsmPI6pWeUPd -FrMsd4H/55poeO9n54COhUs+sZNEzNtg/wanpjpuqHJaxs76HtZryI/K3H7KJ/KDIhqcbJ7kI4ar -XL+sMgXnX0D+Te2Iy5xdP8yueSlQB/x/ED2BTAtyE3K4SYUN6AMNfbO63f4lBW3bUJPbTL+mjSxS -PyRfJkZRgj+VbFv+EzHFi5pKwUEepa4JslMnwkowSRCXI+m5XvEOvtuBrxHdhLalG0JofYBok6qj -YdN2dEngUlbC4PG60M1WEN0piu7Nq7on0mgyyUw3iV1etLo6r/81biWdQ9MWHFaePWZYaq+nmp+t -s3az+sj7eA0jfgPfeoN1 -""") - -MH_MAGIC = 0xfeedface -MH_CIGAM = 0xcefaedfe -MH_MAGIC_64 = 0xfeedfacf -MH_CIGAM_64 = 0xcffaedfe -FAT_MAGIC = 0xcafebabe -BIG_ENDIAN = '>' -LITTLE_ENDIAN = '<' -LC_LOAD_DYLIB = 0xc -maxint = majver == 3 and getattr(sys, 'maxsize') or getattr(sys, 'maxint') - - -class fileview(object): - """ - A proxy for file-like objects that exposes a given view of a file. - Modified from macholib. - """ - - def __init__(self, fileobj, start=0, size=maxint): - if isinstance(fileobj, fileview): - self._fileobj = fileobj._fileobj - else: - self._fileobj = fileobj - self._start = start - self._end = start + size - self._pos = 0 - - def __repr__(self): - return '' % ( - self._start, self._end, self._fileobj) - - def tell(self): - return self._pos - - def _checkwindow(self, seekto, op): - if not (self._start <= seekto <= self._end): - raise IOError("%s to offset %d is outside window [%d, %d]" % ( - op, seekto, self._start, self._end)) - - def seek(self, offset, whence=0): - seekto = offset - if whence == os.SEEK_SET: - seekto += self._start - elif whence == os.SEEK_CUR: - seekto += self._start + self._pos - elif whence == os.SEEK_END: - seekto += self._end - else: - raise IOError("Invalid whence argument to seek: %r" % (whence,)) - self._checkwindow(seekto, 'seek') - self._fileobj.seek(seekto) - self._pos = seekto - self._start - - def write(self, bytes): - here = self._start + self._pos - self._checkwindow(here, 'write') - self._checkwindow(here + len(bytes), 'write') - self._fileobj.seek(here, os.SEEK_SET) - self._fileobj.write(bytes) - self._pos += len(bytes) - - def read(self, size=maxint): - assert size >= 0 - here = self._start + self._pos - self._checkwindow(here, 'read') - size = min(size, self._end - here) - self._fileobj.seek(here, os.SEEK_SET) - bytes = self._fileobj.read(size) - self._pos += len(bytes) - return bytes - - -def read_data(file, endian, num=1): - """ - Read a given number of 32-bits unsigned integers from the given file - with the given endianness. - """ - res = struct.unpack(endian + 'L' * num, file.read(num * 4)) - if len(res) == 1: - return res[0] - return res - - -def mach_o_change(path, what, value): - """ - Replace a given name (what) in any LC_LOAD_DYLIB command found in - the given binary with a new name (value), provided it's shorter. - """ - - def do_macho(file, bits, endian): - # Read Mach-O header (the magic number is assumed read by the caller) - cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = read_data(file, endian, 6) - # 64-bits header has one more field. - if bits == 64: - read_data(file, endian) - # The header is followed by ncmds commands - for n in range(ncmds): - where = file.tell() - # Read command header - cmd, cmdsize = read_data(file, endian, 2) - if cmd == LC_LOAD_DYLIB: - # The first data field in LC_LOAD_DYLIB commands is the - # offset of the name, starting from the beginning of the - # command. - name_offset = read_data(file, endian) - file.seek(where + name_offset, os.SEEK_SET) - # Read the NUL terminated string - load = file.read(cmdsize - name_offset).decode() - load = load[:load.index('\0')] - # If the string is what is being replaced, overwrite it. - if load == what: - file.seek(where + name_offset, os.SEEK_SET) - file.write(value.encode() + '\0'.encode()) - # Seek to the next command - file.seek(where + cmdsize, os.SEEK_SET) - - def do_file(file, offset=0, size=maxint): - file = fileview(file, offset, size) - # Read magic number - magic = read_data(file, BIG_ENDIAN) - if magic == FAT_MAGIC: - # Fat binaries contain nfat_arch Mach-O binaries - nfat_arch = read_data(file, BIG_ENDIAN) - for n in range(nfat_arch): - # Read arch header - cputype, cpusubtype, offset, size, align = read_data(file, BIG_ENDIAN, 5) - do_file(file, offset, size) - elif magic == MH_MAGIC: - do_macho(file, 32, BIG_ENDIAN) - elif magic == MH_CIGAM: - do_macho(file, 32, LITTLE_ENDIAN) - elif magic == MH_MAGIC_64: - do_macho(file, 64, BIG_ENDIAN) - elif magic == MH_CIGAM_64: - do_macho(file, 64, LITTLE_ENDIAN) - - assert(len(what) >= len(value)) - do_file(open(path, 'r+b')) - - -if __name__ == '__main__': - main() - -## TODO: -## Copy python.exe.manifest -## Monkeypatch distutils.sysconfig diff --git a/resources/common/zodb-install-instructions.rst b/resources/common/zodb-install-instructions.rst deleted file mode 100644 index 16dd439e..00000000 --- a/resources/common/zodb-install-instructions.rst +++ /dev/null @@ -1,124 +0,0 @@ -Installing the ZODB -=================== - -This takes some work. Start early. - - -*nix Prep ---------- - -The ZODB uses C extensions, and so installing it on OS X or Linux requires a -compiler and python's development headers. On linux systems, this means using -the system package manager: - -**Ubuntu/Debian**: - - $ sudo apt-get install python-dev - - -For **OS X** my usual approach is to install XCode. It's *big*, so expect it -to take a while if you don't already have it. Once you've downloaded it you'll -also need to install the command-line tools: - -* Open XCode -* Open the XCode menu, then click 'Preferences' > 'Downloads' > 'Install - Command Line Tools' -* Once this is done, you can close XCode again - -If XCode is too much, there appear to be alternatives available. I have not -tried any of these, but would appreciate any testimonial to their effectiveness: - -* There is a stand-alone package of the command-line tools available through - the apple developer center: https://developer.apple.com/downloads - -* Kenneth Reitz has a github repository with instructions and installers for a - stand-alone gcc compiler: https://github.com/kennethreitz/osx-gcc-installer - -If you use either of these methods to get gcc, you will still require the -python development headers. Installing a new version of Python using the -python.org installers or MacPorts or Homebrew should suffice to get this done. - - -**Windows**: See the next section - -Windows Prep ------------- - -Although there are pre-compiled binaries available for Windows, you'll need -one `.bat` file to get them to work properly. To get that file, you'll need to -install Visual Studio 2008 Express: - -* Download the installer (894MB): - http://download.microsoft.com/download/8/B/5/8B5804AD-4990-40D0-A6AA-CE894CBBB3DC/VS2008ExpressENUX1397868.iso -* Extract the files to a folder (call it VS2008ExpressENUX1397868—it will be - 2.68GB) using something like 7zip -* Inside that folder double-click on Setup.hta -* On the screen that comes up, click on the installer for Visual C++ 2008 - Express Edition and follow the instructions. **Note**: It does work if you - include the following two options which are pre-selected for you: (1) MSDN - Express Library for Visual Studio 2008, and (2) Microsoft SQL Server 2005 - Express Edition (x86). - -The above will work for 32-bit Windows. If you are using 64-bit Windwos, try -using the instructions on this wiki: - - http://wiki.cython.org/64BitCythonExtensionsOnWindows - -I have yet to hear of anyone getting Python or C-extensions running on Windows -8 without a titanic struggle. - - -Virtualenv ----------- - -With that prep work out of the way, you're ready to start. First, you'll need -to set up a virtualenv. Working with virtualenv is something we will cover in -session 3, so **if you are not comfortable or have never seen virtualenv -before, you probably want to wait to take these next steps**. - -These instructions assume you will be manually creating a virtualenv rather -than installing virtualenv with pip or easy_install. If you need a copy of the -``virtualenv.py`` file, you can find it in the same ``resources/common`` -directory where these instructions are located. Once you have a copy, simply: - -.. code-block:: bash - - $ python2.7 virtualenv.py pyramidenv - ... - $ source pyramidenv/bin/activate - (pyramidenv)$ - -Windows users: ``> pyramidenv\Scripts\activate`` - - -Install ZODB ------------- - -If you're on OS X or Linux: - - (pyramidenv)$ easy_install ZODB3==3.10.5 - -This will take some time. If you get errors, contact me directly or via the -Google Group. - -Windows users, you'll have it a bit easier here. You have to install a binary -egg: - - [pyramidenv]> pip install --egg ZODB3==3.10.5 - -Self Evaluation ---------------- - -At this point, you can check your work. Fire up a python interpreter in your -virtualenv: - -.. code-block:: bash - - (pyramidenv)$ python - >>> import ZODB - >>> ^D - (pyramidenv)$ - -If you get an ImportError when you try that, you're not done. Contact me. - -If you get no errors, you are ready to go for our final sessions. diff --git a/resources/session01/echo_client.py b/resources/session01/echo_client.py index c95e7c60..6b2f0472 100644 --- a/resources/session01/echo_client.py +++ b/resources/session01/echo_client.py @@ -2,32 +2,47 @@ import sys -def client(msg): +def client(msg, log_buffer=sys.stderr): server_address = ('localhost', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - print >>sys.stderr, 'connecting to %s port %s' % server_address - sock.connect(server_address) + # TODO: Replace the following line with your code which will instantiate + # a TCP socket with IPv4 Addressing, call the socket you make 'sock' + sock = None + print('connecting to {0} port {1}'.format(*server_address), file=log_buffer) + # TODO: connect your socket to the server here. + + # you can use this variable to accumulate the entire message received back + # from the server + received_message = '' + + # this try/finally block exists purely to allow us to close the socket + # when we are finished with it try: - # Send data - print >>sys.stderr, 'sending "%s"' % msg - sock.sendall(msg) - # Look for the response - amount_received = 0 - amount_expected = len(msg) - while amount_received < amount_expected: - data = sock.recv(16) - amount_received += len(data) - print >>sys.stderr, 'received "%s"' % data + print('sending "{0}"'.format(msg), file=log_buffer) + # TODO: send your message to the server here. + + # TODO: the server should be sending you back your message as a series + # of 16-byte chunks. Accumulate the chunks you get to build the + # entire reply from the server. Make sure that you have received + # the entire message and then you can break the loop. + # + # Log each chunk you receive. Use the print statement below to + # do it. This will help in debugging problems + chunk = '' + print('received "{0}"'.format(chunk.decode('utf8')), file=log_buffer) finally: - print >>sys.stderr, 'closing socket' - sock.close() + # TODO: after you break out of the loop receiving echoed chunks from + # the server you will want to close your client socket. + print('closing socket', file=log_buffer) + + # TODO: when all is said and done, you should return the entire reply + # you received from the server as the return value of this function. if __name__ == '__main__': if len(sys.argv) != 2: - usg = '\nusage: python echo_client.py "this is my message"\n' - print >>sys.stderr, usg + usage = '\nusage: python echo_client.py "this is my message"\n' + print(usage, file=sys.stderr) sys.exit(1) - + msg = sys.argv[1] - client(msg) \ No newline at end of file + client(msg) diff --git a/resources/session01/echo_server.py b/resources/session01/echo_server.py index f28d8517..4103ac6a 100644 --- a/resources/session01/echo_server.py +++ b/resources/session01/echo_server.py @@ -2,39 +2,75 @@ import sys -def server(): +def server(log_buffer=sys.stderr): + # set an address for our server address = ('127.0.0.1', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print >>sys.stderr, "making a server on %s:%s" % address - sock.bind(address) - sock.listen(1) - + # TODO: Replace the following line with your code which will instantiate + # a TCP socket with IPv4 Addressing, call the socket you make 'sock' + sock = None + # TODO: You may find that if you repeatedly run the server script it fails, + # claiming that the port is already used. You can set an option on + # your socket that will fix this problem. We DID NOT talk about this + # in class. Find the correct option by reading the very end of the + # socket library documentation: + # http://docs.python.org/3/library/socket.html#example + + # log that we are building a server + print("making a server on {0}:{1}".format(*address), file=log_buffer) + + # TODO: bind your new sock 'sock' to the address above and begin to listen + # for incoming connections + try: + # the outer loop controls the creation of new connection sockets. The + # server will handle each incoming connection one at a time. while True: - print >>sys.stderr, 'waiting for a connection' - conn, addr = sock.accept() # blocking + print('waiting for a connection', file=log_buffer) + + # TODO: make a new socket when a client connects, call it 'conn', + # at the same time you should be able to get the address of + # the client so we can report it below. Replace the + # following line with your code. It is only here to prevent + # syntax errors + addr = ('bar', 'baz') try: - print >>sys.stderr, 'connection - %s:%s' % addr + print('connection - {0}:{1}'.format(*addr), file=log_buffer) + + # the inner loop will receive messages sent by the client in + # buffers. When a complete message has been received, the + # loop will exit while True: - data = conn.recv(16) - print >>sys.stderr, 'received "%s"' % data - if data: - msg = 'sending data back to client' - print >>sys.stderr, msg - conn.sendall(data) - else: - msg = 'no more data from %s:%s' % addr - print >>sys.stderr, msg - break + # TODO: receive 16 bytes of data from the client. Store + # the data you receive as 'data'. Replace the + # following line with your code. It's only here as + # a placeholder to prevent an error in string + # formatting + data = b'' + print('received "{0}"'.format(data.decode('utf8'))) + # TODO: Send the data you received back to the client, log + # the fact using the print statement here. It will help in + # debugging problems. + print('sent "{0}"'.format(data.decode('utf8'))) + # TODO: Check here to see if the message you've received is + # complete. If it is, break out of this inner loop. + finally: - conn.close() - + # TODO: When the inner loop exits, this 'finally' clause will + # be hit. Use that opportunity to close the socket you + # created above when a client connected. + print( + 'echo complete, client connection closed', file=log_buffer + ) + except KeyboardInterrupt: - sock.close() - return + # TODO: Use the python KeyboardInterrupt exception as a signal to + # close the server socket and exit from the server function. + # Replace the call to `pass` below, which is only there to + # prevent syntax problems + pass + print('quitting echo server', file=log_buffer) if __name__ == '__main__': server() - sys.exit(0) \ No newline at end of file + sys.exit(0) diff --git a/resources/session01/session1.py b/resources/session01/session1.py deleted file mode 100644 index 228296d8..00000000 --- a/resources/session01/session1.py +++ /dev/null @@ -1,21 +0,0 @@ -import socket - - -def get_constants(prefix): - return dict( (getattr(socket, n), n) - for n in dir(socket) - if n.startswith(prefix)) - - -def get_address_info(host, port): - families = get_constants('AF_') - types = get_constants('SOCK_') - protocols = get_constants('IPPROTO_') - for response in socket.getaddrinfo(host, port): - fam, typ, pro, nam, add = response - print 'family: ', families[fam] - print 'type: ', types[typ] - print 'protocol: ', protocols[pro] - print 'canonical name: ', nam - print 'socket address: ', add - print diff --git a/resources/session01/socket_tools.py b/resources/session01/socket_tools.py new file mode 100644 index 00000000..d2bc1e18 --- /dev/null +++ b/resources/session01/socket_tools.py @@ -0,0 +1,21 @@ +import socket + + +def get_constants(prefix): + return {getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)} + + +families = get_constants('AF_') +types = get_constants('SOCK_') +protocols = get_constants('IPPROTO_') + + +def get_address_info(host, port): + for response in socket.getaddrinfo(host, port): + fam, typ, pro, nam, add = response + print('family: {}'.format(families[fam])) + print('type: {}'.format(types[typ])) + print('protocol: {}'.format(protocols[pro])) + print('canonical name: {}'.format(nam)) + print('socket address: {}'.format(add)) + print() diff --git a/resources/session01/tasks.txt b/resources/session01/tasks.txt new file mode 100644 index 00000000..8fdab003 --- /dev/null +++ b/resources/session01/tasks.txt @@ -0,0 +1,53 @@ +Session 4 Homework +================== + +Required Tasks: +--------------- + +* Complete the code in ``echo_server.py`` to create a server that sends back + whatever messages it receives from a client + +* Complete the code in ``echo_client.py`` to create a client function that + can send a message and receive a reply. + +* Ensure that the tests in ``tests.py`` pass. + +To run the tests: + +* Open one terminal while in this folder and execute this command: + + $ python echo_server.py + +* Open a second terminal in this same folder and execute this command: + + $ python tests.py + + + + +Optional Tasks: +--------------- + +Simple: + +* Write a python function that lists the services provided by a given range of + ports. + + * accept the lower and upper bounds as arguments + * provide sensible defaults + * Ensure that it only accepts valid port numbers (0-65535) + +Challenging: + +* The echo server as outlined will only process a connection from one client + at a time. If a second client were to attempt a connection, it would have to + wait until the first message was fully echoed before it could be dealt with. + + Python provides a module called `select` that allows waiting for I/O events + in order to control flow. The `select.select` method can be used to allow + our echo server to handle more than one incoming connection in "parallel". + + Read the documentation about the `select` module + (http://docs.python.org/3/library/select.html) and attempt to write a second + version of the echo server that can handle multiple client connections in + "parallel". You do not need to invoke threading of any kind to do this. diff --git a/resources/session01/tests.py b/resources/session01/tests.py new file mode 100644 index 00000000..d4c6c791 --- /dev/null +++ b/resources/session01/tests.py @@ -0,0 +1,46 @@ +from echo_client import client +import socket +import unittest + + +class EchoTestCase(unittest.TestCase): + """tests for the echo server and client""" + + def send_message(self, message): + """Attempt to send a message using the client + + In case of a socket error, fail and report the problem + """ + try: + reply = client(message) + except socket.error as e: + if e.errno == 61: + msg = "Error: {0}, is the server running?" + self.fail(msg.format(e.strerror)) + else: + self.fail("Unexpected Error: {0}".format(str(e))) + return reply + + def test_short_message_echo(self): + """test that a message short than 16 bytes echoes cleanly""" + expected = "short message" + actual = self.send_message(expected) + self.assertEqual( + expected, + actual, + "expected {0}, got {1}".format(expected, actual) + ) + + def test_long_message_echo(self): + """test that a message longer than 16 bytes echoes in 16-byte chunks""" + expected = "Four score and seven years ago our fathers did stuff" + actual = self.send_message(expected) + self.assertEqual( + expected, + actual, + "expected {0}, got {1}".format(expected, actual) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/session02/homework/http_server.py b/resources/session02/homework/http_server.py new file mode 100644 index 00000000..84ceffe1 --- /dev/null +++ b/resources/session02/homework/http_server.py @@ -0,0 +1,86 @@ +import socket +import sys + + +def response_ok(body=b"this is a pretty minimal response", mimetype=b"text/plain"): + """returns a basic HTTP response""" + resp = [] + resp.append(b"HTTP/1.1 200 OK") + resp.append(b"Content-Type: text/plain") + resp.append(b"") + resp.append(b"this is a pretty minimal response") + return b"\r\n".join(resp) + + +def response_method_not_allowed(): + """returns a 405 Method Not Allowed response""" + resp = [] + resp.append("HTTP/1.1 405 Method Not Allowed") + resp.append("") + return "\r\n".join(resp).encode('utf8') + + +def response_not_found(): + """returns a 404 Not Found response""" + return b"" + + +def parse_request(request): + first_line = request.split("\r\n", 1)[0] + method, uri, protocol = first_line.split() + if method != "GET": + raise NotImplementedError("We only accept GET") + return uri + + +def resolve_uri(uri): + """This method should return appropriate content and a mime type""" + return b"still broken", b"text/plain" + + +def server(log_buffer=sys.stderr): + address = ('127.0.0.1', 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + print("making a server on {0}:{1}".format(*address), file=log_buffer) + sock.bind(address) + sock.listen(1) + + try: + while True: + print('waiting for a connection', file=log_buffer) + conn, addr = sock.accept() # blocking + try: + print('connection - {0}:{1}'.format(*addr), file=log_buffer) + request = '' + while True: + data = conn.recv(1024) + request += data.decode('utf8') + if len(data) < 1024: + break + + try: + uri = parse_request(request) + except NotImplementedError: + response = response_method_not_allowed() + else: + try: + content, mime_type = resolve_uri(uri) + except NameError: + response = response_not_found() + else: + response = response_ok(content, mime_type) + + print('sending response', file=log_buffer) + conn.sendall(response) + finally: + conn.close() + + except KeyboardInterrupt: + sock.close() + return + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/resources/session02/homework/simple_client.py b/resources/session02/homework/simple_client.py new file mode 100644 index 00000000..2c9ed4cd --- /dev/null +++ b/resources/session02/homework/simple_client.py @@ -0,0 +1,44 @@ +import socket +import sys + + +def bytes_client(msg): + server_address = ('localhost', 10000) + sock = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) + print( + 'connecting to {0} port {1}'.format(*server_address), + file=sys.stderr + ) + sock.connect(server_address) + response = b'' + done = False + bufsize = 1024 + try: + print('sending "{0}"'.format(msg), file=sys.stderr) + sock.sendall(msg.encode('utf8')) + while not done: + chunk = sock.recv(bufsize) + if len(chunk) < bufsize: + done = True + response += chunk + print('received "{0}"'.format(response), file=sys.stderr) + finally: + print('closing socket', file=sys.stderr) + sock.close() + return response + + +def client(msg): + return bytes_client(msg).decode('utf8') + + +if __name__ == '__main__': + if len(sys.argv) != 2: + usg = '\nusage: python echo_client.py "this is my message"\n' + print(usg, file=sys.stderr) + sys.exit(1) + + msg = sys.argv[1] + client(msg) diff --git a/resources/session02/homework/tests.py b/resources/session02/homework/tests.py new file mode 100644 index 00000000..45007311 --- /dev/null +++ b/resources/session02/homework/tests.py @@ -0,0 +1,427 @@ +import mimetypes +import os +import pathlib +import socket +import unittest + + +CRLF = '\r\n' +CRLF_BYTES = CRLF.encode('utf8') +KNOWN_TYPES = set( + map(lambda x: x.encode('utf8'), mimetypes.types_map.values()) +) + + +def extract_response_code(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[1].strip() + + +def extract_response_protocol(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[0].strip() + + +def extract_headers(response): + return response.split(CRLF_BYTES*2, 1)[0].split(CRLF_BYTES)[1:] + + +def extract_body(response): + return response.split(CRLF_BYTES*2, 1)[1] + + +class ResponseOkTestCase(unittest.TestCase): + """unit tests for the response_ok method in our server + + Becase this is a unit test case, it does not require the server to be + running. + """ + + def call_function_under_test(self, body=b"", mimetype=b"text/plain"): + """call the `response_ok` function from our http_server module""" + from http_server import response_ok + return response_ok(body=body, mimetype=mimetype) + + def test_response_code(self): + ok = self.call_function_under_test() + expected = "200 OK" + actual = extract_response_code(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_protocol(self): + ok = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_has_content_type_header(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + has_header = False + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + has_header = True + break + self.assertTrue(has_header) + + def test_response_has_legitimate_content_type(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + self.assertTrue(value.strip() in KNOWN_TYPES) + return + self.fail('no content type header found') + + def test_passed_mimetype_in_response(self): + mimetypes = [ + b'image/jpeg', b'text/html', b'text/x-python', + ] + header_name = b'content-type' + for expected in mimetypes: + ok = self.call_function_under_test(mimetype=expected) + headers = extract_headers(ok) + for header in headers: + name, value = header.split(b':') + if header_name == name.strip().lower(): + actual = value.strip() + self.assertEqual( + expected, + actual, + "expected {0}, got {1}".format(expected, actual) + ) + + def test_passed_body_in_response(self): + bodies = [ + b"a body", + b"a longer body\nwith two lines", + pathlib.Path("webroot/sample.txt").read_bytes(), + ] + for expected in bodies: + ok = self.call_function_under_test(body=expected) + actual = extract_body(ok) + self.assertEqual( + expected, + actual, + "expected {0}, got {1}".format(expected, actual)) + + +class ResponseMethodNotAllowedTestCase(unittest.TestCase): + """unit tests for the response_method_not_allowed function""" + + def call_function_under_test(self): + """call the `response_method_not_allowed` function""" + from http_server import response_method_not_allowed + return response_method_not_allowed() + + def test_response_code(self): + resp = self.call_function_under_test() + expected = "405 Method Not Allowed" + actual = extract_response_code(resp) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_method(self): + resp = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(resp) + self.assertEqual(expected.encode('utf8'), actual) + + +class ResponseNotFoundTestCase(unittest.TestCase): + """unit tests for the response_not_found function""" + + def call_function_under_test(self): + """call the 'response_not_found' function""" + from http_server import response_not_found + return response_not_found() + + def test_response_code(self): + resp = self.call_function_under_test() + expected = "404 Not Found" + actual = extract_response_code(resp) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_method(self): + resp = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(resp) + self.assertEqual(expected.encode('utf8'), actual) + + +class ParseRequestTestCase(unittest.TestCase): + """unit tests for the parse_request method""" + + def call_function_under_test(self, request): + """call the `parse_request` function""" + from http_server import parse_request + return parse_request(request) + + def test_get_method(self): + """verify that GET HTTP requests do not raise an error""" + request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + try: + self.call_function_under_test(request) + except (NotImplementedError, Exception) as e: + self.fail('GET method raises an error {0}'.format(str(e))) + + def test_bad_http_methods(self): + """verify that non-GET HTTP methods raise a NotImplementedError""" + methods = ['POST', 'PUT', 'DELETE', 'HEAD'] + request_template = "{0} / HTTP/1.1\r\nHost: example.com\r\n\r\n" + for method in methods: + request = request_template.format(method) + self.assertRaises( + NotImplementedError, self.call_function_under_test, request + ) + + def test_uri_returned(self): + """verify that the parse_request function returns a URI""" + URIs = [ + '/', '/a_web_page.html', '/sample.txt', '/images/sample_1.png', + ] + request_tmplt = "GET {0} HTTP/1.1" + for expected in URIs: + request = request_tmplt.format(expected) + actual = self.call_function_under_test(request) + self.assertEqual( + expected, + actual, + "expected {0}, got {1}".format(expected, actual) + ) + + +class ResolveURITestCase(unittest.TestCase): + """unit tests for the resolve_uri function""" + + def call_function_under_test(self, uri): + """call the resolve_uri function""" + from http_server import resolve_uri + content, mime_type = resolve_uri(uri) + return content, mime_type.decode('utf8') + + def test_directory_resource(self): + uri = '/' + expected_names = [ + 'a_web_page.html', 'images', 'make_time.py', 'sample.txt', + ] + expected_mimetype = "text/plain" + actual_body, actual_mimetype = self.call_function_under_test(uri) + self.assertEqual( + expected_mimetype, + actual_mimetype, + 'expected {0} got {1}'.format(expected_mimetype, actual_mimetype) + ) + actual_body = actual_body.decode('utf8') + for expected in expected_names: + self.assertTrue( + expected in actual_body, + '"{0}" not in "{1}"'.format(expected, actual_body) + ) + + def test_file_resource(self): + uris_types = { + '/a_web_page.html': 'text/html', + '/make_time.py': 'text/x-python', + '/sample.txt': 'text/plain', + } + for uri, expected_mimetype in uris_types.items(): + path = pathlib.Path("webroot{0}".format(uri)) + expected_body = path.read_bytes() + actual_body, actual_mimetype = self.call_function_under_test(uri) + self.assertEqual( + expected_mimetype, + actual_mimetype, + 'expected {0} got {1}'.format( + expected_mimetype, actual_mimetype + ) + ) + self.assertEqual( + expected_body, + actual_body, + 'expected {0} got {1}'.format( + expected_mimetype, actual_mimetype + ) + ) + + def test_image_resource(self): + names_types = { + 'JPEG_example.jpg': 'image/jpeg', + 'sample_1.png': 'image/png', + } + for filename, expected_mimetype in names_types.items(): + uri = "/images/{0}".format(filename) + path = pathlib.Path("webroot{0}".format(uri)) + expected_body = path.read_bytes() + actual_body, actual_mimetype = self.call_function_under_test(uri) + self.assertEqual( + expected_mimetype, + actual_mimetype, + 'expected {0} got {1}'.format( + expected_mimetype, actual_mimetype + ) + ) + self.assertEqual( + expected_body, + actual_body, + 'expected {0} got {1}'.format( + expected_mimetype, actual_mimetype + ) + ) + + def test_missing_resource(self): + uri = "/missing.html" + self.assertRaises(NameError, self.call_function_under_test, uri) + + +class HTTPServerFunctionalTestCase(unittest.TestCase): + """functional tests of the HTTP Server + + This test case interacts with the http server, and as such requires it to + be running in order for the tests to pass + """ + + def send_message(self, message, use_bytes=False): + """Attempt to send a message using the client and the test buffer + + In case of a socket error, fail and report the problem + """ + response = '' + if not use_bytes: + from simple_client import client + else: + from simple_client import bytes_client as client + + try: + response = client(message) + except socket.error as e: + if e.errno == 61: + msg = "Error: {0}, is the server running?" + self.fail(msg.format(e.strerror)) + else: + self.fail("Unexpected Error: {0}".format(str(e))) + return response + + def test_get_request(self): + message = CRLF.join(['GET / HTTP/1.1', 'Host: example.com', '']) + expected = '200 OK' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + def test_post_request(self): + message = CRLF.join(['POST / HTTP/1.1', 'Host: example.com', '']) + expected = '405 Method Not Allowed' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + def test_webroot_directory_resources(self): + """verify that directory uris are properly served""" + message_tmpl = CRLF.join(['GET {0} HTTP/1.1', 'Host: example.com', '']) + root = "webroot/" + for directory, directories, files in os.walk(root): + directory_uri = "/{0}".format(directory[len(root):]) + message = message_tmpl.format(directory_uri) + actual = self.send_message(message) + # verify that directory listings are correct + self.assertTrue( + "200 OK" in actual, + "request for {0} did not result in OK".format(directory_uri)) + for expected in directories + files: + self.assertTrue( + expected in actual, + '"{0}" not in "{1}"'.format(expected, actual) + ) + + def test_webroot_file_uris(self): + """verify that file uris are properly served""" + message_tmpl = CRLF.join(['GET {0} HTTP/1.1', 'Host: example.com', '']) + root = pathlib.Path("webroot") + for file_path in root.iterdir(): + # set up expectations for this file + if file_path.is_dir(): + continue + expected_body = file_path.read_bytes().decode('utf8') + expected_mimetype = mimetypes.types_map[ + os.path.splitext(str(file_path))[1] + ] + file_uri = str(file_path)[len(str(root)):] + message = message_tmpl.format(file_uri) + actual = self.send_message(message) + self.assertTrue( + "200 OK" in actual, + "request for {0} did not result in OK".format( + file_uri + ) + ) + self.assertTrue( + expected_mimetype in actual, + "mimetype {0} not in response for {1}".format( + expected_mimetype, file_uri + ) + ) + self.assertTrue( + expected_body in actual, + "body of {0} not in response for {1}".format( + file_path, file_uri + ) + ) + + def test_webroot_image_uris(self): + """verify that image uris are properly served + + requires using a client that does not attempt to decode the response + body + """ + message_tmpl = CRLF.join(['GET {0} HTTP/1.1', 'Host: example.com', '']) + root = pathlib.Path("webroot") + images_path = root / 'images' + for file_path in images_path.iterdir(): + # set up expectations for this file + if file_path.is_dir(): + continue + expected_body = file_path.read_bytes() + expected_mimetype = mimetypes.types_map[ + os.path.splitext(str(file_path))[1] + ] + file_uri = str(file_path)[len(str(root)):] + message = message_tmpl.format(file_uri) + actual = self.send_message(message, use_bytes=True) + self.assertTrue( + b"200 OK" in actual, + "request for {0} did not result in OK".format( + file_uri + ) + ) + self.assertTrue( + expected_mimetype.encode('utf8') in actual, + "mimetype {0} not in response for {1}".format( + expected_mimetype, file_uri + ) + ) + self.assertTrue( + expected_body in actual, + "body of {0} not in response for {1}".format( + file_path, file_uri + ) + ) + + def test_missing_resource(self): + message = CRLF.join( + ['GET /missing.html HTTP/1.1', 'Host: example.com', ''] + ) + expected = '404 Not Found' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/session02/webroot/a_web_page.html b/resources/session02/homework/webroot/a_web_page.html similarity index 100% rename from resources/session02/webroot/a_web_page.html rename to resources/session02/homework/webroot/a_web_page.html diff --git a/resources/session02/webroot/images/JPEG_example.jpg b/resources/session02/homework/webroot/images/JPEG_example.jpg similarity index 100% rename from resources/session02/webroot/images/JPEG_example.jpg rename to resources/session02/homework/webroot/images/JPEG_example.jpg diff --git a/resources/session02/webroot/images/Sample_Scene_Balls.jpg b/resources/session02/homework/webroot/images/Sample_Scene_Balls.jpg similarity index 100% rename from resources/session02/webroot/images/Sample_Scene_Balls.jpg rename to resources/session02/homework/webroot/images/Sample_Scene_Balls.jpg diff --git a/resources/session02/webroot/images/sample_1.png b/resources/session02/homework/webroot/images/sample_1.png similarity index 100% rename from resources/session02/webroot/images/sample_1.png rename to resources/session02/homework/webroot/images/sample_1.png diff --git a/resources/session02/webroot/make_time.py b/resources/session02/homework/webroot/make_time.py similarity index 89% rename from resources/session02/webroot/make_time.py rename to resources/session02/homework/webroot/make_time.py index d3064dd2..b69acf38 100644 --- a/resources/session02/webroot/make_time.py +++ b/resources/session02/homework/webroot/make_time.py @@ -17,9 +17,6 @@

%s

-"""% time_str - -print html - - +""" % time_str +print(html) diff --git a/resources/session02/webroot/sample.txt b/resources/session02/homework/webroot/sample.txt similarity index 100% rename from resources/session02/webroot/sample.txt rename to resources/session02/homework/webroot/sample.txt diff --git a/resources/session02/http_server.py b/resources/session02/http_server.py new file mode 100644 index 00000000..d5aaf480 --- /dev/null +++ b/resources/session02/http_server.py @@ -0,0 +1,39 @@ +import socket +import sys + + +def server(log_buffer=sys.stderr): + address = ('127.0.0.1', 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + print("making a server on {0}:{1}".format(*address), file=log_buffer) + sock.bind(address) + sock.listen(1) + + try: + while True: + print('waiting for a connection', file=log_buffer) + conn, addr = sock.accept() # blocking + try: + print('connection - {0}:{1}'.format(*addr), file=log_buffer) + while True: + data = conn.recv(16) + print('received "{0}"'.format(data), file=log_buffer) + if data: + print('sending data back to client', file=log_buffer) + conn.sendall(data) + else: + msg = 'no more data from {0}:{1}'.format(*addr) + print(msg, log_buffer) + break + finally: + conn.close() + + except KeyboardInterrupt: + sock.close() + return + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/resources/session02/http_server_1.py b/resources/session02/http_server_1.py deleted file mode 100644 index c9d129f3..00000000 --- a/resources/session02/http_server_1.py +++ /dev/null @@ -1,71 +0,0 @@ -import socket -import sys - - -def response_ok(): - """returns a basic HTTP response""" - resp = [] - resp.append("HTTP/1.1 200 OK") - resp.append("Content-Type: text/plain") - resp.append("") - resp.append("this is a pretty minimal response") - return "\r\n".join(resp) - - -def response_method_not_allowed(): - """returns a 405 Method Not Allowed response""" - resp = [] - resp.append("HTTP/1.1 405 Method Not Allowed") - resp.append("") - return "\r\n".join(resp) - - -def parse_request(request): - first_line = request.split("\r\n", 1)[0] - method, uri, protocol = first_line.split() - if method != "GET": - raise NotImplementedError("We only accept GET") - print >>sys.stderr, 'request is okay' - - -def server(): - address = ('127.0.0.1', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print >>sys.stderr, "making a server on %s:%s" % address - sock.bind(address) - sock.listen(1) - - try: - while True: - print >>sys.stderr, 'waiting for a connection' - conn, addr = sock.accept() # blocking - try: - print >>sys.stderr, 'connection - %s:%s' % addr - request = "" - while True: - data = conn.recv(1024) - request += data - if len(data) < 1024 or not data: - break - - try: - parse_request(request) - except NotImplementedError: - response = response_method_not_allowed() - else: - response = response_ok() - - print >>sys.stderr, 'sending response' - conn.sendall(response) - finally: - conn.close() - - except KeyboardInterrupt: - sock.close() - return - - -if __name__ == '__main__': - server() - sys.exit(0) \ No newline at end of file diff --git a/resources/session02/http_server_2.py b/resources/session02/http_server_2.py deleted file mode 100644 index b7c250d5..00000000 --- a/resources/session02/http_server_2.py +++ /dev/null @@ -1,102 +0,0 @@ -import socket -import sys -import os -import mimetypes - - -def response_ok(body, mimetype): - """returns a basic HTTP response""" - resp = [] - resp.append("HTTP/1.1 200 OK") - resp.append("Content-Type: %s" % mimetype) - resp.append("") - resp.append(body) - return "\r\n".join(resp) - - -def response_method_not_allowed(): - """returns a 405 Method Not Allowed response""" - resp = [] - resp.append("HTTP/1.1 405 Method Not Allowed") - resp.append("") - return "\r\n".join(resp) - - -def response_not_found(): - """return a 404 Not Found response""" - resp = [] - resp.append("HTTP/1.1 404 Not Found") - resp.append("") - return "\r\n".join(resp) - - -def parse_request(request): - first_line = request.split("\r\n", 1)[0] - method, uri, protocol = first_line.split() - if method != "GET": - raise NotImplementedError("We only accept GET") - print >>sys.stderr, 'serving request for %s' % uri - return uri - - -def resolve_uri(uri): - """return the filesystem resources identified by 'uri'""" - home = 'webroot' # this is relative to the location of - # the server script, could be a full path - filename = os.path.join(home, uri.lstrip('/')) - if os.path.isfile(filename): - ext = os.path.splitext(filename)[1] - mimetype = mimetypes.types_map.get(ext, 'text/plain') - contents = open(filename, 'rb').read() - return contents, mimetype - elif os.path.isdir(filename): - listing = "\n".join(os.listdir(filename)) - return listing, 'text/plain' - else: - raise ValueError("Not Found") - - -def server(): - address = ('127.0.0.1', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print >>sys.stderr, "making a server on %s:%s" % address - sock.bind(address) - sock.listen(1) - - try: - while True: - print >>sys.stderr, 'waiting for a connection' - conn, addr = sock.accept() # blocking - try: - print >>sys.stderr, 'connection - %s:%s' % addr - request = "" - while True: - data = conn.recv(1024) - request += data - if len(data) < 1024 or not data: - break - - try: - uri = parse_request(request) - content, mimetype = resolve_uri(uri) - except NotImplementedError: - response = response_method_not_allowed() - except ValueError: - response = response_not_found() - else: - response = response_ok(content, mimetype) - - print >>sys.stderr, 'sending response' - conn.sendall(response) - finally: - conn.close() - - except KeyboardInterrupt: - sock.close() - return - - -if __name__ == '__main__': - server() - sys.exit(0) \ No newline at end of file diff --git a/resources/session02/simple_client.py b/resources/session02/simple_client.py new file mode 100644 index 00000000..74523a2a --- /dev/null +++ b/resources/session02/simple_client.py @@ -0,0 +1,40 @@ +import socket +import sys + + +def client(msg): + server_address = ('localhost', 10000) + sock = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) + print( + 'connecting to {0} port {1}'.format(*server_address), + file=sys.stderr + ) + sock.connect(server_address) + response = '' + done = False + bufsize = 1024 + try: + print('sending "{0}"'.format(msg), file=sys.stderr) + sock.sendall(msg.encode('utf8')) + while not done: + chunk = sock.recv(bufsize) + if len(chunk) < bufsize: + done = True + response += chunk.decode('utf8') + print('received "{0}"'.format(response), file=sys.stderr) + finally: + print('closing socket', file=sys.stderr) + sock.close() + return response + + +if __name__ == '__main__': + if len(sys.argv) != 2: + usg = '\nusage: python echo_client.py "this is my message"\n' + print(usg, file=sys.stderr) + sys.exit(1) + + msg = sys.argv[1] + client(msg) diff --git a/resources/session02/tests.py b/resources/session02/tests.py new file mode 100644 index 00000000..a4da1793 --- /dev/null +++ b/resources/session02/tests.py @@ -0,0 +1,165 @@ +import mimetypes +import socket +import unittest + + +CRLF = '\r\n' +CRLF_BYTES = CRLF.encode('utf8') +KNOWN_TYPES = set( + map(lambda x: x.encode('utf8'), mimetypes.types_map.values()) +) + + +def extract_response_code(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[1].strip() + + +def extract_response_protocol(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[0].strip() + + +def extract_headers(response): + return response.split(CRLF_BYTES*2, 1)[0].split(CRLF_BYTES)[1:] + + +class ResponseOkTestCase(unittest.TestCase): + """unit tests for the response_ok method in our server + + Becase this is a unit test case, it does not require the server to be + running. + """ + + def call_function_under_test(self): + """call the `response_ok` function from our http_server module""" + from http_server import response_ok + return response_ok() + + def test_response_code(self): + ok = self.call_function_under_test() + expected = "200 OK" + actual = extract_response_code(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_protocol(self): + ok = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_has_content_type_header(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + has_header = False + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + has_header = True + break + self.assertTrue(has_header) + + def test_response_has_legitimate_content_type(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + self.assertTrue(value.strip() in KNOWN_TYPES) + return + self.fail('no content type header found') + + +class ResponseMethodNotAllowedTestCase(unittest.TestCase): + """unit tests for the response_method_not_allowed function""" + + def call_function_under_test(self): + """call the `response_method_not_allowed` function""" + from http_server import response_method_not_allowed + return response_method_not_allowed() + + def test_response_code(self): + resp = self.call_function_under_test() + expected = "405 Method Not Allowed" + actual = extract_response_code(resp) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_method(self): + resp = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(resp) + self.assertEqual(expected.encode('utf8'), actual) + + +class ParseRequestTestCase(unittest.TestCase): + """unit tests for the parse_request method""" + + def call_function_under_test(self, request): + """call the `parse_request` function""" + from http_server import parse_request + return parse_request(request) + + def test_get_method(self): + """verify that GET HTTP requests do not raise an error""" + request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + try: + self.call_function_under_test(request) + except (NotImplementedError, Exception) as e: + self.fail('GET method raises an error {0}'.format(str(e))) + + def test_bad_http_methods(self): + """verify that non-GET HTTP methods raise a NotImplementedError""" + methods = ['POST', 'PUT', 'DELETE', 'HEAD'] + request_template = "{0} / HTTP/1.1\r\nHost: example.com\r\n\r\n" + for method in methods: + request = request_template.format(method) + self.assertRaises( + NotImplementedError, self.call_function_under_test, request + ) + + +class HTTPServerFunctionalTestCase(unittest.TestCase): + """functional tests of the HTTP Server + + This test case interacts with the http server, and as such requires it to + be running in order for the tests to pass + """ + + def send_message(self, message): + """Attempt to send a message using the client and the test buffer + + In case of a socket error, fail and report the problem + """ + from simple_client import client + response = '' + try: + response = client(message) + except socket.error as e: + if e.errno == 61: + msg = "Error: {0}, is the server running?" + self.fail(msg.format(e.strerror)) + else: + self.fail("Unexpected Error: {0}".format(str(e))) + return response + + def test_get_request(self): + message = CRLF.join(['GET / HTTP/1.1', 'Host: example.com', '']) + expected = '200 OK' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + def test_post_request(self): + message = CRLF.join(['POST / HTTP/1.1', 'Host: example.com', '']) + expected = '405 Method Not Allowed' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/session02/webroot/web/a_web_page.html b/resources/session02/webroot/web/a_web_page.html deleted file mode 100644 index 82e96100..00000000 --- a/resources/session02/webroot/web/a_web_page.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - -

My First Heading

- -

My first paragraph.

- - - - diff --git a/resources/session02/webroot/web/images/JPEG_example.jpg b/resources/session02/webroot/web/images/JPEG_example.jpg deleted file mode 100644 index 13506f01..00000000 Binary files a/resources/session02/webroot/web/images/JPEG_example.jpg and /dev/null differ diff --git a/resources/session02/webroot/web/images/Sample_Scene_Balls.jpg b/resources/session02/webroot/web/images/Sample_Scene_Balls.jpg deleted file mode 100644 index 1c0ccade..00000000 Binary files a/resources/session02/webroot/web/images/Sample_Scene_Balls.jpg and /dev/null differ diff --git a/resources/session02/webroot/web/images/sample_1.png b/resources/session02/webroot/web/images/sample_1.png deleted file mode 100644 index 5b2f52df..00000000 Binary files a/resources/session02/webroot/web/images/sample_1.png and /dev/null differ diff --git a/resources/session02/webroot/web/make_time.py b/resources/session02/webroot/web/make_time.py deleted file mode 100644 index d3064dd2..00000000 --- a/resources/session02/webroot/web/make_time.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python - -""" -make_time.py - -simple script that returns and HTML page with the current time -""" - -import datetime - -time_str = datetime.datetime.now().isoformat() - -html = """ - - -

The time is:

-

%s

- - -"""% time_str - -print html - - - diff --git a/resources/session02/webroot/web/sample.txt b/resources/session02/webroot/web/sample.txt deleted file mode 100644 index 1965c7d3..00000000 --- a/resources/session02/webroot/web/sample.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a very simple text file. -Just to show that we can server it up. -It is three lines long. diff --git a/resources/session04/cgi/cgi-bin/cgi_1.py b/resources/session03/cgi/cgi-bin/cgi_1.py similarity index 52% rename from resources/session04/cgi/cgi-bin/cgi_1.py rename to resources/session03/cgi/cgi-bin/cgi_1.py index b969de64..baa5c3e9 100755 --- a/resources/session04/cgi/cgi-bin/cgi_1.py +++ b/resources/session03/cgi/cgi-bin/cgi_1.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import cgi diff --git a/resources/session03/cgi/cgi-bin/cgi_2.py b/resources/session03/cgi/cgi-bin/cgi_2.py new file mode 100755 index 00000000..100ccded --- /dev/null +++ b/resources/session03/cgi/cgi-bin/cgi_2.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import cgi +import cgitb +cgitb.enable() +import os +import datetime + + +default = "No Value Present" + + +print("Content-Type: text/html") +print() + +body = """ + +Lab 1 - CGI experiments + + +

Hey there, this page has been generated by {software}, running {script}

+

Today is {month} {date}, {year}.

+

This page was requested by IP Address {client_ip}

+ +""".format( + software=os.environ.get('SERVER_SOFTWARE', default), + script='aaaa', + month='bbbb', + date='cccc', + year='dddd', + client_ip='eeee' +) +print(body) diff --git a/resources/session03/cgi/cgi-bin/cgi_sums.py b/resources/session03/cgi/cgi-bin/cgi_sums.py new file mode 100755 index 00000000..feed2bc8 --- /dev/null +++ b/resources/session03/cgi/cgi-bin/cgi_sums.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +import cgi +import cgitb + +cgitb.enable() + +print("Content-type: text/plain") +print() +print("Your job is to make this work") diff --git a/resources/session04/cgi/index.html b/resources/session03/cgi/index.html similarity index 70% rename from resources/session04/cgi/index.html rename to resources/session03/cgi/index.html index f554102f..d6725d8b 100644 --- a/resources/session04/cgi/index.html +++ b/resources/session03/cgi/index.html @@ -1,11 +1,11 @@ - IPIP: Week 4 Lab Examples + Python 200: Session 06 Lab Examples -

Internet Programming In Python

-

Week 4: CGI, WSGI and Living Online

+

Python 200

+

Session 06: CGI, WSGI and Living Online

CGI Examples

  1. CGI Test 1
  2. @@ -14,4 +14,4 @@

    CGI Examples

WSGI Examples

- \ No newline at end of file + diff --git a/resources/session03/craigslist_results.html b/resources/session03/craigslist_results.html deleted file mode 100644 index e9483d9b..00000000 --- a/resources/session03/craigslist_results.html +++ /dev/null @@ -1,3985 +0,0 @@ - - - - - raleigh apts/housing for rent classifieds - craigslist - - - - - - - - - - - -
-
-
-
-
- [ - - help - - ] - [ - - post - - ] -
- -
- - - -
-
-
- -
-
- - apts/housing for rent - -
- - search for: - - - - in: - - - - -
- - - -
- - -
- - rent: - - - - - - - - - -
-
-
-
-
-
- sort by: - - most recent - - - low price - - - high price - -
-
- - - - -
-
-
- found 2500 postings -
-

- - |< - - first - - -
-
- - < - - prev - - - - - - next - - > - - - - 1 - 100 - -
-
-

-

- - - - - - - - Jul 20 - - - - NEW Listing! Town Home in Mid-Town with a Huge Back Yard, FP, Storage - - - - - - $895 - - / 2br - 1160ft² - - - - - (Raleigh, NC - near North Hills) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - 122 Andrew: Quiet cul-de-sac in Wake Forest! Ranch! - - - - - - $995 - - / 3br - 1100ft² - - - - - (Wake Forest: Available 9/3/13) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Large Pets Welcome - - - - - - $775 - - / 2br - 1095ft² - - - - - (Raleigh) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - NEW Listing!! Great Home w/ Hardwoods, Fenced in Yard, Garage - - - - - - $995 - - / 2br - 912ft² - - - - - (Raleigh, NC ) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - NEW Listing Great Condo off Western Blvd w/ Patio, Washer & Dryer - - - - - - $925 - - / 2br - 1296ft² - - - - - (Raleigh, NC ) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Brand New 3 BR, 2 BA - - - - - - $600 - - / 3br - - - - - (Raleigh nc) - - - - - - - -

-

- - - - - - - - Jul 20 - - - - Find your perfect home! - - - - - - $750 - - / 2br - 1075ft² - - - - - (Raleigh) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - New Listing!! Great Condo in N Raleigh w/ Hardwoods, Deck Storage - - - - - - $895 - - / 2br - 1392ft² - - - - - (Raleigh, NC ) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Get Lucky with Us! - - - - - - $960 - - / 3br - 1185ft² - - - - - (Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Quaint 3 bedroom, close to beach - - - - - - $550 - - / 3br - - - - - (Raleigh, nc) - - - - - - - -

-

- - - - - - - - Jul 20 - - - - Almost new property in irresistible community! - - - - - - $500 - - / 2br - 1075ft² - - - - - (Apex) - - - - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Brand NEW LUXURY Community...This One's a NO BRAINER!! - - - - - - $995 - - / 2br - 1065ft² - - - - - (Cary/Morrisville/Apex/RTP/Hwy 55) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Pick Me, Pick Me! - - - - - - $835 - - / 2br - 951ft² - - - - - (Chapel Hill) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Our Best Kept Secret! - - - - - - $900 - - / 3br - 1298ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Trinity Park has everything except you! - - - - - - $779 - - / 2br - 901ft² - - - - - (5301 Creek Ridge Lane) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - A Unique Two Bedroom Layout! Very Open and Inviting! - - - - - - $890 - - / 2br - 1254ft² - - - - - (Southpoint Mall Area) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Come in NOW! - - - - - - $952 - - / 2br - 1198ft² - - - - - (Wake Forest) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Multi-Million Dollar Renovations! - - - - - - $990 - - / 2br - 1291ft² - - - - - (Chapel Hill) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Get Lucky with Us! - - - - - - $840 - - / 2br - 1298ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Washer and Dryer included!! - - - - - - $703 - - / 2br - 950ft² - - - - - (Durham) - - - - - - - -

-

- - - - - - - - Jul 20 - - - - Great location, affordable rent!! - - - - - - $635 - - / 2br - 950ft² - - - - - (Durham) - - - - - - - -

-

- - - - - - - - Jul 20 - - - - Students Are Welcome!!!! - - - - - - $870 - - / 2br - 1031ft² - - - - - (Durham NC) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - three BR home - - - - - - $925 - - / 3br - 1625ft² - - - - - (Morrisville) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - 2 bedroom apt in downtown raleigh----great location - - - - - - $695 - - / 2br - 700ft² - - - - - (721 S. Bloodworth st. 27603) - - - - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Townhouse de 2 cuartos - - - - - - $925 - - / 2br - 1200ft² - - - - - (Old Wake Forest Rd. y Millbrook) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Beautiful 4 bd/2.5 bath - - - - - - $900 - - / 4br - 2000ft² - - - - - (Clayton) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - RELAX! IT'S ALL HERE... - - - - - - $850 - - / 2br - 1094ft² - - - - - (Southpoint, Durham) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - *****Upgraded 2 Bedroom Apartments starting at $799***** - - - - - - $799 - - / 2br - 11263ft² - - - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Total City Access - - - - - - $960 - - / 3br - 1185ft² - - - - - (Raleigh) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - *You should View this beautiful 3 bedroom 2.5 bathroom unit* - - - - - - $675 - - / 3br - 1350ft² - - - - - (Garner) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Save some GREEN at Ashbrook - - - - - - $805 - - / 2br - 1052ft² - - - - - (Carrboro) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - ***WELCOME HOME*** - - - - - - $904 - - / 2br - 1120ft² - - - - - (NEAR DUKE ) ) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - 2-Story Townhome for Rent (38127) - - - - - - $975 - - / 3br - 1700ft² - - - - - (Memphis ) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - 2 Minutes to 98 Bypass! Contemporary Upgraded 2BR/2BA! Only 1! - - - - - - $899 - - / 2br - - - - - (Wakefield, Open 12-4) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - In a great location close to Cary/Raleigh right off the beltline - - - - - - $795 - - / 2br - 827ft² - - - - - (Raleigh - Brook Hill) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Spacious First Floor 2 BR 2 BA Apartment with Huge Closet Space - - - - - - $884 - - / 2br - 1121ft² - - - - - (Durham/RTP area) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Make your friends jealous! - - - - - - $780 - - / 2br - 1062ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - It's All About Style & Comfort...Specials! - - - - - - $850 - - / 2br - 1028ft² - - - - - (Pine Winds Apartments- Raleigh,NC) - - - - pic img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - A Great Townhouse Floorplan! - - - - - - $830 - - / 2br - 1030ft² - - - - - (Chapel Hill - Booker Creek) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Capture the Excitement at Arbor Creek - - - - - - $675 - - / 2br - 810ft² - - - - - (Raleigh) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Get a Great Apartment FAST! Look Today, Move-in Tomorrow! - - - - - - $985 - - / 2br - 900ft² - - - - - (Carrboro, UNC, Chapel Hill) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - U Suites NCSU -- Save $710--1st month free! Plus best monthly rate! - - - - - - $530 - - / 4br - - - - - (Raleigh, NCSU) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Large 2x2 with sunroom, walkin closets, laundry room and much more! - - - - - - $985 - - / 2br - 1151ft² - - - - - (231 Calibre Chase Dr) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Catch The Bus To UNC At Your Front Door! - - - - - - $935 - - / 2br - 1000ft² - - - - - (Chapel Hill - Franklin Woods) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Awesome 2 Bedroom Townhomes!!!!!$600 off This Weekend Only*** - - - - - - $849 - - / 2br - 1163ft² - - - - - (Carrboro/Chapel Hill) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - 2 bed $750 2nd floor - - - - - - $750 - - / 2br - 1137ft² - - - - - (Durham) - - - - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Style and Convenience with Landmark - - - - - - $977 - - / 2br - 1270ft² - - - - - (Wake Forest) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - 4 BR 2 BA home for Rent $1000 - - - - - - $1000 - - / 4br - 2100ft² - - - - - (Lillington ) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - ▬▬Don't miss this opportunity! Apts available, Great prices!! - - - - - - $603 - - / 2br - 940ft² - - - - - (Pines of Ashton) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Welcome Home! - - - - - - $640 - - / 2br - 770ft² - - - - - (Durham, NC) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Pick Me, Pick Me! - - - - - - $775 - - / 2br - 1095ft² - - - - - (Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Two Bedroom Two Bath Perfect for Roommates on J Bus Line - - - - - - $979 - - / 2br - - - - - (Chapel Hill) - - - - pic img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - 2 Bedrooms/2Bathrooms near NCSU - - - - - - $695 - - / 2br - 900ft² - - - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - WOW!!!!! Call us at 919-544-1779 - - - - - - $795 - - / 2br - 945ft² - - - - - (3207 Stonesthrow Lane ) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Hurry in limited units leasing quickly - - - - - - $650 - - / 2br - 900ft² - - - - - (Royal Park) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Don't Miss July Specials! Huge 2 BR/2 BA Only $749! 7 Miles To DT! - - - - - - $749 - - / 2br - 1355ft² - - - - - (Raleigh) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Wow what a deal! - - - - - - $825 - - / 2br - 1151ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - WOWZERS!! THAT PRICE IS UNBEATABLE!! - - - - - - $677 - - / 2br - 824ft² - - - - - (Carrboro NC) - - - - pic img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Two Bedroom Two Bathroom Apartments Available - - - - - - $927 - - / 2br - 1100ft² - - - - - (Morrisville, Cary) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Move on Up with Landmark - - - - - - $952 - - / 2br - 1198ft² - - - - - (Wake Forest) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Friendly Staff!!! - - - - - - $560 - - / 2br - 770ft² - - - - - (Estes Park, Carrboro) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Find your perfect home! - - - - - - $925 - - / 2br - 1298ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Pet-Friendly Home with All the Right Amenities - - - - - - $780 - - / 2br - 1062ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Morrisville Town Home Ready in September - - - - - - $942 - - / 2br - 1100ft² - - - - - (Morrisville, Cary) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - A Must See! Fantastic Location! Beautiful Townhome - - - - - - $855 - - / 3br - 1310ft² - - - - - (Durham Hwy 54) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - RENOVATED TUB SURROUNDS, SINKS, VANITIES, & MORE! COME SEE TODAY!!!! - - - - - - $753 - - / 2br - 840ft² - - - - - (Cary, NC) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - New to Market! Stunning 2BR/2BA! Posh Wakefield Plantation! Only 1! - - - - - - $899 - - / 2br - 1242ft² - - - - - (Wakefield/North Raleigh) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - TWO BED TWO BATH for $661!! - - - - - - $661 - - / 2br - 945ft² - - - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - $799 LAST MINUTE HOUSING AVAILABLE!!!! - - - - - - $799 - - / 2br - - - - - (CHAPEL HILL/CARRBORO/UNC) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Available July 25th -Cozy 2 BR Large living area with great kitchen! - - - - - - $887 - - / 2br - 1070ft² - - - - - (Chapel Hill ) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Apartamento de Lujoso de 2 Recamaras con Vestidor y Lavadora y Secador - - - - - - $860 - - / 2br - 1120ft² - - - - - (Cary, NC) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - This Apartment is a Great Value!!! - - - - - - $695 - - / 2br - 890ft² - - - - - (Cary, NC) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Minutes From Durant Nature Park - - - - - - $915 - - / 2br - 1004ft² - - - - - (North Raleigh) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Pet-Friendly and Loving It - - - - - - $900 - - / 3br - 1298ft² - - - - - (North Raleigh) - - - - pic img - - - - -

-

- - - - - - - - Jul 20 - - - - Feel At Home At Autumn Pointe! - - - - - - $915 - - / 2br - 1004ft² - - - - - (North Raleigh) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - ~~Amazing 2 bd Townhome! W/D Conn.! Pets Welcome! Close to everything - - - - - - $855 - - / 2br - 1150ft² - - - - - (North Hills Drive) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Spacious 2BR/ Immediate Move-In - - - - - - $899 - - / 2br - 1085ft² - - - - - (Amelia Village) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - It's All About Location! - - - - - - $804 - - / 2br - 847ft² - - - - - (The Timbers) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Got Pets? We want to meet them!! - - - - - - $769 - - / 2br - - - - - (Super Pet Friendly BMW) - - - - pic img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Move in with your Sweetheart! - - - - - - $977 - - / 2br - 1270ft² - - - - - (Wake Forest) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - North Raleigh Apartment Community - - - - - - $915 - - / 2br - - - - - - - - - -

-

- - - - - - - - Jul 20 - - - - State/Gov't Workers take 3% off per month!! - - - - - - $939 - - / 2br - - - - - (Amelia Village) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Spacious 2 Bedroom Apartment homes - - - - - - $690 - - / 2br - 800ft² - - - - - (Kingswood, Chapel Hill (919) 967-2231) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - UPGRADED TOWNHOMES!!!! $849..THIS WEEKEND ONLY!!! - - - - - - $799 - - / 2br - - - - - (CHAPEL Hill/Carrboro/UNC) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Move In Ready 2 Bedrooms - - - - - - $899 - - / 2br - 1085ft² - - - - - (Amelia Village) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Don't miss out on this greal deal!!! - - - - - - $889 - - / 2br - 1075ft² - - - - - (Raleigh, NC) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Great Clayton Location - - - - - - $899 - - / 2br - - - - - (Amelia Village) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Pet Friendly!! - - - - - - $939 - - / 2br - - - - - (Amelia Village) - - - - pic - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Stop in today and take a tour - - - - - - $795 - - / 2br - 827ft² - - - - - (Raleigh - Brook Hill) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Townhouse on Guess Road--2br/1.5bath - - - - - - $825 - - / 2br - - - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - HUGE Walk In Closet!!! - - - - - - $889 - - / 2br - 1075ft² - - - - - (Raleigh, NC) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Call today before they are all gone!!!! - - - - - - $889 - - / 2br - 1075ft² - - - - - (Raleigh, NC) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - AWESOME 2BR 2.5 BATHS CONDO $975.00 - - - - - - $975 - - / 2br - - - - - (RALEIGH) - - - - pic - - - - -

-

- - - - - - - - Jul 20 - - - - Summer Savings!!! - - - - - - $795 - - / 2br - 945ft² - - - - - (Durham) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Duplex Apartment - - - - - - $695 - - / 2br - 950ft² - - - - - (North Raleigh) - - - - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Start Yor Day With Northwoods Townhomes - - - - - - $995 - - / 2br - - - - - (Cary, Chapel Hill) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Chambers Ridge - A Great Place for Roommates! - - - - - - $985 - - / 2br - 900ft² - - - - - (Carrboro, UNC, Chapel Hill) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - July Special! Only $749! Massive 2 BR/2 BA Includes W/D! Last One! - - - - - - $749 - - / 2br - 1355ft² - - - - - (Raleigh) - - - - img - - - - -

-

- - - - - - - - Jul 20 - - - - Lovely 2BR 1BA Traditional Apartment For Only $809! 2 mi from UNC! - - - - - - $809 - - / 2br - 718ft² - - - - - (Chapel Hill) - - - - img - - map - - - - - -

-

- - - - - - - - Jul 20 - - - - Hang The Home Sweet Home Sign At Balfour West Townhomes! - - - - - - $915 - - / 2br - 1162ft² - - - - - (Durham, NC) - - - - img - - map - - - - - -

-

- - |< - - first - - -
-
- - < - - prev - - - - - - next - - > - - - - 1 - 100 - -
-
-

-
- - -
-
-
-
    -
  • - - FORMAT: - -
  • -
  • - mobile -
  • -
  • - regular -
  • -
- -
- - - - - - - - - - - - - - diff --git a/resources/session03/http_server.py b/resources/session03/http_server.py new file mode 100644 index 00000000..e42ea086 --- /dev/null +++ b/resources/session03/http_server.py @@ -0,0 +1,112 @@ +import mimetypes +import pathlib +import socket +import sys + + +def response_ok(body=b"this is a pretty minimal response", mimetype=b"text/plain"): + """returns a basic HTTP response as bytes""" + resp = [] + resp.append(b"HTTP/1.1 200 OK") + resp.append(b"Content-Type: " + mimetype) + resp.append(b"") + resp.append(body) + return b"\r\n".join(resp) + + +def response_method_not_allowed(): + """returns a 405 Method Not Allowed response as bytes""" + resp = [] + resp.append("HTTP/1.1 405 Method Not Allowed") + resp.append("") + return "\r\n".join(resp).encode('utf8') + + +def response_not_found(): + """returns a 404 Not Found response as bytes""" + resp = [] + resp.append("HTTP/1.1 404 Not Found") + resp.append("") + return "\r\n".join(resp).encode('utf8') + + +def parse_request(request): + first_line = request.split("\r\n", 1)[0] + method, uri, protocol = first_line.split() + if method != "GET": + raise NotImplementedError("We only accept GET") + return uri + + +def resolve_uri(uri): + """This method should return appropriate content and a mime type + + Both content and mime type should be expressed as bytes + """ + root_path = pathlib.Path('./webroot') + resource_path = root_path / uri.lstrip('/') + if resource_path.is_dir(): + # the resource is a directory, content type is text/html, produce a + # listing of the directory contents + item_template = '* {}' + listing = [] + for item_path in resource_path.iterdir(): + listing.append(item_template.format(str(item_path))) + content = "\n".join(listing).encode('utf8') + mime_type = 'text/plain'.encode('utf8') + elif resource_path.is_file(): + # the resource is a file, figure out its mime type and read + content = resource_path.read_bytes() + mime_type = mimetypes.guess_type(str(resource_path))[0].encode('utf8') + else: + raise NameError() + return content, mime_type + + + +def server(log_buffer=sys.stderr): + address = ('127.0.0.1', 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + print("making a server on {0}:{1}".format(*address), file=log_buffer) + sock.bind(address) + sock.listen(1) + + try: + while True: + print('waiting for a connection', file=log_buffer) + conn, addr = sock.accept() # blocking + try: + print('connection - {0}:{1}'.format(*addr), file=log_buffer) + request = '' + while True: + data = conn.recv(1024) + request += data.decode('utf8') + if len(data) < 1024: + break + + try: + uri = parse_request(request) + except NotImplementedError: + response = response_method_not_allowed() + else: + try: + content, mime_type = resolve_uri(uri) + except NameError: + response = response_not_found() + else: + response = response_ok(content, mime_type) + + print('sending response', file=log_buffer) + conn.sendall(response) + finally: + conn.close() + + except KeyboardInterrupt: + sock.close() + return + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/resources/session03/mashup.py b/resources/session03/mashup.py deleted file mode 100644 index 8d124a2e..00000000 --- a/resources/session03/mashup.py +++ /dev/null @@ -1,96 +0,0 @@ -import requests -from bs4 import BeautifulSoup -import pprint -import json - - -def fetch_search_results(**kwargs): - base = 'http://raleigh.craigslist.org/search/apa' - valid_kws = ('query', 'minAsk', 'maxAsk', 'bedrooms') - use_kwargs = dict( - [(key, val) for key, val in kwargs.items() if key in valid_kws]) - if not use_kwargs: - raise ValueError("No valid keywords") - - resp = requests.get(base, params=use_kwargs, timeout=3) - resp.raise_for_status() - return resp.content, resp.apparent_encoding - - -def read_results(filename): - fh = open(filename, 'r') - body = fh.read() - fh.close() - return body - - -def parse_source(html, encoding='utf-8'): - parsed = BeautifulSoup(html, from_encoding=encoding) - return parsed - - -def extract_listings(doc): - location_attrs = {'data-latitude': True, - 'data-longitude': True} - for row in doc.find_all('p', class_='row', - attrs=location_attrs): - location = dict( - [(key, row.attrs.get(key)) for key in location_attrs]) - link = row.find('span', class_='pl').find('a') - price_span = row.find('span', class_='price') - listing = { - 'location': location, - 'href': link.attrs['href'], - 'description': link.string.strip(), - 'price': price_span.string.strip(), - 'size': price_span.next_sibling.strip(' \n-/') - } - yield listing - - -def add_address(listing): - api_url = 'http://maps.googleapis.com/maps/api/geocode/json' - loc = listing['location'] - parameters = { - 'sensor': 'false', - 'latlng': "%s,%s" % (loc['data-latitude'], - loc['data-longitude']) - } - resp = requests.get(api_url, params=parameters) - data = json.loads(resp.text) - if data['status'] == 'OK': - best = data['results'][0] - listing['address'] = best['formatted_address'] - else: - listing['address'] = 'unavailable' - return listing - - -def add_walkscore(listing): - api_url = 'http://api.walkscore.com/score' - apikey = 'YOURAPIKEYGOESHERE' - loc = listing['location'] - if listing['address'] == 'unavailable': - return listing - parameters = { - 'lat': loc['data-latitude'], 'lon': loc['data-longitude'], - 'address': listing['address'], 'wsapikey': apikey, - 'format': 'json' - } - resp = requests.get(api_url, params=parameters) - data = json.loads(resp.text) - if data['status'] == 1: - listing['ws_description'] = data['description'] - listing['ws_score'] = data['walkscore'] - listing['ws_link'] = data['ws_link'] - return listing - - -if __name__ == '__main__': - params = {'minAsk': 500, 'maxAsk': 1000, 'bedrooms': 2} - html, encoding = fetch_search_results(**params) - doc = parse_source(html, encoding) - for listing in extract_listings(doc): - listing = add_address(listing) - listing = add_walkscore(listing) - pprint.pprint(listing) diff --git a/resources/session04/wsgi/bookapp.py b/resources/session03/wsgi/bookapp.py similarity index 81% rename from resources/session04/wsgi/bookapp.py rename to resources/session03/wsgi/bookapp.py index 809d0a0a..d2284c6f 100644 --- a/resources/session04/wsgi/bookapp.py +++ b/resources/session03/wsgi/bookapp.py @@ -13,11 +13,11 @@ def books(): return "

a list of books

" -def application(environ, start_response): +def application(environ, start_response): status = "200 OK" headers = [('Content-type', 'text/html')] start_response(status, headers) - return ["

No Progress Yet

", ] + return ["

No Progress Yet

".encode('utf8')] if __name__ == '__main__': diff --git a/resources/session03/wsgi/bookdb.py b/resources/session03/wsgi/bookdb.py new file mode 100644 index 00000000..f3a72414 --- /dev/null +++ b/resources/session03/wsgi/bookdb.py @@ -0,0 +1,45 @@ + +class BookDB(): + def titles(self): + titles = [ + dict(id=id, title=database[id]['title']) for id in database.keys() + ] + return titles + + def title_info(self, id): + return database.get(id, None) + + +# let's pretend we're getting this information from a database somewhere +database = { + 'id1': { + 'title': 'CherryPy Essentials: Rapid Python Web Application Development', + 'isbn': '978-1904811848', + 'publisher': 'Packt Publishing (March 31, 2007)', + 'author': 'Sylvain Hellegouarch', + }, + 'id2': { + 'title': 'Python for Software Design: How to Think Like a Computer Scientist', + 'isbn': '978-0521725965', + 'publisher': 'Cambridge University Press; 1 edition (March 16, 2009)', + 'author': 'Allen B. Downey', + }, + 'id3': { + 'title': 'Foundations of Python Network Programming', + 'isbn': '978-1430230038', + 'publisher': 'Apress; 2 edition (December 21, 2010)', + 'author': 'John Goerzen', + }, + 'id4': { + 'title': 'Python Cookbook, Second Edition', + 'isbn': '978-0-596-00797-3', + 'publisher': 'O''Reilly Media', + 'author': 'Alex Martelli, Anna Ravenscroft, David Ascher', + }, + 'id5': { + 'title': 'The Pragmatic Programmer: From Journeyman to Master', + 'isbn': '978-0201616224', + 'publisher': 'Addison-Wesley Professional (October 30, 1999)', + 'author': 'Andrew Hunt, David Thomas', + }, +} diff --git a/resources/session03/wsgi/tests.py b/resources/session03/wsgi/tests.py new file mode 100644 index 00000000..b92bc78d --- /dev/null +++ b/resources/session03/wsgi/tests.py @@ -0,0 +1,128 @@ +import unittest + + +class BookAppTestCase(unittest.TestCase): + """shared functionality""" + + def setUp(self): + from bookdb import database + self.db = database + + +class BookDBTestCase(BookAppTestCase): + """tests for the bookdb code""" + + def makeOne(self): + from bookdb import BookDB + return BookDB() + + def test_all_titles_returned(self): + actual_titles = self.makeOne().titles() + self.assertEqual(len(actual_titles), len(self.db)) + + def test_all_titles_correct(self): + actual_titles = self.makeOne().titles() + for actual_title in actual_titles: + self.assertTrue(actual_title['id'] in self.db) + actual = actual_title['title'] + expected = self.db[actual_title['id']]['title'] + self.assertEqual(actual, expected) + + def test_title_info_complete(self): + use_id, expected = self.db.items()[0] + actual = self.makeOne().title_info(use_id) + # demonstrate all actual keys are expected + for key in actual: + self.assertTrue(key in expected) + # demonstrate all expected keys are present in actual + for key in expected: + self.assertTrue(key in actual) + + def test_title_info_correct(self): + for book_id, expected in self.db.items(): + actual = self.makeOne().title_info(book_id) + self.assertEqual(actual, expected) + + +class ResolvePathTestCase(BookAppTestCase): + """tests for the resolve_path function""" + + def call_function_under_test(self, path): + from bookapp import resolve_path + return resolve_path(path) + + def test_root_returns_books_function(self): + """verify that the correct function is returned by the root path""" + from bookapp import books as expected + path = '/' + actual, args = self.call_function_under_test(path) + self.assertTrue(actual is expected) + + def test_root_returns_no_args(self): + """verify that no args are returned for the root path""" + path = '/' + func, actual = self.call_function_under_test(path) + self.assertTrue(not actual) + + def test_book_path_returns_book_function(self): + from bookapp import book as expected + book_id = self.db.keys()[0] + path = '/book/{0}'.format(book_id) + actual, args = self.call_function_under_test(path) + self.assertTrue(actual is expected) + + def test_book_path_returns_bookid_in_args(self): + expected = self.db.keys()[0] + path = '/book/{0}'.format(expected) + func, actual = self.call_function_under_test(path) + self.assertTrue(expected in actual) + + def test_bad_path_raises_name_error(self): + path = '/not/valid/path' + self.assertRaises(NameError, self.call_function_under_test, path) + + +class BooksTestCase(BookAppTestCase): + """tests for the books function""" + + def call_function_under_test(self): + from bookapp import books + return books() + + def test_all_book_titles_in_result(self): + actual = self.call_function_under_test() + for book_id, info in self.db.items(): + expected = info['title'] + self.assertTrue(expected in actual) + + def test_all_book_ids_in_result(self): + actual = self.call_function_under_test() + for expected in self.db: + self.assertTrue(expected in actual) + + +class BookTestCase(BookAppTestCase): + """tests for the book function""" + + def call_function_under_test(self, id): + from bookapp import book + return book(id) + + def test_all_ids_have_results(self): + for book_id in self.db: + actual = self.call_function_under_test(book_id) + self.assertTrue(actual) + + def test_id_returns_correct_results(self): + for book_id, book_info in self.db.items(): + actual = self.call_function_under_test(book_id) + for expected in book_info.values(): + self.assertTrue(expected in actual) + + def test_bad_id_raises_name_error(self): + bad_id = "sponge" + self.assertRaises(NameError, self.call_function_under_test, bad_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/session03/wsgi/wsgi_1.py b/resources/session03/wsgi/wsgi_1.py new file mode 100644 index 00000000..85498d13 --- /dev/null +++ b/resources/session03/wsgi/wsgi_1.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +import datetime + +default = "No Value Set" + +body = """ + +Lab 3 - WSGI experiments + + +

Hey there, this page has been generated by {software}, running at {path}

+

Today is {month} {date}, {year}.

+

This page was requested by IP Address {client_ip}

+ +""" + + +def application(environ, start_response): + import pprint + pprint.pprint(environ) + + response_body = body.format( + software=environ.get('SERVER_SOFTWARE', default), + path="aaaa", + month="bbbb", + date="cccc", + year="dddd", + client_ip="eeee" + ) + status = '200 OK' + + response_headers = [('Content-Type', 'text/html'), + ('Content-Length', str(len(response_body)))] + start_response(status, response_headers) + + return [response_body.encode('utf8')] + + +if __name__ == '__main__': + from wsgiref.simple_server import make_server + srv = make_server('localhost', 8080, application) + srv.serve_forever() diff --git a/resources/session04/cgi/cgi-bin/cgi_2.py b/resources/session04/cgi/cgi-bin/cgi_2.py deleted file mode 100755 index eb6f2702..00000000 --- a/resources/session04/cgi/cgi-bin/cgi_2.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python -import cgi -import cgitb -cgitb.enable() -import os -import datetime - - -default = "No Value Present" - - -print "Content-Type: text/html" -print - -body = """ - -Lab 1 - CGI experiments - - -The server name is %s. (if an IP address, then a DNS problem)
-
-The server address is %s:%s.
-
-Your hostname is %s.
-
-You are coming from %s:%s.
-
-The currenly executing script is %s
-
-The request arrived at %s
- - -""" % ( - os.environ.get('SERVER_NAME', default), # Server Hostname - 'aaaa', # server IP - 'bbbb', # server port - 'cccc', # client hostname - 'dddd', # client IP - 'eeee', # client port - 'ffff', # this script name - 'gggg', # time - ) - -print body, diff --git a/resources/session04/cgi/cgi-bin/cgi_sums.py b/resources/session04/cgi/cgi-bin/cgi_sums.py deleted file mode 100755 index 524ca256..00000000 --- a/resources/session04/cgi/cgi-bin/cgi_sums.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/python -import cgi -import cgitb -import json - -cgitb.enable() - -print "Content-type: text/plain" -print -print "Your job is to make this work" \ No newline at end of file diff --git a/resources/session04/mashup_1.py b/resources/session04/mashup_1.py new file mode 100644 index 00000000..0880e87c --- /dev/null +++ b/resources/session04/mashup_1.py @@ -0,0 +1,51 @@ +from bs4 import BeautifulSoup +import requests + + +INSPECTION_DOMAIN = 'http://info.kingcounty.gov' +INSPECTION_PATH = '/health/ehs/foodsafety/inspections/Results.aspx' +INSPECTION_PARAMS = { + 'Output': 'W', + 'Business_Name': '', + 'Business_Address': '', + 'Longitude': '', + 'Latitude': '', + 'City': '', + 'Zip_Code': '', + 'Inspection_Type': 'All', + 'Inspection_Start': '', + 'Inspection_End': '', + 'Inspection_Closed_Business': 'A', + 'Violation_Points': '', + 'Violation_Red_Points': '', + 'Violation_Descr': '', + 'Fuzzy_Search': 'N', + 'Sort': 'H' +} + + +def get_inspection_page(**kwargs): + url = INSPECTION_DOMAIN + INSPECTION_PATH + params = INSPECTION_PARAMS.copy() + for key, val in kwargs.items(): + if key in INSPECTION_PARAMS: + params[key] = val + resp = requests.get(url, params=params) + resp.raise_for_status() + return resp.text + + +def parse_source(html): + parsed = BeautifulSoup(html) + return parsed + + +if __name__ == '__main__': + use_params = { + 'Inspection_Start': '2/1/2014', + 'Inspection_End': '2/1/2016', + 'Zip_Code': '98101' + } + html = get_inspection_page(**use_params) + parsed = parse_source(html) + print(parsed.prettify()) diff --git a/resources/session04/mashup_2.py b/resources/session04/mashup_2.py new file mode 100644 index 00000000..6d4778c9 --- /dev/null +++ b/resources/session04/mashup_2.py @@ -0,0 +1,66 @@ +from bs4 import BeautifulSoup +import pathlib +import re +import requests + + +INSPECTION_DOMAIN = 'http://info.kingcounty.gov' +INSPECTION_PATH = '/health/ehs/foodsafety/inspections/Results.aspx' +INSPECTION_PARAMS = { + 'Output': 'W', + 'Business_Name': '', + 'Business_Address': '', + 'Longitude': '', + 'Latitude': '', + 'City': '', + 'Zip_Code': '', + 'Inspection_Type': 'All', + 'Inspection_Start': '', + 'Inspection_End': '', + 'Inspection_Closed_Business': 'A', + 'Violation_Points': '', + 'Violation_Red_Points': '', + 'Violation_Descr': '', + 'Fuzzy_Search': 'N', + 'Sort': 'H' +} + + +def get_inspection_page(**kwargs): + url = INSPECTION_DOMAIN + INSPECTION_PATH + params = INSPECTION_PARAMS.copy() + for key, val in kwargs.items(): + if key in INSPECTION_PARAMS: + params[key] = val + resp = requests.get(url, params=params) + resp.raise_for_status() + return resp.text + + +def parse_source(html): + parsed = BeautifulSoup(html) + return parsed + + +def load_inspection_page(name): + file_path = pathlib.Path(name) + return file_path.read_text(encoding='utf8') + + +def restaurant_data_generator(html): + id_finder = re.compile(r'PR[\d]+~') + return html.find_all('div', id=id_finder) + + +if __name__ == '__main__': + use_params = { + 'Inspection_Start': '2/1/2013', + 'Inspection_End': '2/1/2015', + 'Zip_Code': '98101' + } + # html = get_inspection_page(**use_params) + html = load_inspection_page('inspection_page.html') + parsed = parse_source(html) + content_col = parsed.find("td", id="contentcol") + data_list = restaurant_data_generator(content_col) + print(data_list[0].prettify()) diff --git a/resources/session04/mashup_3.py b/resources/session04/mashup_3.py new file mode 100644 index 00000000..79e46632 --- /dev/null +++ b/resources/session04/mashup_3.py @@ -0,0 +1,93 @@ +from bs4 import BeautifulSoup +import pathlib +import re +import requests + + +INSPECTION_DOMAIN = 'http://info.kingcounty.gov' +INSPECTION_PATH = '/health/ehs/foodsafety/inspections/Results.aspx' +INSPECTION_PARAMS = { + 'Output': 'W', + 'Business_Name': '', + 'Business_Address': '', + 'Longitude': '', + 'Latitude': '', + 'City': '', + 'Zip_Code': '', + 'Inspection_Type': 'All', + 'Inspection_Start': '', + 'Inspection_End': '', + 'Inspection_Closed_Business': 'A', + 'Violation_Points': '', + 'Violation_Red_Points': '', + 'Violation_Descr': '', + 'Fuzzy_Search': 'N', + 'Sort': 'H' +} + + +def get_inspection_page(**kwargs): + url = INSPECTION_DOMAIN + INSPECTION_PATH + params = INSPECTION_PARAMS.copy() + for key, val in kwargs.items(): + if key in INSPECTION_PARAMS: + params[key] = val + resp = requests.get(url, params=params) + resp.raise_for_status() + return resp.text + + +def parse_source(html): + parsed = BeautifulSoup(html) + return parsed + + +def load_inspection_page(name): + file_path = pathlib.Path(name) + return file_path.read_text(encoding='utf8') + + +def restaurant_data_generator(html): + id_finder = re.compile(r'PR[\d]+~') + return html.find_all('div', id=id_finder) + + +def has_two_tds(elem): + is_tr = elem.name == 'tr' + td_children = elem.find_all('td', recursive=False) + has_two = len(td_children) == 2 + return is_tr and has_two + + +def clean_data(td): + return td.text.strip(" \n:-") + + +def extract_restaurant_metadata(elem): + restaurant_data_rows = elem.find('tbody').find_all( + has_two_tds, recursive=False + ) + rdata = {} + current_label = '' + for data_row in restaurant_data_rows: + key_cell, val_cell = data_row.find_all('td', recursive=False) + new_label = clean_data(key_cell) + current_label = new_label if new_label else current_label + rdata.setdefault(current_label, []).append(clean_data(val_cell)) + return rdata + + +if __name__ == '__main__': + use_params = { + 'Inspection_Start': '2/1/2013', + 'Inspection_End': '2/1/2015', + 'Zip_Code': '98101' + } + # html = get_inspection_page(**use_params) + html = load_inspection_page('inspection_page.html') + parsed = parse_source(html) + content_col = parsed.find("td", id="contentcol") + data_list = restaurant_data_generator(content_col) + for data_div in data_list: + metadata = extract_restaurant_metadata(data_div) + print(metadata) diff --git a/resources/session04/mashup_4.py b/resources/session04/mashup_4.py new file mode 100644 index 00000000..ab5ebdd5 --- /dev/null +++ b/resources/session04/mashup_4.py @@ -0,0 +1,133 @@ +from bs4 import BeautifulSoup +import pathlib +import re +import requests + + +INSPECTION_DOMAIN = 'http://info.kingcounty.gov' +INSPECTION_PATH = '/health/ehs/foodsafety/inspections/Results.aspx' +INSPECTION_PARAMS = { + 'Output': 'W', + 'Business_Name': '', + 'Business_Address': '', + 'Longitude': '', + 'Latitude': '', + 'City': '', + 'Zip_Code': '', + 'Inspection_Type': 'All', + 'Inspection_Start': '', + 'Inspection_End': '', + 'Inspection_Closed_Business': 'A', + 'Violation_Points': '', + 'Violation_Red_Points': '', + 'Violation_Descr': '', + 'Fuzzy_Search': 'N', + 'Sort': 'H' +} + + +def get_inspection_page(**kwargs): + url = INSPECTION_DOMAIN + INSPECTION_PATH + params = INSPECTION_PARAMS.copy() + for key, val in kwargs.items(): + if key in INSPECTION_PARAMS: + params[key] = val + resp = requests.get(url, params=params) + resp.raise_for_status() + return resp.text + + +def parse_source(html): + parsed = BeautifulSoup(html) + return parsed + + +def load_inspection_page(name): + file_path = pathlib.Path(name) + return file_path.read_text(encoding='utf8') + + +def restaurant_data_generator(html): + id_finder = re.compile(r'PR[\d]+~') + return html.find_all('div', id=id_finder) + + +def has_two_tds(elem): + is_tr = elem.name == 'tr' + td_children = elem.find_all('td', recursive=False) + has_two = len(td_children) == 2 + return is_tr and has_two + + +def clean_data(td): + return td.text.strip(" \n:-") + + +def extract_restaurant_metadata(elem): + restaurant_data_rows = elem.find('tbody').find_all( + has_two_tds, recursive=False + ) + rdata = {} + current_label = '' + for data_row in restaurant_data_rows: + key_cell, val_cell = data_row.find_all('td', recursive=False) + new_label = clean_data(key_cell) + current_label = new_label if new_label else current_label + rdata.setdefault(current_label, []).append(clean_data(val_cell)) + return rdata + + +def is_inspection_data_row(elem): + is_tr = elem.name == 'tr' + if not is_tr: + return False + td_children = elem.find_all('td', recursive=False) + has_four = len(td_children) == 4 + this_text = clean_data(td_children[0]).lower() + contains_word = 'inspection' in this_text + does_not_start = not this_text.startswith('inspection') + return is_tr and has_four and contains_word and does_not_start + + +def get_score_data(elem): + inspection_rows = elem.find_all(is_inspection_data_row) + samples = len(inspection_rows) + total = 0 + high_score = 0 + average = 0 + for row in inspection_rows: + strval = clean_data(row.find_all('td')[2]) + try: + intval = int(strval) + except (ValueError, TypeError): + samples -= 1 + else: + total += intval + high_score = intval if intval > high_score else high_score + + if samples: + average = total/float(samples) + data = { + 'Average Score': average, + 'High Score': high_score, + 'Total Inspections': samples + } + return data + + +if __name__ == '__main__': + use_params = { + 'Inspection_Start': '2/1/2013', + 'Inspection_End': '2/1/2015', + 'Zip_Code': '98101' + } + # html = get_inspection_page(**use_params) + html = load_inspection_page('inspection_page.html') + parsed = parse_source(html) + content_col = parsed.find("td", id="contentcol") + data_list = restaurant_data_generator(content_col) + for data_div in data_list: + metadata = extract_restaurant_metadata(data_div) + inspection_data = get_score_data(data_div) + metadata.update(inspection_data) + print(metadata) diff --git a/resources/session04/mashup_5.py b/resources/session04/mashup_5.py new file mode 100644 index 00000000..20f62b49 --- /dev/null +++ b/resources/session04/mashup_5.py @@ -0,0 +1,164 @@ +from bs4 import BeautifulSoup +import geocoder +import json +import pathlib +import re +import requests + + +INSPECTION_DOMAIN = 'http://info.kingcounty.gov' +INSPECTION_PATH = '/health/ehs/foodsafety/inspections/Results.aspx' +INSPECTION_PARAMS = { + 'Output': 'W', + 'Business_Name': '', + 'Business_Address': '', + 'Longitude': '', + 'Latitude': '', + 'City': '', + 'Zip_Code': '', + 'Inspection_Type': 'All', + 'Inspection_Start': '', + 'Inspection_End': '', + 'Inspection_Closed_Business': 'A', + 'Violation_Points': '', + 'Violation_Red_Points': '', + 'Violation_Descr': '', + 'Fuzzy_Search': 'N', + 'Sort': 'H' +} + + +def get_inspection_page(**kwargs): + url = INSPECTION_DOMAIN + INSPECTION_PATH + params = INSPECTION_PARAMS.copy() + for key, val in kwargs.items(): + if key in INSPECTION_PARAMS: + params[key] = val + resp = requests.get(url, params=params) + resp.raise_for_status() + return resp.text + + +def parse_source(html): + parsed = BeautifulSoup(html) + return parsed + + +def load_inspection_page(name): + file_path = pathlib.Path(name) + return file_path.read_text(encoding='utf8') + + +def restaurant_data_generator(html): + id_finder = re.compile(r'PR[\d]+~') + return html.find_all('div', id=id_finder) + + +def has_two_tds(elem): + is_tr = elem.name == 'tr' + td_children = elem.find_all('td', recursive=False) + has_two = len(td_children) == 2 + return is_tr and has_two + + +def clean_data(td): + return td.text.strip(" \n:-") + + +def extract_restaurant_metadata(elem): + restaurant_data_rows = elem.find('tbody').find_all( + has_two_tds, recursive=False + ) + rdata = {} + current_label = '' + for data_row in restaurant_data_rows: + key_cell, val_cell = data_row.find_all('td', recursive=False) + new_label = clean_data(key_cell) + current_label = new_label if new_label else current_label + rdata.setdefault(current_label, []).append(clean_data(val_cell)) + return rdata + + +def is_inspection_data_row(elem): + is_tr = elem.name == 'tr' + if not is_tr: + return False + td_children = elem.find_all('td', recursive=False) + has_four = len(td_children) == 4 + this_text = clean_data(td_children[0]).lower() + contains_word = 'inspection' in this_text + does_not_start = not this_text.startswith('inspection') + return is_tr and has_four and contains_word and does_not_start + + +def get_score_data(elem): + inspection_rows = elem.find_all(is_inspection_data_row) + samples = len(inspection_rows) + total = 0 + high_score = 0 + average = 0 + for row in inspection_rows: + strval = clean_data(row.find_all('td')[2]) + try: + intval = int(strval) + except (ValueError, TypeError): + samples -= 1 + else: + total += intval + high_score = intval if intval > high_score else high_score + + if samples: + average = total/float(samples) + data = { + u'Average Score': average, + u'High Score': high_score, + u'Total Inspections': samples + } + return data + + +def result_generator(count): + use_params = { + 'Inspection_Start': '2/1/2013', + 'Inspection_End': '2/1/2015', + 'Zip_Code': '98101' + } + # html = get_inspection_page(**use_params) + html = load_inspection_page('inspection_page.html') + parsed = parse_source(html) + content_col = parsed.find("td", id="contentcol") + data_list = restaurant_data_generator(content_col) + for data_div in data_list[:count]: + metadata = extract_restaurant_metadata(data_div) + inspection_data = get_score_data(data_div) + metadata.update(inspection_data) + yield metadata + + +def get_geojson(result): + address = " ".join(result.get('Address', '')) + if not address: + return None + geocoded = geocoder.google(address) + geojson = geocoded.geojson + inspection_data = {} + use_keys = ( + 'Business Name', 'Average Score', 'Total Inspections', 'High Score' + ) + for key, val in result.items(): + if key not in use_keys: + continue + if isinstance(val, list): + val = " ".join(val) + inspection_data[key] = val + geojson['properties'] = inspection_data + return geojson + + +if __name__ == '__main__': + total_result = {'type': 'FeatureCollection', 'features': []} + for result in result_generator(10): + geojson = get_geojson(result) + total_result['features'].append(geojson) + with open('my_map.json', 'w') as fh: + json.dump(total_result, fh) diff --git a/resources/session04/wsgi/bookapp_2.py b/resources/session04/wsgi/bookapp_2.py deleted file mode 100644 index d1a4cdfe..00000000 --- a/resources/session04/wsgi/bookapp_2.py +++ /dev/null @@ -1,54 +0,0 @@ -import re - -from bookdb import BookDB - -DB = BookDB() - - -def book(book_id): - return "

a book with id %s

" % book_id - - -def books(): - return "

a list of books

" - - -def resolve_path(path): - urls = [(r'^$', books), - (r'^book/(id[\d]+)$', book)] - matchpath = path.lstrip('/') - for regexp, func in urls: - match = re.match(regexp, matchpath) - if match is None: - continue - args = match.groups([]) - return func, args - # we get here if no url matches - raise NameError - - -def application(environ, start_response): - headers = [("Content-type", "text/html")] - try: - path = environ.get('PATH_INFO', None) - if path is None: - raise NameError - func, args = resolve_path(path) - body = func(*args) - status = "200 OK" - except NameError: - status = "404 Not Found" - body = "

Not Found

" - except Exception: - status = "500 Internal Server Error" - body = "

Internal Server Error

" - finally: - headers.append(('Content-length', str(len(body)))) - start_response(status, headers) - return [body] - - -if __name__ == '__main__': - from wsgiref.simple_server import make_server - srv = make_server('localhost', 8080, application) - srv.serve_forever() diff --git a/resources/session04/wsgi/bookapp_3.py b/resources/session04/wsgi/bookapp_3.py deleted file mode 100644 index 39a4eb33..00000000 --- a/resources/session04/wsgi/bookapp_3.py +++ /dev/null @@ -1,72 +0,0 @@ -import re - -from bookdb import BookDB - -DB = BookDB() - - -def book(book_id): - page = """ -

%(title)s

- - - - -
Author%(author)s
Publisher%(publisher)s
ISBN%(isbn)s
-Back to the list -""" - book = DB.title_info(book_id) - if book is None: - raise NameError - return page % book - - -def books(): - all_books = DB.titles() - body = ['

My Bookshelf

', '
    '] - item_template = '
  • %(title)s
  • ' - for book in all_books: - body.append(item_template % book) - body.append('
') - return '\n'.join(body) - - -def resolve_path(path): - urls = [(r'^$', books), - (r'^book/(id[\d]+)$', book)] - matchpath = path.lstrip('/') - for regexp, func in urls: - match = re.match(regexp, matchpath) - if match is None: - continue - args = match.groups([]) - return func, args - # we get here if no url matches - raise NameError - - -def application(environ, start_response): - headers = [("Content-type", "text/html")] - try: - path = environ.get('PATH_INFO', None) - if path is None: - raise NameError - func, args = resolve_path(path) - body = func(*args) - status = "200 OK" - except NameError: - status = "404 Not Found" - body = "

Not Found

" - except Exception: - status = "500 Internal Server Error" - body = "

Internal Server Error

" - finally: - headers.append(('Content-length', str(len(body)))) - start_response(status, headers) - return [body] - - -if __name__ == '__main__': - from wsgiref.simple_server import make_server - srv = make_server('localhost', 8080, application) - srv.serve_forever() diff --git a/resources/session04/wsgi/bookdb.py b/resources/session04/wsgi/bookdb.py deleted file mode 100644 index 621387e8..00000000 --- a/resources/session04/wsgi/bookdb.py +++ /dev/null @@ -1,38 +0,0 @@ - -class BookDB(): - def titles(self): - titles = [dict(id=id, title=database[id]['title']) for id in database.keys()] - return titles - - def title_info(self, id): - return database.get(id, None) - - -# let's pretend we're getting this information from a database somewhere -database = { - 'id1' : {'title' : 'CherryPy Essentials: Rapid Python Web Application Development', - 'isbn' : '978-1904811848', - 'publisher' : 'Packt Publishing (March 31, 2007)', - 'author' : 'Sylvain Hellegouarch', - }, - 'id2' : {'title' : 'Python for Software Design: How to Think Like a Computer Scientist', - 'isbn' : '978-0521725965', - 'publisher' : 'Cambridge University Press; 1 edition (March 16, 2009)', - 'author' : 'Allen B. Downey', - }, - 'id3' : {'title' : 'Foundations of Python Network Programming', - 'isbn' : '978-1430230038', - 'publisher' : 'Apress; 2 edition (December 21, 2010)', - 'author' : 'John Goerzen', - }, - 'id4' : {'title' : 'Python Cookbook, Second Edition', - 'isbn' : '978-0-596-00797-3', - 'publisher' : 'O''Reilly Media', - 'author' : 'Alex Martelli, Anna Ravenscroft, David Ascher', - }, - 'id5' : {'title' : 'The Pragmatic Programmer: From Journeyman to Master', - 'isbn' : '978-0201616224', - 'publisher' : 'Addison-Wesley Professional (October 30, 1999)', - 'author' : 'Andrew Hunt, David Thomas', - }, -} diff --git a/resources/session04/wsgi/wsgi_1.py b/resources/session04/wsgi/wsgi_1.py deleted file mode 100755 index a2849c4a..00000000 --- a/resources/session04/wsgi/wsgi_1.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/python -import datetime - -body = """ - -Lab 2 - WSGI experiments - - - -The server name is %s. (if an IP address, then a DNS problem)
-
-The server address is %s:%s.
-
-You are coming from %s (%s).
-
-The URI we are serving is %s.
-
-The request arrived at %s
- - -""" - -def application(environ, start_response): - import pprint - pprint.pprint(environ) - response_body = body % ( - environ.get('SERVER_NAME', 'Unset'), # server name - 'aaaa', # server IP - 'bbbb', # server port - 'cccc', # client IP - 'dddd', # client host - 'eeee', # the URI path - 'ffff', # time - ) - status = '200 OK' - - response_headers = [('Content-Type', 'text/html'), - ('Content-Length', str(len(response_body)))] - start_response(status, response_headers) - - return [response_body] - - -if __name__ == '__main__': - from wsgiref.simple_server import make_server - srv = make_server('localhost', 8080, application) - srv.serve_forever() diff --git a/resources/session05/sql/createdb.py b/resources/session05/sql/createdb.py deleted file mode 100644 index 429bbdbf..00000000 --- a/resources/session05/sql/createdb.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -import sqlite3 - -DB_FILENAME = 'books.db' -DB_IS_NEW = not os.path.exists(DB_FILENAME) - -def main(): - pass - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/resources/session05/sql/ddl.sql b/resources/session05/sql/ddl.sql deleted file mode 100644 index a41b6e9d..00000000 --- a/resources/session05/sql/ddl.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Schema for a simple book database - --- Author table - -CREATE TABLE author( - authorid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name TEXT -); - -CREATE TABLE book( - bookid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - title TEXT, - author INTEGER NOT NULL, - FOREIGN KEY(author) REFERENCES author(authorid) -); diff --git a/resources/session05/sql/populatedb.py b/resources/session05/sql/populatedb.py deleted file mode 100644 index 92baff4d..00000000 --- a/resources/session05/sql/populatedb.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import sys -import sqlite3 -from utils import AUTHORS_BOOKS - -DB_FILENAME = 'books.db' -DB_IS_NEW = not os.path.exists(DB_FILENAME) - -author_insert = "INSERT INTO author (name) VALUES(?);" -author_query = "SELECT * FROM author;" -book_query = "SELECT * FROM book;" -book_insert = """ -INSERT INTO book (title, author) VALUES(?, ( - SELECT authorid FROM author WHERE name=? )); -""" - - -def show_query_results(conn, query): - cur = conn.cursor() - cur.execute(query) - had_rows = False - for row in cur.fetchall(): - print row - had_rows = True - if not had_rows: - print "no rows returned" - - -def show_authors(conn): - query = author_query - show_query_results(conn, query) - - -def show_books(conn): - query = book_query - show_query_results(conn, query) - - -if __name__ == '__main__': - if DB_IS_NEW: - print "Database does not yet exist, please import `createdb` first" - sys.exit(1) - - print "Do something cool here" diff --git a/resources/session05/sql/utils.py b/resources/session05/sql/utils.py deleted file mode 100644 index 750669b1..00000000 --- a/resources/session05/sql/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -TABLEPRAGMA = "PRAGMA table_info(%s);" - - -def print_table_metadata(cursor): - tmpl = "%-10s |" - rowdata = cursor.description - results = cursor.fetchall() - for col in rowdata: - print tmpl % col[0], - print '\n' + '-----------+-'*len(rowdata) - for row in results: - for value in row: - print tmpl % value, - print '\n' + '-----------+-'*len(rowdata) - print '\n' - - -def show_table_metadata(cursor, tablename): - stmt = TABLEPRAGMA % tablename - cursor.execute(stmt) - print "Table Metadata for '%s':" % tablename - print_table_metadata(cursor) - - -AUTHORS_BOOKS = { - 'China Mieville': ["Perdido Street Station", "The Scar", "King Rat"], - 'Frank Herbert': ["Dune", "Hellstrom's Hive"], - 'J.R.R. Tolkien': ["The Hobbit", "The Silmarillion"], - 'Susan Cooper': ["The Dark is Rising", ["The Greenwitch"]], - 'Madeline L\'Engle': ["A Wrinkle in Time", "A Swiftly Tilting Planet"] -} diff --git a/resources/session06/__init__.py b/resources/session06/__init__.py new file mode 100644 index 00000000..32e9cc81 --- /dev/null +++ b/resources/session06/__init__.py @@ -0,0 +1,30 @@ +from pyramid.config import Configurator +from sqlalchemy import engine_from_config + +from .models import ( + DBSession, + Base, + ) + + +def create_session(settings): + from sqlalchemy.orm import sessionmaker + engine = engine_from_config(settings, 'sqlalchemy.') + Session = sessionmaker(bind=engine) + return Session() + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.add_route('detail', '/journal/{id:\d+}') + config.add_route('action', '/journal/{action}') + config.scan() + return config.make_wsgi_app() diff --git a/resources/session10/wikitutorial/development.ini b/resources/session06/development.ini similarity index 56% rename from resources/session10/wikitutorial/development.ini rename to resources/session06/development.ini index 9dc585f2..1139ff82 100644 --- a/resources/session10/wikitutorial/development.ini +++ b/resources/session06/development.ini @@ -1,10 +1,10 @@ ### # app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html ### [app:main] -use = egg:wikitutorial +use = egg:learning_journal pyramid.reload_templates = true pyramid.debug_authorization = false @@ -13,16 +13,19 @@ pyramid.debug_routematch = false pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar - pyramid_zodbconn pyramid_tm -tm.attempts = 3 -zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 + +[pshell] +create_session = learning_journal.create_session +Entry = learning_journal.models.Entry + ### # wsgi server configuration ### @@ -34,11 +37,11 @@ port = 6543 ### # logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html ### [loggers] -keys = root, wikitutorial +keys = root, learning_journal, sqlalchemy [handlers] keys = console @@ -50,10 +53,18 @@ keys = generic level = INFO handlers = console -[logger_wikitutorial] +[logger_learning_journal] level = DEBUG handlers = -qualname = wikitutorial +qualname = learning_journal + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) [handler_console] class = StreamHandler diff --git a/resources/session06/forms.py b/resources/session06/forms.py new file mode 100644 index 00000000..88d9d348 --- /dev/null +++ b/resources/session06/forms.py @@ -0,0 +1,21 @@ +from wtforms import ( + Form, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) diff --git a/resources/session06/layout.jinja2 b/resources/session06/layout.jinja2 new file mode 100644 index 00000000..8dbff846 --- /dev/null +++ b/resources/session06/layout.jinja2 @@ -0,0 +1,29 @@ + + + + + Python Learning Journal + + + + +
+ +
+
+

My Python Journal

+
+ {% block body %}{% endblock %} +
+
+
+

Created in the UW PCE Python Certificate Program

+
+ + diff --git a/resources/session06/learning_journal/.gitignore b/resources/session06/learning_journal/.gitignore new file mode 100644 index 00000000..c7332211 --- /dev/null +++ b/resources/session06/learning_journal/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.DS_Store +*.egg-info diff --git a/resources/session10/wikitutorial/CHANGES.txt b/resources/session06/learning_journal/CHANGES.txt similarity index 100% rename from resources/session10/wikitutorial/CHANGES.txt rename to resources/session06/learning_journal/CHANGES.txt diff --git a/resources/session06/learning_journal/MANIFEST.in b/resources/session06/learning_journal/MANIFEST.in new file mode 100644 index 00000000..3a0de395 --- /dev/null +++ b/resources/session06/learning_journal/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include learning_journal *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/resources/session06/learning_journal/README.txt b/resources/session06/learning_journal/README.txt new file mode 100644 index 00000000..f49a002c --- /dev/null +++ b/resources/session06/learning_journal/README.txt @@ -0,0 +1,14 @@ +learning_journal README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/python setup.py develop + +- $VENV/bin/initialize_learning_journal_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/resources/session06/learning_journal/development.ini b/resources/session06/learning_journal/development.ini new file mode 100644 index 00000000..1139ff82 --- /dev/null +++ b/resources/session06/learning_journal/development.ini @@ -0,0 +1,76 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + + +[pshell] +create_session = learning_journal.create_session +Entry = learning_journal.models.Entry + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html +### + +[loggers] +keys = root, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_learning_journal] +level = DEBUG +handlers = +qualname = learning_journal + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/resources/session06/learning_journal/learning_journal/__init__.py b/resources/session06/learning_journal/learning_journal/__init__.py new file mode 100644 index 00000000..32e9cc81 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/__init__.py @@ -0,0 +1,30 @@ +from pyramid.config import Configurator +from sqlalchemy import engine_from_config + +from .models import ( + DBSession, + Base, + ) + + +def create_session(settings): + from sqlalchemy.orm import sessionmaker + engine = engine_from_config(settings, 'sqlalchemy.') + Session = sessionmaker(bind=engine) + return Session() + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.add_route('detail', '/journal/{id:\d+}') + config.add_route('action', '/journal/{action}') + config.scan() + return config.make_wsgi_app() diff --git a/resources/session06/learning_journal/learning_journal/forms.py b/resources/session06/learning_journal/learning_journal/forms.py new file mode 100644 index 00000000..88d9d348 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/forms.py @@ -0,0 +1,21 @@ +from wtforms import ( + Form, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) diff --git a/resources/session06/learning_journal/learning_journal/models.py b/resources/session06/learning_journal/learning_journal/models.py new file mode 100644 index 00000000..7afb0ddb --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/models.py @@ -0,0 +1,64 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) + + + + + diff --git a/resources/session06/learning_journal/learning_journal/scripts/__init__.py b/resources/session06/learning_journal/learning_journal/scripts/__init__.py new file mode 100644 index 00000000..5bb534f7 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/resources/session06/learning_journal/learning_journal/scripts/initializedb.py b/resources/session06/learning_journal/learning_journal/scripts/initializedb.py new file mode 100644 index 00000000..7dfdece1 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/scripts/initializedb.py @@ -0,0 +1,40 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models import ( + DBSession, + MyModel, + Base, + ) + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=1) + DBSession.add(model) diff --git a/resources/session06/learning_journal/learning_journal/static/pyramid-16x16.png b/resources/session06/learning_journal/learning_journal/static/pyramid-16x16.png new file mode 100644 index 00000000..97920311 Binary files /dev/null and b/resources/session06/learning_journal/learning_journal/static/pyramid-16x16.png differ diff --git a/resources/session06/learning_journal/learning_journal/static/pyramid.png b/resources/session06/learning_journal/learning_journal/static/pyramid.png new file mode 100644 index 00000000..4ab837be Binary files /dev/null and b/resources/session06/learning_journal/learning_journal/static/pyramid.png differ diff --git a/resources/session06/learning_journal/learning_journal/static/styles.css b/resources/session06/learning_journal/learning_journal/static/styles.css new file mode 100644 index 00000000..951ac84f --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/static/styles.css @@ -0,0 +1,73 @@ +body{ + color:#111; + padding:0; + margin:0; + background-color: #eee;} +header{ + margin:0; + padding:0 0.75em; + width:100%; + background: #222; + color: #ccc; + border-bottom: 3px solid #fff;} +header:after{ + content:""; + display:table; + clear:both;} +header a, +footer a{ + text-decoration:none} +header a:hover, +footer a:hover { + color:#fff; +} +header a:visited, +footer a:visited { + color:#eee; +} +header aside{ + float:right; + text-align:right; + padding-right:0.75em} +header ul{ + list-style:none; + list-style-type:none; + display:inline-block} +header ul li{ + margin:0 0.25em 0 0} +header ul li a{ + padding:0; + display:inline-block} +main{padding:0 0.75em 1em} +main:after{ + content:""; + display:table; + clear:both} +main article{ + margin-bottom:1em; + padding-left:0.5em} +main article h3{margin-top:0} +main article .entry_body{ + margin:0.5em} +main aside{float:right} +main aside .field{ + margin-bottom:1em} +main aside .field input, +main aside .field label, +main aside .field textarea{ + vertical-align:top} +main aside .field label{ + display:inline-block; + width:15%; + padding-top:2px} +main aside .field input, +main aside .field textarea{ + width:83%} +main aside .control_row input{ + margin-left:16%} +footer{ + padding: 1em 0.75em; + background: #222; + color: #ccc; + border-top: 3px solid #fff; + border-bottom: 3px solid #fff;} diff --git a/resources/session06/learning_journal/learning_journal/static/theme.css b/resources/session06/learning_journal/learning_journal/static/theme.css new file mode 100644 index 00000000..228768e2 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/static/theme.css @@ -0,0 +1,152 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 25px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/resources/session06/learning_journal/learning_journal/static/theme.min.css b/resources/session06/learning_journal/learning_journal/static/theme.min.css new file mode 100644 index 00000000..2f924bcc --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file diff --git a/resources/session06/learning_journal/learning_journal/templates/detail.jinja2 b/resources/session06/learning_journal/learning_journal/templates/detail.jinja2 new file mode 100644 index 00000000..29d736f5 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/templates/detail.jinja2 @@ -0,0 +1,11 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+

{{ entry.title }}

+
+

{{ entry.body }}

+
+

Created {{entry.created}}

+
+

Go Back

+{% endblock %} diff --git a/resources/session06/learning_journal/learning_journal/templates/edit.jinja2 b/resources/session06/learning_journal/learning_journal/templates/edit.jinja2 new file mode 100644 index 00000000..ebe0f6b9 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/templates/edit.jinja2 @@ -0,0 +1,17 @@ +{% extends "templates/layout.jinja2" %} +{% block body %} +

Create a Journal Entry

+
+{% for field in form %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +

{{ field.label }}: {{ field }}

+{% endfor %} +

+
+{% endblock %} diff --git a/resources/session06/learning_journal/learning_journal/templates/layout.jinja2 b/resources/session06/learning_journal/learning_journal/templates/layout.jinja2 new file mode 100644 index 00000000..a8b27afd --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/templates/layout.jinja2 @@ -0,0 +1,29 @@ + + + + + Python Learning Journal + + + + +
+ +
+
+

My Python Journal

+
+ {% block body %}{% endblock %} +
+
+
+

Created in the UW PCE Python Certificate Program

+
+ + diff --git a/resources/session06/learning_journal/learning_journal/templates/list.jinja2 b/resources/session06/learning_journal/learning_journal/templates/list.jinja2 new file mode 100644 index 00000000..09c835a8 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/templates/list.jinja2 @@ -0,0 +1,16 @@ +{% extends "layout.jinja2" %} +{% block body %} +{% if entries %} +

Journal Entries

+ +{% else %} +

This journal is empty

+{% endif %} +

New Entry

+{% endblock %} diff --git a/resources/session06/learning_journal/learning_journal/templates/mytemplate.pt b/resources/session06/learning_journal/learning_journal/templates/mytemplate.pt new file mode 100644 index 00000000..9e88dc4b --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/templates/mytemplate.pt @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid Alchemy scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework 1.5.2.

+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/resources/session06/learning_journal/learning_journal/tests.py b/resources/session06/learning_journal/learning_journal/tests.py new file mode 100644 index 00000000..4fc444a6 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/tests.py @@ -0,0 +1,55 @@ +import unittest +import transaction + +from pyramid import testing + +from .models import DBSession + + +class TestMyViewSuccessCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_passing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'learning_journal') + + +class TestMyViewFailureCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_failing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info.status_int, 500) diff --git a/resources/session06/learning_journal/learning_journal/views.py b/resources/session06/learning_journal/learning_journal/views.py new file mode 100644 index 00000000..ad76afb5 --- /dev/null +++ b/resources/session06/learning_journal/learning_journal/views.py @@ -0,0 +1,43 @@ +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.view import view_config + +from .models import ( + DBSession, + MyModel, + Entry, + ) + +from .forms import EntryCreateForm + + +@view_config(route_name='home', renderer='templates/list.jinja2') +def index_page(request): + entries = Entry.all() + return {'entries': entries} + + +@view_config(route_name='detail', renderer='templates/detail.jinja2') +def view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + return {'entry': entry} + + +@view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2') +def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='action', match_param='action=edit', + renderer='string') +def update(request): + return 'edit page' diff --git a/resources/session10/wikitutorial/production.ini b/resources/session06/learning_journal/production.ini similarity index 54% rename from resources/session10/wikitutorial/production.ini rename to resources/session06/learning_journal/production.ini index 2b952479..1db7a630 100644 --- a/resources/session10/wikitutorial/production.ini +++ b/resources/session06/learning_journal/production.ini @@ -1,10 +1,10 @@ ### # app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html ### [app:main] -use = egg:wikitutorial +use = egg:learning_journal pyramid.reload_templates = false pyramid.debug_authorization = false @@ -13,14 +13,8 @@ pyramid.debug_routematch = false pyramid.default_locale_name = en pyramid.includes = pyramid_tm - pyramid_zodbconn -tm.attempts = 3 -zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 - -### -# wsgi server configuration -### +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite [server:main] use = egg:waitress#main @@ -29,11 +23,11 @@ port = 6543 ### # logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html ### [loggers] -keys = root, wikitutorial +keys = root, learning_journal, sqlalchemy [handlers] keys = console @@ -45,10 +39,18 @@ keys = generic level = WARN handlers = console -[logger_wikitutorial] +[logger_learning_journal] +level = WARN +handlers = +qualname = learning_journal + +[logger_sqlalchemy] level = WARN handlers = -qualname = wikitutorial +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) [handler_console] class = StreamHandler diff --git a/resources/session10/wikitutorial/setup.py b/resources/session06/learning_journal/setup.py similarity index 59% rename from resources/session10/wikitutorial/setup.py rename to resources/session06/learning_journal/setup.py index ebf894e9..e4bb0bcd 100644 --- a/resources/session10/wikitutorial/setup.py +++ b/resources/session06/learning_journal/setup.py @@ -3,24 +3,26 @@ from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', - 'pyramid_zodbconn', - 'transaction', - 'pyramid_tm', + 'pyramid_jinja2', 'pyramid_debugtoolbar', - 'ZODB3', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', 'waitress', - 'docutils', - 'webtest', + 'wtforms', ] -setup(name='wikitutorial', +setup(name='learning_journal', version='0.0', - description='wikitutorial', + description='learning_journal', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", @@ -31,15 +33,16 @@ author='', author_email='', url='', - keywords='web pylons pyramid', + keywords='web wsgi bfg pylons pyramid', packages=find_packages(), include_package_data=True, zip_safe=False, + test_suite='learning_journal', install_requires=requires, - tests_require=requires, - test_suite="wikitutorial", entry_points="""\ [paste.app_factory] - main = wikitutorial:main + main = learning_journal:main + [console_scripts] + initialize_learning_journal_db = learning_journal.scripts.initializedb:main """, ) diff --git a/resources/session06/microblog/microblog.cfg b/resources/session06/microblog/microblog.cfg deleted file mode 100644 index f4ee5c52..00000000 --- a/resources/session06/microblog/microblog.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# application configuration for a Flask microblog - -DATABASE = 'microblog.db' -SECRET_KEY = "sooperseekritvaluenooneshouldknow" -USERNAME = "admin" -PASSWORD = "secret" \ No newline at end of file diff --git a/resources/session06/microblog/microblog.py b/resources/session06/microblog/microblog.py deleted file mode 100644 index f5f7f793..00000000 --- a/resources/session06/microblog/microblog.py +++ /dev/null @@ -1,107 +0,0 @@ -import sqlite3 -from contextlib import closing - -from flask import abort -from flask import Flask -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import session -from flask import url_for - -app = Flask(__name__) - -app.config.from_pyfile('microblog.cfg') - - -def connect_db(): - return sqlite3.connect(app.config['DATABASE']) - - -def init_db(): - with closing(connect_db()) as db: - with app.open_resource('schema.sql') as f: - db.cursor().executescript(f.read()) - db.commit() - - -def get_database_connection(): - db = getattr(g, 'db', None) - if db is None: - g.db = db = connect_db() - return db - - -@app.teardown_request -def teardown_request(exception): - db = getattr(g, 'db', None) - if db is not None: - db.close() - - -def write_entry(title, text): - con = get_database_connection() - con.execute('insert into entries (title, text) values (?, ?)', - [title, text]) - con.commit() - - -def get_all_entries(): - con = get_database_connection() - cur = con.execute('SELECT title, text FROM entries ORDER BY id DESC') - return [dict(title=row[0], text=row[1]) for row in cur.fetchall()] - - -def do_login(usr, pwd): - if usr != app.config['USERNAME']: - raise ValueError - elif pwd != app.config['PASSWORD']: - raise ValueError - else: - session['logged_in'] = True - - -@app.route('/') -def show_entries(): - entries = get_all_entries() - return render_template('show_entries.html', entries=entries) - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - error = None - if request.method == 'POST': - try: - do_login(request.form['username'], - request.form['password']) - except ValueError: - error = "Invalid Login" - else: - flash('You were logged in') - return redirect(url_for('show_entries')) - return render_template('login.html', error=error) - - -@app.route('/logout') -def logout(): - session.pop('logged_in', None) - flash('You were logged out') - return redirect(url_for('show_entries')) - - -@app.route('/add', methods=['POST']) -def add_entry(): - if not session.get('logged_in'): - abort(401) - try: - write_entry(request.form['title'], request.form['text']) - flash('New entry was successfully posted') - except sqlite3.Error as e: - flash('There was an error: %s' % e.args[0]) - return redirect(url_for('show_entries')) - - -if __name__ == '__main__': - app.run(debug=True) diff --git a/resources/session06/microblog/microblog_tests.py b/resources/session06/microblog/microblog_tests.py deleted file mode 100644 index 2363655d..00000000 --- a/resources/session06/microblog/microblog_tests.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import tempfile -import unittest -from flask import session - - -import microblog - -class MicroblogTestCase(unittest.TestCase): - - def setUp(self): - db_fd = tempfile.mkstemp() - self.db_fd, microblog.app.config['DATABASE'] = db_fd - microblog.app.config['TESTING'] = True - self.client = microblog.app.test_client() - self.app = microblog.app - microblog.init_db() - - def tearDown(self): - os.close(self.db_fd) - os.unlink(microblog.app.config['DATABASE']) - - def login(self, username, password): - return self.client.post('/login', data=dict( - username=username, - password=password - ), follow_redirects=True) - - def logout(self): - return self.client.get('/logout', - follow_redirects=True) - - def test_database_setup(self): - con = microblog.connect_db() - cur = con.execute('PRAGMA table_info(entries);') - rows = cur.fetchall() - self.assertEquals(len(rows), 3) - - def test_write_entry(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - con = microblog.connect_db() - cur = con.execute("select * from entries;") - rows = cur.fetchall() - self.assertEquals(len(rows), 1) - for val in expected: - self.assertTrue(val in rows[0]) - - def test_get_all_entries_empty(self): - with self.app.test_request_context('/'): - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 0) - - def test_get_all_entries(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 1) - for entry in entries: - self.assertEquals(expected[0], entry['title']) - self.assertEquals(expected[1], entry['text']) - - def test_empty_listing(self): - response = self.client.get('/') - assert 'No entries here so far' in response.data - - def test_listing(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - response = self.client.get('/') - for value in expected: - assert value in response.data - - def test_login_passes(self): - with self.app.test_request_context('/'): - microblog.do_login(microblog.app.config['USERNAME'], - microblog.app.config['PASSWORD']) - self.assertTrue(session.get('logged_in', False)) - - def test_login_fails(self): - with self.app.test_request_context('/'): - self.assertRaises(ValueError, - microblog.do_login, - microblog.app.config['USERNAME'], - 'incorrectpassword') - - def test_login_logout(self): - # verify we can log in - response = self.login('admin', 'secret') - assert 'You were logged in' in response.data - # verify we can log back out - response = self.logout() - assert 'You were logged out' in response.data - # verify that incorrect credentials get a proper message - response = self.login('notadmin', 'secret') - assert 'Invalid Login' in response.data - response = self.login('admin', 'notsosecret') - assert 'Invalid Login' in response.data - - def test_add_entries(self): - self.login('admin', 'secret') - response = self.client.post('/add', data=dict( - title='Hello', - text='This is a post' - ), follow_redirects=True) - assert 'No entries here so far' not in response.data - assert 'Hello' in response.data - assert 'This is a post' in response.data - - -if __name__ == '__main__': - unittest.main() diff --git a/resources/session06/microblog/schema.sql b/resources/session06/microblog/schema.sql deleted file mode 100644 index 71fe0588..00000000 --- a/resources/session06/microblog/schema.sql +++ /dev/null @@ -1,6 +0,0 @@ -drop table if exists entries; -create table entries ( - id integer primary key autoincrement, - title string not null, - text string not null -); diff --git a/resources/session06/microblog/static/style.css b/resources/session06/microblog/static/style.css deleted file mode 100644 index 3c24310b..00000000 --- a/resources/session06/microblog/static/style.css +++ /dev/null @@ -1,20 +0,0 @@ -body { font-family: 'Helvetica', sans-serif; background: #eaeced; } -a, h1, h2 { color: #1E727F; } -h1, h2 { font-family: 'Helvetica', sans-serif; margin: 0; } -h1 { font-size: 2em; border-bottom: 2px solid #1E727F; padding-bottom: 0.25em; margin-bottom: 0.5em;} -h2 { font-size: 1.4em; } -.page { margin: 2em auto; width: 35em; border: 5px solid #1E727F; - padding: 0.8em; background: white; } -.entries { list-style: none; margin: 0; padding: 0; } -.entries li { margin: 0.8em 1.2em; } -.entries li h2 { margin-left: -1em; } -.add_entry { float: right; clear: right; width: 50%; font-size: 0.9em; - border: 1px solid #1E727F; padding: 1em; background: #fafafa;} -.add_entry dl { font-weight: bold; } -.add_entry label {display: block; } -.add_entry .field {margin-bottom: 0.25em;} -.metanav { text-align: left; font-size: 0.8em; padding: 0.3em; - margin-bottom: 1em; background: #fafafa; border: 1px solid #1E727F} -.flash { width: 30%; background: #00B0CC; padding: 1em; - border: 1px solid #1E727F; margin-bottom: 1em;} -.error { background: #F0D6D6; padding: 0.5em; } \ No newline at end of file diff --git a/resources/session06/microblog/templates/layout.html b/resources/session06/microblog/templates/layout.html deleted file mode 100644 index 26d11973..00000000 --- a/resources/session06/microblog/templates/layout.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - Microblog! - - - -

My Microblog

-
- {% if not session.logged_in %} - log in - {% else %} - log_out - {% endif %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} -
- {% block body %}{% endblock %} -
- - \ No newline at end of file diff --git a/resources/session06/microblog/templates/login.html b/resources/session06/microblog/templates/login.html deleted file mode 100644 index 86de3647..00000000 --- a/resources/session06/microblog/templates/login.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

Login

- {% if error -%} -

Error {{ error }} - {%- endif %} -

-
- - -
-
- - -
-
- -
-
-{% endblock %} \ No newline at end of file diff --git a/resources/session06/microblog/templates/show_entries.html b/resources/session06/microblog/templates/show_entries.html deleted file mode 100644 index f44fd92b..00000000 --- a/resources/session06/microblog/templates/show_entries.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "layout.html" %} -{% block body %} - {% if session.logged_in %} -
-
- - -
-
- - -
-
- -
-
- {% endif %} -

Posts

-
    - {% for entry in entries %} -
  • -

    {{ entry.title }}

    -
    - {{ entry.text|safe }} -
    -
  • - {% else %} -
  • No entries here so far
  • - {% endfor %} -
-{% endblock %} \ No newline at end of file diff --git a/resources/session06/models.py b/resources/session06/models.py new file mode 100644 index 00000000..e87ac2c8 --- /dev/null +++ b/resources/session06/models.py @@ -0,0 +1,59 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) diff --git a/resources/session06/styles.css b/resources/session06/styles.css new file mode 100644 index 00000000..951ac84f --- /dev/null +++ b/resources/session06/styles.css @@ -0,0 +1,73 @@ +body{ + color:#111; + padding:0; + margin:0; + background-color: #eee;} +header{ + margin:0; + padding:0 0.75em; + width:100%; + background: #222; + color: #ccc; + border-bottom: 3px solid #fff;} +header:after{ + content:""; + display:table; + clear:both;} +header a, +footer a{ + text-decoration:none} +header a:hover, +footer a:hover { + color:#fff; +} +header a:visited, +footer a:visited { + color:#eee; +} +header aside{ + float:right; + text-align:right; + padding-right:0.75em} +header ul{ + list-style:none; + list-style-type:none; + display:inline-block} +header ul li{ + margin:0 0.25em 0 0} +header ul li a{ + padding:0; + display:inline-block} +main{padding:0 0.75em 1em} +main:after{ + content:""; + display:table; + clear:both} +main article{ + margin-bottom:1em; + padding-left:0.5em} +main article h3{margin-top:0} +main article .entry_body{ + margin:0.5em} +main aside{float:right} +main aside .field{ + margin-bottom:1em} +main aside .field input, +main aside .field label, +main aside .field textarea{ + vertical-align:top} +main aside .field label{ + display:inline-block; + width:15%; + padding-top:2px} +main aside .field input, +main aside .field textarea{ + width:83%} +main aside .control_row input{ + margin-left:16%} +footer{ + padding: 1em 0.75em; + background: #222; + color: #ccc; + border-top: 3px solid #fff; + border-bottom: 3px solid #fff;} diff --git a/resources/session07/detail.jinja2 b/resources/session07/detail.jinja2 new file mode 100644 index 00000000..f80810d3 --- /dev/null +++ b/resources/session07/detail.jinja2 @@ -0,0 +1,15 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+

{{ entry.title }}

+
+

{{ entry.body }}

+
+

Created {{entry.created}}

+
+

+ Go Back :: + + Edit Entry +

+{% endblock %} diff --git a/resources/session07/forms.py b/resources/session07/forms.py new file mode 100644 index 00000000..fad71bd1 --- /dev/null +++ b/resources/session07/forms.py @@ -0,0 +1,26 @@ +from wtforms import ( + Form, + HiddenField, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) + + +class EntryEditForm(EntryCreateForm): + id = HiddenField() diff --git a/resources/session07/learning_journal/.gitignore b/resources/session07/learning_journal/.gitignore new file mode 100644 index 00000000..2ffa3242 --- /dev/null +++ b/resources/session07/learning_journal/.gitignore @@ -0,0 +1,4 @@ +*.pyc +.DS_Store +*.egg-info +*.sqlite diff --git a/resources/session07/learning_journal/CHANGES.txt b/resources/session07/learning_journal/CHANGES.txt new file mode 100644 index 00000000..35a34f33 --- /dev/null +++ b/resources/session07/learning_journal/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/resources/session07/learning_journal/MANIFEST.in b/resources/session07/learning_journal/MANIFEST.in new file mode 100644 index 00000000..3a0de395 --- /dev/null +++ b/resources/session07/learning_journal/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include learning_journal *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/resources/session07/learning_journal/Procfile b/resources/session07/learning_journal/Procfile new file mode 100644 index 00000000..e6450506 --- /dev/null +++ b/resources/session07/learning_journal/Procfile @@ -0,0 +1 @@ +web: ./run diff --git a/resources/session07/learning_journal/README.txt b/resources/session07/learning_journal/README.txt new file mode 100644 index 00000000..f49a002c --- /dev/null +++ b/resources/session07/learning_journal/README.txt @@ -0,0 +1,14 @@ +learning_journal README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/python setup.py develop + +- $VENV/bin/initialize_learning_journal_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/resources/session07/learning_journal/build_db b/resources/session07/learning_journal/build_db new file mode 100755 index 00000000..a912dc68 --- /dev/null +++ b/resources/session07/learning_journal/build_db @@ -0,0 +1,3 @@ +#!/bin/bash +python setup.py develop +initialize_learning_journal_db production.ini diff --git a/resources/session07/learning_journal/development.ini b/resources/session07/learning_journal/development.ini new file mode 100644 index 00000000..f52e811f --- /dev/null +++ b/resources/session07/learning_journal/development.ini @@ -0,0 +1,78 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite + +jinja2.filters = + markdown = learning_journal.views.render_markdown + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +[pshell] +create_session = learning_journal.create_session +Entry = learning_journal.models.Entry + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html +### + +[loggers] +keys = root, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_learning_journal] +level = DEBUG +handlers = +qualname = learning_journal + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/resources/session07/learning_journal/learning_journal/__init__.py b/resources/session07/learning_journal/learning_journal/__init__.py new file mode 100644 index 00000000..6f51ba85 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/__init__.py @@ -0,0 +1,44 @@ +import os +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator +from sqlalchemy import engine_from_config + +from .models import ( + DBSession, + Base, + ) +from .security import EntryFactory + + +def create_session(settings): + from sqlalchemy.orm import sessionmaker + engine = engine_from_config(settings, 'sqlalchemy.') + Session = sessionmaker(bind=engine) + return Session() + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + if 'DATABASE_URL' in os.environ: + settings['sqlalchemy.url'] = os.environ['DATABASE_URL'] + engine = engine_from_config(settings, 'sqlalchemy.') + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + secret = os.environ.get('AUTH_SECRET', 'somesecret') + config = Configurator( + settings=settings, + authentication_policy=AuthTktAuthenticationPolicy(secret), + authorization_policy=ACLAuthorizationPolicy(), + default_permission='view' + ) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/', factory=EntryFactory) + config.add_route('detail', '/journal/{id:\d+}', factory=EntryFactory) + config.add_route('action', '/journal/{action}', factory=EntryFactory) + config.add_route('auth', '/sign/{action}', factory=EntryFactory) + config.scan() + return config.make_wsgi_app() diff --git a/resources/session07/learning_journal/learning_journal/forms.py b/resources/session07/learning_journal/learning_journal/forms.py new file mode 100644 index 00000000..652c286b --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/forms.py @@ -0,0 +1,32 @@ +from wtforms import ( + Form, + HiddenField, + PasswordField, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) + + +class EntryEditForm(EntryCreateForm): + id = HiddenField() + + +class LoginForm(Form): + username = TextField('Username', [validators.Length(min=1, max=255)]) + password = PasswordField('Password', [validators.Length(min=1, max=255)]) diff --git a/resources/session07/learning_journal/learning_journal/models.py b/resources/session07/learning_journal/learning_journal/models.py new file mode 100644 index 00000000..6f2290c2 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/models.py @@ -0,0 +1,79 @@ +import datetime +from passlib.context import CryptContext +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +password_context = CryptContext(schemes=['pbkdf2_sha512']) + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + + @classmethod + def by_name(cls, name, session=None): + if session is None: + session = DBSession + return DBSession.query(cls).filter(cls.name == name).first() + + def verify_password(self, password): + return password_context.verify(password, self.password) diff --git a/resources/session07/learning_journal/learning_journal/scripts/__init__.py b/resources/session07/learning_journal/learning_journal/scripts/__init__.py new file mode 100644 index 00000000..5bb534f7 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/resources/session07/learning_journal/learning_journal/scripts/initializedb.py b/resources/session07/learning_journal/learning_journal/scripts/initializedb.py new file mode 100644 index 00000000..a5dc7b20 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/scripts/initializedb.py @@ -0,0 +1,46 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models import ( + DBSession, + MyModel, + Base, + User, + password_context + ) + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + if 'DATABASE_URL' in os.environ: + settings['sqlalchemy.url'] = os.environ['DATABASE_URL'] + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + password = os.environ.get('ADMIN_PASSWORD', 'admin') + encrypted = password_context.encrypt(password) + admin = User(name=u'admin', password=encrypted) + DBSession.add(admin) diff --git a/resources/session07/learning_journal/learning_journal/security.py b/resources/session07/learning_journal/learning_journal/security.py new file mode 100644 index 00000000..2ee4d4eb --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/security.py @@ -0,0 +1,12 @@ +from pyramid.security import Allow, Everyone, Authenticated + + +class EntryFactory(object): + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, Authenticated, 'create'), + (Allow, Authenticated, 'edit'), + ] + + def __init__(self, request): + pass diff --git a/resources/session07/learning_journal/learning_journal/static/pyramid-16x16.png b/resources/session07/learning_journal/learning_journal/static/pyramid-16x16.png new file mode 100644 index 00000000..97920311 Binary files /dev/null and b/resources/session07/learning_journal/learning_journal/static/pyramid-16x16.png differ diff --git a/resources/session07/learning_journal/learning_journal/static/pyramid.png b/resources/session07/learning_journal/learning_journal/static/pyramid.png new file mode 100644 index 00000000..4ab837be Binary files /dev/null and b/resources/session07/learning_journal/learning_journal/static/pyramid.png differ diff --git a/resources/session07/learning_journal/learning_journal/static/styles.css b/resources/session07/learning_journal/learning_journal/static/styles.css new file mode 100644 index 00000000..e9299ebd --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/static/styles.css @@ -0,0 +1,138 @@ +body{ + color:#111; + padding:0; + margin:0; + background-color: #eee;} +header{ + margin:0; + padding:0 0.75em; + width:100%; + background: #222; + color: #ccc; + border-bottom: 3px solid #fff;} +header:after{ + content:""; + display:table; + clear:both;} +header a, +footer a{ + text-decoration:none} +header a:hover, +footer a:hover { + color:#fff; +} +header a:visited, +footer a:visited { + color:#eee; +} +header aside{ + float:right; + text-align:right; + padding-right:0.75em} +header ul{ + list-style:none; + list-style-type:none; + display:inline-block} +header ul li{ + margin:0 0.25em 0 0} +header ul li a{ + padding:0; + display:inline-block} +main{padding:0 0.75em 1em} +main:after{ + content:""; + display:table; + clear:both} +main article{ + margin-bottom:1em; + padding-left:0.5em} +main article h3{margin-top:0} +main article .entry_body{ + margin:0.5em} +main aside{float:right} +main aside .field{ + margin-bottom:1em} +main aside .field input, +main aside .field label, +main aside .field textarea{ + vertical-align:top} +main aside .field label{ + display:inline-block; + width:15%; + padding-top:2px} +main aside .field input, +main aside .field textarea{ + width:83%} +main aside .control_row input{ + margin-left:16%} +footer{ + padding: 1em 0.75em; + background: #222; + color: #ccc; + border-top: 3px solid #fff; + border-bottom: 3px solid #fff;} +.codehilite {padding: 0.25em 1em;} +/* Pygments code hilight styles */ +.codehilite .hll { background-color: #ffffcc } +.codehilite { background: #ffffff; } +.codehilite .c { color: #888888 } /* Comment */ +.codehilite .err { color: #FF0000; background-color: #FFAAAA } /* Error */ +.codehilite .k { color: #008800; font-weight: bold } /* Keyword */ +.codehilite .o { color: #333333 } /* Operator */ +.codehilite .cm { color: #888888 } /* Comment.Multiline */ +.codehilite .cp { color: #557799 } /* Comment.Preproc */ +.codehilite .c1 { color: #888888 } /* Comment.Single */ +.codehilite .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ +.codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.codehilite .ge { font-style: italic } /* Generic.Emph */ +.codehilite .gr { color: #FF0000 } /* Generic.Error */ +.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.codehilite .gi { color: #00A000 } /* Generic.Inserted */ +.codehilite .go { color: #888888 } /* Generic.Output */ +.codehilite .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.codehilite .gs { font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.codehilite .gt { color: #0044DD } /* Generic.Traceback */ +.codehilite .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ +.codehilite .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ +.codehilite .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ +.codehilite .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */ +.codehilite .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ +.codehilite .kt { color: #333399; font-weight: bold } /* Keyword.Type */ +.codehilite .m { color: #6600EE; font-weight: bold } /* Literal.Number */ +.codehilite .s { background-color: #fff0f0 } /* Literal.String */ +.codehilite .na { color: #0000CC } /* Name.Attribute */ +.codehilite .nb { color: #007020 } /* Name.Builtin */ +.codehilite .nc { color: #BB0066; font-weight: bold } /* Name.Class */ +.codehilite .no { color: #003366; font-weight: bold } /* Name.Constant */ +.codehilite .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.codehilite .ni { color: #880000; font-weight: bold } /* Name.Entity */ +.codehilite .ne { color: #FF0000; font-weight: bold } /* Name.Exception */ +.codehilite .nf { color: #0066BB; font-weight: bold } /* Name.Function */ +.codehilite .nl { color: #997700; font-weight: bold } /* Name.Label */ +.codehilite .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.codehilite .nt { color: #007700 } /* Name.Tag */ +.codehilite .nv { color: #996633 } /* Name.Variable */ +.codehilite .ow { color: #000000; font-weight: bold } /* Operator.Word */ +.codehilite .w { color: #bbbbbb } /* Text.Whitespace */ +.codehilite .mb { color: #6600EE; font-weight: bold } /* Literal.Number.Bin */ +.codehilite .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */ +.codehilite .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */ +.codehilite .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ +.codehilite .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */ +.codehilite .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ +.codehilite .sc { color: #0044DD } /* Literal.String.Char */ +.codehilite .sd { color: #DD4422 } /* Literal.String.Doc */ +.codehilite .s2 { background-color: #fff0f0 } /* Literal.String.Double */ +.codehilite .se { color: #666666; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ +.codehilite .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ +.codehilite .si { background-color: #eeeeee } /* Literal.String.Interpol */ +.codehilite .sx { color: #DD2200; background-color: #fff0f0 } /* Literal.String.Other */ +.codehilite .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ +.codehilite .s1 { background-color: #fff0f0 } /* Literal.String.Single */ +.codehilite .ss { color: #AA6600 } /* Literal.String.Symbol */ +.codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.codehilite .vc { color: #336699 } /* Name.Variable.Class */ +.codehilite .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */ +.codehilite .vi { color: #3333BB } /* Name.Variable.Instance */ +.codehilite .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ diff --git a/resources/session07/learning_journal/learning_journal/static/theme.css b/resources/session07/learning_journal/learning_journal/static/theme.css new file mode 100644 index 00000000..be50ad42 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/static/theme.css @@ -0,0 +1,152 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/resources/session07/learning_journal/learning_journal/static/theme.min.css b/resources/session07/learning_journal/learning_journal/static/theme.min.css new file mode 100644 index 00000000..2f924bcc --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file diff --git a/resources/session07/learning_journal/learning_journal/templates/detail.jinja2 b/resources/session07/learning_journal/learning_journal/templates/detail.jinja2 new file mode 100644 index 00000000..8f966471 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/templates/detail.jinja2 @@ -0,0 +1,18 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+

{{ entry.title }}

+
+

{{ entry.body|markdown }}

+
+

Created {{entry.created}}

+
+

+ Go Back + {% if logged_in %} + :: + + Edit Entry + {% endif %} +

+{% endblock %} diff --git a/resources/session07/learning_journal/learning_journal/templates/edit.jinja2 b/resources/session07/learning_journal/learning_journal/templates/edit.jinja2 new file mode 100644 index 00000000..ebe0f6b9 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/templates/edit.jinja2 @@ -0,0 +1,17 @@ +{% extends "templates/layout.jinja2" %} +{% block body %} +

Create a Journal Entry

+
+{% for field in form %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +

{{ field.label }}: {{ field }}

+{% endfor %} +

+
+{% endblock %} diff --git a/resources/session07/learning_journal/learning_journal/templates/layout.jinja2 b/resources/session07/learning_journal/learning_journal/templates/layout.jinja2 new file mode 100644 index 00000000..a8b27afd --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/templates/layout.jinja2 @@ -0,0 +1,29 @@ + + + + + Python Learning Journal + + + + +
+ +
+
+

My Python Journal

+
+ {% block body %}{% endblock %} +
+
+
+

Created in the UW PCE Python Certificate Program

+
+ + diff --git a/resources/session07/learning_journal/learning_journal/templates/list.jinja2 b/resources/session07/learning_journal/learning_journal/templates/list.jinja2 new file mode 100644 index 00000000..7f1e4795 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/templates/list.jinja2 @@ -0,0 +1,31 @@ +{% extends "layout.jinja2" %} +{% block body %} +{% if login_form %} + +{% endif %} +{% if entries %} +

Journal Entries

+ +{% else %} +

This journal is empty

+{% endif %} +{% if not login_form %} +

New Entry

+{% endif %} +{% endblock %} diff --git a/resources/session07/learning_journal/learning_journal/templates/mytemplate.pt b/resources/session07/learning_journal/learning_journal/templates/mytemplate.pt new file mode 100644 index 00000000..9e88dc4b --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/templates/mytemplate.pt @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid Alchemy scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework 1.5.2.

+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/resources/session07/learning_journal/learning_journal/tests.py b/resources/session07/learning_journal/learning_journal/tests.py new file mode 100644 index 00000000..4fc444a6 --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/tests.py @@ -0,0 +1,55 @@ +import unittest +import transaction + +from pyramid import testing + +from .models import DBSession + + +class TestMyViewSuccessCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_passing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'learning_journal') + + +class TestMyViewFailureCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_failing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info.status_int, 500) diff --git a/resources/session07/learning_journal/learning_journal/views.py b/resources/session07/learning_journal/learning_journal/views.py new file mode 100644 index 00000000..d6248a0b --- /dev/null +++ b/resources/session07/learning_journal/learning_journal/views.py @@ -0,0 +1,94 @@ +from jinja2 import Markup +import markdown +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.security import forget, remember, authenticated_userid +from pyramid.view import view_config + +from .models import ( + DBSession, + MyModel, + Entry, + User + ) + +from .forms import ( + EntryCreateForm, + EntryEditForm, + LoginForm +) + + +@view_config(route_name='home', renderer='templates/list.jinja2') +def index_page(request): + entries = Entry.all() + form = None + if not authenticated_userid(request): + form = LoginForm() + return {'entries': entries, 'login_form': form} + + +@view_config(route_name='detail', renderer='templates/detail.jinja2') +def view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + logged_in = authenticated_userid(request) + return {'entry': entry, 'logged_in': logged_in} + + +@view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2', + permission='create') +def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2', + permission='edit') +def update(request): + id = int(request.params.get('id', -1)) + entry = Entry.by_id(id) + if not entry: + return HTTPNotFound() + form = EntryEditForm(request.POST, entry) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + return HTTPFound(location=request.route_url('detail', id=entry.id)) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='auth', match_param='action=in', renderer='string', + request_method='POST') +@view_config(route_name='auth', match_param='action=out', renderer='string') +def sign_in(request): + login_form = None + if request.method == 'POST': + login_form = LoginForm(request.POST) + if login_form and login_form.validate(): + user = User.by_name(login_form.username.data) + if user and user.verify_password(login_form.password.data): + headers = remember(request, user.name) + else: + headers = forget(request) + else: + headers = forget(request) + return HTTPFound(location=request.route_url('home'), + headers=headers) + + +def render_markdown(content): + output = Markup( + markdown.markdown( + content, + extensions=['codehilite(pygments_style=colorful)', 'fenced_code'] + ) + ) + return output diff --git a/resources/session07/learning_journal/production.ini b/resources/session07/learning_journal/production.ini new file mode 100644 index 00000000..d203746a --- /dev/null +++ b/resources/session07/learning_journal/production.ini @@ -0,0 +1,66 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite + +jinja2.filters = + markdown = learning_journal.views.render_markdown + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html +### + +[loggers] +keys = root, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_learning_journal] +level = WARN +handlers = +qualname = learning_journal + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/resources/session07/learning_journal/requirements.txt b/resources/session07/learning_journal/requirements.txt new file mode 100644 index 00000000..871e054a --- /dev/null +++ b/resources/session07/learning_journal/requirements.txt @@ -0,0 +1,34 @@ +appnope==0.1.0 +decorator==4.0.6 +ipython==4.0.1 +ipython-genutils==0.1.0 +Jinja2==2.8 +Mako==1.0.3 +Markdown==2.6.5 +MarkupSafe==0.23 +passlib==1.6.5 +PasteDeploy==1.5.2 +path.py==8.1.2 +pexpect==4.0.1 +pickleshare==0.5 +psycopg2==2.6.1 +ptyprocess==0.5 +Pygments==2.0.2 +pyramid==1.5.7 +pyramid-debugtoolbar==2.4.2 +pyramid-jinja2==2.5 +pyramid-mako==1.0.2 +pyramid-tm==0.12.1 +repoze.lru==0.6 +simplegeneric==0.8.1 +SQLAlchemy==1.0.11 +traitlets==4.0.0 +transaction==1.4.4 +translationstring==1.3 +venusian==1.0 +waitress==0.8.10 +WebOb==1.5.1 +WTForms==2.1 +zope.deprecation==4.1.2 +zope.interface==4.1.3 +zope.sqlalchemy==0.7.6 diff --git a/resources/session07/learning_journal/run b/resources/session07/learning_journal/run new file mode 100755 index 00000000..3689ebe2 --- /dev/null +++ b/resources/session07/learning_journal/run @@ -0,0 +1,3 @@ +#!/bin/bash +python setup.py develop +python runapp.py diff --git a/resources/session07/learning_journal/runapp.py b/resources/session07/learning_journal/runapp.py new file mode 100644 index 00000000..df24540c --- /dev/null +++ b/resources/session07/learning_journal/runapp.py @@ -0,0 +1,10 @@ +import os + +from paste.deploy import loadapp +from waitress import serve + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app = loadapp('config:production.ini', relative_to='.') + + serve(app, host='0.0.0.0', port=port) diff --git a/resources/session07/learning_journal/runtime.txt b/resources/session07/learning_journal/runtime.txt new file mode 100644 index 00000000..294a23e9 --- /dev/null +++ b/resources/session07/learning_journal/runtime.txt @@ -0,0 +1 @@ +python-3.5.0 diff --git a/resources/session07/learning_journal/setup.py b/resources/session07/learning_journal/setup.py new file mode 100644 index 00000000..f681f70b --- /dev/null +++ b/resources/session07/learning_journal/setup.py @@ -0,0 +1,51 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + 'wtforms', + 'passlib', + 'markdown', + 'pygments', + ] + +setup(name='learning_journal', + version='0.0', + description='learning_journal', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='learning_journal', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = learning_journal:main + [console_scripts] + initialize_learning_journal_db = learning_journal.scripts.initializedb:main + """, + ) diff --git a/resources/session07/models.py b/resources/session07/models.py new file mode 100644 index 00000000..a5625056 --- /dev/null +++ b/resources/session07/models.py @@ -0,0 +1,72 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + + @classmethod + def by_name(cls, name, session=None): + if session is None: + session = DBSession + return DBSession.query(cls).filter(cls.name == name).first() diff --git a/resources/session07/myblog_test_fixture.json b/resources/session07/myblog_test_fixture.json deleted file mode 100644 index 592dea17..00000000 --- a/resources/session07/myblog_test_fixture.json +++ /dev/null @@ -1,38 +0,0 @@ -[ -{ - "pk": 1, - "model": "auth.user", - "fields": { - "username": "admin", - "first_name": "Mr.", - "last_name": "Administrator", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "admin@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -}, -{ - "pk": 2, - "model": "auth.user", - "fields": { - "username": "noname", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "noname@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -} -] diff --git a/resources/session07/views.py b/resources/session07/views.py new file mode 100644 index 00000000..35e37963 --- /dev/null +++ b/resources/session07/views.py @@ -0,0 +1,54 @@ +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.view import view_config + +from .models import ( + DBSession, + MyModel, + Entry, + ) + +from .forms import ( + EntryCreateForm, + EntryEditForm, +) + + +@view_config(route_name='home', renderer='templates/list.jinja2') +def index_page(request): + entries = Entry.all() + return {'entries': entries} + + +@view_config(route_name='detail', renderer='templates/detail.jinja2') +def view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + return {'entry': entry} + + +@view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2') +def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2') +def update(request): + id = int(request.params.get('id', -1)) + entry = Entry.by_id(id) + if not entry: + return HTTPNotFound() + form = EntryEditForm(request.POST, entry) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + return HTTPFound(location=request.route_url('detail', id=entry.id)) + return {'form': form, 'action': request.matchdict.get('action')} diff --git a/resources/session08/django_blog.css b/resources/session08/django_blog.css index 64560dc0..45a882de 100644 --- a/resources/session08/django_blog.css +++ b/resources/session08/django_blog.css @@ -71,4 +71,4 @@ ul.categories { } ul.categories li { display: inline; -} \ No newline at end of file +} diff --git a/resources/session08/myblog_test_fixture.json b/resources/session08/myblog_test_fixture.json new file mode 100644 index 00000000..bf5269e9 --- /dev/null +++ b/resources/session08/myblog_test_fixture.json @@ -0,0 +1,38 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "Mr.", + "last_name": "Administrator", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "admin@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + }, + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "noname", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "noname@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + } +] diff --git a/resources/session08/mysite/myblog/admin.py b/resources/session08/mysite/myblog/admin.py deleted file mode 100644 index a2c21638..00000000 --- a/resources/session08/mysite/myblog/admin.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.contrib import admin -from django.core.urlresolvers import reverse -from myblog.models import Post, Category - - -class CategoryInlineAdmin(admin.TabularInline): - model = Category.posts.through - extra = 1 - verbose_name = "Category" - verbose_name_plural = "Categories" - - -class PostAdmin(admin.ModelAdmin): - list_display = ('__unicode__', 'created_date', 'modified_date', - 'published_date', 'author_link') - readonly_fields = ('created_date', 'modified_date') - inlines = [CategoryInlineAdmin, ] - - def author_link(self, post): - url = reverse('admin:auth_user_change', args=(post.id,)) - name = post.author_name() - return '%s' % (url, name) - author_link.allow_tags = True - - def get_readonly_fields(self, request, obj=None): - fields = () - if obj is not None: - fields = self.readonly_fields - return fields - - -class CategoryAdmin(admin.ModelAdmin): - exclude = ('posts', ) - - -admin.site.register(Post, PostAdmin) -admin.site.register(Category, CategoryAdmin) diff --git a/resources/session08/mysite/myblog/fixtures/myblog_test_fixture.json b/resources/session08/mysite/myblog/fixtures/myblog_test_fixture.json deleted file mode 100644 index 592dea17..00000000 --- a/resources/session08/mysite/myblog/fixtures/myblog_test_fixture.json +++ /dev/null @@ -1,38 +0,0 @@ -[ -{ - "pk": 1, - "model": "auth.user", - "fields": { - "username": "admin", - "first_name": "Mr.", - "last_name": "Administrator", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "admin@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -}, -{ - "pk": 2, - "model": "auth.user", - "fields": { - "username": "noname", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "noname@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -} -] diff --git a/resources/session08/mysite/myblog/forms.py b/resources/session08/mysite/myblog/forms.py deleted file mode 100644 index 541dcd3a..00000000 --- a/resources/session08/mysite/myblog/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import forms -from myblog.models import Post - -class PostForm(forms.ModelForm): - - class Meta: - model = Post - fields = ('title', 'text', 'author') - widgets = { - 'author': forms.HiddenInput(), - } \ No newline at end of file diff --git a/resources/session08/mysite/myblog/views.py b/resources/session08/mysite/myblog/views.py deleted file mode 100644 index 3768595e..00000000 --- a/resources/session08/mysite/myblog/views.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.http import HttpResponse, Http404 -from django.http import HttpResponseRedirect -from django.shortcuts import render -from django.core.exceptions import PermissionDenied -from django.core.urlresolvers import reverse -from django.contrib import messages - -from myblog.models import Post -from myblog.forms import PostForm - - -def stub_view(request, *args, **kwargs): - body = "Stub View\n\n" - if args: - body += "Args:\n" - body += "\n".join(["\t%s" % a for a in args]) - if kwargs: - body += "Kwargs:\n" - body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) - return HttpResponse(body, content_type="text/plain") - - -def list_view(request): - published = Post.objects.exclude(published_date__exact=None) - posts = published.order_by('-published_date') - context = {'posts': posts} - return render(request, 'list.html', context) - - -def detail_view(request, post_id): - published = Post.objects.exclude(published_date__exact=None) - try: - post = published.get(pk=post_id) - except Post.DoesNotExist: - raise Http404 - context = {'post': post} - return render(request, 'detail.html', context) - - -def add_view(request): - user = request.user - if not user.is_authenticated: - raise PermissionDenied - if request.method == 'POST': - form = PostForm(request.POST) - if form.is_valid: - post = form.save() - msg = "post '%s' saved" % post - messages.add_message(request, messages.INFO, msg) - return HttpResponseRedirect(reverse('blog_index')) - else: - messages.add_message("please fix the errors below") - else: - initial = {'author': user} - form = PostForm(initial=initial) - - context = {'form': form} - return render(request, 'add.html', context) diff --git a/resources/session08/mysite/mysite/settings.py b/resources/session08/mysite/mysite/settings.py deleted file mode 100644 index 8c2a28a9..00000000 --- a/resources/session08/mysite/mysite/settings.py +++ /dev/null @@ -1,162 +0,0 @@ -# Django settings for mysite project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mysite.db', - # The following settings are not used with sqlite3: - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', - } -} - -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts -ALLOWED_HOSTS = [] - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# In a Windows environment this must be set to your system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale. -USE_L10N = True - -# If you set this to False, Django will not use timezone-aware datetimes. -USE_TZ = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/var/www/example.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://example.com/media/", "http://media.example.com/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/var/www/example.com/static/" -STATIC_ROOT = '' - -# URL prefix for static files. -# Example: "http://example.com/static/", "http://static.example.com/" -STATIC_URL = '/static/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '(g!gi6orza2nez)tf-xj_g%5g3!+pmdhs15xjgat)0g!10qku*' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'mysite.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'mysite.wsgi.application' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - '/Users/cewing/projects/training/uw_pce/testme/training.python_web/resources/session08/mysite/mysite/templates' -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'myblog', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - - -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/' \ No newline at end of file diff --git a/resources/session08/mysite/mysite/wsgi.py b/resources/session08/mysite/mysite/wsgi.py deleted file mode 100644 index 34e900eb..00000000 --- a/resources/session08/mysite/mysite/wsgi.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -WSGI config for mysite project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. - -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. - -""" -import os - -# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks -# if running multiple sites in the same mod_wsgi process. To fix this, use -# mod_wsgi daemon mode with each site in its own daemon process, or use -# os.environ["DJANGO_SETTINGS_MODULE"] = "mysite.settings" -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() - -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) diff --git a/resources/session08/mysite/manage.py b/resources/session08/mysite_stage_1/manage.py old mode 100644 new mode 100755 similarity index 100% rename from resources/session08/mysite/manage.py rename to resources/session08/mysite_stage_1/manage.py diff --git a/resources/session08/mysite/myblog/__init__.py b/resources/session08/mysite_stage_1/myblog/__init__.py similarity index 100% rename from resources/session08/mysite/myblog/__init__.py rename to resources/session08/mysite_stage_1/myblog/__init__.py diff --git a/resources/session08/mysite_stage_1/myblog/admin.py b/resources/session08/mysite_stage_1/myblog/admin.py new file mode 100644 index 00000000..310e7294 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from myblog.models import Category +from myblog.models import Post + + +admin.site.register(Category) +admin.site.register(Post) diff --git a/resources/session08/mysite_stage_1/myblog/apps.py b/resources/session08/mysite_stage_1/myblog/apps.py new file mode 100644 index 00000000..5e29c8d9 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MyblogConfig(AppConfig): + name = 'myblog' diff --git a/resources/session08/mysite_stage_1/myblog/fixtures/myblog_test_fixture.json b/resources/session08/mysite_stage_1/myblog/fixtures/myblog_test_fixture.json new file mode 100644 index 00000000..bf5269e9 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/fixtures/myblog_test_fixture.json @@ -0,0 +1,38 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "Mr.", + "last_name": "Administrator", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "admin@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + }, + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "noname", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "noname@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + } +] diff --git a/resources/session08/mysite_stage_1/myblog/migrations/0001_initial.py b/resources/session08/mysite_stage_1/myblog/migrations/0001_initial.py new file mode 100644 index 00000000..18d659f1 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 19:13 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=128)), + ('text', models.TextField(blank=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('modified_date', models.DateTimeField(auto_now=True)), + ('published_date', models.DateTimeField(blank=True, null=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/resources/session08/mysite_stage_1/myblog/migrations/0002_category.py b/resources/session08/mysite_stage_1/myblog/migrations/0002_category.py new file mode 100644 index 00000000..218b1052 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/migrations/0002_category.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 21:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('myblog', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True)), + ('posts', models.ManyToManyField(blank=True, related_name='categories', to='myblog.Post')), + ], + ), + ] diff --git a/resources/session08/mysite/mysite/__init__.py b/resources/session08/mysite_stage_1/myblog/migrations/__init__.py similarity index 100% rename from resources/session08/mysite/mysite/__init__.py rename to resources/session08/mysite_stage_1/myblog/migrations/__init__.py diff --git a/resources/session08/mysite/myblog/models.py b/resources/session08/mysite_stage_1/myblog/models.py similarity index 63% rename from resources/session08/mysite/myblog/models.py rename to resources/session08/mysite_stage_1/myblog/models.py index 44ed1e0f..cef1beac 100644 --- a/resources/session08/mysite/myblog/models.py +++ b/resources/session08/mysite_stage_1/myblog/models.py @@ -10,27 +10,18 @@ class Post(models.Model): modified_date = models.DateTimeField(auto_now=True) published_date = models.DateTimeField(blank=True, null=True) - def __unicode__(self): + def __str__(self): return self.title - def author_name(self): - raw_name = "%s %s" % (self.author.first_name, - self.author.last_name) - name = raw_name.strip() - if not name: - name = self.author.username - return name - class Category(models.Model): name = models.CharField(max_length=128) description = models.TextField(blank=True) - posts = models.ManyToManyField(Post, + posts = models.ManyToManyField( + Post, blank=True, - null=True, related_name='categories' ) - def __unicode__(self): + def __str__(self): return self.name - diff --git a/resources/session08/mysite_stage_1/myblog/tests.py b/resources/session08/mysite_stage_1/myblog/tests.py new file mode 100644 index 00000000..308dd6f1 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/tests.py @@ -0,0 +1,27 @@ +from django.test import TestCase +from django.contrib.auth.models import User + +from myblog.models import Category +from myblog.models import Post + + +class PostTestCase(TestCase): + fixtures = ['myblog_test_fixture.json'] + + def setUp(self): + self.user = User.objects.get(pk=1) + + def test_string_representation(self): + expected = "This is a title" + p1 = Post(title=expected) + actual = str(p1) + self.assertEqual(expected, actual) + + +class CategoryTestCase(TestCase): + + def test_string_representation(self): + expected = "A Category" + c1 = Category(name=expected) + actual = str(c1) + self.assertEqual(expected, actual) diff --git a/resources/session08/mysite_stage_1/myblog/views.py b/resources/session08/mysite_stage_1/myblog/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/resources/session08/mysite_stage_1/myblog/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/resources/session08/mysite_stage_1/mysite/__init__.py b/resources/session08/mysite_stage_1/mysite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_1/mysite/settings.py b/resources/session08/mysite_stage_1/mysite/settings.py new file mode 100644 index 00000000..3753c661 --- /dev/null +++ b/resources/session08/mysite_stage_1/mysite/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.9. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'i=n^tc%@@gq#8ev6dlymy9+-%@^f!q54sjf0rvikt_k5bl(t1=' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myblog', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/resources/session08/mysite_stage_1/mysite/urls.py b/resources/session08/mysite_stage_1/mysite/urls.py new file mode 100644 index 00000000..40cce1f4 --- /dev/null +++ b/resources/session08/mysite_stage_1/mysite/urls.py @@ -0,0 +1,22 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Import the include() function: from django.conf.urls import url, include + 3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/resources/session08/mysite_stage_1/mysite/wsgi.py b/resources/session08/mysite_stage_1/mysite/wsgi.py new file mode 100644 index 00000000..328bae0f --- /dev/null +++ b/resources/session08/mysite_stage_1/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/resources/session08/mysite_stage_2/manage.py b/resources/session08/mysite_stage_2/manage.py new file mode 100755 index 00000000..8a50ec04 --- /dev/null +++ b/resources/session08/mysite_stage_2/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/resources/session08/mysite_stage_2/myblog/__init__.py b/resources/session08/mysite_stage_2/myblog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_2/myblog/admin.py b/resources/session08/mysite_stage_2/myblog/admin.py new file mode 100644 index 00000000..310e7294 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from myblog.models import Category +from myblog.models import Post + + +admin.site.register(Category) +admin.site.register(Post) diff --git a/resources/session08/mysite_stage_2/myblog/apps.py b/resources/session08/mysite_stage_2/myblog/apps.py new file mode 100644 index 00000000..5e29c8d9 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MyblogConfig(AppConfig): + name = 'myblog' diff --git a/resources/session08/mysite_stage_2/myblog/fixtures/myblog_test_fixture.json b/resources/session08/mysite_stage_2/myblog/fixtures/myblog_test_fixture.json new file mode 100644 index 00000000..bf5269e9 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/fixtures/myblog_test_fixture.json @@ -0,0 +1,38 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "Mr.", + "last_name": "Administrator", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "admin@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + }, + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "noname", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "noname@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + } +] diff --git a/resources/session08/mysite_stage_2/myblog/migrations/0001_initial.py b/resources/session08/mysite_stage_2/myblog/migrations/0001_initial.py new file mode 100644 index 00000000..18d659f1 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 19:13 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=128)), + ('text', models.TextField(blank=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('modified_date', models.DateTimeField(auto_now=True)), + ('published_date', models.DateTimeField(blank=True, null=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/resources/session08/mysite_stage_2/myblog/migrations/0002_category.py b/resources/session08/mysite_stage_2/myblog/migrations/0002_category.py new file mode 100644 index 00000000..218b1052 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/migrations/0002_category.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 21:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('myblog', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True)), + ('posts', models.ManyToManyField(blank=True, related_name='categories', to='myblog.Post')), + ], + ), + ] diff --git a/resources/session08/mysite_stage_2/myblog/migrations/__init__.py b/resources/session08/mysite_stage_2/myblog/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_2/myblog/models.py b/resources/session08/mysite_stage_2/myblog/models.py new file mode 100644 index 00000000..cef1beac --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Post(models.Model): + title = models.CharField(max_length=128) + text = models.TextField(blank=True) + author = models.ForeignKey(User) + created_date = models.DateTimeField(auto_now_add=True) + modified_date = models.DateTimeField(auto_now=True) + published_date = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return self.title + + +class Category(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(blank=True) + posts = models.ManyToManyField( + Post, + blank=True, + related_name='categories' + ) + + def __str__(self): + return self.name diff --git a/resources/session08/mysite_stage_2/myblog/templates/list.html b/resources/session08/mysite_stage_2/myblog/templates/list.html new file mode 100644 index 00000000..e21bc51c --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/templates/list.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +

Recent Posts

+ + {% comment %} here is where the query happens {% endcomment %} + {% for post in posts %} +
+

{{ post }}

+ +
+ {{ post.text }} +
+
    + {% for category in post.categories.all %} +
  • {{ category }}
  • + {% endfor %} +
+
+ {% endfor %} +{% endblock %} diff --git a/resources/session08/mysite_stage_2/myblog/tests.py b/resources/session08/mysite_stage_2/myblog/tests.py new file mode 100644 index 00000000..d9418619 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/tests.py @@ -0,0 +1,60 @@ +import datetime +from django.contrib.auth.models import User +from django.test import TestCase +from django.utils.timezone import utc + +from myblog.models import Category +from myblog.models import Post + + +class PostTestCase(TestCase): + fixtures = ['myblog_test_fixture.json'] + + def setUp(self): + self.user = User.objects.get(pk=1) + + def test_string_representation(self): + expected = "This is a title" + p1 = Post(title=expected) + actual = str(p1) + self.assertEqual(expected, actual) + + +class CategoryTestCase(TestCase): + + def test_string_representation(self): + expected = "A Category" + c1 = Category(name=expected) + actual = str(c1) + self.assertEqual(expected, actual) + + +class FrontEndTestCase(TestCase): + """test views provided in the front-end""" + fixtures = ['myblog_test_fixture.json', ] + + def setUp(self): + self.now = datetime.datetime.utcnow().replace(tzinfo=utc) + self.timedelta = datetime.timedelta(15) + author = User.objects.get(pk=1) + for count in range(1, 11): + post = Post(title="Post %d Title" % count, + text="foo", + author=author) + if count < 6: + # publish the first five posts + pubdate = self.now - self.timedelta * count + post.published_date = pubdate + post.save() + + def test_list_only_published(self): + resp = self.client.get('/') + # the content of the rendered response is always a bytestring + resp_text = resp.content.decode(resp.charset) + self.assertTrue("Recent Posts" in resp_text) + for count in range(1, 11): + title = "Post %d Title" % count + if count < 6: + self.assertContains(resp, title, count=1) + else: + self.assertNotContains(resp, title) diff --git a/resources/session08/mysite_stage_2/myblog/urls.py b/resources/session08/mysite_stage_2/myblog/urls.py new file mode 100644 index 00000000..8428ffe4 --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import url + +from myblog.views import stub_view +from myblog.views import list_view + + +urlpatterns = [ + url(r'^$', + list_view, + name="blog_index"), + url(r'^posts/(?P\d+)/$', + stub_view, + name='blog_detail'), +] diff --git a/resources/session08/mysite_stage_2/myblog/views.py b/resources/session08/mysite_stage_2/myblog/views.py new file mode 100644 index 00000000..ab45c18b --- /dev/null +++ b/resources/session08/mysite_stage_2/myblog/views.py @@ -0,0 +1,23 @@ +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.shortcuts import render +from django.template import RequestContext, loader + +from myblog.models import Post + + +def stub_view(request, *args, **kwargs): + body = "Stub View\n\n" + if args: + body += "Args:\n" + body += "\n".join(["\t%s" % a for a in args]) + if kwargs: + body += "Kwargs:\n" + body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) + return HttpResponse(body, content_type="text/plain") + + +def list_view(request): + published = Post.objects.exclude(published_date__exact=None) + posts = published.order_by('-published_date') + context = {'posts': posts} + return render(request, 'list.html', context) diff --git a/resources/session08/mysite_stage_2/mysite/__init__.py b/resources/session08/mysite_stage_2/mysite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_2/mysite/settings.py b/resources/session08/mysite_stage_2/mysite/settings.py new file mode 100644 index 00000000..e81fb826 --- /dev/null +++ b/resources/session08/mysite_stage_2/mysite/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.9. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'i=n^tc%@@gq#8ev6dlymy9+-%@^f!q54sjf0rvikt_k5bl(t1=' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myblog', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'mysite/templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/resources/session08/mysite_stage_2/mysite/templates/base.html b/resources/session08/mysite_stage_2/mysite/templates/base.html new file mode 100644 index 00000000..2eacafa0 --- /dev/null +++ b/resources/session08/mysite_stage_2/mysite/templates/base.html @@ -0,0 +1,15 @@ + + + + My Django Blog + + +
+
+ {% block content %} + [content will go here] + {% endblock %} +
+
+ + diff --git a/resources/session08/mysite_stage_2/mysite/urls.py b/resources/session08/mysite_stage_2/mysite/urls.py new file mode 100644 index 00000000..bf31d5d5 --- /dev/null +++ b/resources/session08/mysite_stage_2/mysite/urls.py @@ -0,0 +1,24 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Import the include() function: from django.conf.urls import url, include + 3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import url +from django.conf.urls import include +from django.contrib import admin + +urlpatterns = [ + url(r'^', include('myblog.urls')), + url(r'^admin/', admin.site.urls), +] diff --git a/resources/session08/mysite_stage_2/mysite/wsgi.py b/resources/session08/mysite_stage_2/mysite/wsgi.py new file mode 100644 index 00000000..328bae0f --- /dev/null +++ b/resources/session08/mysite_stage_2/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/resources/session08/mysite_stage_3/manage.py b/resources/session08/mysite_stage_3/manage.py new file mode 100755 index 00000000..8a50ec04 --- /dev/null +++ b/resources/session08/mysite_stage_3/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/resources/session08/mysite_stage_3/myblog/__init__.py b/resources/session08/mysite_stage_3/myblog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_3/myblog/admin.py b/resources/session08/mysite_stage_3/myblog/admin.py new file mode 100644 index 00000000..310e7294 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from myblog.models import Category +from myblog.models import Post + + +admin.site.register(Category) +admin.site.register(Post) diff --git a/resources/session08/mysite_stage_3/myblog/apps.py b/resources/session08/mysite_stage_3/myblog/apps.py new file mode 100644 index 00000000..5e29c8d9 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MyblogConfig(AppConfig): + name = 'myblog' diff --git a/resources/session08/mysite_stage_3/myblog/fixtures/myblog_test_fixture.json b/resources/session08/mysite_stage_3/myblog/fixtures/myblog_test_fixture.json new file mode 100644 index 00000000..bf5269e9 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/fixtures/myblog_test_fixture.json @@ -0,0 +1,38 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "Mr.", + "last_name": "Administrator", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "admin@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + }, + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "noname", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "noname@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + } +] diff --git a/resources/session08/mysite_stage_3/myblog/migrations/0001_initial.py b/resources/session08/mysite_stage_3/myblog/migrations/0001_initial.py new file mode 100644 index 00000000..18d659f1 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 19:13 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=128)), + ('text', models.TextField(blank=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('modified_date', models.DateTimeField(auto_now=True)), + ('published_date', models.DateTimeField(blank=True, null=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/resources/session08/mysite_stage_3/myblog/migrations/0002_category.py b/resources/session08/mysite_stage_3/myblog/migrations/0002_category.py new file mode 100644 index 00000000..218b1052 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/migrations/0002_category.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-31 21:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('myblog', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True)), + ('posts', models.ManyToManyField(blank=True, related_name='categories', to='myblog.Post')), + ], + ), + ] diff --git a/resources/session08/mysite_stage_3/myblog/migrations/__init__.py b/resources/session08/mysite_stage_3/myblog/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_3/myblog/models.py b/resources/session08/mysite_stage_3/myblog/models.py new file mode 100644 index 00000000..cef1beac --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Post(models.Model): + title = models.CharField(max_length=128) + text = models.TextField(blank=True) + author = models.ForeignKey(User) + created_date = models.DateTimeField(auto_now_add=True) + modified_date = models.DateTimeField(auto_now=True) + published_date = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return self.title + + +class Category(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(blank=True) + posts = models.ManyToManyField( + Post, + blank=True, + related_name='categories' + ) + + def __str__(self): + return self.name diff --git a/resources/session08/mysite/myblog/static/django_blog.css b/resources/session08/mysite_stage_3/myblog/static/django_blog.css similarity index 99% rename from resources/session08/mysite/myblog/static/django_blog.css rename to resources/session08/mysite_stage_3/myblog/static/django_blog.css index 64560dc0..45a882de 100644 --- a/resources/session08/mysite/myblog/static/django_blog.css +++ b/resources/session08/mysite_stage_3/myblog/static/django_blog.css @@ -71,4 +71,4 @@ ul.categories { } ul.categories li { display: inline; -} \ No newline at end of file +} diff --git a/resources/session08/mysite/myblog/templates/detail.html b/resources/session08/mysite_stage_3/myblog/templates/detail.html similarity index 96% rename from resources/session08/mysite/myblog/templates/detail.html rename to resources/session08/mysite_stage_3/myblog/templates/detail.html index 34e1b356..cd0322ff 100644 --- a/resources/session08/mysite/myblog/templates/detail.html +++ b/resources/session08/mysite_stage_3/myblog/templates/detail.html @@ -14,4 +14,4 @@

{{ post }}

  • {{ category }}
  • {% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/resources/session08/mysite/myblog/templates/list.html b/resources/session08/mysite_stage_3/myblog/templates/list.html similarity index 86% rename from resources/session08/mysite/myblog/templates/list.html rename to resources/session08/mysite_stage_3/myblog/templates/list.html index b2299169..88920817 100644 --- a/resources/session08/mysite/myblog/templates/list.html +++ b/resources/session08/mysite_stage_3/myblog/templates/list.html @@ -2,7 +2,8 @@ {% block content %}

    Recent Posts

    - + + {% comment %} here is where the query happens {% endcomment %} {% for post in posts %}

    @@ -21,4 +22,4 @@

    {% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/resources/session08/mysite/myblog/tests.py b/resources/session08/mysite_stage_3/myblog/tests.py similarity index 51% rename from resources/session08/mysite/myblog/tests.py rename to resources/session08/mysite_stage_3/myblog/tests.py index a51abbe7..c4f547bd 100644 --- a/resources/session08/mysite/myblog/tests.py +++ b/resources/session08/mysite_stage_3/myblog/tests.py @@ -1,80 +1,43 @@ import datetime -from django.test import TestCase from django.contrib.auth.models import User -from django.contrib.admin.sites import AdminSite +from django.test import TestCase from django.utils.timezone import utc -from myblog.models import Post from myblog.models import Category -from myblog.admin import PostAdmin +from myblog.models import Post class PostTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] + fixtures = ['myblog_test_fixture.json'] def setUp(self): self.user = User.objects.get(pk=1) - def test_unicode(self): + def test_string_representation(self): expected = "This is a title" p1 = Post(title=expected) - actual = unicode(p1) + actual = str(p1) self.assertEqual(expected, actual) - def test_author_name(self): - for author in User.objects.all(): - fn, ln, un = (author.first_name, author.last_name, - author.username) - author_name = Post(author=author).author_name() - if not (fn and ln): - self.assertEqual(author_name, un) - else: - if fn: - self.assertTrue(fn in author_name) - if ln: - self.assertTrue(ln in author_name) - class CategoryTestCase(TestCase): - def test_unicode(self): + def test_string_representation(self): expected = "A Category" c1 = Category(name=expected) - actual = unicode(c1) + actual = str(c1) self.assertEqual(expected, actual) -class PostAdminTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] - - def setUp(self): - admin = AdminSite() - self.ma = PostAdmin(Post, admin) - for author in User.objects.all(): - title = "%s's title" % author.username - post = Post(title=title, author=author) - post.save() - self.client.login(username='admin', password='secret') - - def test_author_link(self): - expected_link_path = '/admin/auth/user/%s' - for post in Post.objects.all(): - expected = expected_link_path % post.author.pk - actual = self.ma.author_link(post) - self.assertTrue(expected in actual) - - class FrontEndTestCase(TestCase): """test views provided in the front-end""" fixtures = ['myblog_test_fixture.json', ] - + def setUp(self): self.now = datetime.datetime.utcnow().replace(tzinfo=utc) self.timedelta = datetime.timedelta(15) author = User.objects.get(pk=1) - self.category = Category(name='A Category') - self.category.save() - for count in range(1,11): + for count in range(1, 11): post = Post(title="Post %d Title" % count, text="foo", author=author) @@ -83,14 +46,13 @@ def setUp(self): pubdate = self.now - self.timedelta * count post.published_date = pubdate post.save() - if bool(count & 1): - # put odd items in category: - self.category.posts.add(post) def test_list_only_published(self): resp = self.client.get('/') - self.assertTrue("Recent Posts" in resp.content) - for count in range(1,11): + # the content of the rendered response is always a bytestring + resp_text = resp.content.decode(resp.charset) + self.assertTrue("Recent Posts" in resp_text) + for count in range(1, 11): title = "Post %d Title" % count if count < 6: self.assertContains(resp, title, count=1) @@ -98,7 +60,7 @@ def test_list_only_published(self): self.assertNotContains(resp, title) def test_details_only_published(self): - for count in range(1,11): + for count in range(1, 11): title = "Post %d Title" % count post = Post.objects.get(title=title) resp = self.client.get('/posts/%d/' % post.pk) diff --git a/resources/session08/mysite_stage_3/myblog/urls.py b/resources/session08/mysite_stage_3/myblog/urls.py new file mode 100644 index 00000000..5caacf17 --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import url + +from myblog.views import stub_view +from myblog.views import list_view +from myblog.views import detail_view + + +urlpatterns = [ + url(r'^$', + list_view, + name="blog_index"), + url(r'^posts/(?P\d+)/$', + detail_view, + name='blog_detail'), +] diff --git a/resources/session08/mysite_stage_3/myblog/views.py b/resources/session08/mysite_stage_3/myblog/views.py new file mode 100644 index 00000000..c1e8c41a --- /dev/null +++ b/resources/session08/mysite_stage_3/myblog/views.py @@ -0,0 +1,33 @@ +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.shortcuts import render +from django.template import RequestContext, loader + +from myblog.models import Post + + +def stub_view(request, *args, **kwargs): + body = "Stub View\n\n" + if args: + body += "Args:\n" + body += "\n".join(["\t%s" % a for a in args]) + if kwargs: + body += "Kwargs:\n" + body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) + return HttpResponse(body, content_type="text/plain") + + +def list_view(request): + published = Post.objects.exclude(published_date__exact=None) + posts = published.order_by('-published_date') + context = {'posts': posts} + return render(request, 'list.html', context) + + +def detail_view(request, post_id): + published = Post.objects.exclude(published_date__exact=None) + try: + post = published.get(pk=post_id) + except Post.DoesNotExist: + raise Http404 + context = {'post': post} + return render(request, 'detail.html', context) diff --git a/resources/session08/mysite_stage_3/mysite/__init__.py b/resources/session08/mysite_stage_3/mysite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session08/mysite_stage_3/mysite/settings.py b/resources/session08/mysite_stage_3/mysite/settings.py new file mode 100644 index 00000000..3fd7eb4c --- /dev/null +++ b/resources/session08/mysite_stage_3/mysite/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.9. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'i=n^tc%@@gq#8ev6dlymy9+-%@^f!q54sjf0rvikt_k5bl(t1=' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myblog', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'mysite/templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' + +LOGIN_URL = '/login/' +LOGIN_REDIRECT_URL = '/' diff --git a/resources/session08/mysite/mysite/templates/base.html b/resources/session08/mysite_stage_3/mysite/templates/base.html similarity index 54% rename from resources/session08/mysite/mysite/templates/base.html rename to resources/session08/mysite_stage_3/mysite/templates/base.html index 5168b320..1529aead 100644 --- a/resources/session08/mysite/mysite/templates/base.html +++ b/resources/session08/mysite_stage_3/mysite/templates/base.html @@ -1,15 +1,15 @@ +{% load staticfiles %} My Django Blog - +
    - {% if messages %} -
    - {% for message in messages %} -

    {{ message }}

    - {% endfor %} -
    - {% endif %}
    {% block content %} [content will go here] @@ -31,4 +24,4 @@
    - \ No newline at end of file + diff --git a/resources/session08/mysite/mysite/templates/login.html b/resources/session08/mysite_stage_3/mysite/templates/login.html similarity index 92% rename from resources/session08/mysite/mysite/templates/login.html rename to resources/session08/mysite_stage_3/mysite/templates/login.html index e1a56ee8..1566d0f7 100644 --- a/resources/session08/mysite/mysite/templates/login.html +++ b/resources/session08/mysite_stage_3/mysite/templates/login.html @@ -6,4 +6,4 @@

    My Blog Login

    {{ form.as_p }}

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/resources/session08/mysite_stage_3/mysite/urls.py b/resources/session08/mysite_stage_3/mysite/urls.py new file mode 100644 index 00000000..91f7819c --- /dev/null +++ b/resources/session08/mysite_stage_3/mysite/urls.py @@ -0,0 +1,33 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Import the include() function: from django.conf.urls import url, include + 3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import url +from django.conf.urls import include +from django.contrib import admin +from django.contrib.auth.views import login, logout + +urlpatterns = [ + url(r'^', include('myblog.urls')), + url(r'^login/$', + login, + {'template_name': 'login.html'}, + name="login"), + url(r'^logout/$', + logout, + {'next_page': '/'}, + name="logout"), + url(r'^admin/', admin.site.urls), +] diff --git a/resources/session08/mysite_stage_3/mysite/wsgi.py b/resources/session08/mysite_stage_3/mysite/wsgi.py new file mode 100644 index 00000000..328bae0f --- /dev/null +++ b/resources/session08/mysite_stage_3/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/resources/session09/mysite/manage.py b/resources/session09/mysite/manage.py new file mode 100755 index 00000000..8a50ec04 --- /dev/null +++ b/resources/session09/mysite/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/resources/session09/mysite/myblog/__init__.py b/resources/session09/mysite/myblog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session09/mysite/myblog/admin.py b/resources/session09/mysite/myblog/admin.py new file mode 100644 index 00000000..d772219e --- /dev/null +++ b/resources/session09/mysite/myblog/admin.py @@ -0,0 +1,44 @@ +import datetime +from django.contrib import admin +from django.core.urlresolvers import reverse +from myblog.models import Post, Category + + +class CategorizationInline(admin.TabularInline): + model = Category.posts.through + + +def make_published(modeladmin, request, queryset): + now = datetime.datetime.now() + queryset.update(published_date=now) +make_published.short_description = "Set publication date for selected posts" + + +class PostAdmin(admin.ModelAdmin): + inlines = [ + CategorizationInline, + ] + list_display = ( + '__unicode__', 'author_for_admin', 'created_date', 'modified_date', 'published_date' + ) + readonly_fields = ( + 'created_date', 'modified_date', + ) + actions = [make_published, ] + + def author_for_admin(self, obj): + author = obj.author + url = reverse('admin:auth_user_change', args=(author.pk,)) + name = author.get_full_name() or author.username + link = '{}'.format(url, name) + return link + author_for_admin.short_description = 'Author' + author_for_admin.allow_tags = True + + +class CategoryAdmin(admin.ModelAdmin): + exclude = ('posts', ) + + +admin.site.register(Post, PostAdmin) +admin.site.register(Category, CategoryAdmin) diff --git a/resources/session09/mysite/myblog/fixtures/myblog_test_fixture.json b/resources/session09/mysite/myblog/fixtures/myblog_test_fixture.json new file mode 100644 index 00000000..bf5269e9 --- /dev/null +++ b/resources/session09/mysite/myblog/fixtures/myblog_test_fixture.json @@ -0,0 +1,38 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "Mr.", + "last_name": "Administrator", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "admin@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + }, + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "noname", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2013-05-24T05:35:58.628Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", + "email": "noname@example.com", + "date_joined": "2013-05-24T05:35:58.628Z" + } + } +] diff --git a/resources/session09/mysite/myblog/migrations/0001_initial.py b/resources/session09/mysite/myblog/migrations/0001_initial.py new file mode 100644 index 00000000..9772455c --- /dev/null +++ b/resources/session09/mysite/myblog/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=128)), + ('text', models.TextField(blank=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('modified_date', models.DateTimeField(auto_now=True)), + ('published_date', models.DateTimeField(null=True, blank=True)), + ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/resources/session09/mysite/myblog/migrations/0002_category.py b/resources/session09/mysite/myblog/migrations/0002_category.py new file mode 100644 index 00000000..cd06d71e --- /dev/null +++ b/resources/session09/mysite/myblog/migrations/0002_category.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('myblog', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True)), + ('posts', models.ManyToManyField(related_name='categories', null=True, to='myblog.Post', blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/resources/session09/mysite/myblog/migrations/__init__.py b/resources/session09/mysite/myblog/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session09/mysite/myblog/models.py b/resources/session09/mysite/myblog/models.py new file mode 100644 index 00000000..8b5f59cf --- /dev/null +++ b/resources/session09/mysite/myblog/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Post(models.Model): + title = models.CharField(max_length=128) + text = models.TextField(blank=True) + author = models.ForeignKey(User) + created_date = models.DateTimeField(auto_now_add=True) + modified_date = models.DateTimeField(auto_now=True) + published_date = models.DateTimeField(blank=True, null=True) + + def __unicode__(self): + return self.title + + +class Category(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(blank=True) + posts = models.ManyToManyField(Post, blank=True, null=True, + related_name='categories') + + class Meta: + verbose_name_plural = 'Categories' + + def __unicode__(self): + return self.name diff --git a/resources/session09/mysite/myblog/static/django_blog.css b/resources/session09/mysite/myblog/static/django_blog.css new file mode 100644 index 00000000..45a882de --- /dev/null +++ b/resources/session09/mysite/myblog/static/django_blog.css @@ -0,0 +1,74 @@ +body { + background-color: #eee; + color: #111; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + margin:0; + padding:0; +} +#container { + margin:0; + padding:0; + margin-top: 0px; +} +#header { + background-color: #333; + border-botton: 1px solid #111; + margin:0; + padding:0; +} +#control-bar { + margin: 0em 0em 1em; + list-style: none; + list-style-type: none; + text-align: right; + color: #eee; + font-size: 80%; + padding-bottom: 0.4em; +} +#control-bar li { + display: inline-block; +} +#control-bar li a { + color: #eee; + padding: 0.5em; + text-decoration: none; +} +#control-bar li a:hover { + color: #cce; +} +#content { + margin: 0em 1em 1em; +} + +ul#entries { + list-style: none; + list-style-type: none; +} +div.entry { + margin-right: 2em; + margin-top: 1em; + border-top: 1px solid #cecece; +} +ul#entries li:first-child div.entry { + border-top: none; + margin-top: 0em; +} +div.entry-body { + margin-left: 2em; +} +.notification { + float: right; + text-align: center; + width: 25%; + padding: 1em; +} +.info { + background-color: #aae; +} +ul.categories { + list-style: none; + list-style-type: none; +} +ul.categories li { + display: inline; +} diff --git a/resources/session09/mysite/myblog/templates/detail.html b/resources/session09/mysite/myblog/templates/detail.html new file mode 100644 index 00000000..cd0322ff --- /dev/null +++ b/resources/session09/mysite/myblog/templates/detail.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +Home +

    {{ post }}

    + +
    + {{ post.text }} +
    +
      + {% for category in post.categories.all %} +
    • {{ category }}
    • + {% endfor %} +
    +{% endblock %} diff --git a/resources/session09/mysite/myblog/templates/list.html b/resources/session09/mysite/myblog/templates/list.html new file mode 100644 index 00000000..88920817 --- /dev/null +++ b/resources/session09/mysite/myblog/templates/list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

    Recent Posts

    + + {% comment %} here is where the query happens {% endcomment %} + {% for post in posts %} +
    +

    + {{ post }} +

    + +
    + {{ post.text }} +
    +
      + {% for category in post.categories.all %} +
    • {{ category }}
    • + {% endfor %} +
    +
    + {% endfor %} +{% endblock %} diff --git a/resources/session08/blog_view_tests.py b/resources/session09/mysite/myblog/tests.py similarity index 64% rename from resources/session08/blog_view_tests.py rename to resources/session09/mysite/myblog/tests.py index 783a9ce7..1f967b24 100644 --- a/resources/session08/blog_view_tests.py +++ b/resources/session09/mysite/myblog/tests.py @@ -1,20 +1,41 @@ import datetime from django.test import TestCase +from django.contrib.auth.models import User from django.utils.timezone import utc -from djagno.contrib.auth.models import User +from myblog.models import Post, Category -from myblog.models import Post + +class PostTestCase(TestCase): + fixtures = ['myblog_test_fixture.json', ] + + def setUp(self): + self.user = User.objects.get(pk=1) + + def test_unicode(self): + expected = u"This is a title" + p1 = Post(title=expected) + actual = unicode(p1) + self.assertEqual(expected, actual) + + +class CategoryTestCase(TestCase): + + def test_unicode(self): + expected = "A Category" + c1 = Category(name=expected) + actual = unicode(c1) + self.assertEqual(expected, actual) class FrontEndTestCase(TestCase): """test views provided in the front-end""" fixtures = ['myblog_test_fixture.json', ] - + def setUp(self): self.now = datetime.datetime.utcnow().replace(tzinfo=utc) self.timedelta = datetime.timedelta(15) author = User.objects.get(pk=1) - for count in range(1,11): + for count in range(1, 11): post = Post(title="Post %d Title" % count, text="foo", author=author) @@ -27,7 +48,7 @@ def setUp(self): def test_list_only_published(self): resp = self.client.get('/') self.assertTrue("Recent Posts" in resp.content) - for count in range(1,11): + for count in range(1, 11): title = "Post %d Title" % count if count < 6: self.assertContains(resp, title, count=1) @@ -35,7 +56,7 @@ def test_list_only_published(self): self.assertNotContains(resp, title) def test_details_only_published(self): - for count in range(1,11): + for count in range(1, 11): title = "Post %d Title" % count post = Post.objects.get(title=title) resp = self.client.get('/posts/%d/' % post.pk) @@ -43,4 +64,4 @@ def test_details_only_published(self): self.assertEqual(resp.status_code, 200) self.assertContains(resp, title) else: - self.assertEqual(resp.status_code, 404) \ No newline at end of file + self.assertEqual(resp.status_code, 404) diff --git a/resources/session08/mysite/myblog/urls.py b/resources/session09/mysite/myblog/urls.py similarity index 65% rename from resources/session08/mysite/myblog/urls.py rename to resources/session09/mysite/myblog/urls.py index 3c72e6bc..d31e75c9 100644 --- a/resources/session08/mysite/myblog/urls.py +++ b/resources/session09/mysite/myblog/urls.py @@ -1,14 +1,12 @@ from django.conf.urls import patterns, url -urlpatterns = patterns('myblog.views', +urlpatterns = patterns( + 'myblog.views', url(r'^$', 'list_view', name="blog_index"), url(r'^posts/(?P\d+)/$', 'detail_view', name="blog_detail"), - url(r'^add/$', - 'add_view', - name="add_post"), ) diff --git a/resources/session09/mysite/myblog/views.py b/resources/session09/mysite/myblog/views.py new file mode 100644 index 00000000..389fa2ed --- /dev/null +++ b/resources/session09/mysite/myblog/views.py @@ -0,0 +1,32 @@ +from django.shortcuts import render +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.template import RequestContext, loader +from myblog.models import Post + + +def stub_view(request, *args, **kwargs): + body = "Stub View\n\n" + if args: + body += "Args:\n" + body += "\n".join(["\t%s" % a for a in args]) + if kwargs: + body += "Kwargs:\n" + body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) + return HttpResponse(body, content_type="text/plain") + + +def list_view(request): + published = Post.objects.exclude(published_date__exact=None) + posts = published.order_by('-published_date') + context = {'posts': posts} + return render(request, 'list.html', context) + + +def detail_view(request, post_id): + published = Post.objects.exclude(published_date__exact=None) + try: + post = published.get(pk=post_id) + except Post.DoesNotExist: + raise Http404 + context = {'post': post} + return render(request, 'detail.html', context) diff --git a/resources/session09/mysite/mysite/__init__.py b/resources/session09/mysite/mysite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/session09/mysite/mysite/settings.py b/resources/session09/mysite/mysite/settings.py new file mode 100644 index 00000000..8d7e926a --- /dev/null +++ b/resources/session09/mysite/mysite/settings.py @@ -0,0 +1,89 @@ +""" +Django settings for mysite project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'e@3=0i!#n4l25r*ul*sbx6b$@gh7a6pjee6lr-slw9!ayj#*@f' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myblog', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'mysite.urls' + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' + + +TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'mysite/templates'), ) +LOGIN_URL = '/login/' +LOGIN_REDIRECT_URL = '/' diff --git a/resources/session09/mysite/mysite/templates/base.html b/resources/session09/mysite/mysite/templates/base.html new file mode 100644 index 00000000..eed438f9 --- /dev/null +++ b/resources/session09/mysite/mysite/templates/base.html @@ -0,0 +1,29 @@ +{% load staticfiles %} + + + + My Django Blog + + + + +
    +
    + {% block content %} + [content will go here] + {% endblock %} +
    +
    + + diff --git a/resources/session08/mysite/myblog/templates/add.html b/resources/session09/mysite/mysite/templates/login.html similarity index 58% rename from resources/session08/mysite/myblog/templates/add.html rename to resources/session09/mysite/mysite/templates/login.html index f5f68bb0..1566d0f7 100644 --- a/resources/session08/mysite/myblog/templates/add.html +++ b/resources/session09/mysite/mysite/templates/login.html @@ -1,9 +1,9 @@ {% extends "base.html" %} {% block content %} -

    New Blog Post

    +

    My Blog Login

    {% csrf_token %} {{ form.as_p }} -

    +

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/resources/session08/mysite/mysite/urls.py b/resources/session09/mysite/mysite/urls.py similarity index 53% rename from resources/session08/mysite/mysite/urls.py rename to resources/session09/mysite/mysite/urls.py index f3f985db..bbcf775f 100644 --- a/resources/session08/mysite/mysite/urls.py +++ b/resources/session09/mysite/mysite/urls.py @@ -1,8 +1,5 @@ from django.conf.urls import patterns, include, url - -# Uncomment the next two lines to enable the admin: from django.contrib import admin -admin.autodiscover() urlpatterns = patterns('', url(r'^', include('myblog.urls')), @@ -14,13 +11,5 @@ 'django.contrib.auth.views.logout', {'next_page': '/'}, name="logout"), - # Examples: - # url(r'^$', 'mysite.views.home', name='home'), - # url(r'^mysite/', include('mysite.foo.urls')), - - # Uncomment the admin/doc line below to enable admin documentation: - # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), ) diff --git a/resources/session09/mysite/mysite/wsgi.py b/resources/session09/mysite/mysite/wsgi.py new file mode 100644 index 00000000..15c7d49c --- /dev/null +++ b/resources/session09/mysite/mysite/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/resources/session10/base.pt b/resources/session10/base.pt deleted file mode 100644 index 6d7e665c..00000000 --- a/resources/session10/base.pt +++ /dev/null @@ -1,62 +0,0 @@ - - - - Pyramid Wiki - - - - - - - - -
    -
    -
    -
    - pyramid -
    -
    -
    -
    -
    -
    - - Viewing Page Name Goes - Here - -
    - You can return to the - FrontPage. -
    - -
    -
    -
    -
    - -
    -
    -
    - - - \ No newline at end of file diff --git a/resources/session10/wikitutorial/.coverage b/resources/session10/wikitutorial/.coverage deleted file mode 100644 index 10fd5510..00000000 Binary files a/resources/session10/wikitutorial/.coverage and /dev/null differ diff --git a/resources/session10/wikitutorial/MANIFEST.in b/resources/session10/wikitutorial/MANIFEST.in deleted file mode 100644 index 8bf2ce54..00000000 --- a/resources/session10/wikitutorial/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include *.txt *.ini *.cfg *.rst -recursive-include wikitutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/resources/session10/wikitutorial/README.txt b/resources/session10/wikitutorial/README.txt deleted file mode 100644 index 0d22b384..00000000 --- a/resources/session10/wikitutorial/README.txt +++ /dev/null @@ -1 +0,0 @@ -wikitutorial README diff --git a/resources/session10/wikitutorial/setup.cfg b/resources/session10/wikitutorial/setup.cfg deleted file mode 100644 index 46289de6..00000000 --- a/resources/session10/wikitutorial/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=wikitutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = wikitutorial/locale -domain = wikitutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = wikitutorial/locale/wikitutorial.pot -width = 80 - -[init_catalog] -domain = wikitutorial -input_file = wikitutorial/locale/wikitutorial.pot -output_dir = wikitutorial/locale - -[update_catalog] -domain = wikitutorial -input_file = wikitutorial/locale/wikitutorial.pot -output_dir = wikitutorial/locale -previous = true diff --git a/resources/session10/wikitutorial/wikitutorial/__init__.py b/resources/session10/wikitutorial/wikitutorial/__init__.py deleted file mode 100644 index 44ee3ee8..00000000 --- a/resources/session10/wikitutorial/wikitutorial/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from pyramid.config import Configurator -from pyramid_zodbconn import get_connection -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy - -from .models import appmaker -from .security import groupfinder - - -def root_factory(request): - conn = get_connection(request) - return appmaker(conn.root()) - - -def main(global_config, **settings): - """ This function returns a Pyramid WSGI application. - """ - authn_policy = AuthTktAuthenticationPolicy( - 'youdontknowit', callback=groupfinder, hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() - config = Configurator(root_factory=root_factory, settings=settings) - config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) - config.add_static_view('static', 'static', cache_max_age=3600) - config.scan() - return config.make_wsgi_app() diff --git a/resources/session10/wikitutorial/wikitutorial/models.py b/resources/session10/wikitutorial/wikitutorial/models.py deleted file mode 100644 index c7ecbc23..00000000 --- a/resources/session10/wikitutorial/wikitutorial/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from persistent.mapping import PersistentMapping -from persistent import Persistent -from pyramid.security import Allow, Everyone - - -class Wiki(PersistentMapping): - __name__ = None - __parent__ = None - __acl__ = [(Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit')] - - -class Page(Persistent): - - def __init__(self, data): - self.data = data - - -def appmaker(zodb_root): - if not 'app_root' in zodb_root: - app_root = Wiki() - frontpage = Page('This is the front page') - app_root['FrontPage'] = frontpage - frontpage.__name__ = 'FrontPage' - frontpage.__parent__ = app_root - zodb_root['app_root'] = app_root - import transaction - transaction.commit() - return zodb_root['app_root'] - - diff --git a/resources/session10/wikitutorial/wikitutorial/security.py b/resources/session10/wikitutorial/wikitutorial/security.py deleted file mode 100644 index bb0ca862..00000000 --- a/resources/session10/wikitutorial/wikitutorial/security.py +++ /dev/null @@ -1,14 +0,0 @@ -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - - -GROUPS = { - 'editor': ['group:editors'], -} - - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) diff --git a/resources/session10/wikitutorial/wikitutorial/static/favicon.ico b/resources/session10/wikitutorial/wikitutorial/static/favicon.ico deleted file mode 100644 index 71f837c9..00000000 Binary files a/resources/session10/wikitutorial/wikitutorial/static/favicon.ico and /dev/null differ diff --git a/resources/session10/wikitutorial/wikitutorial/static/pylons.css b/resources/session10/wikitutorial/wikitutorial/static/pylons.css deleted file mode 100644 index 4b1c017c..00000000 --- a/resources/session10/wikitutorial/wikitutorial/static/pylons.css +++ /dev/null @@ -1,372 +0,0 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td -{ - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; /* 16px */ - vertical-align: baseline; - background: transparent; -} - -body -{ - line-height: 1; -} - -ol, ul -{ - list-style: none; -} - -blockquote, q -{ - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after -{ - content: ''; - content: none; -} - -:focus -{ - outline: 0; -} - -ins -{ - text-decoration: none; -} - -del -{ - text-decoration: line-through; -} - -table -{ - border-collapse: collapse; - border-spacing: 0; -} - -sub -{ - vertical-align: sub; - font-size: smaller; - line-height: normal; -} - -sup -{ - vertical-align: super; - font-size: smaller; - line-height: normal; -} - -ul, menu, dir -{ - display: block; - list-style-type: disc; - margin: 1em 0; - padding-left: 40px; -} - -ol -{ - display: block; - list-style-type: decimal-leading-zero; - margin: 1em 0; - padding-left: 40px; -} - -li -{ - display: list-item; -} - -ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl -{ - margin-top: 0; - margin-bottom: 0; -} - -ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir -{ - list-style-type: circle; -} - -ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir -{ - list-style-type: square; -} - -.hidden -{ - display: none; -} - -p -{ - line-height: 1.5em; -} - -h1 -{ - font-size: 1.75em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h2 -{ - font-size: 1.5em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h3 -{ - font-size: 1.25em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h4 -{ - font-size: 1em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -html, body -{ - width: 100%; - height: 100%; -} - -body -{ - margin: 0; - padding: 0; - background-color: #fff; - position: relative; - font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; -} - -a -{ - color: #1b61d6; - text-decoration: none; -} - -a:hover -{ - color: #e88f00; - text-decoration: underline; -} - -body h1, body h2, body h3, body h4, body h5, body h6 -{ - font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; - font-weight: 400; - color: #373839; - font-style: normal; -} - -#wrap -{ - min-height: 100%; -} - -#header, #footer -{ - width: 100%; - color: #fff; - height: 40px; - position: absolute; - text-align: center; - line-height: 40px; - overflow: hidden; - font-size: 12px; - vertical-align: middle; -} - -#header -{ - background: #000; - top: 0; - font-size: 14px; -} - -#footer -{ - bottom: 0; - background: #000 url(footerbg.png) repeat-x 0 top; - position: relative; - margin-top: -40px; - clear: both; -} - -.header, .footer -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.wrapper -{ - width: 100%; -} - -#top, #top-small, #bottom -{ - width: 100%; -} - -#top -{ - color: #000; - height: 230px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#top-small -{ - color: #000; - height: 60px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#bottom -{ - color: #222; - background-color: #fff; -} - -.top, .top-small, .middle, .bottom -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.top -{ - padding-top: 40px; -} - -.top-small -{ - padding-top: 10px; -} - -#middle -{ - width: 100%; - height: 100px; - background: url(middlebg.png) repeat-x; - border-top: 2px solid #fff; - border-bottom: 2px solid #b2b2b2; -} - -.app-welcome -{ - margin-top: 25px; -} - -.app-name -{ - color: #000; - font-weight: 700; -} - -.bottom -{ - padding-top: 50px; -} - -#left -{ - width: 350px; - float: left; - padding-right: 25px; -} - -#right -{ - width: 350px; - float: right; - padding-left: 25px; -} - -.align-left -{ - text-align: left; -} - -.align-right -{ - text-align: right; -} - -.align-center -{ - text-align: center; -} - -ul.links -{ - margin: 0; - padding: 0; -} - -ul.links li -{ - list-style-type: none; - font-size: 14px; -} - -form -{ - border-style: none; -} - -fieldset -{ - border-style: none; -} - -input -{ - color: #222; - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 12px; - line-height: 16px; -} - -input[type=text], input[type=password] -{ - width: 205px; -} - -input[type=submit] -{ - background-color: #ddd; - font-weight: 700; -} - -/*Opera Fix*/ -body:before -{ - content: ""; - height: 100%; - float: left; - width: 0; - margin-top: -32767px; -} diff --git a/resources/session10/wikitutorial/wikitutorial/static/pyramid-small.png b/resources/session10/wikitutorial/wikitutorial/static/pyramid-small.png deleted file mode 100644 index a5bc0ade..00000000 Binary files a/resources/session10/wikitutorial/wikitutorial/static/pyramid-small.png and /dev/null differ diff --git a/resources/session10/wikitutorial/wikitutorial/static/pyramid.png b/resources/session10/wikitutorial/wikitutorial/static/pyramid.png deleted file mode 100644 index 347e0554..00000000 Binary files a/resources/session10/wikitutorial/wikitutorial/static/pyramid.png and /dev/null differ diff --git a/resources/session10/wikitutorial/wikitutorial/templates/base.pt b/resources/session10/wikitutorial/wikitutorial/templates/base.pt deleted file mode 100644 index 6d7e665c..00000000 --- a/resources/session10/wikitutorial/wikitutorial/templates/base.pt +++ /dev/null @@ -1,62 +0,0 @@ - - - - Pyramid Wiki - - - - - - - - -
    -
    -
    -
    - pyramid -
    -
    -
    -
    -
    -
    - - Viewing Page Name Goes - Here - -
    - You can return to the - FrontPage. -
    - -
    -
    -
    -
    - -
    -
    -
    - - - \ No newline at end of file diff --git a/resources/session10/wikitutorial/wikitutorial/templates/edit.pt b/resources/session10/wikitutorial/wikitutorial/templates/edit.pt deleted file mode 100644 index 9d6ef02a..00000000 --- a/resources/session10/wikitutorial/wikitutorial/templates/edit.pt +++ /dev/null @@ -1,15 +0,0 @@ - - - Editing - Page Name Goes Here - - - - -
    - - -
    - -
    -
    - {% endif %} -

    Posts

    + .. code-block:: jinja + {% extends "layout.jinja2" %} + {% block body %} + {% if entries %} + ... + {% else %} + ... + {% endif %} + +

    New Entry

    + {% endblock %} -All Done --------- +Homework +======== -Okay. That's it. We've got an app all written. +.. rst-class:: left +.. container:: -.. class:: incremental + You have a website now that allows you to create, view and list journal + entries -So far, we haven't actually touched our browsers at all, but we have -reasonable certainty that this works because of our tests. Let's try it. + .. rst-class:: build + .. container:: + However, there are still a few flaws in this system. -.. class:: incremental + You should be able to edit a journal entry that already exists, in case + you make a spelling error. -In the terminal where you've been running tests, run our microblog app: + It would also be nice to see a prettier site. -.. class:: incremental + Let's handle that for homework this week. -:: +Part 1: Add Editing +------------------- - (flaskenv)$ python microblog.py - * Running on http://127.0.0.1:5000/ - * Restarting with reloader +For part one of your assignment, add editing of existing entries. You will need: +* A form that shows an existing entry (what is different about this form from + one for creating a new entry?) +* A pyramid view that handles that form. It should: -The Big Payoff --------------- + * Show the form with the requested entry when the page is first loaded + * Accept edits only on POST + * Update an existing entry with new data from the form + * Show the view of the entry after editing so that the user can see the edits + saved correctly + * Show errors from form validation, if any are present -Now load ``http://localhost:5000/`` in your browser and enjoy your reward. +* A link somewhere that leads to the editing page for a single entry (probably + on the view page for a entry) +You'll need to update a bit of configuration, but not much. Use the create +form we did here in class as an example. -Making It Pretty ----------------- +Part 2: Make it Yours +--------------------- -What we've got here is pretty ugly. +I've created for you a very bare-bones layout and stylesheet. -.. class:: incremental +You will certainly want to add a bit of your own style and panache. -If you've fallen behind, or want to start fresh, you can find the finished -``microblog`` directory in the class resources. +Spend a few hours this week playing with the styles and getting a site that +looks more like you want it to look. -.. class:: incremental +The Mozilla Developer Network has `some excellent resources`_ for learning CSS. -In that directory inside the ``static`` directory you will find -``styles.css``. Open it in your editor. It contains basic CSS for this app. +In particular, the `Getting Started with CSS`_ tutorial is a thorough +introduction to the basics. -.. class:: incremental +You might also look at their `CSS 3 Demos`_ to help fire up your creative +juices. -We'll need to include this file in our ``layout.html``. +Here are a few more resources: +* `A List Apart `_ offers outstanding articles. Their + `Topics list `_ is worth a browse. +* `Smashing Magazine `_ is another excellent + resource for articles on design. -Static Files ------------- +.. _some excellent resources: https://developer.mozilla.org/en-US/docs/Web/CSS +.. _Getting Started with CSS: https://developer.mozilla.org/en-US/docs/CSS/Getting_Started +.. _CSS 3 Demos: https://developer.mozilla.org/en-US/demos/tag/tech:css3 -Like page templates, Flask locates static resources like images, css and -javascript by looking for a ``static`` directory relative to the app root. -.. class:: incremental +Part 3: User Model +------------------ -You can use the special url endpoint ``static`` to build urls that point here. -Open ``layout.html`` and add the following: +As it stands, our journal accepts entries from anyone who comes by. -.. code-block:: jinja - :class: small incremental +Next week we will add security to allow only logged-in users to create and edit +entries. - - Flaskr - - +To do so, we'll need a user model +The model should have: -Going Further -------------- +* An ``id`` field that is a primary key +* A ``username`` field that is unicode, no more than 255 characters, not + nullable, unique and indexed. +* A ``password`` field that is unicode and not nullable + +In addition, the model should have a classmethod that retrieves a specific user +when given a username. -It's not too hard to see ways you could improve this. +Part 4: Preparation for Deployment +---------------------------------- -.. class:: incremental +At the end of class next week we will be deploying our application to Heroku. -* For my part, I made a version with styles from Bootstrap.js. -* You could limit the number of posts shown on the front page and add - pagination. -* You could add *created date* to the entry schema and provide archived views - for older posts. -* You could add the ability to edit existing posts (and add a modified date to - the schema) -* You could support multi-user blogging by providing a more complex - authentication system and some more views. +You will need to get a free account. +Once you have your free account set up and you have logged in, run through the +`getting started with Python`_ tutorial. -Wrap-Up -------- +Be sure to at least complete the *set up* step. It will have you install the +Heroku Toolbelt, which you will need to have ready in class. -For educational purposes you might try taking a look at the source code for -Flask and Werkzeug. Neither is too large a package. +.. _getting started with Python: https://devcenter.heroku.com/articles/getting-started-with-python#introduction + +Submitting Your Work +-------------------- -.. class:: incremental +As usual, submit your work by committing and pushing it to your project github +repository -In particular seeing how Werkzeug sets up a Request and Response--and how -these relate to the WSGI specification--can be very enlightening. +Commit early and commit often. -.. class:: incremental center +Write yourself good commit messages explaining what you have done and why. -**See You Tomorrow!** +When you are ready to have your work reviewed, email the link to your +repository to us, we'll take a look and make comments. diff --git a/source/presentations/session07.rst b/source/presentations/session07.rst index 77d7975b..787bf014 100644 --- a/source/presentations/session07.rst +++ b/source/presentations/session07.rst @@ -1,1796 +1,1889 @@ -Python Web Programming -====================== +********** +Session 07 +********** -.. image:: img/django-pony.png - :align: left - :width: 50% +.. figure:: /_static/no_entry.jpg + :align: center + :width: 60% -Session 7: Introducing Django + By `Joel Kramer via Flickr`_ -.. class:: intro-blurb right +.. _Joel Kramer via Flickr: https://www.flickr.com/photos/75001512@N00/2707796203 -Wherein we become 'perfectionists with deadlines' +Security And Deployment +======================= -.. class:: image-credit +.. rst-class:: left +.. container:: -image: http://djangopony.com/ + By the end of this session we'll have deployed our learning journal to a + public server. + So we will need to add a bit of security to it. -Full Stack Framework --------------------- + We'll get started on that in a moment -Django comes with: +But First +--------- -.. class:: incremental +.. rst-class:: large center -* Persistence via the *Django ORM* -* CRUD content editing via the automatic *Django Admin* -* URL Mapping via *urlpatterns* -* Templating via the *Django Template Language* -* Caching with levels of configurability -* Internationalization via i18n hooks -* Form rendering and handling -* User authentication and authorization +Questions About the Homework? -.. class:: incremental +.. nextslide:: A Working Edit Form -Pretty much everything you need to make a solid website quickly +.. code-block:: python + class EntryEditForm(EntryCreateForm): + id = HiddenField() -What Sets it Apart? -------------------- +`View the form online `_ -Lots of frameworks offer some of these features, if not all. +.. nextslide:: A Working Edit View -.. class:: incremental +.. code-block:: python -What is Django's *killer feature* + @view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2') + def update(request): + id = int(request.params.get('id', -1)) + entry = Entry.by_id(id) + if not entry: + return HTTPNotFound() + form = EntryEditForm(request.POST, entry) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + return HTTPFound(location=request.route_url('detail', id=entry.id)) + return {'form': form, 'action': request.matchdict.get('action')} -.. class:: incremental center +`See this view online `_ -**The Django Admin** +.. nextslide:: Linking to the Edit Form +.. code-block:: html+jinja -The Django Admin ----------------- + {% extends "layout.jinja2" %} + {% block body %} +
    + +
    +

    + Go Back :: + + Edit Entry +

    + {% endblock %} -Works in concert with the Django ORM to provide automatic CRUD functionality -.. class:: incremental +`View this template online `_ -You write the models, it provides the UI +.. nextslide:: A Working User Model -.. class:: incremental center +.. code-block:: python -**Really** + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + @classmethod + def by_name(cls, name): + return DBSession.query(cls).filter(cls.name == name).first() -The Pareto Principle --------------------- +`View this model online `_ -The Django Admin is a great example of the Pareto Priciple, a.k.a. the 80/20 -rule: +Securing An Application +======================= -.. class:: incremental center +.. rst-class:: left +.. container:: -**80% of the problems can be solved by 20% of the effort** + We've got a solid start on our learning journal. -.. class:: incremental + .. rst-class:: build + .. container:: -The converse also holds true: + We can: -.. class:: incremental center + .. rst-class:: build -**Fixing the last 20% of the problems will take the remaining 80% of the -effort.** + * view a list of entries + * view a single entry + * create a new entry + * edit existing entries + But so can everyone who visits the journal. -Other Django Advantages ------------------------ + It's a recipe for **TOTAL CHAOS** -Clearly the most popular full-stack Python web framework at this time + Let's lock it down a bit. -.. class:: incremental -Popularity translates into: +AuthN and AuthZ +--------------- -.. class:: incremental +There are two aspects to the process of access control online. -* Active, present community -* Plethora of good examples to be found online -* Rich ecosystem of *apps* (encapsulated add-on functionality) +.. rst-class:: build +.. container:: -.. class:: incremental center + .. rst-class:: build -**Jobs** + * **Authentication**: Verification of the identity of a *principal* + * **Authorization**: Enumeration of the rights of that *principal* in a + context. + Think of them as **Who Am I** and **What Can I Do** -Active Development ------------------- + All systems with access control involve both of these aspects. -Django releases in the last 12+ months: + But many systems wire them together as one. -.. class:: incremental -* 1.5.1 (March 2013) -* 1.5 (February 2013) -* 1.4.5 (February 2013) -* 1.3.7 (February 2013) -* 1.4.3 (December 2012) -* 1.3.5 (December 2012) -* 1.4.2 (November 2012) -* 1.3.3 (August 2012) -* 1.4.1 (July 2012) -* 1.3.2 (July 2012) -* 1.4 (March 2012) +.. nextslide:: Pyramid Security +In Pyramid these two aspects are handled by separate configuration settings: -Great Documentation -------------------- +.. rst-class:: build +.. container:: -Thorough, readable, and discoverable. + .. rst-class:: build -.. class:: incremental + * ``config.set_authentication_policy(AuthnPolicy())`` + * ``config.set_authorization_policy(AuthzPolicy())`` -Led the way to better documentation for all Python + If you set one, you must set the other. -.. class:: incremental + Pyramid comes with a few policy classes included. -`Read The Docs `_ - built in connection with -Django, sponsored by the Django Software Foundation. + You can also roll your own, so long as they fulfill the requried interface. -.. class:: incremental + You can learn about the interfaces for `authentication`_ and + `authorization`_ in the Pyramid documentation -Write documentation as part of your python package, and render new versions of -that documentation for every commit +.. _authentication: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/interfaces.html#pyramid.interfaces.IAuthenticationPolicy +.. _authorization: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/interfaces.html#pyramid.interfaces.IAuthorizationPolicy -.. class:: incremental center +.. nextslide:: Our Journal Security -**this is awesome** +We'll be using two built-in policies today: +.. rst-class:: build +.. container:: -Django Organization -------------------- + .. rst-class:: build -A Django *project* represents a whole website: + * ``AuthTktAuthenticationPolicy``: sets an expirable + `authentication ticket`_ cookie. + * ``ACLAuthorizationPolicy``: uses an `Access Control List`_ to grant + permissions to *principals* -.. class:: incremental + Our access control system will have the following properties: -* global configuration settings -* inclusion points for additional functionality -* master list of URL endpoints + .. rst-class:: build -.. class:: incremental + * Everyone can view entries, and the list of all entries + * Users who log in may edit entries or create new ones -A Django *app* encapsulates a unit of functionality: +.. _authentication ticket: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktAuthenticationPolicy +.. _Access Control List: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/authorization.html#pyramid.authorization.ACLAuthorizationPolicy -.. class:: incremental +.. nextslide:: Engaging Security -* A blog section -* A discussion forum -* A content tagging system +By default, Pyramid uses no security. We enable it through configuration. +.. rst-class:: build +.. container:: -Apps Make Up a Project ----------------------- + Open ``learning_journal/__init__.py`` and update it as follows: -.. class:: big-centered + .. code-block:: python -One *project* can (and likely will) consist of many *apps* + # add these imports + from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + # and add this configuration: + def main(global_config, **settings): + # ... + # update building the configurator to pass in our policies + config = Configurator( + settings=settings, + authentication_policy=AuthTktAuthenticationPolicy('somesecret'), + authorization_policy=ACLAuthorizationPolicy(), + default_permission='view' + ) + # ... +.. nextslide:: Verify It Worked -Practice Safe Development -------------------------- +We've now informed our application that we want to use security. -We'll install Django and any other packages we use with it in a virtualenv. +.. rst-class:: build +.. container:: -.. class:: incremental + By default we require the 'view' permission to see anything. -This will ensure that it is isolated from everything else we do in class (and -vice versa) + But we have yet to assign *any permissions to anyone* at all. -.. container:: incremental + Let's verify now that we are unable to see anything in the website. - Remember the basic format for creating a virtualenv: + Start your application, and try to view any page (You should get a 403 + Forbidden error response): - .. class:: small + .. code-block:: bash - :: + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 - $ python virtualenv.py [options] - - $ virtualenv [options] + .. rst-class:: build + * http://localhost:6543/ + * http://localhost:6543/journal/1 + * http://localhost:6543/journal/create + * http://localhost:6543/journal/edit?id=1 -Set Up a VirtualEnv -------------------- +Implementing Authz +------------------ -Start by creating your virtualenv:: +Next we have to grant some permissions to principals. - $ python virtualenv.py djangoenv - - $ virtualenv djangoenv - ... +.. rst-class:: build +.. container:: -.. container:: incremental + Pyramid authorization relies on a concept it calls "context". - Then, activate it:: + A *principal* can be granted rights in a particular *context* - $ source djangoenv/bin/activate - - C:\> djangoenv\Scripts\activate + Context can be made as specific as a single persistent object + Or it can be generalized to a *route* or *view* -Install Django --------------- + To have a context, we need a Python object called a *factory* that must + have an ``__acl__`` special attribute. -Finally, install Django 1.5.1 using `setuptools` or `pip`: + The framework will use this object to determine what permissions a + *principal* has -.. class:: small + Let's create one -:: +.. nextslide:: Add ``security.py`` - (djangoenv)$ pip install Django==1.5.1 - Downloading/unpacking Django==1.5.1 - Downloading Django-1.5.1.tar.gz (8.0MB): 8.0MB downloaded - Running setup.py egg_info for package Django - changing mode of /path/to/djangoenv/bin/django-admin.py to 755 - Successfully installed Django - Cleaning up... - (djangoenv)$ +In the same folder where you have ``models.py`` and ``views.py``, add a new +file ``security.py`` +.. rst-class:: build +.. container:: -Starting a Project ------------------- + .. code-block:: python -Everything in Django stems from the *project* + from pyramid.security import Allow, Everyone, Authenticated -.. class:: incremental + class EntryFactory(object): + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, Authenticated, 'create'), + (Allow, Authenticated, 'edit'), + ] + def __init__(self, request): + pass -To get started learning, we'll create one + The ``__acl__`` attribute of this object contains a list of *ACE*\ s -.. class:: incremental + An *ACE* combines an *action* (Allow, Deny), a *principal* and a *permission* -We'll use a script installed by Django, ``django-admin.py``: +.. nextslide:: Using Our Context Factory -.. code-block:: - :class: incremental +Now that we have a factory that will provide context for permissions to work, +we can tell our configuration to use it. - (djangoenv)$ django-admin.py startproject mysite +.. rst-class:: build +.. container:: -.. class:: incremental + Open ``learning_journal/__init__.py`` and update the route configuration + for our routes: -This will create a folder called 'mysite'. Let's take a look at it: + .. code-block:: python + # add an import at the top: + from .security import EntryFactory + # update the route configurations: + def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + # ... Add the factory keyword argument to our route configurations: + config.add_route('home', '/', factory=EntryFactory) + config.add_route('detail', '/journal/{id:\d+}', factory=EntryFactory) + config.add_route('action', '/journal/{action}', factory=EntryFactory) -Project Layout --------------- +.. nextslide:: What We've Done -The folder created by ``django-admin.py`` contains the following structure: +We've now told our application we want a principal to have the *view* +permission by default. -.. code-block:: +.. rst-class:: build +.. container:: - mysite/ - manage.py - mysite/ - __init__.py - settings.py - urls.py - wsgi.py + And we've provided a factory to supply context and an ACL for each route. -.. class:: incremental + Check our ACL. Who can view the home page? The detail page? The action + pages? -If what you see doesn't match that, you're using an older version of Django. -Make sure you've installed 1.5.1. + Pyramid allows us to set a *default_permission* for *all views*\ . + But view configuration allows us to require a different permission for *a view*\ . -What Got Created ----------------- + Let's make our action views require appropriate permissions next -.. class:: incremental +.. nextslide:: Requiring Permissions for a View -* **outer *mysite* folder**: this is just a container and can be renamed or - moved at will -* **inner *mysite* folder**: this is your project directory. It should not be - renamed. -* **__init__.py**: magic file that makes *mysite* a python package. -* **settings.py**: file which holds configuration for your project, more soon. -* **urls.py**: file which holds top-level URL configuration for your project, - more soon. -* **wsgi.py**: binds a wsgi application created from your project to the - symbol ``application`` -* **manage.py**: a management control script. +Open ``learning_journal/views.py``, and edit the ``@view_config`` for +``create`` and ``update``: +.. code-block:: python -Django and WSGI ---------------- + @view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2', + permission='create') # <-- ADD THIS + def create(request): + # ... -If you open ``wsgi.py``, you'll see the following: + @view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2', + permission='edit') # <-- ADD THIS + def update(request): + # ... -.. code-block:: python - :class: small +.. nextslide:: Verify It Worked - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") +At this point, our "action" views should require permissions other than the +default ``view``. - from django.core.wsgi import get_wsgi_application - application = get_wsgi_application() +.. rst-class:: build +.. container:: -.. container:: incremental + Start your application and verify that it is true: - Django is pointing the python environment at your settings file and then - getting a wsgi application: + .. code-block:: bash - .. code-block:: python - :class: small + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 - def get_wsgi_application(): - return WSGIHandler() + .. rst-class:: build + * http://localhost:6543/ + * http://localhost:6543/journal/1 + * http://localhost:6543/journal/create + * http://localhost:6543/journal/edit?id=1 -The Django WSGIHandler ----------------------- -.. code-block:: python - :class: small - - class WSGIHandler(base.BaseHandler): - #... - def __call__(self, environ, start_response): - #... set up django middleware - try: - #... build a request - request = self.request_class(environ) - except UnicodeDecodeError: - #... handle request errors - else: - # build a response - response = self.get_response(request) - #... determine response status - status = '%s %s' % (response.status_code, status_text) - #... build response headers - response_headers = [(str(k), str(v)) for k, v in response.items()] - #... start a response with status and headers - start_response(force_str(status), response_headers) - return response + You should get a ``403 Forbidden`` for the action pages only. +Implement AuthN +--------------- -*django-admin.py* and *manage.py* ---------------------------------- +Now that we have authorization implemented, we need to add authentication. -*django-admin.py* provides a hook for administrative tasks and abilities: +.. rst-class:: build +.. container:: -.. class:: incremental + By providing the system with an *authenticated user*, our ACEs for + ``Authenticated`` will apply. -* creating a new project or app -* running the development server -* executing tests -* entering a python interpreter -* entering a database shell session with your database -* much much more (run ``django-admin.py`` without an argument) + We'll need to have a way for a user to prove who they are to the + satisfaction of the system. -.. class:: incremental + The most common way of handling this is through a *username* and + *password*. -*manage.py* wraps this functionality, adding the full environment of your -project. + A person provides both in an html form. + When the form is submitted, the system seeks a user with that name, and + compares the passwords. -Development Server ------------------- + If there is no such user, or the password does not match, authentication + fails. -At this point, you should be ready to use the development server:: +.. nextslide:: An Example - (djangoenv)$ cd mysite - (djangoenv)$ python manage.py runserver - ... +Let's imagine that Alice wants to authenticate with our website. -.. class:: incremental +.. rst-class:: build +.. container:: -Load ``http://localhost:8000`` in your browser. + Her username is ``alice`` and her password is ``s3cr3t``. + She fills these out in a form on our website and submits the form. -A Blank Slate -------------- + Our website looks for a ``User`` object in the database with the username + ``alice``. -You should see this: + Let's imagine that there is one, so our site next compares the value she + sent for her *password* to the value stored in the database. -.. image:: img/django-start.png - :align: center - :width: 98% + If her stored password is also ``s3cr3t``, then she is who she says she is. -.. class:: incremental center + All set, right? -**Do you?** +.. nextslide:: Encryption +The problem here is that the value we've stored for her password is in ``plain +text``. -Connecting A Database ---------------------- +.. rst-class:: build +.. container:: -Django supplies its own ORM (Object-Relational Mapper) + This means that anyone could potentially steal our database and have access + to all our users' passwords. -.. class:: incremental + Instead, we should *encrypt* her password with a strong one-way hash. -This ORM sits on top of the DB-API implementation you choose. + Then we can store the hashed value. -.. class:: incremental + When she provides the plain text password to us, we *encrypt* it the same + way, and compare the result to the stored value. -You must provide connection information through Django configuration. + If they match, then we know the value she provided is the same we used to + create the stored hash. -.. class:: incremental +.. nextslide:: Adding Encryption -All Django configuration takes place in ``settings.py`` in your project -folder. +Python provides a number of libraries for implementing strong encryption. +.. rst-class:: build +.. container:: -Your Database Settings ----------------------- + You should always use a well-known library for encryption. -Edit your ``settings.py`` to match: + We'll use a good one called `Passlib`_. -.. code-block:: python - :class: small - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mysite.db', - # The following settings are not used with sqlite3: - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', - } - } - - -Django and Your Database ------------------------- + This library provides a number of different algorithms and a *context* that + implements a simple interface for each. -Django's ORM provides a layer of *abstraction* between you and SQL + .. code-block:: python -.. class:: incremental + from passlib.context import CryptContext + password_context = CryptContext(schemes=['pbkdf2_sha512']) + hashed = password_context.encrypt('password') + if password_context.verify('password', hashed): + print "It matched" -You write Python *models* describing the object that make up your system. +.. _Passlib: https://pythonhosted.org/passlib/ -.. class:: incremental +.. nextslide:: Install Passlib -The ORM handles converting data from these objects into SQL statements (and -back) +To install a new package as a dependency, we add the package to our list in +``setup.py``. -.. class:: incremental +``Passlib`` provides a large number of different hashing schemes. Some (like +``bcrypt``) require underlying ``C`` extensions to be compiled. If you do not +have a ``C`` compiler, these extensions will be disabled. -We'll learn much more about this in a bit +.. rst-class:: build +.. container:: + .. code-block:: python -Core Django *Apps* ------------------- + requires = [ + ... + 'wtforms', + 'passlib', + ] -Django already includes some *apps* for you. + Then, we re-install our package to pick up the new dependency: -.. container:: incremental + .. code-block:: bash - They're in ``settings.py`` in the ``INSTALLED_APPS`` setting: + (ljenv)$ python setup.py develop - .. code-block:: python - :class: small - - INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - ) - - -Creating the Database ---------------------- + *note* if you have a c compiler installed but not the Python dev headers, + this may not work. Let me know if you get errors. -These *apps* define models of their own, tables must be created. +.. nextslide:: Using Passlib -.. container:: incremental +As noted above, the passlib library uses a ``context`` object to manage +passwords. - You make them by running the ``syncdb`` management command: - - .. class:: small - - :: +.. rst-class:: build +.. container:: - (djangoenv)$ python manage.py syncdb - Creating tables ... - Creating table auth_permission - Creating table auth_group_permissions - Creating table auth_group - ... - You just installed Django's auth system, ... - Would you like to create one now? (yes/no): + This object supports a lot of functionality, but the only API we care about + for this project is encrypting and verifying passwords. -.. class:: incremental + We'll create a single, global context to be used by our project. -Add your first user at this prompt (remember the password) + Since the ``User`` class is the component in our system that should have + the responsibility for password interactions, we'll create our context in + the same place it is defined. + In ``learning_journal/models.py`` add the following code: -Our Class App -------------- + .. code-block:: python + + # add an import at the top + from passlib.context import CryptContext -We are going to build an *app* to add to our *project*. To start with our app -will: + # then lower down, make a context at module scope: + password_context = CryptContext(schemes=['pbkdf2_sha512']) -.. class:: incremental -* allow a user to create and edit blog posts -* allow a user to define categories -* allow a user to place a post in one or more categories +.. nextslide:: Comparing Passwords -.. class:: incremental +Now that we have a context object available, let's write an instance method for +our ``User`` class that uses it to verify a plaintext password: -As stated above, an *app* represents a unit within a system, the *project*. We -have a project, we need to create an *app* +.. rst-class:: build +.. container:: + Again, in ``learning_journal/models.py`` add the following to the ``User`` + class: -Create an App -------------- + .. code-block:: python -This is accomplished using ``manage.py``. + # add this method to the User class: + class User(Base): + # ... + def verify_password(self, password): + return password_context.verify(password, self.password) -.. class:: incremental +.. nextslide:: Create a User -In your terminal, make sure you are in the *outer* mysite directory, where the -file ``manage.py`` is located. Then: +We'll also need to have a user for our system. -.. class:: incremental +.. rst-class:: build +.. container:: -:: + We can use the database initialization script to create one for us. - (djangoenv)$ python manage.py startapp myblog + Open ``learning_journal/scripts/initialzedb.py``: + .. code-block:: python -What is Created ---------------- + from learning_journal.models import password_context + from learning_journal.models import User + # and update the main function like so: + def main(argv=sys.argv): + # ... + with transaction.manager: + # replace the code to create a MyModel instance + encrypted = password_context.encrypt('admin') + admin = User(name='admin', password=encrypted) + DBSession.add(admin) -This should leave you with the following structure: +.. nextslide:: Rebuild the Database: -.. class:: small +In order to get our user created, we'll need to delete our database and +re-build it. -:: +.. rst-class:: build +.. container:: - mysite/ - manage.py - mysite/ - ... - myblog/ - __init__.py - models.py - tests.py - views.py + Make sure you are in the folder where ``setup.py`` appears. -.. class:: incremental + Then remove the sqlite database: -We'll start by defining the main Python class in our blog system, a ``Post``. + .. code-block:: bash + (ljenv)$ rm *.sqlite -Django Models -------------- + And re-initialize: -Any Python class in Django that is meant to be persisted *must* inherit from -the Django ``Model`` class. + .. code-block:: bash -.. class:: incremental + (ljenv)$ initialize_learning_journal_db development.ini + ... + 2015-01-17 16:43:55,237 INFO [sqlalchemy.engine.base.Engine][MainThread] + INSERT INTO users (name, password) VALUES (?, ?) + 2015-01-17 16:43:55,237 INFO [sqlalchemy.engine.base.Engine][MainThread] + ('admin', '$2a$10$4Z6RVNhTE21mPLJW5VeiVe0EG57gN/HOb7V7GUwIr4n1vE.wTTTzy') -This base class hooks in to the ORM functionality converting Python code to -SQL. +Providing Login UI +------------------ -.. class:: incremental +We now have a user in our database with a strongly encrypted password. -You can override methods from the base ``Model`` class to alter how this works -or write new methods to add functionality. +.. rst-class:: build +.. container:: -.. class:: incremental + We also have a method on our user model that will verify a supplied + password against this encrypted version. -Learn more about `models -`_ + We must now provide a view that lets us log in to our application. + We start by adding a new *route* to our configuration in + ``learning_journal/__init__.py``: -Our Post Model --------------- + .. code-block:: python -Open the ``models.py`` file created in our ``myblog`` package. Add the -following: + config.add_rount('action' ...) + # ADD THIS + config.add_route('auth', '/sign/{action}', factory=EntryFactory) -.. code-block:: python - :class: small +.. nextslide:: A Login Form - from django.db import models - from django.contrib.auth.models import User - - class Post(models.Model): - title = models.CharField(max_length=128) - text = models.TextField(blank=True) - author = models.ForeignKey(User) - created_date = models.DateTimeField(auto_now_add=True) - modified_date = models.DateTimeField(auto_now=True) - published_date = models.DateTimeField(blank=True, null=True) +It would be nice to use the form library again to make a login form. +.. rst-class:: build +.. container:: -Model Fields ------------- + Open ``learning_journal/forms.py`` and add the following: -We've created a subclass of the Django ``Model`` class and added a bunch of -attributes. + .. code-block:: python -.. class:: incremental + # add an import: + from wtforms import PasswordField + # and a new form class + class LoginForm(Form): + username = TextField( + 'Username', [validators.Length(min=1, max=255)] + ) + password = PasswordField( + 'Password', [validators.Length(min=1, max=255)] + ) -* These attributes are all instances of ``Field`` classes defined in Django -* Field attributes on a model map to columns in a database table -* The arguments you provide to each Field customize how it works - * This means *both* how it operates in Django *and* how it is defined in SQL +.. nextslide:: Login View in ``learning_journal/views.py`` -* There are arguments shared by all Field types -* There are also arguments specific to individual types +.. ifnotslides:: -.. class:: incremental + Next, we'll create a login view in ``learning_journal/views.py`` -You can read much more about `Model Fields and options -`_ +.. code-block:: python + # new imports: + from pyramid.security import forget, remember + from .forms import LoginForm + from .models import User + # and a new view + @view_config(route_name='auth', match_param='action=in', renderer='string', + request_method='POST') + def sign_in(request): + login_form = None + if request.method == 'POST': + login_form = LoginForm(request.POST) + if login_form and login_form.validate(): + user = User.by_name(login_form.username.data) + if user and user.verify_password(login_form.password.data): + headers = remember(request, user.name) + else: + headers = forget(request) + else: + headers = forget(request) + return HTTPFound(location=request.route_url('home'), headers=headers) -Field Details -------------- +.. nextslide:: Where's the Renderer? -There are some features of our fields worth mentioning in specific: +Notice that this view doesn't render anything. No matter what, you end up +returning to the ``home`` route. -.. class:: incremental +.. rst-class:: build +.. container:: -Notice we have no field that is designated as the *primary key* + We have to incorporate our login form somewhere. -.. class:: incremental + The home page seems like a good place. -* You *can* make a field the primary key by adding ``primary_key=True`` in the - arguments -* If you do not, Django will automatically create one. This field is always - called ``id`` -* No matter what the primary key field is called, its value is always - available on a model instance as ``pk`` + But we don't want to show it all the time. + Only when we aren't logged in already. -Field Details -------------- + Let's give that a whirl. -.. code-block:: python - :class: small - - title = models.CharField(max_length=128) +.. nextslide:: Updating ``index_page`` -.. class:: incremental +Pyramid security provides a method that returns the id of the user who is +logged in, if any. -The required ``max_length`` argument is specific to ``CharField`` fields. +.. rst-class:: build +.. container:: -.. class:: incremental + We can use that to update our home page in ``learning_journal/views.py``: -It affects *both* the Python and SQL behavior of a field. + .. code-block:: python -.. class:: incremental + # add an import: + from pyramid.security import authenticated_userid + + # and update the index_page view: + @view_config(...) + def index_page(request): + # ... get all entries here + form = None + if not authenticated_userid(request): + form = LoginForm() + return {'entries': entries, 'login_form': form} + +.. nextslide:: Update ``list.jinja2`` + +Now we have to update the template for the ``index_page`` to display the form, *if it is there* + +.. rst-class:: build +.. container:: + + .. code-block:: jinja + + {% block body %} + {% if login_form %} + + {% endif %} + {% if entries %} + ... -In python, it is used to *validate* supplied values during *model validation* +.. nextslide:: Try It Out -.. class:: incremental +We should be ready at this point. -In SQL it is used in the column definition: ``VARCHAR(128)`` +.. rst-class:: build +.. container:: + Fire up your application and see it in action: -Field Details -------------- + .. code-block:: bash -.. code-block:: python - :class: small + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 - text = models.TextField(blank=True) - # ... - published_date = models.DateTimeField(blank=True, null=True) + Load the home page and see your login form: -.. class:: incremental + * http://localhost:6543/ + + Fill it in and submit the form, verify that you can add a new entry. -The argument ``blank`` is shared across all field types. The default is -``False`` +.. nextslide:: Break Time -.. class:: incremental +That's enough for now. We have a working application. -This argument affects only the Python behavior of a field, determining if the -field is *required* +When we return, we'll deploy it. -.. class:: incremental -The related ``null`` argument affects the SQL definition of a field: is the -column NULL or NOT NULL +Deploying An Application +======================== +.. rst-class:: left +.. container:: -Field Details -------------- + Now that we have a working application, our next step is to deploy it. -.. code-block:: python - :class: small + .. rst-class:: build + .. container:: - created_date = models.DateTimeField(auto_now_add=True) - modified_date = models.DateTimeField(auto_now=True) + This will allow us to interact with the application in a live setting. -.. class:: incremental + We will be able to see the application from any computer, and can share + it with friends and family. -``auto_now_add`` is available on all date and time fields. It sets the value -of the field to *now* when an instance is first saved. + To do this, we'll be using one of the most popular platforms for + deploying web applications today, `Heroku`_. -.. class:: incremental +.. _Heroku: http://heroku.com -``auto_now`` is similar, but sets the value anew each time an instance is -saved. +Heroku +------ -.. class:: incremental +.. figure:: /_static/heroku-logo.png + :align: center + :width: 40% -Setting either of these will cause the ``editable`` attribute of a field to be -set to ``False``. +.. rst-class:: build +.. container:: + Heroku provides all the infrastructure needed to run many types of + applications. -Field Details -------------- + It also provides `add-on services`_ that support everything from analytics + to payment processing. -.. code-block:: python - :class: small + Elaborate applications deployed on Heroku can be quite expensive. - author = models.ForeignKey(User) + But for simple applications like our learning journal, the price is just + right: **free** -.. class:: incremental +.. _add-on services: https://addons.heroku.com -Django also models SQL *relationships* as specific field types. +.. nextslide:: How Heroku Works -.. class:: incremental +Heroku is predicated on interaction with a git repository. -The required positional argument is the class of the related Model. +.. rst-class:: build +.. container:: -.. class:: incremental + You initialize a new Heroku app in a repository on your machine. -By default, the reverse relation is implemented as the attribute -``_set``. + This adds Heroku as a *remote* to your repository. -.. class:: incremental + When you are ready to deploy your application, you ``git push heroku + master``. -You can override this by providing the ``related_name`` argument. + Adding a few special files to your repository allows Heroku to tell what + kind of application you are creating. + It responds to your push by running an appropriate build process and then + starting your app with a command you provide. -Our Category Model ------------------- +Preparing to Run Your App +------------------------- -Our app specification says that a user should be able to place a post in one -or more categories. +In order for Heroku to deploy your application, it has to have a command it can +run from a standard shell. -.. class:: incremental +.. rst-class:: build +.. container:: -We'll create a second Model to represent this. It should: + We could use the ``pserve`` command we've been using locally, but the + server it uses is designed for development. -.. class:: incremental + It's not really suitable for a public deployment. -* Have a unique name -* Have a description -* Be in a many-to-many relationship with our ``Post`` model -* Instances of ``Category`` should have a ``posts`` attribute that provides - access to all posts in that category -* Instances of ``Post`` should have a ``categories`` attribute that provides - access to all the categories it has been placed in. + Instead we'll use a more robust, production-ready server that came as one + of our dependencies: `waitress`_. + We'll start by creating a python file that can be executed to start the + ``waitress`` server. -My Solution ------------ +.. _waitress: http://waitress.readthedocs.org/en/latest/ -Add this new Model class to ``models.py``. +.. nextslide:: Creating ``runapp.py`` -.. class:: incremental small +At the very top level of your application project, in the same folder where you +find ``setup.py``, create a new file: ``runapp.py`` -https://docs.djangoproject.com/en/1.5/ref/models/fields/ +.. code-block:: python -.. container:: incremental + import os + from paste.deploy import loadapp + from waitress import serve - Here's my model code: + if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app = loadapp('config:production.ini', relative_to='.') - .. code-block:: python - :class: small - - class Category(models.Model): - name = models.CharField(max_length=128) - description = models.TextField(blank=True) - posts = models.ManyToManyField(Post, - blank=True, - null=True, - related_name='categories' - ) + serve(app, host='0.0.0.0', port=port) +.. rst-class:: build +.. container:: -A Word About Development ------------------------- + Once this exists, you can try running your app with it: -These models we've created are not going to change often. This is unusual for -a development cycle. + .. code-block:: bash -.. class:: incremental + (ljenv)$ python runapp.py + serving on http://0.0.0.0:5000 -The ``syncdb`` management command only creates tables that *do not yet exist*. -It **does not update tables**. +.. nextslide:: Running Via Shell -.. class:: incremental +This would be enough, but we also want to *install* our application as a Python +package. -The ``sqlclear `` command will print the ``DROP TABLE`` statements to -remove the tables for your app. +.. rst-class:: build +.. container:: -.. class:: incremental + This will ensure that the dependencies for the application are installed. -Or ``sql `` will show the ``CREATE TABLE`` statements, and you can work -out the differences and update manually. + Add a new file called simply ``run`` in the same folder: + .. code-block:: bash -ACK!!! ------- + #!/bin/bash + python setup.py develop + python runapp.py -That doesn't sound very nice, does it? + The first line of this file will install our application and its + dependencies. -.. class:: incremental + The second line will execute the server script. -Luckily, there is an app available for Django that helps with this: ``South`` +.. nextslide:: Build the Database -.. class:: incremental +We'll need to do the same thing for initializing the database. -South allows you to incrementally update your database in a simplified way. +.. rst-class:: build +.. container:: -.. class:: incremental + Create another new file called ``build_db`` in the same folder: -South supports forward, backward and data migrations. + .. code-block:: bash -.. class:: incremental + #!/bin/bash + python setup.py develop + initialize_learning_journal_db production.ini -We won't have time to `cover it `_ in -this class, but know it's there. + Now, add ``run``, ``build_db`` and ``runapp.py`` to your repository and + commit the changes. +.. nextslide:: Make it Executable -Hooking it Up -------------- +For Heroku to use them, ``run`` and ``build_db`` must be *executable* -In order to use our new models, we need Django to know about our *app* +.. rst-class:: build +.. container:: -.. class:: incremental + For OSX and Linux users this is easy (do the same for ``run`` and + ``build_db``): -This is accomplished by configuration in the ``settings.py`` file. + .. code-block:: bash -.. class:: incremental + (ljenv)$ chmod 755 run -Open that file now, in your editor, and find the INSTALLED_APPS setting. + Windows users, if you have ``git-bash``, you can do the same + For the rest of you, try this (for both ``run`` and ``build_db``): -Installing Apps ---------------- + .. code-block:: posh -You extend Django functionality by *installing apps*. This is pretty simple: + C:\views\myproject>git ls-tree HEAD + ... + 100644 blob 55c0287d4ef21f15b97eb1f107451b88b479bffe run + C:\views\myproject>git update-index --chmod=+x run + C:\views\myproject>git ls-tree HEAD + 100755 blob 3689ebe2a18a1c8ec858cf531d8c0ec34c8405b4 run -.. code-block:: python - :class: small + Commit your changes to git to make them permanent. - INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'myblog', # <- YOU ADD THIS PART - ) +.. nextslide:: Procfile -Setting Up the Database ------------------------ +Next, we have to inform Heroku that we will be using this script to run our +application online -You know what the next step will be: +.. rst-class:: build +.. container:: -.. code-block:: - :class: incremental + Heroku uses a special file called ``Procfile`` to do this. - (djangoenv)$ python manage.py syncdb - Creating tables ... - Creating table myblog_post - Creating table myblog_category_posts - Creating table myblog_category - Installing custom SQL ... - Installing indexes ... - Installed 0 object(s) from 0 fixture(s) + Add that file now, in the same directory. -.. class:: incremental + .. code-block:: bash -Django has now created tables for our app. How many did it create? + web: ./run + This file tells Heroku that we have one ``web`` process to run, and that it + is the ``run`` script located right here. -ORM and SQL ------------ + Providing the ``./`` at the start of the file name allows the shell to + execute scripts that are not on the system PATH. -That third table is the SQL mechanism by which a ``Post`` is related to a -``Category``. + Add this new file to your repository and commit it. -.. class:: incremental -The ORM shields us, as Python developers, from SQL intricacies. +.. nextslide:: Select a Python Version -.. class:: incremental +By default, Heroku uses the latest update of Python version 2.7 for any Python +app. -We don't need to know that a join table is needed for a ManyToMany relation. +.. rst-class:: build +.. container:: -.. class:: incremental + You can override this and specify any runtime version of Python + `available in Heroku`_. + + Just add a file called ``runtime.txt`` to your repository, with one line + only: -This is but one of the ways that the ORM helps us. More soon. + .. code-block:: ini + + python-3.5.0 + Create that file, add it to your repository, and commit the changes. -Break Time ----------- +.. _available in Heroku: https://devcenter.heroku.com/articles/python-runtimes#supported-python-runtimes -.. class:: big-centered -Let's take a break here and return in 10 minutes. +Set Up a Heroku App +------------------- +The next step is to create a new app with heroku. -The Django Shell ----------------- +.. rst-class:: build +.. container:: -Django provides a management command ``shell``: + You installed the Heroku toolbelt prior to class. -.. class:: incremental + The toolbelt provides a command to create a new app. -* Shares the same ``sys.path`` as your project, so all installed python - packages are present. -* Imports the ``settings.py`` file from your project, and so shares all - installed apps and other settings. -* Handles connections to your database, so you can interact with live data - directly. + From the root of your project (where the ``setup.py`` file is) run: -.. class:: incremental + .. code-block:: bash -Let's explore the Model Instance API directly using this shell: + (ljenv)$ heroku create + Creating rocky-atoll-9934... done, stack is cedar-14 + https://rocky-atoll-9934.herokuapp.com/ | https://git.heroku.com/rocky-atoll-9934.git + Git remote heroku added -.. class:: incremental + Note that a new *remote* called ``heroku`` has been added: -:: + .. code-block:: bash - (djangoenv)$ python manage.py shell + $ git remote -v + heroku https://git.heroku.com/rocky-atoll-9934.git (fetch) + heroku https://git.heroku.com/rocky-atoll-9934.git (push) +.. nextslide:: Adding PostgreSQL -Creating Instances ------------------- +Your application will require a database, but ``sqlite`` is not really +appropriate for production. -Instances of our model can be created by simple instantiation: +.. rst-class:: build +.. container:: -.. code-block:: python - :class: small + For the deployed app, you'll use `PostgreSQL`_, the best open-source + database. - >>> from myblog.models import Post - >>> p1 = Post(title="My first post", - ... text="This is the first post I've written") - >>> p1 - + Heroku `provides an add-on`_ that supports PostgreSQL, and you'll need to + set it up. -.. container:: incremental + Again, use the Heroku Toolbelt: - We can also validate that our new object is okay before we try to save it: + .. code-block:: bash - .. code-block:: python - :class: small + $ heroku addons:create heroku-postgresql:hobby-dev + Creating postgresql-amorphous-6784... done, (free) + Adding postgresql-amorphous-6784 to rocky-atoll-9934... done + Setting DATABASE_URL and restarting rocky-atoll-9934... done, v3 + Database has been created and is available + ! This database is empty. If upgrading, you can transfer + ! data from another database with pg:copy + Use `heroku addons:docs heroku-postgresql` to view documentation. - >>> p1.full_clean() - Traceback (most recent call last): - ... - ValidationError: {'author': [u'This field cannot be null.']} +.. _PostgreSQL: http://www.postgresql.org +.. _provides an add-on: https://www.heroku.com/postgres +.. nextslide:: PostgreSQL Settings -Django Model Managers ---------------------- +You can get information about the status of your PostgreSQL service with the +toolbelt: -We have to hook our ``Post`` to an author, which must be a ``User``. +.. rst-class:: build +.. container:: -.. class:: incremental + .. code-block:: bash -To do this, we need to have an instance of the ``User`` class. + (ljenv)$ heroku pg + === DATABASE_URL + Plan: Hobby-dev + ... + Data Size: 6.4 MB + Tables: 0 + Rows: 0/10000 (In compliance) -.. class:: incremental + And there is also information about the configuration for the database (and + your app): -We can use the ``User`` *model manager* to run table-level operations like -``SELECT``: + .. code-block:: bash -.. class:: incremental + (ljenv)$ heroku config + === rocky-atoll-9934 Config Vars + DATABASE_URL: postgres://:@:/ -All Django models have a *manager*. By default it is accessed through the -``objects`` class attribute. +Configuration for Heroku +------------------------ +Notice that the configuration for our application on Heroku provides a specific +database URL. -Making a ForeignKey Relation ----------------------------- +.. rst-class:: build +.. container:: -Let's use the *manager* to get an instance of the ``User`` class: + We could copy this value and paste it into our ``production.ini`` + configuration file. -.. code-block:: python - :class: small + But if we do that, then we will be storing that value in GitHub, where + anyone at all can see it. - >>> from django.contrib.auth.models import User - >>> all_users = User.objects.all() - >>> all_users - [] - >>> u1 = all_users[0] - >>> p1.author = u1 + That's not particularly secure. -.. container:: incremental + Luckily, Heroku provides configuration like the database URL in + *environment variables* that we can read in Python. - And now our instance should validate properly: + In fact, we've already done this with our ``runapp.py`` script: .. code-block:: python - :class: small - >>> p1.full_clean() - >>> + port = int(os.environ.get("PORT", 5000)) +.. nextslide:: Adjusting Our DB Configuration -Saving New Objects ------------------- +The Python standard library provides ``os.environ`` to allow access to +*environment variables* from Python code. -Our model has three date fields, two of which are supposed to be -auto-populated: +.. rst-class:: build +.. container:: -.. class:: python - :class: small - - >>> print(p1.created_date) - None - >>> print(p1.modified_date) - None + This attribute is a dictionary keyed by the name of the variable. -.. container:: incremental + We can use it to gain access to configuration provided by Heroku. + + Update ``learning_journal/__init__.py`` like so: - When we save our post, these fields will get values assigned: - .. code-block:: python - :class: small - - >>> p1.save() - >>> p1.created_date - datetime.datetime(2013, 7, 26, 20, 2, 38, 104217, tzinfo=) - >>> p1.modified_date - datetime.datetime(2013, 7, 26, 20, 2, 38, 104826, tzinfo=) + # import the os module: + import os + # then look up the value we need for the database url + def main(global_config, **settings): + # ... + if 'DATABASE_URL' in os.environ: + settings['sqlalchemy.url'] = os.environ['DATABASE_URL'] + engine = engine_from_config(settings, 'sqlalchemy.') + # ... -Updating An Instance --------------------- +.. nextslide:: Adjust ``initializedb.py`` -Models operate much like 'normal' python objects. +We'll need to make the same changes to +``learning_journal/scripts/initializedb.py``: -.. container:: incremental +.. code-block:: python - To change the value of a field, simply set the instance attribute to a new - value. Call ``save()`` to persist the change: + def main(argv=sys.argv): + # ... + settings = get_appsettings(config_uri, options=options) + if 'DATABASE_URL' in os.environ: + settings['sqlalchemy.url'] = os.environ['DATABASE_URL'] + engine = engine_from_config(settings, 'sqlalchemy.') + # ... - .. code-block:: python - :class: small - - >>> p1.title = p1.title + " (updated)" - >>> p1.save() - >>> p1.title - 'My first post (updated)' +.. nextslide:: Additional Security +This mechanism allows us to defer other sensitive values such as the password +for our initial user: -Create a Few Posts ------------------- +.. rst-class:: build +.. container:: -Let's create a few more posts so we can explore the Django model manager query -API: + .. code-block:: python -.. code-block:: python - :class: small + # in learning_journal/scripts/initializedb.py + with transaction.manager: + password = os.environ.get('ADMIN_PASSWORD', 'admin') + encrypted = password_context.encrypt(password) + admin = User(name=u'admin', password=encrypted) + DBSession.add(admin) - >>> p2 = Post(title="Another post", - ... text="The second one created", - ... author=u1).save() - >>> p3 = Post(title="The third one", - ... text="With the word 'heffalump'", - ... author=u1).save() - >>> p4 = Post(title="Posters are great decoration", - ... text="When you are a poor college student", - ... author=u1).save() - >>> Post.objects.count() - 4 + And for the secret value for our AuthTktAuthenticationPolicy + .. code-block:: python -The Django Query API --------------------- + # in learning_journal/__init__.py + def main(global_config, **settings): + # ... + secret = os.environ.get('AUTH_SECRET', 'somesecret') + ... + authentication_policy=AuthTktAuthenticationPolicy(secret) + # ... -The *manager* on each model class supports a full-featured query API. +.. nextslide:: Heroku Config -.. class:: incremental +We will now be looking for three values from the OS environment: -API methods take keyword arguments, where the keywords are special -constructions combining field names with field *lookups*: +.. rst-class:: build -.. class:: incremental small +* DATABASE_URL +* ADMIN_PASSWORD +* AUTH_SECRET -* title__exact="The exact title" -* text__contains="decoration" -* id__in=range(1,4) -* published_date__lte=datetime.datetime.now() +.. rst-class:: build +.. container:: -.. class:: incremental + The ``DATABASE_URL`` value is set for us by the PosgreSQL add-on. -Each keyword argument generates an SQL clause. + But the other two are not. We must set them ourselves using ``heroku + config:set``: + .. code-block:: bash -QuerySets ---------- + (ljenv)$ heroku config:set ADMIN_PASSWORD= + ... + (ljenv)$ heroku config:set AUTH_SECRET= + ... -API methods can be divided into two basic groups: methods that return -``QuerySets`` and those that do not. +.. nextslide:: Checking Configuration -.. class:: incremental +You can see the values that you have set at any time using ``heroku config``: -The former may be chained without hitting the database: +.. code-block:: bash -.. code-block:: python - :class: small incremental + (ljenv)$ heroku config + === rocky-atoll-9934 Config Vars + ADMIN_PASSWORD: + AUTH_SECRET: + DATABASE_URL: - >>> a = Post.objects.all() #<-- no query yet - >>> b = a.filter(title__icontains="post") #<-- not yet - >>> c = b.exclude(text__contains="created") #<-- nope - >>> [(p.title, p.text) for p in c] #<-- This will issue the query +.. rst-class:: build +.. container:: -.. container:: incremental + These values are sent and received using secure transport. - Conversely, the latter will issue an SQL query when executed. + You do not need to worry about them being intercepted. - .. code-block:: python - :class: small - - >>> a.count() # immediately executes an SQL query + This mechanism allows you to place important configuration values outside + the code for your application. +.. nextslide:: Installing Dependencies -QuerySets and SQL ------------------ +We've been handling our application's dependencies by adding them to +``setup.py``. -If you are curious, you can see the SQL that a given QuerySet will use: +.. rst-class:: build +.. container:: -.. code-block:: python - :class: small incremental + It's a good idea to install all of these before attempting to run our app. - >>> print(c.query) - SELECT "myblog_post"."id", "myblog_post"."title", - "myblog_post"."text", "myblog_post"."author_id", - "myblog_post"."created_date", "myblog_post"."modified_date", - "myblog_post"."published_date" - FROM "myblog_post" - WHERE ("myblog_post"."title" LIKE %post% ESCAPE '\' - AND NOT ("myblog_post"."text" LIKE %created% ESCAPE '\' ) - ) + The ``pip`` package manager allows us to dump a list of the packages we've + installed in a virtual environment using the ``freeze`` command: -.. class:: incremental + .. code-block:: bash + + (ljenv)$ pip freeze + ... + zope.interface==4.1.3 + zope.sqlalchemy==0.7.6 -The SQL will vary depending on which DBAPI backend you use (yay ORM!!!) + We can tell heroku to install these dependencies by creating a file called + ``requirements.txt`` at the root of our project repository: + .. code-block:: bash + + (ljenv)$ pip freeze > requirements.txt -Exploring the QuerySet API --------------------------- + Add this file to your repository and commit the changes. -See https://docs.djangoproject.com/en/1.5/ref/models/querysets +.. nextslide:: Heroku-specific Dependencies -.. code-block:: python - :class: small +But there is also a new dependency we've added that is only needed for Heroku. - >>> [p.pk for p in Post.objects.all().order_by('created_date')] - [1, 2, 3, 4] - >>> [p.pk for p in Post.objects.all().order_by('-created_date')] - [4, 3, 2, 1] - >>> [p.pk for p in Post.objects.filter(title__contains='post')] - [1, 2, 4] - >>> [p.pk for p in Post.objects.exclude(title__contains='post')] - [3] - >>> qs = Post.objects.exclude(title__contains='post') - >>> qs = qs.exclude(id__exact=3) - >>> [p.pk for p in qs] - [] - >>> qs = Post.objects.exclude(title__contains='post', id__exact=3) - >>> [p.pk for p in qs] - [1, 2, 3, 4] +.. rst-class:: build +.. container:: + Because we are using a PostgreSQL database, we need to install the + ``psycopg2`` package, which handles communicating with the database. -Updating via QuerySets ----------------------- + We don't want to install this locally, though, where we use sqlite. -You can update all selected objects at the same time. + Go ahead and add one more line to ``requirements.txt`` with the latest + version of the ``pyscopg2`` package: -.. class:: incremental + .. code-block:: bash -Changes are persisted without needing to call ``save``. + psycopg2==2.6.1 -.. code-block:: python - :class: small incremental - - >>> qs = Post.objects.all() - >>> [p.published_date for p in qs] - [None, None, None, None] - >>> from datetime import datetime - >>> from django.utils.timezone import UTC - >>> utc = UTC() - >>> now = datetime.now(utc) - >>> qs.update(published_date=now) - 4 - >>> [p.published_date for p in qs] - [datetime.datetime(2013, 7, 27, 1, 20, 30, 505307, tzinfo=), - ...] - - -Testing Our Models ------------------- + Commit the change to your repository. -As with any project, we want to test our work. Django provides a testing -framework to allow this. +Deployment +---------- -.. class:: incremental +We are now ready to deploy our application. -Django supports both *unit tests* and *doctests*. I strongly suggest using -*unit tests*. +.. rst-class:: build +.. container:: -.. class:: incremental + All we need to do is push our repository to the ``heroku`` master: -You add tests for your *app* to the file ``tests.py``, which should be at the -same package level as ``models.py``. + .. code-block:: bash -.. class:: incremental + (ljenv)$ git push heroku master + ... + remote: Building source: + remote: + remote: -----> Python app detected + ... + remote: Verifying deploy... done. + To https://git.heroku.com/rocky-atoll-9934.git + b59b7c3..54f7e4d master -> master -Locate and open this file in your editor. +.. nextslide:: Using ``heroku run`` +You can use the ``run`` command to execute arbitrary commands in the Heroku +environment. -Django TestCase Classes ------------------------ +.. rst-class:: build +.. container:: -**SimpleTestCase** is for basic unit testing with no ORM requirements + You can use this to initialize the database, using the shell script you + created earlier: -.. class:: incremental + .. code-block:: bash -**TransactionTestCase** is useful if you need to test transactional -actions (commit and rollback) in the ORM + (ljenv)$ heroku run ./build_db + ... -.. class:: incremental + This will install our application and then run the database initialization + script. -**TestCase** is used when you require ORM access and a test client +.. nextslide:: Test Your Results -.. class:: incremental +At this point, you should be ready to view your application online. -**LiveServerTestCase** launches the django server during test runs for -front-end acceptance tests. +.. rst-class:: build +.. container:: + Use the ``open`` command from heroku to open your website in a browser: -Testing Data ------------- + .. code-block:: bash -Sometimes testing requires base data to be present. We need a User for ours. + (ljenv)$ heroku open -.. class:: incremental + If you don't see your application, check to see if it is running: -Django provides *fixtures* to handle this need. + .. code-block:: bash -.. class:: incremental + (ljenv)$ heroku ps + === web (1X): `./run` + web.1: up 2015/01/18 16:44:37 (~ 31m ago) -Create a directory called ``fixtures`` inside your ``myblog`` app directory. + If you get no results, use the ``scale`` command to try turning on a web + *dyno*: -.. class:: incremental + .. code-block:: bash -Copy the file ``myblog_test_fixture.json`` from the class resources into this -directory, it contains users for our tests. + (ljenv)$ heroku scale web=1 + Scaling dynos... done, now running web at 1:1X. +.. nextslide:: A Word About Scaling -Setting Up Our Tests --------------------- +Heroku pricing is dependent on the number of *dynos* you are running. -Now that we have a fixture, we need to instruct our tests to use it. +.. rst-class:: build +.. container:: -.. container:: incremental + So long as you only run one dyno per application, you will remain in the + free tier. - Edit ``tests.py`` (which comes with one test already) to look like this: + Scaling above one dyno will begin to incur costs. - .. code-block:: python - :class: small + **Pay attention to the number of dynos you have running**. - from django.test import TestCase - from django.contrib.auth.models import User - - class PostTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] +.. nextslide:: Troubleshooting - def setUp(self): - self.user = User.objects.get(pk=1) +Troubleshooting problems with Heroku deployment can be challenging. +.. rst-class:: build +.. container:: -Our First Enhancement ---------------------- + Your most powerful tool is the ``logs`` command: -Look at the way our Post represents itself in the Django shell: + .. code-block:: bash -.. code-block:: python - :class: small + (ljenv)$ heroku logs + ... + 2015-01-19T01:17:59.443720+00:00 app[web.1]: serving on http://0.0.0.0:53843 + 2015-01-19T01:17:59.505003+00:00 heroku[web.1]: State changed from starting to update - >>> [p for p in Post.objects.all()] - [, , - , ] + This command will print the last 50 or so lines of logging from your + application. -.. class:: incremental + You can use the ``-t`` flag to *tail* the logs. -Wouldn't it be nice if the posts showed their titles instead? + This will continually update log entries to your terminal as you interact + with the application. -.. class:: incremental +.. nextslide:: Revel In Your Glory -In Django, the ``__unicode__`` method is used to determine how a Model -instance represents itself. +Try logging in to your application with the password you set up in Heroku +configuration. -.. class:: incremental +.. rst-class:: build +.. container:: -Then, calling ``unicode(instance)`` gives the desired result. + Once you are logged in, try adding an entry or two. + You are now off to the races! -Write The Test --------------- + .. rst-class:: center -Let's write a test that demonstrates our desired outcome: + **Congratulations** -.. code-block:: python - :class: small - - # add this import at the top - from myblog.models import Post +Adding Polish +============= - # and this test method to the PostTestCase - test_unicode(self): - expected = "This is a title" - p1 = Post(title=expected) - actual = unicode(p1) - self.assertEqual(expected, actual) +.. rst-class:: left +.. container:: + So we have now deployed a running application. -Run The Test ------------- + .. rst-class:: build + .. container:: -To run tests, use the ``test`` management command + But there are a number of things we can do to make the application + better. -.. class:: incremental + Let's start by adding a way to log out. -Without arguments, it will run all TestCases it finds in all installed *apps* -.. class:: incremental +Adding Logout +------------- -You can pass the name of a single app to focus on those tests +Our ``login`` view is already set up to work for logout. -.. class:: incremental +.. rst-class:: build +.. container:: -Quit your Django shell and in your terminal run the test we wrote: + What is the logical path taken if that view is accessed via ``GET``? -.. code-block:: bash - :class: small incremental + All we need to do is add a view_config that allows that. - (djangoenv)$ python manage.py test myblog + Open ``learning_journal/views.py`` and make these changes: + .. code-block:: python -The Result ----------- + @view_config(route_name='auth', match_param='action=in', renderer='string', + request_method='POST') # <-- THIS IS ALREADY THERE + # ADD THE FOLLOWING LINE + @view_config(route_name='auth', match_param='action=out', renderer='string') + # UPDATE THE VIEW FUNCTION NAME + def sign_in_out(request): + # ... -We have yet to implement this enhancement, so our test should fail: +.. nextslide:: Re-Deploy -.. class:: small +The chief advantage of Heroku is that we can re-deploy with a single command. -:: +.. rst-class:: build +.. container:: - Creating test database for alias 'default'... - F - ====================================================================== - FAIL: test_unicode (myblog.tests.PostTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "/Users/cewing/projects/training/uw_pce/training.python_web/scripts/session07/mysite/myblog/tests.py", line 15, in test_unicode - self.assertEqual(expected, actual) - AssertionError: 'This is a title' != u'Post object' + Add and commit your changes to git. - ---------------------------------------------------------------------- - Ran 1 test in 0.007s + Then re-deploy by pushing to the ``heroku master``: - FAILED (failures=1) - Destroying test database for alias 'default'... + .. code-block:: bash + (ljenv)$ git push heroku master -Make it Pass ------------- + Once that completes, you should be able to reload your application in the + browser. -Let's add an appropriate ``__unicode__`` method to our Post class + Visit the following URL path to test log out: -.. class:: incremental + * /sign/out -It will take ``self`` as its only argument +Hide UI for Anonymous +--------------------- -.. class:: incremental +Another improvement we can make is to hide UI that is not available for users +who are not logged in. -And it should return its own title as the result +.. rst-class:: build +.. container:: -.. class:: incremental + The first step is to update our ``detail`` view to tell us if someone is + logged in: -Go ahead and take a stab at this in ``models.py`` + .. code-block:: python -.. code-block:: python - :class: small incremental - - class Post(models.Model): - #... + # learning_journal/views.py + @view_config(route_name='detail', renderer='templates/detail.jinja2') + def view(request): + # ... + logged_in = authenticated_userid(request) + return {'entry': entry, 'logged_in': logged_in} - def __unicode__(self): - return self.title + The ``authenticated_userid`` function returns the id of the logged in user, + if there is one, and ``None`` if there is not. + We can use that. -Did It Work? ------------- +.. nextslide:: Hide "Create Entry" UI -Re-run the tests to see: +First we can hide the UI for creating a new entry: -.. code-block:: bash - :class: small +.. rst-class:: build +.. container:: - (djangoenv)$ python manage.py test myblog - Creating test database for alias 'default'... - . - ---------------------------------------------------------------------- - Ran 1 test in 0.007s + Edit ``templates/list.jinja2``: - OK - Destroying test database for alias 'default'... + .. code-block:: jinja -.. class:: incremental center + {% extends "layout.jinja2" %} + {% block body %} + + {% if not login_form %} +

    New Entry

    + {% endif %} + {% endblock %} -**YIPEEEE!** + This relies on the fact that the login form will only be present if there + is **not** an authenticated user. +.. nextslide:: Hide "Edit Entry" UI -Repeat the Exercise -------------------- +Next, we can hide the UI for editing an existing entry: -Although we haven't played with it yet, our Category class could use the same -treatment, using the ``name`` field. +.. rst-class:: build +.. container:: -.. class:: incremental + Edit ``templates/detail.jinja2``: -Add a CategoryTestCase to ``tests.py`` with one test that shows this. + .. code-block:: jinja -.. class:: incremental + {% extends "layout.jinja2" %} + {% block body %} + +

    + Go Back + {% if logged_in %} + :: + + Edit Entry + {% endif %} +

    + {% endblock %} -Run the tests, demonstrating that you have two tests and one failure +Format Entries +-------------- -.. class:: incremental +It would be nice if our journal entries could have HTML formatting. -Add the appropriate method to the appropriate class in ``models.py`` and -re-run the tests. +.. rst-class:: build +.. container:: + We could write HTML by hand in the body field, but that'd be a pain. -My Test -------- + Instead, let's allow ourselves to write entries `in Markdown`_, a popular + markup syntax used by GitHub and many other websites. -.. code-block:: python - :class: incremental - - # another import - from myblog.models import Category - - # and the test case and test - class CategoryTestCase(TestCase): + .. _in Markdown: http://daringfireball.net/projects/markdown/syntax - def test_unicode(self): - expected = "A Category" - c1 = Category(name=expected) - actual = unicode(c1) - self.assertEqual(expected, actual) + Python provides several libraries that implement markdown formatting. + They will take text that contains markdown formatting and convert it to + HTML. -My Method ---------- + Let's use one. -.. code-block:: python +.. nextslide:: Adding the Dependency - class Category(models.Model): - #... - - def __unicode__(self): - return self.name +The first step, is to pick a package and add it to our dependencies. +.. rst-class:: build +.. container:: -What to Test ------------- + My recommendation is the `markdown`_ python library. -In any framework, the question arises of what to test. Much of your app's -functionality is provided by framework tools. Does that need testing? + Open ``setup.py`` and add the package to the ``requires`` list: -.. class:: incremental + .. code-block:: python -I *usually* don't write tests covering features provided directly by the -framework. + requires = [ + # ... + 'cryptacular', + 'markdown', # <-- ADD THIS + ] -.. class:: incremental + We'll test this locally first, so go ahead and re-install your app: -I *do* write tests for functionality I add, and for places where I make -changes to how the default functionality works. + .. code-block:: bash -.. class:: incremental + (ljenv)$ python setup.py develop + ... + Finished processing dependencies for learning-journal==0.0 -This is largely a matter of style and taste (and of budget). +.. _markdown: https://pythonhosted.org/Markdown/ +.. nextslide:: Jinja2 Filters -More Later ----------- +We've seen before how Jinja2 provides a number of filters for values when +rendering templates. -We've only begun to test our blog app. +.. rst-class:: build +.. container:: -.. class:: incremental + A nice feature of the templating language is that it also allows you to + `create your own filters`_. -We'll be adding many more tests later + Remember the template syntax for a filter: -.. class:: incremental + .. code-block:: jinja -In between, you might want to take a look at the Django testing documentation: + {{ value|filter(arg1, ..., argN) }} -.. class:: incremental center + A filter is simply a function that takes the value to the left of the ``|`` + character as a first argument, and any supplied arguments as the second and + beyond: -https://docs.djangoproject.com/en/1.5/topics/testing/ + .. code-block:: python + def filter(value, arg1, ..., argN): + # do something to value here -The Django Admin ----------------- +.. _create your own filters: http://jinja.pocoo.org/docs/dev/api/#custom-filters -As I stated earlier, the Django admin is really Django's *killer feature* +.. nextslide:: Our Markdown Filter -.. class:: incremental +Creating a ``markdown`` filter will allow us to convert plain text stored in +the database to HTML at template rendering time. -To demonstrate this, we are going to set up the admin for our blog +.. rst-class:: build +.. container:: + Open ``learning_journal/views.py`` and add the following: -Install the Admin ------------------ + .. code-block:: python -The Django Admin is, itself, an *app*. It is not installed by default. + # add two imports: + from jinja2 import Markup + import markdown + # and a function + def render_markdown(content): + output = Markup(markdown.markdown(content)) + return output -.. class:: incremental + The ``Markup`` class from jinja2 marks a string with HTML tags as "safe". -Open the ``settings.py`` file from our ``mysite`` project package and -uncomment the admin bit: + This prevents the tags from being *escaped* when they are rendered into a + page. -.. code-block:: python - :class: incremental small +.. nextslide:: Register the Filter - INSTALLED_APPS = ( - # ... - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - 'django.contrib.admin', # <- THIS LINE HERE - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'myblog', - ) +In order for ``Jinja2`` to be aware that our filter exists, we need to register +it. +.. rst-class:: build +.. container:: -Add the Admin Tables --------------------- + In Pyramid, we do this in configuration. -As you might expect, enabling the admin alters our DB. We'll need to run -the ``syncdb`` management command:: + Open ``development.ini`` and edit it as follows: - (djangoenv)$ python manage.py syncdb - Creating tables ... - Creating table django_admin_log - Installing custom SQL ... - Installing indexes ... - Installed 0 object(s) from 0 fixture(s) + .. code-block:: ini -.. class:: incremental + [app:main] + ... + jinja2.filters = + markdown = learning_journal.views.render_markdown -All set. Now let's make it visitable + This informs the main app that we wish to register a jinja2 filter. + We will call it ``markdown`` and it will be embodied by the function we + just wrote. -Django URL Resolution ---------------------- +.. nextslide:: Use Your Filter -Django too has a system for routing URLs to code: the *urlconf*. +To see the results of our work, we'll need to use the filter in a template +somewhere. -.. class:: incremental +.. rst-class:: build +.. container:: -* A urlconf is a list of calls to the ``django.conf.urls.url`` function -* This function takes: - - * a regexp *rule*, representing the URL - - * a ``callable`` to be invoked (or a name identifying one) - - * an optional *name* kwarg, used to *reverse* the URL - - * other optional arguments we will skip for now + I suggest using it in the ``learning_journal/templates/detail.jinja2`` + template: -* The function returns a *resolver* that matches the request path to the - callable -* django will load the urlconf named ``urlpatterns`` that it finds in the file - named in ``settings.ROOT_URLCONF``. + .. code-block:: jinja + {% extends "layout.jinja2" %} + {% block body %} +
    + +

    {{ entry.body|markdown }}

    + +
    +

    + + {% endblock %} -Including URLs --------------- +.. nextslide:: Test Your Results -Many Django add-on *apps*, like the Django Admin, come with their own urlconf +Start up your application, and create an entry using valid markdown formatting: -.. class:: incremental +.. code-block:: bash -It is standard to include these urlconfs by rooting them at some path in your -site. + (ljenv)$ pserve development.ini + Starting server in PID 84331. + serving on http://0.0.0.0:6543 -.. container:: incremental +.. rst-class:: build +.. container:: - You can do this by using the ``include`` function as the callable in a - ``url`` call: + Once you save your entry, you should be able to see it with actual + formatting: headers, bulleted lists, links, and so on. - .. code-block:: python - :class: small + That makes quite a difference. - url(r'^forum/', include('random.forum.app.urls')) + Go ahead and add the same filter registration to ``production.ini`` + Then commit your changes and redeploy: -Including the Admin -------------------- + .. code-block:: bash -We can use this to add *all* the URLs provided by the Django admin in one -stroke. + (ljenv)$ git push heroku master -.. container:: incremental - Uncomment three lines in ``urls.py``: +Syntax Highlighting +------------------- - .. code-block:: python - :class: small +The purpose of this journal is to allow you to write entries about the things +you learn in this class and elsewhere. - from django.contrib import admin #<- Uncomment these two - admin.autodiscover() #<- +.. rst-class:: build +.. container:: - urlpatterns = patterns('', + Markdown formatting allows for "preformatted" blocks of text like code + samples. - # Uncomment the next line to enable the admin: - url(r'^admin/', include(admin.site.urls)), #<- and this - ) + But there is nothing in markdown that handles *colorizing* code. + Luckily, the markdown package allows for extensions, and one of these + supports `colorization`_. -Using the Development Server ----------------------------- + It requires the `pygments`_ library -We can now view the admin. We'll use the Django development server. + Let's set this up next. -.. class:: incremental +.. _colorization: https://pythonhosted.org/Markdown/extensions/code_hilite.html +.. _pygments: http://pygments.org -In your terminal, use the ``runserver`` management command to start the -development server: +.. nextslide:: Install the Dependency -.. class:: incremental +Again, we need to install our new dependency first. -:: +.. rst-class:: build +.. container:: - (djangoenv)$ python manage.py runserver - Validating models... + Add the following to ``requires`` in ``setup.py``: - 0 errors found - Django version 1.4.3, using settings 'mysite.settings' - Development server is running at http://127.0.0.1:8000/ - Quit the server with CONTROL-C. + .. code-block:: python + requires = [ + # ... + 'markdown', + 'pygments', # <-- ADD THIS LINE + ] -Viewing the Admin ------------------ + Then re-install your app to pick up the software: -Load ``http://localhost:8000/admin/``. You should see this: + .. code-block:: bash -.. image:: img/django-admin-login.png - :align: center - :width: 50% + (ljenv)$ python setup.py develop + ... + Finished processing dependencies for learning-journal==0.0 -.. class:: incremental +.. nextslide:: Add to Our Filter -Login with the name and password you created before. +The next step is to extend our markdown filter in ``learning_journal/views.py`` +with this feature. +.. rst-class:: build +.. container:: -The Admin Index ---------------- + .. code-block:: python -The index will provide a list of all the installed *apps* and each model -registered. You should see this: + def render_markdown(content): + output = Markup( + markdown.markdown( + content, + extensions=['codehilite(pygments_style=colorful)', 'fenced_code'] + ) + ) + return output -.. image:: img/admin_index.png - :align: center - :width: 90% + Now, you'll be able to make highlighted code blocks just like in GitHub: -.. class:: incremental + .. code-block:: text -Click on ``Users``. Find yourself? Edit yourself, but **don't** uncheck -``superuser``. + ```python + def foo(x, y): + return x**y + ``` +.. nextslide:: Add CSS -Add Posts to the Admin ----------------------- +Code highlighting works by putting HTML ```` tags with special CSS +classes around bits of your code. -Okay, let's add our app models to the admin. +.. rst-class:: build +.. container:: -.. class:: incremental + We need to generate and add the css to support this. -Add a new file to the ``myblog`` app package: ``admin.py``. Open it and add -the following: + You can use the ``pygmentize`` command from pygments to + `generate the css`_. -.. code-block:: python - :class: incremental + Make sure you are in the directory with ``setup.py`` when you run this: - from django.contrib import admin - from myblog.models import Post, Category + .. code-block:: bash + + (ljenv)$ pygmentize -f html -S colorful -a .codehilite \ + >> learning_journal/static/styles.css - admin.site.register(Post) - admin.site.register(Category) + The styles will be printed to standard out. -.. class:: incremental + The ``>>`` shell operator *appends* the output to the file named. -Restart your Development server and reload the admin index +.. _generate the css: http://pygments.org/docs/cmdline/#generating-styles +.. nextslide:: Try It Out -Play A Bit ----------- +Go ahead and restart your application and see the difference a little style +makes: -Visit the admin page for Posts. You should see the posts we created earlier in -the Django shell. +.. code-block:: bash -.. class:: incremental + (ljenv)$ pserve development.ini + Starting server in PID 84331. + serving on http://0.0.0.0:6543 -Look at the listing of Posts. Because of our ``__unicode__`` method we see a -nice title. +.. rst-class:: build +.. container:: -.. class:: incremental + Try writing an entry with a little Python code in it. -Are there other fields you'd like to see listed? + Python is not the only language available. -.. class:: incremental + Any syntax covered by `pygments lexers`_ is available, just use the + *shortname* from a lexer to get that type of style highlighting. -Click on a Post, note what is and is not shown. +.. _pygments lexers: http://pygments.org/docs/lexers/ -.. class:: incremental +.. nextslide:: Deploy Your Changes -Poke at the Category admin a bit too. +When you've got this working as you wish, go ahead and deploy it. +.. rst-class:: build +.. container:: -Next Steps ----------- + Add and commit all the changes you've made. -We've learned a great deal about Django's ORM and Models. + Then push your results to the ``heroku master``: -.. class:: incremental + .. code-block:: bash + + (ljenv)$ git push heroku master -We've also spent some time getting to know the Query API provided by model -managers and QuerySets. +Homework +======== -.. class:: incremental +.. rst-class:: left +.. container:: -We've also hooked up the Django Admin and noted some shortcomings. + That's just about enough for now. -.. class:: incremental + .. rst-class:: build + .. container:: -In our next session we'll improve how the admin works for us. + There's no homework for you to submit this week. You've worked hard enough. -.. class:: incremental + Take the week to review what we've done and make sure you have a solid + understanding of it. -Then we'll put a front-end on this blog. + If you wish, play with HTML and CSS to make your journal more personalized. + However, in preparation for our work with Django next week, I'd like you to + get started a bit ahead of time. -Break Time ----------- + Please read and follow along with this `basic intro to Django`_. + + .. rst-class:: centered -.. class:: big-centered + **See You Then** -See you back soon. +.. _basic intro to Django: django_intro.html diff --git a/source/presentations/session08.rst b/source/presentations/session08.rst index ffa6f7ef..76a08bb7 100644 --- a/source/presentations/session08.rst +++ b/source/presentations/session08.rst @@ -1,473 +1,410 @@ -Internet Programming with Python -================================ +********** +Session 08 +********** -.. image:: img/django-pony.png - :align: left - :width: 50% +.. figure:: /_static/django-pony.png + :align: center + :width: 60% -Session 8: A Django Application + image: http://djangopony.com/ -.. class:: intro-blurb right +Building a Django Application +============================= -Wherein we complete our Django blog app. +.. rst-class:: large -.. class:: image-credit +Wherein we build a simple blogging app. -image: http://djangopony.com/ +A Full Stack Framework +---------------------- -Where We Stand --------------- +Django comes with: -We've created a couple of models, Post and Category, that make up our blog -app. +.. rst-class:: build +.. container:: -.. class:: incremental + .. rst-class:: build -We've taken some time to get familiar with the basic workings of the Django -ORM. + * Persistence via the *Django ORM* + * CRUD content editing via the automatic *Django Admin* + * URL Mapping via *urlpatterns* + * Templating via the *Django Template Language* + * Caching with levels of configurability + * Internationalization via i18n hooks + * Form rendering and handling + * User authentication and authorization -.. class:: incremental + Pretty much everything you need to make a solid website quickly -We've made a minor modification to our model classes and written tests for it. +.. nextslide:: What Sets it Apart? -.. class:: incremental +Lots of frameworks offer some of these features, if not all. -And we've installed the Django Admin site and added our app to it. +.. rst-class:: build +.. container:: + What is Django's *killer feature* -Customizing the Admin ---------------------- + .. rst-class:: centered -We have noted, however, that the admin isn't exactly right for our needs. + **The Django Admin** -.. class:: incremental +.. nextslide:: The Django Admin -* Listing of posts should show created, modified and published dates -* Listing of posts should show the author of a post, with a link to the author -* It should be possible to add a post to a category while creating or editing - it +Works in concert with the Django ORM to provide automatic CRUD functionality -.. class:: incremental small center +.. rst-class:: build +.. container:: -https://docs.djangoproject.com/en/1.5/ref/contrib/admin/ + You write the models, it provides the UI + You've seen this in action. Pretty neat, eh? -The ModelAdmin Class --------------------- +.. nextslide:: The Pareto Principle -Open ``admin.py`` from your ``myblog`` package. +The Django Admin is a great example of the Pareto Priciple, a.k.a. the 80/20 +rule: -.. class:: incremental +.. rst-class:: build +.. container:: -* The ``admin.site`` is a globally available instance of the ``Admin`` class. -* It is initialized at runtime automatically. -* It stores a registry of the models that are registered with it. -* Each call to ``admin.site.register`` adds a new model to the global *site*. -* ``register`` takes two args: a *Model* subclass and an optional *ModelAdmin* subclass -* If you call it without the optional subclass, you get the default. + .. rst-class:: centered -.. class:: incremental + **80% of the problems can be solved by 20% of the effort** -Most usable admin functions are provided by the ModelAdmin. + The converse also holds true: + .. rst-class:: centered -Custom ModelAdmin ------------------ + **Fixing the last 20% of the problems will take the remaining 80% of the + effort.** -Our first task is to list date and author information. +.. nextslide:: Other Django Advantages -.. container:: incremental +.. ifnotslides:: - In ``admin.py`` add the following code (): + **Other Django Advantages** - .. code-block:: python - :class: small +Clearly the most popular full-stack Python web framework at this time - # this is new - class PostAdmin(admin.ModelAdmin): - list_display = ('__unicode__', 'created_date', 'modified_date', - 'published_date', 'author') - - admin.site.register(Post, PostAdmin) #<- update this registration +.. rst-class:: build +.. container:: -.. class:: incremental + Popularity translates into: -Let's see what that did. + .. rst-class:: build + * Active, present community + * Plethora of good examples to be found online + * Rich ecosystem of *apps* (encapsulated add-on functionality) -View The Results ----------------- + .. rst-class:: centered -If you haven't already, activate your virtualenv then fire up the development -server: + **Jobs** -:: +.. nextslide:: Active Development - (djangoenv)$ python manage.py runserver +Django releases in the last 12+ months (a short list): -.. class:: incremental +.. rst-class:: build +.. container:: -Load http://localhost:8000/admin and click through to the Post admin. + .. rst-class:: build -.. class:: incremental + * 1.9 (December 2015) + * 1.8.7 (November 2015) + * 1.7.11 (November 2015) + * 1.8.5 (October 2015) + * 1.7.10 (August 2015) + * 1.8.3 (July 2015) + * 1.8 (April 2015) + * 1.7.7 (March 2015) + * 1.7.4 (January 2014) + + Django 1.8 is the second *Long Term Support* version, with a guaranteed support + period of three years. -Pretty simple, eh? +.. nextslide:: Great Documentation +Thorough, readable, and discoverable. -List Display ------------- +.. rst-class:: build +.. container:: -A Couple of things about the ``list_display`` option are important to know: + Led the way to better documentation for all Python -.. class:: incremental + `Read The Docs `_ - built in connection with + Django, sponsored by the Django Software Foundation. -* The value you provide must be an iterable even if it has only one item -* Each item in the iterable becomes a column in the list -* The first item is the one that links to the change page for that object - - * That can be customized by the ``list_display_links`` option - -* Listed items can be field names or callables. + Write documentation as part of your python package. -* Callables can be module-level functions, or methods on the ModelAdmin or - Model + Render new versions of that documentation for every commit. + .. rst-class:: centered -A Better Author Listing ------------------------ + **this is awesome** -Let's use this last bit to fix the author listing. -.. class:: incremental +Where We Stand +-------------- -We'll need functionality that provides: +For your homework this week, you created a ``Post`` model to serve as the heart +of our blogging app. -.. class:: incremental +.. rst-class:: build +.. container:: -* The full name of the author, if present, otherwise the username. -* A link to the admin change form for that author. + You also took some time to get familiar with the basic workings of the + Django ORM. -.. class:: incremental + You made a minor modification to our model class and wrote a test for it. -Where should this go? Module? ModelAdmin? Model? + And you installed the Django Admin site and added your app to it. -.. class:: incremental -* The first could be useful in public listings -* The second is really only useful on the backend +Going Further +------------- +One of the most common features in a blog is the ability to categorize posts. -Add Tests ---------- +.. rst-class:: build +.. container:: -In ``tests.py`` add the following test: + Let's add this feature to our blog! -.. code-block:: python - :class: small - - class PostTestCase(TestCase): - #... - def test_author_name(self): - for author in User.objects.all(): - fn, ln, un = (author.first_name, - author.last_name, - author.username) - author_name = Post(author=author).author_name() - if not (fn and ln): - self.assertEqual(author_name, un) - else: - if fn: - self.assertTrue(fn in author_name) - if ln: - self.assertTrue(ln in author_name) + To do so, we'll be adding a new model, and making some changes to existing + code. + .. rst-class:: build -Add Tests ---------- + This means that we'll need to *change our database schema*. -To test the admin, we'll first need a new TestClass: -.. code-block:: python - :class: small +.. nextslide:: Changing a Database - # new imports - from django.contrib.admin.sites import AdminSite - from myblog.admin import PostAdmin +You've seen how to add new tables to a database using the ``migrate`` command. - # new TestCase - class PostAdminTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] +.. rst-class:: build +.. container:: - def setUp(self): - admin = AdminSite() - self.ma = PostAdmin(Post, admin) - for author in User.objects.all(): - title = "%s's title" % author.username - post = Post(title=title, author=author) - post.save() + And you've created your first migration in setting up the ``Post`` model. + This is an example of altering the *database schema* using Python code. -Add Tests ---------- + Starting in Django 1.7, this ability is available built-in to Django. -And then we need a test added to it: + Before verson 1.7 it was available in an add-on called `South`_. -.. code-block:: python - :class: small +.. _South: http://south.readthedocs.org/en/latest - def test_author_link(self): - expected_link_path = '/admin/auth/user/%s' - for post in Post.objects.all(): - expected = expected_link_path % post.author.pk - actual = self.ma.author_link(post) - self.assertTrue(expected in actual) -.. container:: incremental +.. nextslide:: Adding a Model - Quit the django server and run your tests: - - .. class:: small - - :: - - (djangoenv)$ python manage.py test myblog - ... - Ran 4 tests in 0.026s - FAILED (errors=2) +We want to add a new model to represent the categories our blog posts might +fall into. +.. rst-class:: build +.. container:: -Make Them Pass --------------- + This model will need to have: -First, add the ``author_name`` method to our Post model in ``models.py``: + .. rst-class:: build -.. code-block:: python - :class: small + * a name for the category + * a longer description + * a relationship to the Post model - def author_name(self): - raw_name = "%s %s" % (self.author.first_name, - self.author.last_name) - name = raw_name.strip() - if not name: - name = self.author.username - return name + .. code-block:: python -.. class:: small incremental + # in models.py + class Category(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(blank=True) + posts = models.ManyToManyField(Post, blank=True, + related_name='categories') -:: - (djangoenv)$ python manage.py test myblog - ... - Ran 4 tests in 0.027s - FAILED (errors=1) +.. nextslide:: Strange Relationships +In our ``Post`` model, we used a ``ForeignKeyField`` field to match an author +to her posts. -Make Them Pass --------------- +.. rst-class:: build +.. container:: -Finally, add the ``author_link`` method to the PostAdmin in ``admin.py``: + This models the situation in which a single author can have many posts, + while each post has only one author. -.. code-block:: python - :class: small + We call this a *Many to One* relationship. - # add an import - from django.core.urlresolvers import reverse + But any given ``Post`` might belong in more than one ``Category``. - # and a method - class PostAdmin(admin.ModelAdmin): - #... - def author_link(self, post): - url = reverse('admin:auth_user_change', args=(post.id,)) - name = post.author_name() - return '%s' % (url, name) + And it would be a waste to allow only one ``Post`` for each ``Category``. -.. class:: small incremental + Enter the ``ManyToManyField`` -:: +.. nextslide:: Add a Migration - (djangoenv)$ python manage.py test myblog - ...Ran 4 tests in 0.035s - OK +To get these changes set up, we now add a new migration. +.. rst-class:: build +.. container:: -Hook It Up ----------- + We use the ``makemigrations`` management command to do so: -First, replace the ``'author'`` name in ``list_display`` with -``'author_link'``: + .. code-block:: bash -.. code-block:: python - :class: small - - list_display = (..., 'author_link') + (djangoenv)$ ./manage.py makemigrations + Migrations for 'myblog': + 0002_category.py: + - Create model Category -.. container:: incremental +.. nextslide:: Apply A Migration - We also need to let the admin know our HTML is safe: +Once the migration has been created, we can apply it with the ``migrate`` +management command. - .. code-block:: python - :class: small +.. rst-class:: build +.. container:: - def author_link(self, post): - #... method body - author_link.allow_tags = True + .. code-block:: bash + (djangoenv)$ ./manage.py migrate + Operations to perform: + Apply all migrations: sessions, contenttypes, admin, myblog, auth + Running migrations: + Rendering model states... DONE + Applying myblog.0002_category... OK -Wait, What?? ------------- + You can even look at the migration file you just applied, + ``myblog/migrations/0002_category.py`` to see what happened. -In Python, *everything* is an object. Even methods of classes. -.. class:: incremental +.. nextslide:: Make Categories Look Nice -The Django admin uses special *method attributes* to control the methods you -create for ``list_display``. +Let's make ``Category`` object look nice the same way we did with ``Post``. +Start with a test: -.. container:: incremental +.. rst-class:: build +.. container:: - Another special attribute controls the column title used in the list page: + add this to ``tests.py``: .. code-block:: python - :class: small - - def author_link(self, post): - #... method body - author_link.allow_tags = True - author_link.short_description = "Author" #<- add this - - -See The Results ---------------- - -Start up the Django server again and see what you've done: - -.. class:: small - -:: - - (djangoenv)$ python manage.py runserver -.. class:: incremental + # another import + from myblog.models import Category -Reload your admin site, click on the Post admin and see the new 'Author' -column. + # and the test case and test + class CategoryTestCase(TestCase): -.. class:: incremental + def test_string_representation(self): + expected = "A Category" + c1 = Category(name=expected) + actual = str(c1) + self.assertEqual(expected, actual) -* Click on an author name. -* Set the first and last names (if you haven't already). -* Go back to Posts and see the outcome of this change. +.. nextslide:: Make it Pass -.. class:: incremental +When you run your tests, you now have two, and one is failing because the +``Category`` object doesn't look right. -Not bad, eh? +.. rst-class:: build +.. container:: + .. code-block:: bash -Categorize Posts ----------------- - -We'd like to be able to add categories to posts while adding or editing them. + (djangoenv)$ ./manage.py test myblog + Creating test database for alias 'default'... + ... -.. class:: incremental + Ran 2 tests in 0.011s -But there is no field on the ``Post`` model that would show them. + FAILED (failures=1) -.. class:: incremental + Do you remember how you made that change for a ``Post``? -Django provides the concept of an ``inline`` form to allow adding objects that -are related when there is no field available. + .. code-block:: python -.. class:: incremental + class Category(models.Model): + #... -In the Django Admin, these are created using subclasses of the -``InlineAdmin``. + def __str__(self): + return self.name -Create an Inline Admin ----------------------- +.. nextslide:: Admin for Categories -In ``admin.py`` add the following code *above* the definition of PostAdmin: +Adding our new model to the Django admin is equally simple. -.. code-block:: python - :class: small +.. rst-class:: build +.. container:: - class CategoryInlineAdmin(admin.TabularInline): - model = Category.posts.through - extra = 1 + Simply add the following line to ``myblog/admin.py`` -.. container:: incremental + .. code-block:: python - And then add one line to the PostAdmin class definition: + # a new import + from myblog.models import Category - .. code-block:: python - :class: small - - class PostAdmin(admin.ModelAdmin): - #... other options - inlines = [CategoryInlineAdmin, ] - - #... methods + # and a new admin registration + admin.site.register(Category) -Try It Out ----------- +.. nextslide:: Test It Out -Restart the Django server and see what you've done: +Fire up the Django development server and see what you have in the admin: -.. class:: small +.. code-block:: bash -:: + (djangoenv)$ ./manage.py runserver + Validating models... + ... + Starting development server at http://127.0.0.1:8000/ + Quit the server with CONTROL-C. - (djangoenv)$ python manage.py runserver +.. rst-class:: build +.. container:: -.. class:: incremental + Point your browser at ``http://localhost:8000/admin/``, log in and play. -Note that you can even add *new* categories via the inline form. + Add a few categories, put some posts in them. Visit your posts, add new + ones and then categorize them. -.. class:: incremental -But, in the form for a category, you see the field for Post. That shouldn't be -there. +BREAK TIME +---------- +We've completed a data model for our application. -A Final Tweak -------------- +And thanks to Django's easy-to-use admin, we have a reasonable CRUD application +where we can manage blog posts and the categories we put them in. -See if you can figure out how to remove the ``posts`` field from the -CategoryAdmin. +When we return, we'll put a public face on our new creation. -.. code-block:: python - :class: small incremental - - # create a custom model admin class - class CategoryAdmin(admin.ModelAdmin): - exclude = ('posts', ) - - # and register Category to use it in the Admin - admin.site.register(Category, CategoryAdmin) +If you've fallen behind, the app as it stands now is in our class resources as +``mysite_stage_1`` A Public Face -------------- - -Point your browser at http://localhost:8000/ +============= -.. class:: incremental +.. rst-class:: left -What do you see? +Point your browser at http://localhost:8000/ -.. class:: incremental +.. rst-class:: build left +.. container:: -Why? + What do you see? -.. class:: incremental + Why? -We need to add some public pages for our blog. + We need to add some public pages for our blog. -.. class:: incremental + In Django, the code that builds a page that you can see is called a *view*. -In Django, the code that builds a page that you can see is called a *view*. Django Views ------------ @@ -475,275 +412,311 @@ Django Views A *view* can be defined as a *callable* that takes a request and returns a response. -.. class:: incremental - -This should sound pretty familiar to you. +.. rst-class:: build +.. container:: -.. class:: incremental + This should sound pretty familiar to you. -Classically, Django views were functions. + Classically, Django views were functions. -.. class:: incremental + Version 1.3 added support for Class-based Views (a class with a + ``__call__`` method is a callable) -Version 1.3 added support for Class-based Views (a class with a ``__call__`` -method is a callable) - -A Basic View ------------- +.. nextslide:: A Basic View Let's add a really simple view to our app. -.. class:: incremental - -It will be a stub for our public UI. Add this to ``views.py`` in ``myblog`` - -.. code-block:: python - :class: small incremental - - from django.http import HttpResponse, HttpResponseRedirect, Http404 +.. rst-class:: build +.. container:: - def stub_view(request, *args, **kwargs): - body = "Stub View\n\n" - if args: - body += "Args:\n" - body += "\n".join(["\t%s" % a for a in args]) - if kwargs: - body += "Kwargs:\n" - body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) - return HttpResponse(body, content_type="text/plain") + It will be a stub for our public UI. Add this to ``views.py`` in + ``myblog`` + .. code-block:: python -Hooking It Up -------------- - -We talked in the previous session about the Django urlconf + from django.http import HttpResponse, HttpResponseRedirect, Http404 -.. class:: incremental + def stub_view(request, *args, **kwargs): + body = "Stub View\n\n" + if args: + body += "Args:\n" + body += "\n".join(["\t%s" % a for a in args]) + if kwargs: + body += "Kwargs:\n" + body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) + return HttpResponse(body, content_type="text/plain") -We used our project urlconf to hook the Django admin into our project. +.. nextslide:: Hooking It Up -.. class:: incremental +In your homework tutorial, you learned about Django **urlconfs** -We want to do the same thing for our new app. +.. rst-class:: build +.. container:: -.. class:: incremental + We used our project urlconf to hook the Django admin into our project. -In general, an *app* that serves any sort of views should contain its own -urlconf. + We want to do the same thing for our new app. -.. class:: incremental + In general, an *app* that serves any sort of views should contain its own + urlconf. -The project urlconf should mainly *include* these where possible. + The project urlconf should mainly *include* these where possible. -Adding A Urlconf ----------------- +.. nextslide:: Adding A Urlconf Create a new file ``urls.py`` inside the ``myblog`` app package. -.. container:: incremental +.. rst-class:: build +.. container:: Open it in your editor and add the following code: .. code-block:: python - :class: small - - from django.conf.urls import patterns, url - urlpatterns = patterns('myblog.views', + + from django.conf.urls import url + from myblog.views import stub_view + + urlpatterns = [ url(r'^$', - 'stub_view', + stub_view, name="blog_index"), - ) + ] -Include Blog Urls ------------------ +.. nextslide:: Include Blog Urls In order for our new urls to load, we'll need to include them in our project urlconf -.. container:: incremental +.. rst-class:: build +.. container:: Open ``urls.py`` from the ``mysite`` project package and add this: .. code-block:: python - :class: small - - urlpatterns = patterns('', + + # add this new import + from django.conf.urls import include + + # then modify urlpatterns as follows: + urlpatterns = [ url(r'^', include('myblog.urls')), #<- add this #... other included urls - ) + ] -.. class:: incremental + Try reloading http://localhost:8000/ -Try reloading http://localhost:8000/ + You should see some output now. -.. class:: incremental -You should see some output now. +Project URL Space +----------------- +A project is defined by the urls a user can visit. -A Word On Prefixes ------------------- +.. rst-class:: build +.. container:: -The ``patterns`` function takes a first argument called the *prefix* + What should our users be able to see when they visit our blog? -.. class:: incremental + .. rst-class:: build -When it is not empty, it is added to any view names in ``url()`` calls in the -same ``patterns``. + * A list view that shows blog posts, most recent first. + * An individual post view, showing a single post (a permalink). -.. class:: incremental + Let's add urls for each of these. -In a root urlconf like the one in ``mysite``, this isn't too useful + For now, we'll use the stub view we've created so we can concentrate on the + url routing. -.. class:: incremental +.. nextslide:: Our URLs -But in ``myblog.urls`` it lets us refer to views by simple function name +We've already got a good url for the list page: ``blog_index`` at '/' -.. class:: incremental +.. rst-class:: build +.. container:: -No need to import every view. + For the view of a single post, we'll need to capture the id of the post. + Add this to ``urlpatterns`` in ``myblog/urls.py``: + .. code-block:: python -Project URL Space ------------------ + url(r'^posts/(\d+)/$', + stub_view, + name="blog_detail"), -A project is defined by the urls a user can visit. + ``(\d+)`` captures one or more digits as the post_id. -.. class:: incremental + Load http://localhost:8000/posts/1234/ and see what you get. -What should our users be able to see when they visit our blog? +.. nextslide:: A Word on Capture in URLs -.. class:: incremental +When you load the above url, you should see ``1234`` listed as an *arg* -* A list view that shows blog posts, most recent first. -* An individual post view, showing a single post (a permalink). +.. rst-class:: build +.. container:: -.. class:: incremental + Try changing the route like so: -Let's add urls for each of these, use the stub view for now. + .. code-block:: python + r'^posts/(?P\d+)/$' -Our URLs --------- + Reload the same url. -We've already got a good url for the list page: ``blog_index`` at '/' + Notice the change. -.. container:: incremental + What's going on there? - For the view of a single post, we'll need to capture the id of the post. - Add this to ``urlpatterns``: - - .. code-block:: python - :class: small incremental - - url(r'^posts/(\d+)/$', - 'stub_view', - name="blog_detail"), +.. nextslide:: Regular Expression URLS -.. class:: incremental +Like Pyramid, Django uses Python regular expressions to build routes. -``(\d+)`` captures one or more digits as the post_id. +.. rst-class:: build +.. container:: -.. class:: incremental + Unlike Pyramid, Django *requires* regular expressions to capture segments + in a route. -Load http://localhost:8000/posts/1234/ and see what you get. + When we built our WSGI book app, we used this same appraoch. + There we learned about regular expression *capture groups*. We just changed + an unnamed *capture group* to a named one. -A Word on Capture in URLs -------------------------- + How you declare a capture group in your url pattern regexp influences how + it will be passed to the view callable. -When you load the above url, you should see ``1234`` listed as an *arg* -.. container:: incremental +.. nextslide:: Full Urlconf - Try changing the regexp like so: +.. code-block:: python - .. code-block:: python - :class: small - - r'^posts/(?P\d+)/$' -.. class:: incremental + from django.conf.urls import url + from myblog.views import stub_view -Reload the same url. Notice the change. + urlpatterns = [ + url(r'^$', + stub_view, + name="blog_index"), + url(r'^posts/(?P\d+)/$', + stub_view, + name="blog_detail"), + ] -.. class:: incremental -How you declare a capture group in your url pattern regexp influenced how it -will be passed to the view callable. +.. nextslide:: Testing Views +Before we begin writing real views, we need to add some tests for the views we +are about to create. -Full Urlconf ------------- +.. rst-class:: build +.. container:: + + We'll need tests for a list view and a detail view + + add the following *imports* at the top of ``myblog/tests.py``: + + .. code-block:: python + + import datetime + from django.utils.timezone import utc + + +.. nextslide:: Add a Test Case .. code-block:: python - :class: small - from django.conf.urls import patterns, url + class FrontEndTestCase(TestCase): + """test views provided in the front-end""" + fixtures = ['myblog_test_fixture.json', ] - urlpatterns = patterns('myblog.views', - url(r'^$', - 'stub_view', - name="blog_index"), - url(r'^posts/(?P\d+)/$', - 'stub_view', - name="blog_detail"), - ) + def setUp(self): + self.now = datetime.datetime.utcnow().replace(tzinfo=utc) + self.timedelta = datetime.timedelta(15) + author = User.objects.get(pk=1) + for count in range(1, 11): + post = Post(title="Post %d Title" % count, + text="foo", + author=author) + if count < 6: + # publish the first five posts + pubdate = self.now - self.timedelta * count + post.published_date = pubdate + post.save() -Testing Views +Our List View ------------- -Before we begin, we need to add some tests for the views we are about to -create. +We'd like our list view to show our posts. -.. class:: incremental +.. rst-class:: build +.. container:: -We'll need tests for a list view and a detail view + But in this blog, we have the ability to publish posts. -.. class:: incremental + Unpublished posts should not be seen in the front-end views. -To save us time, I've written these tests already + We set up our tests to have 5 published, and 5 unpublished posts -.. class:: incremental + Let's add a test to demonstrate that the right ones show up. -You can find them in the class resources directory: ``blog_view_tests.py`` +.. nextslide:: Testing the List View -.. class:: incremental +.. code-block:: python -Copy the contents of that file into our blog ``tests.py`` file. + Class FrontEndTestCase(TestCase): # already here + # ... + def test_list_only_published(self): + resp = self.client.get('/') + # the content of the rendered response is always a bytestring + resp_text = resp.content.decode(resp.charset) + self.assertTrue("Recent Posts" in resp_text) + for count in range(1, 11): + title = "Post %d Title" % count + if count < 6: + self.assertContains(resp, title, count=1) + else: + self.assertNotContains(resp, title) +.. rst-class:: build +.. container:: -Run The Tests -------------- + We test first to ensure that each published post is visible in our view. -:: + Note that we also test to ensure that the unpublished posts are *not* visible. - (djangoenv)$ python manage.py test myblog + +.. nextslide:: Run Your Tests + +.. code-block:: bash + + (djangoenv)$ ./manage.py test myblog + Creating test database for alias 'default'... + .F. + ====================================================================== + FAIL: test_list_only_published (myblog.tests.FrontEndTestCase) ... - ---------------------------------------------------------------------- - Ran 7 tests in 0.478s + Ran 3 tests in 0.024s - FAILED (failures=2) + FAILED (failures=1) Destroying test database for alias 'default'... -Our First View --------------- +.. nextslide:: Now Fix That Test! Add the view for listing blog posts to ``views.py``. - + .. code-block:: python - :class: small # add these imports from django.template import RequestContext, loader from myblog.models import Post - + # and this view def list_view(request): published = Post.objects.exclude(published_date__exact=None) @@ -756,97 +729,96 @@ Add the view for listing blog posts to ``views.py``. return HttpResponse(body, content_type="text/html") -Getting Posts -------------- +.. nextslide:: Getting Posts .. code-block:: python - :class: small published = Post.objects.exclude(published_date__exact=None) posts = published.order_by('-published_date') -.. class:: incremental - -We begin by using the QuerySet API to fetch all the posts that have -``published_date`` set +.. rst-class:: build +.. container:: -.. class:: incremental + We begin by using the QuerySet API to fetch all the posts that have + ``published_date`` set -Using the chaining nature of the API we order these posts by -``published_date`` + Using the chaining nature of the API we order these posts by + ``published_date`` -.. class:: incremental + Remember, at this point, no query has actually been issued to the database. -Remember, at this point, no query has actually been issued to the database. - -Getting a Template ------------------- +.. nextslide:: Getting a Template .. code-block:: python - :class: small template = loader.get_template('list.html') -.. class:: incremental +.. rst-class:: build +.. container:: -Django uses configuration to determine how to find templates. + Django uses configuration to determine how to find templates. -.. class:: incremental + By default, Django looks in installed *apps* for a ``templates`` directory -By default, Django looks in installed *apps* for a ``templates`` directory + It also provides a place to list specific directories. -.. class:: incremental + Let's set that up in ``settings.py`` -It also provides a place to list specific directories. -.. class:: incremental +.. nextslide:: Project Templates -Let's set that up in ``settings.py`` +Notice that ``settings.py`` already contains a ``BASE_DIR`` value which points +to the root of our project (where both the project and app packages are +located). +.. rst-class:: build +.. container:: -Project Templates ------------------ + In that same file, you'll find a list bound to the symbol ``TEMPLATES``. -In ``settings.py`` find ``TEMPLATE_DIRS`` and add the absolute path to your -``mysite`` project package: + That list contains one dict with an empty list at the key ``DIRS``. Update + that empty list as shown here: -.. code-block:: python - :class: small + .. code-block:: python - TEMPLATE_DIRS = ('/absolute/path/to/mysite/mysite/templates', ) + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'mysite/templates')], + ... + }, + ] -.. class:: incremental + This will ensure that Django will look in your ``mysite`` project folder + for a directory containing templates. -Then add a ``templates`` directory to your ``mysite`` project package +.. nextslide:: -.. class:: incremental +The ``mysite`` project folder does not contain a ``templates`` directory, add one. -Finally, in that directory add a new file ``base.html`` and populate it with -the following: +.. rst-class:: build +.. container:: + Then, in that directory add a new file ``base.html`` and add the following: -base.html ---------- + .. code-block:: jinja -.. code-block:: jinja - :class: small - - - - - My Django Blog - - -

    -
    - {% block content %} - [content will go here] - {% endblock %} -
    -
    - - + + + + My Django Blog + + +
    +
    + {% block content %} + [content will go here] + {% endblock %} +
    +
    + + Templates in Django @@ -854,57 +826,41 @@ Templates in Django Before we move on, a quick word about Django templates. -.. class:: incremental - -We've seen Jinja2 which was "inspired by Django's templating system". +.. rst-class:: build +.. container:: -.. class:: incremental + We've seen Jinja2 which was "inspired by Django's templating system". -Basically, you already know how to write Django templates. + Basically, you already know how to write Django templates. -.. class:: incremental + Django templates **do not** allow any python expressions. -Django templates **do not** allow any python expressions. + https://docs.djangoproject.com/en/1.9/ref/templates/builtins/ -.. class:: incremental center small -https://docs.djangoproject.com/en/1.5/ref/templates/builtins/ - - -Blog Templates --------------- +.. nextslide:: Blog Templates Our view tries to load ``list.html``. -.. class:: incremental - -This template is probably specific to the blog functionality of our site - -.. class:: incremental +.. rst-class:: build +.. container:: -It is common to keep shared templates in your project directory and -specialized ones in app directories. + This template is probably specific to the blog functionality of our site -.. class:: incremental + It is common to keep shared templates in your project directory and + specialized ones in app directories. -Add a ``templates`` directory to your ``myblog`` app, too. + Add a ``templates`` directory to your ``myblog`` app, too. -.. class:: incremental + In it, create a new file ``list.html`` and add this: -In it, create a new file ``list.html`` and add this: - -list.html ---------- +.. nextslide:: ``list.html`` .. code-block:: jinja - :class: tiny - - {% extends "base.html" %} - {% block content %} + {% extends "base.html" %}{% block content %}

    Recent Posts

    - {% comment %} here is where the query happens {% endcomment %} {% for post in posts %}
    @@ -925,107 +881,96 @@ list.html {% endblock %} -Template Context ----------------- +.. nextslide:: Template Context .. code-block:: python - :class: small context = RequestContext(request, { 'posts': posts, }) body = template.render(context) -.. class:: incremental - -Like Jinja2, django templates are rendered by passing in a *context* +.. rst-class:: build +.. container:: -.. class:: incremental + Like Jinja2, django templates are rendered by passing in a *context* -Django's RequestContext provides common bits, similar to the global context in -Flask + Django's RequestContext provides common bits, similar to the context + provided automatically by Pyramid -.. class:: incremental + We add our posts to that context so they can be used by the template. -We add our posts to that context so they can be used by the template. - -Return a Response ------------------ +.. nextslide:: Return a Response .. code-block:: python - :class: small return HttpResponse(body, content_type="text/html") -.. class:: incremental - -Finally, we build an HttpResponse and return it. +.. rst-class:: build +.. container:: -.. class:: incremental + Finally, we build an HttpResponse and return it. -This is, fundamentally, no different from the ``stub_view`` just above. + This is, fundamentally, no different from the ``stub_view`` just above. - -Fix URLs --------- +.. nextslide:: Fix URLs We need to fix the url for our blog index page -.. container:: incremental +.. rst-class:: build +.. container:: Update ``urls.py`` in ``myblog``: .. code-block:: python - :class: small - + + # import the new view + from myblog.views import list_view + + # and then update the urlconf url(r'^$', - 'list_view', + list_view, #<-- Change this value from stub_view name="blog_index"), -.. class:: incremental small + Then run your tests again: -:: + .. code-block:: bash - (djangoenv)$ python manage.py test myblog - ... - Ran 7 tests in 0.494s - FAILED (failures=1) + (djangoenv)$ ./manage.py test myblog + ... + Ran 3 tests in 0.033s + OK -Common Patterns ---------------- + +.. nextslide:: Common Patterns This is a common pattern in Django views: -.. class:: incremental +.. rst-class:: build * get a template from the loader -* build a context, usually using a RequestContext +* build a context, usually using a RequestContext * render the template * return an HttpResponse -.. class:: incremental - -So common in fact that Django provides two shortcuts for us to use: +.. rst-class:: build +.. container:: -.. class:: incremental + So common in fact that Django provides a shortcut for us to use: -* ``render(request, template[, ctx][, ctx_instance])`` -* ``render_to_response(template[, ctx][, ctx_instance])`` + ``render(request, template[, ctx][, ctx_instance])`` -Shorten Our View ----------------- +.. nextslide:: Shorten Our View Let's replace most of our view with the ``render`` shortcut .. code-block:: python - :class: small - # replace RequestContext and loader import - from django.shortcuts import render - + from django.shortcuts import render # <- already there + # rewrite our view def list_view(request): published = Post.objects.exclude(published_date__exact=None) @@ -1033,39 +978,82 @@ Let's replace most of our view with the ``render`` shortcut context = {'posts': posts} return render(request, 'list.html', context) -.. class:: incremental +.. rst-class:: build Remember though, all we did manually before is still happening -Detail View ------------ +BREAK TIME +---------- -Next, let's write a view function for the detail view of a post +We've got the front page for our application working great. -.. container:: incremental +Next, we'll need to provide a view of a detail page for a single post. - It should have the following signature: +Then we'll provide a way to log in and to navigate between the public part of +our application and the admin behind it. - .. code-block:: python - :class: small - - detail_view(request, post_id) +If you've fallen behind, the app as it stands now is in our class resources as +``mysite_stage_2`` -.. class:: incremental -We will call the template ``detail.html`` +Our Detail View +--------------- -.. class:: incremental +Next, let's add a view function for the detail view of a post -Let's start with the code in ``views.py`` +.. rst-class:: build +.. container:: + It will need to get the ``id`` of the post to show as an argument -detail_view ------------ + Like the list view, it should only show published posts + + But unlike the list view, it will need to return *something* if an + unpublished post is requested. + + Let's start with the tests in ``views.py`` + + +.. nextslide:: Testing the Details + +Add the following test to our ``FrontEndTestCase`` in ``myblog/tests.py``: + +.. code-block:: python + + def test_details_only_published(self): + for count in range(1, 11): + title = "Post %d Title" % count + post = Post.objects.get(title=title) + resp = self.client.get('/posts/%d/' % post.pk) + if count < 6: + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, title) + else: + self.assertEqual(resp.status_code, 404) + + +.. nextslide:: Run Your Tests + +.. code-block:: bash + + (djangoenv)$ ./manage.py test myblog + Creating test database for alias 'default'... + .F.. + ====================================================================== + FAIL: test_details_only_published (myblog.tests.FrontEndTestCase) + ... + Ran 4 tests in 0.043s + + FAILED (failures=1) + Destroying test database for alias 'default'... + + +.. nextslide:: Let's Fix That Test + +Now, add a new view to ``myblog/views.py``: .. code-block:: python - :class: incremental small def detail_view(request, post_id): published = Post.objects.exclude(published_date__exact=None) @@ -1076,25 +1064,38 @@ detail_view context = {'post': post} return render(request, 'detail.html', context) -.. class:: incremental -All models raise a DoesNotExist exception if ``get`` returns nothing. +.. nextslide:: Missing Content -.. class:: incremental +.. code-block:: python -We can use that fact to raise a Not Found exception. + try: + post = published.get(pk=post_id) + except Post.DoesNotExist: + raise Http404 -.. class:: incremental +One of the features of the Django ORM is that all models raise a DoesNotExist +exception if ``get`` returns nothing. -Django will handle the rest for us. +.. rst-class:: build +.. container:: + This exception is actually an attribute of the Model you look for. -detail.html ------------ + There's also an ``ObjectDoesNotExist`` for when you don't know which model + you have. + + We can use that fact to raise a Not Found exception. + + Django will handle the rest for us. + + +.. nextslide:: Add the Template + +We also need to add ``detail.html`` to ``myblog/templates``: .. code-block:: jinja - :class: small - + {% extends "base.html" %} {% block content %} @@ -1114,92 +1115,82 @@ detail.html {% endblock %} -Hook it Up ----------- +.. nextslide:: Hook it Up In order to view a single post, we'll need a link from the list view -.. container:: incremental +.. rst-class:: build +.. container:: - We can use the ``url`` template tag (like flask url_for): + We can use the ``url`` template tag (like Pyramid's ``request.route_url``): .. code-block:: jinja - :class: small - + {% url '' arg1 arg2 %} -.. class:: incremental + In our ``list.html`` template, let's link the post titles: -In our ``list.html`` template, let's link the post titles: + .. code-block:: jinja -.. code-block:: jinja - :class: small incremental - - {% for post in posts %} -
    -

    - {{ post }} -

    - ... + {% for post in posts %} +
    +

    + {{ post }} +

    + ... -Fix URLs --------- +.. nextslide:: Fix URLs -Again, we need to insert our new view into the existing ``urls.py`` in +Again, we need to insert our new view into the existing ``myblog/urls.py`` in ``myblog``: .. code-block:: python - :class: small - + + # import the view + from myblog.views import detail_view + url(r'^posts/(?P\d+)/$', - 'detail_view', + detail_view, #<-- Change this from stub_view name="blog_detail"), -.. class:: incremental small +.. rst-class:: build small :: - (djangoenv)$ python manage.py test myblog + (djangoenv)$ ./manage.py test myblog ... - Ran 7 tests in 0.513s + Ran 4 tests in 0.077s + OK -A Moment To Play ----------------- +.. nextslide:: A Moment To Play We've got some good stuff to look at now. Fire up the server -.. class:: incremental - -Reload your blog index page and click around a bit. +.. rst-class:: build +.. container:: -.. class:: incremental + Reload your blog index page and click around a bit. -You can now move back and forth between list and detail view. + You can now move back and forth between list and detail view. -.. class:: incremental + Try loading the detail view for a post that doesn't exist -Try loading the detail view for a post that doesn't exist - -Congratulations ---------------- +.. nextslide:: Congratulations You've got a functional Blog -.. class:: incremental - -It's not very pretty, though. +.. rst-class:: build +.. container:: -.. class:: incremental + It's not very pretty, though. -We can fix that by adding some css + We can fix that by adding some css -.. class:: incremental - -This gives us a chance to learn about Django's handling of *static files* + This gives us a chance to learn about Django's handling of *static files* Static Files @@ -1207,79 +1198,81 @@ Static Files Like templates, Django expects to find static files in particular locations -.. class:: incremental +.. rst-class:: build +.. container:: -It will look for them in a directory named ``static`` in any installed apps. + It will look for them in a directory named ``static`` in any installed + apps. -.. class:: incremental + They will be served from the url path in the STATIC_URL setting. -They will be served from the url path in the STATIC_URL setting. + By default, this is ``/static/`` -.. class:: incremental + To allow Django to automatically build the correct urls for your static + files, you use a special *template tag*:: -By default, this is ``/static/`` + {% static %} -Add CSS -------- +.. nextslide:: Add CSS I've prepared a css file for us to use. You can find it in the class resources -.. class:: incremental +.. rst-class:: build +.. container:: -Create a new directory ``static`` in the ``myblog`` app. + Create a new directory ``static`` in the ``myblog`` app. -.. class:: incremental + Copy the ``django_blog.css`` file into that new directory. -Copy the ``django_css`` file into that new directory. + .. container:: -.. container:: incremental + Next, load the static files template tag into ``base.html`` (this + **must** be on the *first line* of the template): - Then add this link to the of ``base.html``: + .. code-block:: jinja - .. code-block:: html - :class: small - - My Django Blog - + {% load staticfiles %} + .. container:: -View Your Results ------------------ + Finally, add a link to the stylesheet using the special template tag: -Reload http://localhost:8000/ and view the results of your work + .. code-block:: html + + My Django Blog + -.. class:: incremental -We now have a reasonable view of the posts of our blog on the front end +.. nextslide:: View Your Results -.. class:: incremental +Reload http://localhost:8000/ and view the results of your work -And we have a way to create and categorize posts using the admin +.. rst-class:: build +.. container:: -.. class:: incremental + We now have a reasonable view of the posts of our blog on the front end -However, we lack a way to move between the two. + And we have a way to create and categorize posts using the admin -.. class:: incremental + However, we lack a way to move between the two. -Let's add that ability next. + Let's add that ability next. -Adding A Control Bar --------------------- +Global Navigation +----------------- We'll start by adding a control bar to our ``base.html`` template: .. code-block:: jinja - :class: small ...