Lecture 4 - CS50's Web Programming with Python and JavaScript
SQL, Models, and Migrations
SQL
SQL(Structured Query Language): A programming language that allows us to update and query databases.
Databases
- When using SQL, we'll work with a relational database where we can find all of our data stored in a number of tables. Each of these tables is made up of a set number of columns and a flexible number of rows.
> We'll use the example of a website for an airline used to keep track of flights and passengers.
> There are several different relational database management systems that are commonly used to store information, and that can easily interact with SQL commands
- MySQL, PostgreSQL: Heavier-duty database management systems that are typically run on servers separate from those running a website.
- SQLite: A liter-weight system that can store all of its data in a single file. We'll use this, the default system used by Django.
Column Types
SQLite has types that represent different forms of information.
- TEXT: For strings of text (Ex. name)
- NUMERIC: A more general form of numeric data (Ex. A date of boolean value)
- INTEGER: Any non-decimal number (Ex. age)
- REAL: Any real number (Ex. weight / 2.8)
- BLOB(Binary Large Object): Any other binary data that we may want to store in our database (Ex. image)
> MySQL types
- CHAR(size)
- VARCHAR(size): A variable length character. When if something isn't going to be exactly a certain number of characters, but maybe up to a certain number of characters.
- SMALLINT, INT, BIGINT: Each of which uses a different number of bytes in order to store an integer data.
- FLOAT, DOUBLE: To store floating point numbers. A double uses more bytes of memory in order to store information. Numbers that might be real numbered values, where a double just allows us to express a number to a little bit more precision than a floating point number might be able to.
Tables
> Create a new table (Colunms Types Constraints)
CREATE TABLE flights(
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
duration INTEGER NOT NULL
);
- We've added 4 columns to this 'flights' table
1. id: To have an number that allows us to uniquely identify each row in a table. We specified that 'id' is an integer, and also that it is our primary key, meaning it is our unique identifier. 'AUTOINCREMENT' means we will not have to provide an id every time we add to the table because it will be done automatically.
2. origin: This will be a text field, and by writing 'NOT NULL' we have required that it have a value.
3. destination: This will be a text field and prevented it from being null.
4. duration: This value cannot be null and represented by an integer rather than as text.
> Constraints
- CHECK: Makes sure certain constraints are met before allowing a row to be added/modified
- DEFAULT: Provides a default value if no value is given
- NOT NULL: Makes sure a value is provided
- PRIMARY KEY: Indicates this is the primary way of searching for a row in the database
- UNIQUE: Ensures that no two rows have the same value in that column
+..
> Add rows to the table, use the 'INSERT' command
INSERT INTO flights
(origin, destination, duration)
VALUES ("New York", "London", 415);
- We've specified the table name we wish to insert into, then provided a list of the column names we will providing information on, and then specified the 'VALUES' we would like to fill that row in the table, making sure the 'VALUES' come in the same order as our corresponding list of columns. We don't need to provide a value for 'id' because it's automatically incrementing.
SELECT
> SELECT: To access data within that table.
- Simplest 'SELECT' query into our flights table
SELECT * FROM flights;
- The above command(*) retrieves all of the data from our flights table
- To access just some columns, we can replace the * with the column names we would like access to. The following query returns all of the origins and destinations.
SELECT origin, destination FROM flights;
- As our tables get larger, we will also want to narrow down which rows our query returns. Add a WHERE followed by some condition. The following command selects only row with an 'id' of '3'.
SELECT * FROM flights WHERE id = 3;
Working with SQL in the Terminal
- https://www.sqlite.org/download.html, https://sqlitebrowser.org/dl/
- We can start by creating a file for our database either by manually creating a new file, or running 'touch flights.sql' in the terminal. Now, if we run 'sqlite3 flights.sql' in the terminal, we'll be brought to a SQLite prompt where we can run SQL commands.
- In SQLite, you can automatically create a flight.sql file by typing sqlite3 flights.sql. But the reason for running touch flights.sql is to check if the file exists first, to make it easier to add content, and to maintain a consistent workflow. SQLite will automatically create files just by running sqlite3 flights.sql, so you don't have to run touch first. If you just run 'sqlite3.flights.db', a SQLite database file will created immediately.
- In Window, the touch command is not supported. You can use these alternatives to do the same with Windows, the 'echo. > filename.txt' command creates an empty file, and it stays the same if there's an existing file. 'type nul' and 'fsutil file createnew filename.txt filesize' command also creates an empty file. If you want to use 'touch' command in Windows, use Git Bash or WSL.
- I'm inside of the SQLite prompt. I can now begin to write commands that will be executed on this SQL database.
# Entering into the SQLite Prompt
(base) % sqlite3 flights.sql
SQLite version 3.26.0 2018-12-01 12:34:55
Enter ".help" for usage hints.
# Creating a new Table
sqlite> CREATE TABLE flights(
...> id INTEGER PRIMARY KEY AUTOINCREMENT,
...> origin TEXT NOT NULL,
...> destination TEXT NOT NULL,
...> duration INTEGER NOT NULL
...> );
# Listing all current tables (Just flights for now)
sqlite> .tables
flights
# Querying for everything within flights (Which is now empty)
sqlite> SELECT * FROM flights;
# Adding one flight
sqlite> INSERT INTO flights
...> (origin, destination, duration)
...> VALUES ("New York", "London", 415);
# Checking for new information, which we can now see
sqlite> SELECT * FROM flights;
1|New York|London|415
# Adding some more flights
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Shanghai", "Paris", 760);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Istanbul", "Tokyo", 700);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("New York", "Paris", 435);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Moscow", "Paris", 245);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Lima", "New York", 455);
# Querying this new information
sqlite> SELECT * FROM flights;
1|New York|London|415
2|Shanghai|Paris|760
3|Istanbul|Tokyo|700
4|New York|Paris|435
5|Moscow|Paris|245
6|Lima|New York|455
# Changing the settings to make output more readable
sqlite> .mode columns
sqlite> .headers yes
# Querying all information again
sqlite> SELECT * FROM flights;
id origin destination duration
---------- ---------- ----------- ----------
1 New York London 415
2 Shanghai Paris 760
3 Istanbul Tokyo 700
4 New York Paris 435
5 Moscow Paris 245
6 Lima New York 455
# Searching for just those flights originating in New York
sqlite> SELECT * FROM flights WHERE origin = "New York";
id origin destination duration
---------- ---------- ----------- ----------
1 New York London 415
4 New York Paris 435
- Error: In SQLite, string values must be enclosed in small quotation marks(' '). Large quotation marks are used to wrap table names or column names.
- SQLite has some additional tricks if you'd like to format it a little nicer.
- The data is now organized a little more nicely where things are really into columns and there are headers for each of the rows.
- Instead of getting back all of the flights, only going to get back the flights where the origin is 'New York'.
- Instead of getting back all of the flights, only going to get back the flights where the origin is 'New York'.
- We can also use more than just equality to filter out our flights. For integer and real values, we can use greater than or less than.
SELECT * FROM flights WHERE duration > 500;
SELECT * FROM flights WHERE duration > 500 AND destination = "Paris";
SELECT * FROM flights WHERE duration > 500 OR destination = "Paris";
- We can also use the keyword IN to see if a bit of data is one of several options
SELECT * FROM flights WHERE origin IN ("New York", "Lima");
- The sequence of New York and Lima, as long as it is one of those two.
- So, you can begin to construct more sophisticated queries that are able to check for whether an origin is in a list of possible values or even if it matches a particular pattern.
- We can even use regular expressions to search words more broadly using the LIKE keyword. The below query finds all results with an 'a' in the origin, by using '%' as a wildcard character. SELECT * FROM flights WHERE origin LIKE "%a%";
Functions
> There are also a number of SQL functions we can apply to the results of a query. These can be useful if we don't need all of the data returned by a query, but just some summary statistics of the data.
- AVERAGE
- COUNT
- MAX
- MIN
- SUM
+...
UPDATE
- UPDATE: Update rows of a table that already exist.
UPDATE flights
SET duration = 430
WHERE origin = "New York"
AND destination = "London";
DELETE
- DELETE: Delete rows from our database.
DELETE FROM flights WHERE destination = "Tokyo";
Other Clauses
- LIMIT: Limits the number of results returned by a query (SELECT * FROM flights LIMIT 5: I only want 5 results to come back from the table)
- ORDER BY: Orders the results based on a specified column (SELECT * FROM flights ORDER BY destination: Get all of the flights in order by how long they are)
- GROUP BY: Groups results by a specified column (SELECT * FROM flights GROUP BY origin)
- HAVING: Allows for additional constraints based on the number of results (EX: I would like to select all of the flights grouping them by their origin, but they need to have a count of at least three: Meaning there needs to be at least three flights that are leaving from that particular city.
Joining Tables
Many databases in practice are populated by a number of tables that all relate to each other in some way.
- In our flights example, we want to add an airport code to go with the city. We would have to add two more columns to go with each row. We would also be repeating information, as we would have to write in multiple places that city X is associated with code Y.
- How to solve: Deciding to have one table that keeps track of flights, and then another table keeping track of airports.
#Airports
- Now, we have a table relating codes and cities, rather than storing an entire city name in our flights table, it will save storage space if we're able to just save the 'id' of that airport.
- Therefore, we should rewrite the flights table accordingly. Since we're using the 'id' column of the airports table to populate 'origin id' and 'destination id': Foreign Keys
- In addition to flights and airports, let's store data about passengers.
- We can add another table that stores first and last names, and a foreign key representing the flight they are on
- We can do even better than this though, as the same person may be on more than one flight. To accout for this, we can create a 'people' table that stores first and last names, and a passengers table that pairs people with flights.
JOIN Query
> Join query: Combine two tables for the purposes of another query.
- For example, we want to find the origin, destination, and first name of every trip a passenger is taking. Also for simplicity in this table, we're going to be using the unoptimized 'passengers' table that includes the flight id, first name, and last name.
SELECT first, origin, destination
FROM ...
- But the problem is, 'first' is stored in the 'passengers' table, while 'origin' and 'destination' are stored in the 'flights' table.
- We solve this by joining the two tables using the fact that 'flight_id' in the 'passengers' table corresponds to 'id' in the 'flights' table.
SELECT first, origin, destination
FROM flights JOIN passengers
ON passengers.flight_id = flights.id;
- I'd like to select every person's first name and their origin, destination. And I'm going to select that from flights table, but I need to join it with the passengers table. Then I say On to indicate how it is these two tables are related to one another.
- In this case, The flight id column of the passengers table is associated with the id column of the flights table. The flights table has an id that uniquely identifies every flight and the passengers table has a flight id column that uniquely identifies the flight that we're referring to for this particular passenger.
- So the result, the table I might get gives me everyone's first name, but also their origin and destination. Our origin and destination are going to be drawn from that table of flights, and the first name is going to be drawn from the table of passengers.
- But by using a JOIN query, I've been able to take data from two separate tables and join them both back together.
> JOINs
- INNER JOIN: Ignore rows that have no matches between the tables. INNER JOIN will take the two tables, it will cross compare them based on the condition that I specified and only return back to me the results where there's a match on botch sides.- OUTER JOIN: To retrieve all records from tables, even for those records with no matching value in the other table based on the JOIN condition. For the missing columns, it returns NULL as the value.
-- LEFT OUTER JOIN: Keeps all data from the first table
-- RIGHT OUTER JOIN: Keeps all data from the second table
-- FULL OTHER JOIN: Keeps all data from both tables
Indexing
- How to make our queries more efficient when dealing with large tables: Create an index similar to the index you might see in the back of a textbook.
- For example: If we know that we'll often look up passengers by their last name, we could create an index from last name to id using the command
CREATE INDEX name_index ON passengers (last);
- Create an index that we're going to call name index on the passengers table, and in particular on the last name column.
- I'm going to be looking up passengers by their last name, so I would like to create an index on that table to be able to more efficiently search for a passenger based on their last name.
SQL Vulnerabilities
> SQL Injection
- A SQL injection attack: When a malicious user enters SQL code as input on a site in order to bypass the sites security measures.
- For example: We have a table storing usernames and passwords, and then a login form on the home site of a page. We may search for the user using a query such as:
SELECT * FROM users
WHERE username = username AND password = password;
- A user named Harry might go to this site and type 'harry' as a username and 12345 as a password, in which case the query would look like this:
SELECT * FROM users
WHERE username = "harry" AND password = "12345";
- A hacker might type 'harry" --' as a username and nothing as a password. It turns out that '--' stands for a comment in SQL:
SELECT * FROM users
WHERE username = "harry"--" AND password = "12345";
- This means ignore everything that comes after it in the same way that in Python you can use the hashtag symbol to mean the rest of this line is a comment, and the compiler should ignore it.
- In this query, the password checking has been commented out, the hacker can log into Harry's account without knowing their password.
- To solve this problem,
1. Escape characters to make sure SQL treats the input as plain text and not as SQL code.
2. Use an abstraction layer on top of SQL which includes its own escape sequence, so we don't have to write SQL queries ourselves.
> Race Condition
- A race condition is a situation that occurs when multiple queries to a database occur simultaneously. When these are not adequately handled, problems can arise in the precise times that databases are updated.
- For example, I have $150 in my bank account. A race condition could occur if I log into my bank account on both my phone and my laptop, and attempt to withdraw $100 on each device. If the bank's software developers did not deal with race conditions correctly, than I may be able to withdraw $200 from an account with only $150 in it.
- One potential solution for this problem would be locking the database. We could not allow any other interaction with the database until one transaction has been completed.
- In the bank example, after clicking navigating to the "Make a Withdrawl" page on my computer, the bank might not allow me to navigate to that page on my phone.
Django Models
#About Django Virtual Environment
공부용 프로젝트라면 같은 가상환경에서 여러 Django 프로젝트를 만들어도 괜찮으나, 규모가 크거나 실제 서비스용 프로젝트라면 각 프로젝트마다 별도의 가상환경을 쓰도록. 다른 django 버전을 사용한다면 다른 가상환경.
Django Models are a level of abstraction on top of SQL that allow us to work with databases using Python classes and objects rather than direct SQL queries.
> Let's get started on using models by creating a django project for our airline, and creating an app within that project.
django-admin startproject airline
cd airline
python manage.py startapp flights
- The process of adding an app as usual
1. Add 'flights' to the 'INSTALLED_APP' list in 'settings.py'
2. Add a route for 'flights' in 'urls.py', import 'include'
path("flights/", include("flights.urls")),
3. Create a 'urls.py' file within the 'flights' application. And fill it with standard 'urls.py' imports and lists.
- Django is going to figure out what SQL syntax it needs to it to create that table but then manipulate that table. Selecting and updating and inserting anytime I make changes to those models.
- Now, rather than creating actual paths and getting started on 'views.py', we'll create some models in the 'models.py' file.
- In this file, we'll outline what data we want to store in our application.
- Then, Django will determine the SQL syntax necessary to store information on each of our models.
- Every model is going to be a Python class. One model for each of the main tables we care about storing information about.
class Flight(models.Model):
origin = models.CharField(max_length=64)
destination = models.CharField(max_length=64)
duration = models.IntegerField()
- 1st line: We create a new model that extends Django's model class.
- Below, we add fields for origin, destination, and duration.
- We specify maximum lengths of 64 for the two Character Fields.
> Now, I have an airline directory, a flights directory, and a manage.py file.
- So I'll going to tell Django that you should update the database to include information about the models that I have just created.
Migrations
- I create a migration to say, here are some changes that I would like to apply to the database. Then I migrate them to tell Django to take those changes and actually apply them to the database.
1. Create the migration, the instructions for how to actually go about manipulating the database.
2. Then one to take that migration step of saying now take those instructions and actually apply them to the underlying database.
- We've created a model, we don't have a database to store this information. To create a database, we navigate to the main directory of our project and run the command.
python manage.py makemigrations
- This command creates some Python files that will create or edit our database to be able to store what we have in our models.
- You should get an output, and if you navigate to your 'migrations' directory, you'll notice a new file was created. (flights> migrations> 0001_initial.py) This file has instructions to Django for how to manipulate the database to reflect the changes I have made to the model.
- To apply these migrations to our database, run this command
python manage.py migrate
- Now, you'll see some default migrations have been applied
Shell
To begin working adding information to and manipulating this database, we can enter Django's shell where we can run Python commands within our project.
python manage.py shell
Python 3.7.2 (default, Dec 29 2018, 00:00:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.
# Import our flight model
In [1]: from flights.models import Flight
# Create a new flight
In [2]: f = Flight(origin="New York", destination="London", duration=415)
# Instert that flight into our database
In [3]: f.save()
# Query for all flights stored in the database
In [4]: Flight.objects.all()
Out[4]: <QuerySet [<Flight: Flight object (1)>]>
I[1] 'flights': the name of my app, 'models': the name of that file
Import the flight class from that models file that I've just created
I[2] Create a new flight
I[3] Save that new flight that I have created
I[4] Get me all of the flights that exist inside of my database.
O[4] I have one flight(Flight object (1))
- When we query our database, we get just one flight called 'Flight object (1)'. This isn't a very informative name, but we can fix that.
- Inside 'models.py', we'll define a '_str_ function that provides instructions for how to turn a Flight object into a string:
class Flight(models.Model):
origin = models.CharField(max_length=64)
destination = models.CharField(max_length=64)
duration = models.IntegerField()
def __str__(self):
return f"{self.id}: {self.origin} to {self.destination}"
- The string representation of any flight is going to be a string that gives its ID and then says origin to destination.
- Now, when we go back to the shell, our output is a bit more readable.
# Create a variable called flights to store the results of a query
In [7]: flights = Flight.objects.all()
# Displaying all flights
In [8]: flights
Out[8]: <QuerySet [<Flight: 1: New York to London>]>
# Isolating just the first flight
In [9]: flight = flights.first()
# Printing flight information
In [10]: flight
Out[10]: <Flight: 1: New York to London>
# Display flight id
In [11]: flight.id
Out[11]: 1
# Display flight origin
In [12]: flight.origin
Out[12]: 'New York'
# Display flight destination
In [13]: flight.destination
Out[13]: 'London'
# Display flight duration
In [14]: flight.duration
Out[14]: 415
O[8]: Much nicer!
I[9]: 'flights' is a query set, 'first' gets me that first flight.
[11]~[14] I can access, as values, all of the properties of this flight.
- This is not the model that I actually want to represent my flight. Because I'm using a character field, a char field, for things like origin and destination.
- We don't want to have to store the city name as an origin and destination for every flight, so we probably want another model for an airport that is somehow related to the flight model.
class Airport(models.Model):
code = models.CharField(max_length=3)
city = models.CharField(max_length=64)
def __str__(self):
return f"{self.city} ({self.code})"
class Flight(models.Model):
origin = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="departures")
destination = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="arrivals")
duration = models.IntegerField()
def __str__(self):
return f"{self.id}: {self.origin} to {self.destination}"
- In the new 'Airport' class, the changes to the 'origin' and 'destination' fields within the 'Flight' class are new to us.
-- Create a class called airport that is also a model. And I'd like for this airport class to have a code, which is a character field with a max length of 3.
-- As well as a city, which would be a character field with a max length of 64.
-- Give this airport a string representation.
- In the 'Flight' class
-- The 'origin' and 'destination' fields are each Foreign Keys, which means they refer to another object. A foreign key references another table, like the 'airport' table.
-- By entering 'Airport' as our first argument, we are specifying the type of object this field refers to.
-- 'on_delete=models.CASCADE' gives instructions for what should happen if an airport is deleted. In this case, we specify that when an airport is deleted, all flights associated with it should also be deleted.
---When I have tables that are related to each other, SQL needs some way of knowing what whould happen if you ever delete something.
EX) If I have a flight from JFK to London, and later in time decide to delete JFK airport from my Ddatabase. What happens to flights when the thing that it is referencing gets deleted?
--- If I were to ever delete an airport from the airports table, it's going to also delete any of the corresponding flights.
--- models.protect: on_delete parameter that do not delete an airport if there are flights that are leaving from or going to that airport
-- We provide a related name, which gives us a way to search for all flights with a given airport as their origin or destination.
--- A related name is going to be a way of me accessing a relationship in the reverse order.
--- If I have an airport, how do I get all of the flights that have that airport as an origin?
--- If I give a related name to the foreign key, Django will automatically set up the relationship going in that opposite direction.
--- The first argument, if we have an airport,and I want to know all of the flights that have that airport as their origin, the reasonable name for a related name is something like departures. So if I have an airport, I can access all of the departures, which gets me all of the flights that are leaving from that airport.
- I've added two classes and this has changed in my Python code, but it hasn't yet changed in my database. So in order to make the change in my database,
1. python manage.py makemigrations: Look for any new changes that have been made to models.py
2. python manage.py migrate: Make those changes to the database
- Every time we make changes in 'models.py', we have to make migrations and then migrate.
- You may have to delete your existing flight from New York to London, as it doesn't fit in with the new database structure.
# Create New Migrations
python manage.py makemigrations
# Migrate
python manage.py migrate
New models out in the Django shell:
# Import all models
In [1]: from flights.models import *
# Create some new airports
In [2]: jfk = Airport(code="JFK", city="New York")
In [4]: lhr = Airport(code="LHR", city="London")
In [6]: cdg = Airport(code="CDG", city="Paris")
In [9]: nrt = Airport(code="NRT", city="Tokyo")
# Save the airports to the database
In [3]: jfk.save()
In [5]: lhr.save()
In [8]: cdg.save()
In [10]: nrt.save()
# Add a flight and save it to the database
f = Flight(origin=jfk, destination=lhr, duration=414)
f.save()
# Display some info about the flight
In [14]: f
Out[14]: <Flight: 1: New York (JFK) to London (LHR)>
In [15]: f.origin
Out[15]: <Airport: New York (JFK)>
# Using the related name to query by airport of arrival:
In [17]: lhr.arrivals.all()
Out[17]: <QuerySet [<Flight: 1: New York (JFK) to London (LHR)>]>
[1] Import everything
[2] Create an airport and save airport that get added to my airport table.
[11] Add a flight
Starting our application
We can now build an application around this process of using models to interact with a database.
- Create an index route for our airline.
#'urls.py'
urlpatterns = [
path('', views.index, name="index"),
]
#'Views.py'
from django.shortcuts import render
from .models import Flight, Airport
# Create your views here.
def index(request):
return render(request, "flights/index.html", {
"flights": Flight.objects.all()
})
- Render a template called flight/index.html, give index.html access to a variable called flights. This variable is going to be equal to Flight.objects.all to get me all of the flights that I would like to put right here.
- Create individual templates.
#'layout.html'
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flights</title>
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
- The default layout for this particular page.
#'index.html'
{% extends "flights/layout.html" %}
{% block body %}
<h1>Flights:</h1>
<ul>
{% for flight in flights %}
<li>Flight {{ flight.id }}: {{ flight.origin }} to {{ flight.destination }}</li>
{% endfor %}
</ul>
{% endblock %}
- I've created a template that I'm going to give access to a variable called flights, where flights is going to be a variable that represents all of the flights that I queried by running Flight.objects.all.
- Using Django's API, using the functions that it has given me access to, take the flight and get all of the flights that are stored inside of Django's database.
When we've done here is created a default page where we have a list of all flights we've created so far.
- Add some more flights to the application by returning to the Django shell:
# Using the filter command to find all airports based in New York
In [3]: Airport.objects.filter(city="New York")
Out[3]: <QuerySet [<Airport: New York (JFK)>]>
# Using the get command to get only one airport in New York
In [5]: Airport.objects.get(city="New York")
Out[5]: <Airport: New York (JFK)>
# Assigning some airports to variable names:
In [6]: jfk = Airport.objects.get(city="New York")
In [7]: cdg = Airport.objects.get(city="Paris")
# Creating and saving a new flight:
In [8]: f = Flight(origin=jfk, destination=cdg, duration=435)
In [9]: f.save()
- filter: If I want to filter my airports list, not get all of the airports but just get some of them
- get: Take that query set, and just get me the first and only thing in that query set. An error if ever there's more than one or if there's none.
- Ex) Create a flight that is going from New York to Paris, cdg equals Airport.objects.get.. Now, I have the variable cdg, which represents the airport Paris. f is going to be a flight whose origin is jfk, destination is cdg, duration is 435.
Django Admin
Django comes with a default admin interface that allows us to do this more easily.
- To begin using this tool, we must first create an administrative user.
- We need to create an administrative account inside of our Django web application.
(base) cleggett@Connors-MacBook-Pro airline % python manage.py createsuperuser
Username: admin
Email address: 생략
Password: adminpw
Password (again):
Superuser created successfully.
(admin/adminpw)
- We must add our models to the admin application by entering the 'admin.py' file within our app, and importing and registering our models.
- This tells Django which models we would like to have access to in the admin app.
from django.contrib import admin
from .models import Flight, Airport
# Register your models here.
admin.site.register(Flight)
admin.site.register(Airport)
- Tell Django's admin app that I would like to use the admin app to be able to manipulate airports and to be able to manipulate flights as well.
- Now, when we visit our site and add '/admin' to the url, we can log into a page that looks like this.
- After loggin in, you'll be brought to a page like the one below where you can create, edit, and delete objects stored in the database.
- Now I have the ability to add and manipulate airports and flights via this web interface.
- I'd like to every flight have its own page, not just /flights for all the flights, but /flight/1 for flight ID 1.
- Let's add a few more pages to our site. We'll begin by adding the ability to click on a flight to get more information about it. Create a URL path that includes the 'id' of a flight:
path("<int:flight_id>", views.flight, name="flight")
- Specify a flight ID, which would be an integer. Load the flight view, whose name will be flight.
- Then, in 'views.py' we'll create a 'flight' function that takes in a flight id and renders a new html pages:
def flight(request, flight_id):
flight = Flight.objects.get(id=flight_id)
return render(request, "flights/flight.html", {
"flight": flight
})
- Define a flight function that accepts as an argument a flight ID.
- Get me the flight whose id is equal to flight_id. Or alternatively, Django also let's you use pk instead of id. It's a much more generic way of referencing the primary key, for whatever the primary key happens to be called the pk in this case is just the id.
- Render a template and pass as input to that the flight. So we're passing this flight to flight.html.
- Create a template to display this flight information with a link back to the home page.
{% extends "flights/layout.html" %}
{% block body %}
<h1>Flight {{ flight.id }}</h1>
<ul>
<li>Origin: {{ flight.origin }}</li>
<li>Destination: {{ flight.destination }}</li>
<li>Duration: {{ flight.duration }} minutes</li>
</ul>
<a href="{% url 'index' %}">All Flights</a>
{% endblock %}
- I have a page that displays flight information about any particular flight. Querying for that particular flight, then printing out its origin, destination, and duration.
- There is some error. If I try and access a flight that doesn't exist, I'm going to get some sort of error 'DoesNotExist'.
- Add the ability, not only to have flights that have airports associated with them, but let's also add passengers to our flights as well to be able to represent passengers that might actually be on these flights too.
- Finally, we need to add the ability to link from one page to another, so we'll modify our index page to include links:
{% extends "flights/layout.html" %}
{% block body %}
<h1>Flights:</h1>
<ul>
{% for flight in flights %}
<li><a href="{% url 'flight' flight.id %}">Flight {{ flight.id }}</a>: {{ flight.origin }} to {{ flight.destination }}</li>
{% endfor %}
</ul>
{% endblock %}
- Our homepage > Flight 5
사진들
Many-to-Many Relationships
Let's work on integrating passengers into our models. Create a passenger model to start:
class Passenger(models.Model):
first = models.CharField(max_length=64)
last = models.CharField(max_length=64)
flights = models.ManyToManyField(Flight, blank=True, related_name="passengers")
def __str__(self):
return f"{self.first} {self.last}"
- Passengers have a Many to Many relationship with flights, which we describe in Django using the ManyToManyField.
- The first argument in this field is the class of objects that this one is related to.
We have provided the argument 'blank=True' which means a passenger can have no flights.
- We have added a 'related_name' that serves the same purpose as it did earlier: it will allow us to find all passengers on a given flight.
To actually make these changes, we must make migrations and migrate. We can then register the Passenger model in 'admin.py' and visit the admin page to create some passengers.
- Go into admin.py, and register 'admin.site.register(Passenger).
from django.contrib import admin
from .models import Flight, Airport, Passenger
# Register your models here.
admin.site.register(Flight)
admin.site.register(Airport)
admin.site.register(Passenger)
- Now that we've added some passengers, let's update our flight page so that it displays all passengers on a flight.
- Visit 'views.py' and update our flight view to provide a list of passengers as context. We access the list using the related name we defined earlier.
def flight(request, flight_id):
flight = Flight.objects.get(id=flight_id)
return render(request, "flights/flight.html",{
"flight":flight,
"passengers":flight.passengers.all()
})
-> In addition to giving access to the flight, also to passengers.
-> flight.passengers.all(): 'passengers' is the related name. Take a flight and get all of the passengers that happened to be on that flight.
- Add a list of passengers to 'flight.html':
<h2>Passengers:</h2>
<ul>
{% for passenger in passengers %}
<li>{{ passenger }}</li>
{% empty %}
<li>No passengers</li>
{% endfor %}
</ul>
- The homepage > flight 2
> Link those pages to 'flight.html' by adding a link to the url index. I've already done it.
<a href="{% url 'index' %}">All Flights</a>
- So here is a link that takes me to the index view.
- Go into 'index.html', and for each of the list items, is going to be a link that links to its url to a particular flight. The flight route takes as a parameter a flight ID. So inside this url substitution, I can specify use 'flight.id' as the ID of the flight.
- I've put every single flight inside of a link that takes me to the flight route. But because the flight route requires as an argument the flight ID, I can specify the flight ID.
{% for flight in flights %}
<li>
<a href="{% url 'flight' flight.id %}">
Flight {{ flight.id }}: {{flight.origin }} to {{ flight.destination}}
</a>
</li>
{% endfor %}
> In addition to displaying all the passengers on any particular flight, also give myself the ability to add passengers to a flight as well.
- Give visitors to our site the ability to book a flight. Add a booking route in 'urls.py':
path("<int:flight_id>/book", views.book, name="book")
- We'll add a book function to 'views.py' that adds a passenger to a flight:
def book(request, flight_id):
# For a post request, add a new flight
if request.method == "POST":
# Accessing the flight
flight = Flight.objects.get(pk=flight_id)
# Finding the passenger id from the submitted form data
passenger_id = int(request.POST["passenger"])
# Finding the passenger based on the id
passenger = Passenger.objects.get(pk=passenger_id)
# Add passenger to the flight
passenger.flights.add(flight)
# Redirect user to flight page
return HttpResponseRedirect(reverse("flight", args=(flight.id,)))
- There are multiple ways that I can request a web page.
1. GET request method: I would just like to get this page
2. POST: I would like to send data to the page
-- Anytime you want to manipulate the state of something, especially manipulating our database, that should be inside of a POST request.
-- I'm submitting some form, some data. And in response to that POST submission, you should manipulate what's going on inside of the database.
def book(request, flight_id):
if request.method == "POST":
flight = Flight.objects.get(pk=flight_id)
passenger = Passenger.objects.get(pk=int(request.POST["passenger"]))
passenger.flights.add(flight)
return HttpResponseRedirect(reverse("flight", args=(flight.id, )))
> Code:
- If request method is POST.
- The flight is just going to be Flight.objects.get. Get the flight whose primary key is that flight ID.
Those are the two pieces of information you need to know in order to actually book a flight. The flight and the passenger information.
- The data about which passenger ID we want to register on this flight is going to be passed in via a form with an input field whose name is passenger. The name on any particular input field dictates what name we get, is received when a route like this book route is able to process the request from the user. Because by default this might be a string, convert it into an integer to make sure we're dealing with an integer.
=> If the request method is POST, meaning someone submitted this form via the POST request method, 'Flights.objects.get' to get a particular flight, get me the flight with that flight ID. And then, I'm getting a passenger who's pk(ID) is equal to whatever was submitted via this post form with a name of passenger.
- To access a passenger's flights, say passenger.flights. And in order to add a new item to some set like flights, say 'passenger.flights.add flight'. This will do the equivalent of adding a new row into a table of keeping track that the passengers on that flight.
=> Take this passenger, take their set of flights and add a new flight to that set of flights.
- Return some sort of redirect that redirects the user back to the flight page. Return an HTTP response redirect. I'd like to take them to the flight route("flight") and reverse, takes the name of a particular view, and gets me what the URL is. The flight route takes an argument, the flight's ID. The flight's ID is going to redirect me back to the flight route so that I can see that flight page again.
+ from django.http import HttpResponseRedirect, from django.urls import reverse
=> Now I can redirect the user back to the flight page after they're done submitting the form, reverse takes the name of a particular views defined in urls.py(Ex. name="index"), and gets me what the actual URL path should be.
- I don't have to hard code URLs into my Django web application. Just reference URLs by their name and if ever I need to change a URL, just change it in one place in urls.py and that change is going to reflect everywhere else.
- I've just made a function called book that is waiting for a post request to be made to it. I'd like to add that form now.
> Next, we'll add some context to our flight template so that the page has access to everyone who is not currently a passenger on the flight using Django's ability to exclude certain objects from a query:
def flight(request, flight_id):
flight = Flight.objects.get(id=flight_id)
passengers = flight.passengers.all()
non_passengers = Passenger.objects.exclude(flights=flight).all()
return render(request, "flights/flight.html", {
"flight": flight,
"passengers": passengers,
"non_passengers": non_passengers
})
+ The flight page only has access to actual passengers, and doesn't yet have access to people that are not passengers on the flight. So I want to exclude passengers who have this as one of their flights.
-- When I render flight.html, there's a couple pieces of information that it should have. It needs to know what flight is being rendered, who are the passengers.
But if I want a dropdown where I can choose from all the people who aren't already on the flight, I also need all of the non-passengers. Passengers except the ones who are already on the flight, and get me all of them is what that '.all' is ultimately saying.
- Add a form to our flight page's HTML using a select input field:
<form action="{% url 'book' flight.id %}" method="post">
{% csrf_token %}
<select name="passenger" id="">
{% for passenger in non_passengers %}
<option value="{{ passenger.id }}">{{ passenger }}</option>
{% endfor %}
</select>
<input type="submit">
</form>
-- In urls.py, the route with name book requires as a parameter some flight ID. So I need to provide the flight ID as an argument for what flight I'm booking the passenger on.
-- I need to give it the CSRF token for security to make sure that Django knows it's relly this application that is submitting this form.
-- Add a dropdown list which can create in HTML using a select field. The name of this select field is passenger. The reason for that is inside of 'views.py', when I get the passenger, I'm looking for inside the post data for a field whose name is passenger. (request.POST["passenger"])
-- The option for everyone who isn't a passenger on this flight.
-- The option for everyone who isn't a passenger on this flight.
-- The options value is going to be the passenger's ID. Because when I submit the form, what I care about getting is what is the ID of this passenger that I've chosen from this dropdown. But the user who's looking at this page, they don't want to see people's IDs. So Inside of the option tag, just print out the passenger's name.
-- Add an input whose type is submit to let myself submit this form to.
The site looks like when I go to a flight page and then add a passenger
> Customizable: Another advantage of using the Django admin app
- Ex) If we wish to see all aspects of a flight in the admin interface, we can create a new class within 'admin.py' and add it as anargument when registering the 'Flight' model:
class FlightAdmin(admin.ModelAdmin):
list_display = ("id", "origin", "destination", "duration")
# Register your models here.
admin.site.register(Flight, FlightAdmin)
- Make a class called FlightAdmin, which is going to be a subclass of ModelAdmin.
- When I register the flight, use the FlightAdmin settings.
- Now, When we visit the admin page for flights, we can see the 'id' as well.
> Update my passenger admin
- Use a horizontal filter on flights to make it a bit nicer for manipulating the flights that a passenger is on.
Users
Authentication, allowing users to log in and out of a website.
- Create a new app called 'users'. We'll go through all the normal steps of creating a new app.
1. terminal: python manage.py startapp users
2. settings.py - INSTALLED_APPS: 'users',
3. urls.py: path('users/', include("users.urls"))
> In our new 'urls.py' file, we'll add a few more routes:
urlpatterns = [
path('', views.index, name="index"),
path("login", views.login_view, name="login"),
path("logout", views.logout_view, name="logout")
]
- One main index route is going to display information about the currently signed in user. - One route for logging someone in, a form that will display the place where they can type in a user name and password to log in.
- One route to allow users to be able to log out from this application.
> templates/users - layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Users</title>
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
> Create a form where a user can log in. We'll create a 'layout.html' file as always, and then create a 'login.html' file which contains a form, and that displays a message if one exists.
{% extends "users/layout.html" %}
{% block body %}
{% if message %}
<div>{{ message }}</div>
{% endif %}
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
<input type="text", name="username", placeholder="Username">
<input type="password", name="password", placeholder="Password">
<input type="submit", value="Login">
</form>
{% endblock %}
- When I submit the form, go to the login url using the POST request method. You don't want the user name and password to be passed in as get parameters because those show up in the URL.
- CSRF token for security.
- Input type is text, a placeholder for the user knows what to type in.
- Input type is password, means our HTML will know in the browser will know to show the password as dots.
> In 'views.py', add three functions:
def index(request):
# If no user is signed in, return to login page:
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse("login"))
return render(request, "users/user.html")
def login_view(request):
return render(request, "users/login.html")
def logout_view(request):
# Pass is a simple way to tell python to do nothing.
pass
- The index function is going to display information about the currently signed in user.
-- If someone tries to access this page but they're not authenticated. The request object that gets passed in as part of the request to every user in Django automatically has a user attribute associated with it. And that user object has an 'is_authenticated' attribute that tells us if the user is signed in or not.
-- If they're not signed in, HttpResponseRedirect them to the log in view.
- The login_view render 'users/login.html'.
> We can head to the admin site and add some users.
> We'll go back to 'views.py' and update our 'login_view' function to handle a 'POST' request with a username and password:
# Additional imports we'll need:
from django.contrib.auth import authenticate, login, logout
def login_view(request):
if request.method == "POST":
# Accessing username and password from form data
username = request.POST["username"]
password = request.POST["password"]
# Check if username and password are correct, returning User object if so
user = authenticate(request, username=username, password=password)
# If user object is returned, log in and route to index page:
if user:
login(request, user)
return HttpResponseRedirect(reverse("index"))
# Otherwise, return login page again with new context
else:
return render(request, "users/login.html", {
"message": "Invalid Credentials"
})
return render(request, "users/login.html")
- In the login_view, there are two ways the login_view function could be called.
1. GET request method: show me the login form.
2. POST: submit data to the login form.
-- Let me first get the user name which will be inside of the post data in a field called user name. And get the password which will be in a field in the request.POST inside of password.
-- Import three functions. from django.contrib.auth import authenticate, login, logout
--- authenticate: Checks if user name and password are correct
--- Takes the request, a user name, a password and if the user name&password are valid, they give me back who the user actually is.
-- If the user is not none, that means the authentication was successful, let the user log in.
-- If the authentication failed, render the same users login page again.
--- Inside of 'login.html', add some logic that if there's a message, then display that message inside of a div.
> Now, create the 'user.html' file that the 'index' function renders when a user is authenticated.
{% extends "users/layout.html" %}
{% block body %}
<h1>Welcome, {{ request.user.first_name }}</h1>
<ul>
<li>Username: {{ request.user.username }}</li>
<li>Email: {{ request.user.email }}</li>
</ul>
<a href="{% url 'logout' %}">Log Out</a>
{% endblock %}
- {{ request.user.first__name }}: I have access to the request that was used to make this HTTP request, which means I also have access to request.user who is the user associated with that request.
- Add a link that will go to the log out route. If users click log out, they gets logged out and brought back to the logout page because now 'request.user.isauthenticated' is going to be false.
> Finally, to allow the user to log out, we'll update the 'logout_view' function so that it uses Django's built-in 'logout' function:
def logout_view(request):
logout(request)
return render(request, "users/login.html", {
"message": "Logged Out"
})
- Take them back to the login page with a message of Logged Out to indicate that the user has now been logged out.
> Now, we can log in and out.
댓글 없음:
댓글 쓰기