diff --git a/python_flask/.gitignore b/python_flask/.gitignore index c3a4a1c..1540f1b 100644 --- a/python_flask/.gitignore +++ b/python_flask/.gitignore @@ -1,3 +1,3 @@ -/__pycache__/ +__pycache__/ /venv/ *.sqlite diff --git a/python_flask/app.js b/python_flask/app.js deleted file mode 100644 index a32f61b..0000000 --- a/python_flask/app.js +++ /dev/null @@ -1,16 +0,0 @@ -document.body.addEventListener('htmx:responseError', function(evt) { - console.log(evt.detail); - let detail = evt.detail; - let responsecode = detail.xhr.status; - if (responsecode == 400 && detail.requestConfig.path === "/list/") { - alert(detail.xhr.response) - console.log(evt.detail.xhr.repsonse); - } -}); - -// document.body.addEventListener('htmx:beforeRequest', function(evt) { -// let detail = evt.detail; -// console.log(evt.detail); -// return false; -// }); -console.log("Added event listener"); diff --git a/python_flask/packager/__init__.py b/python_flask/packager/__init__.py new file mode 100644 index 0000000..81b5fc4 --- /dev/null +++ b/python_flask/packager/__init__.py @@ -0,0 +1,55 @@ +import uuid +import sqlalchemy +from flask import Flask + +from .helpers import * + +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///./db.sqlite" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +db = SQLAlchemy(app) + +from packager.models import * +import packager.views + + +db.create_all() +try: + db.session.add( + PackageList( + id="ab2f16c2-d5f5-460b-b149-0fc9eec12887", + name="EDC", + description="What you always carry", + ) + ) + db.session.add( + PackageList( + id="9f3a72cd-7e30-4263-bd52-92fb7bed1242", + name="Camping", + description="For outdoors", + ) + ) + + db.session.add( + PackageListItem( + id="4c08f0d5-583e-4882-8bea-2b2faab61fff", + name="Taschenmesser", + description="", + packagelist_id="ab2f16c2-d5f5-460b-b149-0fc9eec12887", + ) + ) + + db.session.add( + PackageListItem( + id="f7fe1c35-23c8-4e57-bec0-56212cff940a", + name="Geldbeutel", + description="", + packagelist_id="ab2f16c2-d5f5-460b-b149-0fc9eec12887", + ) + ) + + db.session.commit() +except sqlalchemy.exc.IntegrityError: + pass diff --git a/python_flask/packager/components/NewPackageList.py b/python_flask/packager/components/NewPackageList.py new file mode 100644 index 0000000..a4c4f35 --- /dev/null +++ b/python_flask/packager/components/NewPackageList.py @@ -0,0 +1,105 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +def NewPackageList(name=None, description=None, error=False, errormsg=None): + assert not (error and not errormsg) + with t.form( + id="new-pkglist", + name="new_pkglist", + data_hx_post="/list/", + data_hx_target="#pkglist-manager", + data_hx_swap="outerHTML", + _class=cls("mt-8", "p-5", "border-2", "border-gray-200"), + **{"x-on:htmx:before-request": "(e) => submit_enabled || e.preventDefault()"}, + ) 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")) + t.p("Add new package list", _class=cls("inline", "text-xl")) + with t.div(_class=cls("w-11/12", "mx-auto")): + with t.div(_class=cls("pb-8")): + with t.div( + _class=cls("flex", "flex-row", "justify-center", "items-start") + ): + t.label( + "Name", + _for="listname", + _class=cls("font-bold", "w-1/2", "p-2", "text-center"), + ) + with t.div(_class=cls("w-1/2")): + t._input( + type="text", + id="listname", + name="name", + **{"value": name} if name is not None else {}, + data_hx_target="#new-pkglist", + data_hx_post="/list/name/validate", + data_hx_swap="outerHTML", + _class=cls( + "block", + "w-full", + "p-2", + "bg-gray-50", + "appearance-none" if error else None, + "border-2", + "border-red-400" if error else "border-gray-300", + "rounded", + "focus:outline-none", + "focus:bg-white", + "focus:border-purple-500" if not error else None, + ), + **{ + "x-on:input": "submit_enabled = $event.srcElement.value.trim().length !== 0;" + }, + ) + t.p( + errormsg, _class=cls("mt-1", "text-red-400", "text-sm") + ) if error else None + with t.div( + _class=cls("flex", "flex-row", "justify-center", "items-center", "pb-8") + ): + t.label( + "Description", + _for="listdesc", + _class=cls("font-bold", "w-1/2", "text-center"), + ) + t._input( + type="text", + id="listdesc", + name="description", + **{"value": description} if description is not None else {}, + _class=cls( + "block", + "w-1/2", + "p-2", + "bg-gray-50", + "appearance-none", + "border-2", + "border-gray-300", + "rounded", + "focus:outline-none", + "focus:bg-white", + "focus:border-purple-500", + ), + ) + t._input( + type="submit", + value="Add", + **{ + "x-bind:class": 'submit_enabled ? "" : "cursor-not-allowed opacity-50"' + }, + _class=cls( + "py-2", + "border-2", + "rounded", + "border-gray-300", + "mx-auto", + "w-full", + "hover:border-purple-500" if not error else None, + "hover:bg-purple-200" if not error else None, + ), + ) + return doc diff --git a/python_flask/packager/components/PackageListManager.py b/python_flask/packager/components/PackageListManager.py new file mode 100644 index 0000000..274c688 --- /dev/null +++ b/python_flask/packager/components/PackageListManager.py @@ -0,0 +1,30 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from . import NewPackageList, PackageListTable + +from ..helpers import * + + +class PackageListManager: + def __init__( + 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: + PackageListTable(pkglists), + NewPackageList( + name=name, description=description, error=error, errormsg=errormsg + ) + + self.doc = doc + + def render(self): + return self.doc.render() diff --git a/python_flask/packager/components/PackageListTable.py b/python_flask/packager/components/PackageListTable.py new file mode 100644 index 0000000..742c57f --- /dev/null +++ b/python_flask/packager/components/PackageListTable.py @@ -0,0 +1,80 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +def PackageListTable(pkglists): + doc = t.div(id="packagelist-table") + with doc: + t.h1("Package Lists", _class=cls("text-2xl", "mb-5")) + with t.table( + id="packagelist-table", + _class=cls( + "table", + "table-auto", + # "border-separate", + "border-collapse", + "border-spacing-0", + "border", + "w-full", + ), + ): + with t.thead(_class=cls("bg-gray-200")): + t.tr( + t.th("Name", _class=cls("border", "p-2")), + t.th("Description", _class=cls("border", "p-2")), + t.th(_class=cls("border p-2")), + t.th(_class=cls("border p-2")), + t.th(_class=cls("border p-2")), + _class="h-10", + ) + with t.tbody(data_hx_target="closest tr", data_hx_swap="outerHTML"): + for pkglist in pkglists: + t.tr( + 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", + ), + ), + t.td( + t.span(_class=cls("mdi", "mdi-arrow-right", "text-xl")), + id="edit-packagelist", + # data_hx_post=f"/list/{pkglist.id}/edit", + _class=cls( + "border", + "bg-green-200", + "hover:bg-green-400", + "cursor-pointer", + "w-8", + "text-center", + ), + ), + _class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200"), + ) + + return doc diff --git a/python_flask/packager/components/__init__.py b/python_flask/packager/components/__init__.py new file mode 100644 index 0000000..cfc6e69 --- /dev/null +++ b/python_flask/packager/components/__init__.py @@ -0,0 +1,3 @@ +from .NewPackageList import NewPackageList +from .PackageListTable import PackageListTable +from .PackageListManager import PackageListManager diff --git a/python_flask/packager/helpers.py b/python_flask/packager/helpers.py new file mode 100644 index 0000000..319fcbd --- /dev/null +++ b/python_flask/packager/helpers.py @@ -0,0 +1,2 @@ +def cls(*args): + return " ".join([a for a in args if a is not None]) diff --git a/python_flask/packager/js/app.js b/python_flask/packager/js/app.js new file mode 100644 index 0000000..b0f02ec --- /dev/null +++ b/python_flask/packager/js/app.js @@ -0,0 +1,3 @@ +document.body.addEventListener('htmx:responseError', function(evt) { + console.log(evt.detail); +}); diff --git a/python_flask/packager/models.py b/python_flask/packager/models.py new file mode 100644 index 0000000..75becab --- /dev/null +++ b/python_flask/packager/models.py @@ -0,0 +1,19 @@ +from . import db + + +class PackageList(db.Model): + __tablename__ = "packagelist" + id = db.Column(db.String(36), primary_key=True) + name = db.Column(db.Text, unique=True, nullable=False) + description = db.Column(db.Text) + items = db.relationship("PackageListItem", backref="packagelist", lazy=True) + + +class PackageListItem(db.Model): + __tablename__ = "packagelistitem" + id = db.Column(db.String(36), primary_key=True) + name = db.Column(db.Text, unique=True, nullable=False) + description = db.Column(db.Text) + packagelist_id = db.Column( + db.String(36), db.ForeignKey("packagelist.id"), nullable=False + ) diff --git a/python_flask/app.py b/python_flask/packager/views.py similarity index 61% rename from python_flask/app.py rename to python_flask/packager/views.py index e661f36..2fe58e3 100644 --- a/python_flask/app.py +++ b/python_flask/packager/views.py @@ -1,46 +1,18 @@ -import uuid import sqlalchemy -from flask import Flask, request, make_response +from . import app +from .models import * +from .helpers import * -from flask_sqlalchemy import SQLAlchemy +import uuid +import os import dominate import dominate.tags as t from dominate.util import raw +from .components import PackageListManager -app = Flask(__name__) -app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///./db.sqlite" -app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -db = SQLAlchemy(app) - - -class PackageList(db.Model): - id = db.Column(db.String(36), primary_key=True) - name = db.Column(db.Text, unique=True) - description = db.Column(db.Text) - - -db.create_all() -try: - db.session.add( - PackageList( - id="ab2f16c2-d5f5-460b-b149-0fc9eec12887", - name="EDC", - description="What you always carry", - ) - ) - db.session.add( - PackageList( - id="9f3a72cd-7e30-4263-bd52-92fb7bed1242", - name="Camping", - description="For outdoors", - ) - ) - - db.session.commit() -except sqlalchemy.exc.IntegrityError: - pass +from flask import request, make_response def get_packagelists(): @@ -71,202 +43,21 @@ def delete_packagelist(id): return True -def pkglist_table(): - pkglists = get_packagelists() - doc = t.div(id="packagelist-table") - with doc: - t.h1("Package Lists", _class=cls("text-2xl", "mb-5")) - with t.table( - id="packagelist-table", - _class=cls( - "table", - "table-auto", - # "border-separate", - "border-collapse", - "border-spacing-0", - "border", - "w-full", - ), - ): - with t.thead(_class=cls("bg-gray-200")): - t.tr( - t.th("Name", _class=cls("border", "p-2")), - t.th("Description", _class=cls("border", "p-2")), - t.th(_class=cls("border p-2")), - t.th(_class=cls("border p-2")), - _class="h-10", - ) - with t.tbody(data_hx_target="closest tr", data_hx_swap="outerHTML"): - for pkglist in pkglists: - t.tr( - 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", - ), - ), - _class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200"), - ) - - return doc - - -def cls(*args): - return " ".join([a for a in args if a is not None]) - - -def new_pkglist_form(name=None, description=None, error=False, errormsg=None): - assert not (error and not errormsg) - with t.form( - id="new-pkglist", - name="new_pkglist", - data_hx_post="/list/", - data_hx_target="#pkglist-manager", - data_hx_swap="outerHTML", - _class=cls("mt-8", "p-5", "border-2", "border-gray-200"), - **{"x-on:htmx:before-request": "(e) => submit_enabled || e.preventDefault()"}, - ) 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")) - t.p("Add new package list", _class=cls("inline", "text-xl")) - with t.div(_class=cls("w-11/12", "mx-auto")): - with t.div(_class=cls("pb-8")): - with t.div( - _class=cls("flex", "flex-row", "justify-center", "items-start") - ): - t.label( - "Name", - _for="listname", - _class=cls("font-bold", "w-1/2", "p-2", "text-center"), - ) - with t.div(_class=cls("w-1/2")): - t._input( - type="text", - id="listname", - name="name", - **{"value": name} if name is not None else {}, - data_hx_target="#new-pkglist", - data_hx_post="/list/name/validate", - data_hx_swap="outerHTML", - _class=cls( - "block", - "w-full", - "p-2", - "bg-gray-50", - "appearance-none" if error else None, - "border-2", - "border-red-400" if error else "border-gray-300", - "rounded", - "focus:outline-none", - "focus:bg-white", - "focus:border-purple-500" if not error else None, - ), - **{ - "x-on:input": "submit_enabled = $event.srcElement.value.trim().length !== 0;" - }, - ) - t.p( - errormsg, _class=cls("mt-1", "text-red-400", "text-sm") - ) if error else None - with t.div( - _class=cls("flex", "flex-row", "justify-center", "items-center", "pb-8") - ): - t.label( - "Description", - _for="listdesc", - _class=cls("font-bold", "w-1/2", "text-center"), - ) - t._input( - type="text", - id="listdesc", - name="description", - **{"value": description} if description is not None else {}, - _class=cls( - "block", - "w-1/2", - "p-2", - "bg-gray-50", - "appearance-none", - "border-2", - "border-gray-300", - "rounded", - "focus:outline-none", - "focus:bg-white", - "focus:border-purple-500", - ), - ) - t._input( - type="submit", - value="Add", - **{ - "x-bind:class": 'submit_enabled ? "" : "cursor-not-allowed opacity-50"' - }, - _class=cls( - "py-2", - "border-2", - "rounded", - "border-gray-300", - "mx-auto", - "w-full", - "hover:border-purple-500" if not error else None, - "hover:bg-purple-200" if not error else None, - ), - ) - return doc - - -def pkglist_manager(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: - pkglist_table() - new_pkglist_form( - name=name, description=description, error=error, errormsg=errormsg - ) - return doc - - @app.route("/") def root(): doc = dominate.document(title="My cool title") 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.min.js", defer=True) + 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("app.js").read())) - pkglist_manager() + + t.script(raw(open(os.path.join(app.root_path, "js/app.js")).read())) + PackageListManager(get_packagelists()) return make_response(doc.render(), 200) @@ -284,8 +75,12 @@ def add_new_list(): errormsg = f'Name "{name}" already exists' return make_response( - pkglist_manager( - name=name, description=description, error=error, errormsg=errormsg + PackageListManager( + get_packagelists(), + name=name, + description=description, + error=error, + errormsg=errormsg, ).render(), 200, ) @@ -361,7 +156,7 @@ def edit_list_submit(id): description = request.form["description"] if len(name) == 0: with t.tr(id="pkglist-edit-row") as doc: - with t.td(colspan=2, _class=cls("border-none", "bg-purple-100", "h-10")): + 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( @@ -435,7 +230,7 @@ def edit_list_submit(id): except sqlalchemy.exc.IntegrityError: with t.tr(id="pkglist-edit-row") as doc: with t.td( - colspan=2, _class=cls("border-none", "bg-purple-100", "h-10") + colspan=3, _class=cls("border-none", "bg-purple-100", "h-10") ): t.p( f"Name {name} already exists", @@ -546,10 +341,10 @@ def get_edit_list(pkglist): _class="h-10", id="pkglist-edit-row", **{ - "x-data": '{ edit_submit_enabled: document.getElementById("listedit-name").value.trim().length() !== 0 }' + "x-data": '{ edit_submit_enabled: document.getElementById("listedit-name").value.trim().length !== 0 }' }, ) as doc: - with t.td(colspan=2, _class=cls("border-none", "bg-purple-100", "h-full")): + 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( diff --git a/python_flask/run.sh b/python_flask/run.sh index 17a1ab2..5fc5659 100755 --- a/python_flask/run.sh +++ b/python_flask/run.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash source ./venv/bin/activate + +export FLASK_APP=packager +export FLASK_ENV=development + python3 -m flask run --reload