First commit :baby:

Commit
51c9fed15381421c4b7e8ba95af60b5204483d50
Author
Marius Peter <marius.peter@tutanota.com>
Author date
Committer
Marius Peter <marius.peter@tutanota.com>
Committer date
Changed files
.gitignore
index 00000000..e4add26c 000000..100644
@@ -0,0 +1,4 @@
1 Added: # .gitignore for mdl
2 Added:
3 Added: .venv/
4 Added: downloads/
app/.gitignore
index 00000000..1ac0ed94 000000..100644
@@ -0,0 +1,4 @@
1 Added: # .gitignore for app
2 Added:
3 Added: *__pycache__
4 Added: *.db
app/__init__.py
index 00000000..bd180380 000000..100644
@@ -0,0 +1,42 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: """This is the application factory.
4 Added:
5 Added: When the mdl app package is imported, Flask uses the create_app
6 Added: function to instantiate the web app.
7 Added:
8 Added: """
9 Added:
10 Added: from flask import Flask
11 Added: from flask_sqlalchemy import SQLAlchemy
12 Added: from flask_login import LoginManager
13 Added:
14 Added: from .models import db
15 Added:
16 Added:
17 Added: def create_app():
18 Added: app = Flask(__name__)
19 Added: app.config.from_pyfile("../config.py")
20 Added:
21 Added: db.init_app(app)
22 Added:
23 Added: login_manager = LoginManager()
24 Added: login_manager.login_view = "auth.login"
25 Added: login_manager.init_app(app)
26 Added:
27 Added: from .models import User
28 Added:
29 Added: @login_manager.user_loader
30 Added: def load_user(user_id):
31 Added: return User.query.get(int(user_id))
32 Added:
33 Added: from .main import main
34 Added:
35 Added: app.register_blueprint(main)
36 Added:
37 Added: from .modules import common, auth
38 Added:
39 Added: app.register_blueprint(common)
40 Added: app.register_blueprint(auth)
41 Added:
42 Added: return app
app/forms.py
index 00000000..a093d5dc 000000..100644
@@ -0,0 +1,33 @@
1 Added: from flask_wtf import FlaskForm
2 Added: from wtforms import (
3 Added: SubmitField,
4 Added: SelectField,
5 Added: HiddenField,
6 Added: StringField,
7 Added: PasswordField,
8 Added: IntegerField,
9 Added: FloatField,
10 Added: BooleanField,
11 Added: DateTimeField,
12 Added: )
13 Added: from wtforms.validators import (
14 Added: InputRequired,
15 Added: Length,
16 Added: NumberRange,
17 Added: EqualTo,
18 Added: ValidationError,
19 Added: )
20 Added:
21 Added:
22 Added: class DownloadToRemote(FlaskForm):
23 Added: url = StringField("Link URL", validators=[InputRequired()])
24 Added: download_remote = SubmitField("Download on remote")
25 Added:
26 Added:
27 Added: class ManageRemote(FlaskForm):
28 Added: file_name = HiddenField()
29 Added: download_local = SubmitField("Download locally")
30 Added: remove_remote = SubmitField("Remove remote")
31 Added:
32 Added: # def __init__(self, name):
33 Added: # self.name = HiddenField(name)
app/main.py
index 00000000..149ef641 000000..100644
@@ -0,0 +1,147 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: """
4 Added: routes.py module
5 Added: ----------------
6 Added:
7 Added: This Python module contains the logic supporting:
8 Added: 1. Navigating between website pages
9 Added: 2. Interpreting user requests to the server
10 Added: 3. Dispatching requested content back to the user
11 Added:
12 Added: Python dependencies:
13 Added: - flask: provides web application features
14 Added: - forms: provides secure user form submission
15 Added: - sqlalchemy: provides communication with database on server.
16 Added:
17 Added: Personal imports:
18 Added: These are used to avoid cluttering this file with
19 Added: placeholder data for posts' content.
20 Added: """
21 Added:
22 Added: import os
23 Added: import time
24 Added: import glob
25 Added: from datetime import datetime
26 Added: from concurrent.futures import ThreadPoolExecutor
27 Added:
28 Added: from flask import (
29 Added: Blueprint,
30 Added: render_template,
31 Added: send_from_directory,
32 Added: request,
33 Added: redirect,
34 Added: flash,
35 Added: url_for,
36 Added: jsonify,
37 Added: abort,
38 Added: )
39 Added: from flask_login import login_required, current_user
40 Added: import youtube_dl
41 Added:
42 Added: from .models import db, Download
43 Added: from .forms import DownloadToRemote, ManageRemote
44 Added:
45 Added: main = Blueprint("main", __name__)
46 Added: executor = ThreadPoolExecutor(4)
47 Added:
48 Added:
49 Added: @main.route("/")
50 Added: @main.route("/index")
51 Added: @login_required
52 Added: def home():
53 Added: """Prompt for video URL."""
54 Added: # https://www.youtube.com/watch?v=86khmc6y1yE&list=RDGMEMYH9CUrFO7CfLJpaD7UR85w&index=13
55 Added: download_history = Download.query.order_by(Download.primary_key.desc()).all()
56 Added: downloaded_files = [
57 Added: file for file in os.listdir("downloads") if file.endswith((".mp3", ".m4a"))
58 Added: ]
59 Added: pending_files = [file for file in os.listdir("downloads") if file.endswith(".part")]
60 Added: return render_template(
61 Added: "home.html",
62 Added: user=current_user,
63 Added: form_download_remote=DownloadToRemote(),
64 Added: form_manage_remote=ManageRemote(),
65 Added: downloaded_files=downloaded_files,
66 Added: pending_files=pending_files,
67 Added: download_history=download_history,
68 Added: )
69 Added:
70 Added:
71 Added: @main.route("/download-remote", methods=["POST"])
72 Added: @login_required
73 Added: def download_remote():
74 Added: """Download audio from URL onto server."""
75 Added: form = DownloadToRemote()
76 Added: if form.validate_on_submit():
77 Added: url = request.form["url"]
78 Added: ydl_opts = {
79 Added: "format": "bestaudio/best",
80 Added: "outtmpl": os.path.join(
81 Added: os.getcwd(),
82 Added: "downloads",
83 Added: "%(title)s.%(ext)s",
84 Added: ),
85 Added: "noplaylist": True,
86 Added: "postprocessors": [
87 Added: {
88 Added: "key": "FFmpegExtractAudio",
89 Added: "preferredcodec": "mp3",
90 Added: "preferredquality": "192",
91 Added: }
92 Added: ],
93 Added: }
94 Added: executor.submit(youtube_dl.YoutubeDL(ydl_opts).download, [url])
95 Added: with youtube_dl.YoutubeDL(ydl_opts) as ydl:
96 Added: # time.sleep(1)
97 Added: info = ydl.extract_info(url, download=False)
98 Added: title = info.get("title")
99 Added: new_download = Download(
100 Added: title=title,
101 Added: url=url,
102 Added: user_id=current_user.primary_key,
103 Added: )
104 Added: db.session.add(new_download)
105 Added: db.session.commit()
106 Added: flash(f"Successfully started downloading {title}.")
107 Added: return redirect("/index")
108 Added: flash(f"Couldn't download {title}.")
109 Added: return redirect("/index")
110 Added:
111 Added:
112 Added: @main.route("/manage-remote/", methods=["POST"])
113 Added: @login_required
114 Added: def manage_remote():
115 Added: """Manage all files downloaded on remote device.
116 Added:
117 Added: The value of the submit button pressed is checked, then the
118 Added: appropriate redirection is performed.
119 Added:
120 Added: """
121 Added: form = ManageRemote()
122 Added: if form.validate_on_submit():
123 Added: file_name = request.form.get("file_name")
124 Added: if form.download_local.data:
125 Added: return redirect(url_for("main.download_local", file=file_name))
126 Added: elif form.remove_remote.data:
127 Added: return redirect(url_for("main.remove_remote", file=file_name))
128 Added: flash("Couldn't manage remote file.", "error")
129 Added: return redirect("/index")
130 Added:
131 Added:
132 Added: @main.route("/download-local/<file>", methods=["GET", "POST"])
133 Added: @login_required
134 Added: def download_local(file):
135 Added: """Download file from remote to local device."""
136 Added: downloads = os.path.join(os.getcwd(), "downloads")
137 Added: return send_from_directory(downloads, file, as_attachment=True)
138 Added:
139 Added:
140 Added: @main.route("/remove-remote/<file>", methods=["GET", "POST"])
141 Added: @login_required
142 Added: def remove_remote(file):
143 Added: """Remove file from remote device."""
144 Added: file_to_remove = os.path.join(os.getcwd(), "downloads", file)
145 Added: os.remove(file_to_remove)
146 Added: flash(f"Successfully removed file {file}.")
147 Added: return redirect("/index")
app/models.py
index 00000000..5b61045a 000000..100644
@@ -0,0 +1,63 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added:
4 Added: from flask_sqlalchemy import SQLAlchemy
5 Added: from sqlalchemy.sql import func
6 Added:
7 Added: from flask_login import UserMixin
8 Added:
9 Added: db = SQLAlchemy()
10 Added:
11 Added:
12 Added: class User(UserMixin, db.Model):
13 Added: """UserMixin inheritance required for features described here:
14 Added:
15 Added: https://stackoverflow.com/questions/63231163/what-is-the-usermixin-in-flask"""
16 Added:
17 Added: __tablename__ = "User"
18 Added:
19 Added: def get_id(self):
20 Added: return self.primary_key
21 Added:
22 Added: primary_key = db.Column("UserId", db.Integer, primary_key=True)
23 Added: username = db.Column("Username", db.String(20), nullable=False)
24 Added: hashed_password = db.Column("HashedPassword", db.String(100), nullable=False)
25 Added: name_first = db.Column("NameFirst", db.String(20), nullable=False)
26 Added: name_last = db.Column("NameLast", db.String(20), nullable=False)
27 Added: date_time_created = db.Column(
28 Added: "DateTimeCreated", db.String, server_default=func.now()
29 Added: )
30 Added: date_time_updated = db.Column(
31 Added: "DateTimeUpdated", db.String, server_onupdate=func.now()
32 Added: )
33 Added: downloads = db.relationship("Download", back_populates="user")
34 Added:
35 Added: def __repr__(self):
36 Added: return f"<User {self.name_first} {self.name_last}>"
37 Added:
38 Added:
39 Added: class Download(db.Model):
40 Added: """One record per file downloaded."""
41 Added:
42 Added: __tablename__ = "Download"
43 Added: primary_key = db.Column("DownloadId", db.Integer, primary_key=True)
44 Added: title = db.Column("Title", db.String(20), nullable=False)
45 Added: url = db.Column("URL", db.String, nullable=False)
46 Added: date_time_downloaded = db.Column(
47 Added: "DateTimeDownloaded", db.String, server_default=func.now()
48 Added: )
49 Added: user_id = db.Column("UserId", db.Integer, db.ForeignKey("User.UserId"))
50 Added: user = db.relationship("User", back_populates="downloads")
51 Added:
52 Added: def __init__(
53 Added: self,
54 Added: title,
55 Added: url,
56 Added: user_id,
57 Added: ):
58 Added: self.title = title
59 Added: self.url = url
60 Added: self.user_id = user_id
61 Added:
62 Added: def __repr__(self):
63 Added: return f"<Download {self.user.title} for {self.user.first_name} downloaded {self.date_time_downloaded}>"
app/modules/__init__.py
index 00000000..fcd1d43e 000000..100644
@@ -0,0 +1,4 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: from .common import common
4 Added: from .auth.routes import auth
app/modules/auth/forms.py
index 00000000..a4b45556 000000..100644
@@ -0,0 +1,34 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added:
4 Added: from flask_wtf import FlaskForm
5 Added: from wtforms import (
6 Added: SubmitField,
7 Added: HiddenField,
8 Added: StringField,
9 Added: PasswordField,
10 Added: BooleanField,
11 Added: )
12 Added: from wtforms.validators import (
13 Added: InputRequired,
14 Added: Length,
15 Added: ValidationError,
16 Added: )
17 Added:
18 Added:
19 Added: class LoginForm(FlaskForm):
20 Added: username = StringField("Username", validators=[InputRequired()])
21 Added: password = PasswordField("Password", validators=[InputRequired()])
22 Added: remember = BooleanField("Remember")
23 Added: submit = SubmitField("Login")
24 Added:
25 Added:
26 Added: class RegisterForm(LoginForm):
27 Added: def validate_invite_code(self, field):
28 Added: if field.data != "mdltesters2022":
29 Added: raise ValidationError("Invitation code does not match")
30 Added:
31 Added: invitation_code = StringField("Invitation code", validators=[InputRequired()])
32 Added: name_first = StringField("First name", validators=[InputRequired()])
33 Added: name_last = StringField("Last name", validators=[InputRequired()])
34 Added: submit = SubmitField("Register")
app/modules/auth/routes.py
index 00000000..db59bf17 000000..100644
@@ -0,0 +1,73 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: from flask import Blueprint, render_template, redirect, url_for, request, flash
4 Added: from flask_login import login_user, login_required, logout_user
5 Added: from werkzeug.security import generate_password_hash, check_password_hash
6 Added:
7 Added: from ... import db
8 Added: from ...models import User
9 Added: from .forms import LoginForm, RegisterForm
10 Added:
11 Added:
12 Added: auth = Blueprint("auth", __name__)
13 Added:
14 Added:
15 Added: @auth.route("/login", methods=["GET", "POST"])
16 Added: def login():
17 Added: form = LoginForm()
18 Added: if form.validate_on_submit():
19 Added: req = request.form
20 Added: # print(req["remember"])
21 Added: remember = True if req.get("remember") else False
22 Added: user = User.query.filter_by(username=req["username"]).first()
23 Added: if user is None:
24 Added: flash("User not registered.", "error")
25 Added: return redirect(url_for("auth.register"))
26 Added: if check_password_hash(user.hashed_password, req["password"]) is False:
27 Added: flash("Wrong password.", "error")
28 Added: return redirect(url_for("auth.login"))
29 Added: login_user(user, remember=remember)
30 Added: flash(
31 Added: f"Logged in as user {user.username} successfully. "
32 Added: + f"You will{' not ' if remember is False else ' '}be remembered next time!"
33 Added: )
34 Added: return redirect(url_for("main.home"))
35 Added: return render_template("modules/login.html", form=form)
36 Added:
37 Added:
38 Added: @auth.route("/register", methods=["GET", "POST"])
39 Added: def register():
40 Added: form = RegisterForm()
41 Added: if form.validate_on_submit():
42 Added: req = request.form
43 Added: user_already_exists = User.query.filter_by(
44 Added: name_first=req["name_first"],
45 Added: name_last=req["name_last"],
46 Added: ).first()
47 Added: if user_already_exists:
48 Added: flash(
49 Added: f"User {req['name_first']} {req['name_last']} already exists.", "error"
50 Added: )
51 Added: return redirect(url_for("auth.login"))
52 Added: if req["invitation_code"] != "mdltesters2022":
53 Added: flash("Wrong invitation code.", "error")
54 Added: return redirect(url_for("auth.register"))
55 Added: new_user = User(
56 Added: username=req["username"],
57 Added: hashed_password=generate_password_hash(req["password"], method="sha256"),
58 Added: name_first=req["name_first"],
59 Added: name_last=req["name_last"],
60 Added: )
61 Added: db.session.add(new_user)
62 Added: db.session.commit()
63 Added: flash(f"Created user {req['name_first']} {req['name_last']} successfully.")
64 Added: return redirect(url_for("main.home"))
65 Added: return render_template("modules/register.html", form=form)
66 Added:
67 Added:
68 Added: @auth.route("/logout")
69 Added: @login_required
70 Added: def logout():
71 Added: logout_user()
72 Added: flash(f"Logged out successfully.")
73 Added: return redirect(url_for("main.home"))
app/modules/common.py
index 00000000..12508780 000000..100644
@@ -0,0 +1,75 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: import inspect
4 Added: from flask import Blueprint, request, render_template, redirect, flash, jsonify
5 Added: from flask_login import login_required, current_user
6 Added:
7 Added: from .. import db
8 Added: from .. import models
9 Added: # from . import forms
10 Added:
11 Added: from wtforms import SelectField
12 Added:
13 Added:
14 Added: common = Blueprint("common", __name__)
15 Added:
16 Added:
17 Added: @common.route("/modules/<module>/add/<table>", methods=["GET", "POST"])
18 Added: @login_required
19 Added: def add_item(module, table):
20 Added: """Add new item to table accessible via module."""
21 Added: # print("db table keys are", db.metadata.tables.keys())
22 Added: if table not in db.metadata.tables.keys():
23 Added: return render_template("errors/item-not-found.html", table=table)
24 Added: form = getattr(forms, f"Add{table}")()
25 Added: if form.validate_on_submit():
26 Added: model = getattr(models, table)
27 Added: table_fields = inspect.signature(model).parameters
28 Added: form_values = {key: request.form.get(key) for key in table_fields}
29 Added: print(f"Ready to insert in {table} from {module} {form_values}")
30 Added: record = model(**form_values)
31 Added: db.session.add(record)
32 Added: db.session.commit()
33 Added: item_pk = model.query.order_by(model.primary_key.desc()).first().primary_key
34 Added: flash(f"Successfully added item #{item_pk} to {table} table.", "info")
35 Added: return redirect(f"/modules/{module}")
36 Added: return render_template("modules/add-item.html", table=table, form=form)
37 Added:
38 Added:
39 Added: @common.route("/modules/<module>/edit/<table>/<int:pk>", methods=["GET", "POST"])
40 Added: @login_required
41 Added: def edit_item(module, table, pk):
42 Added: """Edit existing item in table accessible via module."""
43 Added: if table not in db.metadata.tables.keys():
44 Added: return render_template("errors/item-not-found.html", table=table)
45 Added: model = getattr(models, table)
46 Added: item = model.query.filter_by(primary_key=pk).first()
47 Added: # Instantiate form with selected item's field values.
48 Added: form = getattr(forms, f"Add{table}")(**item.__dict__)
49 Added: if form.validate_on_submit():
50 Added: table_fields = inspect.signature(model).parameters
51 Added: form_values = {key: request.form.get(key) for key in table_fields}
52 Added: print(f"Ready to update {form_values}")
53 Added: model.query.filter_by(primary_key=pk).update(form_values)
54 Added: db.session.commit()
55 Added: flash(f"Successfully edited item #{pk} in {table} table.", "info")
56 Added: return redirect(f"/modules/{module}")
57 Added: return render_template("modules/edit-item.html", table=table, pk=pk, form=form)
58 Added:
59 Added:
60 Added: @common.route("/modules/<module>/delete/<table>/<int:pk>", methods=["POST"])
61 Added: @login_required
62 Added: def delete_item(module, table, pk):
63 Added: """Delete item with Primary Key = pk from table in module."""
64 Added: model = getattr(models, table)
65 Added: record = model.query.filter_by(primary_key=pk).first()
66 Added: db.session.delete(record)
67 Added: db.session.commit()
68 Added: flash(f"Successfully removed item #{pk} from {table} table.", "info")
69 Added: return redirect(f"/modules/{module}")
70 Added:
71 Added:
72 Added: @common.route("/modules/settings")
73 Added: @login_required
74 Added: def settings():
75 Added: return render_template("modules/settings.html", user=current_user)
app/static/fonts/CourierPrime-Bold.ttf
index 00000000..1b0888c2 000000..100644

