Merge branch 'alternate-dashboard-2'

This commit is contained in:
2025-04-17 19:28:40 +02:00
24 changed files with 178 additions and 101 deletions

Binary file not shown.

4
app.py
View File

@@ -1,11 +1,9 @@
from application import app
from flask import redirect, url_for
from flask_login import login_required # type: ignore
# home route
# home route, place holder in case we want a home page
@app.route("/")
@login_required
def index():
return redirect(url_for("dash.index"))

View File

@@ -20,9 +20,6 @@ db.init_app(app)
migrate = Migrate(app, db)
# bp import
from application.auth.views import auth_blueprint
from application.dash.views import dash_blueprint
# Login manager
from application.auth.models import User
@@ -32,6 +29,7 @@ login_manager.init_app(app) # type: ignore
login_manager.login_view = "auth.login" # type: ignore
# Gets all the user data
@login_manager.user_loader # type: ignore
def load_user(user_id): # type: ignore
return User.query.get(int(user_id)) # type: ignore
@@ -39,5 +37,14 @@ def load_user(user_id): # type: ignore
# Blueprint magic
# bp import
# Would like to do this at the top of the file,
# but can't easily figure out how to do this.
# I think everything that the views depend on have to be moved
# into a seperate .py and imported.
from application.auth.views import auth_blueprint
from application.dash.views import dash_blueprint
# Register blueprints
app.register_blueprint(dash_blueprint, url_prefix="/dash")
app.register_blueprint(auth_blueprint, url_prefix="/auth")

View File

