# 第六週: More about Flask ###### tags: `Flask` ### Part 1: 透過 Flask 套件, 建立python 網頁伺服器 練習1: 步驟1: 創建新的專案資料夾, flask_blog 步驟2: 在該專案資料夾中, 新建 app.py 檔案. app.py ```python= from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello, World!' if __name__ == '__main__' : app.run() ``` 練習2: 建立 route with decorator 步驟1: 修改 app.py, 並執行應用程式 ```python= @app.route('/home') def home(): return '<h1>Home Page</h1>' @app.route('/about') def about(): return '<h1>About Page</h1>' ``` ### Part 2: 新增 templates 資料夾, 並新增 html 檔案: home.html 和 about.html 步驟1: 修改 app.py 呼叫 templates 資料夾中的 html 檔案 app.py ```python= from flask import Flask from flask import render_template app = Flask(__name__) @app.route('/') @app.route('/home') def home(): return render_template("home.html") @app.route('/about') def about(): return render_template("about.html") if __name__ == '__main__' : app.run() ``` 步驟2: 在專案資料夾中, 新增 templates 資料夾 步驟3: 在 templates 資料夾中, 新增 home.html 檔案 home.html ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Flask Blog</title> </head> <body> <h1>Home Page</h1> </body> </html> ``` 步驟4: 在 templates 資料夾中, 新增 about.html 檔案 about.html ```htmlmixed= <!DOCTYPE html> <html> <head> <title></title> </head> <body> <h1>About Page</h1> </body> </html> ``` ### Part 3: 執行應用程式並將資料傳送到網頁頁面 練習1: 新增測試資料( dummy data ) 步驟1: 在 app.py 的主程式中, 建立測試資料 ```python= posts = [ { 'author' : 'Alice Chen', 'title' : 'Blog Post 1', 'content' : 'First post content', 'date_posted': 'October 10, 2021', }, { 'author' : 'Bob Lin', 'title' : 'Blog Post 2', 'content' : 'Second post content', 'date_posted': 'October 12, 2021', } ] .... .... .... @app.route('/home') def home(): return render_template("home.html", posts=posts) ``` 步驟2: 修改 home.html 檔案, 取出主程式傳過來的資料(via posts) ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Flask Blog</title> </head> <body> {% for post in posts %} <h1> {{ post.title}} </h1> <p> By {{ post.author}} on {{ post.date_posted }}</p> <p> {{ post.content}}</p> {% endfor %} </body> </html> ``` 練習2 了解主程式和網頁間,資訊的傳遞. 步驟1: 修改 for both home.html 和 about.html 檔案 ```htmlmixed= <head> {% if title %} <title>Flask Blog - {{ title }}</title> {% else %} <title>Flask Blog</title> {% endif %} </head> ``` 步驟2: 修改 app.py ```python= @app.route('/about') def about(): return render_template("about.html", title='About') ``` 步驟3: 執行程式並觀察 title tab 的執行結果. 練習3 模組化 HTML 頁面 步驟1: 新增 layout.html, 並取出 home.html 和 about.html 共同的程式碼 ```htmlmixed= <!DOCTYPE html> <html> <head> {% if title %} <title>Flask Blog - {{ title }}</title> {% else %} <title>Flask Blog</title> {% endif %} </head> <body> {% block content %} {% endblock %} </body> </html> ``` 步驟2: 修改 home.html ```htmlmixed= {% extends "layout.html" %} {% block content %} {% for post in posts %} <h1> {{ post.title}} </h1> <p> By {{ post.author}} on {{ post.date_posted }}</p> <p> {{ post.content}}</p> {% endfor %} {% endblock content %} ``` 步驟3: 修改 about.html ```htmlmixed= {% extends "layout.html" %} {% block content %} <h1> About Page </h1> {% endblock content %} ``` 步驟4: 執行程式並觀察網頁原始碼(source)的執行結果 ### Part 4: 套入 Bootstrap 練習1 步驟1: 修改 layout.html ```htmlmixed= <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> ...... </head> <body> <div class="container"> {% block content %} {% endblock %} </div> <!-- Optional JavaScript; choose one of the two! --> <!-- Option 1: Bootstrap Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> </body> ``` 步驟2: 套入 Navigator Bar (1) 修改 layout.html 檔案, 套入 navigation ```htmlmixed= <header class="site-header"> <nav class="navbar navbar-expand-md navbar-dark bg-steel fixed-top"> <div class="container"> <a class="navbar-brand mr-4" href="/">Flask Blog</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggle" aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarToggle"> <div class="navbar-nav mr-auto"> <a class="nav-item nav-link" href="/">Home</a> <a class="nav-item nav-link" href="/about">About</a> </div> <!-- Navbar Right Side --> <div class="navbar-nav"> <a class="nav-item nav-link" href="/login">Login</a> <a class="nav-item nav-link" href="/register">Register</a> </div> </div> </div> </nav> </header> ``` 步驟2: 套入主選單 main ```htmlmixed= <main role="main" class="container"> <div class="row"> <div class="col-md-8"> {% block content %}{% endblock %} </div> <div class="col-md-4"> <div class="content-section"> <h3>Our Sidebar</h3> <p class='text-muted'>You can put any information here you'd like. <ul class="list-group"> <li class="list-group-item list-group-item-light">Latest Posts</li> <li class="list-group-item list-group-item-light">Announcements</li> <li class="list-group-item list-group-item-light">Calendars</li> <li class="list-group-item list-group-item-light">etc</li> </ul> </p> </div> </div> </div> </main> ``` 步驟3: 新增 static 資料夾, 並在該資料夾新增 main.css 檔案 ```htmlmixed= body { background: #fafafa; color: #333333; margin-top: 5rem; } h1, h2, h3, h4, h5, h6 { color: #444444; } .bg-steel { background-color: #5f788a; } .site-header .navbar-nav .nav-link { color: #cbd5db; } .site-header .navbar-nav .nav-link:hover { color: #ffffff; } .site-header .navbar-nav .nav-link.active { font-weight: 500; } .content-section { background: #ffffff; padding: 10px 20px; border: 1px solid #dddddd; border-radius: 3px; margin-bottom: 20px; } .article-title { color: #444444; } a.article-title:hover { color: #428bca; text-decoration: none; } .article-content { white-space: pre-line; } .article-img { height: 65px; width: 65px; margin-right: 16px; } .article-metadata { padding-bottom: 1px; margin-bottom: 4px; border-bottom: 1px solid #e3e3e3 } .article-metadata a:hover { color: #333; text-decoration: none; } .article-svg { width: 25px; height: 25px; vertical-align: middle; } .account-img { height: 125px; width: 125px; margin-right: 20px; margin-bottom: 16px; } .account-heading { font-size: 2.5rem; } ``` 步驟4: 修改 home.html, 套入 boostrap ```htmlmixed= <article class="media content-section"> <div class="media-body"> <div class="article-metadata"> <a class="mr-2" href="#">{{ post.author }}</a> <small class="text-muted">{{ post.date_posted }}</small> </div> <h2><a class="article-title" href="#">{{ post.title }}</a></h2> <p class="article-content">{{ post.content }}</p> </div> </article> ``` 步驟5: 套入 url_for 套件, 修改 app.py ```python= from flask import Flask, render_template, url_for ``` 步驟6: 修改 layout.html 檔案 ```htmlmixed= <!-- Bootstrap CSS --> ...... <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}"> ``` 步驟7: 執行程式並觀察網頁執行結果 ### Part 5: 表單處理及使用者輸入資料驗證 練習1: 主要概念 步驟1: 安裝 flask-wtf 套件 ```shell= pip install flask-wtf ``` 步驟2: 在專案資料夾中新增一個檔案, forms.py 步驟3: 表單及其驗證的基本操作方式 ```python= from flask_wtf import FlaskForm from wtforms import StringField from wtforms.validators import DataRequired, Length class RegistrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) ``` 練習2: 完成 registration form 以及 login form 步驟1: registration form. ```python= from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, Length, Email, EqualTo class RegistrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Sign Up') ``` 步驟2: login form ```python= from wtforms import StringField, PasswordField, SubmitField, BooleanField class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) remember = BooleandField('Remember Me') submit = SubmitField('Login') ``` 步驟3: 產生 secret key (1) ```python= app.config['SECRET_KDY'] = '' ``` (2) ```shell= $ python >>> import secrets >>> secrets.token_hex(16) ............... >>> exit() ``` (3) 將上述(2)所產生的亂數, 填入程式碼的 SECRET_KEY 中. ```python= app.config['SECRET_KDY'] = '<secrets.token_hex>' ``` 步驟4: 修改 app.py, 並設定路由(route)到填寫表格的頁面. (1) 修改 app.py, ```python= from forms import RegistrationForm, LoginForm @app.route('/register') def register(): form = RegistrationForm() return render_template("register.html", title='Register', form=form) @app.route('/login') def register(): form = LoginForm() return render_template("login.html", title='Login', form=form) ``` (2) 在 templates 資料夾中, 新建 register.html 及 login.html 檔案. register.html ```htmlmixed= {% extends "layout.html" %} {% block content %} {% for post in posts %} <div class="content-section"> <form method="POST" action=""> {{ form.hidden_tag() }} <fieldset class="form-group"> <legend class="border-bottom mb-4">Join Today</legend> <div class="form-group"> {{ form.username.label(class="form=control-label")}} {{ form.username(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.email.label(class="form=control-label")}} {{ form.email(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.password.label(class="form=control-label")}} {{ form.password(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.confirm_password.label(class="form=control-label")}} {{ form.confirm_password(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.submit(class="btn btn-outline-info") }} </div> </fieldset> </form> </div> <div class="border-top pt-3"> <small class="text-muted"> Already Hav An Account? <a class="ml-2" href="{{ url_for('login')}}">Sign In</a> </small> </div> {% endfor %} {% endblock content %} ``` login.html ```htmlmixed= {% extends "layout.html" %} {% block content %} {% for post in posts %} <h1> About Page </h1> {% endfor %} {% endblock content %} ``` (3) 修改 app.py ```python= @app.route('/register', method=['GET', 'POST']) def register(): form = RegistrationForm() return render_template("register.html", title='Register', form=form) ``` 練習3: 驗證使用者輸入的資料, 並用 flash message 告知/警告使用者 步驟1: 修改 app.py ```python= from flask import Flask, render_template, url_for, flash, redirect @app.route('/register') def register(): form = RegistrationForm() if form.validate_on_submit(): flash(f'Account created for {form.username.data}!','success') return redirect(url_for('home')) return render_template("register.html", title='Register', form=form) ``` 步驟2: 修改 layout.html ```htmlmixed= <main role="main" class="container"> <div class="row"> <div class="col-md-8"> {% with message = get_flashed_messages(with_categories=true) %} {% if messages%} {% for category, message in messages %} <div class="alert alert-{{ category }}" {{ message }} </div> {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %} </div> ..... ..... </div> </main> ``` 步驟3: 修改register.html網頁處理使用者輸入錯誤 ```htmlmixed= <div class="form-group"> {{ form.username.label(class="form=control-label")}} {% if form.username.errors %} {{ form.username(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.username.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.username(class="form-control form-control-lg")}} {% endif %} </div> <div class="form-group"> {{ form.email.label(class="form=control-label")}} {% if form.email.errors %} {{ form.email(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.email.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.email(class="form-control form-control-lg")}} {% endif %} </div> <div class="form-group"> {{ form.password.label(class="form=control-label")}} {% if form.password.errors %} {{ form.password(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.password.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.password(class="form-control form-control-lg")}} {% endif %} </div> <div class="form-group"> {{ form.confirm_password.label(class="form=control-label")}} {% if form.confirm_password.errors %} {{ form.confirm_password(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.confirm_password.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.confirm_password(class="form-control form-control-lg")}} {% endif %} </div> ``` 步驟4: 修改 login.html 網頁處理使用者輸入錯誤 ```htmlmixed= {% extends "layout.html" %} {% block content %} {% for post in posts %} <div class="content-section"> <form method="POST" action=""> {{ form.hidden_tag() }} <fieldset class="form-group"> <legend class="border-bottom mb-4">Log In</legend> <div class="form-group"> {{ form.email.label(class="form=control-label")}} {% if form.email.errors %} {{ form.email(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.email.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.email(class="form-control form-control-lg")}} {% endif %} </div> <div class="form-group"> {{ form.password.label(class="form=control-label")}} {% if form.password.errors %} {{ form.password(class="form-control form-control-lg is-invalid")}} <div class="invalid=feedback"> {% for error in form.password.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.password(class="form-control form-control-lg")}} {% endif %} </div> <div class="form-check"> {{ form.remember(class="form-check-input") }} {{ form.remember.label(class="form-check-label") }} </div> </fieldset> <div class="form-group"> {{ form.submit(class="btn btn-outline-info") }} </div> <small class="text-muted ml-2"> <a href="#">Forgot Password? </a> </small> </form> </div> <div class="border-top pt-3"> <small class="text-muted"> Need An Account? <a class="ml-2" href="{{ url_for('register')}}">Sign Up Now</a> </small> </div> {% endfor %} {% endblock content %} ``` (4) 測試 login page (使用 dummy fake data) ```python= @app.route('/login', method=['GET', 'POST']) def register(): form = LoginForm() if form.validate_on_submit(): if form.email.data == 'admin@blog.com' and form.email.password == 'password': flash('You have been logged in!', 'success') return redirect(url_for('home')) else: flash('Login Unsuccessful. Please check username and password', 'danger') return render_template("login.html", title='Login', form=form) ``` 步驟5 修改 layout.html, 讓應用程式的路由(routing)統一由主程式處理. ```htmlmixed= <div class="collapse navbar-collapse" id="navbarToggle"> <div class="navbar-nav mr-auto"> <a class="nav-item nav-link" href="/">"{{ url_for('home')}}"</a> <a class="nav-item nav-link" href="/about">="/">"{{ url_for('about')}}"</a> </div> <!-- Navbar Right Side --> <div class="navbar-nav"> <a class="nav-item nav-link" href="/login">"{{ url_for('login')}}"</a> <a class="nav-item nav-link" href="/register">"{{ url_for('register')}}"</a> </div> </div> ``` ================================================ ```python= from flask import render_template @app.route('/register') def register(): return render_template("register.html") ``` 並在 template 資料夾中新增 register.html ```htmlmixed= <!DOCTYPE html> <html> <body> <h2>Text input fields</h2> <form> <label for="fname">First name:</label><br> <input type="text" id="fname" name="fname"><br> <label for="lname">Last name:</label><br> <input type="text" id="lname" name="lname"> <input type="submit" value="submit"> </form> <p>Note that the form itself is not visible.</p> <p>Also note that the default width of text input fields is 20 characters.</p> </body> </html> ``` 嘗試從 url 傳入 value ```python= @app.route('/hello/<username>') def hello_user(username): return 'Hello, ' + username ``` 參考資料: 1. https://www.w3schools.com/html/html_forms.asp 2. Bootstrap: https://getbootstrap.com/docs/5.1/getting-started/introduction/#starter-template 3.