2022年12月13日 星期二

在 Rocky Linux 8上搭建 Flask Web 產品環境

學習目標:
  • 在 Rocky Linux 8 平台上,使用 Nginx + Gunicorn 平台,搭建 Flask Web 產品環境。
  • 使用 Python 3.9 版本。
  • 預備知識:Rocky Linux 8 作業系統安裝與設定。

實作流程
  1. 在 Rocky Linux 8 上,設置 Python 3.9 版本:
    # alternatives --list
    # dnf module -y install python39
    # alternatives --list
    # alternatives --config python
    # alternatives --config python3
    There are 2 programs which provide 'python3'.
    
      Selection    Command
    -----------------------------------------------
    *  1           /usr/bin/python3.6
     + 2           /usr/bin/python3.9
    
    Enter to keep the current selection[+], or type selection number: 2
    
    # yum install -y python39-*
    
  2. 安裝 Nginx 服務 Web 套件,使用 v1.20 版本:
    # dnf module reset nginx:1.20
    # dnf module enable nginx:1.20
    # dnf module switch-to nginx:1.20
    # dnf module list nginx
    # yum install nginx-*
    
  3. 安裝 Flask 與 Gunicorn 套件:
    # pip3 install flask gunicorn
    
  4. 建立與設定 Flask 產品環境目錄:
    # mkdir -p /usr/share/nginx/webapp
    # semanage fcontext -a -t httpd_sys_content_t "/usr/share/nginx/webapp(/.*)?"
    # restorecon -Rvv /usr/share/nginx/webapp
    
  5. 試作兩個運行的檔案 application.py 與 wsgi.py:
    # vim /usr/share/nginx/webapp/application.py
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        return 'Hello World!'
    
    # vim /usr/share/nginx/webapp/wsgi.py
    from application import app
    
    if __name__ == '__main__':
        app.run(debug=False)
    
    # restorecon -Rvv /usr/share/nginx/webapp
    
  6. 測試 Flask 與 Gunicorn 是否可以正常運作:(使用溜覽器觀看結果)
    # cd /usr/share/nginx/webapp
    # flask run --host '0.0.0.0'
    (停止方式:Ctrl + C)
    # gunicorn --workers 4 --bind 0.0.0.0:5000 wsgi:app
    (停止方式:Ctrl + C)
    
  7. 自定系統服務,啟動 Gunicorn 產品運作環境:
    # vim /etc/systemd/system/webapp.service
    [Unit]
    Description=webapp.service - A Flask application run with Gunicorn.
    After=network.target
    
    [Service]
    User=nginx
    Group=nginx
    Environment="PATH=/usr/local/bin"
    WorkingDirectory=/usr/share/nginx/webapp/
    ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/run/gunicorn/webapp.sock wsgi:app
    
    [Install]
    WantedBy=multi-user.target
    
    # systemctl daemon-reload
    # mkdir -p /run/gunicorn/
    # chown nginx:nginx /run/gunicorn/
    # systemctl enable --now webapp.service
    # semanage fcontext -a -t httpd_var_run_t "/var/run/gunicorn/webapp.sock"
    # restorecon -Rvv /var/run/gunicorn/webapp.sock
    
  8. 設定 Nginx 服務,新增一個 Web 服務站台,對應 Gunicorn 服務:
    # vim /etc/nginx/conf.d/webapp.conf
    server {
            listen 80;
            server_name webapp.example.com;
    
            access_log /var/log/nginx/webapp_access.log;
            error_log /var/log/nginx/webapp_error.log;
    
            location / {
                    proxy_pass http://unix:/run/gunicorn/webapp.sock;
                    proxy_redirect off;
                    proxy_set_header Host $host:80;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            }
    }
    
    # nginx -t
    # systemctl enable --now nginx
    
  9. 設定防火牆,允許外來的連線:
    # firewall-cmd --add-service=http
    # firewall-cmd --add-service=https
    # firewall-cmd --runtime-to-permanent
    
參考文獻:
  1. https://dev.to/brandonwallace/deploy-flask-the-easy-way-with-gunicorn-and-nginx-jgc
  2. https://gunicorn.org/

2021年12月5日 星期日

Flask MVC 架構的應用 (二)

學習目標:
  • Flask MVC 架構的應用 -- 建立樣板目錄 templates
  • 專案目錄結構:
    flaskproject
    |- app
    |--- templates
    |------ index.html
    |--- __init__.py
    |--- router.py
    |main.py
    