@@ -3,16 +3,21 @@ from wtforms import StringField, SubmitField, PasswordField, BooleanField
from wtforms.validators import DataRequired
# Default Form that inherits from FlaskForm and
# contains a username, password and submit button
class defaultForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Submit")
# LoginForm, contains exactly the same as defaultForm
class LoginForm(defaultForm):
pass
# RegisterForm that inherits from the default.
# Adds a password confirmation and if the user is an admin or not.
class RegisterForm(defaultForm):
confirm_password = PasswordField(
"Confirm Password", validators=[DataRequired()]
@@ -20,6 +25,8 @@ class RegisterForm(defaultForm):
is_admin = BooleanField("Admin")
# Form to update password information.
# Needs a confirmation password and the current password
class UpdateForm(defaultForm):
confirm_password = PasswordField(
"Confirm Password", validators=[DataRequired()]

View File

@@ -2,14 +2,18 @@ from application import db
from flask_login import UserMixin # type: ignore
# User model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String(150), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
# Purely a relationship not a column,
# makes all the services accessible through User.services
services = db.relationship("Service", backref="user", lazy="joined")
# Initialize user, prevents red stuff
def __init__(self, username: str, password: str, is_admin: bool = False):
self.username = username
self.password = password

View File

@@ -1,23 +1,22 @@
from flask import Blueprint, render_template, redirect, url_for
from flask import Blueprint, render_template, redirect, url_for, flash
from application import db
from application.auth.models import User
from application.auth.forms import LoginForm
from flask_login import ( # type: ignore
login_required, # type: ignore
login_user, # type: ignore
logout_user,
current_user,
)
from werkzeug.security import check_password_hash, generate_password_hash
from application.decorators import admin_required
from application.decorators import admin_required, login_required
from application.auth.forms import RegisterForm, UpdateForm
auth_blueprint = Blueprint("auth", __name__, template_folder="templates")
# Routes
@auth_blueprint.route("/register", methods=["GET", "POST"])
# Add user
@auth_blueprint.route("/register_user", methods=["GET", "POST"])
@admin_required
def register():
register_form = RegisterForm()
@@ -29,14 +28,14 @@ def register():
is_admin = register_form.is_admin.data
if confirm_password != password:
return render_template(
"admin.html",
"register_user.html",
form=register_form,
feedback="Passwords don't match, please try again",
active_page="register",
)
if User.query.filter_by(username=username).first():
return render_template(
"admin.html",
"register_user.html",
form=register_form,
feedback="Username is already taken",
active_page="register",
@@ -49,16 +48,17 @@ def register():
db.session.add(new_user)
db.session.commit()
return render_template(
"admin.html",
"register_user.html",
form=RegisterForm(formdata=None),
feedback="User succesfully added",
active_page="register",
)
return render_template(
"admin.html", form=register_form, active_page="register"
"register_user.html", form=register_form, active_page="register"
)
# Update user (specifically password)
@auth_blueprint.route("/update_user", methods=["GET", "POST"])
@login_required
def update():
@@ -85,10 +85,12 @@ def update():
)
db.session.commit()
logout_user()
flash("Password changed succesfully, please log back in")
return redirect(url_for("auth.login"))
return render_template("update_user.html", form=form, active_page="update")
# Login as user or admin
@auth_blueprint.route("/login", methods=["GET", "POST"])
def login():
login_form = LoginForm()
@@ -103,6 +105,7 @@ def login():
user.password, password # type: ignore
):
login_user(user) # type: ignore
flash("Logged in succesfully")
return redirect("/")
else:
feedback = "Username or password is incorrect"
@@ -110,8 +113,10 @@ def login():
return render_template("login.html", form=login_form, feedback=feedback)
# Logout
@auth_blueprint.route("/logout")
@login_required
def logout():
logout_user()
flash("Logged out succesfully")
return redirect(url_for("index"))

View File

@@ -4,9 +4,19 @@ from wtforms.validators import DataRequired
from flask_wtf.file import FileField, FileAllowed # type: ignore
# Form for service on dashboard, connected to database through ORM
class ServiceForm(FlaskForm):
name = StringField("Service name:", validators=[DataRequired()])
url = URLField("Service URL:", validators=[DataRequired()])
name = StringField(
"Service name:",
validators=[DataRequired()],
render_kw={"placeholder": "Service Name"},
)
url = URLField(
"Service URL:",
validators=[DataRequired()],
render_kw={"placeholder": "https://example.com"},
)
# File field that only allows jpg, jpeg or png
image = FileField(
"Icon:",
validators=[

View File

@@ -1,14 +1,17 @@
from application import db
# Service class for dashboard
class Service(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
url = db.Column(db.String, nullable=False)
icon = db.Column(db.String, default="google.png")
# Foreign key to connect to User table
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
# Initialize the service (prevents ugly red lines)
def __init__(
self, name: str, url: str, user_id: int, icon: str = "google.png"
):

View File

@@ -6,43 +6,41 @@
<div class="grid-container">
{% for service in services%}
<div class="bg-light container-xxl">
<div class="row row-cols-3">
<div class="col-sm-2" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
<div class="row">
<div class="col name" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
{{service["name"]}}
</div>
<div class="col-sm-3 dots dropdown">
<button class="btn btn-light py-0" type="button" id="threeDotDropdown" data-bs-toggle="dropdown" aria-expanded="false">
&#x22EE;
</button>
<ul class="dropdown-menu" aria-labelledby="threeDotDropdown">
<li>
<a class="dropdown-item" href="{{ url_for('dash.edit_service', service_id=service.id) }}">Edit</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<form action="{{ url_for('dash.delete_service', service_id=service.id) }}" method="POST" style="display:inline;">
<button style="color: red;" type="submit" class="dropdown-item">Delete</button>
</form>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-sm-9" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
<img class="fit-picture" src="{{ url_for('static', filename='icons/'+service['icon'])}}">
</div>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-10" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
{{service["name"]}}
</div>
<div class="col-sm-2 dots dropdown">
<button class="btn btn-light py-0" type="button" id="threeDotDropdown" data-bs-toggle="dropdown"
aria-expanded="false">
&#x22EE;
</button>
<ul class="dropdown-menu" aria-labelledby="threeDotDropdown">
<li>
<a class="dropdown-item" href="{{ url_for('dash.edit_service', service_id=service.id) }}">Edit</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<form action="{{ url_for('dash.delete_item', service_id=service.id) }}" method="POST" style="display:inline;">
<button style="color: red;" type="submit" class="dropdown-item">Delete</button>
</form>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
{{service["url"]}}
</div>
</div>
</div>
<div class="row">
<div class="col url text-break" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
{{service["url"]}}
</div>
</div>
</div>
{% endfor %}
</div>
{%endblock%}
{%endblock%}

View File

@@ -1,13 +1,16 @@
from application import db
from flask import Blueprint, render_template, redirect, url_for
from flask import Blueprint, render_template, redirect, url_for, flash
from application.dash.forms import ServiceForm
from flask_login import login_required, current_user # type: ignore
from flask_login import current_user # type: ignore
from application.dash.models import Service
from application.utils import saveImage
from application.decorators import login_required
# Dashboard blueprint
dash_blueprint = Blueprint("dash", __name__, template_folder="templates")
# index
@dash_blueprint.route("/", methods=["GET", "POST"])
@login_required
def index():
@@ -17,23 +20,27 @@ def index():
)
@dash_blueprint.route("/delete_item/<int:service_id>", methods=["POST"])
# Deleting a service
@dash_blueprint.route("/delete_service/<int:service_id>", methods=["POST"])
@login_required
def delete_item(service_id: int):
def delete_service(service_id: int):
service = Service.query.get_or_404(service_id)
# Check ownership
if service.user_id != current_user.id:
flash("This is not your service!")
return redirect(url_for("dash.index"))
db.session.delete(service)
db.session.commit()
flash("Service deleted")
return redirect(url_for("dash.index"))
@dash_blueprint.route("/service", methods=["GET", "POST"])
# Add a service
@dash_blueprint.route("/add_service", methods=["GET", "POST"])
@login_required
def service():
def add_service():
service_form = ServiceForm()
if service_form.validate_on_submit(): # type: ignore
@@ -52,17 +59,14 @@ def service():
) # type: ignore
db.session.add(new_service)
db.session.commit()
return render_template(
"add_service.html",
form=ServiceForm(formdata=None),
feedback="Service succesfully added",
active_page="service",
)
flash("Service added")
return redirect(url_for("dash.index"))
return render_template(
"add_service.html", form=service_form, active_page="service"
)
# Edit service
@dash_blueprint.route(
"/edit_service/<int:service_id>", methods=["GET", "POST"]
)
@@ -88,22 +92,8 @@ def edit_service(service_id: int):
commit = True
if commit:
db.session.commit()
return redirect(url_for("dash.index"))
flash("Service edited")
return redirect(url_for("dash.index"))
# Fill in correct data
form = ServiceForm(name=service.name, url=service.url)
return render_template("edit_service.html", form=form)
"""
def saveImage(image: ...):
filename = secure_filename(image.filename)
save_path = os.path.join(
app.config["UPLOAD_FOLDER"], # type: ignore
str(current_user.id),
filename,
)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
image.save(save_path) # type: ignore
filename2 = str(current_user.id) + "/" + filename
return filename2
"""

View File

@@ -4,13 +4,26 @@ from functools import wraps
from flask import redirect, url_for
# Decorator that checks if the current user is logged in and an admin
# Could be shortened by adding the login_required decorator
# and removing the logic here
def admin_required(f: Callable[..., Any]) -> Callable[..., Any]:
@wraps(f)
def decorated_function(*args: ..., **kwargs: ...):
if not current_user.is_authenticated:
return redirect(url_for("login"))
return redirect(url_for("auth.login"))
if not current_user.is_admin:
return redirect(url_for("index"))
return f(*args, **kwargs)
return decorated_function
def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
@wraps(f)
def decorated_function(*args: ..., **kwargs: ...):
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
return f(*args, **kwargs)
return decorated_function

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -5,28 +5,43 @@ body {
/* Dashboard page */
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
display: flex;
grid-template-columns: auto auto auto auto auto;
gap: 20px;
background-color: lightslategray;
padding-top: 20px;
margin-left: 10%;
margin-right: 10%;
justify-content: center;
}
.grid-container > div {
height: fit-content;
width: 400px;
width: 200px;
padding-top: 5px;
padding-bottom: 5px;
border: 2px solid black;
border-radius: 10px;
box-shadow: 5px 5px 10px black;
font-weight: bold;
}
.fit-picture {
width: 45px;
padding-top: 5px;
padding-bottom: 5px;
width: 175px;
margin-top: 5px;
margin-bottom: 5px;
border: 1px solid black;
border-radius: 5px;
box-shadow: 0px 0px 5px black;
}
.url {
font-weight: normal;
}
.name {
margin-left: 60px;
}
/* Login page */

View File

@@ -26,7 +26,7 @@
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'service' %}active{% endif %}"
href="{{url_for('dash.service')}}">Add service</a>
href="{{url_for('dash.add_service')}}">Add service</a>
</li>
{% endif %}
{% if current_user.is_admin %}
@@ -38,20 +38,46 @@
</ul>
{% if current_user.is_authenticated %}
<div class="dropstart">
<button class="btn btn-outline-info" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-outline-info" type="button" id="dropdownMenuButton1"
data-bs-toggle="dropdown" aria-expanded="false">
Profile
</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start" aria-labelledby="dropdownMenuButton1">
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start"
aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item">Username: {{current_user.username}}</a></li>
<li><a class="dropdown-item {% if active_page == 'update' %}active{% endif %}" href="{{url_for('auth.update')}}">Change password</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" style="color: tomato;" data-bs-theme="dark" href="{{url_for('auth.logout')}}">Logout</a></li>
<li><a class="dropdown-item {% if active_page == 'update' %}active{% endif %}"
href="{{url_for('auth.update')}}">Change password</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" style="color: tomato;" data-bs-theme="dark"
href="{{url_for('auth.logout')}}">Logout</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
</nav>
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check-circle-fill" viewBox="0 0 16 16">
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
</symbol>
</svg>
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1050;">
{% for message in get_flashed_messages() %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="15" height="15" role="img" aria-label="Success:">
<use xlink:href="#check-circle-fill" />
</svg>
{{message}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% block content %}
{% endblock %}
</body>

View File

@@ -1,17 +1,23 @@
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename
from application import app
from flask_login import current_user # type: ignore
# save image to static folder
def saveImage(image: ...):
filename = secure_filename(image.filename)
# Path should be /application/static/[user_id]/[filename]
save_path = os.path.join(
app.config["UPLOAD_FOLDER"], # type: ignore
str(current_user.id),
filename,
)
# Create path is it doesn't exist
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# Save the image
image.save(save_path) # type: ignore
filename2 = str(current_user.id) + "/" + filename
# Return the filename that is stored in database.
# Only done to keep a single default image, this should be done differently
filename2 = str(current_user.id) + "/" + filename # [user_id]/[filename]
return filename2

11
seed.py
View File

@@ -3,14 +3,7 @@ from application.dash.models import Service
from application.auth.models import User
from werkzeug.security import generate_password_hash
"""
new_strikers = [
se(name="Erik", strike="y", age=44),
Striker(name="Henk", strike="n", age=88),
]
"""
# User acounts to add
new_users = [
User(
username="admin",
@@ -29,6 +22,7 @@ new_users = [
),
]
# Services to add
new_services = [
Service(name="test123", url="http://google.com", user_id=1),
# Daan services
@@ -51,4 +45,5 @@ with app.app_context():
# Then add new
db.session.add_all(new_services)
db.session.add_all(new_users)
# Commit
db.session.commit()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB