10. Flask

Elenaljh·2023년 6월 20일
0

CS50

목록 보기
13/14

Lecture 9

Welcome!

  • In previous weeks, you have learned numerous programming languages, techniques, and strategies.
  • Indeed, this class has been far less of a C class or Python class and far more of a programming class, such that you can go on to follow future trends.
  • In these past several weeks, you have learned how to learn about programming.
  • Today, we will be moving from HTML and CSS into combining HTML, CSS, SQL, Python, and JavaScript so you can create your own web applications.

Static to Dynamic

  • Up until this point, all HTML you saw was pre-written and static.
  • In the past, when you visited a page, the browser downloaded an HTML page, and you were able to view it.
  • Dynamic pages refer to the ability of Python and similar languages to create HTML files on-the-fly. Accordingly, you can have web pages that are generated by options selected by your user.
  • You have used http-server in the past to serve your web pages. Today, we are going to utilize a new server that can parse out a web address and perform actions based on the URL provided.

Flask

  • Flask is a third-party library that allows you to host web applications using using the Flask framework within Python.
  • You can run flask by executing flask run.
  • To do so, you will need a file called app.py and a folder called templates, and static. -> templates 안에 html 문서들 저장, static 안에 이미지, 동영상, css, js 파일 등을 저장.
  • 이 외에도 problem set 9의 requirements.txt는 페이지 만들 때 필요한 라이브러리들을 모아놓은 파일이라고 한다.

1. hello

[index.html]

  • To get started, create a folder called templates and create a file called index.html with the following code:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>hello</title>
        </head>
        <body>
            hello, {{ name }}
        </body>
    </html>
    

    Notice the double {{ name }} that is a placeholder for something that will be later provided by our Flask server.

    • cf. <meta name="viewport" content="initial-scale=1, width=device-width">: 사용하는 기기의 사이즈에 맞게 페이지의 사이즈를 조정하는 코드 (mobile-friendly code)
  • 다음 내용으로 넘어가기 전, Flask Docs를 읽어보시오!

