[Flask] Web media downloader frontend.
First commit :baby:
Changed files
- .gitignore
- app/.gitignore
- app/__init__.py
- app/forms.py
- app/main.py
- app/models.py
- app/modules/__init__.py
- app/modules/auth/forms.py
- app/modules/auth/routes.py
- app/modules/common.py
- app/static/fonts/CourierPrime-Bold.ttf
- app/static/fonts/CourierPrime-BoldItalic.ttf
- app/static/fonts/CourierPrime-Italic.ttf
- app/static/fonts/CourierPrime-Regular.ttf
- app/static/fonts/Inconsolata.otf
- app/static/fonts/PublicSans-Black.otf
- app/static/fonts/PublicSans-BlackItalic.otf
- app/static/fonts/PublicSans-Bold.otf
- app/static/fonts/PublicSans-BoldItalic.otf
- app/static/fonts/PublicSans-ExtraBold.otf
- app/static/fonts/PublicSans-ExtraBoldItalic.otf
- app/static/fonts/PublicSans-ExtraLight.otf
- app/static/fonts/PublicSans-ExtraLightItalic.otf
- app/static/fonts/PublicSans-Italic.otf
- app/static/fonts/PublicSans-Light.otf
- app/static/fonts/PublicSans-LightItalic.otf
- app/static/fonts/PublicSans-Medium.otf
- app/static/fonts/PublicSans-MediumItalic.otf
- app/static/fonts/PublicSans-Regular.otf
- app/static/fonts/PublicSans-SemiBold.otf
- app/static/fonts/PublicSans-SemiBoldItalic.otf
- app/static/fonts/PublicSans-Thin.otf
- app/static/fonts/PublicSans-ThinItalic.otf
- app/static/styles/style.css
- app/templates/base.html
- app/templates/home.html
- app/templates/modules/add-item.html
- app/templates/modules/invoices.html
- app/templates/modules/login.html
- app/templates/modules/register.html
- app/templates/modules/settings.html
- config.py
- deploy.sh
- initialize_database.py
- run.sh
.gitignore
@@ -0,0 +1,4 @@
1
Added:
# .gitignore for mdl
2
Added:
3
Added:
.venv/
4
Added:
downloads/
app/.gitignore
@@ -0,0 +1,4 @@
1
Added:
# .gitignore for app
2
Added:
3
Added:
*__pycache__
4
Added:
*.db
app/__init__.py
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Binary files differ
app/static/fonts/CourierPrime-BoldItalic.ttf
Binary files differ
app/static/fonts/CourierPrime-Italic.ttf
Binary files differ
app/static/fonts/CourierPrime-Regular.ttf
Binary files differ
app/static/fonts/Inconsolata.otf
Binary files differ
app/static/fonts/PublicSans-Black.otf
Binary files differ
app/static/fonts/PublicSans-BlackItalic.otf
Binary files differ
app/static/fonts/PublicSans-Bold.otf
Binary files differ
app/static/fonts/PublicSans-BoldItalic.otf
Binary files differ
app/static/fonts/PublicSans-ExtraBold.otf
Binary files differ
app/static/fonts/PublicSans-ExtraBoldItalic.otf
Binary files differ
app/static/fonts/PublicSans-ExtraLight.otf
Binary files differ
app/static/fonts/PublicSans-ExtraLightItalic.otf
Binary files differ
app/static/fonts/PublicSans-Italic.otf
Binary files differ
app/static/fonts/PublicSans-Light.otf
Binary files differ
app/static/fonts/PublicSans-LightItalic.otf
Binary files differ
app/static/fonts/PublicSans-Medium.otf
Binary files differ
app/static/fonts/PublicSans-MediumItalic.otf
Binary files differ
app/static/fonts/PublicSans-Regular.otf
Binary files differ
app/static/fonts/PublicSans-SemiBold.otf
Binary files differ
app/static/fonts/PublicSans-SemiBoldItalic.otf
Binary files differ
app/static/fonts/PublicSans-Thin.otf
Binary files differ
app/static/fonts/PublicSans-ThinItalic.otf
Binary files differ
app/static/styles/style.css
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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