Initial commit

This commit is contained in:
David Carmichael
2024-02-11 21:59:18 +00:00
commit 9687db5a96
79 changed files with 9092 additions and 0 deletions

23
app/__init__.py Normal file
View File

@@ -0,0 +1,23 @@
from quart import Quart
from app.extensions import imp, db
def create_app():
app = Quart(__name__, static_url_path="/")
imp.init_app(app)
imp.import_app_resources(
files_to_import=["*"],
folders_to_import=["*"]
)
imp.import_blueprints("blueprints")
imp.import_models("models")
db.init_app(app)
@app.before_serving
async def create_tables():
db.create_all()
return app

View File

@@ -0,0 +1,10 @@
from quart_imp import Blueprint
bp = Blueprint(__name__)
bp.import_resources("routes")
@bp.before_app_request
async def before_app_request():
bp.init_session()

View File

@@ -0,0 +1,25 @@
ENABLED = "yes"
[SETTINGS]
URL_PREFIX = "/"
#SUBDOMAIN = ""
#URL_DEFAULTS = {}
STATIC_FOLDER = "static"
TEMPLATE_FOLDER = "templates"
STATIC_URL_PATH = "/static"
#ROOT_PATH = ""
#CLI_GROUP = ""
[SESSION]
#www_session = "yes"
# Set ENABLED to true to allow the blueprint
# to create a database bind, change settings accordingly.
[DATABASE_BIND]
ENABLED = false
DIALECT = "sqlite"
DATABASE_NAME = "www"
LOCATION = ""
PORT = ""
USERNAME = ""
PASSWORD = ""

View File

@@ -0,0 +1,8 @@
from quart import render_template
from .. import bp
@bp.route("/", methods=["GET"])
async def index():
return await render_template(bp.tmpl("index.html"))

View File