Flask MVC 架構實作
  1. 將專案 flaskproject/app 目錄下,新增 templates 資料夾:
    mkdir flaskproject/app/templates
    
  2. 將專案 flaskproject/app/templates 目錄下,新增 index.html 檔案:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        This is my web site!!
    </body>
    </html>
    
  3. 修改 flaskproject/app/router.py 檔案:
    from flask import render_template
    from app import app
    
    @app.route("/")
    def index():
        #return 'Hello World'
        return render_template('index.html')
    
  4. 重新啟動 Flask :
    flask run
    
  5. 開啟瀏覽器,網址: http://127.0.0.1:5000

Flask MVC 架構的應用 (一)

學習目標:
  • Flask MVC 架構的應用 -- 獨立出 router 的部份
  • 專案目錄結構:
    flaskproject
    |- app
    |--- __init__.py
    |--- router.py
    |main.py
    

Flask MVC 架構實作
  1. 將專案 flaskproject 目錄下,新增 app 資料夾:
    mkdir flaskproject/app
    
  2. 將專案 flaskproject 目錄下,新增 main.py :
    from app import app
    
    if __name__ == "__main__":
        app.run(debug=False)
    
  3. 將專案 flaskproject 目錄下,將 app.py 移至 app 資料夾下並改名稱為 __init__.py :
    mv app.py app/__init__.py
    
  4. 修改在 app 資料夾下的 __init__.py 檔案 :
    from flask import Flask
    
    app = Flask(__name__)
    
    # 導入其他的程式模組
    from app import router
    
  5. 在 app 資料夾下,新增 router.py 檔案 :
    from app import app
    
    @app.route('/')
    def index():
        return 'Hello World'
    
  6. 在 Terminal 視窗下,執行 flask :
    set FLASK_APP=main.py
    flask run
    
  7. 開啟瀏覽器,網址: http://127.0.0.1:5000

Flask 齊步走

學習目標:
  • 安裝 Flask 模組套件!
  • 執行第一支 Flask 程式!

安裝與使用 Flask 模組
  1. 使用 pip 安裝 Flask 模組:
    pip install flask
    
  2. 建立專案資料夾,方便開發專案:
    mkdir flaskproject
    cd flaskproject
    
  3. 建立第一個 flask 專案的檔案 app.py:
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
      return 'Hello World'
    
  4. 啟動 Flask :
    flask run
    
  5. 開啟瀏覽器,網址: http://127.0.0.1:5000

2021年8月17日 星期二

使用 Flask Login 模組

學習目標:
  • 利用 Flask Login 模組,控制站台運作!