Binary files differ

app/static/fonts/CourierPrime-BoldItalic.ttf
index 00000000..d4e71869 000000..100644

Binary files differ

app/static/fonts/CourierPrime-Italic.ttf
index 00000000..75a1343d 000000..100644

Binary files differ

app/static/fonts/CourierPrime-Regular.ttf
index 00000000..db4e6c14 000000..100644

Binary files differ

app/static/fonts/Inconsolata.otf
index 00000000..34888982 000000..100644

Binary files differ

app/static/fonts/PublicSans-Black.otf
index 00000000..bbbaa26f 000000..100644

Binary files differ

app/static/fonts/PublicSans-BlackItalic.otf
index 00000000..46e3f71e 000000..100644

Binary files differ

app/static/fonts/PublicSans-Bold.otf
index 00000000..7a2b62bd 000000..100644

Binary files differ

app/static/fonts/PublicSans-BoldItalic.otf
index 00000000..718357f3 000000..100644

Binary files differ

app/static/fonts/PublicSans-ExtraBold.otf
index 00000000..09b52ddf 000000..100644

Binary files differ

app/static/fonts/PublicSans-ExtraBoldItalic.otf
index 00000000..5b332223 000000..100644

Binary files differ

app/static/fonts/PublicSans-ExtraLight.otf
index 00000000..49b407d8 000000..100644

