This commit is contained in:
2022-07-07 21:40:37 +02:00
parent c03c13886f
commit e1a50c88df
17 changed files with 352 additions and 322 deletions

0
db.sqlite Normal file
View File

Binary file not shown.

View File

@@ -7,7 +7,7 @@ from .helpers import *
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///./db.sqlite"
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{app.root_path}/../db.sqlite"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)

View File

@@ -0,0 +1,22 @@
import os
import dominate
import dominate.tags as t
from dominate.util import raw
class Home:
def __init__(self, element, root_path):
doc = dominate.document(title="Packager")
with doc.head:
t.script(src="https://unpkg.com/htmx.org@1.7.0")
t.script(src="https://cdn.tailwindcss.com")
t.script(src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js", defer=True)
t.link(
rel="stylesheet",
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css",
)
with doc:
t.script(raw(open(os.path.join(root_path, "js/app.js")).read()))
doc.add(element.doc)
self.doc = doc

View File

@@ -17,7 +17,17 @@ def NewPackageList(name=None, description=None, error=False, errormsg=None):
target="_self",
method="post",
_class=cls("mt-8", "p-5", "border-2", "border-gray-200"),
**{"x-on:htmx:before-request": "(e) => submit_enabled || e.preventDefault()"},
**{
"x-on:htmx:before-request": "(e) => submit_enabled || e.preventDefault()",
"x-data": alpinedata(
{
"submit_enabled": (
jsbool(not error)
+ '&& document.getElementById("listname").value.trim().length !== 0'
)
}
),
},
) as doc:
with t.div(_class=cls("mb-5", "flex", "flex-row", "items-center")):
t.span(_class=cls("mdi", "mdi-playlist-plus", "text-2xl", "mr-4"))
@@ -41,6 +51,7 @@ def NewPackageList(name=None, description=None, error=False, errormsg=None):
data_hx_target="#new-pkglist",
data_hx_post="/list/name/validate",
data_hx_swap="outerHTML",
data_hx_trigger="changed",
_class=cls(
"block",
"w-full",
@@ -92,7 +103,7 @@ def NewPackageList(name=None, description=None, error=False, errormsg=None):
type="submit",
value="Add",
**{
"x-bind:class": 'submit_enabled ? "" : "cursor-not-allowed opacity-50"'
"x-bind:class": 'submit_enabled ? "cursor-pointer" : "cursor-not-allowed opacity-50"'
},
_class=cls(
"py-2",

View File

@@ -12,13 +12,7 @@ class PackageListManager:
self, pkglists, name=None, description=None, error=False, errormsg=None
):
assert not (error and not errormsg)
with t.div(
id="pkglist-manager",
_class=cls("p-8", "max-w-xl"),
**{
"x-data": '{ submit_enabled: document.getElementById("listname").value.trim().length !== 0 }'
},
) as doc:
with t.div(id="pkglist-manager", _class=cls("p-8", "max-w-xl")) as doc:
PackageListTable(pkglists),
NewPackageList(
name=name, description=description, error=error, errormsg=errormsg

View File

@@ -5,7 +5,102 @@ from dominate.util import raw
from ..helpers import *
class PackageListTableRow:
class PackageListTableRowEdit:
def __init__(self, pkglist):
error, errormsg = pkglist.error, pkglist.errormsg
assert not (error and not errormsg)
with t.tr(
_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200"),
id="pkglist-edit-row",
**{
"x-data": '{ edit_submit_enabled: document.getElementById("listedit-name").value.trim().length !== 0 }'
},
) as doc:
with t.td(colspan=3, _class=cls("border-none", "bg-purple-100", "h-10")):
with t.div():
t.form(
id="edit-pkglist",
action=f"/list/{pkglist.id}/edit/submit/",
target="_self",
method="post",
data_hx_post=f"/list/{pkglist.id}/edit/submit",
data_hx_target="closest tr",
data_hx_swap="outerHTML",
)
if error:
t.p(errormsg, _class=cls("text-red-400", "text-sm"))
with t.div(_class=cls("flex", "flex-row", "h-full")):
with t.div(
_class=cls(
"border",
"border-1",
"border-purple-500",
"bg-purple-100",
"mr-1",
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
id="listedit-name",
form="edit-pkglist",
name="name",
value=pkglist.name if error else pkglist.name,
**{
"x-on:input": "edit_submit_enabled = $event.srcElement.value.trim().length !== 0;"
},
)
with t.div(
_class=cls(
"border", "border-1", "border-purple-500", "bg-purple-100"
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
name="description",
form="edit-pkglist",
value=pkglist.description,
)
with t.td(
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
id="edit-packagelist-abort",
):
with t.a(
href="/",
data_hx_post=f"/list/{pkglist.id}/edit/cancel",
data_hx_target="closest tr",
data_hx_swap="outerHTML",
):
t.span(_class=cls("mdi", "mdi-cancel", "text-xl")),
with t.td(
id="edit-packagelist-save",
_class=cls(
"border",
"bg-green-200",
"hover:bg-green-400",
"cursor-pointer",
"w-8",
"text-center",
),
**{
"x-bind:class": 'edit_submit_enabled || "cursor-not-allowed opacity-50"',
"x-on:htmx:before-request": "(e) => edit_submit_enabled || e.preventDefault()",
},
):
with t.button(type="submit", form="edit-pkglist"):
t.span(_class=cls("mdi", "mdi-content-save", "text-xl")),
self.doc = doc
class PackageListTableRowNormal:
def __init__(self, pkglist):
with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200")) as doc:
t.td(pkglist.name, _class=cls("border", "px-2")),
@@ -14,6 +109,8 @@ class PackageListTableRow:
t.span(_class=cls("mdi", "mdi-delete", "text-xl")),
id="delete-packagelist",
data_hx_delete=f"/list/{pkglist.id}",
data_hx_target="closest tr",
data_hx_swap="outerHTML",
_class=cls(
"border",
"bg-red-200",
@@ -23,10 +120,7 @@ class PackageListTableRow:
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-pencil", "text-xl")),
id="edit-packagelist",
data_hx_post=f"/list/{pkglist.id}/edit",
with t.td(
_class=cls(
"border",
"bg-blue-200",
@@ -34,12 +128,20 @@ class PackageListTableRow:
"cursor-pointer",
"w-8",
"text-center",
),
),
)
):
with t.a(
id="edit-packagelist",
data_hx_post=f"/list/{pkglist.id}/edit",
href=f"/?edit={pkglist.id}",
data_hx_target="closest tr",
data_hx_swap="outerHTML",
):
t.span(_class=cls("mdi", "mdi-pencil", "text-xl")),
t.td(
t.span(_class=cls("mdi", "mdi-arrow-right", "text-xl")),
id="edit-packagelist",
# data_hx_post=f"/list/{pkglist.id}/edit",
# data_hx_post=f"/list/{pkglist.id}/show",
_class=cls(
"border",
"bg-green-200",
@@ -52,9 +154,16 @@ class PackageListTableRow:
self.doc = doc
class PackageListTableRow:
def __init__(self, pkglist):
if pkglist.edit:
self.doc = PackageListTableRowEdit(pkglist).doc
else:
self.doc = PackageListTableRowNormal(pkglist).doc
def PackageListTable(pkglists):
doc = t.div(id="packagelist-table")
with doc:
with t.div(id="packagelist-table") as doc:
t.h1("Package Lists", _class=cls("text-2xl", "mb-5"))
with t.table(
id="packagelist-table",
@@ -76,7 +185,7 @@ def PackageListTable(pkglists):
t.th(_class=cls("border p-2")),
_class="h-10",
)
with t.tbody(data_hx_target="closest tr", data_hx_swap="outerHTML"):
with t.tbody() as b:
for pkglist in pkglists:
PackageListTableRow(pkglist).doc

View File

@@ -1,4 +1,9 @@
from .NewPackageList import NewPackageList
from .PackageListTable import PackageListTable
from .PackageListTable import (
PackageListTable,
PackageListTableRowEdit,
PackageListTableRowNormal,
PackageListTableRow,
)
from .PackageListManager import PackageListManager
from .Home import Home

View File

@@ -1,2 +1,14 @@
def cls(*args):
return " ".join([a for a in args if a is not None])
def jsbool(b):
return "true" if b else "false"
def alpinedata(d):
elements = []
for k, v in d.items():
elements.append(f"{k}: " + v)
return "{" + ",".join(elements) + "}"

View File

@@ -8,6 +8,10 @@ class PackageList(db.Model):
description = db.Column(db.Text)
items = db.relationship("PackageListItem", backref="packagelist", lazy=True)
edit = False
error = False
errormsg = None
class PackageListItem(db.Model):
__tablename__ = "packagelistitem"

View File

@@ -10,7 +10,14 @@ import dominate
import dominate.tags as t
from dominate.util import raw
from .components import PackageListManager, NewPackageList, Home
from .components import (
PackageListManager,
NewPackageList,
Home,
PackageListTableRowEdit,
PackageListTableRowNormal,
PackageListTableRow,
)
from flask import request, make_response
@@ -45,8 +52,29 @@ def delete_packagelist(id):
@app.route("/")
def root():
packagelists = get_packagelists()
error = False
if not is_htmx():
edit = request.args.get("edit")
if edit is not None:
match = [p for p in packagelists if p.id == edit]
if match:
match[0].edit = True
error = request.args.get("error")
if error and bool(int(error)):
match[0].error = True
errormsg = request.args.get("msg")
if errormsg:
match[0].errormsg = errormsg
else:
name = request.args.get("name")
if name:
match[0].errormsg = f"Invalid name: {name}"
else:
match[0].errormsg = f"Invalid name"
return make_response(
Home(PackageListManager(get_packagelists()), app.root_path).doc.render(), 200
Home(PackageListManager(packagelists), app.root_path).doc.render(), 200
)
@@ -56,7 +84,6 @@ def is_htmx():
@app.route("/list/", methods=["POST"])
def add_new_list():
print(f"headers: {request.headers}")
name = request.form["name"]
description = request.form["description"]
@@ -69,15 +96,13 @@ def add_new_list():
if is_htmx():
return make_response(
str(
PackageListManager(
get_packagelists(),
name=name,
description=description,
error=error,
errormsg=errormsg,
)
),
PackageListManager(
get_packagelists(),
name=name,
description=description,
error=error,
errormsg=errormsg,
).doc.render(),
200 if error else 201,
)
else:
@@ -116,309 +141,63 @@ def validate_list_name():
@app.route("/list/<uuid:id>/edit/cancel", methods=["POST"])
def edit_list_cancel(id):
print("cancelling" * 20)
pkglist = PackageList.query.filter_by(id=str(id)).first()
with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200")) as doc:
t.td(pkglist.name, _class=cls("border", "px-2")),
t.td(str(pkglist.description), _class=cls("border", "px-2")),
t.td(
t.span(_class=cls("mdi", "mdi-delete", "text-xl")),
id="delete-packagelist",
data_hx_delete=f"/list/{pkglist.id}",
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-pencil", "text-xl")),
id="edit-packagelist",
data_hx_post=f"/list/{pkglist.id}/edit",
_class=cls(
"border",
"bg-blue-200",
"hover:bg-blue-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
return make_response(doc.render(), 200)
return make_response(PackageListTableRowNormal(pkglist).doc.render(), 200)
@app.route("/list/<uuid:id>/edit/submit", methods=["POST"])
@app.route("/list/<uuid:id>/edit/submit/", methods=["POST"])
def edit_list_submit(id):
name = request.form["name"]
description = request.form["description"]
if len(name) == 0:
with t.tr(id="pkglist-edit-row") as doc:
with t.td(colspan=3, _class=cls("border-none", "bg-purple-100", "h-10")):
t.p("Name cannot be empty", _class=cls("text-red-400", "text-sm"))
with t.div(_class=cls("flex", "flex-row", "h-full")):
with t.div(
_class=cls(
"box-border" "border",
"border-2",
"border-red-500",
"bg-purple-100",
"mr-1",
)
):
with t.div(_class=cls("h-full")):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
name="name",
value=name,
)
with t.div(
_class=cls(
"border", "border-1", "border-purple-500", "bg-purple-100"
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
name="description",
value=description,
)
t.td(
t.span(_class=cls("mdi", "mdi-cancel", "text-xl")),
id="edit-packagelist-abort",
data_hx_post=f"/list/{id}/edit/cancel",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-content-save", "text-xl")),
id="edit-packagelist-save",
data_hx_post=f"/list/{id}/edit/submit",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
data_hx_include="closest tr",
_class=cls(
"border",
"bg-green-200",
"hover:bg-green-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
return make_response(doc.render(), 200)
error, errormsg = validate_name(name)
if error:
if is_htmx():
return make_response(
PackageListTableRowEdit(error=True, errormsg=errormsg).doc.render(), 200
)
else:
r = make_response("", 303)
r.headers["Location"] = f"/?edit={id}&error=1&msg={errormsg}"
return r
pkglist = PackageList.query.filter_by(id=str(id)).first()
if pkglist is None:
# todo what to do without js?
return make_response("", 404)
pkglist.name = name
pkglist.description = description
try:
pkglist = PackageList.query.filter_by(id=str(id)).first()
if pkglist is None:
return make_response("", 404)
pkglist.name = name
pkglist.description = description
try:
db.session.commit()
except sqlalchemy.exc.IntegrityError:
with t.tr(id="pkglist-edit-row") as doc:
with t.td(
colspan=3, _class=cls("border-none", "bg-purple-100", "h-10")
):
t.p(
f"Name {name} already exists",
_class=cls("text-red-400", "text-sm"),
)
with t.div(_class=cls("flex", "flex-row", "h-full")):
with t.div(
_class=cls(
"box-border" "border",
"border-2",
"border-red-500",
"bg-purple-100",
"mr-1",
)
):
with t.div(_class=cls("h-full")):
t._input(
_class=cls(
"bg-purple-100", "w-full", "h-full", "px-2"
),
type="text",
name="name",
value=name,
)
with t.div(
_class=cls(
"border",
"border-1",
"border-purple-500",
"bg-purple-100",
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
name="description",
value=description,
)
t.td(
t.span(_class=cls("mdi", "mdi-cancel", "text-xl")),
id="edit-packagelist-abort",
data_hx_post=f"/list/{id}/edit/cancel",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-content-save", "text-xl")),
id="edit-packagelist-save",
data_hx_post=f"/list/{id}/edit/submit",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
data_hx_include="closest tr",
_class=cls(
"border",
"bg-green-200",
"hover:bg-green-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
return make_response(doc.render(), 200)
except:
raise
db.session.commit()
except sqlalchemy.exc.IntegrityError:
db.session.rollback()
errormsg = f'Name "{name}" already exists'
if is_htmx():
pkglist.error = True
pkglist.errormsg = errormsg
return make_response(PackageListTableRowEdit(pkglist).doc.render(), 200)
else:
r = make_response("", 303)
r.headers["Location"] = f"/?edit={id}&name={name}&error=1&msg={errormsg}"
return r
with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200")) as doc:
t.td(pkglist.name, _class=cls("border", "px-2")),
t.td(str(pkglist.description), _class=cls("border", "px-2")),
t.td(
t.span(_class=cls("mdi", "mdi-delete", "text-xl")),
id="delete-packagelist",
data_hx_delete=f"/list/{pkglist.id}",
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-pencil", "text-xl")),
id="edit-packagelist",
data_hx_post=f"/list/{pkglist.id}/edit",
_class=cls(
"border",
"bg-blue-200",
"hover:bg-blue-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
return make_response(doc.render(), 200)
def get_edit_list(pkglist):
with t.tr(
_class="h-10",
id="pkglist-edit-row",
**{
"x-data": '{ edit_submit_enabled: document.getElementById("listedit-name").value.trim().length !== 0 }'
},
) as doc:
with t.td(colspan=3, _class=cls("border-none", "bg-purple-100", "h-full")):
with t.div(_class=cls("flex", "flex-row", "h-full")):
with t.div(
_class=cls(
"border",
"border-1",
"border-purple-500",
"bg-purple-100",
"mr-1",
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
id="listedit-name",
name="name",
value=pkglist.name,
**{
"x-on:input": "edit_submit_enabled = $event.srcElement.value.trim().length !== 0;"
},
)
with t.div(
_class=cls(
"border", "border-1", "border-purple-500", "bg-purple-100"
)
):
t._input(
_class=cls("bg-purple-100", "w-full", "h-full", "px-2"),
type="text",
name="description",
value=pkglist.description,
)
t.td(
t.span(_class=cls("mdi", "mdi-cancel", "text-xl")),
id="edit-packagelist-abort",
data_hx_post=f"/list/{pkglist.id}/edit/cancel",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
_class=cls(
"border",
"bg-red-200",
"hover:bg-red-400",
"cursor-pointer",
"w-8",
"text-center",
),
),
t.td(
t.span(_class=cls("mdi", "mdi-content-save", "text-xl")),
id="edit-packagelist-save",
data_hx_post=f"/list/{pkglist.id}/edit/submit",
data_hx_target="#pkglist-edit-row",
data_hx_swap="outerHTML",
data_hx_include="closest #pkglist-edit-row",
_class=cls(
"border",
"bg-green-200",
"hover:bg-green-400",
"cursor-pointer",
"w-8",
"text-center",
),
**{
"x-bind:class": 'edit_submit_enabled || "cursor-not-allowed opacity-50"',
"x-on:htmx:before-request": "(e) => edit_submit_enabled || e.preventDefault()",
},
),
return doc
if is_htmx():
return make_response(PackageListTableRowNormal(pkglist).doc.render(), 200)
else:
r = make_response("", 303)
r.headers["Location"] = "/"
return r
@app.route("/list/<uuid:id>/edit", methods=["POST"])
def edit_list(id):
pkglist = get_packagelist_by_id(id)
return make_response(get_edit_list(pkglist).render(), 200)
out = PackageListTableRowEdit(pkglist).doc
return make_response(out.render(), 200)
@app.route("/list/<uuid:id>", methods=["DELETE"])

1
python_flask/selenium/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/venv/

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
xtightvncviewer 127.0.0.1::5900 -passwd <(printf %s secret | vncpasswd -f)

View File

@@ -0,0 +1,3 @@
helium==3.0.8
selenium==3.141.0
urllib3==1.26.10

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
docker run \
--rm \
--publish 4444:4444 \
--env SE_OPTS="--session-timeout 36000" \
--shm-size="2g" \
--net=host \
--name docker-selenium \
selenium/standalone-firefox:4.3.0-20220706

11
python_flask/selenium/setup.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -p pipefail
python3 -m venv ./venv
source ./venv/bin/activate
python3 -m pip install -r requirements.txt
sudo apt install tigervnc-common xtightvncviewer

66
python_flask/selenium/test.py Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
import time
from helium import *
import selenium
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
opts = selenium.webdriver.firefox.options.Options()
profile = selenium.webdriver.FirefoxProfile()
profile.set_preference("javascript.enabled", "false")
profile.DEFAULT_PREFERENCES['frozen']['javascript.enabled'] = False
opts.profile = profile
driver = selenium.webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=opts,
)
driver.implicitly_wait(0)
Config.implicit_wait_secs = 1
try:
helium.set_driver(driver)
helium.go_to("http://localhost:5000")
assert driver.title == "Packager"
new_entry = Text("Add new package list")
lists_before = find_all(S("table > tbody > tr", below=Text("Package Lists")))
write("newlist", into=TextField(to_right_of="Name"))
write("newlistdesc", into=TextField(to_right_of="Description"))
click(Button("Add"))
lists_after = find_all(S("table > tbody > tr", below=Text("Package Lists")))
assert len(lists_before) == len(lists_after) - 1
nameidx = next(i for i,v in enumerate(find_all(S("table > thead > tr > th"))) if v.web_element.text == "Name")
descidx = next(i for i,v in enumerate(find_all(S("table > thead > tr > th"))) if v.web_element.text == "Description")
new_entry = lists_after[-1]
cells = new_entry.web_element.find_elements_by_tag_name("td")
assert cells[nameidx].text == "newlist"
assert cells[descidx].text == "newlistdesc"
lists_before = lists_after
deletebtn = new_entry.web_element.find_element_by_class_name("mdi-delete")
click(deletebtn)
lists_after = find_all(S("table > tbody > tr", below=Text("Package Lists")))
assert len(lists_before) - 1 == len(lists_after)
import code; code.interact(local=locals())
time.sleep(5)
finally:
driver.quit()