[app.py]_step1. index.html과 연결

  • Then, in the same folder that the templates folder appears, create a file called app.py and add the following code:
    ```py
    # Greets user
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return render_template("index.html", name=request.args.get("name", "world"))
    
    ```
    Notice that this code defines  `app`  as the Flask application. Then, it defines the  `/`  route of  `app`  as returning the contents of  `index.html`  with the argument of  `name`. 즉, `render_template("연결할 페이지 파일명", arg)`에서 `arg = 어쩌고`는 index.html의 `{{ arg }}`와 연결되어  html 파일에 arg값을 전달해준다. 이건 Jinja의 문법이다. By default, the  `request.args.get`  function will look for the  `name`  being provided by the user. If no name is provided, it will default to  `world`. [이 함수가 궁금하다면 이걸 보시오](https://stackoverflow.com/questions/34671217/in-flask-what-is-request-args-and-how-is-it-used)  하여간 요지는, index.html의 {{ name }} 자리에 `페이지주소/name = 뫄뫄&key=value&...`의 `뫄뫄`를 입력하고 싶다는 뜻이다. 
    참고: 메모장에다 대충 정리해놨는데, 예쁘게 따로 정리하기.
    - from flask import Flask, render_template, request: Flask, render_template는 함수, request는 변수임.
    - __name__ 변수: 현재 .py 파일의 이름을 가지고 있는 변수임.
    	<details>
    	<summary>`__name__`이란</summary>
	[참고하쇼](https://dojang.io/mod/page/view.php?id=2448)        

	first.py(main)에서 second.py(module)를 import해서 프로그램을 짜다보면, 나중에 시간이 흘렀을 때 두 파일 중 무엇이 메인이고 무엇이 모듈인지 헷갈릴 수 있다. 이 때 변수 `__name__`을 사용해 모듈과 메인을 구분할 수 있다.     
	1. 모듈 코드 (second.py)
	```py
	print('second 모듈 시작')
	print('second.py __name__:', __name__) 
	print('second 모듈 끝')
	```
	2. 메인 코드 (first.py)
	```py
	import second
	print('first.py __name__:', __name__)
	```
	3. first.py 실행 결과
	```
	second 모듈 시작
	second.py __name__: second
	second 모듈 끝
	first.py __name__: __main__
	```
	파이썬에서 모듈을 import하면 해당 모듈의 스크립트 파일이 한 번 실행되기 때문에 first.py 안에서 second.py가 실행된 것이다.     
	하여튼 second.py안의 `__name__`은 `second`이고, first.py 안의 `__name__`은 `__main__` 이라는 것을 알 수 있다.         
	따라서 파이썬 코드 실행시 `if __name__ == '__main__':`이라는 코드는 현재 실행되고 있는 코드줄이 module 소속인지, main 소속인지 알아내는 코드라는 것임.     
	</details> 
  • Finally, add a final file in the same folder as app.py called requirements.txt that has only a single line of code:

    Flask
    

    Notice only Flask appears in this file.

  • You can run this file by typing flask run in the terminal window. If Flask does not run, ensure that your syntax is correct in each of the files above. Further, if Flask will not run, make sure your files are organized as follows:

    /templates
        index.html
    app.py
    requirements.txt
    

    Once you get it running, you will be prompted to click a link. Once you navigate to that webpage, try adding ?name=[Your Name] to the base URL in your browser’s URL bar. -> 이렇게 하면 request.args.get("name", "world")코드가 url에 입력한 이름을 읽어와서 웹페이지에 전달한다.

2. Greet

[index.html] 발전시키기 -> hello,____을 출력하는 대신, 입력창과 버튼 만들어 이름 입력하게 하는 페이지로 전환.

  • Improving upon our program, we know that most users will not type arguments into the address bar. Instead, programmers rely upon users to fill out forms on web pages. Accordingly, we can modify index.html as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>hello</title>
        </head>
        <body>
            <form action="/greet" method="get"> 
                <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
                <button type="submit">Greet</button>
            </form>
        </body>
    </html>
    
  • Notice that a form is now created that takes the user’s name and then passes it off to a route called /greet (-> <form action="/greet">로 구현함). <form> 태그의 action 속성은 폼 데이터(form data)를 서버로 보낼 때 해당 데이터가 도착할 URL을 명시합니다.

  • method = "get"은 관리자페이지 -> Network -> preserve log check -> All -> 페이지 로드시 로그 최상단에 나타나는 파일 headers -> request method 보면 GET이라고 나오는데 그 GET을 의미하는게 맞음. GET방식은 URL에 폼 데이터를 추가해 서버로 전달하는 방식으로, 보통 쿼리 문자열(query string)에 포함되어 전송되므로 길이의 제한이 있다.(ex. jihye가 /에서 /greet으로 GET방식으로 전달될 경우 /greet페이지의 형태: example.com/greet?name=jihye) 따라서 보안상 취약점이 존재하므로 중요한 데이터는 POST 방식을 사용한다. 또한 GET방식의 HTTP요청은 브라우저에 의해 캐시되어 저장된다.

  • <input placeholder="Name">은 입력창 안의 옅은 회색 글씨로, 어떤 것을 입력해야할지 안내해주는 문구라고 생각하면 됨.

  • <input name="name">: id 속성과 다르게 name속성은 page 영역에서 중복되어 사용이 가능하며, ** Form 객체(ex. input, radio, checkbox 등)에서 action에 해당하는 페이지에 전송하는 파라미터의 key값으로 사용한다. 즉 텍스트박스에 입력한 값은 (key:value) = (name:입력한 값)이 되어 /greet페이지로 전달된다.

  • Further, we can change app.py as follows:

    # Adds a form, second route
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/greet")
    def greet():
        return render_template("greet.html", name=request.args.get("name", "world"))
    

    Notice that the default path will display a form for the user to input their name. The /greet route will pass the name to that web page.

  • To finalize this implementation, you will need another template for greet.html as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>hello</title>
        </head>
        <body>
            hello, {{ name }}
        </body>
    </html>
    

    Notice that this route will now render the greeting to the user, followed by their name.

Layout

  • Both of our web pages, index.html and greet.html, have much of the same data. Wouldn’t it be nice to allow the body to be unique, but copy the same layout from page to page? -> 즉, 번거롭게 페이지 레이아웃 (<head>부분 등)을 모든 페이지마다 복붙하지 말고 페이지의 <body>부분만 쓸 수 있도록 구성해보자.

  • First, create a new template called layout.html and write code as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>hello</title>
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>
    

    Notice that the {% block body %}{% endblock %} allows for the insertion of other code from other HTML files.

  • Then, modify your index.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/greet" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <button type="submit">Greet</button>
        </form>
    
    {% endblock %}
    

    Notice that the line {% extends "layout.html" %} tells the server where to get the layout of this page. Then, the {% block body %}{% endblock %} tells what code to be inserted into layout.html.

  • Finally, change greet.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        hello, {{ name }}
    {% endblock %}
    

    Notice how this code is shorter and more compact.

POST

  • You can imagine scenarios where it is not safe to utilize get, as usernames and passwords would show up in the URL.

  • Post 방식은 폼 데이터를 별도로 첨부해 서버로 전달하는 방식이다. 이 경우 http 요청은 브라우저에 의해 캐시되지 않으므로, 브라우저 히스토리에도 남지 않는다. 또한, post 방식의 http요청에 의한 데이터는 쿼리 문자열과는 별도로 전송되기 때문에 입력한 데이터가 url에 뜨지 않고, 데이터의 길이에 대한 제한도 없으며 get 방식보다 보안성이 높다. 또한 Post 방식은 서버에 어떤 정보를 전달하는데도 쓰인다 (ex. 장바구니에 물건 추가, 데이터베이스에 정보 추가 등) (Get은 getting information만 가능. posting/sending information 불가)

  • We can utilize the method post to help with this problem by modifying app.py as follows:

    # Switches to POST
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/greet", methods=["POST"])
    def greet():
        return render_template("greet.html", name=request.form.get("name", "world"))
    

    Notice that methods=["POST"] is added to the /greet route, and that we use request.form.get rather than request.args.get.

    Flask의 request에 대해..

    The docs describe the attributes available on the request object (from flask import request) during a request. In most common cases request.data will be empty because it's used as a fallback:

    request.data Contains the incoming request data as string in case it came with a mimetype Flask does not handle.

    request.args: the key/value pairs in the URL query string - 'GET'에 쓰임.
    request.form: the key/value pairs in the body, from a HTML 'POST' form, or JavaScript request that isn't JSON encoded
    request.files: the files in the body, which Flask keeps separate from form. HTML forms must use enctype=multipart/form-data or files will not be uploaded.
    request.values: combined args and form, preferring args if keys overlap
    request.json: parsed JSON data. The request must have the application/json content type, or use request.get_json(force=True) to ignore the content type.
    All of these are MultiDict instances (except for json). You can access values using:

    request.form['name']: use indexing if you know the key exists
    request.form.get('name'): use get if the key might not exist
    request.form.getlist('name'): use getlist if the key is sent multiple times and you want a list of values. get only returns the first value.

  • This tells the server to look deeper in the virtual envelope and not reveal the items in post in the URL.

  • post로 보낸 정보는 url에 뜨지 않지만, 개발자도구 -> Network -> Payload에 들어가면 어떤 정보를 보냈는지 알 수 있다.

@app.route("/")과 @app.route("/greet") 합치기

  • Still, this code can be advanced further by utilizing a single route for both get and post.

    • 우선 index.html (이름 적어넣는 텍스트박스 있는 폼 페이지)를 다음과 같이 변경한다.
    {% extends "layout.html" %}
    
    {% block body %}
    
    	<form action="/" method="post">
    		<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
    	</form>
    
    {% endblock %}
    • <form action="/greet">에서 <form action="/">로 변경했다. 즉, 페이지의 정보를 /greet가 아니라 그 자신에게 제출한다는 것이다.
  • then, modify app.py as follows:
    # Uses a single  route
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/", methods=["GET", "POST"])
    def index():
        if request.method == "POST":
            return render_template("greet.html", name=request.form.get("name", "world"))
        return render_template("index.html")
    
  • Notice that both get and post are done in a single routing. ( methods=["GET","POST"]에서 GET과 POST의 순서는 관계없다.)However, request.method is utilized to properly route based upon the type of routing requested by the user.
  • request.args는 GET에서만 쓰인다. POST를 쓰고 싶다면 request.form을 써야 한다.
  • POST로 제출한 페이지에서 새로고침(새로고침 버튼이나 f5키)을 누르는 경우
    	```
    	The page that you're looking for used information that you entered.
    	Returning to that page might cause any action you took to be repeated.
    	Do you want to continue?
    	```
    	다음과 같은 경고문이 뜬다. 사람들이 실수로 같은 정보를 두 번 제출하지 않도록 막기 위함이다. (ex. 장바구니에 동일한 물건을 두 번 등록하는 경우)
  • 그러나 검색창에 커서를 놓고 enter키를 누르면 위와 같은 경고문이 나타나지 않는다. 대신 이런 방식으로 접근하면 default로 GET을 통해 페이지를 request하기 때문에 Hello, world 페이지 대신 이름을 입력하는 텍스트박스가 있는 창이 로드된다.
  • 결론: 주소창을 통해 어떤 페이지의 URL에 접근하면 GET을 통해 접근하게 된다. 이게 default임. POST를 통해 접근하고 싶다면 새로고침버튼이나 f5키, 또는 개발자가 미리 고안한 방식으로만 접근해야 한다.

Frosh IMs

  • Frosh IMs or froshims is a web application that allows students to register for intermural sports.

  • Create a folder by typing mkdir froshims in the terminal window. Then, type cd froshims to browse to this folder. Within, create a directory called templates by typing mkdir templates. Finally, type code app.py and write code as follows:

    # Implements a registration form using a select menu
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/register", methods=["POST"])
    def register():
    
        # Validate submission
        if not request.form.get("name") or request.form.get("sport") not in SPORTS:
            return render_template("failure.html")
    
        # Confirm registration
        return render_template("success.html")
    

    Notice that a failure option is provided, such that a failure message will be displayed to the user if the name or sport field is not properly filled out. -> if not request.form.get("name"): 이름을 입력하지 않음 / if request.form.get("sport") not in SPORTS: 리스트에 없는 스포츠명을 입력함

    • 주의: POST는 주로 양식 데이터를 제출하는데 사용된다. GET은 서버에 데이터를 전달하는 것을 허용하지 않은 메서드다. 따라서 @app.route("/register")이라고만 써버리면, 사용자가 입력한 정보(이름, 스포츠)를 서버에 전송할 수 없게 된다(default method가 GET이기 때문). 따라서 이런 경우, 양식을 제출할 때 405에러가 뜬다 (405 Method not allowed) 따라서 해당 코드는 @app.route("/register", method=["POST"])라고 써야 한다.
  • Next, create a file in the templates folder called index.html by typing code templates/index.html and write code as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Register</h1>
        <form action="/register" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" required type="text">
            <select name="sport">
                <option disabled selected>Sport</option>
                {% for sport in sports %}
                    <option value="{{ sport }}">{{ sport }}</option>
                {% endfor %}
            </select>
            <button type="submit">Register</button>
        </form>
    {% endblock %}
    
    • 주의: input 태그에 name 속성이 있는 것처럼, select(드롭다운 메뉴) 태그에도 name 속성이 있다. 이걸 잘 써줘야 함. 안그러면 오류난다. -> input에 사용자는 이름을 입력하게 되는데, <input name="name">을 쓰면 사용자가 입력한 이름을 name이라는 변수로 관리할 수 있게 된다. 마찬가지로 select를 통해 사용자는 스포츠명을 입력하게 되는데, <select name="sport">를 쓰면 사용자가 입력한 스포츠명을 sport라는 변수로 관리할 수 있다.
    • input태그의 required 속성은, 입력하지 않으면 다음단계로 넘어갈 수 없게 만드는, 필수항목을 만드는 속성이다.
    • html에서 아래 코드는 드롭다운 박스를 만드는 코드이다.
      	```html
      	<select name="sport">
      		<option disabled selected>Sport</option>
      		<option value="Basketball">Basketball</option>
      		<option value="Soccer">Soccer</option>
      		<option value="Frisbee">Frisbee</option>
      	</select>
      	```  
      	여기에서 첫번째 옵션 코드는 default로 선택되어있는 빈칸이다.  +주의: 위와 같은 식으로 코드를 짜면 해킹위험이 있기 때문에 app.py에 `SPORTS = ["Basketball", "Soccer", "Frisbee"]` 리스트를 만든다. 대신 html파일에는 jinja 반복문을 이용해서 SPORTS 리스트를 입력해준다. 
  • Next, create a file called layout.html by typing code templates/layout.html and write code as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>froshims</title>
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>
    
  • Fourth, create a file in templates called success.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        You are registered!
    {% endblock %}
    
  • Finally, create a file in templates called failure.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        You are not registered!
    {% endblock %}
    
  • You can imagine how we might want to accept the registration of many different registrants. We can improve app.py as follows:

    # Implements a registration form, storing registrants in a dictionary, with error messages
    
    from flask import Flask, redirect, render_template, request
    
    app = Flask(__name__)
    
    REGISTRANTS = {}
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/register", methods=["POST"])
    def register():
    
        # Validate name
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="Missing name")
    
        # Validate sport
        sport = request.form.get("sport")
        if not sport:
            return render_template("error.html", message="Missing sport")
        if sport not in SPORTS:
            return render_template("error.html", message="Invalid sport")
    
        # Remember registrant
        REGISTRANTS[name] = sport
    
        # Confirm registration
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        return render_template("registrants.html", registrants=REGISTRANTS)
    

    Notice that a dictionary called REGISTRANTS is used to log the sport selected by REGISTRANTS[name]. Also, notice that registrants=REGISTRANTS passes the dictionary on to this template. 또한 SPORTS는 일부러 app.py에 만든 것이다. 그 이유는 html파일에서 <select>에 일일히 종목명을 적으면 나중에 사람들이 관리자도구를 이용해서 html파일을 수정해서 자기가 원하는 스포츠 항목을 만들어내고 제출해버리는 불상사가 생길 수 있다. 이런 보안문제들을 예방하기 위해서 이런 리스트는 서버쪽에 만들어놓는다. 그리고 이렇게 한 다음에는 꼭 이 리스트를 넣고자 하는 페이지쪽의 코드에 포함시켜줘야 한다. -> return render_template("index.html", sports=SPORTS)

  • Further, create a new template called registrants.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Registrants</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Sport</th>
                </tr>
            </thead>
            <tbody>
                {% for name in registrants %}
                    <tr>
                        <td>{{ name }}</td>
                        <td>{{ registrants[name] }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    Notice that {% for name in registrants %}...{% endfor %} will iterate through each of the registrants(=REGISTRANTS 딕셔너리 말하는거임). Very powerful to be able to iterate on a dynamic web page! +참고로 위 코드는 표(table)를 만드는 것이다.

  • Executing flask run and entering numerous names and sports, you can browse to /registrants to view what data has been logged.

  • You now have a web application! However, there are some security flaws! Because everything is client-side, an adversary could change the HTML and hack a website. Further, this data will not persist if the server is shut down. Could there be some way we could have our data persist even when the server restarts?

Flask and SQL

  • 모든 자료를 global dictionary(REGISTRANTS)에 저장하는 것은 좋지 않은 방법이다. 그 이유는 컴퓨터를 끄거나 vscode에서 ctrl+c를 눌러 Flask를 종료하는 경우 딕셔너리에 저장된 데이터들도 다 날라가기 때문이다. 따라서 csv 등 안정적인 데이터베이스에 자료를 저장하는 것이 좋다.

  • Just as we have seen how Python can interface with a SQL database, we can combine the power of Flask, Python, and SQL to create a web application where data will persist!

  • To implement this, you will need to take a number of steps.

  • First, modify requirements.txt as follows:

    cs50
    Flask
    
  • Modify index.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Register</h1>
        <form action="/register" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            {% for sport in sports %}
                <input name="sport" type="radio" value="{{ sport }}"> {{ sport }}
            {% endfor %}
            <button type="submit">Register</button>
        </form>
    {% endblock %}
    
    • input태그의 type="radio"는 각 스포츠명의 왼쪽에 작은 동그라미를 생성해서 체크할 수 있게 하는 속성이다.
  • Modify layout.html as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>froshims</title>
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>
    
  • Ensure failure.html appears as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        You are not registered!
    {% endblock %}
    
  • Modify registrants.html to appear as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Registrants</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Sport</th>
                    <th></th>  <!--나중에 deregister 버튼이 들어갈 자리임-->
                </tr>
            </thead>
            <tbody>
                {% for registrant in registrants %}
                    <tr>
                        <td>{{ registrant.name }}</td>
                        <td>{{ registrant.sport }}</td>
                        <td>
                            <form action="/deregister" method="post">
                                <input name="id" type="hidden" value="{{ registrant.id }}">
                                <button type="submit">Deregister</button>
                            </form>
                        </td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    Notice that a hidden value registrant.id is included such that it’s possible to use this id later in app.py

  • Finally, modify app.py as follows:

    # Implements a registration form, storing registrants in a SQLite database, with support for deregistration
    
    from cs50 import SQL
    from flask import Flask, redirect, render_template, request
    
    app = Flask(__name__)
    
    db = SQL("sqlite:///froshims.db")
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/deregister", methods=["POST"])
    def deregister():
    
        # Forget registrant
        id = request.form.get("id")
        if id:
            db.execute("DELETE FROM registrants WHERE id = ?", id)
        return redirect("/registrants")
    
    
    @app.route("/register", methods=["POST"])
    def register():
    
        # Validate submission
        name = request.form.get("name")
        sport = request.form.get("sport")
        if not name or sport not in SPORTS:
            return render_template("failure.html")
    
        # Remember registrant
        db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)
    
        # Confirm registration
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        registrants = db.execute("SELECT * FROM registrants")
        return render_template("registrants.html", registrants=registrants)
    

    Notice that the cs50 library is utilized. A route is included for register for the post method. This route will take the name and sport taken from the registration form and execute a SQL query to add the name and the sport to the registrants table. The deregister routes to a SQL query that will grab the user’s id and utilize that information to deregister this individual.

  • if id: 'id가 존재한다면'이라는 뜻

  • db = SQL("sqlite:///froshims.db"): froshims.db라는 파일을 연다.

  • db.execute: cs50 library의 SQL 함수임. 해킹을 대비한 data sanitize까지 다 해놓음. 위 함수는 'registrants'라는 2열짜리 표(name, sport)를 만든다.

  • db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport): ?는 placeholder이다. 각 ? 자리 안에 name과 sport가 들어간다.

  • You can read more in the Flask documentation.

