Merge branch 'master' into 'main'

.Add feature 'Collapsing The Navigation Bar'

See merge request ikt206-g-26v-devops/Group23/flask!1
This commit is contained in:
Teodor Salvesen
2026-05-06 20:09:00 +00:00
28 changed files with 744 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pyc

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# flask-example
A minimal web app developed with [Flask](http://flask.pocoo.org/) framework.
The main purpose is to introduce how to implement the essential elements in web application with Flask, including
- URL Building
- Authentication with Sessions
- Template & Template Inheritance
- Error Handling
- Integrating with *Bootstrap*
- Interaction with Database (SQLite)
- Invoking static resources
For more basic knowledge of Flask, you can refer to [a tutorial on Tutorialspoint](https://www.tutorialspoint.com/flask/).
## How to Run
- Step 1: Make sure you have Python
- Step 2: Install the requirements: `pip install -r requirements.txt`
- Step 3: Go to this app's directory and run `python app.py`
## Details about This Toy App
There are three tabs in this toy app
- **Public**: this is a page which can be accessed by anyone, no matter if the user has logged in or not.
- **Private**: Only logged-in user can access this page. Otherwise the user will get a 401 error page.
- **Admin Page**: This part is only open to the user who logged in as "Admin". In this tab, the administrator can manage accounts (list, delete, or add).
A few accounts were set for testing, like ***admin*** (password: admin), ***test*** (password: 123456), etc. You can also delete or add accounts after you log in as ***admin***.
## References
- http://flask.pocoo.org/
- https://www.tutorialspoint.com/flask/
## Credict
Image private.jpg: https://commons.wikimedia.org/wiki/File:(315-365)_Locked_(6149414678).jpg
Image public.jpg: https://commons.wikimedia.org/wiki/File:Drown%3F!_(131380682).jpg

204
app.py Normal file
View File

@@ -0,0 +1,204 @@
import os
import datetime
import hashlib
from flask import Flask, session, url_for, redirect, render_template, request, abort, flash
from database import list_users, verify, delete_user_from_db, add_user
from database import read_note_from_db, write_note_into_db, delete_note_from_db, match_user_id_with_note_id
from database import image_upload_record, list_images_for_user, match_user_id_with_image_uid, delete_image_from_db
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config.from_object('config')
@app.errorhandler(401)
def FUN_401(error):
return render_template("page_401.html"), 401
@app.errorhandler(403)
def FUN_403(error):
return render_template("page_403.html"), 403
@app.errorhandler(404)
def FUN_404(error):
return render_template("page_404.html"), 404
@app.errorhandler(405)
def FUN_405(error):
return render_template("page_405.html"), 405
@app.errorhandler(413)
def FUN_413(error):
return render_template("page_413.html"), 413
@app.route("/")
def FUN_root():
return render_template("index.html")
@app.route("/public/")
def FUN_public():
return render_template("public_page.html")
@app.route("/private/")
def FUN_private():
if "current_user" in session.keys():
notes_list = read_note_from_db(session['current_user'])
notes_table = zip([x[0] for x in notes_list],\
[x[1] for x in notes_list],\
[x[2] for x in notes_list],\
["/delete_note/" + x[0] for x in notes_list])
images_list = list_images_for_user(session['current_user'])
images_table = zip([x[0] for x in images_list],\
[x[1] for x in images_list],\
[x[2] for x in images_list],\
["/delete_image/" + x[0] for x in images_list])
return render_template("private_page.html", notes = notes_table, images = images_table)
else:
return abort(401)
@app.route("/admin/")
def FUN_admin():
if session.get("current_user", None) == "ADMIN":
user_list = list_users()
user_table = zip(range(1, len(user_list)+1),\
user_list,\
[x + y for x,y in zip(["/delete_user/"] * len(user_list), user_list)])
return render_template("admin.html", users = user_table)
else:
return abort(401)
@app.route("/write_note", methods = ["POST"])
def FUN_write_note():
text_to_write = request.form.get("text_note_to_take")
write_note_into_db(session['current_user'], text_to_write)
return(redirect(url_for("FUN_private")))
@app.route("/delete_note/<note_id>", methods = ["GET"])
def FUN_delete_note(note_id):
if session.get("current_user", None) == match_user_id_with_note_id(note_id): # Ensure the current user is NOT operating on other users' note.
delete_note_from_db(note_id)
else:
return abort(401)
return(redirect(url_for("FUN_private")))
# Reference: http://flask.pocoo.org/docs/0.12/patterns/fileuploads/
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/upload_image", methods = ['POST'])
def FUN_upload_image():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part', category='danger')
return(redirect(url_for("FUN_private")))
file = request.files['file']
# if user does not select file, browser also submit a empty part without filename
if file.filename == '':
flash('No selected file', category='danger')
return(redirect(url_for("FUN_private")))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
upload_time = str(datetime.datetime.now())
image_uid = hashlib.sha1((upload_time + filename).encode()).hexdigest()
# Save the image into UPLOAD_FOLDER
file.save(os.path.join(app.config['UPLOAD_FOLDER'], image_uid + "-" + filename))
# Record this uploading in database
image_upload_record(image_uid, session['current_user'], filename, upload_time)
return(redirect(url_for("FUN_private")))
return(redirect(url_for("FUN_private")))
@app.route("/delete_image/<image_uid>", methods = ["GET"])
def FUN_delete_image(image_uid):
if session.get("current_user", None) == match_user_id_with_image_uid(image_uid): # Ensure the current user is NOT operating on other users' note.
# delete the corresponding record in database
delete_image_from_db(image_uid)
# delete the corresponding image file from image pool
image_to_delete_from_pool = [y for y in [x for x in os.listdir(app.config['UPLOAD_FOLDER'])] if y.split("-", 1)[0] == image_uid][0]
os.remove(os.path.join(app.config['UPLOAD_FOLDER'], image_to_delete_from_pool))
else:
return abort(401)
return(redirect(url_for("FUN_private")))
@app.route("/login", methods = ["POST"])
def FUN_login():
id_submitted = request.form.get("id").upper()
if (id_submitted in list_users()) and verify(id_submitted, request.form.get("pw")):
session['current_user'] = id_submitted
return(redirect(url_for("FUN_root")))
@app.route("/logout/")
def FUN_logout():
session.pop("current_user", None)
return(redirect(url_for("FUN_root")))
@app.route("/delete_user/<id>/", methods = ['GET'])
def FUN_delete_user(id):
if session.get("current_user", None) == "ADMIN":
if id == "ADMIN": # ADMIN account can't be deleted.
return abort(403)
# [1] Delete this user's images in image pool
images_to_remove = [x[0] for x in list_images_for_user(id)]
for f in images_to_remove:
image_to_delete_from_pool = [y for y in [x for x in os.listdir(app.config['UPLOAD_FOLDER'])] if y.split("-", 1)[0] == f][0]
os.remove(os.path.join(app.config['UPLOAD_FOLDER'], image_to_delete_from_pool))
# [2] Delele the records in database files
delete_user_from_db(id)
return(redirect(url_for("FUN_admin")))
else:
return abort(401)
@app.route("/add_user", methods = ["POST"])
def FUN_add_user():
if session.get("current_user", None) == "ADMIN": # only Admin should be able to add user.
# before we add the user, we need to ensure this is doesn't exsit in database. We also need to ensure the id is valid.
if request.form.get('id').upper() in list_users():
user_list = list_users()
user_table = zip(range(1, len(user_list)+1),\
user_list,\
[x + y for x,y in zip(["/delete_user/"] * len(user_list), user_list)])
return(render_template("admin.html", id_to_add_is_duplicated = True, users = user_table))
if " " in request.form.get('id') or "'" in request.form.get('id'):
user_list = list_users()
user_table = zip(range(1, len(user_list)+1),\
user_list,\
[x + y for x,y in zip(["/delete_user/"] * len(user_list), user_list)])
return(render_template("admin.html", id_to_add_is_invalid = True, users = user_table))
else:
add_user(request.form.get('id'), request.form.get('pw'))
return(redirect(url_for("FUN_admin")))
else:
return abort(401)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")

3
config.py Normal file
View File

@@ -0,0 +1,3 @@
SECRET_KEY = "fdsafasd"
UPLOAD_FOLDER = "image_pool"
MAX_CONTENT_LENGTH = 16 * 1024 * 1024

161
database.py Normal file
View File

@@ -0,0 +1,161 @@
import sqlite3
import hashlib
import datetime
user_db_file_location = "database_file/users.db"
note_db_file_location = "database_file/notes.db"
image_db_file_location = "database_file/images.db"
def list_users():
_conn = sqlite3.connect(user_db_file_location)
_c = _conn.cursor()
_c.execute("SELECT id FROM users;")
result = [x[0] for x in _c.fetchall()]
_conn.close()
return result
def verify(id, pw):
_conn = sqlite3.connect(user_db_file_location)
_c = _conn.cursor()
_c.execute("SELECT pw FROM users WHERE id = '" + id + "';")
result = _c.fetchone()[0] == hashlib.sha256(pw.encode()).hexdigest()
_conn.close()
return result
def delete_user_from_db(id):
_conn = sqlite3.connect(user_db_file_location)
_c = _conn.cursor()
_c.execute("DELETE FROM users WHERE id = ?;", (id))
_conn.commit()
_conn.close()
# when we delete a user FROM database USERS, we also need to delete all his or her notes data FROM database NOTES
_conn = sqlite3.connect(note_db_file_location)
_c = _conn.cursor()
_c.execute("DELETE FROM notes WHERE user = ?;", (id))
_conn.commit()
_conn.close()
# when we delete a user FROM database USERS, we also need to
# [1] delete all his or her images FROM image pool (done in app.py)
# [2] delete all his or her images records FROM database IMAGES
_conn = sqlite3.connect(image_db_file_location)
_c = _conn.cursor()
_c.execute("DELETE FROM images WHERE owner = ?;", (id))
_conn.commit()
_conn.close()
def add_user(id, pw):
_conn = sqlite3.connect(user_db_file_location)
_c = _conn.cursor()
_c.execute("INSERT INTO users values(?, ?)", (id.upper(), hashlib.sha256(pw.encode()).hexdigest()))
_conn.commit()
_conn.close()
def read_note_from_db(id):
_conn = sqlite3.connect(note_db_file_location)
_c = _conn.cursor()
command = "SELECT note_id, timestamp, note FROM notes WHERE user = '" + id.upper() + "';"
_c.execute(command)
result = _c.fetchall()
_conn.commit()
_conn.close()
return result
def match_user_id_with_note_id(note_id):
# Given the note id, confirm if the current user is the owner of the note which is being operated.
_conn = sqlite3.connect(note_db_file_location)
_c = _conn.cursor()
command = "SELECT user FROM notes WHERE note_id = '" + note_id + "';"
_c.execute(command)
result = _c.fetchone()[0]
_conn.commit()
_conn.close()
return result
def write_note_into_db(id, note_to_write):
_conn = sqlite3.connect(note_db_file_location)
_c = _conn.cursor()
current_timestamp = str(datetime.datetime.now())
_c.execute("INSERT INTO notes values(?, ?, ?, ?)", (id.upper(), current_timestamp, note_to_write, hashlib.sha1((id.upper() + current_timestamp).encode()).hexdigest()))
_conn.commit()
_conn.close()
def delete_note_from_db(note_id):
_conn = sqlite3.connect(note_db_file_location)
_c = _conn.cursor()
_c.execute("DELETE FROM notes WHERE note_id = ?;", (note_id))
_conn.commit()
_conn.close()
def image_upload_record(uid, owner, image_name, timestamp):
_conn = sqlite3.connect(image_db_file_location)
_c = _conn.cursor()
_c.execute("INSERT INTO images VALUES (?, ?, ?, ?)", (uid, owner, image_name, timestamp))
_conn.commit()
_conn.close()
def list_images_for_user(owner):
_conn = sqlite3.connect(image_db_file_location)
_c = _conn.cursor()
command = "SELECT uid, timestamp, name FROM images WHERE owner = '{0}'".format(owner)
_c.execute(command)
result = _c.fetchall()
_conn.commit()
_conn.close()
return result
def match_user_id_with_image_uid(image_uid):
# Given the note id, confirm if the current user is the owner of the note which is being operated.
_conn = sqlite3.connect(image_db_file_location)
_c = _conn.cursor()
command = "SELECT owner FROM images WHERE uid = '" + image_uid + "';"
_c.execute(command)
result = _c.fetchone()[0]
_conn.commit()
_conn.close()
return result
def delete_image_from_db(image_uid):
_conn = sqlite3.connect(image_db_file_location)
_c = _conn.cursor()
_c.execute("DELETE FROM images WHERE uid = ?;", (image_uid))
_conn.commit()
_conn.close()
if __name__ == "__main__":
print(list_users())

BIN
database_file/images.db Normal file

Binary file not shown.

BIN
database_file/notes.db Normal file

Binary file not shown.

BIN
database_file/users.db Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
Flask==1.1.2
Werkzeug==1.0.1
markupsafe==2.0.1

6
static/css/bootstrap.min.css vendored Executable file

File diff suppressed because one or more lines are too long

7
static/css/bootstrap.min.united.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/img/flask-powered.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/img/public.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

7
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

4
static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

68
templates/admin.html Normal file
View File

@@ -0,0 +1,68 @@
{% extends "layout.html" %}
{% block page_title %}Admin Dashboard{% endblock %}
{% block body %}
{{ super() }}
{# only invoked when failed adding new ID due to duplication #}
{% if id_to_add_is_duplicated %}
<div class="alert alert-dismissible alert-danger">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>Warning!</strong> The account name already exists.
</div>
{% endif %}
{# only invoked when failed adding new ID due to invalid character #}
{% if id_to_add_is_invalid %}
<div class="alert alert-dismissible alert-danger">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>Warning!</strong> The account name is invalid.
</div>
{% endif %}
<div class = "container">
<div class="row">
<div class="col-lg-6">
<h3>Add Account</h3>
<form class="form-inline" action="/add_user" method='post'>
<div class="form-group">
<label for="id">ID</label>
<input type="text" class="form-control" name="id">
</div>
<div class="form-group">
<label for="pw">Password</label>
<input type="password" class="form-control" name="pw">
</div>
<br><br>
<button type="submit" class="btn">Submit</button>
</form>
</div>
<div class="col-lg-6">
<h3>Manage Existing Accounts</h3>
<table class="table small">
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th>Action</th>
</tr>
</thead>
{% for number, id, act in users %}
<tr>
<th> {{ number }} </th>
<td> {{ id }} </td>
<td><a href={{act}}>Delete</a></td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}

20
templates/index.html Normal file
View File

@@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block page_title %}Welcome{% endblock %}
{% block body %}
{{ super() }}
<p>This is a minimal web app developed with <a href="http://flask.pocoo.org/">Flask</a> framework.</p>
<p>The main purpose is to introduce how to implement the essential elements in web applications with Flask, including</p>
<ul>
<li>URL Building</li>
<li>Authentication with Sessions</li>
<li>Template & Template Inheritance</li>
<li>Error Handling</li>
<li>Integrating with Bootstrap</li>
<li>Interaction with Database (SQLite)</li>
<li>Invoking static resources</li>
<li>Upload files</li>
</ul>
<p>For more basic knowledge of Flask, you can refer to <a href="https://www.tutorialspoint.com/flask/">a tutorial on Tutorialspoint</a>.</p>
{% endblock %}

87
templates/layout.html Normal file
View File

@@ -0,0 +1,87 @@
<html>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.united.css') }}">
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
<title>Flask Example</title>
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flask Example</a>
</div>
<div class="collapse navbar-collapse" id="myNavbar">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('FUN_root') }}">Home</a></li>
<li><a href="{{ url_for('FUN_public') }}">Public</a></li>
{% if session.get("current_user", None) != None %}
<li><a href="{{ url_for('FUN_private') }}">Private</a></li>
{% endif%}
{% if session.get("current_user", None) == "ADMIN" %}
<li><a href="{{ url_for('FUN_admin') }}">Admin Dashboard</a></li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if session.get("current_user", None) == None %}
<form action="/login" method="post" class="navbar-form navbar-right">
<div class="form-group">
<input type="text" name="id" placeholder="User Name" class="form-control">
</div>
<div class="form-group">
<input type="password" name="pw" placeholder="Password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Log In</button>
</form>
{% else %}
<li>
<a><b>{{ session.get("current_user") }}</b></a></li>
<li><a href="{{ url_for('FUN_logout') }}"><b><u>Logout</u></b></a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container">
<h1>{% block page_title %}{% endblock %}</h1>
<p>{% block body %}{% endblock %}</p>
</div>
<div class='container'>
<hr>
Developed by <a href='https://github.com/XD-DENG'>XD-DENG</a>
<a href="http://flask.pocoo.org/"><img
src="{{ url_for('static', filename='img/flask-powered.png') }}"
border="0"
align="right"
alt="Flask powered"
title="Flask powered"></a>
</div>
</html>