使用 Flask Login 模組
  1. 新增 app/templates/login.html 檔案:
    {% extends "base.html" %}
    {% block title %}大學麵店系統管理區{% endblock %}


    {% block main %}
    <div class="container">
    <div class="row">
    <div class="col">
    <h1>後台管理</h1>
    <form action="/logins" method="post">
    <label for="loginname">登入帳號</label>
    <input type="text" id="loginname" placeholder="請輸入帳號" name="loginname">
    <br>
    <br>
    <label for="passwrod">登入密碼</label>
    <input type="password" id="password" placeholder="請輸入密碼" name="password">
    <br>
    <input type="submit" value="送出">
    </form>
    </div>
    </div> {% endblock %}
  2. 修改 requirements.txt
    (前面略過....)
    Flask-Login>=0.4.1
    
  3. 修改 app/__init__.py
    (前面略過....)
    from flask_login import LoginManager
    (中間略過....)
    # 導入加密 key
    app.secret_key = config.get('Flask','SecretKey')
    
    lm = LoginManager()
    lm.init_app(app)
    lm.session_protection = "strong"
    lm.login_view = 'login'
    lm.login_message = '請輸入帳號密碼'
    
    # 導入其他的程式模組
    from app import router, linebotmodules, loginmodels
    
  4. 使用 terminal 介面,產生 secret_key 內容:
    C:\workspace\LineBot> python
    >>> import os
    >>> os.urandom(16).hex()
    (產生一堆編碼)
    
  5. 修改 config.ini 檔案內容:
    [Flask]
    SecretKey = 上個步驟產生的一堆編碼
    
  6. 新增 app/loginmodels.py 檔案內容:
    from flask_login import UserMixin
    from app import lm
    
    class User(UserMixin):
        pass
    
    @lm.user_loader
    def user_loader(users):
        if users not in users_dict:
            return
    
        user = User()
        user.id = users
        return user
    
    @lm.request_loader
    def request_loader(request):
        user = request.form.get('user_id')
        if user not in users_dict:
            return
        
        user = User()
        user.id = user
    
        user.is_authenticated = request.form['password'] == users_dict[user]['password']
    
        return user
    
    users_dict = {'Owner': {'password': 'HelloWorld'}}
    
  7. 修改路由檔案 app/router.py
    (前面略過....)
    from flask import request, render_template, url_for, redirect, flash
    from flask_login import login_user, logout_user, login_required, current_user
    from app.loginmodels import User, users_dict
    
    @app.route('/logins', methods=['GET','POST'])
    def login():
        if request.method == 'GET':
            return render_template("login.html")
    
        user = request.form['loginname']
        conditionA = user in users_dict
        conditionB = request.form['password'] == users_dict[user]['password']
    
        if conditionA and conditionB:
            user1 = User()
            user1.id = user
            login_user(user1)
            flash(f'{user}!Welcome home!')
            return redirect(url_for('home'))
    
        flash('login failed...')
        return render_template('login.html')
    
    @app.route('/logout')
    def logout():
        user = current_user.get_id()
        logout_user()
        flash(f'{user}!logout!')
        return render_template('login.html')
    
    @app.route('/show_records')
    @login_required
    def show_records():
        python_records =web_select_overall()
        return render_template('show_records.html', html_records=python_records)
    (後面略過....)
    
  8. 修改 app/templates/base.html
    {% if current_user.is_authenticated %}
    <li class="nav-item">
    <a class="nav-link" href="http://mylinebothellotux.herokuapp.com/showmenu">麵店菜單</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="http://mylinebothellotux.herokuapp.com/showads">活動訊息</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="http://mylinebothellotux.herokuapp.com/logout">登出</a>
    </li>
    {% else %}
    <li class="nav-item">
    <a class="nav-link" href="http://mylinebothellotux.herokuapp.com/logins">登入</a>
    </li>
    {% endif %}
  9. 將專案送上 Heroku 主機!

2021年8月15日 星期日

使用 Line Bot 圖文選單

學習目標:
  • 利用 Line Bot 圖文選單,進行互動!

使用 Line Developer 工具建立圖文選單
  1. 登入 Line Developer,選用 Line Official Account Manager :
  2. 在主頁選項,選擇「聊天室相關」的圖文選單:
  3. 閱讀注意事項:
  4. 建立圖文選單:
  5. 輸入需要的項目內容:
  6. 在內容設定中,選擇版型:
  7. 選擇需要的版型,按下「確定」!
  8. 按下「建立圖片」:
  9. 為每個對達框選擇圖片檔案:
    PS: 可按「Ctrl」+ 三個圖片框,同時上傳三個圖檔,進行設定!
  10. 指定「A」區塊類型為:文字,並輸入文字內容:
  11. 完成後,可以按下「儲存」,完成編輯!
  12. 打開 line 聊天室,可看到該圖文框!