Session

  • While the above code is useful from an administrative standpoint, where a back-office administrator could add and remove individuals from the database, one can imagine how this code is not safe to implement on a public server.

  • For one, bad actors could make decisions on behalf of other users by hitting the deregister button – effectively deleting their recorded answer from the server.

  • Web services like Google use login credentials to ensure users only have access to the right data.

  • We can actually implement this itself using cookies. Cookies are small files that are stored on your computer, such that your computer can communicate with the server and effectively say, “I’m an authorized user that has already logged in.”

  • In the simplest form, we can implement this by creating a folder called login and then adding the following files.

  • First, create a file called requirements.txt that reads as follows:

    Flask
    Flask-Session
    

    Notice that in addition to Flask, we also include Flask-Session, which is required to support login sessions.

  • Second, in a templates folder, create a file called layout.html that appears as follows:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>store</title>
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>
    

    Notice this provides a very simple layout with a title and a body.

  • Third, create a file in the templates folder called index.html that appears as follows:

    {% extends "layout.html" %}
    
    {% block body %}
    
        {% if session["name"] %}
            You are logged in as {{ session["name"] }}. <a href="/logout">Log out</a>.
        {% else %}
            You are not logged in. <a href="/login">Log in</a>.
        {% endif %}
    
    {% endblock %}
    

    Notice that this file looks to see if session["name"] exists. If it does, it will display a welcome message. If not, it will recommend you browse to a page to log in.

  • Fourth, create a file called login.html and add the following code:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/login" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <button type="submit">Log In</button>
        </form>
    
    {% endblock %}
    

    Notice this is the layout of a basic login page.

  • Finally, create a file in the login folder called app.py and write code as follows:

    from flask import Flask, redirect, render_template, request, session
    from flask_session import Session
    
    # Configure app
    app = Flask(__name__)
    
    # Configure session
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    
    @app.route("/")
    def index():
        if not session.get("name"):
            return redirect("/login")
        return render_template("index.html")
    
    
    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            session["name"] = request.form.get("name")
            return redirect("/")
        return render_template("login.html")
    
    
    @app.route("/logout")
    def logout():
        session["name"] = None
        return redirect("/")
    

    Notice the modified imports at the top of the file, including session, which will allow for you to support sessions. Most important, notice how session["name"] is used in the login and logout routes. The login route will assign the login name provided and assign it to session["name"]. However, in the logout route, the logging out is implemented by simply setting session["name"] to None.

  • You can read more about sessions in the Flask documentation.

