Flask to Heroku: The Roadblocks
Unspoken challenges that you face while working with Flask & deploying the app in Heroku
Introduction
For quite some time, I have been thinking of deploying a website as a project, nothing massive but mostly utilizing the important functionalities of a webpage. Personally, I always loved using the Flask framework. It's easy, a lot of libraries to make things even easier, and who doesn't love a lightweight framework!
Youtube videos are great to start with but when it needs customization based on your requirement it's quite the challenge, not to mention the videos are 30 minutes to 1 hour long, while your project could span for days or weeks. Here's me summarizing the challenges that no video prepared you for!
A Brief - Idea behind the webpage
Since I started learning to program, I was quite fascinated by the way GitHub shows the contribution graph. The graph shows how consistent you are. While the color intensity depends on the number of contributions you do, the primary aim is to show if you have contributed or not. I hoped to create something similar. While researching I found that Pixela uses an API to make use of a similar format of the graph. The idea of personalizing it to any activities did seem interesting to me. After using it for a week, I wondered if I could actually make a website that wraps around the API for others to use too and track their goals and other habits. Here's how I thought to approach it:
- Sign Up and Login Page
- A Profile page
- A Graph display page
- An About page
- Hosting it in Heroku
You can view the website here: TracBox
The Challenges
Before I talk about the challenges, here are the libraries I used to make this a successful project:
Since it's a personal project, the idea was to store as little data as possible, while using Flask and login sessions to work with.
1. Gathering multiple WTForms data in a single route
When: I faced this while creating the Profile route. I wanted to create a page where once the user logs in successfully, they can choose to Create a Graph or View any previously created Graph.
Why: I needed two separate data as any user can create
Graph1
and choose to viewGraph2
. For this, I had to use two different WTForms - one, which takes in the required data to create the graph passing the parameters to the Create Graph API call & two, which takes the Graph ID of the respective graph that the User wants to view, as Pixela uses that to return an SVG format of a graph.
This is where a bit of HTML comes in handy. Using name
allows us to specify the name of the form are we referring to. This is how to approach the problem:
# HTML code
...
<input name ="form1" type="submit" value="Create Graph">
...
...
<input name ="form2" type="submit" value="View Graph">
Now to catch it inside the Flask route, we need to address the unique name of each form. Here's how:
# FLASK code
@app.route("/profile/", methods=['GET', 'POST'])
def profile():
form1 = GraphForm()
form2 = ViewGraph()
if "form1" in request.form and form1.validate_on_submit():
--Do something--
if "form2" in request.form and form2.validate_on_submit():
--Something more--
How:
request.form
generates an immutable MultiDict , a data structure, which looks something like thisImmutableMultiDict([('csrf_token', 'something'), ('graph_id', 'graph1'), ('form2', 'View Graph')])
allowing us to catch hold of thename
of the form.
2. Escaping HTML elements
When: I needed to show the graph on the graph.html page as an easy way to view or work with the plot that they themselves generated using the Pixela API.
Why: When passing the Graph ID from the View Graph using the form on the Profile page, the Pixela API returns and SVG format of the graph to be viewed. I needed to render it on the webpage but the format it returned was just a massive HTML SVG tag.
There are two ways to escape HTML characters as I passed the API response to the template. Here's how:
# HTML code
<!-- Way: 1 -->
<div>
{% autoescape false %}
{{ image }}
{% endautoescape %}
<div>
<!-- Way: 2 -->
<div>
{{ image|safe }}
<div>
I recommend the second way more, as you aren't having to force Jinja to explicitly turn off
autoescape
asautoescape
is generally always True inside Jinja.
3. Validators: The Superpower
When: In quite a few instances, I required validators, they were my Han Solo to Luke Skywalker. Without them, I'm sure this project would have been a bit more complicated than I wished it to be.
Why: Pixela uses a set format when registering users inside their database, requiring a dictionary format of a few parameters acknowledging their Terms and Services. It would be easy, if I could set the parameters to 'Yes' while they were on the Sign Up page but I needed that they were mandatorily checked, else it would be impossible to proceed further.
Here's how you can use validators to make life easier for you:
class SignUpForm(FlaskForm):
username = StringField(label='Username', validators=[InputRequired()])
password = PasswordField(label='Password', validators=[InputRequired(), Length(min=4)])
age = BooleanField(label='Yes, I accept the Terms and Conditions & I\'m above age 18.', validators=[InputRequired()])
- InputRequired(): This mandatorily makes sure that there is some kind of input inside the form field, else it would not allow the User to proceed any further.
- Length(): I used the Length field to make sure there were no one-word passwords as that's not only less secure but ruins the hashing too.
However, the most useful of these fields were the age
variable holding the BooleanField which allowed a checkbox with the label, making it mandatory to provide input to proceed further.
4. Login Session & Catching Errors
Login Session: If you have previously worked with any API, you'd know how it requires a token or key to access the API, like a password. Quite rightly, this was one of the challenges I had to face as Pixela uses the token at every moment where we are creating the graph or updating some parameters like pixels in the graph. I didn't want any User to have to type the password every time they update a pixel, cause it's tiresome as well as results in a bad user experience.
The way to avoid it was to create a login session where the user would securely log in with their username and password and they would be able to access their account and any graph that they create. Using a session for login helped me re-use the token from the current session.
Every time I needed the token for any action the User chose, I could simply do
token = current_user.password
and had the required token saved in the variable, which I could then pass as an API call, given I wrapped the decorator @login_required
on the route. This was entirely done by Flask-Login, a simple Flask library that handles Login sessions.
Errors: Creating a Login session is prone to errors, by errors I mean if someone tried to access a page that was wrapped under
@login_required
without logging in, it would show the below image:
This doesn't result in a great user experience as the User doesn't understand what this is all about and would probably blame the creator of the application on how badly it is made. The way to catch these errors and redirect the user gives a better user experience. To catch the error:
@app.errorhandler(401)
def custom_401(error):
return render_template('404.html')
I created a separate 404.html template that doesn't return a dull-looking page so as to make the webpage interaction even more humane.
5. Heroku: The Final Boss
Heroku provides a free server to host a webpage for you to show your work to the world. However, it does come with some good-to-know things. One of the major challenges I faced while using Heroku is the setting up of a database.
Using the webpage in my local server, I started with SQLite, which is generally a good starting point for you to understand a database view it, and work with it. However, using SQLite inside Heroku isn't safe, not to mention your data would probably be deleted daily, having quite a few unhappy users. Here's Heroku themselves explaining why this happens.
The alternative is to use PostgresSQL, it's exactly the same commands that SQLite uses with the added benefit that it's industry-proven. Once you add it, you can simply add the secret key to your program and it will start working. This is what it looks like:
However, with the recent changes to Postgres, the value that Heroku uses results in an error. Here's what you should do instead to avoid that:
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL').replace("postgres://", "postgresql://", 1)
We are simply replacing the postgres://
with postgresql://
as you wouldn't be able to change that inside of Heroku.
Conclusion
I wouldn't sugarcoat and tell you that these are the only troubles you would face but I can tell you that there is almost every solution available out there on the web. You only require patience to search for it. Heroku does generate unseen errors compared to the local server but on the positive side, you get to see the logs and fix the errors.
If you get stuck or face a new challenge, I would love to know about it. You can share that with me on my Twitter or Linkedin. I would really appreciate it if you could visit the website here and share some feedback or comment. I swear it does help you control your daily life better! Thank you for reaching the end! If you'd like to view the code, it's openly hosted here on GitHub.