Binary files differ

app/static/fonts/PublicSans-ExtraLightItalic.otf
index 00000000..c76e8558 000000..100644

Binary files differ

app/static/fonts/PublicSans-Italic.otf
index 00000000..38996ad9 000000..100644

Binary files differ

app/static/fonts/PublicSans-Light.otf
index 00000000..126544eb 000000..100644

Binary files differ

app/static/fonts/PublicSans-LightItalic.otf
index 00000000..1e6aa6f1 000000..100644

Binary files differ

app/static/fonts/PublicSans-Medium.otf
index 00000000..93507a5e 000000..100644

Binary files differ

app/static/fonts/PublicSans-MediumItalic.otf
index 00000000..f5ddb908 000000..100644

Binary files differ

app/static/fonts/PublicSans-Regular.otf
index 00000000..d2b3f169 000000..100644

Binary files differ

app/static/fonts/PublicSans-SemiBold.otf
index 00000000..4ab6b890 000000..100644

Binary files differ

app/static/fonts/PublicSans-SemiBoldItalic.otf
index 00000000..a28f6c0c 000000..100644

Binary files differ

app/static/fonts/PublicSans-Thin.otf
index 00000000..dee0ae29 000000..100644

Binary files differ

app/static/fonts/PublicSans-ThinItalic.otf
index 00000000..c6b481a0 000000..100644