Store

  • Moving on to a final example of utilizing Flask’s ability to enable a session.

  • We examined the following code for store in app.py. The following code was shown:

    from cs50 import SQL
    from flask import Flask, redirect, render_template, request, session
    from flask_session import Session
    
    # Configure app
    app = Flask(__name__)
    
    # Connect to database
    db = SQL("sqlite:///store.db")
    
    # Configure session
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    
    @app.route("/")
    def index():
        books = db.execute("SELECT * FROM books")
        return render_template("books.html", books=books)
    
    
    @app.route("/cart", methods=["GET", "POST"])
    def cart():
    
        # Ensure cart exists
        if "cart" not in session: #session: 딕셔너리에 불과함
            session["cart"] = []  #cart:키, [](리스트):값
    
        # POST
        if request.method == "POST":
            id = request.form.get("id")
            if id:
                session["cart"].append(id)
            return redirect("/cart")
    
        # GET
        books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"])  #session["cart"]:리스트 -> 리스트의 각 값을 콤마(,)로 나눠서 반환함(CS50 library 보면 나와있음)
        return render_template("cart.html", books=books)
    

    Notice that cart is implemented using a list. Items can be added to this list using the Add to Cart buttons in books.html. When clicking such a button, the post method is invoked, where the id of the item is appended to the cart. When viewing the cart, invoking the get method, SQL is executed to display a list of the books in the cart. +참고: session->각 세션은 해당 계정으로 로그인한 사람에게만 적용된다. 또한 각 세션이 언제 종료되는지는 개발자들이 정할 수 있다.

  • Let's see books.html

    	```html
    	{% extends "layout.html" %}
    
    	{% block body %}
    
    		<h1>Books</h1>
    		{% for book in books %}
    			<h2>{{ book["title"] }}</h2>
    			<form action="/cart" method="post">
    				<input name="id" type="hidden" value="{{ book['id'] }}">
    				<button type="submit">Add to cart</button>
    			</form>
    		{% endfor %}
    
    	{% endblock %}
    	```

