diff --git a/crm/crm_cmd.py b/crm/crm_cmd.py index aea3a0e..3452c4f 100644 --- a/crm/crm_cmd.py +++ b/crm/crm_cmd.py @@ -8,7 +8,7 @@ from ngwidgets.cmd import WebserverCmd -from crm.crm_core import CRM +from crm.em import CRM from crm.crm_web import CrmWebServer diff --git a/crm/crm_web.py b/crm/crm_web.py index 8cb4afd..c722ba8 100644 --- a/crm/crm_web.py +++ b/crm/crm_web.py @@ -3,17 +3,21 @@ @author: wf """ +import os +import i18n +from crm.i18n_config import I18nConfig from ngwidgets.input_webserver import InputWebserver, InputWebSolution from ngwidgets.webserver import WebserverConfig from nicegui import Client, ui, run from ngwidgets.lod_grid import ListOfDictsGrid, GridConfig - -from crm.crm_core import CRM, Organizations, Persons +from crm.em import CRM +from crm.db import DB +from crm.crm_core import Organizations, Persons from crm.version import Version - -class EntityIndex: - def __init__(self): - +from lodstorage.persistent_log import Log +from mogwai.schema.graph_schema import GraphSchema +from mogwai.web.node_view import NodeTableView, NodeView, NodeViewConfig +from mogwai.core.mogwaigraph import MogwaiGraph, MogwaiGraphConfig class CrmSolution(InputWebSolution): """ @@ -21,73 +25,79 @@ class CrmSolution(InputWebSolution): """ def __init__(self, webserver: "CrmWebServer", client: Client): super().__init__(webserver, client) - self.organizations = Organizations() - self.persons = Persons() - self.org_lod = [] - self.person_lod = [] - self.org_lod_grid = None - self.person_lod_grid = None - - def doUpdateEntities(self): - """ - update entities - """ - try: - with self.header_row: - self.org_count_label.set_text(f"Organizations: {len(self.org_lod)}") - self.person_count_label.set_text(f"Persons: {len(self.person_lod)}") - - if self.org_lod_grid: - self.org_lod_grid.load_lod(self.org_lod) - if self.person_lod_grid: - self.person_lod_grid.load_lod(self.person_lod) - - except Exception as ex: - self.handle_exception(ex) + self.log=self.webserver.log + self.graph=self.webserver.graph + self.schema=self.webserver.schema - async def updateEntities(self): - await run.io_bound(self.doUpdateEntities) + def prepare_ui(self): + pass - def setup_lod_grid(self, lod, key_col: str = "Id"): + def configure_menu(self): """ - setup the list of dicts grid + configure additional non-standard menu entries """ - grid_config = GridConfig( - key_col=key_col, - editable=True, - multiselect=True, - with_buttons=False, - debug=self.args.debug + # Sorting the node types by display_order + sorted_node_types = sorted( + self.schema.node_type_configs.items(), + key=lambda item: item[1].display_order, ) - lod_grid = ListOfDictsGrid(lod=lod, config=grid_config) - lod_grid.set_checkbox_selection(key_col) - return lod_grid - def prepare_ui(self): - pass + for node_type_name, node_type in sorted_node_types: # label e.g. project_list + label_i18nkey = f"{node_type.label.lower()}_list" + label = i18n.t(label_i18nkey) + path = f"/nodes/{node_type_name}" + self.link_button(label, path, node_type.icon, new_tab=False) - async def home(self): - """ - provide the main content page + + async def show_nodes(self, node_type: str): """ - def setup_home(): - with ui.row() as self.header_row: - self.org_count_label = ui.label() - self.person_count_label = ui.label() + show nodes of the given type - with ui.tabs() as tabs: - ui.tab("Organizations") - ui.tab("Persons") + Args: + node_type(str): the type of nodes to show + """ - with ui.tab_panels(tabs, value="Organizations"): - with ui.tab_panel("Organizations"): - self.org_lod_grid = self.setup_lod_grid(self.org_lod, key_col="Id") - with ui.tab_panel("Persons"): - self.person_lod_grid = self.setup_lod_grid(self.person_lod, key_col="Id") + def show(): + try: + config = NodeViewConfig( + solution=self, + graph=self.graph, + schema=self.schema, + node_type=node_type, + ) + if not config.node_type_config: + ui.label(f"{i18n.t('invalid_node_type')}: {node_type}") + return + node_table_view = NodeTableView(config=config) + node_table_view.setup_ui() + except Exception as ex: + self.handle_exception(ex) + + await self.setup_content_div(show) + + async def show_node(self, node_type: str, node_id: str): + """ + show the given node + """ - ui.timer(0, self.updateEntities, once=True) + def show(): + config = NodeViewConfig( + solution=self, graph=self.graph, schema=self.schema, node_type=node_type + ) + if not config.node_type_config: + ui.label(f"{i18n.t('invalid_node_type')}: {node_type}") + return + # default view is the general NodeView + view_class = NodeView + # unless there is a specialization configured + if config.node_type_config._viewclass: + view_class = config.node_type_config._viewclass + node_view = view_class(config=config, node_id=node_id) + node_view.setup_ui() + pass + + await self.setup_content_div(show) - await self.setup_content_div(setup_home) class CrmWebServer(InputWebserver): """ @@ -107,10 +117,46 @@ def get_config(cls) -> WebserverConfig: return server_config def __init__(self): - lods= { - org: - } - self.org_lod = self.organizations.from_json_file() - self.person_lod = self.persons.from_json_file() super().__init__(config=CrmWebServer.get_config()) - + self.log = Log() + config=MogwaiGraphConfig(name_field="_node_name", index_config="minimal") + self.graph = MogwaiGraph(config=config) + + @ui.page("/nodes/{node_type}") + async def show_nodes(client: Client, node_type: str): + """ + show the nodes of the given type + """ + await self.page(client, CrmSolution.show_nodes, node_type) + + @ui.page("/node/{node_type}/{node_id}") + async def node(client: Client, node_type: str, node_id: str): + """ + show the node with the given node_id + """ + await self.page(client, CrmSolution.show_node, node_type, node_id) + + def configure_run(self): + """ + configure with args + """ + #args = self.args + I18nConfig.config() + + InputWebserver.configure_run(self) + module_path = os.path.dirname(os.path.abspath(__file__)) + yaml_path = os.path.join(module_path, "resources", "crm-schema.yaml") + + self.schema = GraphSchema.load(yaml_path=yaml_path) + self.schema.add_to_graph(self.graph) + self.db = DB() + + for entity_class in (Organizations, Persons): + entities = entity_class() + lod = entities.from_db(self.db) + for record in lod: + _node = self.graph.add_labeled_node( + entities.entity_name, + name=entities.entity_name, + properties=record + ) diff --git a/crm/i18n_config.py b/crm/i18n_config.py new file mode 100644 index 0000000..4006da9 --- /dev/null +++ b/crm/i18n_config.py @@ -0,0 +1,27 @@ +""" +Created on 2024-11-16 + +@author: wf +""" + +import os + +import i18n + + +class I18nConfig: + """ + Internationalization module configuration + """ + + @classmethod + def config(cls, debug: bool = False): + module_path = os.path.dirname(os.path.abspath(__file__)) + translations_path = os.path.join(module_path, "resources", "i18n") + if debug: + print(f"Loading translations from: {translations_path}") + print(f"Files in directory: {os.listdir(translations_path)}") + i18n.load_path.append(translations_path) + i18n.set("filename_format", "{locale}.{format}") + i18n.set("file_format", "yaml") + i18n.set("fallback", "en") diff --git a/crm/resources/crm-schema.yaml b/crm/resources/crm-schema.yaml new file mode 100644 index 0000000..07f3253 --- /dev/null +++ b/crm/resources/crm-schema.yaml @@ -0,0 +1,38 @@ +# +# CRM graph schema +# +# WF 2024-11-16 +# +node_id_type_name: str + +# +# configuration of available node types +# +node_type_configs: + NodeTypeConfig: + label: NodeTypeConfig + # https://fonts.google.com/icons?icon.set=Material+Icons + icon: schema + key_field: label + dataclass_name: mogwai.schema.graph_schema.NodeTypeConfig + display_name: Node Type + display_order: 40 + description: Configuration for a graph node type + + Organization: + label: Organization + icon: business + key_field: name + dataclass_name: crm.crm_core.Organization + display_order: 10 + display_name: Organization + description: An Organization + + Person: + label: Person + icon: person + key_field: name + dataclass_name: crm.crm_core.Person + display_order: 10 + display_name: Person + description: A person diff --git a/crm/resources/i18n/de.yaml b/crm/resources/i18n/de.yaml new file mode 100644 index 0000000..5c06942 --- /dev/null +++ b/crm/resources/i18n/de.yaml @@ -0,0 +1,8 @@ +# +# Internationalization of crm +# WF 2014-10-21 +# +de: + person_list: "Personen" + organization_list: "Organizationen" + nodetypeconfig_list: "Knoten" diff --git a/crm/resources/i18n/en.yaml b/crm/resources/i18n/en.yaml new file mode 100644 index 0000000..61dc4a6 --- /dev/null +++ b/crm/resources/i18n/en.yaml @@ -0,0 +1,8 @@ +# +# Internationalization for crm +# WF 2014-10-21 +# +en: + person_list: "Persons" + organization_list: "Organizations" + nodetypeconfig_list: "Nodes" \ No newline at end of file diff --git a/crm/smart_crm.py b/crm/smart_crm.py deleted file mode 100644 index 4c7ba1a..0000000 --- a/crm/smart_crm.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Created on 2024-01-13 - -@author: wf -""" diff --git a/crm/version.py b/crm/version.py index 3444720..a7b77f6 100644 --- a/crm/version.py +++ b/crm/version.py @@ -17,7 +17,7 @@ class Version: name = "niceSmartCRM" version = crm.__version__ date = "2024-01-12" - updated = "2024-01-12" + updated = "2024-11-16" description = "nicegui based Customer Relationship Management" authors = "Wolfgang Fahl" diff --git a/pyproject.toml b/pyproject.toml index 2345da9..fa18e29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ readme = "README.md" license = {text = "Apache-2.0"} dependencies = [ # https://github.com/WolfgangFahl/nicegui_widgets - "ngwidgets>=0.17.2", + "ngwidgets>=0.19.4", # https://pypi.org/project/dataclasses-json/ "dataclasses-json>=0.6.1", # https://github.com/trentm/python-markdown2 @@ -29,6 +29,7 @@ dependencies = [ "PyYAML>=6.0.1", # https://pypi.org/project/linkml/ "linkml>=1.6.8" + # ] requires-python = ">=3.9" diff --git a/tests/test_crm_core.py b/tests/test_crm_core.py index 56a603d..4148112 100644 --- a/tests/test_crm_core.py +++ b/tests/test_crm_core.py @@ -17,7 +17,7 @@ class TestCRM(Basetest): test CRM """ - def setUp(self, debug=False, profile=True): + def setUp(self, debug=True, profile=True): Basetest.setUp(self, debug=debug, profile=profile) self.db = DB() diff --git a/tests/test_db.py b/tests/test_db.py index ba538b0..abb8154 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -14,7 +14,7 @@ class TestDB(Basetest): test Database access layer """ - def setUp(self, debug=False, profile=True): + def setUp(self, debug=True, profile=True): Basetest.setUp(self, debug=debug, profile=profile) self.db = DB() @@ -41,4 +41,6 @@ def test_show_tables(self): """ test showing all tables """ - _results = self.check_query("SHOW TABLES", 25) + results = self.check_query("SHOW TABLES", 25) + if self.debug: + print(results)