<style> .present { text-align: left; } </style> # Getting started with Flask ## Week 18 Day 2 --- ## Lecture Videos 1 (12 min) Watch: - Decorators Demo (10:15) --- ### Wrapping functions with decorators A decorator is just fancy syntax for a function that takes in another function and returns a modified version of the inner function. We can use decorators to extend the functionality of our functions. --- ### Decorators Decorators can be used to add code that runs before and/or after a function runs, or modify the function itself. Let's create a decorator that could be used for logging. ```python= # our decorator has to take a function def logged(func): # define the function that we're going to return def wrapped(): # do some logging print(f"before {func.__name__} runs") # invoke the passed-in function val = func() # log the return value of the function print(val) # do more logging print(f"after {func.__name__} runs") # return the value from invoking the passed-in function return val # decorator returns the wrapper function—without invoking return wrapped ``` --- ### Decorators Without the decorator syntax, we would have to define our function, then reassign our function to the return value from invoking the decorator function on our old function. ```python= # decorator function def logged(func): def wrapped(): print(f"before {func.__name__} runs") val = func() print(val) print(f"after {func.__name__} runs") return val return wrapped # function to decorate def cool_function(): return "sup" # before decorating cool_function() # returns "sup" # decorated function cool_function = logged(cool_function) # after decorating cool_function() ``` --- ### Decorators Using the `@decorator_name` syntax, we can shorten this: ```python= # instead of this def cool_function(): return "sup" cool_function = logged(cool_function) ``` To this: ```python= @logged def cool_function(): return "sup" ``` It does the same thing. You don't have to do anything extra to make your function wrapper work as a decorator with the `@` syntax! --- ### Passing arguments through a decorator What if I want to wrap functions that take arguments? ```python= def logged(func): def wrapped(name): print(f"before {func.__name__} runs") print(name) val = func(name) print(val) print(f"after {func.__name__} runs") return val return wrapped @logged def cool_function_args(name): return f"sup {name}" ``` --- ### Passing arguments through a decorator What if I want to wrap functions that take arguments... but I want to be flexible about what kind of arguments the function takes? ```python= def logged(func): def wrapped(*args, **kwargs): print(f"before {func.__name__} runs") print(args, kwargs) val = func(*args, **kwargs) print(val) print(f"after {func.__name__} runs") return val return wrapped @logged def cool_function_more_args(name, count): return f"sup {name} "*count ``` --- ## Lecture Videos 2 (10 min) Watch: - Psycopg Demo (7:32) --- ### Psycopg `psycopg2` is a utility that lets us connect directly to a postgresql database from Python. It is NOT the equivalent of Sequelize. We can use it directly, but typically we will want to use an actual ORM. Our ORM will rely on `psycopg2`, but it will have a much more convenient interface. --- ### Setting up `psycopg2` [1/2] 1. Install `psycopg2` ```bash pipenv install psycopg2-binary ``` 2. Import the `psycopg2` package at the top of your file. 3. Set up your connection parameters in a dictionary, including `dbname`, `user`, and `password`: ```python= CONNECTION_PARAMETERS = { "dbname": "widget_database", "user": "widget_user", "password": "password", } ``` --- ### Setting up `psycopg2` [2/2] 4. Open a connection to the database. Use the `with` keyword, and the `connect` method on `psycopg2`: ```python= with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: # code to follow ``` 5. Open a "cursor" to perform data operations. ```python= with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: # code to follow ``` 6. With our cursor, we can use the `execute` method to run a SQL command: ```python= with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """CREATE TABLE widgets ( id SERIAL PRIMARY KEY, color VARCHAR(50), shape VARCHAR(50) );""") ``` --- ### Executing SQL with `psycopg2` After executing a command, we can fetch the results using the `fetchone` or `fetchall` methods on the cursor. - `fetchone()` will return a tuple of the first matched record - `fetchall()` will return a list of tuples of all matching records ```python= # Fetching one record with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """ SELECT * FROM widgets """) results = curs.fetchone() print(results) ``` --- ### Creating data with `psycopg2` We can use parameterized SQL statements to insert data into our database. ```python= # Inserting a new record def add_new_widget(color, shape): with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """ INSERT INTO widgets (color, shape) VALUES (%(color)s, %(shape)s) """, { "color": color, "shape": shape }) ``` --- ### Why can't I use f-string syntax? [documentation](https://www.psycopg.org/docs/usage.html#the-problem-with-the-query-parameters) --- ### Reading data with `psycopg2` Write a `SQL` statement using the `SELECT` keyword and execute it with the cursor. ```python= # Selecting records def get_widgets_by_color(color): # Fetching many records (find widgets by color): with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """ SELECT * FROM widgets WHERE color = %(color)s """, {"color": color}) results = curs.fetchall() return results ``` --- ### Updating data with `psycopg2` Use `UPDATE` keyword from `SQL`. ```python= # Updating an existing record def update_widget_color(widget_id, new_color): with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """ UPDATE widgets SET color = %(new_color)s WHERE id = %(widget_id)s """, { "widget_id": widget_id, "new_color": new_color}) ``` --- ### Deleting data with `psycopg2` Use `DELETE` keyword from `SQL`. (... see a pattern here?). ```python= def delete_widget(widget_id): with psycopg2.connect(**CONNECTION_PARAMETERS) as conn: with conn.cursor() as curs: curs.execute( """ DELETE FROM widgets WHERE id = %(widget_id)s """, {"widget_id": widget_id}) ``` --- ### The `with` keyword `with` is a convenient way to make sure we close out our database connection after we are done performing our operation. - works like a `try`/`except`/`finally` block—it runs some setup logic, then "tries" the code in the with block, and whether or not it runs into an error, it runs some clean up code (e.g. closing a database connection or file) --- ## Lecture Videos 3 (28 min) Watch: - Setting Up A Flask App (13:39) - Flask Routing (9:16) --- ### Flask app setup 1. install flask ```bash pipenv install flask ``` 2. import `Flask` to the file where we are building our application ```python= from flask import Flask ``` 3. instantiate a `Flask` instance ```python= app = Flask(__name__) ``` --- ### Flask app setup 4. Add a `.flaskenv` file ```bash= FLASK_APP=app FLASK_ENV=development ``` 5. install python-dotenv ```bash pipenv install python-dotenv ``` 6. run your application... ```bash pipenv run flask run ``` --- ### Adding routes We use the `@route` decorator to turn a function into a route that exists on our application. Whatever we `return` from the function will get sent to the browser. ```python= @app.route("/") def index(): return "Welcome to our app" ``` --- ### Adding a secret key (or other configuration variables) In addition to the `.env` file which contains environment specific (not secret) information, we can use a .env. ```bash= SECRET_KEY=EXTREMELYsecretstufffshhhhhhhh ``` --- ### Making a Config class Get the secret values from your `.env` and define a `Config` class which has all of the values you want to incorporate into your app. ```python= import os class Config(): SECRET_KEY = os.environ.get('SECRET_KEY') ``` Then incorporate these values into your application using the the `config.from_object method`. ```python= # in the __init__.py in the app folder from flask import Flask from app.config import Config app = Flask(__name__) app.config.from_object(Config) ``` --- ## Lecture Videos 4 (25 min) Watch: - Jinja2 (13:57) - Flask Blueprints (7:36) --- ### How the next 2 sections of lecture will work Lecture Repo: https://github.com/mitchellreiss/flask-jinja-forms-demo/ - Clone the repo than follow along with the changes we are making to the app by switching branches - `git branch -a` to see all branches - `git checkout origin/<remote branch name>` --- ### Jinja `git checkout origin/1-jinja` - Jinja is a similar concept to serving pug file from our Express servers - Add the Jinja package to your application - `pipenv install Jinja2` - Create a `templates` folder within your application to house your templates - In `templates`, create html files that you would like your routes to render --- ### Rendering Templates - In your app, import `render_template` from `flask`. - Instead of returning HTML strings directly,invoke `render_template` with the name of the html file you would like to render ```python= @app.route('/') def index(): return render_template('index.html') ``` --- ### Variables and Expressions - We can use `{{ variable-name }}` in our html files to fill in values that were passed in as kwargs to `render_template` - We use the same `{% %}` syntax to evaluate our code within the html --- ### Extending and Including Content - We can reuse small templates with `include` keyword, followed by the name of the template we are including. - We can use `extend` to use a base template and switch out the block content --- ### Variables and Expressions Code `git checkout origin/1-jinja` #### Lets review: 1. `app/init.py` 2. `app/templates/base.html` 3. `app/templates/index.html` 4. `app/templates/show.html` --- ### Blueprints `git checkout origin/2-blueprints` - Blueprints allow us to organize our code by breaking our routes out to individual modules, much like Routers did for us in Express. --- ### Creating a Blueprint - We create a Blueprint by importing it from `flask`, then invoking it with: - A name - A file name to indicate where it's defined (just like when we defined our Flask app) - A url_prefix, which will prepend all routes to this Blueprint ```python= from flask import Blueprint workouts_router = Blueprint('workouts', __name__, url_prefix='/workouts') ``` --- ### Use the Flask Blueprint to make routes - Where we defined the Blueprint, we can use the `@<<blueprint-name>>.route` decorator exactly like we would use it on the app in our main file ```python= # In the routes/workouts.py from flask import Blueprint bp = Blueprint('workouts', __name__, url_prefix='/workouts') @bp.route('/', methods=('GET')) def workout_index(): # Do stuff to show the workouts ``` --- ### Register the Flask Blueprint with the Flask application - To connect our Blueprint to our app, we import the module that it is defined in. - Invoke the app's `register_blueprint` method with a reference to the Blueprint instance as an argument. ```python= # workouts.py from flask import Flask import workouts_router from routes # assuming we have a __init__.py in routes app = Flask() app.register_blueprint(workouts_router) ``` --- ### Lets review our code! `git checkout origin/2-blueprints` #### Lets review: 1. `app/init.py` 2. `app/routes/workouts.py` 3. `app/routes/__init__.py` --- ## Lecture Videos 5 (25 min) Watch: - Handling Form Data With Flask I (12:39) - Handling Form Data With Flask II (9:39) --- ### Using Forms with WTForms `git checkout origin/3-forms` - Must have secret key for csrf token validation to be successful - Create a forms directory - create a `__init__.py` in your forms directory - create a file to contain your form class --- ### Field Types - Be comfortable with using the `wtforms` docs to find other field types - <https://wtforms.readthedocs.io/en/2.3.x/fields/#basic-fields> - We can import validators into our form from `wtforms.validators` - <https://wtforms.readthedocs.io/en/2.3.x/validators/> --- ### Handling Get and Post Routes - In our app, we can handle requests based on whether it is a `GET` request for the form, a `POST` with valid data, or a `POST` with invalid data. --- ### Form Error handling - Invoking `validate_on_submit` on our form instance will return `True` if the `POST` did not have any validation errors. - Once validated we can use `form.data` to access the data dictionary - If the form was submitted with errors, `validate_on_submit` will return `False` - If not validated we can key into `form.errors` to access the errors dictionary --- ### Let's Review our code: `git checkout origin/3-forms` #### Lets review: 1. `app/forms/workout_form.py` 2. `app/forms/__init__.py` 3. `app/templates/new.html` 4. `app/routes/workouts.py` --- ### Using this repo as a Study Guide 1. See changes between branches: `$ git diff branch1..branch2` - Example: `$ git diff origin/1-jinja..origin/2-blueprints` 3. Checkout 0-starter if you want to try to build the app from scratch ```bash= # this will fetch the most current metadata from the remote git fetch origin # If there isn't already a branch named 0-starter it will create one then set the upstream of the repo as the remote git checkout 0-starter ``` ## Lecture Videos 6 (9 min) Watch: - Flask Sessions Demo (6:24) --- ### Flask sessions Sessions let you store user-specific information on a cookie. You must have a secret key (SECRET_KEY) set on your application to use the session object. --- ### Using sessions First, import the session object from Flask. ```python= from flask import Flask, render_template, session ``` The session object works like a dictionary. It starts out empty. We can access the values in the dictionary using the dictionary `.get()` method so that we don't get an error if we try to access a key that isn't in the dictionary yet. ```python= # inside a route value = session.get("some_key", None) ``` --- ### Using sessions Let's use the session object to count a specific user's views. Depending on whether or not the "views" key exists in the dictionary yet, we will either add it or update the value. ```python= # inside a route views = session.get("views", None) if views is not None: # update "views" in the dictionary else: # assign the "views" key to a value of one ``` --- ### Sessions Because sessions are stored client-side, restarting the server won't clear the session cookie. However, if the client clears their cookies, the information from the session will disappear. --- ### Sessions summary Sessions are a useful way to store user-specific information with a secure cookie in the client's browser. It will persist until the client clears their cookies. The library we will use for auth with Flask applications uses sessions—we won't have to interact with the session object directly. --- ### Project Time!