From 51c9fed15381421c4b7e8ba95af60b5204483d50 Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Sun, 5 Jun 2022 14:20:40 +0200 Subject: First commit :baby: --- app/.gitignore | 4 + app/__init__.py | 42 +++++ app/forms.py | 33 ++++ app/main.py | 147 +++++++++++++++++ app/models.py | 63 ++++++++ app/modules/__init__.py | 4 + app/modules/auth/forms.py | 34 ++++ app/modules/auth/routes.py | 73 +++++++++ app/modules/common.py | 75 +++++++++ app/static/fonts/CourierPrime-Bold.ttf | Bin 0 -> 91368 bytes app/static/fonts/CourierPrime-BoldItalic.ttf | Bin 0 -> 91908 bytes app/static/fonts/CourierPrime-Italic.ttf | Bin 0 -> 96196 bytes app/static/fonts/CourierPrime-Regular.ttf | Bin 0 -> 98156 bytes app/static/fonts/Inconsolata.otf | Bin 0 -> 58464 bytes app/static/fonts/PublicSans-Black.otf | Bin 0 -> 49640 bytes app/static/fonts/PublicSans-BlackItalic.otf | Bin 0 -> 51720 bytes app/static/fonts/PublicSans-Bold.otf | Bin 0 -> 56032 bytes app/static/fonts/PublicSans-BoldItalic.otf | Bin 0 -> 60100 bytes app/static/fonts/PublicSans-ExtraBold.otf | Bin 0 -> 56468 bytes app/static/fonts/PublicSans-ExtraBoldItalic.otf | Bin 0 -> 60340 bytes app/static/fonts/PublicSans-ExtraLight.otf | Bin 0 -> 56584 bytes app/static/fonts/PublicSans-ExtraLightItalic.otf | Bin 0 -> 60244 bytes app/static/fonts/PublicSans-Italic.otf | Bin 0 -> 60316 bytes app/static/fonts/PublicSans-Light.otf | Bin 0 -> 55120 bytes app/static/fonts/PublicSans-LightItalic.otf | Bin 0 -> 58588 bytes app/static/fonts/PublicSans-Medium.otf | Bin 0 -> 56180 bytes app/static/fonts/PublicSans-MediumItalic.otf | Bin 0 -> 59748 bytes app/static/fonts/PublicSans-Regular.otf | Bin 0 -> 56792 bytes app/static/fonts/PublicSans-SemiBold.otf | Bin 0 -> 56720 bytes app/static/fonts/PublicSans-SemiBoldItalic.otf | Bin 0 -> 60168 bytes app/static/fonts/PublicSans-Thin.otf | Bin 0 -> 50824 bytes app/static/fonts/PublicSans-ThinItalic.otf | Bin 0 -> 52780 bytes app/static/styles/style.css | 192 +++++++++++++++++++++++ app/templates/base.html | 50 ++++++ app/templates/home.html | 90 +++++++++++ app/templates/modules/add-item.html | 19 +++ app/templates/modules/invoices.html | 80 ++++++++++ app/templates/modules/login.html | 16 ++ app/templates/modules/register.html | 16 ++ app/templates/modules/settings.html | 45 ++++++ 40 files changed, 983 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/__init__.py create mode 100644 app/forms.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/modules/__init__.py create mode 100644 app/modules/auth/forms.py create mode 100644 app/modules/auth/routes.py create mode 100644 app/modules/common.py create mode 100644 app/static/fonts/CourierPrime-Bold.ttf create mode 100644 app/static/fonts/CourierPrime-BoldItalic.ttf create mode 100644 app/static/fonts/CourierPrime-Italic.ttf create mode 100644 app/static/fonts/CourierPrime-Regular.ttf create mode 100644 app/static/fonts/Inconsolata.otf create mode 100644 app/static/fonts/PublicSans-Black.otf create mode 100644 app/static/fonts/PublicSans-BlackItalic.otf create mode 100644 app/static/fonts/PublicSans-Bold.otf create mode 100644 app/static/fonts/PublicSans-BoldItalic.otf create mode 100644 app/static/fonts/PublicSans-ExtraBold.otf create mode 100644 app/static/fonts/PublicSans-ExtraBoldItalic.otf create mode 100644 app/static/fonts/PublicSans-ExtraLight.otf create mode 100644 app/static/fonts/PublicSans-ExtraLightItalic.otf create mode 100644 app/static/fonts/PublicSans-Italic.otf create mode 100644 app/static/fonts/PublicSans-Light.otf create mode 100644 app/static/fonts/PublicSans-LightItalic.otf create mode 100644 app/static/fonts/PublicSans-Medium.otf create mode 100644 app/static/fonts/PublicSans-MediumItalic.otf create mode 100644 app/static/fonts/PublicSans-Regular.otf create mode 100644 app/static/fonts/PublicSans-SemiBold.otf create mode 100644 app/static/fonts/PublicSans-SemiBoldItalic.otf create mode 100644 app/static/fonts/PublicSans-Thin.otf create mode 100644 app/static/fonts/PublicSans-ThinItalic.otf create mode 100644 app/static/styles/style.css create mode 100644 app/templates/base.html create mode 100644 app/templates/home.html create mode 100644 app/templates/modules/add-item.html create mode 100644 app/templates/modules/invoices.html create mode 100644 app/templates/modules/login.html create mode 100644 app/templates/modules/register.html create mode 100644 app/templates/modules/settings.html (limited to 'app') diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..1ac0ed9 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,4 @@ +# .gitignore for app + +*__pycache__ +*.db \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..bd18038 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,42 @@ +# -*- mode: python; -*- + +"""This is the application factory. + +When the mdl app package is imported, Flask uses the create_app +function to instantiate the web app. + +""" + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager + +from .models import db + + +def create_app(): + app = Flask(__name__) + app.config.from_pyfile("../config.py") + + db.init_app(app) + + login_manager = LoginManager() + login_manager.login_view = "auth.login" + login_manager.init_app(app) + + from .models import User + + @login_manager.user_loader + def load_user(user_id): + return User.query.get(int(user_id)) + + from .main import main + + app.register_blueprint(main) + + from .modules import common, auth + + app.register_blueprint(common) + app.register_blueprint(auth) + + return app diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..a093d5d --- /dev/null +++ b/app/forms.py @@ -0,0 +1,33 @@ +from flask_wtf import FlaskForm +from wtforms import ( + SubmitField, + SelectField, + HiddenField, + StringField, + PasswordField, + IntegerField, + FloatField, + BooleanField, + DateTimeField, +) +from wtforms.validators import ( + InputRequired, + Length, + NumberRange, + EqualTo, + ValidationError, +) + + +class DownloadToRemote(FlaskForm): + url = StringField("Link URL", validators=[InputRequired()]) + download_remote = SubmitField("Download on remote") + + +class ManageRemote(FlaskForm): + file_name = HiddenField() + download_local = SubmitField("Download locally") + remove_remote = SubmitField("Remove remote") + + # def __init__(self, name): + # self.name = HiddenField(name) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..149ef64 --- /dev/null +++ b/app/main.py @@ -0,0 +1,147 @@ +# -*- mode: python; -*- + +""" +routes.py module +---------------- + +This Python module contains the logic supporting: +1. Navigating between website pages +2. Interpreting user requests to the server +3. Dispatching requested content back to the user + +Python dependencies: +- flask: provides web application features +- forms: provides secure user form submission +- sqlalchemy: provides communication with database on server. + +Personal imports: +These are used to avoid cluttering this file with +placeholder data for posts' content. +""" + +import os +import time +import glob +from datetime import datetime +from concurrent.futures import ThreadPoolExecutor + +from flask import ( + Blueprint, + render_template, + send_from_directory, + request, + redirect, + flash, + url_for, + jsonify, + abort, +) +from flask_login import login_required, current_user +import youtube_dl + +from .models import db, Download +from .forms import DownloadToRemote, ManageRemote + +main = Blueprint("main", __name__) +executor = ThreadPoolExecutor(4) + + +@main.route("/") +@main.route("/index") +@login_required +def home(): + """Prompt for video URL.""" + # https://www.youtube.com/watch?v=86khmc6y1yE&list=RDGMEMYH9CUrFO7CfLJpaD7UR85w&index=13 + download_history = Download.query.order_by(Download.primary_key.desc()).all() + downloaded_files = [ + file for file in os.listdir("downloads") if file.endswith((".mp3", ".m4a")) + ] + pending_files = [file for file in os.listdir("downloads") if file.endswith(".part")] + return render_template( + "home.html", + user=current_user, + form_download_remote=DownloadToRemote(), + form_manage_remote=ManageRemote(), + downloaded_files=downloaded_files, + pending_files=pending_files, + download_history=download_history, + ) + + +@main.route("/download-remote", methods=["POST"]) +@login_required +def download_remote(): + """Download audio from URL onto server.""" + form = DownloadToRemote() + if form.validate_on_submit(): + url = request.form["url"] + ydl_opts = { + "format": "bestaudio/best", + "outtmpl": os.path.join( + os.getcwd(), + "downloads", + "%(title)s.%(ext)s", + ), + "noplaylist": True, + "postprocessors": [ + { + "key": "FFmpegExtractAudio", + "preferredcodec": "mp3", + "preferredquality": "192", + } + ], + } + executor.submit(youtube_dl.YoutubeDL(ydl_opts).download, [url]) + with youtube_dl.YoutubeDL(ydl_opts) as ydl: + # time.sleep(1) + info = ydl.extract_info(url, download=False) + title = info.get("title") + new_download = Download( + title=title, + url=url, + user_id=current_user.primary_key, + ) + db.session.add(new_download) + db.session.commit() + flash(f"Successfully started downloading {title}.") + return redirect("/index") + flash(f"Couldn't download {title}.") + return redirect("/index") + + +@main.route("/manage-remote/", methods=["POST"]) +@login_required +def manage_remote(): + """Manage all files downloaded on remote device. + + The value of the submit button pressed is checked, then the + appropriate redirection is performed. + + """ + form = ManageRemote() + if form.validate_on_submit(): + file_name = request.form.get("file_name") + if form.download_local.data: + return redirect(url_for("main.download_local", file=file_name)) + elif form.remove_remote.data: + return redirect(url_for("main.remove_remote", file=file_name)) + flash("Couldn't manage remote file.", "error") + return redirect("/index") + + +@main.route("/download-local/", methods=["GET", "POST"]) +@login_required +def download_local(file): + """Download file from remote to local device.""" + downloads = os.path.join(os.getcwd(), "downloads") + return send_from_directory(downloads, file, as_attachment=True) + + +@main.route("/remove-remote/", methods=["GET", "POST"]) +@login_required +def remove_remote(file): + """Remove file from remote device.""" + file_to_remove = os.path.join(os.getcwd(), "downloads", file) + os.remove(file_to_remove) + flash(f"Successfully removed file {file}.") + return redirect("/index") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..5b61045 --- /dev/null +++ b/app/models.py @@ -0,0 +1,63 @@ +# -*- mode: python; -*- + + +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.sql import func + +from flask_login import UserMixin + +db = SQLAlchemy() + + +class User(UserMixin, db.Model): + """UserMixin inheritance required for features described here: + + https://stackoverflow.com/questions/63231163/what-is-the-usermixin-in-flask""" + + __tablename__ = "User" + + def get_id(self): + return self.primary_key + + primary_key = db.Column("UserId", db.Integer, primary_key=True) + username = db.Column("Username", db.String(20), nullable=False) + hashed_password = db.Column("HashedPassword", db.String(100), nullable=False) + name_first = db.Column("NameFirst", db.String(20), nullable=False) + name_last = db.Column("NameLast", db.String(20), nullable=False) + date_time_created = db.Column( + "DateTimeCreated", db.String, server_default=func.now() + ) + date_time_updated = db.Column( + "DateTimeUpdated", db.String, server_onupdate=func.now() + ) + downloads = db.relationship("Download", back_populates="user") + + def __repr__(self): + return f"" + + +class Download(db.Model): + """One record per file downloaded.""" + + __tablename__ = "Download" + primary_key = db.Column("DownloadId", db.Integer, primary_key=True) + title = db.Column("Title", db.String(20), nullable=False) + url = db.Column("URL", db.String, nullable=False) + date_time_downloaded = db.Column( + "DateTimeDownloaded", db.String, server_default=func.now() + ) + user_id = db.Column("UserId", db.Integer, db.ForeignKey("User.UserId")) + user = db.relationship("User", back_populates="downloads") + + def __init__( + self, + title, + url, + user_id, + ): + self.title = title + self.url = url + self.user_id = user_id + + def __repr__(self): + return f"" diff --git a/app/modules/__init__.py b/app/modules/__init__.py new file mode 100644 index 0000000..fcd1d43 --- /dev/null +++ b/app/modules/__init__.py @@ -0,0 +1,4 @@ +# -*- mode: python; -*- + +from .common import common +from .auth.routes import auth diff --git a/app/modules/auth/forms.py b/app/modules/auth/forms.py new file mode 100644 index 0000000..a4b4555 --- /dev/null +++ b/app/modules/auth/forms.py @@ -0,0 +1,34 @@ +# -*- mode: python; -*- + + +from flask_wtf import FlaskForm +from wtforms import ( + SubmitField, + HiddenField, + StringField, + PasswordField, + BooleanField, +) +from wtforms.validators import ( + InputRequired, + Length, + ValidationError, +) + + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[InputRequired()]) + password = PasswordField("Password", validators=[InputRequired()]) + remember = BooleanField("Remember") + submit = SubmitField("Login") + + +class RegisterForm(LoginForm): + def validate_invite_code(self, field): + if field.data != "mdltesters2022": + raise ValidationError("Invitation code does not match") + + invitation_code = StringField("Invitation code", validators=[InputRequired()]) + name_first = StringField("First name", validators=[InputRequired()]) + name_last = StringField("Last name", validators=[InputRequired()]) + submit = SubmitField("Register") diff --git a/app/modules/auth/routes.py b/app/modules/auth/routes.py new file mode 100644 index 0000000..db59bf1 --- /dev/null +++ b/app/modules/auth/routes.py @@ -0,0 +1,73 @@ +# -*- mode: python; -*- + +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, login_required, logout_user +from werkzeug.security import generate_password_hash, check_password_hash + +from ... import db +from ...models import User +from .forms import LoginForm, RegisterForm + + +auth = Blueprint("auth", __name__) + + +@auth.route("/login", methods=["GET", "POST"]) +def login(): + form = LoginForm() + if form.validate_on_submit(): + req = request.form + # print(req["remember"]) + remember = True if req.get("remember") else False + user = User.query.filter_by(username=req["username"]).first() + if user is None: + flash("User not registered.", "error") + return redirect(url_for("auth.register")) + if check_password_hash(user.hashed_password, req["password"]) is False: + flash("Wrong password.", "error") + return redirect(url_for("auth.login")) + login_user(user, remember=remember) + flash( + f"Logged in as user {user.username} successfully. " + + f"You will{' not ' if remember is False else ' '}be remembered next time!" + ) + return redirect(url_for("main.home")) + return render_template("modules/login.html", form=form) + + +@auth.route("/register", methods=["GET", "POST"]) +def register(): + form = RegisterForm() + if form.validate_on_submit(): + req = request.form + user_already_exists = User.query.filter_by( + name_first=req["name_first"], + name_last=req["name_last"], + ).first() + if user_already_exists: + flash( + f"User {req['name_first']} {req['name_last']} already exists.", "error" + ) + return redirect(url_for("auth.login")) + if req["invitation_code"] != "mdltesters2022": + flash("Wrong invitation code.", "error") + return redirect(url_for("auth.register")) + new_user = User( + username=req["username"], + hashed_password=generate_password_hash(req["password"], method="sha256"), + name_first=req["name_first"], + name_last=req["name_last"], + ) + db.session.add(new_user) + db.session.commit() + flash(f"Created user {req['name_first']} {req['name_last']} successfully.") + return redirect(url_for("main.home")) + return render_template("modules/register.html", form=form) + + +@auth.route("/logout") +@login_required +def logout(): + logout_user() + flash(f"Logged out successfully.") + return redirect(url_for("main.home")) diff --git a/app/modules/common.py b/app/modules/common.py new file mode 100644 index 0000000..1250878 --- /dev/null +++ b/app/modules/common.py @@ -0,0 +1,75 @@ +# -*- mode: python; -*- + +import inspect +from flask import Blueprint, request, render_template, redirect, flash, jsonify +from flask_login import login_required, current_user + +from .. import db +from .. import models +# from . import forms + +from wtforms import SelectField + + +common = Blueprint("common", __name__) + + +@common.route("/modules//add/", methods=["GET", "POST"]) +@login_required +def add_item(module, table): + """Add new item to table accessible via module.""" + # print("db table keys are", db.metadata.tables.keys()) + if table not in db.metadata.tables.keys(): + return render_template("errors/item-not-found.html", table=table) + form = getattr(forms, f"Add{table}")() + if form.validate_on_submit(): + model = getattr(models, table) + table_fields = inspect.signature(model).parameters + form_values = {key: request.form.get(key) for key in table_fields} + print(f"Ready to insert in {table} from {module} {form_values}") + record = model(**form_values) + db.session.add(record) + db.session.commit() + item_pk = model.query.order_by(model.primary_key.desc()).first().primary_key + flash(f"Successfully added item #{item_pk} to {table} table.", "info") + return redirect(f"/modules/{module}") + return render_template("modules/add-item.html", table=table, form=form) + + +@common.route("/modules//edit/
/", methods=["GET", "POST"]) +@login_required +def edit_item(module, table, pk): + """Edit existing item in table accessible via module.""" + if table not in db.metadata.tables.keys(): + return render_template("errors/item-not-found.html", table=table) + model = getattr(models, table) + item = model.query.filter_by(primary_key=pk).first() + # Instantiate form with selected item's field values. + form = getattr(forms, f"Add{table}")(**item.__dict__) + if form.validate_on_submit(): + table_fields = inspect.signature(model).parameters + form_values = {key: request.form.get(key) for key in table_fields} + print(f"Ready to update {form_values}") + model.query.filter_by(primary_key=pk).update(form_values) + db.session.commit() + flash(f"Successfully edited item #{pk} in {table} table.", "info") + return redirect(f"/modules/{module}") + return render_template("modules/edit-item.html", table=table, pk=pk, form=form) + + +@common.route("/modules//delete/
/", methods=["POST"]) +@login_required +def delete_item(module, table, pk): + """Delete item with Primary Key = pk from table in module.""" + model = getattr(models, table) + record = model.query.filter_by(primary_key=pk).first() + db.session.delete(record) + db.session.commit() + flash(f"Successfully removed item #{pk} from {table} table.", "info") + return redirect(f"/modules/{module}") + + +@common.route("/modules/settings") +@login_required +def settings(): + return render_template("modules/settings.html", user=current_user) diff --git a/app/static/fonts/CourierPrime-Bold.ttf b/app/static/fonts/CourierPrime-Bold.ttf new file mode 100644 index 0000000..1b0888c Binary files /dev/null and b/app/static/fonts/CourierPrime-Bold.ttf differ diff --git a/app/static/fonts/CourierPrime-BoldItalic.ttf b/app/static/fonts/CourierPrime-BoldItalic.ttf new file mode 100644 index 0000000..d4e7186 Binary files /dev/null and b/app/static/fonts/CourierPrime-BoldItalic.ttf differ diff --git a/app/static/fonts/CourierPrime-Italic.ttf b/app/static/fonts/CourierPrime-Italic.ttf new file mode 100644 index 0000000..75a1343 Binary files /dev/null and b/app/static/fonts/CourierPrime-Italic.ttf differ diff --git a/app/static/fonts/CourierPrime-Regular.ttf b/app/static/fonts/CourierPrime-Regular.ttf new file mode 100644 index 0000000..db4e6c1 Binary files /dev/null and b/app/static/fonts/CourierPrime-Regular.ttf differ diff --git a/app/static/fonts/Inconsolata.otf b/app/static/fonts/Inconsolata.otf new file mode 100644 index 0000000..3488898 Binary files /dev/null and b/app/static/fonts/Inconsolata.otf differ diff --git a/app/static/fonts/PublicSans-Black.otf b/app/static/fonts/PublicSans-Black.otf new file mode 100644 index 0000000..bbbaa26 Binary files /dev/null and b/app/static/fonts/PublicSans-Black.otf differ diff --git a/app/static/fonts/PublicSans-BlackItalic.otf b/app/static/fonts/PublicSans-BlackItalic.otf new file mode 100644 index 0000000..46e3f71 Binary files /dev/null and b/app/static/fonts/PublicSans-BlackItalic.otf differ diff --git a/app/static/fonts/PublicSans-Bold.otf b/app/static/fonts/PublicSans-Bold.otf new file mode 100644 index 0000000..7a2b62b Binary files /dev/null and b/app/static/fonts/PublicSans-Bold.otf differ diff --git a/app/static/fonts/PublicSans-BoldItalic.otf b/app/static/fonts/PublicSans-BoldItalic.otf new file mode 100644 index 0000000..718357f Binary files /dev/null and b/app/static/fonts/PublicSans-BoldItalic.otf differ diff --git a/app/static/fonts/PublicSans-ExtraBold.otf b/app/static/fonts/PublicSans-ExtraBold.otf new file mode 100644 index 0000000..09b52dd Binary files /dev/null and b/app/static/fonts/PublicSans-ExtraBold.otf differ diff --git a/app/static/fonts/PublicSans-ExtraBoldItalic.otf b/app/static/fonts/PublicSans-ExtraBoldItalic.otf new file mode 100644 index 0000000..5b33222 Binary files /dev/null and b/app/static/fonts/PublicSans-ExtraBoldItalic.otf differ diff --git a/app/static/fonts/PublicSans-ExtraLight.otf b/app/static/fonts/PublicSans-ExtraLight.otf new file mode 100644 index 0000000..49b407d Binary files /dev/null and b/app/static/fonts/PublicSans-ExtraLight.otf differ diff --git a/app/static/fonts/PublicSans-ExtraLightItalic.otf b/app/static/fonts/PublicSans-ExtraLightItalic.otf new file mode 100644 index 0000000..c76e855 Binary files /dev/null and b/app/static/fonts/PublicSans-ExtraLightItalic.otf differ diff --git a/app/static/fonts/PublicSans-Italic.otf b/app/static/fonts/PublicSans-Italic.otf new file mode 100644 index 0000000..38996ad Binary files /dev/null and b/app/static/fonts/PublicSans-Italic.otf differ diff --git a/app/static/fonts/PublicSans-Light.otf b/app/static/fonts/PublicSans-Light.otf new file mode 100644 index 0000000..126544e Binary files /dev/null and b/app/static/fonts/PublicSans-Light.otf differ diff --git a/app/static/fonts/PublicSans-LightItalic.otf b/app/static/fonts/PublicSans-LightItalic.otf new file mode 100644 index 0000000..1e6aa6f Binary files /dev/null and b/app/static/fonts/PublicSans-LightItalic.otf differ diff --git a/app/static/fonts/PublicSans-Medium.otf b/app/static/fonts/PublicSans-Medium.otf new file mode 100644 index 0000000..93507a5 Binary files /dev/null and b/app/static/fonts/PublicSans-Medium.otf differ diff --git a/app/static/fonts/PublicSans-MediumItalic.otf b/app/static/fonts/PublicSans-MediumItalic.otf new file mode 100644 index 0000000..f5ddb90 Binary files /dev/null and b/app/static/fonts/PublicSans-MediumItalic.otf differ diff --git a/app/static/fonts/PublicSans-Regular.otf b/app/static/fonts/PublicSans-Regular.otf new file mode 100644 index 0000000..d2b3f16 Binary files /dev/null and b/app/static/fonts/PublicSans-Regular.otf differ diff --git a/app/static/fonts/PublicSans-SemiBold.otf b/app/static/fonts/PublicSans-SemiBold.otf new file mode 100644 index 0000000..4ab6b89 Binary files /dev/null and b/app/static/fonts/PublicSans-SemiBold.otf differ diff --git a/app/static/fonts/PublicSans-SemiBoldItalic.otf b/app/static/fonts/PublicSans-SemiBoldItalic.otf new file mode 100644 index 0000000..a28f6c0 Binary files /dev/null and b/app/static/fonts/PublicSans-SemiBoldItalic.otf differ diff --git a/app/static/fonts/PublicSans-Thin.otf b/app/static/fonts/PublicSans-Thin.otf new file mode 100644 index 0000000..dee0ae2 Binary files /dev/null and b/app/static/fonts/PublicSans-Thin.otf differ diff --git a/app/static/fonts/PublicSans-ThinItalic.otf b/app/static/fonts/PublicSans-ThinItalic.otf new file mode 100644 index 0000000..c6b481a Binary files /dev/null and b/app/static/fonts/PublicSans-ThinItalic.otf differ diff --git a/app/static/styles/style.css b/app/static/styles/style.css new file mode 100644 index 0000000..328a1aa --- /dev/null +++ b/app/static/styles/style.css @@ -0,0 +1,192 @@ +/* -*- mode: web; -*- */ + + +:root { + --primary-color: #003B5C; + --secondary-color: #C3D7EE; + --home: #c8c8c8; + --yes: #80FF80; + --no: #FF8080; + font-size: 18; + --fast-speed: 0.2s; + --med-speed: 0.4s; + --slow-speed: 1s; +} + +body { + font-family: "Public Sans", sans-serif; + line-height: 1.2; + margin: 0; + padding: 0; +} + + +@font-face { + font-family: "Public Sans"; + src: url("/static/fonts/PublicSans-Regular.otf"); +} + +@font-face { + font-family: "Inconsolata"; + src: url("/static/fonts/Inconsolata.otf"); +} + + +h1 { + margin: 0.25em; +} + +nav { + /* display: flex; */ + background: darkgrey; + color: white; + /* margin: 0.5em; */ + padding: 0 0.5em; + /* justify-content: space-between; */ +} + +nav#user { + /* background: red; */ + display: flex; + justify-content: space-between; +} + +nav#user ul { + justify-content: end; +} + +nav#modules { + /* left: 0; */ +} + +nav#actions { + top: 0; + position: sticky; +} + + +nav ul { + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0.25em 0; + list-style: none; +} + +nav ul li { + margin: 0.25em; + /* margin: 0 0 0.5em 0; */ + /* padding: 0.5em 0; */ +} + + + + +.button { + display: inline-block; + padding: 0.5em; + background: dimgray; + color: white; + border-radius: 12px; + text-decoration: none; + border: 1px dimgray solid; +} + +.button-light { + background: white; + color: dimgray; +} + +.button:hover { + background: white; + color: black; + border: 1px dimgray solid; +} + +#content { + max-width: 60vw; + margin: 0 auto; +} + +table { + font-family: "Inconsolata"; + line-height: 1.5; + /* border-collapse: collapse; */ + /* margin: 2em auto; */ + width: 100%; +} + +table thead { + background: dimgray; + color: white; +} + +table tr:nth-child(even) { + background: lightgray; +} + +#flash { + position: fixed; + max-width: 16em; + bottom: 0; + right: 0; + padding: 0 0.5em; +} + +#flash ul { + margin: 0; + padding: 0.25em 0; + list-style-type: none; +} + +#flash ul li { + margin: 0.25em; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + transform: translateY(100%); + } + 100% { + opacity: 0.8; + transform: translateY(0); + } +} + +.alert { + padding: 1em; + margin: 0.5em; + border-radius: 8px; + /* border: 1px dimgray solid; */ + opacity: 0.8; + animation: 0.5s ease-out 0s 1 fadeIn; +} + +/* The default alert category */ +.alert-message { + background: cornflowerblue; + color: black; +} + +.alert-info { + background: darkblue; + color: white; +} + +.alert-error { + background: maroon; + color: white; +} +#downloads { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1em; +} +fieldset { + margin: 0 auto; + max-width: 16em; + display: flex; + flex-direction: column; +} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..7a63ddf --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,50 @@ +{# -*- mode: web; -*- #} + + + + + + + + mdl + + + + + + + +
+ {% block content %}{% endblock %} +
+ {# Flashed messages added last, so that they appear on top of the content. #} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+
+ {% endif %} + {% endwith %} + + + diff --git a/app/templates/home.html b/app/templates/home.html new file mode 100644 index 0000000..e4e690f --- /dev/null +++ b/app/templates/home.html @@ -0,0 +1,90 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} + +{% block title %} +{% if current_user.is_authenticated %} +Welcome, {{ user.name_first }} {{ user.name_last }}! +{% else %} +Welcome to mdl, the music downloader +{% endif %} +{% endblock %} + +{% block actions %} +{#
  • Download database
  • #} +{% endblock %} + +{% block content %} +{% if current_user.is_authenticated %} +
    +
    +

    New

    +
    +
    + Download audio from a URL to the server. + {% with form = form_download_remote %} + {{ form.csrf_token }} + {{ form.url.label() }} + {{ form.url() }} + {{ form.download_remote() }} + {% endwith %} +
    + +
    + {% if pending_files %} +
    +

    Pending

    + {% for file in pending_files %} +
    +
    + {{ file }} +
    + + {% endfor %} +
    + {% endif %} + {% if downloaded_files %} +
    +

    Finished

    + {% for file in downloaded_files %} +
    +
    + {{ file }} + {% with form = form_manage_remote %} + {{ form.csrf_token }} + {{ form.file_name(value=file) }} + {{ form.download_local() }} + {{ form.remove_remote() }} + {% endwith %} +
    + + {% endfor %} +
    + {% endif %} +
    +

    Download history

    +
    + + + + + + + + + + {% for file in download_history %} + + + + + + + {% endfor %} + +
    IDTitleDownloadedUser
    {{ file.primary_key }}{{ file.title }}{{ file.date_time_downloaded }}{{ file.user.username }}
    +{% else %} +

    You need to be logged in before using this web app.

    +{% endif %} + +{% endblock %} diff --git a/app/templates/modules/add-item.html b/app/templates/modules/add-item.html new file mode 100644 index 0000000..4eaee3d --- /dev/null +++ b/app/templates/modules/add-item.html @@ -0,0 +1,19 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} + + +{% block title %}Add {{ table }} item{% endblock %} +{% block content %} + +
    +
    + Add a new item to our {{ table }} table. + {% for field in form %} + {{ field.label() }}
    + {{ field() }}
    + {% endfor %} +
    +
    + +{% endblock %} diff --git a/app/templates/modules/invoices.html b/app/templates/modules/invoices.html new file mode 100644 index 0000000..9e8f765 --- /dev/null +++ b/app/templates/modules/invoices.html @@ -0,0 +1,80 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} +{% block title %} +Invoices +{% endblock %} + +{% block actions %} +
  • Add invoice
  • +
  • +{% endblock %} + +{% block content %} +Track your invoices and create new ones here.
    + +{# Pagination Links #} +{# gotten from https://betterprogramming.pub/simple-flask-pagination-example-4190b12c2e2e #} +
    + {# Loop through the number of pages to display a link for each #} + {% for page_num in invoices.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} + {% if page_num %} + {# Check for the active page and set the link to "Active" #} + {% if invoices.page == page_num %} + + {{ page_num }} + + {% else %} + + {{ page_num }} + + {% endif %} + {% else %} + ... + {% endif %} + {% endfor %} +
    + + + + + + + + + + + + + + + + + + {% for invoice in invoices.items %} + + + + + + + + + + + + + + {% endfor %} + +
    IDCreatedAlternative Invoice IDCustomer NameCustomer ReferenceDate BilledDate DueAmount (Net €)Amount (Gross €)Tax Amount (€)
    +
    + +
    +
    + +
    +
    {{ invoice.primary_key }}{{ invoice.date_time_created }}{{ invoice.invoice_id_alt }}{{ invoice.customer.name }}{{ invoice.customer_reference }}{{ invoice.date_billed }}{{ invoice.date_due }}{{ invoice.amount_net }}{{ invoice.amount_gross }}{{ invoice.amount_tax }}
    +{% endblock %} diff --git a/app/templates/modules/login.html b/app/templates/modules/login.html new file mode 100644 index 0000000..e66e6a4 --- /dev/null +++ b/app/templates/modules/login.html @@ -0,0 +1,16 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} + +{# the login form #} +{% block content %} +
    +
    + Login + {% for field in form %} + {{ field.label() }} + {{ field() }}
    + {% endfor %} +
    +
    +{% endblock %} diff --git a/app/templates/modules/register.html b/app/templates/modules/register.html new file mode 100644 index 0000000..76fc1e2 --- /dev/null +++ b/app/templates/modules/register.html @@ -0,0 +1,16 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} + +{# the register form #} +{% block content %} +
    +
    + Register + {% for field in form %} + {{ field.label() }} + {{ field() }}
    + {% endfor %} +
    +
    +{% endblock %} diff --git a/app/templates/modules/settings.html b/app/templates/modules/settings.html new file mode 100644 index 0000000..392ab47 --- /dev/null +++ b/app/templates/modules/settings.html @@ -0,0 +1,45 @@ +{# -*- mode: web; -*- #} + +{% extends "base.html" %} + +{% block title %} +Settings for user {{ current_user.username }} +{% endblock %} + +{% block content %} + +

    Welcome, {{ current_user.name_first }} {{ current_user.name_last }}!

    + +

    User profile

    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldValue
    Username{{ current_user.username }}
    First Name{{ current_user.name_first }}
    Last Name{{ current_user.name_last }}
    Last Updated{{ current_user.date_time_updated }}
    +{% endblock %} -- cgit v1.2.3