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
View File
@@ -0,0 +1 @@
*.pyc
+60
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
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
View File
@@ -0,0 +1,3 @@
SECRET_KEY = "fdsafasd"
UPLOAD_FOLDER = "image_pool"
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
+161
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())
Binary file not shown.
Binary file not shown.
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
View File
@@ -0,0 +1,3 @@
Flask==1.1.2
Werkzeug==1.0.1
markupsafe==2.0.1
Vendored Executable
+6
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+7
View File
File diff suppressed because one or more lines are too long
+4
View File
File diff suppressed because one or more lines are too long
+68
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
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
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
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
View File
@@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block page_title %}Forbidden(403){% endblock %}
{% block body %}
{{ super() }}
This operation is forbidden.
{% endblock %}
+6
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
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
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 %}
+76
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 %}
+7
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 %}