More tentacles progress
This commit is contained in:
parent
1f7c21f26e
commit
e05cf362df
14 changed files with 442 additions and 139 deletions
|
@ -1,4 +1,9 @@
|
|||
SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
|
||||
|
||||
[db]
|
||||
uri = "tentacles.sqlite3"
|
||||
uri = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tentacles.sqlite3"
|
||||
|
||||
[[users]]
|
||||
email = "root@tirefireind.us"
|
||||
group_id = 0
|
||||
status_id = 1
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
from pathlib import Path
|
||||
import click
|
||||
from flask import Flask, request, session, current_app, Authorization
|
||||
from flask import Flask, request, session, current_app
|
||||
import tomllib
|
||||
|
||||
from tentacles.blueprints import ui, api
|
||||
from tentacles.blueprints import user_ui, printer_ui, api
|
||||
from tentacles.store import Store
|
||||
|
||||
|
||||
|
@ -19,22 +19,28 @@ def cli():
|
|||
|
||||
|
||||
def open_db():
|
||||
current_app.db = Store(current_app.config.get("db", {}).get("uri"))
|
||||
current_app.db.connect()
|
||||
request.db = Store(current_app.config.get("db", {}).get("uri"))
|
||||
request.db.connect()
|
||||
|
||||
|
||||
def commit_db(resp):
|
||||
current_app.db.commit()
|
||||
request.db.close()
|
||||
return resp
|
||||
|
||||
|
||||
def create_j2_request_global():
|
||||
current_app.jinja_env.globals["request"] = request
|
||||
|
||||
|
||||
def user_session():
|
||||
if (session_id := request.cookies.get("sid", "")) and (
|
||||
uid := current_app.db.try_key(session_id)
|
||||
uid := request.db.try_key(session_id)
|
||||
):
|
||||
request.sid = session_id
|
||||
request.uid = uid
|
||||
_, gid, name, _ = current_app.db.fetch_user(uid)
|
||||
_id, gid, name, _email, _hash, _status, _verification = request.db.fetch_user(
|
||||
uid
|
||||
)
|
||||
request.gid = gid
|
||||
request.username = name
|
||||
request.is_admin = gid == 0
|
||||
|
@ -58,11 +64,19 @@ def serve(hostname: str, port: int, config: Path):
|
|||
|
||||
print(app.config)
|
||||
|
||||
app.before_first_request(open_db)
|
||||
# Before first request
|
||||
app.before_first_request(create_j2_request_global)
|
||||
|
||||
# Before request
|
||||
app.before_request(open_db)
|
||||
app.before_request(user_session)
|
||||
|
||||
# After request
|
||||
app.after_request(commit_db)
|
||||
|
||||
app.register_blueprint(ui.BLUEPRINT)
|
||||
# Blueprints
|
||||
app.register_blueprint(user_ui.BLUEPRINT)
|
||||
app.register_blueprint(printer_ui.BLUEPRINT)
|
||||
app.register_blueprint(api.BLUEPRINT)
|
||||
|
||||
app.run(host=hostname, port=port)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Blueprints for HTML serving 'ui'."""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from importlib.resources import files
|
||||
|
||||
from click import group
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
request,
|
||||
Request,
|
||||
redirect,
|
||||
render_template,
|
||||
session,
|
||||
url_for,
|
||||
flash,
|
||||
)
|
||||
|
||||
from .util import is_logged_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("printer", __name__)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers")
|
||||
def printers():
|
||||
return render_template("printers.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers/add", methods=["get", "post"])
|
||||
def add_printer():
|
||||
if not is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
pass
|
||||
|
||||
return render_template("add_printer.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers/delete")
|
||||
def delete_printer():
|
||||
return render_template("delete_printer.html.j2")
|
|
@ -1,95 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Blueprints for HTML serving 'ui'."""
|
||||
|
||||
from datetime import timedelta
|
||||
from importlib.resources import files
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
request,
|
||||
Request,
|
||||
redirect,
|
||||
render_template,
|
||||
session,
|
||||
url_for,
|
||||
flash,
|
||||
)
|
||||
|
||||
BLUEPRINT = Blueprint("ui", __name__)
|
||||
|
||||
|
||||
def is_logged_in(request: Request) -> bool:
|
||||
return request.uid is not None
|
||||
|
||||
|
||||
@BLUEPRINT.route("/")
|
||||
def root():
|
||||
return (
|
||||
render_template(
|
||||
"index.html.j2",
|
||||
request=request,
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
if sid := current_app.db.try_login(
|
||||
username := request.form["username"],
|
||||
request.form["password"],
|
||||
timedelta(days=1),
|
||||
):
|
||||
resp = redirect("/")
|
||||
resp.set_cookie("sid", sid)
|
||||
flash(f"Welcome, {username}", category="success")
|
||||
return resp
|
||||
|
||||
else:
|
||||
flash("Incorrect username/password", category="error")
|
||||
return render_template("login.html.j2")
|
||||
|
||||
else:
|
||||
return render_template("login.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
try:
|
||||
if uid := current_app.db.try_create_user(
|
||||
request.form["username"],
|
||||
request.form["email"],
|
||||
request.form["password"],
|
||||
):
|
||||
flash(
|
||||
"Please check your email for a verification request",
|
||||
category="success",
|
||||
)
|
||||
return render_template("register.html.j2")
|
||||
except:
|
||||
pass
|
||||
|
||||
flash("Unable to register that username", category="error")
|
||||
return render_template("register.html.j2")
|
||||
|
||||
else:
|
||||
return render_template("register.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/logout")
|
||||
def logout():
|
||||
# Invalidate the user's authorization
|
||||
current_app.db.delete_key(request.sid)
|
||||
resp = redirect("/")
|
||||
resp.set_cookie("sid", "")
|
||||
return resp
|
123
projects/tentacles/src/python/tentacles/blueprints/user_ui.py
Normal file
123
projects/tentacles/src/python/tentacles/blueprints/user_ui.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Blueprints for HTML serving 'ui'."""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from importlib.resources import files
|
||||
|
||||
from click import group
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
request,
|
||||
Request,
|
||||
redirect,
|
||||
render_template,
|
||||
session,
|
||||
url_for,
|
||||
flash,
|
||||
)
|
||||
|
||||
from .util import salt, is_logged_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("user", __name__)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/")
|
||||
def root():
|
||||
return (
|
||||
render_template(
|
||||
"index.html.j2",
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
if sid := request.db.try_login(
|
||||
username := request.form["username"],
|
||||
salt(request.form["password"]),
|
||||
timedelta(days=1),
|
||||
):
|
||||
resp = redirect("/")
|
||||
resp.set_cookie("sid", sid)
|
||||
flash(f"Welcome, {username}", category="success")
|
||||
return resp
|
||||
|
||||
else:
|
||||
flash("Incorrect username/password", category="error")
|
||||
return render_template("login.html.j2")
|
||||
|
||||
else:
|
||||
return render_template("login.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
try:
|
||||
username = request.form["username"]
|
||||
email = request.form["email"]
|
||||
group_id = None
|
||||
status_id = None
|
||||
|
||||
for user_config in current_app.config.get("users", []):
|
||||
if user_config["email"] == email:
|
||||
if "group_id" in user_config:
|
||||
group_id = user_config["group_id"]
|
||||
|
||||
if "status_id" in user_config:
|
||||
status_id = user_config["status_id"]
|
||||
|
||||
break
|
||||
|
||||
if res := request.db.try_create_user(
|
||||
username, email, salt(request.form["password"]), group_id, status_id
|
||||
):
|
||||
id, status = res
|
||||
if status == -1:
|
||||
flash(
|
||||
"Please check your email for a verification request",
|
||||
category="success",
|
||||
)
|
||||
return render_template("register.html.j2")
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Error encountered while registering a user...")
|
||||
|
||||
flash("Unable to register that username", category="error")
|
||||
return render_template("register.html.j2")
|
||||
|
||||
else:
|
||||
return render_template("register.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user/logout")
|
||||
def logout():
|
||||
# Invalidate the user's authorization
|
||||
request.db.delete_key(request.sid)
|
||||
resp = redirect("/")
|
||||
resp.set_cookie("sid", "")
|
||||
return resp
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user", methods=["GET", "POST"])
|
||||
def settings():
|
||||
if is_logged_in(request):
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
pass
|
||||
|
||||
else:
|
||||
return render_template("user.html.j2")
|
29
projects/tentacles/src/python/tentacles/blueprints/util.py
Normal file
29
projects/tentacles/src/python/tentacles/blueprints/util.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from importlib.resources import files
|
||||
|
||||
from click import group
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
request,
|
||||
Request,
|
||||
redirect,
|
||||
render_template,
|
||||
session,
|
||||
url_for,
|
||||
flash,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_logged_in(request: Request) -> bool:
|
||||
return request.uid is not None
|
||||
|
||||
|
||||
def salt(password: str) -> str:
|
||||
return "$SALT$" + current_app.config["SECRET_KEY"] + password
|
|
@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
, FOREIGN KEY(group_id) REFERENCES groups(id)
|
||||
, FOREIGN KEY(status_id) REFERENCES user_statuses(id)
|
||||
, UNIQUE(name)
|
||||
, UNIQUE(email)
|
||||
);
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
|
|
@ -10,31 +10,35 @@ $secondary_blue: #288BC2;
|
|||
$secondary_green: #A5C426;
|
||||
$secondary_light_grey: #CACBCA;
|
||||
$secondary_dark_grey: #9A9A9A;
|
||||
$secondary_red: red;
|
||||
$clear: rgba(255, 255, 255, 255);
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aaux Next';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/AauxNextBlk.woff') format('woff');
|
||||
src: local('Aaux Next'), url('/static/font/AauxNextBlk.otf') format('otf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aaux Next';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextbdwebfont.woff') format('woff');
|
||||
src: local('Aaux Next'), url('/static/font/aauxnextbdwebfont.otf') format('otf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aaux Next';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextltwebfont.woff') format('woff');
|
||||
src: local('Aaux Next'), url('/static/font/aauxnextltwebfont.otf') format('otf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Aaux Next';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextmdwebfont.woff') format('woff');
|
||||
src: local('Aaux Next'), url('/static/font/aauxnextmdwebfont.otf') format('otf');
|
||||
}
|
||||
|
||||
@import url(https://fonts.googleapis.com/css?family=Raleway);
|
||||
|
@ -55,6 +59,20 @@ html, body {
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content, .footer {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
||||
.content {
|
||||
.flash, .panel {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -228,3 +246,29 @@ $navbar_padding: 10px;
|
|||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flashes {
|
||||
.flash {
|
||||
border: 10px solid $secondary_blue;
|
||||
border-radius: 20px;
|
||||
min-height: 40px;
|
||||
p {
|
||||
font-size: 20px;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: $secondary_green;
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: $secondary_red;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import sha3_256
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
from textwrap import indent
|
||||
from importlib.resources import files
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
@ -45,14 +45,28 @@ def one(it, *args, **kwargs):
|
|||
return it
|
||||
|
||||
|
||||
class StoreError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LoginError(StoreError):
|
||||
pass
|
||||
|
||||
|
||||
class Store(object):
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._conn: sqlite3.Connection = None
|
||||
|
||||
def _factory(self, cursor, row):
|
||||
fields = [column[0] for column in cursor.description]
|
||||
cls = namedtuple("Row", fields)
|
||||
return cls._make(row)
|
||||
|
||||
def connect(self):
|
||||
if not self._conn:
|
||||
self._conn = sqlite3.connect(self._path, isolation_level="IMMEDIATE")
|
||||
self._conn.row_factory = self._factory
|
||||
for hunk in PRELUDE.split("\n\n"):
|
||||
try:
|
||||
self._conn.executescript(hunk).fetchall()
|
||||
|
@ -71,16 +85,28 @@ class Store(object):
|
|||
self._conn.close()
|
||||
self._conn = None
|
||||
|
||||
@fmap(one)
|
||||
################################################################################
|
||||
# Users
|
||||
|
||||
@requires_conn
|
||||
def try_create_user(self, username, email, password):
|
||||
"""Attempt to create a new user."""
|
||||
def try_create_user(self, username, email, password, group_id=10, status_id=-2):
|
||||
"""Attempt to create a new user.
|
||||
|
||||
:param username: The name of the user to be created.
|
||||
:param email: The email of the user to be created.
|
||||
:param password: The (hopefully salted!) plain text password for the user. Will be hashed before storage.
|
||||
:param group_id: The numeric ID of a group to assign the user to. Default 10 AKA normal user.
|
||||
:param status_id: The numeric ID of the status to assign the user to. Default -2 AKA email verification required.
|
||||
|
||||
"""
|
||||
|
||||
digest = sha3_256()
|
||||
digest.update(password.encode("utf-8"))
|
||||
digest = digest.hexdigest()
|
||||
print(f"{username}: {digest!r}")
|
||||
return self._conn.execute(
|
||||
"INSERT INTO users (name, email, hash) VALUES (?, ?, ?) RETURNING (id)",
|
||||
[username, email, digest.hexdigest()],
|
||||
"INSERT INTO users (name, email, hash, group_id, status_id) VALUES (?, ?, ?, ?, ?) RETURNING id, status_id",
|
||||
[username, email, digest, group_id, status_id],
|
||||
).fetchone()
|
||||
|
||||
@requires_conn
|
||||
|
@ -99,6 +125,15 @@ class Store(object):
|
|||
def list_users(self):
|
||||
return self._conn.execute("SELECT id, name FROM users").fetchall()
|
||||
|
||||
@fmap(one)
|
||||
@requires_conn
|
||||
def fetch_user_status(self, user_status_id: int):
|
||||
"""Fetch a user status by ID"""
|
||||
|
||||
return self._conn.execute(
|
||||
"SELECT id, name FROM user_statuses WHERE id = ?", [user_status_id]
|
||||
).fetchone()
|
||||
|
||||
################################################################################
|
||||
# Sessions / 'keys'
|
||||
|
||||
|
@ -120,15 +155,23 @@ class Store(object):
|
|||
|
||||
digest = sha3_256()
|
||||
digest.update(password.encode("utf-8"))
|
||||
digest = digest.hexdigest()
|
||||
print(f"{username}: {digest!r}")
|
||||
res = self._conn.execute(
|
||||
"SELECT id FROM users WHERE name=? AND hash=? LIMIT 1",
|
||||
[username, digest.hexdigest()],
|
||||
"SELECT id, status_id FROM users WHERE (name=?1 AND hash=?2) OR (email=?1 AND hash=?2) LIMIT 1",
|
||||
[username, digest],
|
||||
).fetchone()
|
||||
if not res:
|
||||
return
|
||||
uid = res[0]
|
||||
|
||||
uid, status = res
|
||||
if status > 0:
|
||||
return self._create_session(uid, ttl)
|
||||
|
||||
else:
|
||||
_, status = self.fetch_user_status(status)
|
||||
raise LoginError(status)
|
||||
|
||||
@requires_conn
|
||||
def create_key(self, kid: str, ttl) -> Optional[str]:
|
||||
"""Given an _existing_ login session, create a new key.
|
||||
|
@ -183,16 +226,23 @@ class Store(object):
|
|||
# Printers
|
||||
#
|
||||
# Printers represent connections to OctoPrint instances controlling physical machines.
|
||||
|
||||
@fmap(one)
|
||||
@requires_conn
|
||||
def create_printer(self):
|
||||
pass
|
||||
def try_create_printer(self, url, api_key):
|
||||
self._conn.execute(
|
||||
"INSERT INTO printers (url, api_key, status_id) VALUES (?, ?, 0) RETURNING id",
|
||||
[url, api_key],
|
||||
).fetchone()
|
||||
|
||||
@requires_conn
|
||||
def list_printers(self):
|
||||
pass
|
||||
return self._conn.execute(
|
||||
"SELECT id, url, last_poll_date, s.name as status FROM printers p INNER JOIN printer_stauses s ON p.status_id = s.id"
|
||||
).fetchall()
|
||||
|
||||
@requires_conn
|
||||
def update_printer_status(self):
|
||||
def update_printer_status(self, printer_id, status_id):
|
||||
pass
|
||||
|
||||
################################################################################
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<h1>Add printer</h1>
|
||||
<form method="post" id="form">
|
||||
<p>Hostname: <input type="text" name="hostname" /></p>
|
||||
<p>API key: <input type="text" name="key" /></p>
|
||||
<p><input id="test" type="button" value="Test" enabled="false" /></p>
|
||||
<p><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></p>
|
||||
<input type="hidden" name="tested" value="false" />
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.getElementById("input").disabled = True;
|
||||
|
||||
function testSettings() {
|
||||
var formData = new FormData(document.getElementById("form"))
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/printer/test");
|
||||
req.send(formData);
|
||||
|
||||
};
|
||||
|
||||
function maybeSubmit() {
|
||||
if (document.getElementById("tested").value == "true") {
|
||||
document.getElementById("form").submit();
|
||||
} else {
|
||||
console.error("Form values have not been tested!");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -24,28 +24,31 @@
|
|||
|
||||
<ul class="menu">
|
||||
{% if not request.uid %}
|
||||
<li><a href="/login">Log in</a></li>
|
||||
<li><a href="/register">Register</a></li>
|
||||
<li><a href="/user/login">Log in</a></li>
|
||||
<li><a href="/user/register">Register</a></li>
|
||||
{% else %}
|
||||
{% if request.is_admin %}
|
||||
<li><a href="/printers">Settings</a></li>
|
||||
<li><a href="/printers">Printers</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><a href="/logout">Log out</a></li>
|
||||
<li><a href="/user">Settings</a></li>
|
||||
<li><a href="/user/logout">Log out</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-{{ category }}">{{ message }}</div>
|
||||
<div class="flash {{ category }}">
|
||||
<center><p>{{ message }}</p></center>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="content">
|
||||
|
||||
{% block content %}Oops, an empty page :/{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
|
|
|
@ -1,4 +1,49 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<p>Hello, {% if request.uid %}{{ request.username }}{% else %}world{% endif %}!</p>
|
||||
<div class="panel printers">
|
||||
<h2>Printers</h2>
|
||||
{% with printers = request.db.list_printers() %}
|
||||
{% if printers %}
|
||||
<ul>
|
||||
{% for printer in printers %}
|
||||
<li></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
No printers available. {% if request.is_admin %}<a href="/printers/add">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="panel queue">
|
||||
<h2>Queue</h2>
|
||||
{% with jobs = request.db.list_jobs(uid=request.uid) %}
|
||||
{% if jobs %}
|
||||
<ul>
|
||||
{% for job in jobs %}
|
||||
<li></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
No pending tasks. {% if request.uid %}Start something!{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% if request.uid %}
|
||||
<div class="panel files">
|
||||
<h2>Files</h2>
|
||||
{% with files = request.db.list_files(uid=request.uid) %}
|
||||
{% if files %}
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
You don't have any files. Upload something!
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<p>Username: <input type="text" name="username">
|
||||
<p>Email address: <input type="text" name="email">
|
||||
<p>Password: <input type="password" name="password">
|
||||
<p><input type="submit" value=Register>
|
||||
<p><input type="submit" value="Register">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -26,7 +26,13 @@ def password_testy():
|
|||
|
||||
@pytest.fixture
|
||||
def uid_testy(store, username_testy, password_testy):
|
||||
return store.try_create_user(username_testy, password_testy)
|
||||
uid, status = store.try_create_user(
|
||||
username_testy,
|
||||
username_testy,
|
||||
password_testy,
|
||||
status_id=1,
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
Loading…
Reference in a new issue