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

1
flinventory_gui/thing.py Symbolic link
View file

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