@@ -0,0 +1,880 @@
/**
* Forced dark theme version
*/
:root {
--background-body: #202b38;
--background: #161f27;
--background-alt: #1a242f;
--selection: #1c76c5;
--text-main: #dbdbdb;
--text-bright: #fff;
--text-muted: #a9b1ba;
--links: #41adff;
--focus: #0096bfab;
--border: #526980;
--code: #ffbe85;
--animation-duration: 0.1s;
--button-base: #0c151c;
--button-hover: #040a0f;
--scrollbar-thumb: var(--button-hover);
--scrollbar-thumb-hover: rgb(0, 0, 0);
--form-placeholder: #a9a9a9;
--form-text: #fff;
--variable: #d941e2;
--highlight: #efdb43;
--select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3 .org/1999/xlink' height='62.5' width='116.9' fill='%23efef ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6, 7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115. 3,1.6Z'/%3E %3C/svg%3E");
}
html {
scrollbar-color: #040a0f #202b38;
scrollbar-color: var(--scrollbar-thumb) var(--background-body);
scrollbar-width: thin;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif;
line-height: 1.4;
max-width: 800px;
margin: 20px auto;
padding: 0 10px;
word-wrap: break-word;
color: #dbdbdb;
color: var(--text-main);
background: #202b38;
background: var(--background-body);
text-rendering: optimizeLegibility;
}
button {
transition: background-color 0.1s linear,
border-color 0.1s linear,
color 0.1s linear,
box-shadow 0.1s linear,
transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
input {
transition: background-color 0.1s linear,
border-color 0.1s linear,
color 0.1s linear,
box-shadow 0.1s linear,
transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
textarea {
transition: background-color 0.1s linear,
border-color 0.1s linear,
color 0.1s linear,
box-shadow 0.1s linear,
transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
h1 {
font-size: 2.2em;
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 12px;
margin-top: 24px;
}
h1 {
color: #fff;
color: var(--text-bright);
}
h2 {
color: #fff;
color: var(--text-bright);
}
h3 {
color: #fff;
color: var(--text-bright);
}
h4 {
color: #fff;
color: var(--text-bright);
}
h5 {
color: #fff;
color: var(--text-bright);
}
h6 {
color: #fff;
color: var(--text-bright);
}
strong {
color: #fff;
color: var(--text-bright);
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong,
th {
font-weight: 600;
}
q::before {
content: none;
}
q::after {
content: none;
}
blockquote {
border-left: 4px solid #0096bfab;
border-left: 4px solid var(--focus);
margin: 1.5em 0;
padding: 0.5em 1em;
font-style: italic;
}
q {
border-left: 4px solid #0096bfab;
border-left: 4px solid var(--focus);
margin: 1.5em 0;
padding: 0.5em 1em;
font-style: italic;
}
blockquote > footer {
font-style: normal;
border: 0;
}
blockquote cite {
font-style: normal;
}
address {
font-style: normal;
}
a[href^='mailto\:']::before {
content: '📧 ';
}
a[href^='tel\:']::before {
content: '📞 ';
}
a[href^='sms\:']::before {
content: '💬 ';
}
mark {
background-color: #efdb43;
background-color: var(--highlight);
border-radius: 2px;
padding: 0 2px 0 2px;
color: #000;
}
a > code,
a > strong {
color: inherit;
}
button,
select,
input[type='submit'],
input[type='reset'],
input[type='button'],
input[type='checkbox'],
input[type='range'],
input[type='radio'] {
cursor: pointer;
}
input,
select {
display: block;
}
[type='checkbox'],
[type='radio'] {
display: initial;
}
input {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
button {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
textarea {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
select {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
button {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='submit'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='reset'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='button'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
button:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='submit']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='reset']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='button']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='color'] {
min-height: 2rem;
padding: 8px;
cursor: pointer;
}
input[type='checkbox'],
input[type='radio'] {
height: 1em;
width: 1em;
}
input[type='radio'] {
border-radius: 100%;
}
input {
vertical-align: top;
}
label {
vertical-align: middle;
margin-bottom: 4px;
display: inline-block;
}
input:not([type='checkbox']):not([type='radio']),
input[type='range'],
select,
button,
textarea {
-webkit-appearance: none;
}
textarea {
display: block;
margin-right: 0;
box-sizing: border-box;
resize: vertical;
}
textarea:not([cols]) {
width: 100%;
}
textarea:not([rows]) {
min-height: 40px;
height: 140px;
}
select {
background: #161f27 url("data:image/svg+xml;charset=utf-8,% 3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version= '1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w 3.org/1999/xlink' height='62.5' width='116.9' fill='%23efef ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L5 8.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7. 4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60 .5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3, 1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat;
background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat;
padding-right: 35px;
}
select::-ms-expand {
display: none;
}
select[multiple] {
padding-right: 10px;
background-image: none;
overflow-y: auto;
}
input:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
select:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
button:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
textarea:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
input[type='checkbox']:active,
input[type='radio']:active,
input[type='submit']:active,
input[type='reset']:active,
input[type='button']:active,
input[type='range']:active,
button:active {
transform: translateY(2px);
}
input:disabled,
select:disabled,
button:disabled,
textarea:disabled {
cursor: not-allowed;
opacity: 0.5;
}
::-moz-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
:-ms-input-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
::-ms-input-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
::placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
fieldset {
border: 1px #0096bfab solid;
border: 1px var(--focus) solid;
border-radius: 6px;
margin: 0;
margin-bottom: 12px;
padding: 10px;
}
legend {
font-size: 0.9em;
font-weight: 600;
}
input[type='range'] {
margin: 10px 0;
padding: 10px 0;
background: transparent;
}
input[type='range']:focus {
outline: none;
}
input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: 9.5px;
-webkit-transition: 0.2s;
transition: 0.2s;
background: #161f27;
background: var(--background);
border-radius: 3px;
}
input[type='range']::-webkit-slider-thumb {
box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
-webkit-appearance: none;
margin-top: -7px;
}
input[type='range']:focus::-webkit-slider-runnable-track {
background: #161f27;
background: var(--background);
}
input[type='range']::-moz-range-track {
width: 100%;
height: 9.5px;
-moz-transition: 0.2s;
transition: 0.2s;
background: #161f27;
background: var(--background);
border-radius: 3px;
}
input[type='range']::-moz-range-thumb {
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
}
input[type='range']::-ms-track {
width: 100%;
height: 9.5px;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
}
input[type='range']::-ms-fill-lower {
background: #161f27;
background: var(--background);
border: 0.2px solid #010101;
border-radius: 3px;
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
}
input[type='range']::-ms-fill-upper {
background: #161f27;
background: var(--background);
border: 0.2px solid #010101;
border-radius: 3px;
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
}
input[type='range']::-ms-thumb {
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
border: 1px solid #000;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
}
input[type='range']:focus::-ms-fill-lower {
background: #161f27;
background: var(--background);
}
input[type='range']:focus::-ms-fill-upper {
background: #161f27;
background: var(--background);
}
a {
text-decoration: none;
color: #41adff;
color: var(--links);
}
a:hover {
text-decoration: underline;
}
code {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
samp {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
time {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
pre > code {
padding: 10px;
display: block;
overflow-x: auto;
}
var {
color: #d941e2;
color: var(--variable);
font-style: normal;
font-family: monospace;
}
kbd {
background: #161f27;
background: var(--background);
border: 1px solid #526980;
border: 1px solid var(--border);
border-radius: 2px;
color: #dbdbdb;
color: var(--text-main);
padding: 2px 4px 2px 4px;
}
img,
video {
max-width: 100%;
height: auto;
}
hr {
border: none;
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
}
table {
border-collapse: collapse;
margin-bottom: 10px;
width: 100%;
table-layout: fixed;
}
table caption {
text-align: left;
}
td,
th {
padding: 6px;
text-align: left;
vertical-align: top;
word-wrap: break-word;
}
thead {
border-bottom: 1px solid #526980;
border-bottom: 1px solid var(--border);
}
tfoot {
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
}
tbody tr:nth-child(even) {
background-color: #161f27;
background-color: var(--background);
}
tbody tr:nth-child(even) button {
background-color: #1a242f;
background-color: var(--background-alt);
}
tbody tr:nth-child(even) button:hover {
background-color: #202b38;
background-color: var(--background-body);
}
::-webkit-scrollbar {
height: 10px;
width: 10px;
}
::-webkit-scrollbar-track {
background: #161f27;
background: var(--background);
border-radius: 6px;
}
::-webkit-scrollbar-thumb {
background: #040a0f;
background: var(--scrollbar-thumb);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgb(0, 0, 0);
background: var(--scrollbar-thumb-hover);
}
::-moz-selection {
background-color: #1c76c5;
background-color: var(--selection);
color: #fff;
color: var(--text-bright);
}
::selection {
background-color: #1c76c5;
background-color: var(--selection);
color: #fff;
color: var(--text-bright);
}
details {
display: flex;
flex-direction: column;
align-items: flex-start;
background-color: #1a242f;
background-color: var(--background-alt);
padding: 10px 10px 0;
margin: 1em 0;
border-radius: 6px;
overflow: hidden;
}
details[open] {
padding: 10px;
}
details > :last-child {
margin-bottom: 0;
}
details[open] summary {
margin-bottom: 10px;
}
summary {
display: list-item;
background-color: #161f27;
background-color: var(--background);
padding: 10px;
margin: -10px -10px 0;
cursor: pointer;
outline: none;
}
summary:hover,
summary:focus {
text-decoration: underline;
}
details > :not(summary) {
margin-top: 0;
}
summary::-webkit-details-marker {
color: #dbdbdb;
color: var(--text-main);
}
dialog {
background-color: #1a242f;
background-color: var(--background-alt);
color: #dbdbdb;
color: var(--text-main);
border: none;
border-radius: 6px;
border-color: #526980;
border-color: var(--border);
padding: 10px 30px;
}
dialog > header:first-child {
background-color: #161f27;
background-color: var(--background);
border-radius: 6px 6px 0 0;
margin: -10px -30px 10px;
padding: 10px;
text-align: center;
}
dialog::-webkit-backdrop {
background: #0000009c;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
dialog::backdrop {
background: #0000009c;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
footer {
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
padding-top: 10px;
color: #a9b1ba;
color: var(--text-muted);
}
body > footer {
margin-top: 40px;
}
@media print {
body,
pre,
code,
summary,
details,
button,
input,
textarea {
background-color: #fff;
}
button,
input,
textarea {
border: 1px solid #000;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
button,
input,
textarea,
footer,
summary,
strong {
color: #000;
}
summary::marker {
color: #000;
}
summary::-webkit-details-marker {
color: #000;
}
tbody tr:nth-child(even) {
background-color: #f2f2f2;
}
a {
color: #00f;
text-decoration: underline;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1 @@
console.log('This log is from the file /home/david/PycharmProjects/quart-imp/app/blueprints/www/static/main.js')

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
<title>Quart-Imp</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" sizes="16x16 32x32" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('www.static', filename='css/water.css') }}">
<script defer src="{{ url_for('www.static', filename='js/main.js') }}"></script>
<script>
// inline script
</script>
</head>
<body>
{% include 'www/includes/header.html' %}
{% block content %}{% endblock %}
{% include 'www/includes/footer.html' %}
</body>
</html>

View File

@@ -0,0 +1,6 @@
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
<div>
<p>This is the footer, located here: <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/footer.html</code></p>
<p>It's being imported in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code> template.</p>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<div style="display: flex; flex-direction: row; align-items: center;
justify-content: start; gap: 2rem; margin-bottom: 2rem;">
<img style="border-radius: 50%"
src="{{ url_for('www.static', filename='img/quart-imp-logo.png') }}" alt="quart-imp logo">
<h1 style="font-size: 4rem;">Quart-Imp</h1>
</div>
<div style="margin-bottom: 2rem;">
<p>This is the header, located here: <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/header.html</code></p>
<p>It's being imported in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code> template.</p>
</div>

View File

@@ -0,0 +1,17 @@
{% extends 'www/extends/main.html' %}
{% block content %}
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
<div>
<h2 style="margin: 0;">Blueprint: www</h2>
<h3>This is the index route of the included example blueprint.</h3>
<p style="margin-bottom: 0;">
This template page is located in <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/index.html</code><br/>
it extends from <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code><br/>
with its route defined in <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/routes/index.py</code><br/><br/>
It's being imported by <code>bp.import_resources("routes")</code>
in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/__init__.py</code> file.
</p>
</div>
</div>
{% endblock %}

81
app/default.config.toml Normal file
View File

@@ -0,0 +1,81 @@
# Quart-Imp Config File
# ------------------------
# Updates the Quart app config with the variables below.
# If any variable below does not exist in the standard Quart env
# vars it is created and will be accessible using
# app.config. All key names defined below will be
# capitalised when imported.
[FLASK]
DEBUG = false
#PROPAGATE_EXCEPTIONS = true
TRAP_HTTP_EXCEPTIONS = false
#TRAP_BAD_REQUEST_ERRORS = true
SECRET_KEY = "86685ef98889e7db7da6df2dd7184f866f4ef34244fe2c52"
SESSION_COOKIE_NAME = "session"
#SESSION_COOKIE_DOMAIN = "domain-here.com"
#SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_HTTPONLY = true
SESSION_COOKIE_SECURE = false
SESSION_COOKIE_SAMESITE = "Lax"
PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
SESSION_REFRESH_EACH_REQUEST = true
USE_X_SENDFILE = false
#SEND_FILE_MAX_AGE_DEFAULT = 43200
ERROR_404_HELP = true
#SERVER_NAME = "localhost:5000"
APPLICATION_ROOT = "/"
PREFERRED_URL_SCHEME = "http"
#MAX_CONTENT_LENGTH = 0
#TEMPLATES_AUTO_RELOAD = true
EXPLAIN_TEMPLATE_LOADING = false
MAX_COOKIE_SIZE = 4093
# This will set the default session variables for the app.
# Anything here will be accessible using session["your_var_name"]
# or session.get("your_var_name")
[SESSION]
logged_in = false
# These settings are specific to the Flask-SQLAlchemy extension.
# Anything here will be accessible using app.config
[SQLALCHEMY]
SQLALCHEMY_ECHO = false
SQLALCHEMY_TRACK_MODIFICATIONS = false
SQLALCHEMY_RECORD_QUERIES = false
# Below are extra settings that Quart-Imp uses but relates to Flask-SQLAlchemy.
# This sets the file extension for SQLite databases, and where to create the folder
# that the database will be stored in. true will create the folder on the same level as your
# app, false will create the folder in the app root.
SQLITE_DB_EXTENSION = ".sqlite"
SQLITE_STORE_IN_PARENT = false
# [DATABASE.MAIN] is loaded as SQLALCHEMY_DATABASE_URI
# Dialets = mysql / postgresql / sqlite / oracle / mssql
# Uncomment below to generate the SQLALCHEMY_DATABASE_URI.
[DATABASE.MAIN]
ENABLED = true
DIALECT = "sqlite"
DATABASE_NAME = "database"
LOCATION = ""
PORT = ""
USERNAME = ""
PASSWORD = ""
# Adding another database is as simple as adding a new section.
# [DATABASE.ANOTHER] will then be accessible using SQLALCHEMY_BINDS
# The bind key will be stored as a lowercase value, so "ANOTHER" will
# be accessible as "another"
# You can then use the bind key in the model as follows:
# class MyModel(db.Model):
# __bind_key__ = "another"
# ...
# Uncomment below to generate and add to SQLALCHEMY_BINDS.
#[DATABASE.ANOTHER]
#ENABLED = true
#DIALECT = "sqlite"
#DATABASE_NAME = "another"
#LOCATION = ""
#PORT = ""
#USERNAME = ""
#PASSWORD = ""

View File

@@ -0,0 +1,9 @@
import quart_flask_patch
from flask_sqlalchemy import SQLAlchemy
from quart_imp import Imp
_ = quart_flask_patch
imp = Imp()
db = SQLAlchemy()

12
app/models/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
from sqlalchemy import select, update, delete, insert
from sqlalchemy.orm import relationship
from app.extensions import db
__all__ = [
"db",
"select",
"update",
"delete",
"insert",
]

View File

@@ -0,0 +1,72 @@
from quart_imp.auth import authenticate_password
from quart_imp.auth import encrypt_password
from quart_imp.auth import generate_private_key
from quart_imp.auth import generate_salt
from . import *
class ExampleUserTable(db.Model):
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), nullable=False)
password = db.Column(db.String(512), nullable=False)
salt = db.Column(db.String(4), nullable=False)
private_key = db.Column(db.String(256), nullable=False)
disabled = db.Column(db.Boolean)
@classmethod
def login(cls, username, password: str) -> bool:
user = cls.get_by_username(username)
if user is None:
return False
return authenticate_password(password, user.password, user.salt)
@classmethod
def get_by_id(cls, user_id: int):
return db.session.execute(
select(cls).filter_by(user_id=user_id).limit(1)
).scalar_one_or_none()
@classmethod
def get_by_username(cls, username: str):
return db.session.execute(
select(cls).filter_by(username=username).limit(1)
).scalar_one_or_none()
@classmethod
def create(cls, username, password, disabled):
salt = generate_salt()
salt_pepper_password = encrypt_password(password, salt)
private_key = generate_private_key(username)
db.session.execute(
insert(cls).values(
username=username,
password=salt_pepper_password,
salt=salt,
private_key=private_key,
disabled=disabled,
)
)
db.session.commit()
@classmethod
def update(cls, user_id: int, username, private_key, disabled):
db.session.execute(
update(cls).where(
cls.user_id == user_id
).values(
username=username,
private_key=private_key,
disabled=disabled,
)
)
db.session.commit()
@classmethod
def delete(cls, user_id: int):
db.session.execute(
delete(cls).where(
cls.user_id == user_id
)
)
db.session.commit()

57
app/resources/cli/cli.py Normal file
View File

@@ -0,0 +1,57 @@
from quart import current_app as app
from app.extensions import db
from app.models.example_user_table import ExampleUserTable
@app.cli.command("config")
async def create_tables():
print(app.config)
@app.cli.command("create-tables")
async def create_tables():
db.create_all()
@app.cli.command("get-example-user")
async def get_example_user():
result = ExampleUserTable.get_by_id(1)
if not result:
print("User not found.")
return
print(
f"""
user_id: {result.user_id}
username: {result.username}
salt: {result.salt}
password: {result.password}
private_key: {result.private_key}
disabled: {result.disabled}
"""
)
@app.cli.command("create-example-user")
async def add_example_user():
ExampleUserTable.create(
username="admin",
password="password",
disabled=False,
)
@app.cli.command("update-example-user")
async def update_example_user():
ExampleUserTable.update(
user_id=1,
username="admin-updated",
private_key="private_key",
disabled=False,
)
@app.cli.command("delete-example-user")
async def delete_example_user():
ExampleUserTable.delete(
user_id=1,
)

View File

@@ -0,0 +1,14 @@
from quart import current_app as app
@app.context_processor
async def example__utility_processor():
"""
Usage:
{{ format_price(100.33) }} -> $100.33
"""
async def example__format_price(amount, currency='$'):
return '{1}{0:.2f}'.format(amount, currency)
return dict(format_price=example__format_price)

View File

@@ -0,0 +1,44 @@
from quart import current_app as app
from quart import render_template
@app.errorhandler(400)
async def error_400(error):
return await render_template(
"errors/400.html",
), 400
@app.errorhandler(401)
async def error_401(error):
return await render_template(
"errors/401.html",
), 401
@app.errorhandler(403)
async def error_403(error):
return await render_template(
"errors/403.html",
), 403
@app.errorhandler(404)
async def error_404(error):
return await render_template(
"errors/404.html",
), 404
@app.errorhandler(405)
async def error_405(error):
return await render_template(
"errors/405.html",
), 405
@app.errorhandler(500)
async def error_500(error):
return await render_template(
"errors/500.html",
), 500

View File

@@ -0,0 +1,30 @@
from quart import current_app as app
@app.template_filter('example__num_to_month')
async def example__num_to_month(num: str) -> str:
"""
Usage:
{{ 1 | example__num_to_month }} -> January
"""
if isinstance(num, int):
num = str(num)
months = {
"1": "January",
"2": "February",
"3": "March",
"4": "April",
"5": "May",
"6": "June",
"7": "July",
"8": "August",
"9": "September",
"10": "October",
"11": "November",
"12": "December",
}
if num in months:
return months[num]
return "Month not found"

View File

@@ -0,0 +1,7 @@
from quart import current_app as app
from quart import render_template
@app.route("/resources")
async def index():
return await render_template("index.html")

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>400 Bad Request</title>
</head>
<body>
<p>It's not us, it's you.</p>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>401 Unauthorized</title>
</head>
<body>
<p>You lack valid authentication credentials for the requested resource</p>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Access forbidden!</p>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>404 Page Not Found</title>
</head>
<body>
<p>No route associated with the URL</p>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>405 Method Not Allowed</title>
</head>
<body>
<p>Should of GET when you POST, or POST when you GET</p>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>500 Server Error!</title>
</head>
<body>
<p>There has been a server error!</p>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
<title>Quart-Imp Global Template</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" sizes="16x16 32x32" type="image/x-icon">
</head>
<body>
<p>This is the example resources template file located in <code>resources/templates/index.html</code></p>
</body>