用Flask寫一個手動儲存觀看紀錄的網頁的開發日誌

方便自己整理所有的觀看紀錄,無論是動漫、電影、還是劇

有什麼用?

  • 純粹我自己會在各種網站上看電影、動漫等等的,但有些網頁會記錄,下次登入紀錄又沒了。又或者是例如有些漫畫網站,手機和電腦瀏覽的紀錄又不同,久了沒看,回來要再看的時候就又忘了看到哪,翻半天。總之呢,做這個project的目的,就是解決這個我一直想開發的工具,順便玩玩網頁。
  • 網路上找不到相似的工具,可能有很多替代方案,例如純手寫就是一個🤣

目標

  1. 寫出簡單表格,並且可以新增紀錄,包含作品類型、作品名稱、集(季)數、目前的觀看進度
  2. 多用戶,使用帳號密碼管理

語言與工具的選擇

  • 我第一個就想到Flask,因為他是快速開發網頁的好選擇,之前有稍微玩一下
  • 數據庫方面使用sqlite,最輕量的就可以滿足我的需求的

GPT老師問一問GG了,ajax+flask+jinja,讓我整個卡在update功能,眼見不會的太多了,對於全部都不熟悉,我還是決定打掉重來比較快,於是參考了這個影片做一遍,對於flask和jinja更熟悉了,就來試試看是否能改造為我自己的網頁

Code都是參考那部影片裡去做修改的,在另一篇文章裡有詳細流程

資料庫建置

大致結構為:

  • title: 節目標題
  • season: 季、卷 etc.
  • value: season的值
  • episode: 集數
  • progress: 進度
  • date_created: 以便照建立順序排序

至於資料庫的創建請見: 把資料庫造出來

# Config. database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///watching-history.db"
# Init. database
db = SQLAlchemy(app)


