Quantcast
Channel: Twilio Cloud Communications Blog » DOers In Action
Viewing all articles
Browse latest Browse all 9

Choose Your Own Adventure Presentations: Wizard Mode Part 2 of 3

$
0
0

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.

image05
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.

image00
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:

image03

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.

image07
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 '' % 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//', 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.

image06

Use your wizard credentials to sign in. At the presentations landing screen after signing in, click the “New Presentation” button.

image01

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.

image02

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('//', 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('//', 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.

image04

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

Choose Your Own Adventure Presentations: Wizard Mode Part 2 of 3


Viewing all articles
Browse latest Browse all 9

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>