diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b402e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.bundle diff --git a/python_flask/.gitignore b/python_flask/.gitignore index 1540f1b..2341ea3 100644 --- a/python_flask/.gitignore +++ b/python_flask/.gitignore @@ -1,3 +1,6 @@ __pycache__/ /venv/ *.sqlite +*.sqlite3 +*.bak +*.bundle diff --git a/python_flask/NOTES.md b/python_flask/NOTES.md new file mode 100644 index 0000000..fca5cd4 --- /dev/null +++ b/python_flask/NOTES.md @@ -0,0 +1,42 @@ +# ideas + +Trip info: +* Itinerary +* Members + +--- + +Item info: + +Comments +Links +n:1 Product + +--- + +Item groups that can be selected together. 1:1 product. + +--- + +State management. + +--- + +Item comments per trip (that are added to item overview page) + +"This item was already on the following trips:" + +--- + +Todos, with preparation time window. + +--- + +Review when setting to "done", review per item and for the whole trip. + +"Would take again" (default yes) + +# todos + +Category CRUD + diff --git a/python_flask/packager.bundle b/python_flask/packager.bundle deleted file mode 100644 index 37ce34d..0000000 Binary files a/python_flask/packager.bundle and /dev/null differ diff --git a/python_flask/packager/__init__.py b/python_flask/packager/__init__.py index 11cad38..a1bc76c 100644 --- a/python_flask/packager/__init__.py +++ b/python_flask/packager/__init__.py @@ -2,6 +2,7 @@ import uuid import sqlalchemy import csv from flask import Flask +from flask_migrate import Migrate from .helpers import * @@ -10,56 +11,11 @@ from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{app.root_path}/../db.sqlite" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + db = SQLAlchemy(app) +migrate = Migrate(app, db, render_as_batch=True) from packager.models import * import packager.views db.create_all() - -try: - categories = ( - {"id": uuid.uuid4(), "name": "Sleeping"}, - {"id": uuid.uuid4(), "name": "Shelter"}, - {"id": uuid.uuid4(), "name": "Fire"}, - {"id": uuid.uuid4(), "name": "Cooking"}, - {"id": uuid.uuid4(), "name": "Water"}, - {"id": uuid.uuid4(), "name": "Protection"}, - {"id": uuid.uuid4(), "name": "Tools"}, - {"id": uuid.uuid4(), "name": "Insulation"}, - {"id": uuid.uuid4(), "name": "Electronics"}, - {"id": uuid.uuid4(), "name": "Carry"}, - {"id": uuid.uuid4(), "name": "Medic"}, - {"id": uuid.uuid4(), "name": "Hygiene"}, - ) - - for category in categories: - db.session.add( - PackageListItemCategory( - id=str(category['id']), - name=category['name'], - description="", - ) - ) - - with open("./items.csv") as csvfile: - reader = csv.reader(csvfile, delimiter=',') - for row in reader: - print(row) - (name, category, weight) = row - - db.session.add( - PackageListItem( - id=str(uuid.uuid4()), - name=name, - description="", - weight=weight, - category_id=str([c['id'] for c in categories if c['name'] == category][0]) - ) - ) - - - print("db init done") - db.session.commit() -except sqlalchemy.exc.IntegrityError: - pass diff --git a/python_flask/packager/components/Base.py b/python_flask/packager/components/Base.py new file mode 100644 index 0000000..c7d9076 --- /dev/null +++ b/python_flask/packager/components/Base.py @@ -0,0 +1,55 @@ +import os + +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +class Base: + def __init__(self, element, root_path, active_page=None): + 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())) + with t.header( + _class=cls( + "bg-gray-200", + "p-5", + "flex", + "flex-row", + "flex-nowrap", + "justify-between", + "items-center", + ) + ): + t.span("Packager", _class=cls("text-xl", "font-semibold")) + with t.nav( + _class=cls("grow", "flex", "flex-row", "justify-center", "gap-x-6") + ): + basecls = ["text-lg"] + activecls = ["font-bold", "underline"] + t.a( + "Inventory", + href="/inventory/", + _class=cls( + *basecls, *(activecls if active_page == "inventory" else []) + ), + ) + t.a( + "Trips", + href="/trips/", + _class=cls( + *basecls, *(activecls if active_page == "trips" else []) + ), + ) + doc.add(element.doc) + self.doc = doc diff --git a/python_flask/packager/components/CategoryList.py b/python_flask/packager/components/CategoryList.py deleted file mode 100644 index 230e2cd..0000000 --- a/python_flask/packager/components/CategoryList.py +++ /dev/null @@ -1,35 +0,0 @@ -import dominate -import dominate.tags as t -from dominate.util import raw - -from ..helpers import * - -def CategoryList(categories): - with t.div(id="packagelist-table") as doc: - t.h1("Categories", _class=cls("text-2xl", "mb-5")) - with t.table( - id="packagelist-table", - _class=cls( - "table", - "table-auto", - "border-collapse", - "border-spacing-0", - "border", - "w-full", - ), - ): - with t.tbody() as b: - for category in categories: - with t.tr(_class=cls("h-10", "hover:bg-purple-200")) as doc: - with t.td(_class=cls("border", "p-0", "m-0")): - t.a( - category.name, - id="select-category", - # data_hx_post=f"/list/{pkglist.id}/edit", - href=f"/category/{category.id}", - # data_hx_target="closest tr", - # data_hx_swap="outerHTML", - _class=cls("block", "p-2", "m-2"), - ) - - return doc diff --git a/python_flask/packager/components/Home.py b/python_flask/packager/components/Home.py index bc22b30..3c28ed8 100644 --- a/python_flask/packager/components/Home.py +++ b/python_flask/packager/components/Home.py @@ -4,19 +4,15 @@ import dominate import dominate.tags as t from dominate.util import raw +from ..helpers import * + 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) + def __init__(self): + with t.div(id="home", _class=cls("p-8", "max-w-xl")) as doc: + with t.p(): + t.a("Inventory", href="/inventory/") + with t.p(): + t.a("Trips", href="/trips/") + self.doc = doc diff --git a/python_flask/packager/components/InventoryCategoryList.py b/python_flask/packager/components/InventoryCategoryList.py new file mode 100644 index 0000000..2c4d372 --- /dev/null +++ b/python_flask/packager/components/InventoryCategoryList.py @@ -0,0 +1,85 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +def InventoryCategoryList(categories): + with t.div() as doc: + t.h1("Categories", _class=cls("text-2xl", "mb-5")) + with t.table( + _class=cls( + "table", + "table-auto", + "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("Weight", _class=cls("border", "p-2")), + _class="h-10", + ) + with t.tbody() as b: + biggest_category_weight = max( + [sum([i.weight for i in c.items]) for c in categories] or [0] + ) + for category in categories: + with t.tr( + _class=cls("h-10", "hover:bg-purple-100", "m-3", "h-full") + ) as doc: + with t.td( + _class=cls( + "border", + "p-0", + "m-0", + *["font-bold"] if category.active else [], + ) + ): + t.a( + category.name, + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/inventory/category/{category.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + with t.td( + _class=cls("border", "p-0", "m-0"), + style="position:relative;", + ): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/inventory/category/{category.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ): + weight = sum([i.weight for i in category.items]) + width = int(weight / biggest_category_weight * 100) + t.p(weight) + t.div( + _class=cls("bg-blue-600", "h-1.5"), + style=f"width: {width}%;position:absolute;left:0;bottom:0;right:0;", + ) + # t.progress(max=biggest_category_weight, value=weight) + with t.tr( + _class=cls( + "h-10", "hover:bg-purple-200", "bg-gray-300", "font-bold" + ) + ) as doc: + with t.td(_class=cls("border", "p-0", "m-0")): + t.a("Sum", _class=cls("block", "p-2", "m-2")) + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + sum([sum([i.weight for i in c.items]) for c in categories]), + _class=cls("block", "p-2", "m-2"), + ) + + return doc diff --git a/python_flask/packager/components/InventoryItemDetails.py b/python_flask/packager/components/InventoryItemDetails.py new file mode 100644 index 0000000..25ade23 --- /dev/null +++ b/python_flask/packager/components/InventoryItemDetails.py @@ -0,0 +1,143 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + +import urllib + +class InventoryItemInfoEditRow: + def __init__(self, baseurl, name, value, attribute, inputtype="text"): + + with t.tr() as doc: + t.form( + id=f"edit-{attribute}", + action=urllib.parse.urljoin(baseurl, f"edit/{attribute}/submit/"), + target="_self", + method="post", + # data_hx_post=f"/list/{pkglist.id}/edit/submit", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ) + with t.tr(_class=cls("h-full")): + t.td(name, _class=cls("border", "p-2", "h-full")) + with t.td(_class=cls("border", "p-0")): + t._input( + _class=cls("bg-blue-200", "w-full", "h-full", "px-2"), + type=inputtype, + id="item-edit-name", + form=f"edit-{attribute}", + name=attribute, + value=value, + ) + + with t.td( + _class=cls( + "border", + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer", + "w-8", + "text-center", + ), + id=f"edit-{attribute}-abort", + ): + with t.a(href=baseurl): + with t.button(): + t.span(_class=cls("mdi", "mdi-cancel", "text-xl")), + with t.td( + id=f"edit-{attribute}-save", + _class=cls( + "border", + "bg-green-200", + "hover:bg-green-400", + "cursor-pointer", + "w-8", + "text-center", + ), + ): + with t.button(type="submit", form=f"edit-{attribute}"): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + self.doc = doc + + +class InventoryItemInfoNormalRow: + def __init__(self, editable, baseurl, name, value, attribute): + with t.tr() as doc: + t.td(name, _class=cls("border", "p-2")) + t.td(value, _class=cls("border", "p-2")) + if editable: + with t.td( + _class=cls( + "border", + "bg-blue-200", + "hover:bg-blue-400", + "cursor-pointer", + "w-8", + "text-center", + ) + ): + if editable: + with t.a( + # data_hx_post=f"/item/{item.id}/edit", + href=f"?edit={attribute}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-pencil", "text-xl")), + + self.doc = doc + + +class InventoryItemInfo: + def __init__(self, item, edit, baseurl): + with t.table( + id="trip-table", + _class=cls( + "table", + "table-auto", + "border-collapse", + "border-spacing-0", + "border", + "w-full", + ), + ) as doc: + with t.tbody() as b: + if edit == "name": + InventoryItemInfoEditRow(baseurl, "Name", item.name, "name") + else: + InventoryItemInfoNormalRow( + True, baseurl, "Name", item.name, "name" + ) + + if edit == "weight": + InventoryItemInfoEditRow( + baseurl, + "Weight", + str(item.weight), + "weight", + inputtype="number", + ) + else: + InventoryItemInfoNormalRow( + True, baseurl, "Weight", str(item.weight), "weight" + ) + + self.doc = doc + + + +class InventoryItemDetails: + def __init__( + self, + item, edit, baseurl + ): + with t.div(_class=cls("p-8") + ) as doc: + t.h1("Item", _class=cls("text-2xl", "font-semibold")) + with t.div(_class=cls("my-6")): + InventoryItemInfo(item, edit, baseurl) + + self.doc = doc + diff --git a/python_flask/packager/components/InventoryItemList.py b/python_flask/packager/components/InventoryItemList.py new file mode 100644 index 0000000..9194c1b --- /dev/null +++ b/python_flask/packager/components/InventoryItemList.py @@ -0,0 +1,194 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +class InventoryItemRowEdit(object): + def __init__(self, item, biggest_item_weight): + with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-100")) as doc: + with t.td(colspan=2, _class=cls("border-none", "bg-purple-100", "h-10")): + with t.div(): + t.form( + id="edit-item", + action=f"/inventory/item/{item.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", + ) + with t.div(_class=cls("flex", "flex-row", "h-full")): + with t.span( + _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="item-edit-name", + form="edit-item", + name="name", + value=item.name, + **{ + "x-on:input": "edit_submit_enabled = $event.srcElement.value.trim().length !== 0;" + }, + ) + with t.span( + _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", + id="item-edit-weight", + name="weight", + form="edit-item", + value=item.weight, + ) + with t.td( + _class=cls( + "border", + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer", + "w-8", + "text-center", + ), + id="edit-item-abort", + ): + with t.a( + href=f"/inventory/category/{item.category.id}", + # data_hx_post=f"/item/{item.id}/edit/cancel", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-cancel", "text-xl")), + with t.td( + id="edit-item-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-item"): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + self.doc = doc + + +class InventoryItemRowNormal(object): + def __init__(self, item, biggest_item_weight): + with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-100")) as doc: + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + item.name, + href=f"/inventory/item/{item.id}/", + _class=cls("p-2", "w-full", "inline-block"), + ) + width = int(item.weight / biggest_item_weight * 100) + with t.td(_class=cls("border", "px-2"), style="position:relative;"): + t.p(str(item.weight)) + t.div( + _class=cls("bg-blue-600", "h-1.5"), + style=";".join( + [ + f"width: {width}%", + "position:absolute", + "left:0", + "bottom:0", + "right:0", + ] + ), + ) + with t.td( + _class=cls( + "border", + "bg-blue-200", + "hover:bg-blue-400", + "cursor-pointer", + "w-8", + "text-center", + ) + ): + with t.a( + # data_hx_post=f"/item/{item.id}/edit", + href=f"?edit={item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + id="start-edit-item", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-pencil", "text-xl")), + with t.td( + _class=cls( + "border", + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer", + "w-8", + "text-center", + ) + ): + with t.a( + # data_hx_delete=f"/item/{item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + href=f"/inventory/item/{item.id}/delete" + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-delete", "text-xl")) + self.doc = doc + + +class InventoryItemRow(object): + def __init__(self, item, biggest_item_weight): + if item.edit: + self.doc = InventoryItemRowEdit(item, biggest_item_weight) + else: + self.doc = InventoryItemRowNormal(item, biggest_item_weight) + + +def InventoryItemList(items): + with t.div() as doc: + t.h1("Items", _class=cls("text-2xl", "mb-5")) + with t.table( + id="item-table", + _class=cls( + "table", + "table-auto", + "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("Weight", _class=cls("border", "p-2")), + _class="h-10", + ) + with t.tbody() as b: + biggest_item_weight = max([i.weight for i in items] or [0]) + if biggest_item_weight <= 0: + biggest_item_weight = 1 + for item in items: + InventoryItemRow(item, biggest_item_weight).doc + + return doc diff --git a/python_flask/packager/components/InventoryItemManager.py b/python_flask/packager/components/InventoryItemManager.py new file mode 100644 index 0000000..ea41e72 --- /dev/null +++ b/python_flask/packager/components/InventoryItemManager.py @@ -0,0 +1,33 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from .InventoryItemList import InventoryItemList +from .InventoryCategoryList import InventoryCategoryList +from .InventoryNewItem import InventoryNewItem + +from ..helpers import * + + +class InventoryItemManager: + def __init__( + self, + categories, + items, + active_category, + name=None, + description=None, + error=False, + errormsg=None, + ): + assert not (error and not errormsg) + with t.div( + id="pkglist-item-manager", _class=cls("p-8", "grid", "grid-cols-4", "gap-3") + ) as doc: + with t.div(_class=cls("col-span-2")): + InventoryCategoryList(categories), + with t.div(_class=cls("col-span-2")): + InventoryItemList(items), + InventoryNewItem(categories, active_category) + + self.doc = doc diff --git a/python_flask/packager/components/InventoryNewItem.py b/python_flask/packager/components/InventoryNewItem.py new file mode 100644 index 0000000..427d8a5 --- /dev/null +++ b/python_flask/packager/components/InventoryNewItem.py @@ -0,0 +1,152 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +def InventoryNewItem(categories, active_category, name=None, weight=None): + with t.form( + id="new-item", + name="new_item", + # data_hx_post="/list/", + # data_hx_target="#item-manager", + # data_hx_swap="outerHTML", + action="/inventory/item/", + 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-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")) + t.p("Add new item", _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="item-name", + _class=cls("font-bold", "w-1/2", "p-2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + t._input( + type="text", + id="item-name", + name="name", + **{"value": name} if name is not None else {}, + # data_hx_target="#new-item", + # data_hx_post="/item/name/validate", + # data_hx_swap="outerHTML", + # data_hx_trigger="changed", + _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( + "Weight", + _for="item-weight", + _class=cls("font-bold", "w-1/2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + t._input( + type="text", + id="item-weight", + name="weight", + **{"value": weight} if weight is not None else {}, + _class=cls( + "block", + "w-full", + "p-2", + "bg-gray-50", + "appearance-none", + "border-2", + "border-gray-300", + "rounded", + "focus:outline-none", + "focus:bg-white", + "focus:border-purple-500", + ), + ) + with t.div( + _class=cls("flex", "flex-row", "justify-center", "items-center", "pb-8") + ): + t.label( + "Category", + _for="item-category", + _class=cls("font-bold", "w-1/2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + with t.select( + id="item-category", + name="category", + _class=cls( + "block", + "w-full", + "p-2", + "bg-gray-50", + # "appearance-none", + "border-2", + "border-gray-300", + "rounded", + "focus:outline-none", + "focus:bg-white", + "focus:border-purple-500", + ), + ): + for category in categories: + if active_category and category.id == active_category.id: + t.option( + category.name, value=category.id, selected=True + ) + else: + t.option(category.name, value=category.id) + t._input( + type="submit", + value="Add", + # **{ + # "x-bind:class": 'submit_enabled ? "cursor-pointer" : "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/ItemList.py b/python_flask/packager/components/ItemList.py deleted file mode 100644 index b90af4e..0000000 --- a/python_flask/packager/components/ItemList.py +++ /dev/null @@ -1,33 +0,0 @@ -import dominate -import dominate.tags as t -from dominate.util import raw - -from ..helpers import * - -def ItemList(items): - with t.div(id="packagelist-table") as doc: - t.h1("Items", _class=cls("text-2xl", "mb-5")) - with t.table( - id="packagelist-table", - _class=cls( - "table", - "table-auto", - "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("Weight", _class=cls("border", "p-2")), - _class="h-10", - ) - with t.tbody() as b: - for item in items: - with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-200")) as doc: - t.td(item.name, _class=cls("border", "px-2")), - t.td(str(item.weight), _class=cls("border", "px-2")), - - return doc diff --git a/python_flask/packager/components/NewPackageList.py b/python_flask/packager/components/NewPackageList.py deleted file mode 100644 index 64a7257..0000000 --- a/python_flask/packager/components/NewPackageList.py +++ /dev/null @@ -1,120 +0,0 @@ -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", - action="/list/", - 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-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")) - 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.span(_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", - data_hx_trigger="changed", - _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"), - ) - with t.span(_class=cls("w-1/2")): - t._input( - type="text", - id="listdesc", - name="description", - **{"value": description} if description is not None else {}, - _class=cls( - "block", - "w-full", - "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-pointer" : "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/NewTrip.py b/python_flask/packager/components/NewTrip.py new file mode 100644 index 0000000..04af8d5 --- /dev/null +++ b/python_flask/packager/components/NewTrip.py @@ -0,0 +1,145 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +def NewTrip(name=None): + with t.form( + id="new-trip", + name="new_trip", + # data_hx_post="/list/", + # data_hx_target="#trip-manager", + # data_hx_swap="outerHTML", + action="/trip/", + 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-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", "trips-center")): + t.span(_class=cls("mdi", "mdi-playlist-plus", "text-2xl", "mr-4")) + t.p("Add new trip", _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", "trips-start") + ): + t.label( + "Name", + _for="trip-name", + _class=cls("font-bold", "w-1/2", "p-2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + t._input( + type="text", + id="trip-name", + name="name", + **{"value": name} if name is not None else {}, + # data_hx_target="#new-trip", + # data_hx_post="/trip/name/validate", + # data_hx_swap="outerHTML", + # data_hx_trigger="changed", + _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", "trips-center", "pb-8") + ): + t.label( + "Start date", + _for="start-date", + _class=cls("font-bold", "w-1/2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + t._input( + type="date", + id="start-date", + name="start-date", + _class=cls( + "block", + "w-full", + "p-2", + "bg-gray-50", + "appearance-none", + "border-2", + "border-gray-300", + "rounded", + "focus:outline-none", + "focus:bg-white", + "focus:border-purple-500", + ), + ) + with t.div( + _class=cls("flex", "flex-row", "justify-center", "trips-center", "pb-8") + ): + t.label( + "End date", + _for="end-date", + _class=cls("font-bold", "w-1/2", "text-center"), + ) + with t.span(_class=cls("w-1/2")): + t._input( + type="date", + id="end-date", + name="end-date", + _class=cls( + "block", + "w-full", + "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-pointer" : "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/PackageListItemManager.py b/python_flask/packager/components/PackageListItemManager.py deleted file mode 100644 index 85cd0b2..0000000 --- a/python_flask/packager/components/PackageListItemManager.py +++ /dev/null @@ -1,21 +0,0 @@ -import dominate -import dominate.tags as t -from dominate.util import raw - -from . import CategoryList, ItemList - -from ..helpers import * - - -class PackageListItemManager: - def __init__( - self, categories, items, name=None, description=None, error=False, errormsg=None - ): - assert not (error and not errormsg) - with t.div(id="pkglist-item-manager", _class=cls("p-8", "grid", "grid-cols-4", "gap-3")) as doc: - with t.div(_class=cls("col-span-1")): - CategoryList(categories), - with t.div(_class=cls("col-span-3")): - ItemList(items), - - self.doc = doc diff --git a/python_flask/packager/components/PackageListManager.py b/python_flask/packager/components/PackageListManager.py deleted file mode 100644 index a3f4cbf..0000000 --- a/python_flask/packager/components/PackageListManager.py +++ /dev/null @@ -1,21 +0,0 @@ -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")) as doc: - PackageListTable(pkglists), - NewPackageList( - name=name, description=description, error=error, errormsg=errormsg - ) - - self.doc = doc diff --git a/python_flask/packager/components/PackageListTable.py b/python_flask/packager/components/PackageListTable.py deleted file mode 100644 index ad8a309..0000000 --- a/python_flask/packager/components/PackageListTable.py +++ /dev/null @@ -1,197 +0,0 @@ -import dominate -import dominate.tags as t -from dominate.util import raw - -from ..helpers import * - - -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.span( - _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.span( - _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", - ): - with t.button(): - 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")), - t.td(str(pkglist.description), _class=cls("border", "px-2")), - with t.td( - _class=cls( - "border", - "bg-red-200", - "hover:bg-red-400", - "cursor-pointer", - "w-8", - "text-center", - ) - ): - with t.a( - data_hx_delete=f"/list/{pkglist.id}", - data_hx_target="closest tr", - data_hx_swap="outerHTML", - href=f"/list/{pkglist.id}/delete", - ): - with t.button(): - t.span(_class=cls("mdi", "mdi-delete", "text-xl")) - with t.td( - _class=cls( - "border", - "bg-blue-200", - "hover:bg-blue-400", - "cursor-pointer", - "w-8", - "text-center", - ) - ): - with t.a( - data_hx_post=f"/list/{pkglist.id}/edit", - href=f"/?edit={pkglist.id}", - data_hx_target="closest tr", - data_hx_swap="outerHTML", - id="edit-packagelist", - ): - with t.button(): - t.span(_class=cls("mdi", "mdi-pencil", "text-xl")), - with t.td( - _class=cls( - "border", - "bg-green-200", - "hover:bg-green-400", - "cursor-pointer", - "w-8", - "text-center", - ) - ): - with t.button(): - t.span(_class=cls("mdi", "mdi-arrow-right", "text-xl")) - # data_hx_post=f"/list/{pkglist.id}/show", - 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): - with t.div(id="packagelist-table") as doc: - t.h1("Package Lists", _class=cls("text-2xl", "mb-5")) - with t.table( - id="packagelist-table", - _class=cls( - "table", - "table-auto", - "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() as b: - for pkglist in pkglists: - PackageListTableRow(pkglist).doc - - return doc diff --git a/python_flask/packager/components/TripCategoryList.py b/python_flask/packager/components/TripCategoryList.py new file mode 100644 index 0000000..662ee45 --- /dev/null +++ b/python_flask/packager/components/TripCategoryList.py @@ -0,0 +1,134 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +class TripCategoryList: + def __init__(self, trip, categories): + with t.div() as doc: + t.h1("Categories", _class=cls("text-xl", "mb-5")) + with t.table( + _class=cls( + "table", + "table-auto", + "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("Weight", _class=cls("border", "p-2")), + t.th("Max", _class=cls("border", "p-2")), + _class="h-10", + ) + with t.tbody() as b: + for category in categories: + items = [ + i + for i in trip.items + if i.inventory_item.category_id == category.id + ] + biggest_category_weight = 1 + + for cat in categories: + category_items = [ + i + for i in trip.items + if i.inventory_item.category_id == cat.id + ] + weight_sum = sum( + [ + i.inventory_item.weight + for i in category_items + if i.pick + ] + ) + if weight_sum > biggest_category_weight: + biggest_category_weight = weight_sum + + weight = sum([i.inventory_item.weight for i in items if i.pick]) + + with t.tr( + _class=cls( + "h-10", + "hover:bg-purple-100", + "m-3", + "h-full", + *["bg-blue-100"] + if category.active + else ( + ["bg-red-100"] + if any([i.pick != i.pack for i in items]) + else [] + ), + ) + ) as doc: + with t.td( + _class=cls( + "border", + "p-0", + "m-0", + *["font-bold"] if category.active else [], + ) + ): + t.a( + category.name, + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}/category/{category.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + + with t.td( + _class=cls("border", "p-0", "m-0"), + style="position:relative;", + ): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/category/{category.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ): + width = int(weight / biggest_category_weight * 100) + t.p(weight) + t.div( + _class=cls("bg-blue-600", "h-1.5"), + style=f"width: {width}%;position:absolute;left:0;bottom:0;right:0;", + ) + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + sum([i.inventory_item.weight for i in items]), + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/category/{category.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + # with t.tr(_class=cls("h-10", "hover:bg-purple-200", "bg-gray-300", "font-bold")) as doc: + # with t.td(_class=cls("border", "p-0", "m-0")): + # t.a( + # "Sum", + # _class=cls("block", "p-2", "m-2"), + # ) + # with t.td(_class=cls("border", "p-0", "m-0")): + # t.a( + # sum(([i.inventory_item.weight for i in c.items]) for c in categories]), + # _class=cls("block", "p-2", "m-2"), + # ) + # with t.td(_class=cls("border", "p-0", "m-0", "font-normal")): + # t.a( + # sum(([i.inventory_item.weight for i in c.items]) for c in categories]), + # _class=cls("block", "p-2", "m-2"), + # ) + + self.doc = doc diff --git a/python_flask/packager/components/TripItemList.py b/python_flask/packager/components/TripItemList.py new file mode 100644 index 0000000..41e7748 --- /dev/null +++ b/python_flask/packager/components/TripItemList.py @@ -0,0 +1,266 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +class TripItemRowEdit(object): + def __init__(self, item, biggest_item_weight): + with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-100")) as doc: + with t.td(_class=cls("border")): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"?item_{'unpick' if item.pick else 'pick'}={item.inventory_item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls( + "inline-block", + "p-2", + "m-0", + "w-full", + "flex", + "justify-center", + "content-center", + ), + ): + t._input( + type="checkbox", **({"checked": True} if item.pick else {}) + ) + with t.td(_class=cls("border")): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"?item_{'unpack' if item.pack else 'pack'}={item.inventory_item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls( + "inline-block", + "p-2", + "m-0", + "w-full", + "flex", + "justify-center", + "content-center", + ), + ): + t._input( + type="checkbox", **({"checked": True} if item.pack else {}) + ) + # with t.td(item.name, _class=cls("border", "px-2")), + with t.td(colspan=2, _class=cls("border-none", "bg-purple-100", "h-10")): + with t.div(): + t.form( + id="edit-item", + action=f"/inventory/item/{item.inventory_item.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", + ) + with t.div(_class=cls("flex", "flex-row", "h-full")): + with t.span( + _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="item-edit-name", + form="edit-item", + name="name", + value=item.name, + **{ + "x-on:input": "edit_submit_enabled = $event.srcElement.value.trim().length !== 0;" + }, + ) + with t.span( + _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", + id="item-edit-weight", + name="weight", + form="edit-item", + value=item.weight, + ) + with t.td( + _class=cls( + "border", + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer", + "w-8", + "text-center", + ), + id="edit-item-abort", + ): + with t.a( + href=f"/inventory/category/{item.category.id}", + # data_hx_post=f"/item/{item.id}/edit/cancel", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-cancel", "text-xl")), + with t.td( + id="edit-item-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-item"): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + self.doc = doc + + +class TripItemRowNormal(object): + def __init__(self, item, biggest_item_weight): + with t.tr(_class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-100")) as doc: + with t.td(_class=cls("border")): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"?item_{'unpick' if item.pick else 'pick'}={item.inventory_item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls( + "inline-block", + "p-2", + "m-0", + "w-full", + "justify-center", + "content-center", + "flex", + ), + ): + t._input( + type="checkbox", + form=f"toggle-item-pick", + name="pick-{item.inventory_item.id}", + **({"checked": True} if item.pick else {}), + ) # , xstyle="position: relative;z-index: 1;pointer-events: auto; ") + with t.td(_class=cls("border")): + with t.a( + id="select-category", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"?item_{'unpack' if item.pack else 'pack'}={item.inventory_item.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls( + "inline-block", + "p-2", + "m-0", + "w-full", + "flex", + "justify-center", + "content-center", + ), + ): + t._input( + type="checkbox", + form=f"toggle-item-pack", + name="pack-{item.inventory_item.id}", + **({"checked": True} if item.pack else {}), + ) # , xstyle="position: relative;z-index: 1;pointer-events: auto; ") + t.td( + item.inventory_item.name, + _class=cls( + "border", + "px-2", + *(("bg-red-100",) if item.pick != item.pack else ()), + ), + ), + width = int(item.inventory_item.weight / biggest_item_weight * 100) + with t.td(_class=cls("border", "px-2"), style="position:relative;"): + t.p(str(item.inventory_item.weight)) + t.div( + _class=cls("bg-blue-600", "h-1.5"), + style=";".join( + [ + f"width: {width}%", + "position:absolute", + "left:0", + "bottom:0", + "right:0", + ] + ), + ) + self.doc = doc + + +class TripItemRow(object): + def __init__(self, item, biggest_item_weight): + if item.edit: + self.doc = TripItemRowEdit(item, biggest_item_weight) + else: + self.doc = TripItemRowNormal(item, biggest_item_weight) + + +class TripItemList: + def __init__(self, trip, active_category): + with t.div() as doc: + t.h1("Items", _class=cls("text-xl", "mb-5")) + with t.table( + id="item-table", + _class=cls( + "table", + "table-auto", + "border-collapse", + "border-spacing-0", + "border", + "w-full", + ), + ): + with t.thead(_class=cls("bg-gray-200")): + t.tr( + t.th("Take?", _class=cls("border", "p-2", "w-0")), + t.th("Packed?", _class=cls("border", "p-2", "w-0")), + t.th("Name", _class=cls("border", "p-2")), + t.th("Weight", _class=cls("border", "p-2")), + _class="h-10", + ) + with t.tbody() as b: + if active_category: + biggest_item_weight = max( + [ + i.inventory_item.weight + for i in trip.items + if i.inventory_item.category_id == active_category.id + ] + or [0] + ) + else: + biggest_item_weight = max( + [i.inventory_item.weight for i in trip.items] or [0] + ) + if biggest_item_weight <= 0: + biggest_item_weight = 1 + print(active_category) + for item in trip.items: + if ( + not active_category + or item.inventory_item.category_id == active_category.id + ): + TripItemRow(item, biggest_item_weight).doc + + self.doc = doc diff --git a/python_flask/packager/components/TripItemManager.py b/python_flask/packager/components/TripItemManager.py new file mode 100644 index 0000000..eaf6c05 --- /dev/null +++ b/python_flask/packager/components/TripItemManager.py @@ -0,0 +1,21 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from .TripCategoryList import TripCategoryList +from .TripItemList import TripItemList + +from ..helpers import * + + +class TripItemManager: + def __init__(self, trip, categories, active_category): + with t.div( + id="pkglist-item-manager", _class=cls("grid", "grid-cols-4", "gap-3") + ) as doc: + with t.div(_class=cls("col-span-2")): + TripCategoryList(trip, categories), + with t.div(_class=cls("col-span-2")): + TripItemList(trip, active_category=active_category), + + self.doc = doc diff --git a/python_flask/packager/components/TripList.py b/python_flask/packager/components/TripList.py new file mode 100644 index 0000000..b0c0df3 --- /dev/null +++ b/python_flask/packager/components/TripList.py @@ -0,0 +1,19 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +from .TripTable import TripTable +from .NewTrip import NewTrip + +from ..helpers import * + + +class TripList: + def __init__(self, trips): + with t.div(id="trips-manager", _class=cls("p-8")) as doc: + TripTable(trips), + NewTrip( + # name=name, description=description, error=error, errormsg=errormsg + ) + + self.doc = doc diff --git a/python_flask/packager/components/TripManager.py b/python_flask/packager/components/TripManager.py new file mode 100644 index 0000000..0a8421c --- /dev/null +++ b/python_flask/packager/components/TripManager.py @@ -0,0 +1,381 @@ +import dominate +import dominate.tags as t +from dominate.util import raw + +import urllib +import decimal + +from .TripTable import TripTable +from .NewTrip import NewTrip +from .TripItemManager import TripItemManager + +from ..helpers import * +from ..models import TripState + + +class TripInfoEditRow: + def __init__(self, baseurl, name, value, attribute, inputtype="text"): + + with t.tr() as doc: + t.form( + id=f"edit-{attribute}", + action=urllib.parse.urljoin(baseurl, f"edit/{attribute}/submit/"), + target="_self", + method="post", + # data_hx_post=f"/list/{pkglist.id}/edit/submit", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ) + with t.tr(_class=cls("h-full")): + t.td(name, _class=cls("border", "p-2", "h-full")) + with t.td(_class=cls("border", "p-0")): + t._input( + _class=cls("bg-blue-200", "w-full", "h-full", "px-2"), + type=inputtype, + id="item-edit-name", + form=f"edit-{attribute}", + name=attribute, + value=value, + ) + + with t.td( + _class=cls( + "border", + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer", + "w-8", + "text-center", + ), + id=f"edit-{attribute}-abort", + ): + with t.a(href=baseurl): + with t.button(): + t.span(_class=cls("mdi", "mdi-cancel", "text-xl")), + with t.td( + id=f"edit-{attribute}-save", + _class=cls( + "border", + "bg-green-200", + "hover:bg-green-400", + "cursor-pointer", + "w-8", + "text-center", + ), + ): + with t.button(type="submit", form=f"edit-{attribute}"): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + self.doc = doc + + +class TripInfoNormalRow: + def __init__(self, editable, baseurl, name, value, attribute): + with t.tr() as doc: + t.td(name, _class=cls("border", "p-2")) + t.td(value, _class=cls("border", "p-2")) + if editable: + with t.td( + _class=cls( + "border", + "bg-blue-200", + "hover:bg-blue-400", + "cursor-pointer", + "w-8", + "text-center", + ) + ): + if editable: + with t.a( + # data_hx_post=f"/item/{item.id}/edit", + href=f"?edit={attribute}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-pencil", "text-xl")), + + self.doc = doc + + +class TripInfo: + def __init__(self, trip, edit, baseurl, triptypes): + with t.table( + id="trip-table", + _class=cls( + "table", + "table-auto", + "border-collapse", + "border-spacing-0", + "border", + "w-full", + ), + ) as doc: + with t.tbody() as b: + TripInfoNormalRow(False, "", "State", trip.state.name, "") + + if edit == "location": + TripInfoEditRow(baseurl, "Location", trip.location, "location") + else: + TripInfoNormalRow( + True, baseurl, "Location", trip.location, "location" + ) + + if edit == "start_date": + TripInfoEditRow( + baseurl, + "From", + str(trip.start_date), + "start_date", + inputtype="date", + ) + else: + TripInfoNormalRow( + True, baseurl, "From", str(trip.start_date), "start_date" + ) + + if edit == "end_date": + TripInfoEditRow( + baseurl, "To", str(trip.end_date), "end_date", inputtype="date" + ) + else: + TripInfoNormalRow( + True, baseurl, "To", str(trip.end_date), "end_date" + ) + + if edit == "temp_min": + TripInfoEditRow( + baseurl, + "Temp (min)", + trip.temp_min, + "temp_min", + inputtype="number", + ) + else: + TripInfoNormalRow( + True, baseurl, "Temp (min)", trip.temp_min, "temp_min" + ) + + if edit == "temp_max": + TripInfoEditRow( + baseurl, + "Temp (max)", + trip.temp_max, + "temp_max", + inputtype="number", + ) + else: + TripInfoNormalRow( + True, baseurl, "Temp (max)", trip.temp_max, "temp_max" + ) + + with t.tr(): + t.td(f"Types", _class=cls("border", "p-2")) + with t.td(_class=cls("border", "p-2")): + with t.ul( + _class=cls( + "flex", + "flex-row", + "flex-wrap", + "gap-2", + "justify-between", + ) + ): + with t.div( + _class=cls( + "flex", + "flex-row", + "flex-wrap", + "gap-2", + "justify-start", + ) + ): + for triptype in trip.types: + with t.a(href=f"?type_remove={triptype.id}"): + with t.li( + _class=cls( + "border", + "rounded-2xl", + "py-0.5", + "px-2", + "bg-green-100", + "cursor-pointer", + "flex", + "flex-column", + "items-center", + "hover:bg-red-200", + "gap-1", + ) + ): + t.span(triptype.name) + t.span( + _class=cls( + "mdi", "mdi-delete", "text-sm" + ) + ) + + with t.div( + _class=cls( + "flex", + "flex-row", + "flex-wrap", + "gap-2", + "justify-start", + ) + ): + for triptype in triptypes: + if triptype not in trip.types: + with t.a(href=f"?type_add={triptype.id}"): + with t.li( + _class=cls( + "border", + "rounded-2xl", + "py-0.5", + "px-2", + "bg-gray-100", + "cursor-pointer", + "flex", + "flex-column", + "items-center", + "hover:bg-green-200", + "gap-1", + "opacity-60", + ) + ): + t.span(triptype.name) + t.span( + _class=cls( + "mdi", "mdi-plus", "text-sm" + ) + ) + + with t.tr(): + t.td(f"Carried weight", _class=cls("border", "p-2")) + weight = sum( + [i.inventory_item.weight for i in trip.items if i.pick] + ) / decimal.Decimal(1000) + t.td(f"{weight} kg", _class=cls("border", "p-2")) + + self.doc = doc + + +class TripComments: + def __init__(self, trip, baseurl): + with t.div() as doc: + t.h1("Comments", _class=cls("text-xl", "mb-5")) + t.form( + id="edit-comment", + action=urllib.parse.urljoin(baseurl, f"edit/comment/submit/"), + target="_self", + method="post", + # data_hx_post=f"/list/{pkglist.id}/edit/submit", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ) + # https://stackoverflow.com/a/48460773 + t.textarea( + trip.comment or "", + name="comment", + form="edit-comment", + _class=cls("border", "w-full", "h-48"), + oninput='this.style.height = "";this.style.height = this.scrollHeight + 2 + "px"', + ) + + with t.button( + type="submit", + form=f"edit-comment", + _class=cls( + "mt-2", + "border", + "bg-green-200", + "hover:bg-green-400", + "cursor-pointer", + "flex", + "flex-column", + "p-2", + "gap-2", + "items-center", + ), + ): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + t.span("Save") + + self.doc = doc + + +class TripActions: + def __init__(self, trip): + with t.div() as doc: + t.h1("Actions", _class=cls("text-xl", "mb-5")) + + with t.div(_class=cls("flex", "flex-column", "gap-2")): + if trip.state == TripState.Planning: + t.button("Finish planning", _class=cls("border", "p-2")) + if trip.state in (TripState.Planned, TripState.Active): + t.button("Start review", _class=cls("border", "p-2")) + if trip.state == TripState.Done: + t.button("Back to review", _class=cls("border", "p-2")) + + self.doc = doc + + +class TripManager: + def __init__(self, trip, categories, active_category, edit, baseurl, triptypes): + with t.div(id="trips-manager", _class=cls("p-8")) as doc: + if edit == "name": + t.form( + id=f"edit-name", + action=urllib.parse.urljoin(baseurl, f"edit/name/submit/"), + target="_self", + method="post", + # data_hx_post=f"/list/{pkglist.id}/edit/submit", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ) + with t.div(_class=cls("flex", "flex-row", "items-center", "gap-x-3")): + t._input( + _class=cls("bg-blue-200", "w-full", "h-full", "px-2", "text-2xl", "font-semibold"), + type="text", + id="item-edit-name", + form=f"edit-name", + name="name", + value=trip.name, + ) + with t.a(href=baseurl): + with t.button(_class=cls( + "bg-red-200", + "hover:bg-red-400", + "cursor-pointer")): + t.span(_class=cls("mdi", "mdi-cancel", "text-xl")), + with t.button(type="submit", form=f"edit-name", _class=cls( + "bg-green-200", + "hover:bg-green-400", + "cursor-pointer")): + t.span(_class=cls("mdi", "mdi-content-save", "text-xl")), + else: + with t.div(_class=cls("flex", "flex-row", "items-center", "gap-x-3")): + t.h1(trip.name, _class=cls("text-2xl", "font-semibold")) + with t.span(): + with t.a( + # data_hx_post=f"/item/{item.id}/edit", + href=f"?edit=name", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + ): + with t.button(): + t.span(_class=cls("mdi", "mdi-pencil", "text-xl", "opacity-50")), + + with t.div(_class=cls("my-6")): + TripInfo(trip, edit, baseurl, triptypes).doc + + with t.div(_class=cls("my-6")): + TripComments(trip, baseurl).doc + + with t.div(_class=cls("my-6")): + TripActions(trip).doc + + with t.div(_class=cls("my-6")): + TripItemManager( + trip, categories=categories, active_category=active_category + ).doc + + self.doc = doc diff --git a/python_flask/packager/components/TripTable.py b/python_flask/packager/components/TripTable.py new file mode 100644 index 0000000..0d0cc33 --- /dev/null +++ b/python_flask/packager/components/TripTable.py @@ -0,0 +1,96 @@ +import datetime + +import dominate +import dominate.tags as t +from dominate.util import raw + +from ..helpers import * + + +class TripRow(object): + def __init__(self, trip): + with t.tr( + _class=cls("h-10", "even:bg-gray-100", "hover:bg-purple-100", "h-full") + ) as doc: + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + trip.name, + id="select-trip", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + t.p(str(trip.start_date)), + id="select-trip", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + t.p(str(trip.end_date)), + id="select-trip", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + t.p((trip.end_date - trip.start_date).days), + id="select-trip", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + today = datetime.datetime.now().date() + with t.td(_class=cls("border", "p-0", "m-0")): + t.a( + t.p(trip.state.name), + id="select-trip", + # data_hx_post=f"/list/{pkglist.id}/edit", + href=f"/trip/{trip.id}", + # data_hx_target="closest tr", + # data_hx_swap="outerHTML", + _class=cls("inline-block", "p-2", "m-0", "w-full"), + ) + self.doc = doc + + +def TripTable(trips): + with t.div() as doc: + t.h1("Trips", _class=cls("text-2xl", "mb-5")) + with t.table( + id="trip-table", + _class=cls( + "table", + "table-auto", + "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("From", _class=cls("border", "p-2")), + t.th("To", _class=cls("border", "p-2")), + t.th("Nights", _class=cls("border", "p-2")), + t.th("State", _class=cls("border", "p-2")), + _class="h-10", + ) + with t.tbody() as b: + for trip in trips: + TripRow(trip).doc + + return doc diff --git a/python_flask/packager/components/__init__.py b/python_flask/packager/components/__init__.py index c993cd8..5c7363f 100644 --- a/python_flask/packager/components/__init__.py +++ b/python_flask/packager/components/__init__.py @@ -1,13 +1,6 @@ -from .NewPackageList import NewPackageList -from .PackageListTable import ( - PackageListTable, - PackageListTableRowEdit, - PackageListTableRowNormal, - PackageListTableRow, -) - -from .ItemList import ItemList -from .CategoryList import CategoryList -from .PackageListManager import PackageListManager -from .PackageListItemManager import PackageListItemManager +from .Base import Base from .Home import Home +from .InventoryItemManager import InventoryItemManager +from .InventoryItemDetails import InventoryItemDetails +from .TripManager import TripManager +from .TripList import TripList diff --git a/python_flask/packager/models.py b/python_flask/packager/models.py index 80e2c7c..9dc6dd0 100644 --- a/python_flask/packager/models.py +++ b/python_flask/packager/models.py @@ -1,36 +1,86 @@ from . import db +import enum -class PackageList(db.Model): - __tablename__ = "packagelist" +class InventoryItemCategory(db.Model): + __tablename__ = "inventoryitemcategories" 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) + items = db.relationship("InventoryItem", backref="category", lazy=True) - edit = False - error = False - errormsg = None + active = False -class PackageListItemCategory(db.Model): - __tablename__ = "packagelistitemcategory" +class InventoryItem(db.Model): + __tablename__ = "inventoryitems" 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="category", 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) - weight = db.Column(db.Integer) - # packagelist_id = db.Column( - # db.String(36), db.ForeignKey("packagelist.id"), nullable=False - # ) + weight = db.Column(db.Integer, nullable=False) category_id = db.Column( - db.String(36), db.ForeignKey("packagelistitemcategory.id"), nullable=False + db.String(36), db.ForeignKey("inventoryitemcategories.id"), nullable=False ) + edit = False + + +class TripItems(db.Model): + __tablename__ = "tripitems" + item_id = db.Column( + db.String(36), + db.ForeignKey("inventoryitems.id"), + nullable=False, + primary_key=True, + ) + trip_id = db.Column( + db.String(36), db.ForeignKey("trips.id"), nullable=False, primary_key=True + ) + inventory_item = db.relationship("InventoryItem", lazy=True) + + pick = db.Column(db.Boolean, nullable=False) + pack = db.Column(db.Boolean, nullable=False) + + edit = False + + +class TripType(db.Model): + __tablename__ = "triptypes" + id = db.Column(db.String(36), primary_key=True) + name = db.Column(db.Text, unique=True, nullable=False) + + +class TripToTripType(db.Model): + __tablename__ = "trips_to_triptypes" + trip_id = db.Column( + db.String(36), db.ForeignKey("trips.id"), nullable=False, primary_key=True + ) + trip_type_id = db.Column( + db.String(36), db.ForeignKey("triptypes.id"), nullable=False, primary_key=True + ) + + +class TripState(enum.Enum): + Planning = 1 + Planned = 2 + Active = 3 + Review = 4 + Done = 5 + + +class Trip(db.Model): + __tablename__ = "trips" + id = db.Column(db.String(36), primary_key=True) + name = db.Column(db.Text, unique=True, nullable=False) + start_date = db.Column(db.Date, nullable=False) + end_date = db.Column(db.Date, nullable=False) + location = db.Column(db.Text, nullable=False) + temp_min = db.Column(db.Integer, nullable=False) + temp_max = db.Column(db.Integer, nullable=False) + + comment = db.Column(db.Text, nullable=False) + + types = db.relationship("TripType", secondary="trips_to_triptypes", lazy=True) + items = db.relationship("TripItems", lazy=True) + + state = db.Column(db.Enum(TripState), nullable=False) diff --git a/python_flask/packager/views.py b/python_flask/packager/views.py index c399055..ff6734a 100644 --- a/python_flask/packager/views.py +++ b/python_flask/packager/views.py @@ -5,121 +5,435 @@ from .helpers import * import uuid import os +import urllib +import datetime import dominate import dominate.tags as t from dominate.util import raw from .components import ( - PackageListManager, - PackageListItemManager, - NewPackageList, + InventoryItemManager, + Base, Home, - PackageListTableRowEdit, - PackageListTableRowNormal, - PackageListTableRow, + TripList, + TripManager, + InventoryItemDetails, ) from flask import request, make_response -def get_packagelists(): - return PackageList.query.all() - - def get_categories(): - return PackageListItemCategory.query.all() + return InventoryItemCategory.query.all() + + +def get_trips(): + return Trip.query.all() def get_all_items(): - return PackageListItem.query.all() + return InventoryItem.query.all() + + +def get_triptypes(): + return TripType.query.all() + def get_items(category): - return PackageListItem.query.filter_by(category_id=str(category.id)) - -def get_packagelist_by_id(id): - return PackageList.query.filter_by(id=str(id)).first() + return InventoryItem.query.filter_by(category_id=str(category.id)) -def add_packagelist(name, description): - try: - db.session.add( - PackageList(id=str(uuid.uuid4()), name=name, description=description) +def get_item(id): + return InventoryItem.query.filter_by(id=str(id)).first() + + +def get_trip(id): + return Trip.query.filter_by(id=str(id)).first() + + +def pick_item(trip_id, item_id): + item = TripItems.query.filter_by(item_id=str(item_id), trip_id=str(trip_id)).first() + item.pick = True + db.session.commit() + + +def unpick_item(trip_id, item_id): + item = TripItems.query.filter_by(item_id=str(item_id), trip_id=str(trip_id)).first() + item.pick = False + db.session.commit() + + +def pack_item(trip_id, item_id): + item = TripItems.query.filter_by(item_id=str(item_id), trip_id=str(trip_id)).first() + item.pack = True + db.session.commit() + + +def unpack_item(trip_id, item_id): + item = TripItems.query.filter_by(item_id=str(item_id), trip_id=str(trip_id)).first() + item.pack = False + db.session.commit() + + +def add_item(name, weight, category_id): + db.session.add( + InventoryItem( + id=str(uuid.uuid4()), + name=name, + description="", + weight=weight, + category_id=category_id, ) - db.session.commit() - except sqlalchemy.exc.IntegrityError: - db.session.rollback() - return False - - -def delete_packagelist(id): - deletions = PackageList.query.filter_by(id=str(id)).delete() - if deletions == 0: - return False - else: - db.session.commit() - return True + ) + db.session.commit() @app.route("/") def root(): + return make_response(Base(Home(), app.root_path).doc.render(), 200) + + +@app.route("/inventory/") +def inventory(): categories = get_categories() items = get_all_items() + + args = request.args.to_dict() + 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] + match = [i for i in items if i.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(PackageListItemManager(categories, items), app.root_path).doc.render(), 200 + Base( + InventoryItemManager(categories, items, active_category=None), + app.root_path, + active_page="inventory", + ).doc.render(), + 200, ) -@app.route("/category/") +@app.route("/inventory/item//") +def inventory_item(id): + item = get_item(id) + + args = request.args.to_dict() + edit = args.pop("edit", None) + + return make_response( + Base( + InventoryItemDetails(item, edit=edit, baseurl=f"/inventory/item/{id}/",), app.root_path, active_page="inventory" + ).doc.render(), + 200, + ) + +@app.route( + "/inventory/item//edit//submit/", + methods=["POST"], +) +def edit_inventory_submit(id, attribute): + new_value = request.form[attribute] + + if attribute in ("weight"): + new_value = int(new_value) + + updates = InventoryItem.query.filter_by(id=str(id)).update({attribute: new_value}) + db.session.commit() + if updates == 0: + # todo what to do without js? + return make_response("", 404) + + redirect = request.path[: -(len(f"edit/{attribute}/submit/"))] + + r = make_response("", 303) + r.headers["Location"] = redirect + return r + +@app.route("/trips/") +def trips(): + trips = get_trips() + + return make_response( + Base(TripList(trips), app.root_path, active_page="trips").doc.render(), 200 + ) + + +@app.route("/trip//") +def trip(id): + + args = request.args.to_dict() + + item_to_pick = args.pop("item_pick", None) + if item_to_pick: + pick_item(id, item_to_pick) + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + item_to_unpick = args.pop("item_unpick", None) + if item_to_unpick: + unpick_item(id, item_to_unpick) + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + item_to_pack = args.pop("item_pack", None) + if item_to_pack: + pack_item(id, item_to_pack) + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + item_to_unpack = args.pop("item_unpack", None) + if item_to_unpack: + unpack_item(id, item_to_unpack) + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + type_to_add = args.pop("type_add", None) + if type_to_add: + newtype = TripToTripType(trip_id=str(id), trip_type_id=str(type_to_add)) + db.session.add(newtype) + db.session.commit() + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + type_to_remove = args.pop("type_remove", None) + if type_to_remove: + newtype = TripToTripType.query.filter_by( + trip_id=str(id), trip_type_id=str(type_to_remove) + ).delete() + db.session.commit() + r = make_response("", 303) + if args: + r.headers["Location"] = f"/trip/{id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/" + return r + + trip = get_trip(id) + + items = get_all_items() + + categories = get_categories() + + edit = args.pop("edit", None) + + for item in items: + try: + db.session.add( + TripItems(trip_id=str(id), item_id=str(item.id), pick=False, pack=False) + ) + db.session.commit() + except sqlalchemy.exc.IntegrityError: + db.session.rollback() + + return make_response( + Base( + TripManager( + trip, + categories=categories, + active_category=None, + edit=edit, + baseurl=f"/trip/{id}/", + triptypes=get_triptypes(), + ), + app.root_path, + active_page="trips", + ).doc.render(), + 200, + ) + + +@app.route( + "/trip//category//edit//submit/", + methods=["POST"], +) +@app.route("/trip//edit//submit/", methods=["POST"]) +def edit_trip_category_location_edit_submit(id, category_id=None, attribute=None): + new_value = request.form[attribute] + + if attribute in ("start_date", "end_date"): + new_value = datetime.date.fromisoformat(new_value) + + updates = Trip.query.filter_by(id=str(id)).update({attribute: new_value}) + db.session.commit() + if updates == 0: + # todo what to do without js? + return make_response("", 404) + + redirect = request.path[: -(len(f"edit/{attribute}/submit/"))] + + r = make_response("", 303) + r.headers["Location"] = redirect + return r + + +@app.route("/trip//category//") +def trip_with_active_category(id, category_id): + trip = get_trip(id) + + args = request.args.to_dict() + + item_to_pick = args.pop("item_pick", None) + if item_to_pick: + pick_item(id, item_to_pick) + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + item_to_unpick = args.pop("item_unpick", None) + if item_to_unpick: + unpick_item(id, item_to_unpick) + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + item_to_pack = args.pop("item_pack", None) + if item_to_pack: + pack_item(id, item_to_pack) + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + item_to_unpack = args.pop("item_unpack", None) + if item_to_unpack: + unpack_item(id, item_to_unpack) + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + type_to_add = args.pop("type_add", None) + if type_to_add: + newtype = TripToTripType(trip_id=str(id), trip_type_id=str(type_to_add)) + db.session.add(newtype) + db.session.commit() + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + type_to_remove = args.pop("type_remove", None) + if type_to_remove: + newtype = TripToTripType.query.filter_by( + trip_id=str(id), trip_type_id=str(type_to_remove) + ).delete() + db.session.commit() + r = make_response("", 303) + if args: + r.headers[ + "Location" + ] = f"/trip/{id}/category/{category_id}/?" + urllib.parse.urlencode(params) + else: + r.headers["Location"] = f"/trip/{id}/category/{category_id}/" + return r + + items = get_all_items() + + categories = get_categories() + active_category = [c for c in categories if str(c.id) == str(category_id)][0] + active_category.active = True + + for item in items: + try: + db.session.add( + TripItems(trip_id=str(id), item_id=str(item.id), pick=False, pack=False) + ) + db.session.commit() + except sqlalchemy.exc.IntegrityError: + db.session.rollback() + + edit = args.pop("edit", None) + + return make_response( + Base( + TripManager( + trip, + categories=categories, + active_category=active_category, + edit=edit, + baseurl=f"/trip/{id}/category/{category_id}/", + triptypes=get_triptypes(), + ), + app.root_path, + active_page="trips", + ).doc.render(), + 200, + ) + + +@app.route("/inventory/category//") def category(id): categories = get_categories() print(id) - for c in categories: - print(f"{c.id} | {c.name}") active_category = [c for c in categories if str(c.id) == str(id)][0] + active_category.active = True + + args = request.args.to_dict() + items = get_items(active_category) 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] + match = [i for i in items if i.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(PackageListItemManager(categories, items), app.root_path).doc.render(), 200 + Base( + InventoryItemManager(categories, items, active_category), + app.root_path, + active_page="inventory", + ).doc.render(), + 200, ) @@ -127,33 +441,33 @@ def is_htmx(): return request.headers.get("HX-Request") is not None -@app.route("/list/", methods=["POST"]) -def add_new_list(): +@app.route("/inventory/item/", methods=["POST"]) +def add_new_item(): name = request.form["name"] - description = request.form["description"] + weight = int(request.form["weight"]) + category_id = request.form["category"] - error, errormsg = validate_name(name) + add_item(name=name, weight=weight, category_id=category_id) - if not error: - if add_packagelist(name=name, description=description) is False: - error = True - errormsg = f'Name "{name}" already exists' + r = make_response("", 303) + r.headers["Location"] = f"/inventory/category/{category_id}" + return r - if is_htmx(): - return make_response( - PackageListManager( - get_packagelists(), - name=name, - description=description, - error=error, - errormsg=errormsg, - ).doc.render(), - 200 if error else 201, - ) - else: - r = make_response("", 303) - r.headers["Location"] = "/" - return r + +@app.route("/trip/", methods=["POST"]) +def add_new_trip(): + name = request.form["name"] + start_date = datetime.date.fromisoformat(request.form["start-date"]) + end_date = datetime.date.fromisoformat(request.form["end-date"]) + + newid = str(uuid.uuid4()) + db.session.add(Trip(id=newid, name=name, start_date=start_date, end_date=end_date)) + db.session.commit() + + r = make_response("", 303) + r.headers["Location"] = f"/trip/{newid}" # TODO enable this + r.headers["Location"] = f"/trip/" + return r def validate_name(name): @@ -169,93 +483,92 @@ def validate_name(name): return error, errormsg -@app.route("/list/name/validate", methods=["POST"]) -def validate_list_name(): +@app.route("/inventory/item//edit/submit/", methods=["POST"]) +def edit_item_submit(id): name = request.form["name"] + weight = int(request.form["weight"]) - error, errormsg = validate_name(name) - - if not error: - if PackageList.query.filter_by(name=name).first() is not None: - error = True - errormsg = f'Name "{name}" already exists' - doc = NewPackageList(name=name, error=error, errormsg=errormsg) - - return make_response(doc.render(), 200) - - -@app.route("/list//edit/cancel", methods=["POST"]) -def edit_list_cancel(id): - print("cancelling" * 20) - pkglist = PackageList.query.filter_by(id=str(id)).first() - return make_response(PackageListTableRowNormal(pkglist).doc.render(), 200) - - -@app.route("/list//edit/submit/", methods=["POST"]) -def edit_list_submit(id): - name = request.form["name"] - description = request.form["description"] - 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: + item = InventoryItem.query.filter_by(id=str(id)).first() + if item is None: # todo what to do without js? return make_response("", 404) - pkglist.name = name - pkglist.description = description + item.name = name + item.weight = weight - try: - 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 + db.session.commit() - if is_htmx(): - return make_response(PackageListTableRowNormal(pkglist).doc.render(), 200) - else: - r = make_response("", 303) - r.headers["Location"] = "/" - return r - - -@app.route("/list//edit", methods=["POST"]) -def edit_list(id): - pkglist = get_packagelist_by_id(id) - - out = PackageListTableRowEdit(pkglist).doc - return make_response(out.render(), 200) - - -@app.route("/list//delete", methods=["GET"]) -def delete_list_via_get(id): - if not delete_packagelist(id=id): - return make_response("", 404) r = make_response("", 303) - r.headers["Location"] = "/" + r.headers["Location"] = f"/inventory/category/{item.category.id}" return r -@app.route("/list/", methods=["DELETE"]) -def delete_list(id): - if not delete_packagelist(id=id): +@app.route("/inventory/item//pick/submit/", methods=["POST"]) +def edit_item_pick(id): + print(request.form) + if "pick" in request.form: + pick = request.form["pick"] == "on" + else: + pick = False + print(pick) + + item = InventoryItem.query.filter_by(id=str(id)).first() + if item is None: + # todo what to do without js? return make_response("", 404) + + item.picked = pick + + db.session.commit() + + r = make_response("", 303) + r.headers["Location"] = f"/inventory/category/{item.category.id}" + return r + + +@app.route("/inventory/item//pack/submit/", methods=["POST"]) +def edit_item_pack(id): + print(request.form) + if "pack" in request.form: + pack = request.form["pack"] == "on" + else: + pack = False + print(pack) + + item = InventoryItem.query.filter_by(id=str(id)).first() + if item is None: + # todo what to do without js? + return make_response("", 404) + + item.pack = pack + + db.session.commit() + + r = make_response("", 303) + r.headers["Location"] = f"/inventory/category/{item.category.id}" + return r + + +@app.route("/inventory/item/", methods=["DELETE"]) +def delete_item(id): + deletions = InventoryItem.query.filter_by(id=str(id)).delete() + if deletions == 0: + return make_response("", 404) + else: + db.session.commit() return make_response("", 200) + + +@app.route("/inventory/item//delete", methods=["GET"]) +def delete_item_get(id): + print(request.headers) + print(request.args) + print(f"deleting {id}") + deletions = InventoryItem.query.filter_by(id=str(id)).delete() + if deletions == 0: + return make_response("", 404) + else: + db.session.commit() + r = make_response("", 303) + r.headers["Location"] = request.headers["Referer"] + return r diff --git a/python_flask/requirements.txt b/python_flask/requirements.txt index 2799641..e970621 100644 --- a/python_flask/requirements.txt +++ b/python_flask/requirements.txt @@ -1,11 +1,15 @@ +alembic==1.8.1 click==8.1.3 dominate==2.6.0 Flask==2.1.2 +Flask-Migrate==3.1.0 Flask-SQLAlchemy==2.5.1 greenlet==1.1.2 importlib-metadata==4.12.0 +importlib-resources==5.9.0 itsdangerous==2.1.2 Jinja2==3.1.2 +Mako==1.2.2 MarkupSafe==2.1.1 SQLAlchemy==1.4.39 Werkzeug==2.1.2 diff --git a/python_flask/run.sh b/python_flask/run.sh index 5fc5659..e1e0bd9 100755 --- a/python_flask/run.sh +++ b/python_flask/run.sh @@ -5,4 +5,8 @@ source ./venv/bin/activate export FLASK_APP=packager export FLASK_ENV=development -python3 -m flask run --reload +if (( $# == 0 )) ; then + python3 -m flask run --reload --host 0.0.0.0 --port 5000 +else + python3 -m flask "${@}" +fi