# Create a model for database
class WatchingHistory(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    season = db.Column(db.String(10), nullable=False)
    value = db.Column(db.Integer, nullable=False)
    episode = db.Column(db.Integer, nullable=False)
    progress = db.Column(db.String(10), nullable=False)
    date_created = db.Column(db.DateTime, default=datetime.utcnow)

    # Return a string when create a new element
    def __repr__(self):
        return "<Watching History %r>" % self.id

__repr__是一個自我描述的方法,repr(物件)就會回傳__repr__function裡的東西

請參見: repr與str雜談———暴風雨前的輕鬆小品技術文

好奇心驅使下寫了一個簡短的python來印出sqlite的內容

import sqlite3


sqliteConnection = sqlite3.connect('instance/watching-history.db')

sql_query = """SELECT * FROM watching_history;"""

cursor = sqliteConnection.cursor()

cursor.execute(sql_query)

print(cursor.fetchall())

不懂id是怎麼被創建的,可能是primary_key預設的行為,研究了一下,是id裡的autoincrement argument默認為true,也就是等於 id = db.Column(db.Integer, primary_key=True, autoincrement=True)

請見第一個回答: unable to create autoincrementing primary key with flask-sqlalchemy

Create開發

# Route to display the watching history
@app.route("/", methods=["POST", "GET"])
def index():
    if request.method == "POST":
        # 收到資料
        history_title = request.form["title"]
        history_season = request.form["season"]
        history_value = request.form["value"]
        history_episode = request.form["episode"]
        history_progress = request.form["progress"]

        # 資料轉換為WatchingHistory class
        new_history = WatchingHistory(
            title=history_title,
            season=history_season,
            value=history_value,
            episode=history_episode,
            progress=history_progress,
        )

        try:
            # 資料加入
            db.session.add(new_history)
            db.session.commit()
            return redirect("/")
        except:
            return "There was an issue adding your task"
    else:
        # 獲取所有資料
        history = WatchingHistory.query.order_by(WatchingHistory.date_created).all()
        # 傳資料到index.html
        return render_template("index.html", history=history)

Delete開發

基本上可以照搬文章中的code,改一下變數名稱就好

@app.route('/delete/<int:id>')
def delete(id):
    # 從資料庫取得該task
    history_to_delete = WatchingHistory.query.get_or_404(id)

    try:
        # 刪除該task
        db.session.delete(history_to_delete)
        db.session.commit()
        return redirect('/')
    except:
        return 'There was an issue deleting the history'

這裡附上完整實作了createdelete功能的html,以免遺漏

{% extends 'base.html' %}

{% block head %}
<title>Watching History</title>
{% endblock %}

{% block body %}
    <h1 style="text-align: center;">Watching History</h1>
    <table border="1" id="watching_history">
        <tr>
            <th>Title</th>
            <th>Season</th>
            <th>Episode</th>
            <th>Progress</th>
        </tr>
        {% for entry in history %}
        <tr>
            <td>{{ entry.title }}</td>
            <td>{{ entry.value }}{{ entry.season}}</td>
            <td>{{ entry.episode}}</td>
            <td>{{ entry.progress }}</td>
            <td>
                <a href="/delete/{{entry.id}}">Delete</a>
                <br>
                <a href="">Update</a>
            </td>
        </tr>
        {% endfor %}
    </table>

    <h2>Add New Entry</h2>
    <form action="/" method="POST">
        <label for="title">Title:</label>
        <input type="text" name="title" required><br>

        <label for="season">Season:</label>
        <select name="season">
            <option value=""></option>
            <option value=""></option>
            <option value=""></option>
        </select><br>

        <label for="value">Value:</label>
        <input type="number" name="value" required><br>

        <label for="episode">Episode:</label>
        <input type="number" name="episode" required><br>

        <label for="progress">Progress:</label>
        <input type="text" name="progress" required><br>

        <input type="submit" value="新增">
    </form>

{% endblock %}

Update開發

是稍微複雜一點點,但概念都是相通的,需要修改或是新增以下檔案:

  • app.py
  • update.html
  • index.html

首先app.py,加上update function

@app.route('/update/<int:id>', methods=['GET', 'POST'])    
def update(id):
    # 從資料庫取得該history
    history = WatchingHistory.query.get_or_404(id)

    if request.method == 'POST':
        # 取得用戶輸入的資料
        history.title = request.form['title']
        history.season = request.form['season']
        history.value = request.form['value']
        history.episode = request.form['episode']
        history.progress = request.form['progress']
        
        try:
            # 更新資料庫
            db.session.commit()
            return redirect('/')
        except:
            return 'There was an issue updating your history'
    else:
        return render_template('update.html', history=history)

接著是新增給用戶update的頁面-update.html

{% extends 'base.html' %}

{% block head %}
<title>Watching History</title>
{% endblock %}

{% block body %}
    <h1 style="text-align: center;">Updating History</h1>

    <div class="form">
        <form action="/update/{{history.id}}" method="POST">
            <label for="title">Title:</label>
            <input type="text" name="title" value="{{history.title}}" required><br>

            <label for="season">Season:</label>
            <select name="season">
                {% for season_option in ['季', '卷', '無'] %}
                    {% if history.season == season_option %}
                        <option value="{{ season_option }}" selected>{{ season_option }}</option>
                    {% else %}
                        <option value="{{ season_option }}">{{ season_option }}</option>
                    {% endif %}
                {% endfor %}
            </select><br>

            <label for="value">Value:</label>
            <input type="number" name="value" value="{{history.value}}" required><br>

            <label for="episode">Episode:</label>
            <input type="number" name="episode" value="{{history.episode}}" required><br>

            <label for="progress">Progress:</label>
            <input type="text" name="progress" value="{{history.progress}}" required><br>

            <input type="submit" value="更新">
        </form>
    </div>
{% endblock %}

比較特別的是:

  • action要記得附上history.id
  • 各個欄位的值要用jinja2附上,這樣用戶才會看得到資料,方便修改
  • season的部分為了讓網頁默認選擇該historyseason,所以用Jinja2判斷哪個該是selected,也就是默認被選擇的

最後是index.html

{% extends 'base.html' %}

{% block head %}
<title>Watching History</title>
{% endblock %}

{% block body %}
    <h1 style="text-align: center;">Watching History</h1>
    <table border="1" id="watching_history">
        <tr>
            <th>Title</th>
            <th>Season</th>
            <th>Episode</th>
            <th>Progress</th>
        </tr>
        {% for entry in history %}
        <tr>
            <td>{{ entry.title }}</td>
            {% if entry.season != "無" %}
                <td>{{ entry.value }}{{ entry.season}}</td>
            {% else %}
                <td></td>
            {% endif %}
            <td>{{ entry.episode}}</td>
            <td>{{ entry.progress }}</td>
            <td>
                <a href="/delete/{{entry.id}}">Delete</a>
                <br>
                <a href="/update/{{entry.id}}">Update</a>
            </td>
        </tr>
        {% endfor %}
    </table>

    <h2>Add New Entry</h2>
    <form action="/" method="POST">
        <label for="title">Title:</label>
        <input type="text" name="title" required><br>

        <label for="season">Season:</label>
        <select name="season" id="seasonSelect">
            <option value=""></option>
            <option value=""></option>
            <option value=""></option>
        </select><br>

        <label for="value" id="valueLabel">Value:</label>
        <input type="number" name="value" id="valueInput" value="0" required><br>

        <label for="episode">Episode:</label>
        <input type="number" name="episode" id="episodeInput" required><br>

        <label for="progress">Progress:</label>
        <input type="text" name="progress" required><br>

        <input type="submit" value="新增">
    </form>
{% endblock %}

當資料的season為的時候只顯示

額外修改

核心功能至此已開發完畢,接下來就是外觀或是使用上面的調整了

選擇”無”時隱藏input

season選擇的時候就是代表用戶要記錄的無須或是等等的,只純粹紀錄集數,所以隱藏labelinput,把相關的labelinput都用附上id以便JS操作

<script>
    const seasonSelect = document.getElementById('seasonSelect');
    const valueLabel = document.getElementById('valueLabel');
    const valueInput = document.getElementById('valueInput');
    const episodeInput = document.getElementById('episodeInput');

    seasonSelect.addEventListener('change', function() {
        if (seasonSelect.value === '無') {
            valueLabel.style.visibility = 'hidden'; // Hide the label
            valueInput.style.visibility = 'hidden'; // Hide the input
        } else {
            valueLabel.style.visibility = 'visible'; // Show the label
            valueInput.style.visibility = 'visible'; // Show the input
        }
    });

    // Trigger the change event initially in case there's a default value selected
    seasonSelect.dispatchEvent(new Event('change'));
</script>

成果預覽

幾乎沒有調整CSS

Index preview
Update preview

body, html {
    margin: 0;
    font-family: sans-serif;
    background-color: rgb(212, 244, 255);
}

.content {
    margin: 0 auto;
    width: 400px;
}

table, td, th {
    border: 1px solid #aaa;
}

table {
    border-collapse: collapse;
    width: 100%;
}

th {
    height: 30px;
}

td {
    text-align: center;
    padding: 5px;
}

.form {
    margin-top: 20px;
}

#content {
    width: 70%;
}

用Flask寫一個手動儲存觀看紀錄的網頁的開發日誌
https://f88083.github.io/2023/12/03/用Flask寫一個手動儲存觀看紀錄的網頁的開發日誌/
作者
Simon Lai
發布於
2023年12月3日
許可協議