試寫自己的訂購單
  1. 在 app/router.py 中,加入下列內容:
    @app.route('/orderMenu/<nodes>')
    def orders(nodes):
    return render_template("orderMenu.html",nodeslist=nodes)

    @app.route('/orderMenu',methods = ['GET','POST'])
    def addorders():
    if request.method == 'POST':
    print(request.form)
    data = ""
    for key,value in request.form.items():
    data += value
    data += ','
    data = data[:-1]
    connectDB.addOrders(data)
    return render_template("showOrders.html")
    else:
    return render_template("showOrders.html")

    # 顯示訂單列表
    @app.route("/showOrders")
    def showOrders():
    orderslists = connectDB.showOrders()
    return render_template('showOrders.html',menulist=orderslists)
  2. 在 app/templates/orderMenu.html
    {% extends "base.html" %}
    {% block title %}My Web Site{% endblock %}
    {% block main %}
    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>{{ nodeslist }}訂購單</h1>
    <form action="/orderMenu" method="post">
    <label for="nos">{{ nodeslist }}數量</label>
    <input type="text" id="nos" placeholder="請輸入數量" name="nos">
    <br>
    <label for="cusphone">手機號碼</label>
    <input type="text" id="cusphone" placeholder="請輸入手機號碼" name="cusphone">
    <br>
    <input type="hidden" name="nodes" value="{{ nodeslist }}">
    <input type="submit" value="送出">
    </form>
    </div>
    </div>

    </div>
    {% endblock %}
  3. 新增訂單資料表:
    C:\workspace\LineBot> heroku login -i
    C:\workspace\LineBot> python
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> print(DATABASE_URL)
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> SQL_cmd = '''CREATE TABLE ordermenu(
    ... order_id serial PRIMARY KEY,
    ... nodes VARCHAR (50) NOT NULL,
    ... nos INT NOT NULL,
    ... cusphone VARCHAR (20) NOT NULL
    ... );'''
    >>> cursor.execute(SQL_cmd)
    >>> connection_db.commit()
    >>> cursor.close()
    >>> connection_db.close()
    >>>
    
  4. 新增訂單列表:
    {% extends "base.html" %}
    {% block title %}大學麵店訂單系統{% endblock %}

    {% block main %}

    <!-- 宣告巨集 -->
    {% macro show_row(data, tag) -%}
    {% for menu in data %}
    <div class="row">
    {% if menu[4] == 'Action' %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    {% else %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    <div class="col">
    <button type="submit" value="{{ menu[0],menu[1] }}" name="deldata">刪除</button>
    </div>
    {% endif %}
    </div>
    {% endfor %}
    {%- endmacro %}
    <!-- 宣告結束 -->

    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>訂單列表</h1>
    </div>
    </div>
    <!-- 顯示菜單用 -->
    {% set cols = (("項次","菜單","數量","手機號碼","Action"),)%}

    {{ show_row(cols, "h2") }}
    <hr class="my-4">
    <form action="/delads" method="post">
    {{ show_row(menulist,"p") }}
    </form>
    <form action="/addOrders" method="get">
    <input type="submit" value="新增訂單">
    </form>
    </div>

    {% endblock %}
  5. 修改 app/dataSQL/connectDB:
    (前面略過....)
    def addOrders(text):
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        data = text.split(',')
        table_columns = '(nos,cusphone,nodes)'
        SQL_cmd = f"""INSERT INTO ordermenu { table_columns } VALUES(%s,%s,%s);"""
        cursor.execute(SQL_cmd,(int(data[0]),str(data[1]),str(data[2])))
        connection_db.commit()
        cursor.close()
        connection_db.close()
        return text
    
    def showOrders():
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        SQL_cmd = f"""SELECT * FROM ordermenu ORDER by order_id DESC limit 10;"""
        cursor.execute(SQL_cmd)
        connection_db.commit()
        raws = cursor.fetchall()
        data = []
        for raw in raws:
            data.append(raw)
        
        cursor.close()
        connection_db.close()
        return data
    
  6. 將資料表送上 Heroku 主機,進行測試!

2021年8月10日 星期二

推送廣告活動訊息

學習目標:
  • 利用己學過的知識,進行商業化活動!

建立廣告活動訊息資料庫
  1. 增加一個資料表 ads 存放訊息:
    C:\workspace\LineBot> heroku login -i
    C:\workspace\LineBot> python
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> print(DATABASE_URL)
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> SQL_cmd = '''CREATE TABLE ads(
    ... ads_id serial PRIMARY KEY,
    ... adstitle VARCHAR (150) NOT NULL,
    ... adscontent TEXT NOT NULL,
    ... adspicname VARCHAR (150) NOT NULL
    ... );'''
    >>> cursor.execute(SQL_cmd)
    >>> connection_db.commit()
    >>> cursor.close()
    >>> connection_db.close()
    >>>
    
  2. 修改 app/dataSQL/connectDB.py,寫入一段顯示活動資料的SQL語法:
    def showAds():
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        SQL_cmd = f"""SELECT * FROM ads ORDER by ads_id DESC limit 5;"""
        cursor.execute(SQL_cmd)
        connection_db.commit()
        raws = cursor.fetchall()
        data = []
        for raw in raws:
            data.append(raw)
        
        cursor.close()
        connection_db.close()
        return data  
     
  3. 修改 app/router.py 檔案,加入一段引導到活動列表的程式:
    # 顯示活動清單列表
    @app.route("/showads")
    def showads():
        adslists = connectDB.showAds()
        return render_template('showads.html',menulist=adslists)
    
  4. 增加一個顯示活動清單的 html 程式碼 app/templates/showads.html:
    {% extends "base.html" %}
    {% block title %}大學麵店活動通知系統{% endblock %}

    {% block main %}

    <!-- 宣告巨集 -->
    {% macro show_row(data, tag) -%}
    {% for menu in data %}
    <div class="row">
    {% if menu[4] == 'Action' %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    {% else %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    <div class="col">
    <button type="submit" value="{{ menu[0],menu[1] }}" name="deldata">刪除</button>
    </div>
    {% endif %}
    </div>
    {% endfor %}
    {%- endmacro %}
    <!-- 宣告結束 -->

    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>活動列表</h1>
    </div>
    </div>
    <!-- 顯示菜單用 -->
    {% set cols = (("項次","活動標題","活動內容","參考圖檔","Action"),)%}

    {{ show_row(cols, "h2") }}
    <hr class="my-4">
    <form action="/delmenu" method="post">
    {{ show_row(menulist,"p") }}
    </form>
    <form action="/addmenu" method="get">
    <input type="submit" value="新增活動">
    </form>
    </div>

    {% endblock %}
  5. 推送上 Heroku 專案!檢視一下成果!
  6. 修改 app/templates/showads.html 檔案內容,確定新增/刪改的路徑:
    {{ show_row(cols, "h2") }}
    <hr class="my-4">
    <form action="/delads" method="post">
    {{ show_row(menulist,"p") }}
    </form>
    <form action="/addads" method="get">
    <input type="submit" value="新增活動">
    </form>
  7. 修改 app/router.py 檔案內容,加上新增與刪除的程式碼:
    # 新增活動清單
    @app.route("/addads",methods = ['GET','POST'])
    def addads():
        if request.method == 'POST':
            return showads()
        else:
            return showads()
    # 刪除活動清單
    @app.route("/delads",methods = ['GET','POST'])
    def delads():
        if request.method == 'POST':
            print(request.form)
            data = ""
            data1 = ""
            for key, value in request.form.items():
                data = eval(''.join(value))
                data1 = str(data[0])+','+str(data[1])
                print(data1)
                connectDB.deleteAds(data1)
            return showads()
        else:
            return render_template("showads.html")
    
  8. 修改 app/dataSQL/connectDB.py 程式,加入下列程式碼:
    (前面略過....)
    def deleteAds(text):
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        data = text.split(',')
        SQL_cmd = f"""DELETE FROM ads WHERE ads_id = %s;"""
        print(int(data[0]))
        cursor.execute(SQL_cmd,[int(data[0])])
        connection_db.commit()
        cursor.close()
        connection_db.close()
        return text
    
  9. 推送上 Heroku 專案!檢視一下成果!
  10. 增加一個填寫資料內容的檔案 app/templates/addads.html:
    {% extends "base.html" %}
    {% block title %}大學麵店活動訊息系統{% endblock %}
    {% block main %}
    <div class="container">
    <div class="row">
    <div class="col">
    <h1>新增活動訊息</h1>
    <form action="/addads" method="post">
    <label for="adstitle">訊息標題</label>
    <input type="text" id="adstitle" placeholder="請輸入訊息標題" name="adstitle">
    <br>
    <br>
    <label for="adscontent">訊息內容</label>
    <input type="text" id="adscontent" placeholder="請輸入訊息內容" name="adscontent">
    <br>
    <br>
    <label for="adspicname">圖片名稱</label>
    <input type="text" id="adspicname" placeholder="請輸入圖片名稱" name="adspicname">
    <br>
    <input type="submit" value="送出">
    </form>
    </div>
    </div>
    </div>
    {% endblock %}
  11. 修改檔案 app/timer.py:
    (前面略過....)
    @scheduleEvent.scheduled_job('interval',minutes=1)
    def timed_job():
        msg = connectDB.showAds()
        print(msg)
        line_bot_api.push_message("你的ID",TextSendMessage(text='test'))
    
  12. 修改檔案 app/dataSQL/connectDB.py:
    (前面略過....)
    def addads(text):
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        data = text.split(',')
        table_columns = '(adsname,adscontent,adspicname)'
        SQL_cmd = f"""INSERT INTO menu { table_columns } VALUES(%s,%s,%s);"""
        cursor.execute(SQL_cmd,(str(data[0]),str(data[1]),str(data[2])))
        connection_db.commit()
        cursor.close()
        connection_db.close()
        return text
    
  13. 修改 add/router.py 檔案內容:
    (前面略過....)
    # 新增活動清單
    @app.route("/addads",methods = ['GET','POST'])
    def addads():
        if request.method == 'POST':
            print(request.form)
            data = ""
            for key,value in request.form.items():
                data += value
                data += ','
            data = data[:-1]
            connectDB.addads(data)
            return showads()
        else:
            return render_template("addads.html")
    (後面略過....)
    
  14. 推送上 Heroku 專案!