API

  • An application program interface or API is a series of specifications that allow you to interface with another service. For example, we could utilize IMDB’s API to interface with their database. We might even integrate APIs for handling specific types of data downloadable from a server.

  • We looked at an example called shows.

  • Looking at app.py, we saw the following:

    # Searches for shows using Ajax
    
    from cs50 import SQL
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    db = SQL("sqlite:///shows.db")
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/search")
    def search():
        q = request.args.get("q")
        if q:
            shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%") #SQL에서 %는 와일드카드이다. ?는 단순한 placeholder으로, %?%와 같이 쓸 수 없다.
        else:
            shows = []
        return render_template("search.html", shows=shows)
    

    Notice that the search route executes a SQL query.

  • Looking at search.html, you’ll notice that it is very simple:

    {% for show in shows %}
        <li>{{ show["title"] }}</li>
    {% endfor %}

    Notice that it provides a bulleted list.

    • 참고: 위의 search.html코드에 {% extends "layout.html" %}와 같은 코드가 없다. 위 코드는 단지 html코드의 조각일 뿐이다. -> 아무데나 넣어서 써먹기 좋음. 밑의 index.html코드를 보고 어떻게 search.html을 써먹는지 보자.
  • Finally, looking at index.html, notice that AJAX code is utilized to power the search:

  • 참고: AJAX = Asynchronous, JavaScript, XML

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>shows</title>
        </head>
        <body>
    
            <input autocomplete="off" autofocus placeholder="Query" type="search">
    
            <ul></ul>
    
            <script>
    
                let input = document.querySelector('input');
                input.addEventListener('input', async function() {
                    let response = await fetch('/search?q=' + input.value);
                    let shows = await response.text();
                    document.querySelector('ul').innerHTML = shows;
                });
    
            </script>
    
        </body>
    </html>
    

    Notice an event listener is utilized to dynamically query the server to provide a list that matches the title provided. This will locate the ul tag in the HTML and modify the web page accordingly to include the list of the matches.

    • <script>태그: 자바스크립트와 같은 클라이언트 사이드 스크립트(client-side scripts)를 정의할 때 사용. 여기서는 자바스크립트 정의할때 사용함.
    • let input = document.querySelector('input');: 변수 input정의. document는 페이지 전체를 의미. querySelector('제어대상')에서 제어대상에 들어갈 선택자는 CSS 선택자가 그대로 적용됨. CSS 선택자는 3가지로 html 태그, id, class임. 여기에서 제어대상으로 사용한 것은 html태그인 input이다. 선택자로 id와 class를 사용하고 싶다면 각자 #과 .를 앞에 써야 한다. ex) document.querySelector('#demo')
    • input.addEventListener('input', async function(): input(이 경우 텍스트박스)에 어떤 변화가 생길때마다 async function()을 호출한다.
    • async function(): 참고 교수님은 백그라운드에서 돌아가는 일 정의하는 함수... 라고 설명했음 대충..
    • let response = await fetch('/search?q=' + input.value);: fetch는 '가져오다'라는 뜻이다. fetch('/search?q=' + input.value)/search?q=찾는정보페이지를 가져와라는 뜻이다.
    • let shows = await response.text();: response.text()가 실행될때까지 기다리자(await)라는 뜻
    • document.querySelector('ul').innerHTML = shows;: search the document, the whole web page, for 'ul' tag, and change its inner HTML(<ul></ul>사이) to be that fragment of <li> of all of those matching shows(위에서 만든 search.html말임)
  • You can read more in the AJAX documentation.

JSON

  • JavaScript Object Notation or JSON is text file of dictionaries with keys and values. This is a raw, computer-friendly way to get lots of data.

  • JSON is a very useful way of getting back data from the server.

  • You can see this in action in the index.html we examined together:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>shows</title>
        </head>
        <body>
    
            <input autocomplete="off" autofocus placeholder="Query" type="text">
    
            <ul></ul>
    
            <script>
    
                let input = document.querySelector('input');
                input.addEventListener('input', async function() {
                    let response = await fetch('/search?q=' + input.value);
                    let shows = await response.json();
                    let html = '';
                    for (let id in shows) {
                        let title = shows[id].title.replace('<', '&lt;').replace('&', '&amp;');
                        html += '<li>' + title + '</li>';
                    }
                    document.querySelector('ul').innerHTML = html;
                });
    
            </script>
    
        </body>
    </html>
    

    While the above may be somewhat cryptic, it provides a starting point for you to research JSON on your own to see how it can be implemented in your own web applications.

    • let html = '';: 'html' is a variable which initialize to nothing.
    • shows[id].title.replace('<','&lt;').replace('&', '&amp;': shows[id].title을 가져와서 위험한 특수문자(<, &)들을 안전한 문자로 바꿈.
    • html += '<li>' + title + '</li>';: html 변수에 하나의 리스트 조각 추가함.
  • You can read more in the JSON documentation.

Summing Up

In this lesson, you learned how to utilize Python, SQL, and Flask to create web applications. Specifically, we discussed…

  • GET
  • POST
  • Flask
  • Session
  • AJAX
  • JSON

See you next time for our final lecture!

0개의 댓글