用Flask寫一個手動儲存觀看紀錄的網頁的開發日誌
方便自己整理所有的觀看紀錄,無論是動漫、電影、還是劇
有什麼用?
- 純粹我自己會在各種網站上看電影、動漫等等的,但有些網頁會記錄,下次登入紀錄又沒了。又或者是例如有些漫畫網站,手機和電腦瀏覽的紀錄又不同,久了沒看,回來要再看的時候就又忘了看到哪,翻半天。總之呢,做這個project的目的,就是解決這個我一直想開發的工具,順便玩玩網頁。
- 網路上找不到相似的工具,可能有很多替代方案,例如純手寫就是一個🤣
目標
- 寫出簡單表格,並且可以新增紀錄,包含作品類型、作品名稱、集(季)數、目前的觀看進度
- 多用戶,使用帳號密碼管理
語言與工具的選擇
- 我第一個就想到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'
這裡附上完整實作了create
和delete
功能的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
的部分為了讓網頁默認選擇該history
的season
,所以用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
選擇無
的時候就是代表用戶要記錄的無須季
或是卷
等等的,只純粹紀錄集數,所以隱藏label
和input
,把相關的label
和input
都用附上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
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%;
}