diff options
Diffstat (limited to 'app')
40 files changed, 983 insertions, 0 deletions
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/<file>", 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/<file>", 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"<User {self.name_first} {self.name_last}>" + + +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"<Download {self.user.title} for {self.user.first_name} downloaded {self.date_time_downloaded}>" 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/<module>/add/<table>", 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/<module>/edit/<table>/<int:pk>", 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/<module>/delete/<table>/<int:pk>", 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 Binary files differnew file mode 100644 index 0000000..1b0888c --- /dev/null +++ b/app/static/fonts/CourierPrime-Bold.ttf diff --git a/app/static/fonts/CourierPrime-BoldItalic.ttf b/app/static/fonts/CourierPrime-BoldItalic.ttf Binary files differnew file mode 100644 index 0000000..d4e7186 --- /dev/null +++ b/app/static/fonts/CourierPrime-BoldItalic.ttf diff --git a/app/static/fonts/CourierPrime-Italic.ttf b/app/static/fonts/CourierPrime-Italic.ttf Binary files differnew file mode 100644 index 0000000..75a1343 --- /dev/null +++ b/app/static/fonts/CourierPrime-Italic.ttf diff --git a/app/static/fonts/CourierPrime-Regular.ttf b/app/static/fonts/CourierPrime-Regular.ttf Binary files differnew file mode 100644 index 0000000..db4e6c1 --- /dev/null +++ b/app/static/fonts/CourierPrime-Regular.ttf diff --git a/app/static/fonts/Inconsolata.otf b/app/static/fonts/Inconsolata.otf Binary files differnew file mode 100644 index 0000000..3488898 --- /dev/null +++ b/app/static/fonts/Inconsolata.otf diff --git a/app/static/fonts/PublicSans-Black.otf b/app/static/fonts/PublicSans-Black.otf Binary files differnew file mode 100644 index 0000000..bbbaa26 --- /dev/null +++ b/app/static/fonts/PublicSans-Black.otf diff --git a/app/static/fonts/PublicSans-BlackItalic.otf b/app/static/fonts/PublicSans-BlackItalic.otf Binary files differnew file mode 100644 index 0000000..46e3f71 --- /dev/null +++ b/app/static/fonts/PublicSans-BlackItalic.otf diff --git a/app/static/fonts/PublicSans-Bold.otf b/app/static/fonts/PublicSans-Bold.otf Binary files differnew file mode 100644 index 0000000..7a2b62b --- /dev/null +++ b/app/static/fonts/PublicSans-Bold.otf diff --git a/app/static/fonts/PublicSans-BoldItalic.otf b/app/static/fonts/PublicSans-BoldItalic.otf Binary files differnew file mode 100644 index 0000000..718357f --- /dev/null +++ b/app/static/fonts/PublicSans-BoldItalic.otf diff --git a/app/static/fonts/PublicSans-ExtraBold.otf b/app/static/fonts/PublicSans-ExtraBold.otf Binary files differnew file mode 100644 index 0000000..09b52dd --- /dev/null +++ b/app/static/fonts/PublicSans-ExtraBold.otf diff --git a/app/static/fonts/PublicSans-ExtraBoldItalic.otf b/app/static/fonts/PublicSans-ExtraBoldItalic.otf Binary files differnew file mode 100644 index 0000000..5b33222 --- /dev/null +++ b/app/static/fonts/PublicSans-ExtraBoldItalic.otf diff --git a/app/static/fonts/PublicSans-ExtraLight.otf b/app/static/fonts/PublicSans-ExtraLight.otf Binary files differnew file mode 100644 index 0000000..49b407d --- /dev/null +++ b/app/static/fonts/PublicSans-ExtraLight.otf diff --git a/app/static/fonts/PublicSans-ExtraLightItalic.otf b/app/static/fonts/PublicSans-ExtraLightItalic.otf Binary files differnew file mode 100644 index 0000000..c76e855 --- /dev/null +++ b/app/static/fonts/PublicSans-ExtraLightItalic.otf diff --git a/app/static/fonts/PublicSans-Italic.otf b/app/static/fonts/PublicSans-Italic.otf Binary files differnew file mode 100644 index 0000000..38996ad --- /dev/null +++ b/app/static/fonts/PublicSans-Italic.otf diff --git a/app/static/fonts/PublicSans-Light.otf b/app/static/fonts/PublicSans-Light.otf Binary files differnew file mode 100644 index 0000000..126544e --- /dev/null +++ b/app/static/fonts/PublicSans-Light.otf diff --git a/app/static/fonts/PublicSans-LightItalic.otf b/app/static/fonts/PublicSans-LightItalic.otf Binary files differnew file mode 100644 index 0000000..1e6aa6f --- /dev/null +++ b/app/static/fonts/PublicSans-LightItalic.otf diff --git a/app/static/fonts/PublicSans-Medium.otf b/app/static/fonts/PublicSans-Medium.otf Binary files differnew file mode 100644 index 0000000..93507a5 --- /dev/null +++ b/app/static/fonts/PublicSans-Medium.otf diff --git a/app/static/fonts/PublicSans-MediumItalic.otf b/app/static/fonts/PublicSans-MediumItalic.otf Binary files differnew file mode 100644 index 0000000..f5ddb90 --- /dev/null +++ b/app/static/fonts/PublicSans-MediumItalic.otf diff --git a/app/static/fonts/PublicSans-Regular.otf b/app/static/fonts/PublicSans-Regular.otf Binary files differnew file mode 100644 index 0000000..d2b3f16 --- /dev/null +++ b/app/static/fonts/PublicSans-Regular.otf diff --git a/app/static/fonts/PublicSans-SemiBold.otf b/app/static/fonts/PublicSans-SemiBold.otf Binary files differnew file mode 100644 index 0000000..4ab6b89 --- /dev/null +++ b/app/static/fonts/PublicSans-SemiBold.otf diff --git a/app/static/fonts/PublicSans-SemiBoldItalic.otf b/app/static/fonts/PublicSans-SemiBoldItalic.otf Binary files differnew file mode 100644 index 0000000..a28f6c0 --- /dev/null +++ b/app/static/fonts/PublicSans-SemiBoldItalic.otf diff --git a/app/static/fonts/PublicSans-Thin.otf b/app/static/fonts/PublicSans-Thin.otf Binary files differnew file mode 100644 index 0000000..dee0ae2 --- /dev/null +++ b/app/static/fonts/PublicSans-Thin.otf diff --git a/app/static/fonts/PublicSans-ThinItalic.otf b/app/static/fonts/PublicSans-ThinItalic.otf Binary files differnew file mode 100644 index 0000000..c6b481a --- /dev/null +++ b/app/static/fonts/PublicSans-ThinItalic.otf 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; -*- #} + +<!doctype html> + +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to"> + <title>mdl</title> + <meta name="author" content="Marius Peter"> + <meta name="description" content="A draft page for mdl."> + <!-- <link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> --> + <link rel="stylesheet" href="{{ url_for('static', filename='styles/style.css') }}"> + </head> + <body> + <nav id="user"> + <div style="display: flex; align-items: baseline;"> + <ul> + <li><a href="https:apps.mlnp.fr" class="button">Apps</a></li> + </ul> + <h1>{% block title %}{% endblock %}</h1> + </div> + <ul> + {% if current_user.is_authenticated %} + <li><a href="{{ url_for('common.settings') }}" class="button">Settings</a></li> + <li><a href="{{ url_for('auth.logout') }}" class="button">Logout</a></li> + {% else %} + <li><a href="{{ url_for('auth.login') }}" class="button">Login</a></li> + <li><a href="{{ url_for('auth.register') }}" class="button">Register</a></li> + {% endif %} + </ul> + </nav> + <div id="content"> + {% block content %}{% endblock %} + </div> + {# Flashed messages added last, so that they appear on top of the content. #} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + <div id="flash"> + <ul> + {% for category, message in messages %} + <li class="alert alert-{{ category }}">{{ message }}</li> + {% endfor %} + </ul> + </div> + {% endif %} + {% endwith %} + <!-- <script src="js/scripts.js"></script> --> + </body> +</html> 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 <code>mdl</code>, the music downloader +{% endif %} +{% endblock %} + +{% block actions %} +{# <li><a href="{{ url_for('main.download_database') }}" class="button">Download database</a></li> #} +{% endblock %} + +{% block content %} +{% if current_user.is_authenticated %} +<div id="downloads"> + <div id="download-new"> + <h2>New</h2> + <form action="{{ url_for('main.download_remote') }}" method="POST"> + <fieldset> + <legend>Download audio from a URL to the server.</legend> + {% with form = form_download_remote %} + {{ form.csrf_token }} + {{ form.url.label() }} + {{ form.url() }} + {{ form.download_remote() }} + {% endwith %} + </fieldset> + </form> + </div> + {% if pending_files %} + <div id="download-pending"> + <h2>Pending</h2> + {% for file in pending_files %} + <form action="#" method="POST"> + <fieldset> + <legend>{{ file }}</legend> + </fieldset> + </form> + {% endfor %} + </div> + {% endif %} + {% if downloaded_files %} + <div id="download-finished"> + <h2>Finished</h2> + {% for file in downloaded_files %} + <form action="{{ url_for('main.manage_remote') }}" method="POST"> + <fieldset> + <legend>{{ file }}</legend> + {% with form = form_manage_remote %} + {{ form.csrf_token }} + {{ form.file_name(value=file) }} + {{ form.download_local() }} + {{ form.remove_remote() }} + {% endwith %} + </fieldset> + </form> + {% endfor %} + </div> + {% endif %} +</div> +<h2>Download history</h2> +<table> + <thead> + <tr> + <th>ID</th> + <th>Title</th> + <th>Downloaded</th> + <th>User</th> + </tr> + </thead> + <tbody> + {% for file in download_history %} + <tr> + <td>{{ file.primary_key }}</td> + <td><a href="{{ file.url }}">{{ file.title }}</a></td> + <td>{{ file.date_time_downloaded }}</td> + <td>{{ file.user.username }}</td> + </tr> + {% endfor %} + </tbody> +</table> +{% else %} +<p>You need to be logged in before using this web app.</p> +{% 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 %} + +<form action="{{ request.path }}" method="POST"> + <fieldset> + <legend>Add a new item to our {{ table }} table.</legend> + {% for field in form %} + {{ field.label() }}<br/> + {{ field() }}<br/> + {% endfor %} + </fieldset> +</form> + +{% 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 %} +<li><a href="{{ url_for('common.add_item', module='invoices', table='Invoice') }}" class="button">Add invoice</a></li> +<li></li> +{% endblock %} + +{% block content %} +<i>Track your invoices and create new ones here.</i><br/> + +{# Pagination Links #} +{# gotten from https://betterprogramming.pub/simple-flask-pagination-example-4190b12c2e2e #} +<center> + {# 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 %} + <a href="{{ url_for('invoices.view', page=page_num) }}" + class="button"> + {{ page_num }} + </a> + {% else %} + <a href="{{ url_for('invoices.view', page=page_num) }}" + class="button button-light"> + {{ page_num }} + </a> + {% endif %} + {% else %} + ... + {% endif %} + {% endfor %} +</center> +<table> + <thead> + <tr> + <th></th> + <th>ID</th> + <th>Created</th> + <th>Alternative Invoice ID</th> + <th>Customer Name</th> + <th>Customer Reference</th> + <th>Date Billed</th> + <th>Date Due</th> + <th>Amount (Net €)</th> + <th>Amount (Gross €)</th> + <th>Tax Amount (€)</th> + </tr> + </thead> + <tbody> + {% for invoice in invoices.items %} + <tr {% if invoice.archive == True %} style="color: dimgray" {% endif %}> + <td> + <form method="post" action="{{ url_for('common.edit_item', module='invoices', pk=invoice.primary_key, table='Invoice') }}"> + <button>archive</button> + </form> + <form method="get" action="{{ url_for('invoices.preview', pk=invoice.primary_key) }}"> + <button>preview</button> + </form> + </td> + <td>{{ invoice.primary_key }}</td> + <td>{{ invoice.date_time_created }}</td> + <td>{{ invoice.invoice_id_alt }}</td> + <td>{{ invoice.customer.name }}</td> + <td>{{ invoice.customer_reference }}</td> + <td>{{ invoice.date_billed }}</td> + <td>{{ invoice.date_due }}</td> + <td>{{ invoice.amount_net }}</td> + <td>{{ invoice.amount_gross }}</td> + <td>{{ invoice.amount_tax }}</td> + </tr> + {% endfor %} + </tbody> +</table> +{% 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 %} +<form action="{{ url_for('auth.login') }}" method="POST"> + <fieldset> + <legend>Login</legend> + {% for field in form %} + {{ field.label() }} + {{ field() }}<br/> + {% endfor %} + </fieldset> +</form> +{% 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 %} +<form action="{{ url_for('auth.register') }}" method="POST"> + <fieldset> + <legend>Register</legend> + {% for field in form %} + {{ field.label() }} + {{ field() }}<br/> + {% endfor %} + </fieldset> +</form> +{% 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 %} + +<p>Welcome, {{ current_user.name_first }} {{ current_user.name_last }}!</p> + +<h2>User profile</h2> + +<form method="post" action="{{ url_for('common.edit_item', module='settings', pk=current_user.primary_key, table='User' ) }}"> + <button>edit</button> +</form> + +<table> + <thead> + <tr> + <th>Field</th> + <th>Value</th> + </tr> + </thead> + <tbody> + <tr> + <td>Username</td> + <td>{{ current_user.username }}</td> + </tr> + <tr> + <td>First Name</td> + <td>{{ current_user.name_first }}</td> + </tr> + <tr> + <td>Last Name</td> + <td>{{ current_user.name_last }}</td> + </tr> + <tr> + <td> Last Updated</td> + <td>{{ current_user.date_time_updated }}</td> + </tr> + </tbody> +</table> +{% endblock %} |