rename part to thing

This commit is contained in:
flukx 2024-08-21 17:37:30 +02:00
parent 74ed83486b
commit 04690f0f4d
8 changed files with 71 additions and 73 deletions

@ -1 +1 @@
Subproject commit 22218093a040cbf3ff4f68b7da5ce34435be35e5
Subproject commit 436944abb59e999526374875557e76f08346b5de

View file

@ -0,0 +1 @@
flinventory/inventory_io.py

View file

@ -1 +0,0 @@
bikepartsigns-data/locations.json

View file

@ -1 +0,0 @@
flinventory/part.py

View file

@ -1 +0,0 @@
flinventory/part_list_io.py

View file

@ -1 +0,0 @@
bikepartsigns-data/parts.json

View file

@ -1,4 +1,4 @@
"""Search in a parts list."""
"""Search in a thing list."""
import argparse
import asyncio
import itertools
@ -9,18 +9,18 @@ import nicegui
from nicegui import ui
import location
from part import Part
import part_list_io
from thing import Thing
import inventory_io
"""Global (module-wide) variables."""
gl_options: argparse.Namespace
gl_parts: list[Part] = []
gl_things: list[Thing] = []
def get_options() -> argparse.Namespace:
"""Abuse argparse for collecting file names."""
parser = argparse.ArgumentParser()
part_list_io.add_file_args(parser)
inventory_io.add_file_args(parser)
parser.add_argument("--port", "-p",
help="Port on which to serve the website.",
type=int,
@ -37,13 +37,13 @@ gl_options = get_options()
def load_data():
"""Load data from text files and save to global part list.
"""Load data from text files and save to global thing list.
Could implement that data is only loaded when necessary.
Then an argument force would be useful to reload.
"""
start = time.monotonic()
gl_parts[:] = part_list_io.get_parts(gl_options)
gl_things[:] = inventory_io.get_things(gl_options)
end = time.monotonic()
print(f"Data loaded in {end-start} seconds.")
ui.notify(f"Data loaded in {end-start} seconds.", position="top", type="positive")
@ -53,8 +53,8 @@ load_data()
def save_data() -> None:
"""Save parts to files."""
part_list_io.save_parts(gl_parts, gl_options.input_file, gl_options.locations_file)
"""Save things to files."""
inventory_io.save_things(gl_things, gl_options.input_file, gl_options.locations_file)
ui.notify(
message="Data saved",
position="top",
@ -67,15 +67,15 @@ def antilen(string: str):
return 1 / len(string) if string else 0
async def find_parts(parts: list[Part], search_string: str, max_number: int = 10) -> Iterable[Part]:
"""Gives parts that the user might have searched for.
async def find_things(things: list[Thing], search_string: str, max_number: int = 10) -> Iterable[Thing]:
"""Gives things that the user might have searched for.
Args:
parts: the list of parts to search in
things: the list of things to search in
search_string: Input of user
max_number: maximum number of returned parts
max_number: maximum number of returned things
Returns:
list of parts that somehow include the search string
list of things that somehow include the search string
"""
fuzzy = re.compile(".*" + ".*".join(search_string) + ".*", flags=re.IGNORECASE)
score_categories = ("startswith", "startswithLower", "inLower", "fuzzy")
@ -105,24 +105,24 @@ async def find_parts(parts: list[Part], search_string: str, max_number: int = 10
except ValueError: # if iterable is empty
return {category: 0 for category in score_categories}
def match_score(part):
def match_score(thing):
"""Return sortable tuple of decreasing importance.
Good matches have high numbers.
"""
alt_names = itertools.chain(part.name_alt_de if hasattr(part, "name_alt_de") else [],
part.name_alt_en if hasattr(part, "name_alt_en") else [])
score_name = match_score_one_string(part.name)
alt_names = itertools.chain(thing.name_alt_de if hasattr(thing, "name_alt_de") else [],
thing.name_alt_en if hasattr(thing, "name_alt_en") else [])
score_name = match_score_one_string(thing.name)
score_name_lang = max_scores(tuple(map(match_score_one_string,
(part.get("name_en", None),
part.get("name_de", None)
(thing.get("name_en", None),
thing.get("name_de", None)
)
)
))
score_name_alt = max_scores(tuple(map(match_score_one_string, alt_names)))
score_description = max_scores(tuple(map(match_score_one_string,
(part.get("description_en", None),
part.get("description_de", None))
(thing.get("description_en", None),
thing.get("description_de", None))
)
))
return (
@ -142,58 +142,58 @@ async def find_parts(parts: list[Part], search_string: str, max_number: int = 10
score_description['fuzzy']
)
if search_string:
scored_parts = [(part, match_score(part)) for part in parts]
scored_things = [(thing, match_score(thing)) for thing in things]
return map(lambda pair: pair[0],
sorted(
filter(lambda pair: any(pair[1]),
scored_parts),
scored_things),
key=lambda pair: pair[1],
reverse=True
)[:max_number])
return []
async def list_parts(ui_element: nicegui.ui.element, parts: Iterable[Part]) -> None:
"""Replaces content of ui_element with information about the parts.
async def list_things(ui_element: nicegui.ui.element, things: Iterable[Thing]) -> None:
"""Replaces content of ui_element with information about the things.
Args:
ui_element: Some UI element that can be changed.
parts: parts to be displayed
things: things to be displayed
"""
# gives other searches 10 ms time to abort this display which might take long
await asyncio.sleep(0.01)
ui_element.clear()
with ui_element:
for part in parts:
for thing in things:
card = ui.card()
# supplying card and part as default arguments makes it use the current
# supplying card and thing as default arguments makes it use the current
# value instead of the value at the time of usage
def change_card(_, c: ui.element=card, p: Part=part):
show_part_changer(c, p)
def change_card(_, c: ui.element=card, p: Thing=thing):
show_thing_changer(c, p)
with card:
print(f"Create card {id(card)} for {part.name}.")
print(f"Create card {id(card)} for {thing.name}.")
with ui.row(wrap=False):
with ui.column():
with ui.row():
ui.label(text=part.name)
name_en = part.get("name_en", default=None)
if name_en and name_en != part.name:
ui.label(text=thing.name)
name_en = thing.get("name_en", default=None)
if name_en and name_en != thing.name:
ui.label(text=f"({name_en})").style('font-size: 70%')
ui.button("🖉").on_click(change_card)
other_names = ", ".join(itertools.chain(
part.get("name_alt_de", default=[]),
part.get("name_alt_en", default=[])))
thing.get("name_alt_de", default=[]),
thing.get("name_alt_en", default=[])))
if other_names:
ui.label(other_names).style('font-size: 70%')
for member in ("description_de", "description_en", ""):
if hasattr(part, member):
ui.markdown(part.get(member)).style('font-size: 70%')
if part.where:
with ui.label(part.where):
ui.tooltip(part.location.long_name)
if hasattr(part, "image"):
ui.image("/images_landscape/" + part.image).props("width=50%").props(
if hasattr(thing, member):
ui.markdown(thing.get(member)).style('font-size: 70%')
if thing.where:
with ui.label(thing.where):
ui.tooltip(thing.location.long_name)
if hasattr(thing, "image"):
ui.image("/images_landscape/" + thing.image).props("width=50%").props(
"height=100px").props("fit='scale-down'")
@ -201,7 +201,7 @@ async def list_parts(ui_element: nicegui.ui.element, parts: Iterable[Part]) -> N
def search_page() -> None:
"""Create a NiceGUI page with a search input field and search results.
Uses global gl_parts list.
Uses global gl_things list.
"""
print("(Re)build search page.")
# UI container for the search results.
@ -210,7 +210,7 @@ def search_page() -> None:
# Search queries (max. 1) running. Here to be cancellable by different search coroutines.
running_queries: list[asyncio.Task] = []
# should use the parts as they are when clicked: global variable
# should use the things as they are when clicked: global variable
ui.button("Save").on_click(lambda click_event_arguments: save_data())
async def search(event: nicegui.events.ValueChangeEventArguments) -> None:
@ -229,7 +229,7 @@ def search_page() -> None:
except asyncio.exceptions.CancelledError:
# the next letter was already typed, do not search and rerender for this query
return
query = asyncio.create_task(find_parts(gl_parts, event.value))
query = asyncio.create_task(find_things(gl_things, event.value))
running_queries.append(query)
try:
start = time.monotonic()
@ -241,7 +241,7 @@ def search_page() -> None:
pass
else:
if results:
display = asyncio.create_task(list_parts(results, response))
display = asyncio.create_task(list_things(results, response))
running_queries.append(display)
try:
start = time.monotonic()
@ -282,38 +282,38 @@ def try_conversion(value: str,
return value
def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
def show_thing_changer(ui_element: nicegui.ui.element, thing: Thing) -> None:
"""Clear content of ui element and instead display editing fields.
Args:
ui_element: the ui element (e.g. a card) on which to show the part changing ui
part: the part to change
ui_element: the ui element (e.g. a card) on which to show the thing changing ui
thing: the thing to change
"""
input_fields = {}
def save_value(event, member):
"""Copy input field value to part member."""
"""Copy input field value to thing member."""
if not event.value:
del vars(part)[member]
del vars(thing)[member]
else:
vars(part)[member] = event.value
vars(thing)[member] = event.value
def save_sign_value(event, member):
"""Copy input field value to sign member."""
if not event.value:
del vars(part.sign)[member]
del vars(thing.sign)[member]
else:
vars(part.sign)[member] = try_conversion(event.value, (int, float, "bool"))
vars(thing.sign)[member] = try_conversion(event.value, (int, float, "bool"))
def save_list_value(event, member):
"""Split input field at '; ' and save ta part member."""
"""Split input field at '; ' and save ta thing member."""
if not event.value:
del vars(part)[member]
del vars(thing)[member]
else:
values = event.value.split(";")
values = [value.strip() for value in values]
values = [value for value in values if value]
vars(part)[member] = values
vars(thing)[member] = values
def update_location_element(location_ui_element, focus: Optional[str] = None):
"""List location information with input fields.
@ -325,8 +325,8 @@ def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
"""
location_ui_element.clear()
with location_ui_element:
location_info = part.location.json
schema_hierachy = part.location.schema.get_schema_hierachy(dict(iter(part.location)))
location_info = thing.location.json
schema_hierachy = thing.location.schema.get_schema_hierachy(dict(iter(thing.location)))
ui.label(schema_hierachy[0].name)
for level in schema_hierachy:
try:
@ -365,10 +365,10 @@ def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
except location.InvalidLocation:
print("Do not save")
else:
part.location.set(schema.levelname, value)
thing.location.set(schema.levelname, value)
update_location_element(location_ui_element, focus=schema.levelname)
print(f"Try to let edit {part.name} with {id(ui_element)}.")
print(f"Try to let edit {thing.name} with {id(ui_element)}.")
ui_element.clear()
with ui_element:
with ui.row():
@ -376,7 +376,7 @@ def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
for member in ("name_de", "name_en", "description_de", "description_en"):
input_fields[member] = ui.input(
label=member,
value=part.get(member, "")
value=thing.get(member, "")
).props(
'autogrow dense'
).on_value_change(
@ -384,7 +384,7 @@ def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
for member in ("name_alt_de", "name_alt_en"):
input_fields[member] = ui.input(
label=member + " (;-seperated):",
value="; ".join(part.get(member, []))
value="; ".join(thing.get(member, []))
).props(
'autogrow dense'
).on_value_change(
@ -394,15 +394,15 @@ def show_part_changer(ui_element: nicegui.ui.element, part: Part) -> None:
"fontsize_de", "fontsize_en", "location_shift_down"):
input_fields[f"sign.{sign_member}"] = ui.input(
label="sign: " + sign_member,
value=vars(part.sign).get(sign_member, "")
value=vars(thing.sign).get(sign_member, "")
).props('autogrow dense'
).on_value_change(
lambda e, m=sign_member: save_sign_value(e, m)
)
with ui.column() as location_column:
update_location_element(location_column)
if hasattr(part, "image"):
ui.image("/images_landscape/" + part.image).props("width=50%").props(
if hasattr(thing, "image"):
ui.image("/images_landscape/" + thing.image).props("width=50%").props(
"height=100px").props("fit='scale-down'")

1
flinventory_gui/thing.py Symbolic link
View file

@ -0,0 +1 @@
flinventory/thing.py