Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Web Development with Python and Django (2015)

Mike Pirnat
January 06, 2015

Web Development with Python and Django (2015)

Slide deck from the CodeMash 2015 edition of the "Web Development with Python and Django" tutorial by Mike Pirnat and David Stanek, featuring the "Dungeons & Djangos" sample application.

Mike Pirnat

January 06, 2015
Tweet

More Decks by Mike Pirnat

Other Decks in Technology

Transcript

  1. Today •Iteratively build a full-featured site •Describe a feature we

    need •Implement a feature together •Review our example solution • Keep yours? git show tag • Follow directly? git reset --hard tag
  2. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  3. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  4. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  5. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  6. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  7. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  8. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  9. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  10. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  11. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  12. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  13. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  14. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  15. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  16. from module.submodule import SomeClass class MyClass(SomeClass): "This explains what the

    class is about." def method(self, x, y=1): "This explains what the method does." dictionary = {'key1': 'val1', 'key2': 'val2'} if y == 1: return ["Hello", dictionary['key1']] else: return ["World", dictionary.get(x, 'val3')] instance = MyClass() for word in instance.method(42): print(word)
  17. Django! •A high-level Python web framework •Encourages rapid development and

    clean, pragmatic design •“For perfectionists with deadlines” •Focus on automation and DRY •Widely supported, many deployment options
  18. Django •ORM •Automatic admin interface •Regex-based URL design •Templating system

    •Cache infrastructure •Internationalization •Command-line job framework
  19. Download and Install •Python 3.4 – https://python.org/downloads (or get it

    from your Linux distro’s package manager) •Git – http://git-scm.com
  20. Got Ubuntu? •Ubuntu’s Python 3.4 is currently broken •Prevents normal

    setup of virtual environments •We have a cloud server already set up that you can use –104.130.195.216 •Or make a new friend
  21. Got Windows? •Be sure to update the PATH so that

    Windows can find your scripts: C:\Python34\;C:\Python34\Scripts\;C:\Python34\Tools \Scripts
  22. Installing Packages •pip install package •Installed packages go into a

    site-packages directory in your Python lib •That’s the “system Python” by default •But different programs may need different versions of packages... •So we have virtual environments!
  23. Virtual Environments •Creates an isolated Python environment with its own

    site-packages •Install whatever you want without fouling anything else up •Now part of the standard library
  24. Create & Activate the Virtual Environment # Mac/Linux/etc... $ pyvenv

    django-tutorial-v2 $ cd django-tutorial-v2 $ source bin/activate # Windows > pyvenv.py django-tutorial-v2 > cd django-tutorial-v2 > Scripts\activate.bat
  25. Get the Example Code # Normally... $ git init src

    # Today... $ git clone https://github.com/mpirnat/django-tutorial- v2.git ./src
  26. Installing Requirements # Still in the 'src' directory from before...

    # Mac/Linux/etc... $ cd src $ pip install -r requirements.txt # Windows > cd src > pip.exe install -r requirements.txt
  27. Check Prerequisites # In that same 'src' directory... # Mac/Linux/etc...

    $ python prerequisites.py # Windows > python.exe prerequisites.py
  28. Rewind to the Start of Exercises # In that same

    'src' directory... $ git reset --hard exercise01.1
  29. “Dungeons & Djangos” •Web-based fantasy role playing game •Inspired by

    D&D (5E is great, btw) •We’ll build the core of the app: • character creation • user registration, login/logout • admin interface • a fancy landing page
  30. Starting a Project •Mac/Linux: $ django-admin.py startproject projectname ./ $

    python manage.py runserver •Windows: > python Scripts\django-admin.py startproject projectname > python manage.py runserver
  31. git reset --hard exercise01.1 | git show solution01.1 Exercise 1.1

    •Start a new project “mysite” •Run the server •Open your browser to localhost:8000
  32. Start an App •Django believes strongly in separating chunks of

    a site into apps that can be reused •Ecosystem of reusable apps available •Create an app; from the src directory: $ python manage.py startapp myapp •Add it to INSTALLED_APPS in settings.py
  33. git reset --hard exercise01.2 | git show solution01.2 Exercise 1.2

    •Start a new app named “characters”
  34. git reset --hard exercise01.3 | git show solution01.3 Exercise 1.3

    •Add the characters app to the list of INSTALLED_APPS in settings.py
  35. Models •Model classes are the nouns of the system •Used

    to create database tables •Integrated with the ORM to interact with the database •Need to create & run migrations when adding or changing model classes
  36. git reset --hard exercise02.1 | git show solution02.1 Exercise 2.1

    •Create a Character model; it should have: • name – text, up to 200 characters • background – text, open-ended • level, experience_points, max_hit_points, current_hit_points – integers • strength, dexterity, constitution, intelligence, wisdom, charisma – integers • created, modified – datetimes, automatically populated • a __str__ method that returns the character’s name
  37. Migrations •Propagates model changes into the database •Keeps a record

    of changes made to the models •Can migrate forward and backward •Formerly provided by third-party tools •New in Django 1.7, required by Django 1.9
  38. Making & Running Migrations •Creating a migration for an app:

    $ python manage.py makemigrations <app> •See the SQL that will be executed: $ python manage.py sqlmigrate <app> <number> •Run migrations: $ python manage.py migrate
  39. git reset --hard exercise02.2 | git show solution02.2 Exercise 2.2

    •Create a migration for the initial version of the Character model •Run the migration
  40. Trying Out the Model •Start a Python shell with all

    the Django stuff all ready to go: $ python manage.py shell •Now we can test out our model
  41. Try the Admin Interface •Django comes with a built-in admin

    interface to explore and manipulate models •Runs on /admin/ by default •Need to create an admin superuser: $ python manage.py createsuperuser •Need to register models in the app’s admin.py
  42. git reset --hard exercise02.3 | git show solution02.3 Exercise 2.3

    •Create a superuser for using the admin interface •Register the Character model for use with the admin interface •Open your browser to localhost:8000/ admin
  43. git reset --hard exercise03.1 | git show solution03.1 Exercise 3.1

    •Add defaults to the Character model: • level – default to 1 • experience_points – default to 0 • max_hit_points, current_hit_points – default to 10 • stats (strength, dexterity, etc.) – use a function that uses Python’s random.randint to model a 3d6 dice roll
  44. git reset --hard exercise03.2 | git show solution03.2 Exercise 3.2

    •Extract dice rolling from stat generation so that we can model any XdY + Z dice roll •Can do it in a one-line function •Don’t panic — this is the weirdest the code gets today!
  45. Choices •When a field can have only a finite set

    of values that won’t change often, use choices •Can define both database native values and their human-friendly equivalents that will be displayed in the app
  46. git reset --hard exercise04.1 | git show solution04.1 Exercise 4.1

    •Add alignment to the Character model; it should have a fixed set of choices: • Lawful Good • Lawful Neutral • Lawful Evil • Neutral Good • Neutral • Neutral Evil •It should default to Neutral • Chaotic Good • Chaotic Neutral • Chaotic Evil
  47. git reset --hard exercise04.2 | git show solution04.2 Exercise 4.2

    •Create the migration to add alignment to the Character model •Run the migration
  48. Foreign Keys •Use a separate model when the data is

    too rich for a simple set of choices •When a model (Character) can be related to only one of some other model (Race, Class), use a ForeignKey •When using the model, need an instance: hobbit = Race('hobbit').save() bilbo = Character(..., race=hobbit)
  49. Foreign Keys •Have to be careful when adding non-null fields

    to an existing model, doubly so when it’s a ForeignKey •Migration will ask for a default •If adding a new model and an FK relationship, no defaults yet! •Do it in several steps...
  50. git reset --hard exercise05.1 | git show solution05.1 Exercise 5.1

    •Create very basic models for Race and Class; they should each have: • name – text, up to 200 characters • description – text, open-ended • a __str__ method that returns the name
  51. git reset --hard exercise05.2 | git show solution05.2 Exercise 5.2

    •Create the migration to create the Race and Class models in the database •Run the migration
  52. git reset --hard exercise05.3 | git show solution05.3 Exercise 5.3

    •Add a ForeignKey relationship from the Race model to the Character model •Add a ForeignKey relationship from the Class model to the Character model •Be careful about names here; “class” is a Python builtin that we don’t want to shadow
  53. Data Migrations •Adding default values into a DB is best

    accomplished with data migrations •Allow you to reliably populate data at the right point in the evolution of the DB •To create one: $ python manage.py makemigrations --empty appname
  54. git reset --hard exercise05.4 | git show solution05.4 Exercise 5.4

    •Create a data migration that populates a default race (“Human”) and class (“Generic”) into the database •Run the migration
  55. git reset --hard exercise05.5 | git show solution05.5 Exercise 5.5

    •Create a migration to add the ForeignKey relationships for Character/Race and Character/Class •Run the migration •When prompted for a default for the FK, enter the id you want (in this case 1 to match the data migration)
  56. git reset --hard exercise05.6 | git show solution05.6 Exercise 5.6

    •Register the Race and Class models for use with the admin interface •What do you notice about the Class model in the admin interface?
  57. git reset --hard exercise05.7 | git show solution05.7 Exercise 5.7

    •Fix the pluralization of “class” in the admin interface by adding verbose_name_plural metadata to the Class model •Observe the change in the admin app •What do you notice about how the class appears when looking at a character?
  58. git reset --hard exercise05.8 | git show solution05.8 Exercise 5.8

    •Add a verbose_name of “class” to the Character/Class ForeignKey in the Character model •Observe the change in the admin interface
  59. git reset --hard exercise05.9 | git show solution05.9 Exercise 5.9

    •Create a migration to sync up the previous change •Run the migration
  60. Many-to-Many Relations •When a model (Character) can be related to

    many of some other model (Item), use a ManyToManyField •When using the model, need a list of instances: arkenstone = Item(name='Arkenstone').save() sting = Item(name='Sting (shortsword)').save() bilbo.inventory = [arkenstone, sting]
  61. git reset --hard exercise06.1 | git show solution06.1 Exercise 6.1

    •Create an Item model; it should have: • name – text, up to 200 characters • description – text, open-ended • value – integer, default to 0 • weight – integer, default to 1 • a __str__ method that returns the name •Add a ManyToMany relation between Character and Item to the Character
  62. git reset --hard exercise06.2 | git show solution06.2 Exercise 6.2

    •Create a migration to add the Item model and the ManyToMany relation to the database •Run the migration
  63. git reset --hard exercise06.3 | git show solution06.3 Exercise 6.3

    •Register the Item model for use with the admin interface •Create an Item using the admin interface •Edit a Character in the admin interface; add an item to it •Create a Character without any items in the admin interface; what do you notice?
  64. git reset --hard exercise06.4 | git show solution06.4 Exercise 6.4

    •Change the ManyToMany to be nullable and blank
  65. git reset --hard exercise06.5 | git show solution06.5 Exercise 6.5

    •Create a migration to alter the relationship •Run the migration •Observe the change in the admin app
  66. git reset --hard exercise07.1 | git show solution07.1 Exercise 7.1

    •Modify the admin wiring to group character fields in meaningful groups: • name, level, class, background • max hit points, current hit points • stats (strength, dexterity, constitution, intelligence, wisdom, charisma) • experience points, inventory
  67. git reset --hard exercise07.2 | git show exercise07.2 Exercise 7.2

    •Make the character’s creation and modification dates appear in the admin interface •Show the creation and modification dates in a separate fieldset
  68. git reset --hard exercise07.3 | git show solution07.3 Exercise 7.3

    •Add more fields to the character list in the admin interface: • name • level • race • class • creation & modification dates
  69. git reset --hard exercise07.4 | git show solution07.4 Exercise 7.4

    •Add filters for race, class, creation, and modification date to the admin interface
  70. git reset --hard exercise07.5 | git show solution07.5 Exercise 7.5

    •Change the model name of Race.name to “race” •Change the model name of Class.name to “class”
  71. git reset --hard exercise07.6 | git show solution07.6 Exercise 7.6

    •Create the migration for adding model names to Race.name and Class.name •Run the migration
  72. git reset --hard exercise07.7 | git show solution07.7 Exercise 7.7

    •Add search capability to the character list in the admin interface •Make characters searchable by name, background, race, and class
  73. URLs •Map URLs in requests to code that can be

    executed •Regular expressions! •Each app has its own urls.py
  74. Views •Code that handles requests •Other frameworks often call these

    “controllers” •Basically a function that: • gets a request passed to it • returns text or a response
  75. git reset --hard exercise08.1 | git show solution08.1 Exercise 8.1

    •Create a simple view in the characters app •It can just emit “hello world” for now •Create a URL mapping for /characters/ to this view
  76. git reset --hard exercise08.2 | git show solution08.2 Exercise 8.2

    •Create a stub view for viewing details of a single character •The URL should include the character_id •The view should say which character_id is being displayed •The view does NOT need to query the DB
  77. git reset --hard exercise08.3 | git show solution08.3 Exercise 8.3

    •Change the character index view to return a list of all characters •Change the character detail view to return the requested single character
  78. Django Template Language •Call a function or do logic: {%

    ... %} •Variable substitution: {{ bar }} •Filters: {{ foo|bar }}
  79. git reset --hard exercise09.1 | git show solution09.1 Exercise 9.1

    •Create a template to list characters •Change the character list view to use the template
  80. git reset --hard exercise09.2 | git show solution09.2 Exercise 9.2

    •Create a template to show the details of a character •Change the character detail view to use the template
  81. git reset --hard exercise09.3 | git show solution09.3 Exercise 9.3

    •Change the character details template to show a character’s inventory
  82. Relations in Templates •When a model object retrieved from the

    ORM has relations, you get a relation manager and not an actual iterable collection •Need to call .all() (or get or filter) on it before you get back the related model objects
  83. Optimizing Queries •Be careful using relations in loops in templates;

    can result in many extra queries •Go read up on: • select_related: does a join in SQL • prefetch_related: queries in advance, caches results, allows “join” in Python
  84. git reset --hard exercise09.4 | git show solution09.4 Exercise 9.4

    •Change the character details view to return a 404 if it can’t find a character
  85. git reset --hard exercise09.5 | git show solution09.5 Exercise 9.5

    •Change the character details view to use the get_object_or_404 shortcut
  86. git reset --hard exercise09.6 | git show solution09.6 Exercise 9.6

    •Change the templates to use dynamically generated URLs in links
  87. git reset --hard exercise09.7 | git show solution09.7 Exercise 9.7

    •Add a namespace to the characters app URLs to prevent collisions with other apps •Update dynamic links in templates to include the namespace
  88. Form Validation •Why? •Classes for each kind of input field

    •Form class to gather input fields •View uses the form class to validate inputs
  89. git reset --hard exercise10.1 | git show solution10.1 Exercise 10.1

    •Create a page with a simple form to create new characters •Just show minimal fields (name, background) for now •Default race and class to the defaults we created earlier •Redirect to the character list
  90. git reset --hard exercise10.2 | git show solution10.2 Exercise 10.2

    •Change how race and class are set to reduce the number of database queries during character creation
  91. Class-Based Views • Separate methods for separate HTTP methods instead

    of conditional logic • Ease development for common cases • Enables advanced designs • Optional; our example code today doesn’t use them
  92. Generic Views •Many views fall into the same patterns •Django

    provides generic view classes for things like showing a list of objects, creating an object, updating an object, deleting, etc. •Subclass and set properties or override certain methods to customize
  93. git reset --hard exercise11.1 | git show solution11.1 Exercise 11.1

    •Change the character list to use a generic ListView •Change the character detail view to use a generic DetailView
  94. git reset --hard exercise11.2 | git show solution11.2 Exercise 11.2

    •Change the character list queryset to order results by character name
  95. git reset --hard exercise11.3 | git show solution11.3 Exercise 11.3

    •Change the character creation view to use a generic CreateView
  96. Model Forms •Forms must declare every field explicitly •ModelForms read

    a model and automatically create the necessary fields •Can quickly customize •Use with caution; exclude fields which should not be visible/editable
  97. git reset --hard exercise12.1 | git show solution12.1 Exercise 12.1

    •Refactor character creation to use a ModelForm
  98. git reset --hard exercise12.2 | git show solution12.2 Exercise 12.2

    •During character creation, only show the fields the user can choose: • name • background • race • class • alignment
  99. git reset --hard exercise12.3 | git show solution12.3 Exercise 12.3

    •Show the stats (strength, dexterity, constitution, intelligence, wisdom, charisma) as read only during character creation •The solution is for illustration only; it has some security issues outside of our scope
  100. git reset --hard exercise13.1 | git show solution13.1 Exercise 13.1

    •Create a page to update characters using a generic UpdateView
  101. git reset --hard exercise13.2 | git show exercise13.2 Exercise 13.2

    •Create a page to delete characters using a generic DeleteView
  102. No Need to Reinvent •Django comes with a robust user

    framework: django.contrib.auth •Registration •Login/Logout •Password Recovery •Etc.
  103. git reset --hard exercise14.1 | git show solution14.1 Exercise 14.1

    •Start a new “accounts” app for managing user accounts
  104. git reset --hard exercise14.2 | git show solution14.2 Exercise 14.2

    •Create a page for registering users •Use django.contrib.auth’s user creation form •Provide our own template and view; base the view on a generic FormView •Don’t forget URLs and settings.py!
  105. The User Object •Always have one in every request •Always

    available to templates •Can be anonymous or populated depending on whether the user has authenticated
  106. git reset --hard exercise14.3 | git show solution14.3 Exercise 14.3

    •Change the character list template •Show the username when signed in •Show the registration link when signed out
  107. More Reusable Goodness •django.contrib.auth provides URLs and views for login

    and logout •Will want to provide our own templates
  108. git reset --hard exercise15.1 | git show solution15.1 Exercise 15.1

    •Enable user login and logout •Create templates for login and logged out pages •Use django.contrib.auth views •Link to the login/logout in the character list template •Try it out; what do you observe?
  109. git reset --hard exercise15.2 | git show solution15.2 Exercise 15.2

    •Change the default login redirect URL to the character list page (/characters)
  110. git reset --hard exercise15.3 | git show solution15.3 Exercise 15.3

    •Fix the template resolution order for logout
  111. git reset --hard exercise15.4 | git show solution15.4 Exercise 15.4

    •Create a base template for the site •Change all existing templates to extend the base template •Move the header navigation block (with our login/logout and registration links) to the base template
  112. git reset --hard exercise16.1 | git show solution16.1 Exercise 16.1

    •Add a ForeignKey relationship between Characters and Users
  113. git reset --hard exercise16.2 | git show solution16.2 Exercise 16.2

    •Create the migration to add the Character/ User relationship •Run the migration
  114. git reset --hard exercise16.3 | git show solution16.3 Exercise 16.3

    •Add the User that created a Character to the admin fields
  115. git reset --hard exercise16.4 | git show solution16.4 Exercise 16.4

    •Add the player username to the player details page
  116. git reset --hard exercise16.5 | git show solution16.5 Exercise 16.5

    •Require the user to be logged in when creating, editing, or deleting a character
  117. git reset --hard exercise16.6 | git show solution16.6 Exercise 16.6

    •When creating a character, save the user who created the character
  118. Model Managers •A place to encapsulate data queries •Extend to

    provide extra queries with developer-friendly interfaces •Generic views can restrict access by limiting the queryset available in the view
  119. git reset --hard exercise17.1 | git show solution17.1 Exercise 17.1

    •Create a model manager for the Character model •Use the model manager to lock down the edit and delete functions
  120. git reset --hard exercise17.2 | git show solution17.2 Exercise 17.2

    •Remove the edit and delete character links when the user doesn’t own the character
  121. git reset --hard exercise18.1 | git show solution18.1 Exercise 18.1

    •Add a basic landing page on the root URL •Include a spiffy logo image
  122. git reset --hard exercise18.1 | git show solution18.2 Exercise 18.2

    •Make sure our pages have awesome branding •Put a small logo on every page
  123. git reset --hard bonus | git show bonus Bonus Ideas

    •User profiles •Character pictures •Parties (groups of characters) •Rooms/locations •Combat •Loot! •Experience points/ leveling up •Weapons & armor •Race and class, for real •Spells •Multi-classing •REST API
  124. Where to Deploy •Gondor.io •Heroku •Webfaction •Your favorite VPS (Rackspace,

    AWS, etc.) •Google App Engine (if that’s what you’re into)
  125. Production Advice •Don’t enable admin in production •Don’t enable debug

    in production •Don’t enable the Django Debug Toolbar in production •Don’t publish your production settings & secrets on github
  126. Credits • Image from Django Unchained by Sony Pictures http://www.nydailynews.com/entertainment/tv-movies/django-star-foxx-life-

    built-race-article-1.1220275 • Image of Django Reinhardt by ~Raimondsy http://raimondsy.deviantart.com/art/Django-Reinhardt-314914547 • Image of Gaming Dice http://www.clker.com/cliparts/R/y/7/z/q/z/role-playing-dice-hi.png • Other images from ClipArt Etc. http://etc.usf.edu/clipart/