In the first part of our Choose Your Own Adventure Presentations Wizard Mode tutorial we set up the necessary code for separating authorized wizards from non-wizards. However, logging in and out of an application isn’t very magical. It’s time to write write some new spells in Wizards Only mode to allow us to deftly manipulate our presentations.
Even Wizards Need Web Forms
The Wizards Only mode interface we’re creating throughout this post will grant us the ability to see which presentations are available as well as create and edit metadata, such as whether a presentation is visible or invisible to non-wizards.
Time to get coding and create for our Wizards Only mode.
What We’ll Need
If you followed along with part 1 of this series then you’ve already set up all of the dependencies and the database connections we’ll need for this part of the tutorial.
If you haven’t read part 1 yet, that’s okay. Here’s a quick recap of what we used to build the application so far.
- PostgreSQL for persistent storage. We used PostgreSQL in part 1 and will expand our usage in this post.
- The psycopg2 Python driver to connect to PostgreSQL. Psycopg2 will continue to drive the connect between our web application and the database.
- Flask-login for authentication. In this post we’ll use Flask-login to protect Wizard Only pages.
- Flask-WTF for web form handling. We’re creating a bunch of new pages in this post so we’ll use Flask-WTF extensively in the following sections.
You can get caught up with us by working through the code in part 1 or just clone the CYOA Presentations repository tutorial-step-4 tag stage. Here are the commands to execute if you’re going for the latter option
git clone git@github.com:makaimc/choose-your-own-adventure-presentations cd choose-your-own-adventure-presentations git checkout -b tutorial tags/tutorial-step-4
If you need help setting up your virtualenv and environment variables for the project, there are detailed steps and explanations for each variable shown in part 1 of this tutorial. Note that if you make a typo somewhere along the way in this post, you can compare your version with the tutorial-step-5 tag.
A New Wizards’ Landing Page
The landing page we created in part 1 is just a stub that’s not worthy of wizards who access our application.
To make the Wizards Only mode functional we’ll build a new landing page that lists every presentation we’ve created through this user interface.
Start by opening cyoa/views.py
and delete the placeholder wizard_landing
function.
@app.route('/wizard/presentations/') @login_required def wizard_landing(): return render_template('wizard/presentations.html')
Next, replace what we just deleted in views.py
by creating a new file named cyoa/wizard_views.py
and populating it with the following code.
from flask import render_template, redirect, url_for from flask.ext.login import login_required from . import app, db @app.route('/wizard/presentations/') @login_required def wizard_list_presentations(): presentations = [] return render_template('wizard/presentations.html', presentations=presentations) @app.route('/wizard/presentation/', methods=['GET', 'POST']) @login_required def wizard_new_presentation(): pass @app.route('/wizard/presentation/<int:id>/', methods=['GET', 'POST']) @login_required def wizard_edit_presentation(id): pass @app.route('/wizard/presentation/<int:pres_id>/decisions/') @login_required def wizard_list_presentation_decisions(pres_id): pass @app.route('/wizard/presentation/<int:pres_id>/decision/', methods=['GET', 'POST']) @login_required def wizard_new_decision(pres_id): pass @app.route('/wizard/presentation/<int:presentation_id>/decision/' '<int:decision_id>/', methods=['GET', 'POST']) @login_required def wizard_edit_decision(presentation_id, decision_id): pass @app.route('/wizard/presentation/<int:pres_id>/decision/' '<int:decision_id>/delete/') @login_required def wizard_delete_decision(pres_id, decision_id): pass
With the exception of the wizard_list_presentations
function, every function above with the pass keyword in its body is just a stub for now. We’ll flesh out those functions with code throughout the remainder of this post and also later in part 3 of the tutorial. For now we need them stubbed because otherwise the url_for
function in our redirects and templates will not be able to look up the appropriate URL paths.
Go back to the cyoa/views.py
file. Edit the return redirect
line shown below so it calls the new wizard_list_presentations
function instead of wizard_landing
.
@app.route('/wizard/', methods=['GET', 'POST']) def sign_in(): form = LoginForm() if form.validate_on_submit(): wizard = Wizard.query.filter_by(wizard_name= form.wizard_name.data).first() if wizard is not None and wizard.verify_password(form.password.data): login_user(wizard) return redirect(url_for('wizard_list_presentations')) return render_template('wizard/sign_in.html', form=form, no_nav=True)
Our Flask application needs to access the new wizard_views.py
functions. Add this single line to the end of the cyoa/__init__.py
file to make that happen:
from . import wizard_views
The templates for our new landing page don’t exist yet, so let’s create them now. Create a new file cyoa/templates/nav.html
and insert the HTML template markup below.
<div class="container"> <div class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="{{ url_for('wizard_list_presentations') }}">Wizards Only</a> </div> <div> <ul class="nav navbar-nav"> <li><a href="{{ url_for('wizard_list_presentations') }}">Presentations</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="{{ url_for('sign_out') }}">Sign out</a></li> </ul> </div> </div> </div> </div>
The above template file is a navigation bar that will be included on logged in Wizard Only pages. You’ll see the template tag {% include "nav.html" %}
in every template that needs to display the navigation bar at the top of the webpage.
Next up we need to modify the temporary markup in the landing page template file so it displays the presentations we will create through the Wizards Only user interface. Replace the temporary code found in cyoa/templates/wizard/presentations.html
with the following HTML template markup.
{% extends "base.html" %} {% block nav %} {% include "nav.html" %} {% endblock %} {% block content %} <div class="container"> <div class="row"> <div class="col-md-10"> <h1>Presentations</h1> {% if not presentations %} No presentations found. <a href="{{ url_for('wizard_new_presentation') }}">Create your first one</a>. {% else %} <table class="table"> <thead> <tr> <th>Name</th> <th>Is Visible?</th> <th>Web browser voting?</th> </tr> </thead> <tbody> {% for p in presentations %} <tr> <td><a href="{{ url_for('wizard_edit_presentation', id=p.id) }}">{{ p.name }}</a></td> <td>{{ p.is_visible }}</td> <td><a href="{{ url_for('wizard_list_presentation_decisions', pres_id=p.id) }}">Manage choices</a></td> </tr> {% endfor %} </tbody> </table> {% endif %} </div> </div> <div class="row"> <div class="col-md-10"> <div class="btn-top-margin"> <a href="{{ url_for('wizard_new_presentation') }}" class="btn btn-primary">New Presentation</a> </div> </div> </div> </div> </div> {% endblock %}
In the above markup we check the presentations
object passed into the template to determine if one or more presentations exist. If not, Flask renders the template with a “No presentations found.” message and a link to create the first presentation. If one or more presentation objects do exist, a table is rendered with the name of the presentation, whether it’s visible to non-wizard users and whether or not we’ve enabled web browser voting (which we will code in part 3).
Time to test out the current state of our application to make sure it’s working properly. Make sure your virtualenv is activated and environment variables are set as we established in part 1 of the tutorial. From the base directory of our project, start the dev server with the python manage.py runserver
command.
(cyoa)$ python manage.py runserver * Running on http://0.0.0.0:5001/ * Restarting with stat
If your development server does not start up properly, make sure you’ve executed pip install -r requirements.txt
to have all the dependencies the server requires. Occasionally there are issues installing the required gevent
library the first time the dependencies are obtained via pip
.
Open http://localhost:5001/wizard/ in your web browser. You should see the unchanged Wizards Only sign in page. Log in with your existing Wizard credentials created in part 1. The suggested credentials for part 1 were “gandalf” for the wizard name and “thegrey” for the password.
When you get into the application, the presentations.html template combined with our existing base.html
and new nav.html
will create a new landing screen that looks like this:
However, we haven’t written any code to power the “Create your first one” link and “New Presentation” buttons yet. If we click on the “New Presentation” button, we’ll get a ValueError
like we see in the screenshot below because that view does not return a response.
Let’s handle creating and editing presentations next.
Creating and Modifying Presentation Metadata
We built a page to display all presentations to logged in Wizards, but there’s currently no way to add or edit presentation metadata. Why are we using the term “metadata” instead of just saying “presentations”? This Wizards Only user interface is only used to create and edit the presentations’ information in the application, not the presentation files themselves. In other words, we’re modifying the presentation metadata, not the HTML markup within the presentations. As we’ll see later in the post, our application will use the metadata to look in the cyoa/templates/presentations
folder for a filename associated with a visible presentation.
Open up cyoa/models.py
and append the following code at the end of the file.
class Presentation(db.Model): """ Contains data regarding a single presentation. """ __tablename__ = 'presentations' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) slug = db.Column(db.String(128), unique=True) filename = db.Column(db.String(256)) is_visible = db.Column(db.Boolean, default=False) def __repr__(self): return '<Presentation %r>' % self.name
The above Presentation
class is a SQLAlchemy database model. Just like with our Wizard
model, this model maps a Python object to the database table presentations
and allows our application to create, read, update and delete rows in the database for that table.
We also need a new form to handle the creating and editing of presentation metadata. We’ll store this form in the cyoa/forms.py
file.
class PresentationForm(Form): name = StringField('Presentation name', validators=[Required(), Length(1, 60)]) filename = StringField('File name', validators=[Required(), Length(1, 255)]) slug = StringField('URL slug', validators=[Required(), Length(1, 255)]) is_visible = BooleanField()
Now we need to tie together our new Presentation
database model and PresentationForm
form. In the cyoa/wizard_views.py
, remove the pass keyword from the listed functions and replace it with the highlighted code. What we’re adding below are two imports for the Presentation
and PresentationFrom
classes we just wrote. Now that we have presentations in the database, we can query for existing presentations in the wizard_list_presentations
function. In the wizard_new_presentation
and wizard_edit_presentation
functions, we’re using the PresentationForm
class to create and modify Presentation
objects through the application’s web forms.
from . import app, db from .models import Presentation from .forms import PresentationForm @app.route('/wizard/presentations/') @login_required def wizard_list_presentations(): presentations = Presentation.query.all() return render_template('wizard/presentations.html', presentations=presentations) @app.route('/wizard/presentation/', methods=['GET', 'POST']) @login_required def wizard_new_presentation(): form = PresentationForm() if form.validate_on_submit(): presentation = Presentation() form.populate_obj(presentation) db.session.add(presentation) db.session.commit() return redirect(url_for('wizard_list_presentations')) return render_template('wizard/presentation.html', form=form, is_new=True) @app.route('/wizard/presentation/<int:id>/', methods=['GET', 'POST']) @login_required def wizard_edit_presentation(id): presentation = Presentation.query.get_or_404(id) form = PresentationForm(obj=presentation) if form.validate_on_submit(): form.populate_obj(presentation) db.session.merge(presentation) db.session.commit() db.session.refresh(presentation) return render_template('wizard/presentation.html', form=form, presentation=presentation)
In the above code, make sure you’ve changed the first line within the wizard_list_presentations
function from presentations = []
to presentations = Presentation.query.all()
. That modification to the code allows us to pass in every presentation found in the database into the render_template
function instead of an empty list.
Create a new file named cyoa/templates/wizard/presentation.html
. There’s already a presentations.html with an ‘s’ at the end, but this filename refers to a singular presentation. Add the following HTML template within the new file:
{% extends "base.html" %} {% block nav %} {% include "nav.html" %} {% endblock %} {% block content %} <div class="container"> <div class="row"> <div class="col-md-6"> {% from "partials/_formhelpers.html" import render_field %} {% if is_new %} <form action="{{ url_for('wizard_new_presentation') }}" method="post"> {% else %} <form action="{{ url_for('wizard_edit_presentation', id=presentation.id) }}" method="post"> {% endif %} <div> {{ form.csrf_token }} {{ render_field(form.name) }} {{ render_field(form.filename) }} {{ render_field(form.slug) }} <dt class="admin-field"> {{ form.is_visible.label }} {{ form.is_visible }} </dt> </div> <div> <input type="submit" class="btn btn-success btn-top-margin" value="Save Presentation"></input> </div> </form> </div> </div> </div> {% endblock %}
Let’s give our upgraded code another spin. First, since we created a new database table we need to sync our models.py
code with the database tables. Run the following manage.py command at the command line from within the project’s base directory.
(cyoa)$ python manage.py syncdb
Now the tables in PostgreSQL match our updated models.py
code. Bring up the application with the runserver command.
(cyoa)$ python manage.py runserver
Point your web browser to http://localhost:5001/wizard/. You should again see the unchanged Wizards Only sign in page.
Use your wizard credentials to sign in. At the presentations landing screen after signing in, click the “New Presentation” button.
Create a presentation for the default template that’s included with the project. Enter “Choose Your Own Adventure Default Template” as the presentation name, “cyoa.html” for the file name, “cyoa” for the URL slug and check the “Is Visible” box. Press the “Save Presentation” button and we’ll be taken back to the presentations list screen where our new presentation is listed.
We can edit existing presentations by clicking on the links within the Name column. Now we’ll use this presentation information to display visible presentations to users not logged into the application.
Listing Available Presentations
There have been a slew of code changes in this blog post, but let’s get one more in before we wrap up that’ll be useful to presentation viewers. We’re going to create a page that lists all presentations that are visible to non-wizard users so you can send out a URL to anyone that wants to bring up the slides on their own.
In addition, our presentation retrieval function will look for presentation files only in the cyoa/templates/presentations/
folder. The presentations can still be accessed from the same URL as before, but it’s easier to remember where presentation files are located when there’s a single folder for them.
Start by deleting the following code in cyoa/views.py
as we will not need it any longer.
@app.route('/<presentation_name>/', methods=['GET']) def landing(presentation_name): try: return render_template(presentation_name + '.html') except TemplateNotFound: abort(404)
In its place, insert the following code.
@app.route('/', methods=['GET']) def list_public_presentations(): presentations = Presentation.query.filter_by(is_visible=True) return render_template('list_presentations.html', presentations=presentations) @app.route('/<slug>/', methods=['GET']) def presentation(slug): presentation = Presentation.query.filter_by(is_visible=True, slug=slug).first() if presentation: return render_template('/presentations/' + presentation.filename) abort(404)
The first function we wrote, list_public_presentations
, performs a PostgreSQL database query through SQLAlchemy for all presentations with the is_visible
field set to True then passes the results to the template renderer. The second function, presentation
, only renders the presentation if the URL slug matches an existing presentation and for that presentation the is_visible
field is True. Otherwise an HTTP 404 status code is returned.
One more step in cyoa/views.py
. Add the following line as a new import to the top of the file so that our new code can use the Presentation
database model for the queries in the functions we just wrote:
from .models import Wizard from . import app, redis_db, socketio, login_manager from .models import Presentation client = TwilioRestClient()
Finally create a new list template for the presentations. Call this file cyoa/templates/list_presentations.html
.
{% extends "base.html" %} {% block content %} <div class="container"> <div class="row"> <div class="col-md-10"> <h1>Available presentations</h1> {% for p in presentations %} <p><a href="{{ url_for('presentation', slug=p.slug) }}">{{ p.name }}</a></p> {% else %} No public presentations found. {% endfor %} </div> </div> </div> {% endblock %}
We need to move our cyoa.html default template into the cyoa/templates/presentations/
folder because our list_public_presentations
function now looks in that folder instead of the cyoa/templates/
folder. Create a new directory within cyoa/templates
called presentations
:
mkdir cyoa/templates/presentations/
Now go to the cyoa/templates/
directory and run the following move command.
mv cyoa.html presentations/
Be sure to store your new presentations within the cyoa/templates/presentations/
folder from now on.
Check out the simple listing of presentations available to the audience. Go to http://localhost:5001 and you’ll see the list of visible presentations.
All Prepared for Part Three!
We now have a working Wizards Only set of screens for managing our presentations. If you want all the code to this point in the tutorial series, you can grab it from the tutorial-step-5 on GitHub.
There’s a big elephant in the room though with our current application. Each presentation has a checkbox to enable websockets-based voting to complement SMS voting. However, we haven’t coded the ability for presentation watchers to vote via web browsers just yet. In part three of this series, we’ll conclude by adding that new voting functionality so that our Choose Your Own Adventure Presentations project is complete.
Let me know what new Choose Your Own Adventure presentation stories you come up with or open a pull request to improve the code base. Contact me via:
- Email: makai@twilio.com
- GitHub: Follow makaimc for repository updates
- Twitter: @mattmakai