Binary files differ

app/static/styles/style.css
index 00000000..328a1aad 000000..100644
@@ -0,0 +1,192 @@
1 Added: /* -*- mode: web; -*- */
2 Added:
3 Added:
4 Added: :root {
5 Added: --primary-color: #003B5C;
6 Added: --secondary-color: #C3D7EE;
7 Added: --home: #c8c8c8;
8 Added: --yes: #80FF80;
9 Added: --no: #FF8080;
10 Added: font-size: 18;
11 Added: --fast-speed: 0.2s;
12 Added: --med-speed: 0.4s;
13 Added: --slow-speed: 1s;
14 Added: }
15 Added:
16 Added: body {
17 Added: font-family: "Public Sans", sans-serif;
18 Added: line-height: 1.2;
19 Added: margin: 0;
20 Added: padding: 0;
21 Added: }
22 Added:
23 Added:
24 Added: @font-face {
25 Added: font-family: "Public Sans";
26 Added: src: url("/static/fonts/PublicSans-Regular.otf");
27 Added: }
28 Added:
29 Added: @font-face {
30 Added: font-family: "Inconsolata";
31 Added: src: url("/static/fonts/Inconsolata.otf");
32 Added: }
33 Added:
34 Added:
35 Added: h1 {
36 Added: margin: 0.25em;
37 Added: }
38 Added:
39 Added: nav {
40 Added: /* display: flex; */
41 Added: background: darkgrey;
42 Added: color: white;
43 Added: /* margin: 0.5em; */
44 Added: padding: 0 0.5em;
45 Added: /* justify-content: space-between; */
46 Added: }
47 Added:
48 Added: nav#user {
49 Added: /* background: red; */
50 Added: display: flex;
51 Added: justify-content: space-between;
52 Added: }
53 Added:
54 Added: nav#user ul {
55 Added: justify-content: end;
56 Added: }
57 Added:
58 Added: nav#modules {
59 Added: /* left: 0; */
60 Added: }
61 Added:
62 Added: nav#actions {
63 Added: top: 0;
64 Added: position: sticky;
65 Added: }
66 Added:
67 Added:
68 Added: nav ul {
69 Added: display: flex;
70 Added: flex-wrap: wrap;
71 Added: margin: 0;
72 Added: padding: 0.25em 0;
73 Added: list-style: none;
74 Added: }
75 Added:
76 Added: nav ul li {
77 Added: margin: 0.25em;
78 Added: /* margin: 0 0 0.5em 0; */
79 Added: /* padding: 0.5em 0; */
80 Added: }
81 Added:
82 Added:
83 Added:
84 Added:
85 Added: .button {
86 Added: display: inline-block;
87 Added: padding: 0.5em;
88 Added: background: dimgray;
89 Added: color: white;
90 Added: border-radius: 12px;
91 Added: text-decoration: none;
92 Added: border: 1px dimgray solid;
93 Added: }
94 Added:
95 Added: .button-light {
96 Added: background: white;
97 Added: color: dimgray;
98 Added: }
99 Added:
100 Added: .button:hover {
101 Added: background: white;
102 Added: color: black;
103 Added: border: 1px dimgray solid;
104 Added: }
105 Added:
106 Added: #content {
107 Added: max-width: 60vw;
108 Added: margin: 0 auto;
109 Added: }
110 Added:
111 Added: table {
112 Added: font-family: "Inconsolata";
113 Added: line-height: 1.5;
114 Added: /* border-collapse: collapse; */
115 Added: /* margin: 2em auto; */
116 Added: width: 100%;
117 Added: }
118 Added:
119 Added: table thead {
120 Added: background: dimgray;
121 Added: color: white;
122 Added: }
123 Added:
124 Added: table tr:nth-child(even) {
125 Added: background: lightgray;
126 Added: }
127 Added:
128 Added: #flash {
129 Added: position: fixed;
130 Added: max-width: 16em;
131 Added: bottom: 0;
132 Added: right: 0;
133 Added: padding: 0 0.5em;
134 Added: }
135 Added:
136 Added: #flash ul {
137 Added: margin: 0;
138 Added: padding: 0.25em 0;
139 Added: list-style-type: none;
140 Added: }
141 Added:
142 Added: #flash ul li {
143 Added: margin: 0.25em;
144 Added: }
145 Added:
146 Added: @keyframes fadeIn {
147 Added: 0% {
148 Added: opacity: 0;
149 Added: transform: translateY(100%);
150 Added: }
151 Added: 100% {
152 Added: opacity: 0.8;
153 Added: transform: translateY(0);
154 Added: }
155 Added: }
156 Added:
157 Added: .alert {
158 Added: padding: 1em;
159 Added: margin: 0.5em;
160 Added: border-radius: 8px;
161 Added: /* border: 1px dimgray solid; */
162 Added: opacity: 0.8;
163 Added: animation: 0.5s ease-out 0s 1 fadeIn;
164 Added: }
165 Added:
166 Added: /* The default alert category */
167 Added: .alert-message {
168 Added: background: cornflowerblue;
169 Added: color: black;
170 Added: }
171 Added:
172 Added: .alert-info {
173 Added: background: darkblue;
174 Added: color: white;
175 Added: }
176 Added:
177 Added: .alert-error {
178 Added: background: maroon;
179 Added: color: white;
180 Added: }
181 Added: #downloads {
182 Added: display: flex;
183 Added: flex-wrap: wrap;
184 Added: justify-content: space-between;
185 Added: gap: 1em;
186 Added: }
187 Added: fieldset {
188 Added: margin: 0 auto;
189 Added: max-width: 16em;
190 Added: display: flex;
191 Added: flex-direction: column;
192 Added: }
app/templates/base.html
index 00000000..7a63ddf5 000000..100644
@@ -0,0 +1,50 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: <!doctype html>
4 Added:
5 Added: <html lang="en">
6 Added: <head>
7 Added: <meta charset="utf-8">
8 Added: <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to">
9 Added: <title>mdl</title>
10 Added: <meta name="author" content="Marius Peter">
11 Added: <meta name="description" content="A draft page for mdl.">
12 Added: <!-- <link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> -->
13 Added: <link rel="stylesheet" href="{{ url_for('static', filename='styles/style.css') }}">
14 Added: </head>
15 Added: <body>
16 Added: <nav id="user">
17 Added: <div style="display: flex; align-items: baseline;">
18 Added: <ul>
19 Added: <li><a href="https:apps.mlnp.fr" class="button">Apps</a></li>
20 Added: </ul>
21 Added: <h1>{% block title %}{% endblock %}</h1>
22 Added: </div>
23 Added: <ul>
24 Added: {% if current_user.is_authenticated %}
25 Added: <li><a href="{{ url_for('common.settings') }}" class="button">Settings</a></li>
26 Added: <li><a href="{{ url_for('auth.logout') }}" class="button">Logout</a></li>
27 Added: {% else %}
28 Added: <li><a href="{{ url_for('auth.login') }}" class="button">Login</a></li>
29 Added: <li><a href="{{ url_for('auth.register') }}" class="button">Register</a></li>
30 Added: {% endif %}
31 Added: </ul>
32 Added: </nav>
33 Added: <div id="content">
34 Added: {% block content %}{% endblock %}
35 Added: </div>
36 Added: {# Flashed messages added last, so that they appear on top of the content. #}
37 Added: {% with messages = get_flashed_messages(with_categories=true) %}
38 Added: {% if messages %}
39 Added: <div id="flash">
40 Added: <ul>
41 Added: {% for category, message in messages %}
42 Added: <li class="alert alert-{{ category }}">{{ message }}</li>
43 Added: {% endfor %}
44 Added: </ul>
45 Added: </div>
46 Added: {% endif %}
47 Added: {% endwith %}
48 Added: <!-- <script src="js/scripts.js"></script> -->
49 Added: </body>
50 Added: </html>
app/templates/home.html
index 00000000..e4e690f0 000000..100644
@@ -0,0 +1,90 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added:
5 Added: {% block title %}
6 Added: {% if current_user.is_authenticated %}
7 Added: Welcome, {{ user.name_first }} {{ user.name_last }}!
8 Added: {% else %}
9 Added: Welcome to <code>mdl</code>, the music downloader
10 Added: {% endif %}
11 Added: {% endblock %}
12 Added:
13 Added: {% block actions %}
14 Added: {# <li><a href="{{ url_for('main.download_database') }}" class="button">Download database</a></li> #}
15 Added: {% endblock %}
16 Added:
17 Added: {% block content %}
18 Added: {% if current_user.is_authenticated %}
19 Added: <div id="downloads">
20 Added: <div id="download-new">
21 Added: <h2>New</h2>
22 Added: <form action="{{ url_for('main.download_remote') }}" method="POST">
23 Added: <fieldset>
24 Added: <legend>Download audio from a URL to the server.</legend>
25 Added: {% with form = form_download_remote %}
26 Added: {{ form.csrf_token }}
27 Added: {{ form.url.label() }}
28 Added: {{ form.url() }}
29 Added: {{ form.download_remote() }}
30 Added: {% endwith %}
31 Added: </fieldset>
32 Added: </form>
33 Added: </div>
34 Added: {% if pending_files %}
35 Added: <div id="download-pending">
36 Added: <h2>Pending</h2>
37 Added: {% for file in pending_files %}
38 Added: <form action="#" method="POST">
39 Added: <fieldset>
40 Added: <legend>{{ file }}</legend>
41 Added: </fieldset>
42 Added: </form>
43 Added: {% endfor %}
44 Added: </div>
45 Added: {% endif %}
46 Added: {% if downloaded_files %}
47 Added: <div id="download-finished">
48 Added: <h2>Finished</h2>
49 Added: {% for file in downloaded_files %}
50 Added: <form action="{{ url_for('main.manage_remote') }}" method="POST">
51 Added: <fieldset>
52 Added: <legend>{{ file }}</legend>
53 Added: {% with form = form_manage_remote %}
54 Added: {{ form.csrf_token }}
55 Added: {{ form.file_name(value=file) }}
56 Added: {{ form.download_local() }}
57 Added: {{ form.remove_remote() }}
58 Added: {% endwith %}
59 Added: </fieldset>
60 Added: </form>
61 Added: {% endfor %}
62 Added: </div>
63 Added: {% endif %}
64 Added: </div>
65 Added: <h2>Download history</h2>
66 Added: <table>
67 Added: <thead>
68 Added: <tr>
69 Added: <th>ID</th>
70 Added: <th>Title</th>
71 Added: <th>Downloaded</th>
72 Added: <th>User</th>
73 Added: </tr>
74 Added: </thead>
75 Added: <tbody>
76 Added: {% for file in download_history %}
77 Added: <tr>
78 Added: <td>{{ file.primary_key }}</td>
79 Added: <td><a href="{{ file.url }}">{{ file.title }}</a></td>
80 Added: <td>{{ file.date_time_downloaded }}</td>
81 Added: <td>{{ file.user.username }}</td>
82 Added: </tr>
83 Added: {% endfor %}
84 Added: </tbody>
85 Added: </table>
86 Added: {% else %}
87 Added: <p>You need to be logged in before using this web app.</p>
88 Added: {% endif %}
89 Added:
90 Added: {% endblock %}
app/templates/modules/add-item.html
index 00000000..4eaee3d3 000000..100644
@@ -0,0 +1,19 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added:
5 Added:
6 Added: {% block title %}Add {{ table }} item{% endblock %}
7 Added: {% block content %}
8 Added:
9 Added: <form action="{{ request.path }}" method="POST">
10 Added: <fieldset>
11 Added: <legend>Add a new item to our {{ table }} table.</legend>
12 Added: {% for field in form %}
13 Added: {{ field.label() }}<br/>
14 Added: {{ field() }}<br/>
15 Added: {% endfor %}
16 Added: </fieldset>
17 Added: </form>
18 Added:
19 Added: {% endblock %}
app/templates/modules/invoices.html
index 00000000..9e8f7655 000000..100644
@@ -0,0 +1,80 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added: {% block title %}
5 Added: Invoices
6 Added: {% endblock %}
7 Added:
8 Added: {% block actions %}
9 Added: <li><a href="{{ url_for('common.add_item', module='invoices', table='Invoice') }}" class="button">Add invoice</a></li>
10 Added: <li></li>
11 Added: {% endblock %}
12 Added:
13 Added: {% block content %}
14 Added: <i>Track your invoices and create new ones here.</i><br/>
15 Added:
16 Added: {# Pagination Links #}
17 Added: {# gotten from https://betterprogramming.pub/simple-flask-pagination-example-4190b12c2e2e #}
18 Added: <center>
19 Added: {# Loop through the number of pages to display a link for each #}
20 Added: {% for page_num in invoices.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
21 Added: {% if page_num %}
22 Added: {# Check for the active page and set the link to "Active" #}
23 Added: {% if invoices.page == page_num %}
24 Added: <a href="{{ url_for('invoices.view', page=page_num) }}"
25 Added: class="button">
26 Added: {{ page_num }}
27 Added: </a>
28 Added: {% else %}
29 Added: <a href="{{ url_for('invoices.view', page=page_num) }}"
30 Added: class="button button-light">
31 Added: {{ page_num }}
32 Added: </a>
33 Added: {% endif %}
34 Added: {% else %}
35 Added: ...
36 Added: {% endif %}
37 Added: {% endfor %}
38 Added: </center>
39 Added: <table>
40 Added: <thead>
41 Added: <tr>
42 Added: <th></th>
43 Added: <th>ID</th>
44 Added: <th>Created</th>
45 Added: <th>Alternative Invoice ID</th>
46 Added: <th>Customer Name</th>
47 Added: <th>Customer Reference</th>
48 Added: <th>Date Billed</th>
49 Added: <th>Date Due</th>
50 Added: <th>Amount (Net €)</th>
51 Added: <th>Amount (Gross €)</th>
52 Added: <th>Tax Amount (€)</th>
53 Added: </tr>
54 Added: </thead>
55 Added: <tbody>
56 Added: {% for invoice in invoices.items %}
57 Added: <tr {% if invoice.archive == True %} style="color: dimgray" {% endif %}>
58 Added: <td>
59 Added: <form method="post" action="{{ url_for('common.edit_item', module='invoices', pk=invoice.primary_key, table='Invoice') }}">
60 Added: <button>archive</button>
61 Added: </form>
62 Added: <form method="get" action="{{ url_for('invoices.preview', pk=invoice.primary_key) }}">
63 Added: <button>preview</button>
64 Added: </form>
65 Added: </td>
66 Added: <td>{{ invoice.primary_key }}</td>
67 Added: <td>{{ invoice.date_time_created }}</td>
68 Added: <td>{{ invoice.invoice_id_alt }}</td>
69 Added: <td>{{ invoice.customer.name }}</td>
70 Added: <td>{{ invoice.customer_reference }}</td>
71 Added: <td>{{ invoice.date_billed }}</td>
72 Added: <td>{{ invoice.date_due }}</td>
73 Added: <td>{{ invoice.amount_net }}</td>
74 Added: <td>{{ invoice.amount_gross }}</td>
75 Added: <td>{{ invoice.amount_tax }}</td>
76 Added: </tr>
77 Added: {% endfor %}
78 Added: </tbody>
79 Added: </table>
80 Added: {% endblock %}
app/templates/modules/login.html
index 00000000..e66e6a45 000000..100644
@@ -0,0 +1,16 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added:
5 Added: {# the login form #}
6 Added: {% block content %}
7 Added: <form action="{{ url_for('auth.login') }}" method="POST">
8 Added: <fieldset>
9 Added: <legend>Login</legend>
10 Added: {% for field in form %}
11 Added: {{ field.label() }}
12 Added: {{ field() }}<br/>
13 Added: {% endfor %}
14 Added: </fieldset>
15 Added: </form>
16 Added: {% endblock %}
app/templates/modules/register.html
index 00000000..76fc1e26 000000..100644
@@ -0,0 +1,16 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added:
5 Added: {# the register form #}
6 Added: {% block content %}
7 Added: <form action="{{ url_for('auth.register') }}" method="POST">
8 Added: <fieldset>
9 Added: <legend>Register</legend>
10 Added: {% for field in form %}
11 Added: {{ field.label() }}
12 Added: {{ field() }}<br/>
13 Added: {% endfor %}
14 Added: </fieldset>
15 Added: </form>
16 Added: {% endblock %}
app/templates/modules/settings.html
index 00000000..392ab475 000000..100644
@@ -0,0 +1,45 @@
1 Added: {# -*- mode: web; -*- #}
2 Added:
3 Added: {% extends "base.html" %}
4 Added:
5 Added: {% block title %}
6 Added: Settings for user {{ current_user.username }}
7 Added: {% endblock %}
8 Added:
9 Added: {% block content %}
10 Added:
11 Added: <p>Welcome, {{ current_user.name_first }} {{ current_user.name_last }}!</p>
12 Added:
13 Added: <h2>User profile</h2>
14 Added:
15 Added: <form method="post" action="{{ url_for('common.edit_item', module='settings', pk=current_user.primary_key, table='User' ) }}">
16 Added: <button>edit</button>
17 Added: </form>
18 Added:
19 Added: <table>
20 Added: <thead>
21 Added: <tr>
22 Added: <th>Field</th>
23 Added: <th>Value</th>
24 Added: </tr>
25 Added: </thead>
26 Added: <tbody>
27 Added: <tr>
28 Added: <td>Username</td>
29 Added: <td>{{ current_user.username }}</td>
30 Added: </tr>
31 Added: <tr>
32 Added: <td>First Name</td>
33 Added: <td>{{ current_user.name_first }}</td>
34 Added: </tr>
35 Added: <tr>
36 Added: <td>Last Name</td>
37 Added: <td>{{ current_user.name_last }}</td>
38 Added: </tr>
39 Added: <tr>
40 Added: <td> Last Updated</td>
41 Added: <td>{{ current_user.date_time_updated }}</td>
42 Added: </tr>
43 Added: </tbody>
44 Added: </table>
45 Added: {% endblock %}
config.py
index 00000000..984a0c5d 000000..100644
@@ -0,0 +1,8 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added:
4 Added: # DEBUG = True
5 Added: APPLICATION_ROOT = "/mdl"
6 Added: SECRET_KEY = "Scooby_Lu,_where_are_you?"
7 Added: SQLALCHEMY_DATABASE_URI = "sqlite:///" + "mdl.db"
8 Added: SQLALCHEMY_TRACK_MODIFICATIONS = True
deploy.sh
index 00000000..218da177 000000..100755
@@ -0,0 +1,7 @@
1 Added: #! /bin/bash
2 Added:
3 Added: remote='root@192.162.71.223:/var/www/apps.mlnp.fr/mdl'
4 Added:
5 Added: echo Deploying now.
6 Added: rsync -razvP . $remote
7 Added: echo successfully deployed.
initialize_database.py
index 00000000..7b7226bf 000000..100644
@@ -0,0 +1,24 @@
1 Added: # -*- mode: python; -*-
2 Added:
3 Added: import os
4 Added: from app import create_app, db
5 Added:
6 Added:
7 Added: app = create_app()
8 Added: DB_NAME = "mdl.db"
9 Added: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + DB_NAME
10 Added: db_path = f"app/{DB_NAME}"
11 Added:
12 Added:
13 Added: if os.path.exists(db_path):
14 Added: os.remove(db_path)
15 Added: print(f"Existing database {db_path} has been deleted successfully.")
16 Added: else:
17 Added: print(f"Database {db_path} does not exist yet, creating now.")
18 Added:
19 Added: with app.app_context():
20 Added: print(f"Creating database {db_path}...")
21 Added: db.create_all()
22 Added:
23 Added:
24 Added: print(f"Database {db_path} created successfully.")
run.sh
index 00000000..79272343 000000..100755
@@ -0,0 +1,7 @@
1 Added: #! /bin/bash
2 Added:
3 Added: source .venv/bin/activate
4 Added: export FLASK_APP=app
5 Added: export FLASK_ENV=development
6 Added: # export FLASK_DEBUG=1
7 Added: flask run