6
templates/page_401.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Unauthorized(401){% endblock %}
{% block body %}
{{ super() }}
You're not allowed to access.
{% endblock %}

6
templates/page_403.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Forbidden(403){% endblock %}
{% block body %}
{{ super() }}
This operation is forbidden.
{% endblock %}

6
templates/page_404.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Not Found (404){% endblock %}
{% block body %}
{{ super() }}
The resource can not be found.
{% endblock %}

6
templates/page_405.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Method not allowd (405){% endblock %}
{% block body %}
{{ super() }}
The method of your request is not allowed.
{% endblock %}

6
templates/page_413.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Request if too big(413){% endblock %}
{% block body %}
{{ super() }}
Please check the file size you're uploading.
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends "layout.html" %}
{% block page_title %}Private Page{% endblock %}
{% block body %}
{{ super() }}
<h4>You can take notes here. Only yourself can access them. They will be removed when your account is removed.</h4>
<hr>
<h3>Add Note</h3>
<div class="form-group">
<label for="textArea" class="col-lg-3 control-label">Note to Take</label>
<div class="col-lg-9">
<form action="/write_note" method="post">
<input class="form-control" name="text_note_to_take"></input>
<button type="submit" class="btn btn-success">Submit</button>
</form>
</div>
</div>
<hr>
{% if notes %}
<h3>Your Notes</h3>
<table class="table small">
<thead>
<tr>
<th>Note ID</th>
<th>Timestamp</th>
<th>Note</th>
<th>Action</th>
</tr>
</thead>
{% for note_id, timestamp, note, act in notes %}
<tr>
<td> {{ note_id }} </td>
<td> {{ timestamp }} </td>
<td> {{ note }} </td>
<td><a href={{act}}>Delete</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
<hr>
<h3>Upload Image</h3>
<form method='post' action='/upload_image' enctype=multipart/form-data>
<p><input type=file name=file>
<input type=submit value=Upload>
</form>
{% if images %}
<h3>Your Images</h3>
<table class="table small">
<thead>
<tr>
<th>Image ID</th>
<th>Timestamp</th>
<th>Image Name</th>
<th>Action</th>
</tr>
</thead>
{% for image_id, timestamp, image_name, act in images %}
<tr>
<td> {{ image_id }} </td>
<td> {{ timestamp }} </td>
<td> {{ image_name }} </td>
<td><a href={{act}}>Delete</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% extends "layout.html" %}
{% block page_title %}Public Page{% endblock %}
{% block body %}
{{ super() }}
<img src="{{ url_for('static', filename='img/public.jpg') }}" class="img-circle" alt="Cinque Terre" width="304" height="236">
You can access this no matter whether you have logged in.
{% endblock %}