From 038af7674d23ead1e5778ae269cce0353b800580 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:18:32 -0500 Subject: [PATCH 1/6] Turned off diagnostic logging - selg.log statements - across all files that make up the app Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/My_Profile/AddCommentScreen.py | 4 +- .../My_Profile/AddToElementsScreens.py | 6 +- .../My_Profile/ConnectionErrorScreen.py | 2 +- .../My_Profile/CreateProfileScreen.py | 4 +- .../CreateSubscriptionRequestScreen.py | 4 +- .../My_Profile/EditElementsScreens.py | 52 +++--- .../DemoCode/My_Profile/FeedbackScreens.py | 2 +- .../DemoCode/My_Profile/MainScreen.py | 23 +-- .../DemoCode/My_Profile/MyTeamScreen.py | 2 +- .../My_Profile/SearchForTermScreen.py | 14 +- .../My_Profile/SelectionOverviewScreen.py | 58 +++--- .../DemoCode/My_Profile/ShopForDataScreen.py | 22 +-- .../DemoCode/My_Profile/ShowCommentsScreen.py | 54 +++--- .../DemoCode/My_Profile/StatusScreen.py | 4 +- .../My_Profile/TechnologyTypeScreens.py | 145 +++++++-------- .../My_Profile/UserIdentitiesScreen.py | 6 +- .../DemoCode/My_Profile/bookmarks_handler.py | 13 +- .../DemoCode/My_Profile/community_handler.py | 31 ++-- .../My_Profile/elements_crud_handler.py | 34 ++-- .../DemoCode/My_Profile/feedback_handler.py | 10 +- .../DemoCode/My_Profile/my_egeria.py | 142 ++++++++------- .../DemoCode/My_Profile/network_handler.py | 19 +- .../DemoCode/My_Profile/team_roles_handler.py | 68 +++---- .../DemoCode/My_Profile/tech_types_handler.py | 170 +++++++++--------- .../my_profile/test_elements_crud_handler.py | 6 + 25 files changed, 461 insertions(+), 434 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py index 4b57e75a..fb3f63de 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py @@ -73,13 +73,13 @@ def handle_button_pressed(self): def action_add_comment(self): if self.comment and self.comment_type: self.comment_data = [self.comment, self.comment_type, self.element_guid] - self.log(f"Comment data: {self.comment_data} being returned from screen") + # self.log(f"Comment data: {self.comment_data} being returned from screen") self.dismiss(self.comment_data) else: self.notify("Please enter both a comment and comment type, before submitting") def on_input_changed(self, event: Input.Changed): - self.log(f"Input detected: {event.input.id}, {event.input.value}") + # self.log(f"Input detected: {event.input.id}, {event.input.value}") if event.input.id == "add_comment_input": self.comment = event.value elif event.input.id == "add_comment_type_input": diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py index 66931b86..be68ee6e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py @@ -127,7 +127,7 @@ def action_add_element(self) -> None: client.create_egeria_bearer_token(self.user_name, self.user_password) guid = self.create_element(client, values) self.created_guids.append(guid) - self.log(f"Created {self.ELEMENT_NAME}: {guid}") + # self.log(f"Created {self.ELEMENT_NAME}: {guid}") self.notify(f"Created {self.ELEMENT_NAME}: {guid}", timeout=10, severity="information") if self.link_to_profile: if not self.user_guid: @@ -402,8 +402,8 @@ def create_element(self, client: Egeria, values: dict[str, str]) -> str: return client.create_actor_role(body=body) def link_element(self, client: Egeria, guid: str) -> None: - self.log(f"link person role, GUID: {guid}") - self.log(f"link person role, user GUID: {self.user_guid}") + # self.log(f"link person role, GUID: {guid}") + # self.log(f"link person role, user GUID: {self.user_guid}") client.link_person_role_to_profile(person_role_guid=guid, person_profile_guid=self.user_guid) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py index 31a631d8..32f9b82d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py @@ -90,7 +90,7 @@ def retry_connection(self) -> None: self.attempts += 1 self.query_one("#connection_retry_status", Static).update("Trying to connect to Egeria...") error = check_egeria_access(self.view_server, self.platform_url, self.user_name, self.user_password) - self.log(f"Connection attempt {self.attempts}: {error or 'connected'}") + # self.log(f"Connection attempt {self.attempts}: {error or 'connected'}") if error is None: self.retry_timer.stop() self.dismiss(200) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py index 673e68f4..177942b0 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py @@ -107,11 +107,11 @@ def create_profile(self) -> int: new_profile_inst = MyProfile(self.view_server, self.platform_url, self.user_name, self.user_password) new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) new_profile_guid = new_profile_inst.add_my_profile(self.new_element_request_body) - self.log(f"Profile created with GUID: {new_profile_guid}") + # self.log(f"Profile created with GUID: {new_profile_guid}") self.dismiss(200) return(200) except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.new_element_request_body}") + # self.log(f"Error creating profile: {e!s} | request={self.new_element_request_body}") self.dismiss(401) return(401) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py index 4f3ae925..555908ad 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py @@ -64,7 +64,7 @@ def on_mount(self) -> None: self.subtitle = "Enter details for the subscription request" def action_quit(self): - self.log("Quitting Create Subscription Request screen") + # self.log("Quitting Create Subscription Request screen") self.dismiss(200) @on(Input.Changed, "#sub_display_name") @@ -86,7 +86,7 @@ def handle_subscription_id_change(self, event): self.identifier = event.value def action_create_subscription(self): - self.log("Creating subscription request") + # self.log("Creating subscription request") self.response: dict = {} if self.selected_item: self.response["externalSourceGUID"] = self.selected_item diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py index 7e65ece1..ee5fe3ac 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py @@ -149,7 +149,8 @@ async def on_mount(self) -> None: if not self.rows_with_keys: self.rows_with_keys = [(row_key.value, main_table.get_row(row_key)) for row_key in main_table.rows] except Exception as e: - self.log(f"Error querying source table #{self.SOURCE_TABLE}: {e}") + pass + # self.log(f"Error querying source table #{self.SOURCE_TABLE}: {e}") self.table.clear(columns=True) if self.columns: @@ -167,7 +168,7 @@ async def on_mount(self) -> None: self.focus() self.call_after_refresh(self.table.focus) except NoMatches as e: - self.log(f"Container #{self.CONTAINER_ID} not found: {e}") + # self.log(f"Container #{self.CONTAINER_ID} not found: {e}") self.dismiss(400) def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: @@ -205,7 +206,7 @@ def _selected_guid(self) -> str | None: try: row = self.table.get_row(self.row_key) except Exception as e: # row removed, or key no longer valid - self.log(f"Could not read row {self.row_key}: {e}") + # self.log(f"Could not read row {self.row_key}: {e}") self.notify("Could not read the selected row.", timeout=5, severity="error") return None @@ -249,7 +250,8 @@ async def on_confirm(confirmed: bool | None) -> None: try: self.table.remove_row(row_key) except Exception as e: - self.log(f"Row {row_key} already gone from the display: {e}") + pass + # self.log(f"Row {row_key} already gone from the display: {e}") self.table.refresh() if self.container is not None: self.container.refresh(layout=True) @@ -409,25 +411,25 @@ def compose(self) -> ComposeResult: if self.karma_points == 0 and hasattr(self, "app"): self.karma_points = getattr(self.app, "karma_points", 0) self.job_title = str(self.user_profile.get("jobTitle", self.user_profile.get("Job Title", ""))) - self.log(f"Job Title: {self.job_title}") + # self.log(f"Job Title: {self.job_title}") self.given_names = str(self.user_profile.get("givenNames", "")) - self.log(f"Given Names: {self.given_names}") + # self.log(f"Given Names: {self.given_names}") self.surname = str(self.user_profile.get("surname", "")) - self.log(f"Surname: {self.surname}") + # self.log(f"Surname: {self.surname}") self.display_name = str(self.user_profile.get("displayName", "")) - self.log(f"Display Name: {self.display_name}") + # self.log(f"Display Name: {self.display_name}") self.pronouns = str(self.user_profile.get("pronouns", "")) - self.log(f"Pronouns: {self.pronouns}") + # self.log(f"Pronouns: {self.pronouns}") self.description = str(self.user_profile.get("description", "")) - self.log(f"Description: {self.description}") + # self.log(f"Description: {self.description}") self.employee_id = str(self.user_profile.get("employeeNumber", self.user_profile.get("employeeId", self.user_profile.get("Employee Number", "")))) - self.log(f"Employee ID: {self.employee_id}") + # self.log(f"Employee ID: {self.employee_id}") self.preferred_language = str(self.user_profile.get("preferredLanguage", "")) - self.log(f"Preferred Language: {self.preferred_language}") + # self.log(f"Preferred Language: {self.preferred_language}") self.resident_country = str(self.user_profile.get("residentCountry", "")) - self.log(f"Resident Country: {self.resident_country}") + # self.log(f"Resident Country: {self.resident_country}") self.time_zone = str(self.user_profile.get("timeZone", "")) - self.log(f"Time Zone: {self.time_zone}") + # self.log(f"Time Zone: {self.time_zone}") self.courtesy_title = str(self.user_profile.get("courtesyTitle", "")) yield Header(show_clock=True) @@ -511,14 +513,14 @@ def create_profile(self) -> Any: input_desc = self.query_one("#user_description", Input).value - self.log(f"Input q_name:{input_q_name}") - self.log(f"Input d_name:{input_d_name}") - self.log(f"Input j_title:{input_j_title}") - self.log(f"Input e_num:{input_e_num}") - self.log(f"Input p_lang:{input_p_lang}") - self.log(f"Input r_country:{input_r_country}") - self.log(f"Input t_z:{input_t_z}") - self.log(f"Input desc:{input_desc}") + # self.log(f"Input q_name:{input_q_name}") + # self.log(f"Input d_name:{input_d_name}") + # self.log(f"Input j_title:{input_j_title}") + # self.log(f"Input e_num:{input_e_num}") + # self.log(f"Input p_lang:{input_p_lang}") + # self.log(f"Input r_country:{input_r_country}") + # self.log(f"Input t_z:{input_t_z}") + # self.log(f"Input desc:{input_desc}") props: dict = {} props.update({"class": "PersonProperties"}) @@ -549,7 +551,7 @@ def create_profile(self) -> Any: if input_desc: props.update({"description": input_desc}) - self.log(f"Props: {props}") + # self.log(f"Props: {props}") self.update_element_request_body: dict = {"class": "UpdateElementRequestBody", "isOwnAnchor": True, @@ -559,11 +561,11 @@ def create_profile(self) -> Any: new_profile_inst = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) new_profile_inst.update_actor_profile(self.user_GUID, self.update_element_request_body) - self.log(f"Profile updated for GUID: {self.user_GUID}") + # self.log(f"Profile updated for GUID: {self.user_GUID}") self.dismiss(200) return (200) except PyegeriaException as e: - self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") + # self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") self.dismiss(401) return (401) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py index 1c12adb8..f0074a54 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py @@ -79,7 +79,7 @@ def action_submit(self): "category": self.category, "text": feedback_text, } - self.log(f"Feedback data: {feedback_data} being returned from screen") + # self.log(f"Feedback data: {feedback_data} being returned from screen") self.dismiss(feedback_data) def action_cancel(self): diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 9e483775..da006a9e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -43,8 +43,9 @@ def __init__(self, *args, **kwargs): def on_mount(self) -> None: # self.log needs a running app, so it can't be called from __init__ - self.log(f"Application Config: {settings.Environment}") - self.log(f"User Profile: {settings.User_Profile}") + pass + # self.log(f"Application Config: {settings.Environment}") + # self.log(f"User Profile: {settings.User_Profile}") def compose(self) -> ComposeResult: # place widgets into grid on screen, note sequence determines position! @@ -123,41 +124,41 @@ def on_descendant_focus(self, event: events.DescendantFocus) -> None: self.selected_row = None else: self.selected_row = None - self.log(f"Focused Table: {self.selected_table}, Row: {self.selected_row}") + # self.log(f"Focused Table: {self.selected_table}, Row: {self.selected_row}") @on(DataTable.RowHighlighted) def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted): """ Collect row and table when the user highlights a row in a table """ self.selected_row = event.row_key self.selected_table = event.data_table.id - self.log(f"Highlighted Table: {self.selected_table}, Row: {self.selected_row}") + # self.log(f"Highlighted Table: {self.selected_table}, Row: {self.selected_row}") @on(DataTable.RowSelected) def on_data_table_row_selected(self, event: DataTable.RowSelected): """ Collect row and table when the user selects a row in a table """ self.selected_row = event.row_key self.selected_table = event.data_table.id - self.log(f"Selected Table: {self.selected_table}, Row: {self.selected_row}") + # self.log(f"Selected Table: {self.selected_table}, Row: {self.selected_row}") @on(DataTable.CellHighlighted) def on_data_table_cell_highlighted(self, event: DataTable.CellHighlighted): """ Collect row and table when a cell is highlighted in a table """ self.selected_row = event.cell_key.row_key self.selected_table = event.data_table.id - self.log(f"Cell Highlighted Table: {self.selected_table}, Row: {self.selected_row}") + # self.log(f"Cell Highlighted Table: {self.selected_table}, Row: {self.selected_row}") @on(DataTable.CellSelected) def on_data_table_cell_selected(self, event: DataTable.CellSelected): """ Collect row and table when a cell is selected in a table """ self.selected_row = event.cell_key.row_key self.selected_table = event.data_table.id - self.log(f"Cell Selected Table: {self.selected_table}, Row: {self.selected_row}") + # self.log(f"Cell Selected Table: {self.selected_table}, Row: {self.selected_row}") @on(DataTable.HeaderSelected) def on_data_table_header_selected(self, event: DataTable.HeaderSelected): """ Collect table when header is selected in a table """ self.selected_table = event.data_table.id - self.log(f"Header Selected Table: {self.selected_table}") + # self.log(f"Header Selected Table: {self.selected_table}") def get_focused_table(self) -> DataTable | None: focused_widget = self.screen.focused @@ -188,7 +189,7 @@ def action_edit_table(self): if table_name: self.selected_table = table_name self.selected_row = row_k - self.log(f"Editing Table: {table_name}, Row: {row_k}") + # self.log(f"Editing Table: {table_name}, Row: {row_k}") self.app.edit_tables(table_name, row_k) else: self.notify("Please select at least a table, or a table and row to edit.", timeout=5, severity="warning") @@ -212,10 +213,10 @@ async def action_add_comment(self): add_comment_return = self.app.add_comment(table_name, row_k) if add_comment_return: if add_comment_return == 200: - self.log("Successfully added comment") + # self.log("Successfully added comment") return else: - self.log("Failed to add comment.") + # self.log("Failed to add comment.") self.notify("Failed to add comment.", timeout=20, severity="error") else: self.notify("Please select a row and table to add a comment.", timeout=5, severity="warning") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py index 8ac9674d..7f7aaa8e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py @@ -62,7 +62,7 @@ def on_mount(self) -> None: for member in self.my_team: self.team_table.add_row(member[0], member[1], member[2]) else: - self.log(f"No team member data for MyTeam Screen", { "my_team": self.my_team}) + # self.log(f"No team member data for MyTeam Screen", { "my_team": self.my_team}) self.team_table.add_column("Error") self.team_table.add_column("Description") self.team_table.add_row("No team members found", "No team members returned from Egeria for this team") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py index af2ef78a..693a4617 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py @@ -65,32 +65,32 @@ def compose(self) -> ComposeResult: yield Footer() def action_quit(self) -> None: - self.log("User requested quit") + # self.log("User requested quit") self.dismiss(200) def action_go_back(self) -> None: - self.log("User requested go back") + # self.log("User requested go back") self.dismiss(201) @on(Input.Changed, "#search_term_input") def handle_search_term_input_changed(self, event: Input.Changed) -> None: self.search_term = event.value - self.log(f"Search term input changed: {self.search_term}") + # self.log(f"Search term input changed: {self.search_term}") @on(Button.Pressed, "#search_term_btn") async def handle_search_term(self) -> None: - self.log("User requested search term") + # self.log("User requested search term") if self.search_term: # search for the term search_result = self.search_for_the_term(self.search_term) - self.log(f"Search result: {search_result}") + # self.log(f"Search result: {search_result}") my_placeholder = self.query_one("#search_term_result") await my_placeholder.remove() markdown = Markdown(search_result, id="search_term_result") markdown.code_indent_guides = False await self.query_one("#search_term_result_container", ScrollableContainer).mount(markdown) else: - self.log("No search term provided") + # self.log("No search term provided") my_placeholder = self.query_one("#search_term_result") await my_placeholder.remove() markdown = Markdown(f"# No Content Found for: {self.search_term}", id="search_term_result") @@ -121,6 +121,6 @@ def search_for_the_term(self, search_term): return self.search_result except PyegeriaException as e: - self.log(f"Error searching for term: {e}") + # self.log(f"Error searching for term: {e}") self.search_result = f"# Error searching for term: {e}" return self.search_result diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py index 1ef46122..db475d94 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py @@ -104,14 +104,14 @@ def handle_tree_node_selected(self, event: Tree.NodeSelected): # Modify to handle for each tree? self.node_selected = event.node self.tree_selected = event.node.parent - self.log(f"Tree selected: {self.tree_selected}") - self.log(f"Node selected: {self.node_selected}") + # self.log(f"Tree selected: {self.tree_selected}") + # self.log(f"Node selected: {self.node_selected}") self.node_label = self.node_selected.label self.node_data = self.node_selected.data # the provided id can be either a GUID or a qualified name! # the variable is labeled guid but it could contain a qualified name, both guid and qualified name are strings. self.node_GUID = self.node_selected.data - self.log(f"Node label: {self.node_label}, GUID: {self.node_GUID}") + # self.log(f"Node label: {self.node_label}, GUID: {self.node_GUID}") if self.category == "glossary": self.display_selected_term_details(self.node_GUID) elif self.category == "catalog": @@ -129,7 +129,7 @@ def display_selected_term_details(self, term_GUID) -> Any: """ The user has selected a glossary term, build a display of the term details, and show along side the glossary tree """ self.term_GUID = term_GUID - self.log(f"Selected glossary term GUID: {self.term_GUID}") + # self.log(f"Selected glossary term GUID: {self.term_GUID}") try: self.glossary_term_data = exec_report_spec(format_set_name="Glossary-Terms", output_format="MD", @@ -140,7 +140,7 @@ def display_selected_term_details(self, term_GUID) -> Any: user=self.user_name, user_pass=self.user_password) except PyegeriaException as e: - self.log(f"Error retrieving glossary details: {e!s}") + # self.log(f"Error retrieving glossary details: {e!s}") self.glossary_term_data = [] if isinstance(self.glossary_term_data, dict): if self.glossary_term_data.get("kind") == "text": @@ -157,10 +157,10 @@ def display_selected_term_details(self, term_GUID) -> Any: # if not self.glossary_term_data or self.glossary_term_data == [] or self.glossary_term_data == None or self.glossary_term_data.get("kind") == "empty": if not self.glossary_term_data or self.glossary_term_data == [] or self.glossary_term_data == None: - self.log(f"No glossary term data returned for GUID: {self.term_GUID}") + # self.log(f"No glossary term data returned for GUID: {self.term_GUID}") container.mount(Static(f"No glossary term data returned for GUID: {self.term_GUID}")) else: - self.log(f"Glossary term data returned for term: {self.term_GUID}, content: {self.glossary_term_data} \n") + # self.log(f"Glossary term data returned for term: {self.term_GUID}, content: {self.glossary_term_data} \n") markdown = Markdown(self.glossary_term_data) markdown.code_indent_guides = False self.query_one("#data_details_placeholder_container").mount(markdown) @@ -169,7 +169,7 @@ def display_selected_digital_product(self, digital_product_GUID) -> Any: """ The user has selected a glossary term, build a display of the term details, and show along side the glossary tree """ self.digital_product_GUID = digital_product_GUID - self.log(f"Selected digital product: {self.digital_product_GUID}") + # self.log(f"Selected digital product: {self.digital_product_GUID}") try: self.digital_product_data = exec_report_spec(format_set_name="Digital-Products-MyE", output_format="MD", @@ -182,17 +182,17 @@ def display_selected_digital_product(self, digital_product_GUID) -> Any: user_pass=self.user_password) except PyegeriaException as e: print_basic_exception(e) - self.log(f"Error retrieving digital product details: {e!s}") + # self.log(f"Error retrieving digital product details: {e!s}") self.digital_product_data = [] container = self.query_one("#data_details_placeholder_container") container.remove_children() if not self.digital_product_data or self.digital_product_data == [] or self.digital_product_data == None: - self.log(f"No digital product data returned for: {self.digital_product_GUID}") + # self.log(f"No digital product data returned for: {self.digital_product_GUID}") container.mount(Static(f"No digital product data returned for: {self.digital_product_GUID}")) else: - self.log(f"Digital product data returned for: {self.digital_product_GUID}") + # self.log(f"Digital product data returned for: {self.digital_product_GUID}") if isinstance(self.digital_product_data, dict) and self.digital_product_data.get("kind") == "empty": self.digital_product_data = "# No content found for this digital product" elif isinstance(self.digital_product_data, dict) and self.digital_product_data.get("kind") == "text": @@ -214,7 +214,7 @@ def display_selected_data_dictionary(self, data_dictionary_GUID) -> Any: """ The user has selected a data dictionary, build a display of the dictionary details, and show along side the dictionary tree """ self.data_dictionary_GUID = data_dictionary_GUID - self.log(f"Selected data dictionary GUID: {self.data_dictionary_GUID}") + # self.log(f"Selected data dictionary GUID: {self.data_dictionary_GUID}") try: self.data_dictionary_data = exec_report_spec(format_set_name="Data-Dictionaries", output_format="MD", @@ -226,17 +226,17 @@ def display_selected_data_dictionary(self, data_dictionary_GUID) -> Any: user=self.user_name, user_pass=self.user_password) except PyegeriaException as e: - self.log(f"Error retrieving data dictionary details: {e!s}") + # self.log(f"Error retrieving data dictionary details: {e!s}") self.data_dictionary_data = [] container = self.query_one("#data_details_placeholder_container") container.remove_children() if not self.data_dictionary_data or self.data_dictionary_data == []: - self.log(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}") + # self.log(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}") container.mount(Static(f"No data dictionary data returned for GUID: {self.data_dictionary_GUID}")) else: - self.log(f"Data dictionary data returned for GUID: {self.data_dictionary_GUID}") + # self.log(f"Data dictionary data returned for GUID: {self.data_dictionary_GUID}") if isinstance(self.data_dictionary_data, dict) and self.data_dictionary_data.get("kind") == "text": if self.data_dictionary_data.get("mime") == "text/markdown": self.data_dictionary_data = self.data_dictionary_data.get("content") @@ -251,7 +251,7 @@ def display_selected_business_domain(self, business_domain_GUID) -> int: """ The user has selected a business domain, build a display of the domain details, and show along side the glossary tree """ self.business_domain_GUID = business_domain_GUID - self.log(f"Selected business domain GUID: {self.business_domain_GUID}") + # self.log(f"Selected business domain GUID: {self.business_domain_GUID}") try: self.business_domain_data = exec_report_spec(format_set_name="BusinessCapabilities", output_format="DICT", @@ -263,17 +263,17 @@ def display_selected_business_domain(self, business_domain_GUID) -> int: user=self.user_name, user_pass=self.user_password) except PyegeriaException as e: - self.log(f"Error retrieving business domain details: {e!s}") + # self.log(f"Error retrieving business domain details: {e!s}") self.business_domain_data = [] container = self.query_one("#data_details_placeholder_container") container.remove_children() if not self.business_domain_data or self.business_domain_data == []: - self.log(f"No business domain data returned for GUID: {self.business_domain_GUID}") + # self.log(f"No business domain data returned for GUID: {self.business_domain_GUID}") container.mount(Static(f"No business domain data returned for GUID: {self.business_domain_GUID}")) else: - self.log(f"Business domain data returned for GUID: {self.business_domain_GUID}") + # self.log(f"Business domain data returned for GUID: {self.business_domain_GUID}") text_area = TextArea(f"Business domain data returned for GUID: {self.business_domain_GUID}", id="business_domain_details_text_area", read_only=True) @@ -285,7 +285,7 @@ def display_selected_data_specification(self, data_specification_GUID) -> int: and show alongside data specifications tree """ self.data_specification_qualified_name = data_specification_GUID - self.log(f"Data specification selected: {self.data_specification_qualified_name}") + # self.log(f"Data specification selected: {self.data_specification_qualified_name}") try: self.data_specification_data = exec_report_spec(format_set_name="Data-Specification", output_format="DICT", @@ -296,17 +296,17 @@ def display_selected_data_specification(self, data_specification_GUID) -> int: user=self.user_name, user_pass=self.user_password) except PyegeriaException as e: - self.log(f"Error retrieving data specification details: {e!s}") + # self.log(f"Error retrieving data specification details: {e!s}") self.data_specification_data = [] container = self.query_one("#data_details_placeholder_container") container.remove_children() if not self.data_specification_data or self.data_specification_data == []: - self.log(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}") + # self.log(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}") container.mount(Static(f"No data specification data returned for qualified name: {self.data_specification_qualified_name}")) else: - self.log(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}") + # self.log(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}") text_area = TextArea(f"Data specification data returned for qualified name: {self.data_specification_qualified_name}", id="data_specification_details_text_area", read_only=True) @@ -317,7 +317,7 @@ def display_selected_root_collection(self, root_collection_GUID) -> int: """ The user has selected a root collection, build a display of the collection details, and show alongside the collection tree """ self.root_collection_GUID = root_collection_GUID - self.log(f"Selected root collection GUID: {self.root_collection_GUID}") + # self.log(f"Selected root collection GUID: {self.root_collection_GUID}") try: self.root_collection_data = exec_report_spec(format_set_name="Collections", output_format="MD", @@ -330,17 +330,17 @@ def display_selected_root_collection(self, root_collection_GUID) -> int: user_pass=self.user_password) except PyegeriaException as e: - self.log(f"Error retrieving root collection data: {e}") + # self.log(f"Error retrieving root collection data: {e}") self.collection_data = [] container = self.query_one("#data_details_placeholder_container") container.remove_children() if not self.root_collection_data or self.root_collection_data == [] or self.root_collection_data == None: - self.log(f"No Root collection data returned for GUID: {self.root_collection_GUID}") + # self.log(f"No Root collection data returned for GUID: {self.root_collection_GUID}") container.mount(Static(f"No Root collection data returned for GUID: {self.root_collection_GUID}")) else: - self.log(f"Root collection data returned for GUID: {self.root_collection_GUID}") + # self.log(f"Root collection data returned for GUID: {self.root_collection_GUID}") if isinstance(self.root_collection_data, dict) and self.root_collection_data.get("kind") == "text": if self.root_collection_data.get("mime") == "text/markdown": self.root_collection_data = self.root_collection_data.get("content") @@ -353,9 +353,9 @@ def display_selected_root_collection(self, root_collection_GUID) -> int: def action_subscribe(self): """ The subscribe option in the footer has been selected. """ - self.log(f"Subscribe to data selected: {self.node_GUID}") + # self.log(f"Subscribe to data selected: {self.node_GUID}") if self.node_GUID == None: - self.log(f"No node GUID selected for subscription") + # self.log(f"No node GUID selected for subscription") self.notify("The node selected for subscription has no Digital Products available, please select another node", timeout=10, severity="error") return else: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py index 35cd4e08..94e47b91 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py @@ -146,20 +146,20 @@ def handle_glossary_table_selection(self, event: DataTable.RowSelected): row_display_name = row_values[0] row_description = row_values[1] row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") + # self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") self.dismiss (["glossary", row_qualified_name, row_display_name]) @on(DataTable.RowSelected, "#digital_product_catalog_table") def handle_digital_product_catalog_table_selection(self, event: DataTable.RowSelected): row_selected = event.row_key - self.log(f"Digital product row selected: {row_selected}, event: {event}") + # self.log(f"Digital product row selected: {row_selected}, event: {event}") row_values = self.query_one("#digital_product_catalog_table", DataTable).get_row(event.row_key) - self.log(f"Digital product row values: {row_values}") + # self.log(f"Digital product row values: {row_values}") row_display_name = row_values[0] row_description = row_values[1] row_qualified_name = row_values[2] row_guid = row_values[3] if len(row_values) > 3 else None - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}, guid: {row_guid}") + # self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}, guid: {row_guid}") self.dismiss(["catalog", row_qualified_name, row_display_name, row_guid]) @on(DataTable.RowSelected, "#data_dictionary_table") @@ -169,7 +169,7 @@ def handle_data_dictionary_table_selection(self, event: DataTable.RowSelected): row_display_name = row_values[0] row_description = row_values[1] row_qualified_name = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") + # self.log(f"Row selected: {row_selected}, values: {row_values}, display name: {row_display_name}, description: {row_description}, qualified name: {row_qualified_name}") self.dismiss (["dictionary", row_qualified_name, row_display_name]) @on(DataTable.RowSelected, "#business_domain_table") @@ -179,7 +179,7 @@ def handle_business_domain_table_selection(self, event: DataTable.RowSelected): row_qualified_name = row_values[0] row_type_name = row_values[1] row_guid = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, type name: {row_type_name}, guid: {row_guid}") + # self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, type name: {row_type_name}, guid: {row_guid}") self.dismiss (["domain", row_qualified_name, row_type_name]) @on (DataTable.RowSelected, "#root_collection_table") @@ -189,7 +189,7 @@ def handle_root_collection_table_selection(self, event: DataTable.RowSelected): row_qualified_name = row_values[0] row_description = row_values[1] row_GUID = row_values[2] - self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") + # self.log(f"Row selected: {row_selected}, values: {row_values}, qualified name: {row_qualified_name}, description: {row_description}, GUID: {row_GUID}") self.dismiss (["collection", row_qualified_name, row_description]) @on(DataTable.RowHighlighted) @@ -198,7 +198,7 @@ def handle_row_highlighted(self, event: DataTable.RowHighlighted) -> None: self.row_highlighted = event.row_key self.cursor_row_highlighted = event.cursor_row self.data_table_highlighted = event.data_table.id - self.log(f"Table {self.data_table_highlighted} Row highlighted: {self.row_highlighted}, cursor: {self.cursor_row_highlighted}") + # self.log(f"Table {self.data_table_highlighted} Row highlighted: {self.row_highlighted}, cursor: {self.cursor_row_highlighted}") def _highlighted_table_and_row(self) -> tuple[DataTable | None, Any]: """ The focused (or last highlighted) table and the row under its cursor, if any """ @@ -286,7 +286,8 @@ def action_sample_data_source(self): else: row_data = list(target_table.get_row_at(0)) except Exception as e: - self.log(f"Error getting row data from table: {e}") + pass + # self.log(f"Error getting row data from table: {e}") self.dismiss([ 212, @@ -347,7 +348,8 @@ def action_subscribe_to_data_source(self): else: row_data = list(target_table.get_row_at(0)) except Exception as e: - self.log(f"Error getting row data from table: {e}") + pass + # self.log(f"Error getting row data from table: {e}") self.dismiss([ 211, diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py index 2a124655..b7174353 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py @@ -72,7 +72,7 @@ def on_mount(self): else: # Extract the correct string identifier backend_id = self.extract_backend_identifier(self.table, self.row) - self.log(f"Backend identifier: {backend_id} extracted") + # self.log(f"Backend identifier: {backend_id} extracted") if not backend_id: self.notify("Selected table does not contain valid GUID or Qualified Name columns.", severity="error") return @@ -97,21 +97,21 @@ def load_comments(self, element_guid: str) -> None: self.notify(f"No comments found: {str(e)}", severity="warning") return - self.log(f"comments_list: {comments_list}") + # self.log(f"comments_list: {comments_list}") comment_table = self.show_comments_datatable if isinstance(comments_list, dict): if comments_list.get("kind") == "json": comments_list_ext = comments_list.get("data") or [] - self.log(f"comments_list_ext: {comments_list_ext}, len: {len(comments_list_ext)}") + # self.log(f"comments_list_ext: {comments_list_ext}, len: {len(comments_list_ext)}") for comment in comments_list_ext: - self.log(f"processing value: {comment}") + # self.log(f"processing value: {comment}") comment_table.add_row(comment.get('Description'), comment.get('Comment Guid'), comment.get('Qualified Name'), comment.get('Display Name')) elif isinstance(comments_list, str): - self.log(f"processing str comment: {comments_list}") + # self.log(f"processing str comment: {comments_list}") comment_table.add_row(comments_list, "", "", "") elif comments_list: - self.log(f"processing list of: {len(comments_list)} comments") + # self.log(f"processing list of: {len(comments_list)} comments") for comment in comments_list: comment_table.add_row(str(comment), "", "", "") @@ -142,12 +142,12 @@ def extract_backend_identifier(self, table_name, row_key): # Fallback to current screen table_addr = self.app.screen.query_one(f"#{table_name}", DataTable) except NoMatches: - self.log(f"Table {table_name} not found on any relevant screen") + # self.log(f"Table {table_name} not found on any relevant screen") return None # Map the clean string representation of column labels to their index positions column_mapping = {col.label.plain.strip(): idx for idx, col in enumerate(table_addr.columns.values())} - self.log(f"Column mapping for table {table_name}: {column_mapping}") + # self.log(f"Column mapping for table {table_name}: {column_mapping}") # Case-insensitive check for target columns upper_mapping = {k.upper(): v for k, v in column_mapping.items()} @@ -161,22 +161,22 @@ def extract_backend_identifier(self, table_name, row_key): params=({"search_string":upper_mapping["QUALIFIED NAME"]})) backend_id = comment_attributes["GUID"] idx = backend_id - self.log(f"Target column idx value: {idx}") + # self.log(f"Target column idx value: {idx}") if idx is None: - self.log(f"Target columns GUID or Qualified Name not found in {list(column_mapping.keys())}") + # self.log(f"Target columns GUID or Qualified Name not found in {list(column_mapping.keys())}") return None self.selected_row = idx - self.log(f"Selected row index: {self.selected_row}") + # self.log(f"Selected row index: {self.selected_row}") try: # handle if row_key is RowKey object or string actual_row_key = row_key row_data = table_addr.get_row(actual_row_key) - self.log(f"Row data: {row_data}") + # self.log(f"Row data: {row_data}") return str(row_data[idx]) except Exception as e: - self.log(f"Error retrieving row {row_key} from table {table_name}: {e}") + # self.log(f"Error retrieving row {row_key} from table {table_name}: {e}") return None @@ -196,7 +196,7 @@ def compose(self) -> ComposeResult: def action_add_comment(self): """ Add an attached Comment to the selected row """ if self.backend_id: - self.log(f"Selected row: {self.backend_id}") + # self.log(f"Selected row: {self.backend_id}") container = (self.query_one("#show_comments_container", ScrollableContainer)) container.remove_children() container.mount(Static(f"[b]Add Comment[/b]")) @@ -210,29 +210,29 @@ def action_add_comment(self): def action_add_response(self): """ Reopen this screen focused on the highlighted comment, to view/add responses to it """ if self.highlighted_comment_guid: - self.log(f"Add response to comment: Selected row: {self.highlighted_comment_guid}") + # self.log(f"Add response to comment: Selected row: {self.highlighted_comment_guid}") self.dismiss([250, self.highlighted_comment_guid]) else: self.notify("Highlight a comment in the table first, to respond to it", severity="warning") def on_input_changed(self, event: Input.Changed): - self.log(f"Input detected: {event.input.id}, {event.input.value}") + # self.log(f"Input detected: {event.input.id}, {event.input.value}") if event.input.id == "add_comment_input": - self.log(f"Input changed: {event.input.value}") + # self.log(f"Input changed: {event.input.value}") self.comment_text = str(event.input.value) elif event.input.id == "add_comment_type_input": - self.log(f"Input changed: {event.input.value}") + # self.log(f"Input changed: {event.input.value}") self.comment_type = str(event.input.value) else: self.notify("Input not recognized! Please try again") return - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") + # self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") @on(Button.Pressed, "#add_comment_button") def handle_button_pressed(self, event: Button.Pressed): """ Handle user request to add a comment to the selected element """ - self.log(f"Button pressed: {event.button.id}") - self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") + # self.log(f"Button pressed: {event.button.id}") + # self.log(f"Comment text: {self.comment_text}, Comment type: {self.comment_type}") if self.comment_text and self.comment_type: # add_comment dismisses the screen itself on success, and stays open on failure self.add_comment() @@ -261,30 +261,30 @@ def add_comment(self): try: # Add comment to selected element self.comment_type = self.comment_type.strip().upper().replace(" ", "_") - self.log(f"Adding comment to element: {self.backend_id}, values: {self.comment_text}, {self.comment_type}") + # self.log(f"Adding comment to element: {self.backend_id}, values: {self.comment_text}, {self.comment_type}") response = egeria_tech.add_comment_to_element( element_guid=self.backend_id, comment=self.comment_text, comment_type=self.comment_type ) - self.log(f"Comment successfully added! New Comment GUID: {response}") + # self.log(f"Comment successfully added! New Comment GUID: {response}") self.notify(f"Comment successfully added! New Comment GUID: {response}") container = (self.query_one("#show_comments_container", ScrollableContainer)) container.remove_children() except Exception as e: - self.log(f"Failed to add comment: {e}") + # self.log(f"Failed to add comment: {e}") self.notify(f"Failed to add comment: {e} \n Please try again.") return self.dismiss(200) @on(DataTable.RowHighlighted, "#show_comments_dt") def on_row_highlighted(self, event: DataTable.RowHighlighted): - self.log(f"Row highlighted: {event.row_key}") + # self.log(f"Row highlighted: {event.row_key}") self._track_comment_row(event.row_key) @on(DataTable.RowSelected, "#show_comments_dt") def on_row_selected(self, event: DataTable.RowSelected): - self.log(f"Row selected: {event.row_key}") + # self.log(f"Row selected: {event.row_key}") self._track_comment_row(event.row_key) def _track_comment_row(self, row_key) -> None: @@ -293,7 +293,7 @@ def _track_comment_row(self, row_key) -> None: try: row_data = self.show_comments_datatable.get_row(row_key) except Exception as e: - self.log(f"Unable to read comment row {row_key}: {e}") + # self.log(f"Unable to read comment row {row_key}: {e}") self.highlighted_comment_guid = None return self.highlighted_comment_guid = row_data[1] or None diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py index f7c6a000..7ddfebc6 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py @@ -55,11 +55,11 @@ def action_copy_guid_to_clipboard(self) -> None: """ The copy GUID to clipboard option in the footer has been selected. Copy the GUID to the clipboard.""" # The GUID is between single ' marks in the status_message when a good result is recorded. match = re.search(r"'(.*?)'", self.status_message) - self.log(f"Match: {match}") + # self.log(f"Match: {match}") if match: guid = match.group(1) copy_to_clipboard(guid) - self.log(f"Copied GUID to clipboard: {guid}") + # self.log(f"Copied GUID to clipboard: {guid}") else: self.dismiss(400) self.dismiss(200) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py index 9a53c545..f525b50e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py @@ -90,25 +90,26 @@ async def on_mount(self) -> int: """Mount the TechnologyTypeOptions screen.""" self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" - self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") + # self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") for widget in self.query(): - self.log(f"Widgets: {widget}") + pass + # self.log(f"Widgets: {widget}") - self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") - self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") + # self.log(f"Templates: {self.tech_type_templates}, Processes: {self.tech_type_processes}") + # self.log(f"Templates Type: {type(self.tech_type_templates)}, Processes Type: {type(self.tech_type_processes)}") if self.tech_type_templates and self.tech_type_templates != "None": for t in self.tech_type_templates: try: - self.log(f"Template: {t}") + # self.log(f"Template: {t}") templates = self.query_one("#template_options", OptionList).add_option( Option(t.get("displayName"))) - self.log(f"Added option: {t.get('Catalog Template Name')}") + # self.log(f"Added option: {t.get('Catalog Template Name')}") await templates.mount(after=self.query_one("#template_options_label")) - self.log(f"Mounted option") + # self.log(f"Mounted option") self.query_one("#select_template_btn", Button).disabled = False - self.log(f"Enabled the template button") + # self.log(f"Enabled the template button") except NoMatches as e: try: templates: OptionList = OptionList(id="template_options") @@ -118,16 +119,16 @@ async def on_mount(self) -> int: self.query_one("#select_template_btn", Button).disabled = False continue except Exception as e: - self.log(f"Error creating template option list: {e}, (410") + # self.log(f"Error creating template option list: {e}, (410") return (410) except Exception as e: - self.log(f"Error creating template option list: {e} (411)") + # self.log(f"Error creating template option list: {e} (411)") return (411) else: continue else: try: - self.log(f" No Templates") + # self.log(f" No Templates") templates = self.query_one("#template_options", OptionList).add_option( Option("No Templates Found for this Tech Type")) await templates.mount(after=self.query_one("#template_options_label")) @@ -139,22 +140,22 @@ async def on_mount(self) -> int: templates.add_option(Option("No Templates found for this Tech Type")) self.query_one("#select_template_btn", Button).disabled = True except Exception as e: - self.log(f"Error creating template option list: {e}") + # self.log(f"Error creating template option list: {e}") return (409) except Exception as e: - self.log(f"Error creating template option list: {e}") + # self.log(f"Error creating template option list: {e}") return (408) if self.tech_type_processes and self.tech_type_processes != "None": for p in self.tech_type_processes: try: - self.log(f"Process: {p}") + # self.log(f"Process: {p}") processes = self.query_one("#process_options", OptionList).add_option( Option(p.get("displayName"))) - self.log(f"Process option added: {p.get('displayName')}") + # self.log(f"Process option added: {p.get('displayName')}") await processes.mount(after=self.query_one("#process_options_label")) self.query_one("#select_process_btn", Button).disabled = False - self.log(f"Enabled the process button") + # self.log(f"Enabled the process button") except NoMatches as e: try: processes: OptionList = OptionList(id="process_options") @@ -163,16 +164,16 @@ async def on_mount(self) -> int: processes.refresh() self.query_one("#select_process_btn", Button).disabled = False except Exception as e: - self.log(f"Error creating process option list: {e}") + # self.log(f"Error creating process option list: {e}") return (407) except Exception as e: - self.log(f"Error creating process option list: {e}") + # self.log(f"Error creating process option list: {e}") return (406) else: continue else: try: - self.log(f" No Processes") + # self.log(f" No Processes") processes = self.query_one("#process_options", OptionList).add_option( Option("No processes found for this Tech Type")) await processes.mount(after=self.query_one("#process_options_label")) @@ -184,10 +185,10 @@ async def on_mount(self) -> int: processes.add_option(Option(" No Processes found for this Tech Type")) self.query_one("#select_process_btn", Button).disabled = True except Exception as e: - self.log(f"Error creating process option list: {e}") + # self.log(f"Error creating process option list: {e}") return (405) except Exception as e: - self.log(f"Error creating process option list: {e}") + # self.log(f"Error creating process option list: {e}") return (404) return(200) @@ -195,44 +196,44 @@ async def on_mount(self) -> int: @on(OptionList.OptionHighlighted, "#template_options") def handle_template_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: """Handle the highlighting of an option in the template option list.""" - self.log(f"Template option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") + # self.log(f"Template option highlighted: {event.option}") + # self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") self.selected_template_index = event.option_index selected_option = event.option self.selected_template = selected_option.prompt - self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") + # self.log(f"Highlighted Template: {self.selected_template}, index: {self.selected_template_index}") @on(OptionList.OptionSelected, "#template_options") def handle_template_option_selected(self, event: OptionList.OptionSelected) -> None: """Handle the selection of an option from the template or process option lists.""" - self.log(f"Template selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") + # self.log(f"Template selected: {event.option}") + # self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") self.option_type_selected = event.option_list.name self.selected_template_index = event.option_index selected_option = event.option self.selected_template = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") + # self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_template}, index: {self.selected_template_index}") @on(OptionList.OptionHighlighted, "#process_options") def handle_process_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: """Handle the highlighting of an option in the process option list.""" - self.log(f"Process option highlighted: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") + # self.log(f"Process option highlighted: {event.option}") + # self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") self.selected_process_index = event.option_index selected_option = event.option self.selected_process = selected_option.prompt - self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") + # self.log(f"Highlighted Process: {self.selected_process}, index: {self.selected_process_index}") @on(OptionList.OptionSelected, "#process_options") def handle_process_option_selected(self, event: OptionList.OptionSelected) -> None: """Handle the selection of an option from the template or process option lists.""" - self.log(f"Process selected: {event.option}") - self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") + # self.log(f"Process selected: {event.option}") + # self.log(f"Option index: {event.option_index}, Option list: {event.option_list.name}") self.option_type_selected = event.option_list.name self.selected_process_index = event.option_index selected_option = event.option self.selected_process = selected_option.prompt - self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") + # self.log(f"Selected Option List: {self.option_type_selected}, template: {self.selected_process}, index: {self.selected_process_index}") def action_go_back(self) -> None: """ Handle the back key press.""" @@ -247,7 +248,7 @@ def handle_template_selected(self, event: Button.Pressed) -> None: """Handle the selection of a template option.""" if self.selected_template is None: return - self.log(f"Template option selected: {self.selected_template}") + # self.log(f"Template option selected: {self.selected_template}") self.dismiss(["template", self.selected_template]) @on(Button.Pressed, "#select_process_btn") @@ -255,7 +256,7 @@ def handle_process_selected(self, event: Button.Pressed) -> None: """Handle the selection of a process option.""" if self.selected_process is None: return - self.log(f"Process option selected: {self.selected_process}") + # self.log(f"Process option selected: {self.selected_process}") self.dismiss(["process", self.selected_process]) class TechnologyTypeProcessesScreen(ModalScreen[Any]): @@ -299,33 +300,33 @@ async def on_mount(self) -> None: self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" if self.selected_t_option == "process": - self.log(f"Processing processes, with data: {self.selected_t_option_selected}") + # self.log(f"Processing processes, with data: {self.selected_t_option_selected}") # get selected process from the tech_type data - self.log(f"Technology Type Process: {self.tech_type_processes}") + # self.log(f"Technology Type Process: {self.tech_type_processes}") if isinstance(self.tech_type_processes, list): for process in self.tech_type_processes: - self.log(f"Process: {process.get("displayName")}, Selected: {self.selected_t_option_selected}") + # self.log(f"Process: {process.get("displayName")}, Selected: {self.selected_t_option_selected}") if self.selected_t_option_selected == process.get("displayName"): self.full_process = process self.selected_t_process = process - self.log(f"Selected Process: {self.selected_t_process}") + # self.log(f"Selected Process: {self.selected_t_process}") break else: continue - self.log(f"Selected Process: {self.selected_t_process}, type: {type(self.selected_t_process)}") + # self.log(f"Selected Process: {self.selected_t_process}, type: {type(self.selected_t_process)}") if self.selected_t_process is None: - self.log("No process selected, skipping placeholder display") + # self.log("No process selected, skipping placeholder display") return else: specification = self.selected_t_process.get("specification") if specification is None: - self.log("Selected process has no specification, skipping placeholder display") + # self.log("Selected process has no specification, skipping placeholder display") return else: placeholderProperties = specification.get("supportedRequestParameter") if placeholderProperties is None: - self.log("Selected process has no req uest parameters, skipping display") + # self.log("Selected process has no req uest parameters, skipping display") return else: for parameter in placeholderProperties: @@ -343,7 +344,7 @@ async def on_mount(self) -> None: safe_name = name.replace(" ", "_") if name else f"placeholder_{id(self.selected_t_process)}" safe_name = safe_name.replace("::", "_") safe_name = safe_name.replace(":", "_") - self.log(f"Safe name = {safe_name}") + # self.log(f"Safe name = {safe_name}") placeholder_text: TextArea = TextArea( f"{name}\n\nDescription: {Description}\nType: {Type}\nExample: {Example}\nRequired: {Required}", id=f"{safe_name}_placeholder_text_area", @@ -355,18 +356,18 @@ async def on_mount(self) -> None: placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") # Remember the real parameter name: the CSS-safe id can't be turned back into it self.parameter_names[placeholder_input.id] = name or safe_name - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") + # self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") # Mount the TextArea and the associated Input field into the ScrollableContainer try: load_point = self.query_one("#technology_type_processes_input") await load_point.mount(placeholder_text, before="#process_submit_button") await load_point.mount(placeholder_input, before="#process_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") + # self.log(f"Placeholder text area loaded: {placeholder_text.text}") + # self.log(f"Placeholder input loaded: {placeholder_input}") continue except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") + # self.log(f"Error loading placeholder container: {e!s}") self.app.dismiss(416) @@ -388,18 +389,18 @@ def action_quit(self) -> None: @on(Button.Pressed, "#process_submit_button") def handle_submit_button_pressed(self, event: Button.Pressed) -> None: """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") + # self.log(f"Submit button pressed, button: {event.button}") save_input_data:dict = {} for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") + # self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") save_input_data.update({self.parameter_names.get(input_widget.id, input_widget.id): input_widget.value}) - self.log(f"Save input data: {save_input_data}") + # self.log(f"Save input data: {save_input_data}") self.dismiss(["input", save_input_data, self.full_process]) @on(Input.Changed, "#technology_type_processes_input") def handle_input_changed(self, event: Input.Changed) -> None: """The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") + # self.log(f"Input changed, input: {event.input}") class TechnologyTypesScreen(ModalScreen): @@ -444,12 +445,12 @@ def compose(self) -> ComposeResult: """ Compose and display the technology type screen""" self.tech_type_tree.refresh() if self.tech_type_list: - self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") + # self.log(f"Technology types: {self.tech_type_list}, type: {type(self.tech_type_list)}") self.render_tech_type_hierarchy_to_tree(self.tech_type_list, self.tech_type_tree) else: self.tech_type_tree.root.add("No technology types found", expand=True) self.tech_type_tree.refresh() - self.log(f"Technology types tree: {self.tech_type_tree}") + # self.log(f"Technology types tree: {self.tech_type_tree}") yield Header(show_clock=True) yield ScrollableContainer( @@ -471,10 +472,10 @@ def action_go_back(self) -> None: @on(Tree.NodeSelected) def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: """ The user has selected a node in the tree. """ - self.log(f"Tree node selected, node: {event.node}") + # self.log(f"Tree node selected, node: {event.node}") self.selected_t_node = event.node self.selected_t_node_label = event.node.label - self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") + # self.log(f"Selected node: {self.selected_t_node}, label: {self.selected_t_node.label}") self.dismiss(str(self.selected_t_node.label)) @on(Tree.NodeCollapsed) @@ -577,33 +578,33 @@ async def on_mount(self) -> None: self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" if self.selected_t_option == "template": - self.log(f"Processing templates, with data: {self.selected_t_option_selected}") + # self.log(f"Processing templates, with data: {self.selected_t_option_selected}") # get selected template from the tech_type data - self.log(f"Technology Type Templates: {self.tech_type_templates}") + # self.log(f"Technology Type Templates: {self.tech_type_templates}") if isinstance(self.tech_type_templates, list): for template in self.tech_type_templates: - self.log(f"Template: {template.get('displayName')}, Selected: {self.selected_t_option_selected}") + # self.log(f"Template: {template.get('displayName')}, Selected: {self.selected_t_option_selected}") if self.selected_t_option_selected == template.get("displayName"): self.full_template = template self.selected_t_template = template - self.log(f"Selected Template: {self.selected_t_template}") + # self.log(f"Selected Template: {self.selected_t_template}") break else: continue - self.log(f"Selected Template: {self.selected_t_template}") + # self.log(f"Selected Template: {self.selected_t_template}") if self.selected_t_template is None: - self.log("No template selected, skipping placeholder display") + # self.log("No template selected, skipping placeholder display") return else: specification = self.selected_t_template.get("specification") if specification is None: - self.log("Selected template has no specification, skipping placeholder display") + # self.log("Selected template has no specification, skipping placeholder display") return else: placeholderProperties = specification.get("placeholderProperty") if placeholderProperties is None: - self.log("Selected template has no placeholder properties, skipping display") + # self.log("Selected template has no placeholder properties, skipping display") return else: for placeholder in placeholderProperties: @@ -627,18 +628,18 @@ async def on_mount(self) -> None: placeholder_text.styles.height = 8 placeholder_input = Input(id=f"{safe_name}_placeholder_input", placeholder="Enter value here") - self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") + # self.log(f"Placeholder: {placeholder_text.text}\n {placeholder_input}") # Mount the TextArea and the associated Input field into the ScrollableContainer try: load_point = self.query_one("#technology_type_templates_input") await load_point.mount(placeholder_text, before="#template_submit_button") await load_point.mount(placeholder_input, before="#template_submit_button") - self.log(f"Placeholder text area loaded: {placeholder_text.text}") - self.log(f"Placeholder input loaded: {placeholder_input}") + # self.log(f"Placeholder text area loaded: {placeholder_text.text}") + # self.log(f"Placeholder input loaded: {placeholder_input}") continue except Exception as e: - self.log(f"Error loading placeholder container: {e!s}") + # self.log(f"Error loading placeholder container: {e!s}") self.app.dismiss(416) def compose(self) -> ComposeResult: @@ -659,15 +660,15 @@ def action_quit(self) -> None: @on(Button.Pressed, "#template_submit_button") def handle_submit_button_pressed(self, event: Button.Pressed) -> None: """ The submit button has been pressed.""" - self.log(f"Submit button pressed, button: {event.button}") + # self.log(f"Submit button pressed, button: {event.button}") save_input_data: dict = {} for input_widget in self.query("Input"): - self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") + # self.log(f"Input widget: {input_widget.id}, value: {input_widget.value}") save_input_data.update({input_widget.id: input_widget.value}) - self.log(f"Save input data: {save_input_data}") + # self.log(f"Save input data: {save_input_data}") self.dismiss(["input", save_input_data, self.full_template]) @on(Input.Changed, "#technology_type_templates_input") def handle_input_changed(self, event: Input.Changed) -> None: """ The user has changed the input on the screen.""" - self.log(f"Input changed, input: {event.input}") + # self.log(f"Input changed, input: {event.input}") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py index 7888e651..ea905418 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py @@ -49,8 +49,8 @@ def on_mount(self) -> None: self.karma_points = getattr(self.app, "karma_points", 0) self.title = "My_Egeria User Identities Screen" self.sub_title = f"User Identities for {self.user_name}, Karma Points: {self.karma_points}" - self.log(f"UserIdentitiesScreen mounted for user: {self.user_name}, Karma Points: {self.karma_points}") - self.log(f"User identity input: {self.user_identities}") + # self.log(f"UserIdentitiesScreen mounted for user: {self.user_name}, Karma Points: {self.karma_points}") + # self.log(f"User identity input: {self.user_identities}") try: self.user_identity_datatable = self.query_one("#user_identity_datatable", DataTable) self.user_identity_datatable.clear(columns=False) @@ -73,7 +73,7 @@ def on_mount(self) -> None: ) if self.user_identities: for identity in self.user_identities: - self.log(f"User identity: {identity}") + # self.log(f"User identity: {identity}") display_name = identity.get('Display Name', 'Unknown') qualified_name = identity.get('Qualified Name', 'Unknown') category = identity.get('Category', 'Unknown') diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/bookmarks_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/bookmarks_handler.py index 34a1190c..7da6aee4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/bookmarks_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/bookmarks_handler.py @@ -95,7 +95,7 @@ def _get_or_create_bookmarks_collection(self, client: Egeria) -> str: } } guid = client.create_collection(body=body) - self.log(f"Created bookmarks collection {guid}") + # self.log(f"Created bookmarks collection {guid}") return guid def _bookmark_rows(self, client: Egeria, collection_guid: str) -> list[tuple[str, str, str]]: @@ -119,7 +119,7 @@ def list_my_bookmarks(self) -> list[tuple[str, str, str]] | None: return [] return self._bookmark_rows(client, collection_guid) except PyegeriaException as e: - self.log(f"Retrieving bookmarks failed: {e}") + # self.log(f"Retrieving bookmarks failed: {e}") self.notify(f"Could not retrieve your bookmarks: {e}", timeout=10, severity="error") return None finally: @@ -135,7 +135,8 @@ def show_my_bookmarks(self, target_guid: str | None = None) -> None: self.push_screen(MyBookMarksScreen(bookmarks, target_guid=target_guid), callback=self.my_bookmarks_callback) def my_bookmarks_callback(self, result: Any) -> None: - self.log(f"Bookmarks screen returned: {result}") + pass + # self.log(f"Bookmarks screen returned: {result}") def add_my_bookmark(self, target_guid: str) -> bool: """Bookmark the element with the given GUID. Returns True on success.""" @@ -150,7 +151,7 @@ def add_my_bookmark(self, target_guid: str) -> bool: self.notify(f"Bookmarked {target_guid}", timeout=10, severity="information") return True except (PyegeriaException, ValueError) as e: - self.log(f"Adding bookmark {target_guid} failed: {e}") + # self.log(f"Adding bookmark {target_guid} failed: {e}") self.notify(f"Adding the bookmark failed: {e}", timeout=10, severity="error") return False finally: @@ -175,7 +176,7 @@ def get_row_guid(self, table: Any, row_key: Any) -> str | None: # "No elements found" is returned, rather than an exception, when nothing matches return resolved if isinstance(resolved, str) and resolved and "No " not in resolved else None except PyegeriaException as e: - self.log(f"Looking up the GUID for {qualified_name} failed: {e}") + # self.log(f"Looking up the GUID for {qualified_name} failed: {e}") return None finally: if client: @@ -207,7 +208,7 @@ def delete_my_bookmark(self, target_guid: str) -> bool: self.notify(f"Removed bookmark {target_guid}", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Removing bookmark {target_guid} failed: {e}") + # self.log(f"Removing bookmark {target_guid} failed: {e}") self.notify(f"Removing the bookmark failed: {e}", timeout=10, severity="error") return False finally: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py index 3713e661..d8aa1dfb 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py @@ -84,7 +84,7 @@ def list_my_communities(self) -> list[tuple[str, str, str, str, str]] | None: client = self._community_client() profile = client.get_my_profile(output_format="JSON") except PyegeriaException as e: - self.log(f"Retrieving my communities failed: {e}") + # self.log(f"Retrieving my communities failed: {e}") self.notify(f"Could not retrieve your communities: {e}", timeout=10, severity="error") return None finally: @@ -130,7 +130,8 @@ def show_communities(self) -> None: self.push_screen(CommunitiesScreen(communities), callback=self.communities_callback) def communities_callback(self, result: Any) -> None: - self.log(f"Communities screen returned: {result}") + pass + # self.log(f"Communities screen returned: {result}") def search_communities(self, search_string: str) -> list[tuple[str, str, str, str]] | None: """Communities matching search_string ('*' for all) as (name, description, qualified name, GUID) rows, @@ -142,13 +143,13 @@ def search_communities(self, search_string: str) -> list[tuple[str, str, str, st found = client.find_communities(search_string=search_string or "*", ignore_case=True, output_format="JSON", graph_query_depth=0) except PyegeriaException as e: - self.log(f"Searching for communities matching {search_string} failed: {e}") + # self.log(f"Searching for communities matching {search_string} failed: {e}") self.notify(f"Searching for communities failed: {e}", timeout=10, severity="error") return None finally: if client: client.close_session() - self.log(f"Communities found for {search_string}: {found}") + # self.log(f"Communities found for {search_string}: {found}") rows = [] # "No elements found" is returned, rather than an exception, when nothing matches for community in found if isinstance(found, list) else []: @@ -168,13 +169,13 @@ def get_community_members(self, community_guid: str) -> list[tuple[str, str, str client = self._community_client() community = client.get_community_by_guid(community_guid, output_format="JSON", graph_query_depth=1) except PyegeriaException as e: - self.log(f"Retrieving members of community {community_guid} failed: {e}") + # self.log(f"Retrieving members of community {community_guid} failed: {e}") self.notify(f"Could not retrieve the community's members: {e}", timeout=10, severity="error") return None finally: if client: client.close_session() - self.log(f"Community {community_guid}: {community}") + # self.log(f"Community {community_guid}: {community}") rows = [] for link in (community.get("assignedActors") if isinstance(community, dict) else None) or []: actor = link.get("relatedElement") or {} @@ -198,7 +199,7 @@ def get_community_blog_posts(self, community_guid: str) -> list[tuple[str, str, graph_query_depth=0) posts += entries if isinstance(entries, list) else [] except PyegeriaException as e: - self.log(f"Retrieving the blog of community {community_guid} failed: {e}") + # self.log(f"Retrieving the blog of community {community_guid} failed: {e}") self.notify(f"Could not retrieve the community's blog: {e}", timeout=10, severity="error") return None finally: @@ -231,7 +232,7 @@ def _community_blog_log(self, client: Egeria, community_guid: str, community_nam } } log_guid = client.create_note_log(element_guid=community_guid, body=body) - self.log(f"Created blog for community {community_guid}: {log_guid}") + # self.log(f"Created blog for community {community_guid}: {log_guid}") return log_guid def add_community_blog_post(self, community_guid: str, community_name: str, title: str, text: str) -> bool: @@ -248,12 +249,12 @@ def add_community_blog_post(self, community_guid: str, community_name: str, titl "description": text, } post_guid = client.create_note(log_guid, body=properties) - self.log(f"Posted to the blog of community {community_guid}: {post_guid}") + # self.log(f"Posted to the blog of community {community_guid}: {post_guid}") self.notify(f"Your post '{title}' has been added to the {community_name} blog", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Posting to the blog of community {community_guid} failed: {e}") + # self.log(f"Posting to the blog of community {community_guid} failed: {e}") self.notify(f"Posting to the community's blog failed: {e}", timeout=10, severity="error") return False finally: @@ -268,7 +269,7 @@ def get_community_comments(self, community_guid: str) -> list[tuple[str, str, st client = self._community_client() comments = client.get_attached_comments(community_guid, graph_query_depth=0) except PyegeriaException as e: - self.log(f"Retrieving the comments on community {community_guid} failed: {e}") + # self.log(f"Retrieving the comments on community {community_guid} failed: {e}") self.notify(f"Could not retrieve the community's comments: {e}", timeout=10, severity="error") return None finally: @@ -291,11 +292,11 @@ def add_community_comment(self, community_guid: str, text: str, comment_type: st try: client = self._community_client() response = client.add_comment_to_element(community_guid, text, comment_type) - self.log(f"Commented on community {community_guid}: {response}") + # self.log(f"Commented on community {community_guid}: {response}") self.notify("Your comment has been added", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Commenting on community {community_guid} failed: {e}") + # self.log(f"Commenting on community {community_guid} failed: {e}") self.notify(f"Adding the comment failed: {e}", timeout=10, severity="error") return False finally: @@ -319,7 +320,7 @@ def join_community(self, community_guid: str, assignment_type: str = JOIN_ASSIGN severity="information") return True except PyegeriaException as e: - self.log(f"Joining community {community_guid} failed: {e}") + # self.log(f"Joining community {community_guid} failed: {e}") self.notify(f"Joining the community failed: {e}", timeout=10, severity="error") return False finally: @@ -336,7 +337,7 @@ def leave_community(self, community_guid: str) -> bool: self.notify("You have left the community", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Leaving community {community_guid} failed: {e}") + # self.log(f"Leaving community {community_guid} failed: {e}") self.notify(f"Leaving the community failed: {e}", timeout=10, severity="error") return False finally: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py index dca318de..cca06fa3 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py @@ -111,7 +111,7 @@ def _get_main_table(self, table_id: str) -> DataTable | None: main_screen = self.get_screen("main") return main_screen.query_one(f"#{table_id}", DataTable) except Exception as e: - self.log(f"Table #{table_id} not available on the main screen: {e}") + # self.log(f"Table #{table_id} not available on the main screen: {e}") return None def _write_back_rows(self, table_id: str, rows_with_keys: Any) -> None: @@ -146,8 +146,8 @@ def edit_profile_callback(self, return_c: Any) -> None: if return_c == 200: self._show_main_screen() else: - self.log(f"Error returned from EditProfileScreen: {return_c}") - self.log("Returning to main screen") + # self.log(f"Error returned from EditProfileScreen: {return_c}") + # self.log("Returning to main screen") self._show_main_screen() elif isinstance(return_c, str): if return_c == "identity": @@ -159,15 +159,16 @@ def edit_profile_callback(self, return_c: Any) -> None: elif return_c == "team": self.push_screen(EditTeamsScreen(), callback=self.edit_teams_callback) else: - self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") + # self.log(f"Unexpected return type from EditProfileScreen: {type(return_c)}") self._show_main_screen() def edit_tables(self, table_name: str, row_k: Any) -> None: """Push the edit screen for the selected table, populated with its live rows.""" - self.log(f"Editing table: {table_name}, row: {row_k}") + # self.log(f"Editing table: {table_name}, row: {row_k}") route = EDIT_TABLE_ROUTES.get(table_name) if route is None: self.log(f"Unexpected table name: {table_name}") + self.notify(f"Unexpected table name: {table_name}", severity="warning") return screen_cls, callback_name = route @@ -211,13 +212,13 @@ async def delete_element(self, source_table: str, guid: str) -> bool: Returns True only if Egeria accepted the delete, so the caller can leave the row on display when it didn't. """ - self.log(f"Deleting element {guid} on behalf of table {source_table}") + # self.log(f"Deleting element {guid} on behalf of table {source_table}") if not guid: return False route = DELETE_METHODS.get(source_table) if route is None: - self.log(f"No delete is defined for table: {source_table}") + # self.log(f"No delete is defined for table: {source_table}") self.notify(f"Deleting rows from {source_table} isn't supported.", timeout=6, severity="warning") return False @@ -228,10 +229,10 @@ async def delete_element(self, source_table: str, guid: str) -> bool: try: client.create_egeria_bearer_token(self.user_name, self.user_password) getattr(client, method_name)(guid, **kwargs) - self.log(f"{method_name}({guid}) succeeded") + # self.log(f"{method_name}({guid}) succeeded") return True except PyegeriaException as e: - self.log(f"{method_name}({guid}) failed: {e}") + # self.log(f"{method_name}({guid}) failed: {e}") self.notify(f"Delete failed: {e}", timeout=10, severity="error") return False finally: @@ -244,7 +245,7 @@ def show_comments(self, table_name: str | None = None, row_k: Any = None, When selected_comment_guid is given, the screen shows the comments attached to that comment (its responses) and Ctrl+N adds a new response to it. """ - self.log(f"Showing comments for table: {table_name}, row: {row_k}, comment: {selected_comment_guid}") + # self.log(f"Showing comments for table: {table_name}, row: {row_k}, comment: {selected_comment_guid}") self.push_screen( ShowCommentsScreen( table_name=table_name, @@ -256,9 +257,9 @@ def show_comments(self, table_name: str | None = None, row_k: Any = None, def show_comments_callback(self, return_c: Any) -> None: """Callback for ShowCommentsScreen.""" - self.log(f"Return from ShowCommentsScreen: {return_c}") + # self.log(f"Return from ShowCommentsScreen: {return_c}") if return_c is None: - self.log("No return from ShowCommentsScreen") + # self.log("No return from ShowCommentsScreen") self._show_main_screen() elif isinstance(return_c, list) and len(return_c) == 2 and return_c[0] == 250: # User asked to respond to a comment: reopen the screen focused on that comment @@ -266,18 +267,19 @@ def show_comments_callback(self, return_c: Any) -> None: elif return_c == 200: self._show_main_screen() else: - self.log(f"Unexpected return type from ShowCommentsScreen: {type(return_c)}") + # self.log(f"Unexpected return type from ShowCommentsScreen: {type(return_c)}") self._show_main_screen() async def add_to_tables(self, selected_table: str, selected_row: Any) -> None: """Called from main screen when user selects to add a row to a table.""" - self.log(f"Adding row {selected_row} to table {selected_table}") + # self.log(f"Adding row {selected_row} to table {selected_table}") self.selected_table = selected_table self.selected_row = selected_row route = ADD_TABLE_ROUTES.get(self.selected_table) if route is None: self.log(f"Unexpected table name: {self.selected_table}") + self.notify(f"Unexpected table name: {self.selected_table}", severity="warning") return screen_cls, callback_name = route @@ -290,13 +292,13 @@ def _after_add(self, kind: str, result: Any) -> None: Popping back to main (rather than returning to the edit screen the Add screen was opened from) also drops that edit screen's now-stale copy of the rows. """ - self.log(f"Add {kind} screen closed with: {result}") + # self.log(f"Add {kind} screen closed with: {result}") self._show_main_screen() self.call_later(self.action_refresh) def add_association_callback(self, result: Any) -> None: """Called from AddAssociationScreen with the kind of association to add.""" - self.log(f"Add association chose: {result}") + # self.log(f"Add association chose: {result}") if result == "project": self.push_screen(AddProjectScreen("projects_table", self.user_GUID), callback=self.add_project_callback) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py index c44688b6..c8ca6263 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py @@ -55,7 +55,7 @@ async def action_feedback(self) -> None: async def feedback_callback(self, feedback: Any) -> None: """Record the feedback returned from the FeedbackScreen in the feedback log.""" if not isinstance(feedback, dict): - self.log("Feedback cancelled") + # self.log("Feedback cancelled") return client = None try: @@ -66,10 +66,10 @@ async def feedback_callback(self, feedback: Any) -> None: timeout=10, severity="error") return note_guid = await client._async_create_note(log_guid, body=self._build_feedback_note(log_guid, feedback)) - self.log(f"Feedback recorded as note: {note_guid}") + # self.log(f"Feedback recorded as note: {note_guid}") self.notify("Thank you, your feedback has been recorded") except PyegeriaException as e: - self.log(f"Failed to record feedback: {e}") + # self.log(f"Failed to record feedback: {e}") self.notify(f"Failed to record feedback: {e}", timeout=10, severity="error") finally: if client: @@ -121,7 +121,7 @@ async def _get_or_create_feedback_log(self, client: Egeria) -> str | None: else: body = {"class": "NewElementRequestBody", "properties": properties} log_guid = await client._async_create_note_log(element_guid=owner_guid, body=body) - self.log(f"Created feedback log: {log_guid}") + # self.log(f"Created feedback log: {log_guid}") return log_guid async def action_view_feedback_log(self) -> None: @@ -136,7 +136,7 @@ async def action_view_feedback_log(self) -> None: client = self._feedback_client() entries = await self.get_feedback_entries(client) except PyegeriaException as e: - self.log(f"Failed to retrieve feedback log: {e}") + # self.log(f"Failed to retrieve feedback log: {e}") self.notify(f"Failed to retrieve feedback log: {e}", timeout=10, severity="error") return finally: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py index 6d0f4838..4f0d11e1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py @@ -166,17 +166,17 @@ def __init__(self, *args, **kwargs): self.description = "Display the user related items for the current user." load_app_config() app_config = settings.Environment - self.log(f"Application Config: {app_config}") + # self.log(f"Application Config: {app_config}") app_user = settings.User_Profile - self.log(f"User Profile: {app_user}") + # self.log(f"User Profile: {app_user}") self.user_name = app_user.user_name or "garygeeke" self.user_password = app_user.user_pwd or "secret" self.view_server = app_config.egeria_view_server or "qs-view-server" self.platform_url = app_config.egeria_platform_url or "https://127.0.0.1:9443" - self.log(f"Platform URL: {self.platform_url}") - self.log(f"View Server: {self.view_server}") - self.log(f"User: {self.user_name}") - self.log(f"User PWD: {self.user_password}") + # self.log(f"Platform URL: {self.platform_url}") + # self.log(f"View Server: {self.view_server}") + # self.log(f"User: {self.user_name}") + # self.log(f"User PWD: {self.user_password}") # Ensure compose() is safe before data loads self.actor_profile: dict = {} @@ -248,13 +248,13 @@ async def mainline(self, splash_return: Any = None) -> None: except asyncio.CancelledError: return except Exception as e: - self.log(f"Error in parallel initial load: {e}") + # self.log(f"Error in parallel initial load: {e}") profile_found = False await self._after_profile_load(profile_found) async def change_user(self, user_name: str, user_password: str) -> None: """Switch the app to new (already validated) credentials and reload the profile for that user.""" - self.log(f"Changing user from {self.user_name} to {user_name}") + # self.log(f"Changing user from {self.user_name} to {user_name}") self.user_name = user_name self.user_password = user_password # Screens read the user from pyegeria's (cached) settings, so record the @@ -279,7 +279,7 @@ async def change_user_callback(self, result: Any) -> None: async def connection_error_callback(self, result: int) -> None: """Return from the connection error screen: 200 Egeria is back, 201 change user, otherwise quit.""" - self.log(f"Connection error screen returned: {result}") + # self.log(f"Connection error screen returned: {result}") if result == 200: await self._load_or_create_profile() elif result == 201: @@ -302,13 +302,13 @@ async def _load_profile_and_populate(self) -> bool: self.my_profile_raw, "My", "MyProfile", "DICT", "My-User-MD") if isinstance(self.my_profile_raw, dict) else self.my_profile_raw ) - self.log(f"retrieve profile result: {self.my_profile_data}") + # self.log(f"retrieve profile result: {self.my_profile_data}") except PyegeriaConnectionException as e: - self.log(f"Unable to connect to Egeria: {e!s}") + # self.log(f"Unable to connect to Egeria: {e!s}") self.load_error = f"Unable to connect to the Egeria platform at {self.platform_url}." return False except PyegeriaException as e: - self.log(f"Error retrieving profile: {e!s}") + # self.log(f"Error retrieving profile: {e!s}") print_basic_exception(e) # str(e) is often empty (e.g. on a 401), so ask Egeria directly what is wrong reason = (check_egeria_access(self.view_server, self.platform_url, self.user_name, self.user_password) @@ -317,7 +317,7 @@ async def _load_profile_and_populate(self) -> bool: return False if not self.my_profile_data: - self.log("No profile found for user.") + # self.log("No profile found for user.") return False self._process_profile_data(self.my_profile_data) @@ -332,11 +332,11 @@ async def _load_or_create_profile(self) -> None: async def _after_profile_load(self, profile_found: bool) -> None: """Wait for Egeria if the load could not reach it; otherwise offer to create a missing profile.""" if self.load_error: - self.log(f"Profile load failed: {self.load_error}") + # self.log(f"Profile load failed: {self.load_error}") await self.push_screen(ConnectionErrorScreen(self.load_error), callback=self.connection_error_callback) return if not profile_found and (not hasattr(self, "my_profile_data") or not self.my_profile_data): - self.log("No profile found. Prompting to create one...") + # self.log("No profile found. Prompting to create one...") await self.push_screen( CreateProfileScreen( user=self.user_name, @@ -368,7 +368,7 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: self.my_communities_data = self.user_profile.get("Communities") or [] self.my_roles_data = self.user_profile.get("Roles") or [] self.my_note_logs = self.user_profile.get("Note Logs") or [] - self.log(f"my_note_logs: {self.my_note_logs}, type: {type(self.my_note_logs)}") + # self.log(f"my_note_logs: {self.my_note_logs}, type: {type(self.my_note_logs)}") for entry in self.my_note_logs: if entry.get("class") == "BlogEntryProperties": self.my_blogs_data.append(entry) @@ -376,14 +376,14 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: self.my_journal_data.append(entry) # are todos part of note logs? - self.log(f"Contribution Record: {self.contribution_record}") - self.log(f"Karma Points: {self.karma_points}") - self.log(f"my_projects_data: {self.my_projects_data}") - self.log(f"my_teams_data: {self.my_teams_data}") - self.log(f"my_communities_data: {self.my_communities_data}") - self.log(f"my_roles_data: {self.my_roles_data}") - self.log(f"my_blogs_data: {self.my_blogs_data}") - self.log(f"my_journal_data: {self.my_journal_data}") + # self.log(f"Contribution Record: {self.contribution_record}") + # self.log(f"Karma Points: {self.karma_points}") + # self.log(f"my_projects_data: {self.my_projects_data}") + # self.log(f"my_teams_data: {self.my_teams_data}") + # self.log(f"my_communities_data: {self.my_communities_data}") + # self.log(f"my_roles_data: {self.my_roles_data}") + # self.log(f"my_blogs_data: {self.my_blogs_data}") + # self.log(f"my_journal_data: {self.my_journal_data}") # User Identities try: @@ -393,9 +393,9 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: if isinstance(raw, dict) else self.my_profile_inst.get_my_profile(report_spec="User-Identities", output_format="DICT") ) - self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") + # self.log(f"User-Identities: {self.user_identities}, type: {type(self.user_identities)}") except PyegeriaException as e: - self.log(f"Error retrieving User-Identities: {e!s}") + # self.log(f"Error retrieving User-Identities: {e!s}") self.user_identities = {} # User To-Dos @@ -405,12 +405,12 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: graph_query_depth=0, # the to-do table shows properties only output_format="DICT", ) - self.log(f"My To-Dos: {self.my_todos_data}, type: {type(self.my_todos_data)}") + # self.log(f"My To-Dos: {self.my_todos_data}, type: {type(self.my_todos_data)}") except PyegeriaException as e: - self.log(f"Error retrieving My To-Dos: {e!s}") + # self.log(f"Error retrieving My To-Dos: {e!s}") self.my_todos_data = {} - self.log(f"my_todos_data: {self.my_todos_data}") + # self.log(f"my_todos_data: {self.my_todos_data}") self.my_collections = self._load_my_collections() @@ -431,14 +431,15 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: ) except PyegeriaException as e: print_basic_exception(e) - self.log(f"Error retrieving actor profile: {e!s}") + # self.log(f"Error retrieving actor profile: {e!s}") actor = None data = actor.get("data") if isinstance(actor, dict) else None if data: self.user_GUID = data[0].get("GUID") or "" else: - self.log("Actor-Profiles lookup returned no data; user_GUID left unset.") - self.log(f"User GUID retrieved: {self.user_GUID!r}") + pass + # self.log("Actor-Profiles lookup returned no data; user_GUID left unset.") + # self.log(f"User GUID retrieved: {self.user_GUID!r}") # Normalize expected keys self.full_name = self.user_profile.get("Full Name") or "" @@ -450,9 +451,9 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: self.journal = self.my_journal_data or [] self.todos = self.my_todos_data or [] self.teams = self.my_teams_data or [] - self.log(f"Blogs data: {self.blogs}") - self.log(f"Journal data: {self.journal}") - self.log(f"Todos data: {self.todos}") + # self.log(f"Blogs data: {self.blogs}") + # self.log(f"Journal data: {self.journal}") + # self.log(f"Todos data: {self.todos}") if isinstance(self.user_identities, list): self.user_identity = self.user_identities else: @@ -468,7 +469,7 @@ def _load_my_collections(self) -> list[dict[str, str]]: # Depth 0: only header/properties are summarised (~70s at the default depth 3 vs <1s). response = eclient.find_collections(search_string="*", output_format="JSON", graph_query_depth=0) except PyegeriaException as e: - self.log(f"Error retrieving My Collections: {e!s}") + # self.log(f"Error retrieving My Collections: {e!s}") return [] finally: if eclient: @@ -481,9 +482,9 @@ def _load_my_collections(self) -> list[dict[str, str]]: def new_profile_return(self, result: int) -> None: """This function handles either the return from the create new profile screen or when the user already has a profile continue processing.""" - self.log(f"Profile creation result: {result}") + # self.log(f"Profile creation result: {result}") if not result or result != 200: - self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") + # self.log(f"Profile creation cancelled/failed; return: {result}, exiting.") self.exit(403) return @@ -495,15 +496,15 @@ def new_profile_return(self, result: int) -> None: output_format="DICT", report_spec="My-User-MD", ) - self.log(f"Profile retrieved successfully: {self.user_profile_struct}") + # self.log(f"Profile retrieved successfully: {self.user_profile_struct}") self.show_main_screen() except PyegeriaException as e2: - self.log(f"Error retrieving user profile: {e2!s}") + # self.log(f"Error retrieving user profile: {e2!s}") self.exit(412) return if not self.user_profile_struct or self.user_profile_struct == []: - self.log("Error retrieving user profile. Exiting.") + # self.log("Error retrieving user profile. Exiting.") self.exit(413) return @@ -686,16 +687,16 @@ def action_quit(self) -> Any: self.exit(200) async def action_refresh(self) -> None: - self.log("Refreshing data...") + # self.log("Refreshing data...") await self._load_or_create_profile() await self._populate_tables() - self.log("Data refresh completed.") + # self.log("Data refresh completed.") @on(OptionList.OptionSelected, "#other_function_list") async def handle_option_selected(self, event: OptionList.OptionSelected) -> None: selected_option = event.option.prompt.strip("[] ") selected_option_id = event.option.id - self.log(f"Selected option: {selected_option} ({selected_option_id})") + # self.log(f"Selected option: {selected_option} ({selected_option_id})") if selected_option == "Technology Types": await self.handle_technology_types_option() elif selected_option == "User Identities": @@ -734,7 +735,7 @@ async def handle_option_selected(self, event: OptionList.OptionSelected) -> None def status_callback(self, status_callback_rc: Any) -> None: """Callback routine from the status screen: the user has read the message, so carry on.""" - self.log(f"Status screen returned: {status_callback_rc}") + # self.log(f"Status screen returned: {status_callback_rc}") self.show_main_screen() @on(DataTable.RowSelected, "#roles_table") @@ -748,25 +749,27 @@ def _on_roles_row_selected(self, event: DataTable.RowSelected) -> None: def show_main_screen(self) -> None: """Show or switch back to the main screen by unwinding the screen stack.""" - self.log("Returning to main screen") + # self.log("Returning to main screen") try: while len(getattr(self, "screen_stack", [])) > 1 and not isinstance(getattr(self, "screen", None), MainScreen): self.pop_screen() except Exception as e: - self.log(f"Error popping screens to return to main screen: {e}") + pass + # self.log(f"Error popping screens to return to main screen: {e}") try: if getattr(self, "is_mounted", False) and not isinstance(getattr(self, "screen", None), MainScreen): self.push_screen("main") except Exception as e: - self.log(f"Error ensuring main screen: {e}") + pass + # self.log(f"Error ensuring main screen: {e}") # Alias for handlers calling _show_main_screen _show_main_screen = show_main_screen def view_subscriptions_callback(self, subscriptions_callback_rc: Any) -> None: """Callback routine from the view subscriptions screen.""" - self.log(f"View subscriptions screen returned: {subscriptions_callback_rc}") + # self.log(f"View subscriptions screen returned: {subscriptions_callback_rc}") self.show_main_screen() def get_data_product_catalog_table(self) -> int: @@ -797,13 +800,13 @@ def get_data_product_catalog_table(self) -> int: user_pass=self.user_password, ) except PyegeriaException as e: - self.log(f"Error retrieving digital product catalog details: {e!s}") + # self.log(f"Error retrieving digital product catalog details: {e!s}") return 421 - self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") + # self.log(f"Digital Product Catalog data returned: {self.digital_product_catalog_data}") self.digital_product_catalog_data_extract = self.digital_product_catalog_data.get("data") or [] - self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") + # self.log(f"Digital Product Catalog data extracted: {self.digital_product_catalog_data_extract}") if not self.digital_product_catalog_data_extract: - self.log(f"No digital product catalog data found for user: {self.user_name}") + # self.log(f"No digital product catalog data found for user: {self.user_name}") self.digital_product_catalog_table.add_row("No digital product catalogs found", "No data returned from Egeria", "") if hasattr(self, "notify"): @@ -831,35 +834,36 @@ def add_comment(self, table_name, table_row): # find the column containing the GUID for key, column in table.columns.items(): if column.label.plain == target_heading: - self.log(f"GUID column found: {column.label.plain}") + # self.log(f"GUID column found: {column.label.plain}") column_key = key break if column_key: # If GUID is found, use the row and column to get the selected row's GUID - self.log(f"GUID found in table: row: {table_row}, column: {column_key}") + pass + # self.log(f"GUID found in table: row: {table_row}, column: {column_key}") else: # No GUID in table so look for Qualified Name to use instead target_heading = "Qualified Name" column_key = None for key, column in table.columns.items(): if column.label.plain == target_heading: - self.log(f"Qualified Name column found: {column.label.plain}") + # self.log(f"Qualified Name column found: {column.label.plain}") column_key = key break if column_key: - self.log (f"Row_key is : {self.table_row}") - self.log(f"Column key is: {column_key}") + # self.log (f"Row_key is : {self.table_row}") + # self.log(f"Column key is: {column_key}") row_id = table.get_cell(self.table_row, column_key) - self.log(f"Row ID is: {row_id}") + # self.log(f"Row ID is: {row_id}") else: self.notify(f"Error retrieving row from table to add comment, no GUID or Qualified Name column found", severity="error", timeout=30) - self.log(f"Column_key is: {column_key} not valid to access Egeria") + # self.log(f"Column_key is: {column_key} not valid to access Egeria") return (400) if row_id == "": - self.log(f"Row ID is: {row_id}, not valid to access Egeria") + # self.log(f"Row ID is: {row_id}, not valid to access Egeria") return (400) except PyegeriaException as e: @@ -873,7 +877,7 @@ def add_comment(self, table_name, table_row): def add_comment_callback(self, comment_content): """ Process feedback from thew add comment functions and then return to main screen""" # If the return is an integer, then either success or failure, return to main screen - self.log(f"Return from Add Comment Screen: {comment_content}") + # self.log(f"Return from Add Comment Screen: {comment_content}") # An integer return is cancel/quit (200) or no element selected (400); the # main screen is already showing once AddCommentScreen has dismissed if not isinstance(comment_content, (list, tuple)): @@ -881,7 +885,7 @@ def add_comment_callback(self, comment_content): # Check that all required data has been returned (Comment, Type, and the GUID of the element the comment applies to) if len(comment_content) != 3: self.notify("Return from Add Comment Screen incomplete, please retry", severity="error", timeout=30) - self.log(f"Return from Add Comment Screen: {comment_content}, length: {len(comment_content)}") + # self.log(f"Return from Add Comment Screen: {comment_content}, length: {len(comment_content)}") return # We have 3 return data items so extract them to individual variables self.comment = comment_content[0] @@ -890,7 +894,7 @@ def add_comment_callback(self, comment_content): # Make sure we really have all the required data if not self.comment or not self.type or not self.element_guid: self.notify("A row must be selected and Both comment text and comment type are required!") - self.log(f"One or more element missing: {comment_content}, length: {len(comment_content)}") + # self.log(f"One or more element missing: {comment_content}, length: {len(comment_content)}") return # We have all the required data, then try to create the Egeria client eclient = None @@ -901,7 +905,7 @@ def add_comment_callback(self, comment_content): self.user_password) eclient.create_egeria_bearer_token(self.user_name, self.user_password) except PyegeriaException as e: - self.log(f"Error creating Egeria client: {e}") + # self.log(f"Error creating Egeria client: {e}") self.notify( "Unable to connect to the Egeria Server, please check the server is running and your credentials and try again", timeout=10, @@ -911,16 +915,16 @@ def add_comment_callback(self, comment_content): try: # Add comment to selected element self.comment_type = self.type.upper() - self.log(f"Adding comment to element: {self.element_guid}, values: {self.comment}, {self.comment_type}") + # self.log(f"Adding comment to element: {self.element_guid}, values: {self.comment}, {self.comment_type}") response = eclient.add_comment_to_element( element_guid=self.element_guid, comment=self.comment, comment_type=self.comment_type ) - self.log(f"Comment successfully added! New Comment GUID: {response}") + # self.log(f"Comment successfully added! New Comment GUID: {response}") self.notify(f"Comment successfully added! New Comment GUID: {response}") except Exception as e: - self.log(f"Failed to add comment: {e}") + # self.log(f"Failed to add comment: {e}") self.notify(f"Failed to add comment: {e} \n Please try again.", severity="error") return finally: @@ -950,7 +954,7 @@ async def get_guid_for_qualified_name(self, qname: str) -> str: ) token = eclient.create_egeria_bearer_token(self.user_name, self.user_password) guid = eclient.get_guid_for_name(name=qname) - self.log(f"GUID: {guid} returned for qualified name: {qname}") + # self.log(f"GUID: {guid} returned for qualified name: {qname}") eclient.close_session() return guid diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py index aa1a27d8..e851d589 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/network_handler.py @@ -52,13 +52,13 @@ def list_my_peers(self) -> list[tuple[str, str, str, str, str, str]] | None: relationship_type=PEER_RELATIONSHIP, body=body) except PyegeriaException as e: - self.log(f"Retrieving peers failed: {e}") + # self.log(f"Retrieving peers failed: {e}") self.notify(f"Could not retrieve your network: {e}", timeout=10, severity="error") return None finally: if client: client.close_session() - self.log(f"Peers: {peers}") + # self.log(f"Peers: {peers}") rows = [] # "No elements found" is returned, rather than an exception, when the user has no peers for peer in peers if isinstance(peers, list) else []: @@ -90,7 +90,8 @@ def show_my_network(self) -> None: self.push_screen(MyNetworkScreen(peers), callback=self.my_network_callback) def my_network_callback(self, result: Any) -> None: - self.log(f"My Network screen returned: {result}") + pass + # self.log(f"My Network screen returned: {result}") def search_people(self, search_string: str) -> list[tuple[str, str, str]] | None: """People whose profile matches search_string, as (display name, GUID, qualified name) rows, @@ -102,13 +103,13 @@ def search_people(self, search_string: str) -> list[tuple[str, str, str]] | None output_format="DICT", report_spec="User-Identities", query_depth=0, page_size=10) except PyegeriaException as e: - self.log(f"Searching for {search_string} failed: {e}") + # self.log(f"Searching for {search_string} failed: {e}") self.notify(f"Searching for people failed: {e}", timeout=10, severity="error") return None finally: if client: client.close_session() - self.log(f"People found for {search_string}: {found}") + # self.log(f"People found for {search_string}: {found}") if isinstance(found, dict): found = [found] rows = [] @@ -138,7 +139,7 @@ def link_peer(self, peer_guid: str, nickname: str = "", description: str = "") - self.notify("Added to your network", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Linking peer {peer_guid} failed: {e}") + # self.log(f"Linking peer {peer_guid} failed: {e}") self.notify(f"Adding to your network failed: {e}", timeout=10, severity="error") return False finally: @@ -155,7 +156,7 @@ def unlink_peer(self, peer_guid: str) -> bool: self.notify("Removed from your network", timeout=10, severity="information") return True except PyegeriaException as e: - self.log(f"Unlinking peer {peer_guid} failed: {e}") + # self.log(f"Unlinking peer {peer_guid} failed: {e}") self.notify(f"Removing from your network failed: {e}", timeout=10, severity="error") return False finally: @@ -171,13 +172,13 @@ def get_peer_details(self, peer_guid: str) -> tuple[list[dict], list[dict]] | No # My-User's detail specs are My-User-Contact-Detail / My-User-Communities-Detail details = client._generate_my_profile_output(profile, output_format="DICT", report_spec="My-User") except PyegeriaException as e: - self.log(f"Retrieving details for peer {peer_guid} failed: {e}") + # self.log(f"Retrieving details for peer {peer_guid} failed: {e}") self.notify(f"Could not retrieve the details: {e}", timeout=10, severity="error") return None finally: if client: client.close_session() - self.log(f"Peer details: {details}") + # self.log(f"Peer details: {details}") if isinstance(details, list) and details: details = details[0] if not isinstance(details, dict): diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py index e9d3fdc3..048af837 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py @@ -54,7 +54,7 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: selected_role_name = selected_row_data[0] selected_role_type = selected_row_data[1] selected_role_guid = selected_row_data[3] - self.log(f"Selected role: {selected_row_data}") + # self.log(f"Selected role: {selected_row_data}") team_members_list: list = [] self.team_members = [] @@ -65,7 +65,7 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: or "TeamMember" in selected_role_name or "TeamMember" in selected_role_type ): - self.log(f"Selected role is a TeamLeader or TeamMember: {selected_role_name}") + # self.log(f"Selected role is a TeamLeader or TeamMember: {selected_role_name}") result = self.find_team_members(selected_role_name) if not isinstance(result, (tuple, list)) or len(result) != 5: return @@ -74,7 +74,7 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: # find_team_members has already told the user why there is no team to show return else: - self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") + # self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") self.notify( f"'{selected_role_name}' is not a team role, so there are no team members to show. " "Select a TeamLeader or TeamMember role.", @@ -83,33 +83,33 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: ) return 201 - self.log(f"team_members_list: {team_members_list}") + # self.log(f"team_members_list: {team_members_list}") # Process team data for display on screen team_properties: list = [] team_properties.append(team_display_name) team_properties.append(team_qualified_name) team_properties.append(team_category) team_properties.append(team_description) - self.log(f"team_properties: {team_properties}") + # self.log(f"team_properties: {team_properties}") # Process team member data for display on screen for team_member in team_members_list: if isinstance(team_member, list) and len(team_member) > 0 and "Selection Error" in str(team_member[0]): - self.log(f"Team member details contain 'Selection Error': {team_member}") + # self.log(f"Team member details contain 'Selection Error': {team_member}") self.team_members.append(team_member) break elif isinstance(team_member, dict): - self.log(f"Processing team member properties: {team_member}") + # self.log(f"Processing team member properties: {team_member}") team_member_properties = [] team_member_properties.append(team_member.get("Individual")) team_member_properties.append(team_member.get("Assignment Type")) team_member_properties.append(team_member.get("Individual GUID")) - self.log(f"team_member_properties: {team_member_properties}") + # self.log(f"team_member_properties: {team_member_properties}") self.team_members.append(team_member_properties) - self.log(f"team_members: {self.team_members}") + # self.log(f"team_members: {self.team_members}") - self.log(f"team_members: {self.team_members}") - self.log(f"team_properties: {team_properties}") - self.log(f"User name: {self.user_name}") + # self.log(f"team_members: {self.team_members}") + # self.log(f"team_properties: {team_properties}") + # self.log(f"User name: {self.user_name}") self.push_screen( MyTeam(my_team=self.team_members, my_team_properties=team_properties, leader_name=self.user_name), callback=self.my_team_callback, @@ -126,7 +126,7 @@ def find_team_members(self, role_name: str) -> list[Any]: search_key_parts = selected_role_name.split("::") role_search_key = "::".join(search_key_parts[1:]) - self.log(f"Role search key: {role_search_key}") + # self.log(f"Role search key: {role_search_key}") try: team_member_data: dict = exec_report_spec( format_set_name="Team-Members", @@ -139,19 +139,19 @@ def find_team_members(self, role_name: str) -> list[Any]: ) except PyegeriaException as e: print_basic_exception(e) - self.log(f"Error retrieving team members: {e!s}") + # self.log(f"Error retrieving team members: {e!s}") self.notify(f"Could not retrieve the team members from Egeria: {e!s}", timeout=20, severity="error") return [[], None, None, None, None] - self.log(f"team_member_data: {team_member_data}") + # self.log(f"team_member_data: {team_member_data}") if team_member_data.get("kind") != "empty": team_members_data_struct = team_member_data.get("data") or [] - self.log(f"team members data extracted: {team_members_data_struct}") + # self.log(f"team members data extracted: {team_members_data_struct}") else: - self.log(f"No team members found for role: {role_search_key}") + # self.log(f"No team members found for role: {role_search_key}") error_category = "Team Members" error_message = "No team members found" - self.log(f"Error retrieving team members: {error_category}, {error_message}") + # self.log(f"Error retrieving team members: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return [[], None, None, None, None] @@ -174,34 +174,36 @@ def find_team_members(self, role_name: str) -> list[Any]: if not team_members_list and not team_display_name: error_category = "Team Member Details" error_message = "No team member details found" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") + # self.log(f"Error retrieving team member details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return [[], None, None, None, None] return [team_members_list, team_display_name, team_qualified_name, team_category, team_description] def my_team_callback(self, status: Any) -> None: - self.log(f"Callback received with status: {status}") + # self.log(f"Callback received with status: {status}") if status == 200: - self.log("MyTeam screen completed successfully") + pass + # self.log("MyTeam screen completed successfully") else: - self.log(f"Error in MyTeam screen: {status}") + pass + # self.log(f"Error in MyTeam screen: {status}") self._show_main_screen() def search_for_term_callback(self, status: Any) -> None: - self.log(f"Callback received with status: {status}") + # self.log(f"Callback received with status: {status}") if status == 200: - self.log("Search for term screen completed successfully") + # self.log("Search for term screen completed successfully") self._show_main_screen() elif status == 201: - self.log("Search for term: back to Shop for Data") + # self.log("Search for term: back to Shop for Data") # Reopen through the handler so the catalog tables are reloaded, not left empty self.call_later(self.handle_shop_for_data_option) else: - self.log(f"Error in Search for term screen: {status}") + # self.log(f"Error in Search for term screen: {status}") error_category = "Search for Term" error_message = "Error retrieving Term details" - self.log(f"Error retrieving team member details: {error_category}, {error_message}") + # self.log(f"Error retrieving team member details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) def display_glossary_term_details(self, term: str) -> int: @@ -220,22 +222,22 @@ def display_glossary_term_details(self, term: str) -> int: ) except PyegeriaException as e: print_basic_exception(e) - self.log(f"Error retrieving term details: {e!s}") + # self.log(f"Error retrieving term details: {e!s}") self.exit(440) return 440 - self.log(f"term_details: {self.term_details}") + # self.log(f"term_details: {self.term_details}") if not self.term_details: error_category = "Glossary Term Details" error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") + # self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return 440 elif self.term_details.get("kind") == "empty": - self.log(f"No glossary term details found for qualified name: {self.target_term}") + # self.log(f"No glossary term details found for qualified name: {self.target_term}") error_category = "Glossary Term Details" error_message = "No glossary term details found" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") + # self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return 440 @@ -244,7 +246,7 @@ def display_glossary_term_details(self, term: str) -> int: if not self.term_details_data: error_category = "Glossary Term Details" error_message = "No glossary term details found or the data dict entry is missing" - self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") + # self.log(f"Error retrieving glossary term details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return 440 else: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py index 0b2284f4..61242701 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py @@ -36,19 +36,19 @@ class TechTypesMixin: async def handle_technology_types_option(self) -> None: """Fetch and display technology types screen.""" - self.log("Fetching technology types...") + # self.log("Fetching technology types...") await self.fetch_technology_types() - self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") + # self.log(f"Tech Type Response: {self.tech_type_response} | {self.tech_type_list}") if self.tech_type_response == []: - self.log("No technology types found.") + # self.log("No technology types found.") self.notify("No technology types are defined in Egeria", timeout=10, severity="warning") return elif len(self.tech_type_response) == 3 and int(self.tech_type_response) >= 400: - self.log("Error fetching technology types.") + # self.log("Error fetching technology types.") self.exit(int(self.tech_type_response)) return - self.log("Technology types fetched successfully.") - self.log("Displaying technology types...") + # self.log("Technology types fetched successfully.") + # self.log("Displaying technology types...") await self.push_screen( TechnologyTypesScreen( ttlist=self.tech_type_list, @@ -56,7 +56,7 @@ async def handle_technology_types_option(self) -> None: ), callback=self.tech_type_callback, ) - self.log("Technology types displayed successfully.") + # self.log("Technology types displayed successfully.") async def tech_type_callback(self, result: Any) -> int: """Callback for Technology Types screen. @@ -73,17 +73,17 @@ async def tech_type_callback(self, result: Any) -> int: self.tech_type_processes = [] # check that we got a valid result from the screen and process accordingly if not self.result or isinstance(self.result, int): - self.log(f"Technology Types screen cancelled or failed; return: {result}.") + # self.log(f"Technology Types screen cancelled or failed; return: {result}.") self._show_main_screen() return self.result # The screen dismisses quit/go back as the strings "200"/"201", not ints if str(self.result) in ("200", "201"): - self.log(f"Technology Types Screen returned on a quit/go back directive: {self.result}") + # self.log(f"Technology Types Screen returned on a quit/go back directive: {self.result}") self._show_main_screen() return self.result self.selected_t_node = str(result) - self.log(f"Technology Types screen returned: {self.selected_t_node}") + # self.log(f"Technology Types screen returned: {self.selected_t_node}") # Request details for selected tech type try: p_client = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) @@ -96,11 +96,11 @@ async def tech_type_callback(self, result: Any) -> int: print_basic_exception(e) error_category = "Technology Type Process Detail Retrieval" error_message = f"Failed to retrieve technology type process details for {getattr(self, 'selected_q_name', self.selected_t_node)}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") + # self.log(f"Error retrieving technology type details: {error_category}, {error_message}") await self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return 417 - self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") + # self.log(f"Technology Type Data: {self.tech_type_data}, type: {type(self.tech_type_data)}") # Egeria returns a plain string such as "No elements found" when there is no detail if not isinstance(self.tech_type_data, dict): self.notify(f"No details found for technology type '{self.selected_t_node}'", @@ -110,25 +110,25 @@ async def tech_type_callback(self, result: Any) -> int: if "specificationMermaidGraph" in self.tech_type_data: del self.tech_type_data["specificationMermaidGraph"] - self.log("Technology Type Mermaid Graph Removed") + # self.log("Technology Type Mermaid Graph Removed") self.tech_type_guid = self.tech_type_data.get("technologyTypeGUID") self.tech_type_name = self.tech_type_data.get("displayName") self.tech_type_templates = self.tech_type_data.get("catalogTemplates") self.tech_type_templates = clean_structure(self.tech_type_templates) self.tech_type_templates = bools_to_strings(self.tech_type_templates) - self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") + # self.log(f"Templates: {self.tech_type_templates}, type: {type(self.tech_type_templates)}") self.tech_type_processes = self.tech_type_data.get("governanceActionProcesses") self.tech_type_processes = clean_structure(self.tech_type_processes) self.tech_type_processes = bools_to_strings(self.tech_type_processes) - self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") + # self.log(f"Processes: {self.tech_type_processes}, type: {type(self.tech_type_processes)}") self.tech_type_description = self.tech_type_data.get("description") - self.log(f"Technology Type GUID: {self.tech_type_guid}") - self.log(f"Technology Type Name: {self.tech_type_name}") - self.log(f"Technology Type Description: {self.tech_type_description}") - self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) - self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) + # self.log(f"Technology Type GUID: {self.tech_type_guid}") + # self.log(f"Technology Type Name: {self.tech_type_name}") + # self.log(f"Technology Type Description: {self.tech_type_description}") + # self.log(f"Technology Type Templates: {self.tech_type_templates}" or [{"templates": "None"}]) + # self.log(f"Technology Type Processes: {self.tech_type_processes}" or [{"processes": "None"}]) await self.push_screen( TechnologyTypeOptionsScreen( @@ -144,40 +144,41 @@ async def tech_type_callback(self, result: Any) -> int: return 200 async def tech_type_options_callback(self, result: Any) -> int: - self.log(f"Technology Type Options screen returned: {result}") + # self.log(f"Technology Type Options screen returned: {result}") self.selected_t_option = None self.selected_t_option_selected = None if not result: - self.log(f"Technology Type Options screen returned no input; return: {result}.") + # self.log(f"Technology Type Options screen returned no input; return: {result}.") self._show_main_screen() return 200 elif isinstance(result, (int, str)): if result == "back" or result == "": - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") + # self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") self._show_main_screen() return 200 elif isinstance(result, int) and result == 200: - self.log("Technology Type Options screen returned successfully.") + # self.log("Technology Type Options screen returned successfully.") self._show_main_screen() return 200 elif isinstance(result, int) and result != 200: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") + # self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") self.exit(415) return 415 else: - self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") + # self.log(f"Technology Type Options screen returned unexpected result: {result}, type: {type(result)}") self._show_main_screen() return 200 elif isinstance(result, (list, tuple)): - self.log(f"Input is a list: {result}, type: {type(result)}") + # self.log(f"Input is a list: {result}, type: {type(result)}") if len(result) == 0: self._show_main_screen() return 200 - self.log(f"Result 0 = {result[0]}") + # self.log(f"Result 0 = {result[0]}") if isinstance(result[0], int): if result[0] == 200: - self.log("Technology Type Options screen returned successfully.") + pass + # self.log("Technology Type Options screen returned successfully.") self._show_main_screen() return result[0] elif result[0] == "back": @@ -186,16 +187,16 @@ async def tech_type_options_callback(self, result: Any) -> int: elif len(result) >= 2: self.selected_t_option = str(result[0]) self.selected_t_option_selected = str(result[1]) - self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") + # self.log(f"Technology Type Options screen returned: {self.selected_t_option} | {self.selected_t_option_selected}") else: self.selected_t_option = str(result[0]) self.selected_t_option_selected = "" else: - self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") + # self.log(f"Technology Type Options screen cancelled/failed; return: {result}, type: {type(result)}.") self._show_main_screen() return 200 - self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") + # self.log(f"Selected: {self.selected_t_option} | {self.selected_t_option_selected}") if self.selected_t_option == "template": await self.push_screen( TechnologyTypeTemplatesScreen( @@ -221,7 +222,7 @@ async def tech_type_options_callback(self, result: Any) -> int: callback=self.tech_type_processes_callback, ) else: - self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") + # self.log(f"Technology Type Options screen returned invalid option: {self.selected_t_option_selected}") self.exit(416) return 200 @@ -231,16 +232,16 @@ async def fetch_technology_types(self) -> int: self.autoc = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) # retrieve the tech type data - self.log("Fetching technology type hierarchy for tech_type='*'") + # self.log("Fetching technology type hierarchy for tech_type='*'") self.tech_type_response = await self.autoc._async_get_tech_type_hierarchy(filter_string="*") except Exception as e: - self.log(f"Exception in get_tech_type_hierarchy: {e}") - self.log(print_basic_exception(e)) + # self.log(f"Exception in get_tech_type_hierarchy: {e}") + # self.log(print_basic_exception(e)) self.tech_type_list = [{}] self.exit(416) return 416 - self.log(f"tech_type_response: {self.tech_type_response}") + # self.log(f"tech_type_response: {self.tech_type_response}") # Copy the data into a working variable for the extraction routine self.tech_type_list = self.tech_type_response return 200 @@ -262,16 +263,16 @@ def unpack_egeria_data(self) -> int: if isinstance(subentry, dict): output_data = [subentry] else: - self.log("error unknown data structure for Tech Type data") + # self.log("error unknown data structure for Tech Type data") return 417 else: - self.log("error unknown outer data structure for Tech Type data") + # self.log("error unknown outer data structure for Tech Type data") return 417 else: - self.log("Tech Type data not dict or list") + # self.log("Tech Type data not dict or list") return 417 - self.log(f"output_data: {output_data}, {type(output_data)}") + # self.log(f"output_data: {output_data}, {type(output_data)}") self.tech_type_list = output_data return 200 @@ -281,12 +282,12 @@ def tech_type_templates_callback(self, result: Any) -> Any: result contains up to 3 elements: [0] = return code or 'input', [1] input data, [2] full template """ - self.log(f"Technology Type Templates screen returned: {result}") + # self.log(f"Technology Type Templates screen returned: {result}") if isinstance(result, int): - self.log(f"Technology Type Templates screen returned: {result}, exiting.") + # self.log(f"Technology Type Templates screen returned: {result}, exiting.") return result if not result or not isinstance(result, (list, tuple)) or len(result) < 3 or result[0] != "input": - self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") + # self.log(f"Technology Type Templates screen cancelled/failed; return: {result}, exiting.") return 418 if isinstance(result[2], dict): @@ -302,20 +303,20 @@ def tech_type_templates_callback(self, result: Any) -> Any: if isinstance(self.placeholder_input, dict): for input_item, input_value in self.placeholder_input.items(): - self.log(f"input_item: {input_item}, input_value: {input_value}") + # self.log(f"input_item: {input_item}, input_value: {input_value}") input_fix1 = input_item.replace("_", " ") - self.log(f"input_item after underscore removal: {input_fix1}") + # self.log(f"input_item after underscore removal: {input_fix1}") input_fix2 = input_fix1.replace(" placeholder input", "") - self.log(f"input_item after placeholder removal: {input_fix2}") - self.log(f"fixed input_item: {input_fix2}, {input_value}") + # self.log(f"input_item after placeholder removal: {input_fix2}") + # self.log(f"fixed input_item: {input_fix2}, {input_value}") my_placeholderPropertyValues[input_fix2] = input_value else: - self.log(f"placeholder_input is not a dict: {self.placeholder_input}") + # self.log(f"placeholder_input is not a dict: {self.placeholder_input}") return 419 - self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") - self.log(f"self.placeholder_input: {self.placeholder_input}") - self.log(f"self.full_template: {self.full_template}") + # self.log(f"my_placeholderPropertyValues: {my_placeholderPropertyValues}") + # self.log(f"self.placeholder_input: {self.placeholder_input}") + # self.log(f"self.full_template: {self.full_template}") request_body: dict = { "class": "TemplateRequestBody", @@ -337,39 +338,41 @@ def tech_type_templates_callback(self, result: Any) -> Any: "parentAtEnd1": self.full_template.get("parentAtEnd1") or True, "effectiveTime": self.full_template.get("effectiveTime") or datetime.datetime.now().isoformat(), } - self.log(f"request_body: {request_body}") + # self.log(f"request_body: {request_body}") for key, value in my_placeholderPropertyValues.items(): request_body["placeholderPropertyValues"][key] = value - self.log(f"request_body after update: {request_body}") + # self.log(f"request_body after update: {request_body}") try: tokendata = self.autoc.create_egeria_bearer_token(self.user_name, self.user_password) my_md_instance = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password, tokendata) new_guid = my_md_instance.create_elem_from_template(body=request_body) except Exception as e: - self.log(f"Exception in create_element_from_template: {e}") + # self.log(f"Exception in create_element_from_template: {e}") if isinstance(e, PyegeriaException): - self.log(print_basic_exception(e)) + pass + # self.log(print_basic_exception(e)) else: - self.log(f"Exception in create_element_from_template: {e}") + pass + # self.log(f"Exception in create_element_from_template: {e}") self.push_screen(StatusScreen(f"Error creating element from Template\n{print_basic_exception(e)}"), callback=self.status_callback) return 420 - self.log(f"new_guid: {new_guid}") + # self.log(f"new_guid: {new_guid}") self.push_screen(StatusScreen(f"Element created from Template/Metadata element created with GUID: '{new_guid}'"), callback=self.status_callback) def tech_type_processes_callback(self, result: Any) -> Any: """Callback for Technology Type Processes screen.""" - self.log(f"Technology Type Processes screen returned: {result}") + # self.log(f"Technology Type Processes screen returned: {result}") if isinstance(result, int): - self.log(f"Technology Type Processes Screen returned: {result}, exiting.") + # self.log(f"Technology Type Processes Screen returned: {result}, exiting.") return result if not result or not isinstance(result, (list, tuple)) or len(result) < 3 or result[0] != "input": - self.log(f"Technology Type Processes screen cancelled or failed; return: {result}, exiting.") + # self.log(f"Technology Type Processes screen cancelled or failed; return: {result}, exiting.") error_category = "Governance Action Process" error_message = f"Unknown response from TechnologyTypeProcessesScreen: {result}" - self.log(f"Error retrieving process details: {error_category}, {error_message}") + # self.log(f"Error retrieving process details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) return 418 @@ -377,18 +380,18 @@ def tech_type_processes_callback(self, result: Any) -> Any: self.input_data = result[1] self.full_process = result[2] self.result = result - self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") + # self.log(f"Received process type: {self.type}, data: {self.input_data}, full process: {self.full_process}") my_process_property_values: list[dict] = [] if isinstance(self.input_data, dict): for input_key, input_value in self.input_data.items(): - self.log(f"Processing input value: {input_key} with value: {input_value}") + # self.log(f"Processing input value: {input_key} with value: {input_value}") # Keys are real parameter names; only a raw widget id needs converting back input_fix2 = input_key for suffix in ("_placeholder_input", "_process_input"): if input_fix2.endswith(suffix): input_fix2 = input_fix2[: -len(suffix)].replace("_", " ") - self.log(f"Process input key after cleaning: {input_fix2}") + # self.log(f"Process input key after cleaning: {input_fix2}") my_process_property_values.append({ "class": "SupportedRequestParameter", "specificationPropertyType": "SUPPORTED_REQUEST_PARAMETER", @@ -400,13 +403,14 @@ def tech_type_processes_callback(self, result: Any) -> Any: "required": "False" }) else: - self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") - self.log(f" Input data type: {type(self.input_data)}, data: {self.input_data}") - self.log("Processing will proceed with no Supported Request Parameters for the process") + pass + # self.log("No process Request Parameter input data returned, this is possibly valid, but unlikely") + # self.log(f" Input data type: {type(self.input_data)}, data: {self.input_data}") + # self.log("Processing will proceed with no Supported Request Parameters for the process") - self.log(f"Process property values: {my_process_property_values}") - self.log(f"Original input: {self.result}") - self.log(f"full process: {self.full_process}") + # self.log(f"Process property values: {my_process_property_values}") + # self.log(f"Original input: {self.result}") + # self.log(f"full process: {self.full_process}") process_qualified_name = self.full_process.get("qualifiedName") if isinstance(self.full_process, dict) else None if not process_qualified_name: @@ -424,13 +428,13 @@ def tech_type_processes_callback(self, result: Any) -> Any: request_parameters=request_parameters or None, ) except Exception as e: - self.log(f"Exception in initiate_gov_action_process: {e}") + # self.log(f"Exception in initiate_gov_action_process: {e}") message = print_basic_exception(e) if isinstance(e, PyegeriaException) else str(e) self.push_screen(StatusScreen(f"Error starting governance action process\n{message}"), callback=self.status_callback) return 420 - self.log(f"Started process {process_qualified_name}: {process_guid}") + # self.log(f"Started process {process_qualified_name}: {process_guid}") self.push_screen(StatusScreen(f"Governance action process started with GUID: '{process_guid}'"), callback=self.status_callback) return 200 @@ -443,10 +447,10 @@ def tech_type_processes_details(self, tech_type: Any, selected_t_option_selected required_process = None self.type = str(tech_type) self.selected_q_name = str(selected_t_option_selected) - self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") + # self.log(f"Received process type: {self.type}, selected: {self.selected_q_name}") if self.selected_q_name is not None: - self.log(f"Selected qualified name: {self.selected_q_name}") + # self.log(f"Selected qualified name: {self.selected_q_name}") try: p_client = AutomatedCuration(self.view_server, self.platform_url, self.user_name, self.user_password) token = p_client.create_egeria_bearer_token(self.user_name, self.user_password) @@ -455,9 +459,9 @@ def tech_type_processes_details(self, tech_type: Any, selected_t_option_selected print_basic_exception(e) error_category = "Technology Type Process Detail Retrieval" error_message = f"Failed to retrieve technology type process details for {self.selected_q_name}: {str(e)}" - self.log(f"Error retrieving technology type details: {error_category}, {error_message}") + # self.log(f"Error retrieving technology type details: {error_category}, {error_message}") self.push_screen(StatusScreen(f"{error_category}: {error_message}"), callback=self.status_callback) - self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") + # self.log(f"Egeria returned: {governance_actions}, type: {type(governance_actions)}") if isinstance(governance_actions, str): if governance_actions != "No elements found": technology_response = json.loads(governance_actions) @@ -466,17 +470,17 @@ def tech_type_processes_details(self, tech_type: Any, selected_t_option_selected else: technology_response = governance_actions - self.log(f"Tech type process data retrieved: {technology_response}") + # self.log(f"Tech type process data retrieved: {technology_response}") self.governance_action_processes = technology_response.get("governanceActionProcesses", []) if technology_response else [] - self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") - self.log(f"Data: {self.governance_action_processes}") + # self.log(f"Retrieved {len(self.governance_action_processes)} governance action processes for {self.selected_q_name}") + # self.log(f"Data: {self.governance_action_processes}") for process in self.governance_action_processes: - self.log(f"Process: {process['name']}, Description: {process['description']}") + # self.log(f"Process: {process['name']}, Description: {process['description']}") if process['name'] == self.selected_q_name: - self.log(f"Found requested process: {process['description']}") + # self.log(f"Found requested process: {process['description']}") required_process = process break return required_process else: - self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") + # self.log(f"selected_q_name in process_details function is None, returnng None to caller: {self.selected_q_name}") return None diff --git a/tests/micro-tests/my_profile/test_elements_crud_handler.py b/tests/micro-tests/my_profile/test_elements_crud_handler.py index 2a81d31f..19924285 100644 --- a/tests/micro-tests/my_profile/test_elements_crud_handler.py +++ b/tests/micro-tests/my_profile/test_elements_crud_handler.py @@ -42,6 +42,7 @@ def __init__(self): self.switched_screens = [] self.shown_main_screen = False self.log_messages = [] + self.notifications = [] self.user_GUID = "test-user-guid" self.view_server = "qs-view-server" self.platform_url = "https://127.0.0.1:9443" @@ -52,6 +53,9 @@ def __init__(self): def log(self, msg, *args, **kwargs): self.log_messages.append(str(msg)) + def notify(self, msg, *args, **kwargs): + self.notifications.append(str(msg)) + def _show_main_screen(self): self.shown_main_screen = True @@ -203,6 +207,7 @@ def test_edit_tables_unexpected(self): app.edit_tables("unknown_table", 0) assert len(app.pushed_screens) == 0 assert any("Unexpected table name" in msg for msg in app.log_messages) + assert any("Unexpected table name" in msg for msg in app.notifications) def test_empty_edit_callbacks(self): app = DummyApp() @@ -246,6 +251,7 @@ async def test_add_to_tables_unexpected(self): await app.add_to_tables("unknown_table", 0) assert app.push_screen.call_count == 0 assert any("Unexpected table name" in msg for msg in app.log_messages) + assert any("Unexpected table name" in msg for msg in app.notifications) @pytest.mark.parametrize( "callback_name", From c023078dc6b9e2e0d79522946f55d5c62ace9956 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:45:38 -0500 Subject: [PATCH 2/6] Tidying up UI elements to make them more visible Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py index 0edfb2ad..e76a5265 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py @@ -118,7 +118,7 @@ def compose(self) -> ComposeResult: Select([(t, t) for t in JOIN_ASSIGNMENT_TYPES], value=JOIN_ASSIGNMENT_TYPES[0], allow_blank=False, id="join_assignment_type"), Button("Join Community", id="join_community_btn", variant="primary"), - Button("Open Community", id="open_found_community_btn", variant="default"), + Button("Open Community", id="open_found_community_btn", variant="primary"), id="found_communities_buttons", ), id="find_communities_container", From 1073c19599ee17fac0cf61625a75559a5c562e45 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:40:23 -0500 Subject: [PATCH 3/6] Updates from PDR team call 10/30/26, ability tpo add a new community, consistent screen titles, show persons not roles in communities, etc. Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- docs/My-Egeria-Doc.md | 50 ++++- docs/my_egeria_manual.md | 27 ++- .../DemoCode/My_Profile/AddCommentScreen.py | 4 +- .../DemoCode/My_Profile/ChangeUserScreen.py | 5 +- .../DemoCode/My_Profile/CommunitiesScreen.py | 113 +++++++++- .../DemoCode/My_Profile/CommunityScreen.py | 14 +- .../My_Profile/ConnectionErrorScreen.py | 5 +- .../My_Profile/CreateProfileScreen.py | 4 +- .../CreateSubscriptionRequestScreen.py | 4 +- .../My_Profile/EditElementsScreens.py | 10 +- .../DemoCode/My_Profile/FeedbackScreens.py | 7 +- .../My_Profile/GenericDataViewScreen.py | 4 +- .../DemoCode/My_Profile/MainScreen.py | 5 +- .../DemoCode/My_Profile/MyBookMarksScreen.py | 4 +- .../DemoCode/My_Profile/MyNetworkScreen.py | 7 +- .../DemoCode/My_Profile/MyTeamScreen.py | 3 +- .../My_Profile/SearchForTermScreen.py | 4 +- .../My_Profile/SelectionOverviewScreen.py | 6 +- .../DemoCode/My_Profile/ShopForDataScreen.py | 2 + .../DemoCode/My_Profile/ShowCommentsScreen.py | 4 +- .../DemoCode/My_Profile/SplashScreen.py | 6 +- .../DemoCode/My_Profile/StatusScreen.py | 4 + .../My_Profile/TechnologyTypeScreens.py | 13 +- .../My_Profile/UserIdentitiesScreen.py | 4 +- .../DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md | 2 +- .../My_Profile/ViewSubscriptionsScreen.py | 3 +- .../DemoCode/My_Profile/community_handler.py | 129 ++++++++++-- .../DemoCode/My_Profile/feedback_handler.py | 3 +- .../DemoCode/My_Profile/my_egeria.py | 6 +- .../DemoCode/My_Profile/my_profile.tcss | 10 +- .../DemoCode/My_Profile/profile_utils.py | 17 ++ tests/micro-tests/my_profile/README.md | 11 +- .../my_profile/test_community_handler.py | 194 +++++++++++++++++- .../micro-tests/my_profile/test_my_egeria.py | 9 +- .../my_profile/test_profile_utils.py | 36 ++++ 35 files changed, 609 insertions(+), 120 deletions(-) diff --git a/docs/My-Egeria-Doc.md b/docs/My-Egeria-Doc.md index 6b6adf93..90d5282b 100644 --- a/docs/My-Egeria-Doc.md +++ b/docs/My-Egeria-Doc.md @@ -31,11 +31,11 @@ All source files named here are in `my_egeria/my_egeria/DemoCode/My_Profile/`. | `team_roles_handler.py`, `MyTeamScreen.py` | `TeamRolesMixin` and the team roster screen | | `bookmarks_handler.py`, `MyBookMarksScreen.py` | `BookmarksMixin` (bookmarks kept in a personal collection) and the bookmarks screen | | `network_handler.py`, `MyNetworkScreen.py` | `NetworkMixin` (peers linked by `Peer` relationships), `MyNetworkScreen` and `ConfirmRemovePeerScreen` | -| `community_handler.py`, `CommunitiesScreen.py` | `CommunitiesMixin` (community membership through `AssignmentScope` relationships), `CommunitiesScreen` and `ConfirmLeaveCommunityScreen` | +| `community_handler.py`, `CommunitiesScreen.py` | `CommunitiesMixin` (community membership through `AssignmentScope` relationships), `CommunitiesScreen`, `CreateCommunityScreen` and `ConfirmLeaveCommunityScreen` | | `CommunityScreen.py` | `CommunityScreen` (one community's members, blog and comments), `NewBlogPostScreen` and `AddCommunityCommentScreen` | | `UserIdentitiesScreen.py` | Read-only user identity table | | `StatusScreen.py` | Result message with copy-GUID | -| `profile_utils.py` | Environment check, Egeria access check (`check_egeria_access`) and data clean-up helpers | +| `profile_utils.py` | Environment check, Egeria access check (`check_egeria_access`), the shared screen heading (`set_screen_heading`) and data clean-up helpers | | `my_profile.tcss` | Layout and theme | | `RETURN_CODES.md`, `catalog_api_calls.md` | Return-code list and Shop for Data API notes | @@ -84,6 +84,7 @@ classDiagram list_my_communities() search_communities() get_community_members() + create_community() join_community() / leave_community() get_community_blog_posts() / add_community_blog_post() get_community_comments() / add_community_comment() @@ -106,6 +107,8 @@ classDiagram - **`check_action`** hides and disables `view_feedback_log` unless the current user is the feedback owner (`garygeeke`). - **Screen registration:** most screens are registered by name in `SCREENS`. `EditCollectionsScreen`, `AddCollectionScreen`, `AddUserIdentityScreen`, `FeedbackScreen`, `FeedbackLogScreen` and `ConfirmDeleteScreen` are pushed as instances instead. - **Mixins and `@on`:** the mixins are plain Python classes, not Textual message pumps. Textual only collects `@on(...)`-decorated handlers from classes built with its own metaclass, so an `@on` handler defined on a mixin is never dispatched. Put any `@on` handler on `MyProfileApp` itself and have it delegate to the mixin, as `_on_roles_row_selected` does. Handlers found by name (`on_worker_state_changed`, `action_*`) and callbacks passed to `push_screen` work on mixins. +- **Screen headings:** every screen with a header calls `set_screen_heading(screen, context)` from `on_mount`. It sets the title to `Egeria - ` (just `Egeria` on the main screen) and the sub-title to `User: , Karma Points: `, read from the app's `user_name` and `karma_points`. The app passes `app=self` when it sets the main screen's heading itself, because `screen.app` isn't always available then. It does that in `_process_profile_data`, once the Karma Points are known, and in `change_user`. Screens opened later read the current values when they mount. +- **Diagnostic logging is off.** The app's `self.log(...)` calls are commented out across every module, so the Textual developer console shows only Textual's own messages. Uncomment the ones you need to trace a problem. Where commenting one out would have left a block empty, a `pass` was added. The two "Unexpected table name" messages in `elements_crud_handler.py` are still logged, and also shown to the user with `notify`. - **Entry point:** `main()` runs the app. It is the target of the `my_profile` console script. --- @@ -149,10 +152,10 @@ flowchart TD - **`ConnectionErrorScreen`** retries every `RETRY_INTERVAL` (10) seconds from a 1-second `set_interval` countdown, or on **Retry Now** (`r`). Each attempt calls `check_egeria_access` with the user in `settings.User_Profile`, and updates the reason shown if it changed. It dismisses with `200` once Egeria accepts the credentials, `201` for **Change User** (`u`) and `402` for **Quit** (`q`), stopping the timer each time. `connection_error_callback` reloads the profile on `200`, pushes `ChangeUserScreen` on `201`, and otherwise calls `exit(402)`. - **`check_egeria_access(view_server, platform_url, user, password)`** returns `None` if Egeria is reachable and accepts the credentials, otherwise a message. The `Egeria` constructor checks the platform answers; the token request then checks the credentials. A successful `create_egeria_bearer_token()` is not proof on its own: on a 401 it returns Egeria's JSON error body as if it were the token, so a token starting with `{` is treated as a rejection and its `exceptionErrorMessage` is reported. - **Change User** is reached three ways: the splash screen's button, the **Change User** menu option, and the connection error screen. All three push `ChangeUserScreen`, which validates the new credentials with `check_egeria_access` before dismissing with `[user, password]`; on a failure it shows the reason and stays open. It dismisses with `200` on Cancel or `Escape`. From the splash screen, a cancel leaves the splash showing; a list is passed on through the splash's own `dismiss` to `mainline`. -- **`change_user(user, pwd)`** does the switch. It sets the app's user and password, writes them into pyegeria's cached `settings.User_Profile` (screens read the user from there, so every screen opened afterwards runs its Egeria calls as the new user), and exports `EGERIA_USER` / `EGERIA_USER_PASSWORD` so pyegeria clients, commands and subprocesses started from the app use them too. It then sets the main screen's subtitle to `User: `, clears `my_profile_data`, calls `refresh_bindings()` (the feedback-log binding depends on the user) and runs `_load_or_create_profile()`. +- **`change_user(user, pwd)`** does the switch. It sets the app's user and password, writes them into pyegeria's cached `settings.User_Profile` (screens read the user from there, so every screen opened afterwards runs its Egeria calls as the new user), and exports `EGERIA_USER` / `EGERIA_USER_PASSWORD` so pyegeria clients, commands and subprocesses started from the app use them too. It then resets `karma_points` to 0 and resets the main screen's heading (the new user's points arrive with their profile), clears `my_profile_data`, calls `refresh_bindings()` (the feedback-log binding depends on the user) and runs `_load_or_create_profile()`. - **`change_user_callback`** handles the menu and connection-error routes. If `ChangeUserScreen` is cancelled while `load_error` is set, it calls `_after_profile_load(False)`, which puts the connection error screen back; otherwise a cancel does nothing. - **What `_process_profile_data` extracts** from the first `My-User-MD` record: - - Contribution Record, which supplies the Karma Points; + - Contribution Record, which supplies the Karma Points (the main screen's heading is updated with them straight away); - Projects, Teams, Communities and Roles; - Note Logs, split into blogs and journal by their `class`. @@ -359,7 +362,7 @@ sequenceDiagram participant Egeria as Egeria (note logs) User->>App: ctrl+f (any screen) or "Leave Feedback" - App->>App: record current screen class and sub-title + App->>App: record current screen class and title App->>FS: push_screen(FeedbackScreen(screen, title)) User->>FS: category + text, Submit (ctrl+s) FS-->>App: {screen, screen_title, category, text} @@ -587,6 +590,7 @@ sequenceDiagram actor User participant App as CommunitiesMixin participant CS as CommunitiesScreen + participant Create as CreateCommunityScreen participant Confirm as ConfirmLeaveCommunityScreen participant EG as Egeria @@ -603,6 +607,15 @@ sequenceDiagram App->>EG: link_assignment_scope(GUID, user_GUID, AssignmentScopeProperties) CS->>App: list_my_communities() and search again end + opt create (ctrl+t) + CS->>Create: push + Create-->>CS: {name, description, mission, assignment type} / None + CS->>App: create_community(name, description, mission, assignment type) + App->>EG: get_communities_by_name(qualified name) + App->>EG: create_community(NewElementRequestBody, CommunityProperties) + App->>EG: link_assignment_scope(new GUID, user_GUID, AssignmentScopeProperties) unless "Don't join" + CS->>App: list_my_communities() and search again, then highlight the new community + end opt leave (ctrl+d) CS->>Confirm: push(community name) Confirm-->>CS: True / False @@ -618,10 +631,12 @@ sequenceDiagram | `list_my_communities()` | `get_my_profile(JSON)`, then reads the profile's own `assignmentScope` links and each `performsRoles` appointment's `nestedElements`, keeping only `Community` elements. Returns one `(name, assignment types, linked through, description, GUID)` row per community; "linked through" is `Your profile` and/or `Role: `. `None` if Egeria can't be reached | | `search_communities(search_string)` | `find_communities(..., ignore_case=True, graph_query_depth=0)` → `(name, description, qualified name, GUID)` rows. The screen drops communities the user already belongs to | | `join_community(guid, assignment_type)` | `link_assignment_scope(guid, user_GUID, NewRelationshipRequestBody)` with `AssignmentScopeProperties`. The screen offers only `Contributor` and `Observer` | +| `create_community(name, description, mission, assignment_type)` | Builds the qualified name with `__create_qualified_name__("Community", name)` and refuses (a warning, returning `None`) if `get_communities_by_name` already finds it, because Egeria answers a duplicate qualified name only with a server error. Otherwise `create_community(NewElementRequestBody)` with `CommunityProperties` (`mission` only when given), then, unless `assignment_type` is empty, links the user's profile exactly as `join_community` does. Returns the new GUID; a failed link is a warning and still returns the GUID, because the community exists. The form offers `Leader` (the default), `Contributor`, `Observer` and "Don't join" (`NOT_JOINING`, an empty string) | | `leave_community(guid)` | `detach_assignment_scope(guid, user_GUID, DeleteRelationshipRequestBody)`, using the same GUID order as `link_assignment_scope` (scope first, actor second) | - **Leaving only unlinks the profile.** The screen refuses to leave a community reached only through a role, because the role may be shared and belongs to whoever manages it. -- **Refresh.** After a successful join or leave the screen re-reads the user's communities and repeats the last search. +- **Refresh.** After a successful create, join or leave the screen re-reads the user's communities and repeats the last search. After a create it also moves the cursor to the new community, in whichever list it appears. +- **Two ways to create a community.** The User Associations edit screen's `AddCommunityScreen` also creates communities, but doesn't check for a duplicate name or link the user. Work with Communities' **Create Community** does both. - **Errors.** Every Egeria failure is reported with `notify(...)` and the screen stays open; none of them exit the app. - **Return code.** `CommunitiesScreen` dismisses with `200` on `q`; `communities_callback` just logs it. @@ -639,6 +654,7 @@ sequenceDiagram CoS->>App: get_community_members(GUID) App->>EG: get_community_by_guid(JSON, graph_query_depth=1) + App->>EG: get_actor_role_by_guid(role, include_only_relationships=[PersonRoleAppointment]) for each role CoS->>App: get_community_blog_posts(GUID) App->>EG: get_attached_note_logs(GUID) App->>EG: get_notes_for_note_log(log, "BlogEntry") for each log @@ -658,13 +674,15 @@ sequenceDiagram | Function | What it does | | :--- | :--- | -| `get_community_members(guid)` | `get_community_by_guid(guid, JSON, graph_query_depth=1)`, reading `assignedActors` → `(name, actor type, assignment type, qualified name, GUID)` rows | +| `get_community_members(guid)` | People only. Reads `assignedActors` from `get_community_by_guid(guid, JSON, graph_query_depth=1)`. A `Person` actor is a direct member. For an actor whose type is or inherits `ActorRole` (checked through `superTypeNames`), it calls `get_actor_role_by_guid(..., graph_query_depth=1, include_only_relationships=["PersonRoleAppointment"])` and takes the `Person` elements in `rolePerformers`. Other actors, such as teams, are skipped. Returns one `(name, assignment types, member through, qualified name, GUID)` row per person, sorted by name; "member through" is `Direct` and/or `Role: `. A role that can't be read is skipped and reported with one warning; `None` if the community itself can't be read | | `get_community_blog_posts(guid)` | Reads the `BlogEntry` notes from every note log attached to the community, so posts written by other tools are included and the notifications some sample communities' logs hold are not. Returns the `LATEST_POSTS` (20) newest as `(title, author, posted, text, GUID)` rows; author and time come from the element header's `versions` | | `add_community_blog_post(guid, name, title, text)` | Finds the note log with qualified name `NoteLog::::CommunityBlog` among those attached to the community, creating it (attached, display name "_name_ Blog") on the first post, then `create_note(log, BlogEntryProperties)` | | `get_community_comments(guid)` | `get_attached_comments(guid, graph_query_depth=0)` → `(comment, type, author, posted, GUID)` rows, newest first | | `add_community_comment(guid, text, type)` | `add_comment_to_element(guid, text, type)`; the form offers pyegeria's `COMMENT_TYPES` | - **Return codes.** `CommunityScreen` dismisses with `200` on `Escape` (back to the communities list) and `210` on `q`; on `210` `CommunitiesScreen` dismisses itself too, so `q` reaches the main screen. +- **Why the role lookups are filtered.** Without `include_only_relationships`, one role can return hundreds of unrelated links. One sample role had 284 `assignmentScope` entries. With the filter, the Sustainability Community's 40 roles resolve in under a second. Asking `get_community_by_guid` for `graph_query_depth=2` instead, to get the appointments in one call, timed out after 30 seconds on the sample data. +- **Names.** People, roles and communities are named by `displayName`, then `fullName`, then `identifier`, then `qualifiedName`. Some sample community roles have only an `identifier`, such as "Community Leader of Data Science special interest group". - **Membership isn't checked.** Anyone can read and add to a community's blog and comments; Egeria's quickstart deployment enforces no access rules here either. --- @@ -739,9 +757,20 @@ The suite in `tests/micro-tests/my_profile/` has one module per handler and scre **My Network.** `test_network_handler.py` covers `NetworkMixin` and `MyNetworkScreen`, including the remove confirmation; `test_my_egeria.py` checks the menu routing. The feature has also been tested by hand against a live server. -**Work with Communities.** `test_community_handler.py` covers `CommunitiesMixin`, `CommunitiesScreen` and `CommunityScreen`, including memberships through roles, the leave confirmation, refusing to leave a role membership, creating the blog on the first post and reusing it afterwards, and the post and comment forms; `test_my_egeria.py` checks the menu routing. The blog and comment functions have also been run against a live server (2026-10-28); the screens themselves have not been tried by hand there yet. +**Work with Communities.** `test_community_handler.py` covers `CommunitiesMixin`, `CommunitiesScreen` and `CommunityScreen`. That includes: +- memberships through roles, the leave confirmation, and refusing to leave a role membership; +- creating a community: leading it by default, not joining, a duplicate name, a failed create and a failed link; +- the Create Community form: validation, cancel, and highlighting the new community; +- members as people only, direct and through roles, merged per person, with an unreadable role skipped; +- creating the blog on the first post and reusing it afterwards, and the post and comment forms. + +`test_my_egeria.py` checks the menu routing. The blog and comment functions were run against a live server on 2026-10-28. Listing and finding communities, `create_community` (with and without joining, and the duplicate check) and `get_community_members` were run there on 2026-10-30. The screens themselves have not been tried by hand there yet. + +**Screen headings.** `test_profile_utils.py::TestSetScreenHeading` covers `set_screen_heading`. `test_my_egeria.py` checks that the main screen's heading shows the Karma Points once the profile has loaded, and resets them to 0 on `change_user`. + +**Live checks that depend on sample data.** A live check must not assume data a real user might not have. For example, a user needn't hold any roles, so `test_app_on_mount_success` accepts zero roles. Tests that need a team role get one from the `live_team_role_name` fixture, which skips when there is none. Tests that run the whole app must `await app._load_task` before checking what was loaded. A live profile load takes several seconds, and a single `pilot.pause()` sees it only half done. -**Connection errors and Change User.** `test_connection_and_change_user.py` covers `check_egeria_access` (good credentials, unreachable platform, a 401 body returned as the token), `ConnectionErrorScreen` (retry, countdown, buttons), `ChangeUserScreen`, the splash screen's Change User button, and the app's callbacks: reload when Egeria comes back, change user from the error screen and the menu, cancel back to the error screen, and quit with `402`. `test_my_egeria.py` checks that a failed profile load shows the connection error screen instead of exiting, and that `change_user` updates `settings`, the environment and the main screen subtitle. +**Connection errors and Change User.** `test_connection_and_change_user.py` covers `check_egeria_access` (good credentials, unreachable platform, a 401 body returned as the token), `ConnectionErrorScreen` (retry, countdown, buttons), `ChangeUserScreen`, the splash screen's Change User button, and the app's callbacks: reload when Egeria comes back, change user from the error screen and the menu, cancel back to the error screen, and quit with `402`. `test_my_egeria.py` checks that a failed profile load shows the connection error screen instead of exiting, and that `change_user` updates `settings`, the environment and the main screen heading. **Add screens.** `test_screens.py::TestBaseAddScreen` covers the `BaseAddScreen` flow: required-field checks, create, link or no link, and keeping the form after a failed create. @@ -753,7 +782,8 @@ The user-visible limitations are listed in the [user manual](my_egeria_manual.md - `get_data_product_catalog_table`, `get_guid_for_qualified_name`, `display_glossary_term_details`, `tech_type_processes_details`, `unpack_egeria_data` and `display_selected_data_specification` are defined but never called. - Bookmarks, My Collections, team joining, subscription links and starting processes have only been tested against fakes; they haven't yet been run against a live Egeria server. -- Work with Communities: the blog and comment functions have been run against a live server, but `list_my_communities`, `search_communities`, `get_community_members`, `join_community`, `leave_community` and the screens have only been tested against fakes. +- Work with Communities: `join_community` (from the found list), `leave_community` and the screens have only been tested against fakes. The other functions have been run against a live server. +- Two Add paths create communities: `AddCommunityScreen` on the User Associations edit screen, and `CreateCommunityScreen` in Work with Communities. Only the second checks for a duplicate name and links the user. --- License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), diff --git a/docs/my_egeria_manual.md b/docs/my_egeria_manual.md index 3f11635d..178370d3 100644 --- a/docs/my_egeria_manual.md +++ b/docs/my_egeria_manual.md @@ -83,6 +83,8 @@ textual run --dev my_egeria.py Use Python 3.13: Textual currently fails on Python 3.14 (an assertion in `textual/rlock.py` at startup). +The app's own diagnostic messages (its `self.log(...)` calls) are commented out, so the developer console shows only Textual's messages. To trace a problem, uncomment the ones you need in the source. + To serve the app in a web browser instead of a terminal: ```bash @@ -104,7 +106,7 @@ You can switch to a different Egeria user at any time: from the splash screen's The screen shows who you are logged in as, with *User Name* and *Password* fields. Press `Enter` in the user name field to move to the password; `Enter` in the password field (or **Submit**) submits. Both fields are required. -Before switching, the app asks Egeria to log in with the new user and password. If Egeria can't be reached or rejects them, a message says why and you stay on the screen to correct them. Once Egeria accepts them, the app discards the current profile and reloads everything for the new user; the main screen's subtitle shows the new user ID. Every screen opened afterwards, and any pyegeria command started from the app, runs as the new user. +Before switching, the app asks Egeria to log in with the new user and password. If Egeria can't be reached or rejects them, a message says why and you stay on the screen to correct them. Once Egeria accepts them, the app discards the current profile and reloads everything for the new user; the header shows the new user ID, with Karma Points of 0 until their profile has loaded. Every screen opened afterwards, and any pyegeria command started from the app, runs as the new user. **Cancel** (or `Escape`) closes the screen without changing anything. @@ -146,7 +148,7 @@ Press **Create Profile**. Egeria creates your `Person` profile, and the app relo ## The Main Screen -The header shows your full name, user ID and **Karma Points** (your contribution score in Egeria). The panels are: +The header reads **Egeria — User: _your user ID_, Karma Points: _n_**. Karma Points are your contribution score in Egeria, and they show as 0 until your profile has loaded. Every other screen has the same header, with the screen's name after "Egeria", for example **Egeria - Communities — User: garygeeke, Karma Points: 135**. The panels are: | Panel | Columns | Contents | | :--- | :--- | :--- | @@ -206,7 +208,7 @@ If a required field is missing or a value is invalid, the screen tells you which | Journal | Title\*, Text\*, Situation | – | An entry in your journal | | User Associations | Choose **Project** or **Community**, then that screen opens | | | | Projects | Name\*, Description\*, Kind (`Project`, `Campaign`, `StudyProject`, `Task`, `PersonalProject`; default `Project`), Identifier, Start Date and Planned End Date (`YYYY-MM-DD`) | Add yourself to the project team | A project | -| Communities | Name\*, Description\* | – | A community | +| Communities | Name\*, Description\* | – | A community. You are not made a member; to create a community and join it in one step, use **Create Community** in [Work with Communities](#work-with-communities) | | Roles | Name\*, Description\* | Appoint yourself to this role | A person role | | Teams | Name\*, Description\* | Join this team as a member | A team; joining creates a TeamMember role for you, scoped to the team | | My Collections | Name\*, Description\*, Category | – | A standalone collection | @@ -294,6 +296,7 @@ The screen lists the communities you belong to (name, assignment type, what link - **Linked Through** is **Your profile** if you joined the community yourself, or **Role: _name_** if you belong through a role you hold. A community you reach both ways is listed once. - **Open a community:** highlight a community in either list and press `Enter`, `ctrl+o` or **Open Community**. See [A community](#a-community) below. +- **Create a community:** press `ctrl+t` (or **Create Community**). Enter a **Name\*** and **Description\***, and optionally a **Mission**, then choose **Your part in it**: **Leader** (the default, since you created it), **Contributor**, **Observer** or **Don't join**. Press `ctrl+s` (or **Create**); **Cancel** or `Escape` discards it. The lists reload and the new community is highlighted: in your communities if you joined it, otherwise in the found list. If a community with that name already exists, you are told to find it and join it instead. - **Find and join:** type a search term in the search field (`*` for all) and press `Enter`. Communities you already belong to are left out. Choose **Contributor** or **Observer**, highlight a community and press `ctrl+n` (or **Join Community**). Leaders and administrators are assigned by the people who manage the community, not chosen here. - **Leave:** highlight one of your communities and press `ctrl+d` (or **Leave Community**), then confirm with **Leave**. **Cancel** or `Escape` keeps you in it. Only the link between you and the community is removed. You can't leave a community you belong to through a role; ask whoever manages that role. - `Escape` or `q` returns to the main screen. @@ -301,7 +304,9 @@ The screen lists the communities you belong to (name, assignment type, what link Membership is an `AssignmentScope` relationship from your profile (or a role) to the community, and the assignment type is stored on that relationship. #### A community -Opening a community shows three lists: its **latest blog posts** (the 20 newest, with title, author and date; the highlighted post's text is shown underneath), its **members** (people or roles) and the **comments** on it, newest first. You don't have to be a member to read or add to them. +Opening a community shows three lists: its **latest blog posts** (the 20 newest, with title, author and date; the highlighted post's text is shown underneath), its **members** and the **comments** on it, newest first. You don't have to be a member to read or add to them. + +The members list shows **people only**, in name order, not roles. It includes everyone linked to the community directly and everyone appointed to a role that is. **Member Through** says how each person belongs: **Direct**, or **Role: _name_** for each role they hold there. A person who belongs in several ways is listed once. Other kinds of member, such as teams, are not shown. If the people in one of the community's roles can't be read, the rest are still listed and a warning says some members may be missing. - **New blog post:** press `ctrl+n` (or **New Blog Post**), enter a title and the text, then press `ctrl+s` (or **Post**). **Cancel** or `Escape` discards it. - **Add a comment:** select the comments list and press `ctrl+n` (or press **Add Comment**), choose a comment type (Standard Comment, Question, Answer, Suggestion, Usage Experience, Requirement or Other), enter the comment and press `ctrl+s` (or **Add Comment**). @@ -320,7 +325,7 @@ See [Leaving Feedback](#leaving-feedback). Every screen follows the same rules: - `Escape` goes back one level, on every screen except the main screen. -- `q` returns to the main screen, or quits the app when you are on the main screen. Screens that are mostly text fields (Add screens, Add Comment, Create Subscription, Create/Edit Profile, Search for Term, Feedback, New Blog Post, Template and Process forms) have no `q`, because it would be typed into the field; use `Escape`. +- `q` returns to the main screen, or quits the app when you are on the main screen. Screens that are mostly text fields (Add screens, Add Comment, Create Subscription, Create/Edit Profile, Search for Term, Feedback, New Blog Post, Create Community, Template and Process forms) have no `q`, because it would be typed into the field; use `Escape`. - Every other action is a `ctrl` key, and a key means the same thing wherever it appears. `ctrl` keys work even while a text field has focus. | Key | Action | Where | @@ -329,11 +334,11 @@ Every screen follows the same rules: | `r` / `u` / `q` | Retry now / Change User / Quit | Connection error screen | | `q` | Main menu / quit the app | Every list screen / Main screen | | `Enter` | Select the highlighted row; continue | Tables and trees; Status screen | -| `ctrl+s` | Save / submit the form | Add screens, Add Comment, Create Subscription, Create/Edit Profile, Feedback, New Blog Post | +| `ctrl+s` | Save / submit the form | Add screens, Add Comment, Create Subscription, Create/Edit Profile, Feedback, New Blog Post, Create Community | | `ctrl+n` | Add a new item | Edit screens (row), Bookmarks, Comments, Main screen (comment), Communities (join), a community (blog post or comment) | | `ctrl+d` | Delete / remove the highlighted item | Edit screens, Bookmarks, My Network, Communities (leave) | | `ctrl+o` | Open the highlighted item | Main screen (comments), Comments (responses), My Network (person details), Communities (open a community) | -| `ctrl+t` | Edit the selected table | Main screen | +| `ctrl+t` | Edit the selected table / create a community | Main screen / Communities | | `ctrl+k` | Bookmark the highlighted row | Main screen, Shop for Data | | `ctrl+b` | Bookmarks, with the highlighted row's GUID ready to add | Main screen, Shop for Data | | `ctrl+u` | Subscribe | Shop for Data, Overview, sample-data view | @@ -379,17 +384,17 @@ The full list is in `RETURN_CODES.md` in the app folder. - the view server named by `EGERIA_VIEW_SERVER` is running; - `EGERIA_USER` and `EGERIA_USER_PASSWORD` are correct, and the user is allowed to use the view server. If the credentials are the problem, press **Change User** rather than restarting the app. -**Something I added doesn't appear.** Press `ctrl+r` on the main screen. Communities you create are not linked to you (see Known Limitations), so they don't appear in User Associations until you join them from **Work with Communities**. +**Something I added doesn't appear.** Press `ctrl+r` on the main screen. A community added from the User Associations edit screen is not linked to you (see Known Limitations), so it doesn't appear in User Associations until you join it from **Work with Communities**. One made with **Create Community** there, with yourself as a member, appears after `ctrl+r`. **The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `403`, `430`; `402` only when you press Quit on the connection error screen), a failure fetching technology types (`416`), or an error looking up a glossary term (`440`). A failed team lookup is reported as a message instead. ## Known Limitations -As of 2026-10-28, these features are incomplete or don't work as described elsewhere: +As of 2026-10-30, these features are incomplete or don't work as described elsewhere: -- **Communities you add** are not linked to you automatically. The community is created but won't appear in User Associations until you join it from **Work with Communities**. +- **Communities added from the User Associations edit screen** are not linked to you. The community is created but won't appear in User Associations until you join it from **Work with Communities**. **Create Community** in Work with Communities does link you. - **Not yet verified against a live Egeria server:** bookmarks, My Collections, joining a team, subscription links and starting processes. The test server was unavailable when they were added, so they have only been tested against fakes. -- **Work with Communities** has only partly been tried against a live server. Reading and adding blog posts and comments works there; the rest (listing, finding, joining and leaving communities, and the screens themselves) has so far only been tested against fakes. +- **Work with Communities** has only partly been tried against a live server. Listing your communities, finding communities, creating a community (and joining it as part of that), the members list, and reading and adding blog posts and comments all work there. Leaving a community, joining one from the found list, and the screens themselves have so far only been tested against fakes. ## Running the Tests diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py index fb3f63de..eb8bc222 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py @@ -12,6 +12,7 @@ from textual.widgets import Header, Static, Footer, Input, Button, DataTable from pyegeria import load_app_config, settings +from profile_utils import set_screen_heading class AddCommentScreen(ModalScreen): @@ -39,8 +40,7 @@ def __init__(self, table_name, element_guid, *args, **kwargs): self.comment_type = "" def on_mount(self): - self.title = "Egeria - My Profile" - self.subtitle = "Add a comment to an element in Egeria" + set_screen_heading(self, "Add Comment") if self.element_guid == None: self.notify(f"A row must be selected from a table on the main screen before adding a comment") self.dismiss(400) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ChangeUserScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ChangeUserScreen.py index b11868a1..6db6b0fe 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ChangeUserScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ChangeUserScreen.py @@ -14,7 +14,7 @@ from textual.widgets import Header, Footer, Button, Label, Input from pyegeria import load_app_config, settings -from profile_utils import check_egeria_access +from profile_utils import check_egeria_access, set_screen_heading class ChangeUserScreen(ModalScreen): @@ -30,8 +30,6 @@ class ChangeUserScreen(ModalScreen): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.title = "Egeria" - self.sub_title = "Change User" load_app_config() self.user_name = settings.User_Profile.user_name self.view_server = settings.Environment.egeria_view_server @@ -52,6 +50,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, "Change User") self.query_one("#new_user_name", Input).focus() def action_cancel(self) -> None: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py index e76a5265..1b36596a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CommunitiesScreen.py @@ -9,15 +9,20 @@ from textual.app import ComposeResult from textual.containers import ScrollableContainer, Horizontal from textual.screen import ModalScreen -from textual.widgets import DataTable, Header, Static, Footer, Input, Button, Select +from textual.widgets import DataTable, Header, Static, Footer, Input, Button, Select, TextArea from CommunityScreen import CommunityScreen +from profile_utils import set_screen_heading # The "Linked Through" value of a membership held by the user's own profile, which they can leave DIRECT_LINK = "Your profile" # Assignment types a user can give themselves when joining. Leaders and administrators are # assigned by the people who manage the community (https://egeria-project.org/concepts/community/) JOIN_ASSIGNMENT_TYPES = ["Contributor", "Observer"] +# Whoever creates a community manages it, so they may also lead it +CREATOR_ASSIGNMENT_TYPES = ["Leader"] + JOIN_ASSIGNMENT_TYPES +# The "Your part in it" choice for a creator who doesn't join their new community +NOT_JOINING = "" def is_direct_membership(row) -> bool: @@ -25,6 +30,71 @@ def is_direct_membership(row) -> bool: return DIRECT_LINK in row[2].split("; ") +class CreateCommunityScreen(ModalScreen): + """Collect the details of a new community, and the part the user will take in it. + + Dismisses with {"name", "description", "mission", "assignment_type"}, where an empty + assignment_type means the user doesn't join, or None if cancelled. + """ + + BINDINGS = [ + ("escape", "cancel", "Back"), + ("ctrl+s", "submit", "Create"), + ] + + CSS_PATH = "my_profile.tcss" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, id="create_community_screen") + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + yield Static("Create Community", classes="span-3", id="create_community_title") + yield ScrollableContainer( + Static("Name *"), + Input(placeholder="Name of the community", id="community_name_input"), + Static("Description *"), + Input(placeholder="What the community is about", id="community_description_input"), + Static("Mission"), + TextArea(id="community_mission_text"), + Static("Your part in it"), + Select([(t, t) for t in CREATOR_ASSIGNMENT_TYPES] + [("Don't join", NOT_JOINING)], + value=CREATOR_ASSIGNMENT_TYPES[0], allow_blank=False, id="creator_assignment_type"), + Horizontal( + Button("Create", id="submit_create_community_btn", variant="primary"), + Button("Cancel", id="cancel_create_community_btn", variant="warning"), + ), + id="create_community_container", + ) + yield Footer() + + def on_mount(self) -> None: + set_screen_heading(self, "Communities - Create Community") + self.query_one("#community_name_input", Input).focus() + + def action_submit(self) -> None: + name = self.query_one("#community_name_input", Input).value.strip() + description = self.query_one("#community_description_input", Input).value.strip() + if not name or not description: + self.notify("Please enter both a name and a description for the community", timeout=5, + severity="warning") + return + self.dismiss({"name": name, "description": description, + "mission": self.query_one("#community_mission_text", TextArea).text.strip(), + "assignment_type": self.query_one("#creator_assignment_type", Select).value}) + + def action_cancel(self) -> None: + self.dismiss(None) + + @on(Button.Pressed, "#submit_create_community_btn") + def handle_submit_button(self, event: Button.Pressed) -> None: + self.action_submit() + + @on(Button.Pressed, "#cancel_create_community_btn") + def handle_cancel_button(self, event: Button.Pressed) -> None: + self.action_cancel() + + class ConfirmLeaveCommunityScreen(ModalScreen[bool]): """Confirmation prompt shown before the user leaves a community. @@ -54,8 +124,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = "Confirm Leaving - Communities" + set_screen_heading(self, "Communities - Confirm Leaving") @on(Button.Pressed, "#confirm_leave_community_btn") def handle_confirm_button(self, event: Button.Pressed) -> None: @@ -71,10 +140,11 @@ def action_cancel(self) -> None: class CommunitiesScreen(ModalScreen): """List the communities the current user belongs to, find others to join, open a - community (members, blog and comments) and leave a community. + community (members, blog and comments), create a new community and leave a community. The Egeria work is done by the app's CommunitiesMixin (list_my_communities, - search_communities, join_community, leave_community); this screen only drives it. + search_communities, create_community, join_community, leave_community); this + screen only drives it. """ BINDINGS = [ @@ -82,6 +152,7 @@ class CommunitiesScreen(ModalScreen): ("q", "quit", "Main Menu"), ("ctrl+o", "open_community", "Open Community"), ("ctrl+n", "join_community", "Join Community"), + ("ctrl+t", "create_community", "Create Community"), ("ctrl+d", "leave_community", "Leave Community"), ] @@ -89,8 +160,6 @@ class CommunitiesScreen(ModalScreen): def __init__(self, my_communities, *args, **kwargs): super().__init__(id="communities_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = "Communities" # Rows of (name, assignment type, linked through, description, GUID) self.my_communities_data = list(my_communities or []) # The last search, repeated after joining or leaving so the found list stays current @@ -101,10 +170,11 @@ def compose(self) -> ComposeResult: yield Static("Communities", classes="span-3", id="communities_title") yield ScrollableContainer( Static("The communities you belong to. Enter or ctrl+o opens one (members, blog and comments); " - "ctrl+d leaves."), + "ctrl+d leaves; ctrl+t creates a new community."), DataTable(id="my_communities_table", zebra_stripes=True, cursor_type="row"), Horizontal( Button("Open Community", id="open_my_community_btn", variant="primary"), + Button("Create Community", id="create_community_btn", variant="primary"), Button("Leave Community", id="leave_community_btn", variant="error"), id="my_communities_buttons", ), @@ -126,6 +196,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, "Communities") self.query_one("#my_communities_table", DataTable).add_columns( "Name", "Assignment Type", "Linked Through", "Description", "GUID") self.query_one("#communities_found_table", DataTable).add_columns( @@ -245,6 +316,32 @@ def action_join_community(self) -> None: if self.app.join_community(row[3], assignment_type): self._reload() + # --- creating ------------------------------------------------------------- + + @on(Button.Pressed, "#create_community_btn") + def handle_create_button(self, event: Button.Pressed) -> None: + self.action_create_community() + + def action_create_community(self) -> None: + """ The user creates a new community, joining it unless they chose not to """ + def on_details(details: dict | None) -> None: + if not details: + return + community_guid = self.app.create_community(details["name"], details["description"], + details["mission"], details["assignment_type"]) + if not community_guid: + return + self._reload() + # Highlight the new community in whichever list it now appears in + for table_id in ("#my_communities_table", "#communities_found_table"): + table = self.query_one(table_id, DataTable) + if community_guid in table.rows: + table.move_cursor(row=table.get_row_index(community_guid)) + table.focus() + break + + self.app.push_screen(CreateCommunityScreen(), callback=on_details) + # --- leaving ------------------------------------------------------------- @on(Button.Pressed, "#leave_community_btn") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CommunityScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CommunityScreen.py index 6dc8f679..27e476d5 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CommunityScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CommunityScreen.py @@ -14,6 +14,7 @@ from textual.widgets import DataTable, Header, Static, Footer, Input, Button, Select, TextArea from pyegeria import COMMENT_TYPES +from profile_utils import set_screen_heading def comment_type_label(comment_type: str) -> str: @@ -56,8 +57,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = "New Blog Post - Communities" + set_screen_heading(self, "Communities - New Blog Post") self.query_one("#blog_post_title_input", Input).focus() def action_submit(self) -> None: @@ -116,8 +116,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = "Add Comment - Communities" + set_screen_heading(self, "Communities - Add Comment") self.query_one("#community_comment_text", TextArea).focus() def action_submit(self) -> None: @@ -159,8 +158,6 @@ class CommunityScreen(ModalScreen): def __init__(self, community_guid: str, community_name: str, *args, **kwargs): super().__init__(id="community_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = f"Community - {community_name}" self.community_guid = community_guid self.community_name = community_name # Full text of each blog post, by GUID, shown below the posts table @@ -177,7 +174,7 @@ def compose(self) -> ComposeResult: id="community_blog_container", ) yield ScrollableContainer( - Static("[b]Members[/b] [dim](an actor may be a person or a role)[/dim]"), + Static("[b]Members[/b] [dim](people linked to the community, directly or through a role they hold)[/dim]"), DataTable(id="community_members_table", zebra_stripes=True, cursor_type="row"), id="community_members_container", ) @@ -190,9 +187,10 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, f"Community - {self.community_name}") self.query_one("#community_posts_table", DataTable).add_columns("Title", "Author", "Posted", "GUID") self.query_one("#community_members_table", DataTable).add_columns( - "Name", "Actor Type", "Assignment Type", "Qualified Name", "GUID") + "Name", "Assignment Type", "Member Through", "Qualified Name", "GUID") self.query_one("#community_comments_table", DataTable).add_columns( "Comment", "Type", "Author", "Posted", "GUID") self._load_members() diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py index 32f9b82d..cd416249 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ConnectionErrorScreen.py @@ -14,7 +14,7 @@ from textual.widgets import Header, Footer, Button, Label, Static from pyegeria import load_app_config, settings -from profile_utils import check_egeria_access +from profile_utils import check_egeria_access, set_screen_heading # Seconds between automatic reconnection attempts RETRY_INTERVAL = 10 @@ -38,8 +38,6 @@ class ConnectionErrorScreen(ModalScreen): def __init__(self, error_message: str, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.title = "Egeria" - self.sub_title = "Unable to connect to Egeria" self.error_message = error_message self.attempts = 0 self.countdown = RETRY_INTERVAL @@ -72,6 +70,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, "Unable to connect to Egeria") self.retry_timer = self.set_interval(1, self._tick) def _status_text(self) -> str: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py index 177942b0..dbb04339 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py @@ -12,6 +12,7 @@ from textual.containers import ScrollableContainer from textual.screen import ModalScreen from textual.widgets import Header, Static, Input, Button, Footer +from profile_utils import set_screen_heading class CreateProfileScreen(ModalScreen[int]): @@ -41,8 +42,7 @@ def __init__(self, user: str | None = None, password: str | None = None, view_se print("View Server:", self.view_server) def on_mount(self) -> None: - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Create Egeria Profile for user: {self.user_name}" + set_screen_heading(self, "Create Profile") def compose(self) -> ComposeResult: yield Header(show_clock=True) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py index 555908ad..ece085a2 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py @@ -11,6 +11,7 @@ from textual.containers import ScrollableContainer from textual.screen import ModalScreen from textual.widgets import Header, Static, Input, Footer +from profile_utils import set_screen_heading class CreateSubscriptionRequestScreen(ModalScreen): @@ -60,8 +61,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Create Subscription Request" - self.subtitle = "Enter details for the subscription request" + set_screen_heading(self, "Create Subscription Request") def action_quit(self): # self.log("Quitting Create Subscription Request screen") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py index ee5fe3ac..4a10a113 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py @@ -15,6 +15,7 @@ from textual.widgets import Header, Footer, DataTable, Static, Placeholder, Input, Button from pyegeria import PyegeriaException, Egeria, load_app_config, settings +from profile_utils import set_screen_heading class ConfirmDeleteScreen(ModalScreen[bool]): @@ -51,8 +52,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = f"Confirm Deletion{f' - {self.context}' if self.context else ''}" + set_screen_heading(self, f"Confirm Deletion{f' - {self.context}' if self.context else ''}") @on(Button.Pressed, "#confirm_delete_btn") def handle_confirm_button(self, event: Button.Pressed) -> None: @@ -129,8 +129,7 @@ def compose(self) -> ComposeResult: yield Footer() async def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = self.TITLE_TEXT + set_screen_heading(self, self.TITLE_TEXT) if (not self.columns or not self.rows_with_keys) and self.SOURCE_TABLE: try: @@ -399,8 +398,7 @@ def on_mount(self) -> None: self.user_GUID = getattr(self.app, "user_GUID", "") if self.karma_points == 0 and hasattr(self, "app"): self.karma_points = getattr(self.app, "karma_points", 0) - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Edit Egeria Profile for user: {self.user_name}" + set_screen_heading(self, "Edit Profile") # retrieve person profile details def compose(self) -> ComposeResult: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py index f0074a54..df10401f 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py @@ -14,6 +14,7 @@ from textual.containers import ScrollableContainer, Horizontal from textual.screen import ModalScreen from textual.widgets import Header, Static, Footer, Button, DataTable, Select, TextArea +from profile_utils import set_screen_heading FEEDBACK_CATEGORIES = ["Bug", "Suggestion", "Question", "Praise", "Other"] @@ -35,8 +36,7 @@ def __init__(self, screen_name: str, screen_title: str = "", *args, **kwargs): self.category = FEEDBACK_CATEGORIES[1] def on_mount(self): - self.title = "Egeria - My Profile" - self.sub_title = "Leave Feedback" + set_screen_heading(self, "Leave Feedback") self.query_one("#feedback_text", TextArea).focus() def compose(self) -> ComposeResult: @@ -109,8 +109,7 @@ def __init__(self, feedback_entries: list[dict[str, Any]], *args, **kwargs): self.feedback_entries = feedback_entries or [] def on_mount(self): - self.title = "Egeria - My Profile" - self.sub_title = f"Feedback Log ({len(self.feedback_entries)} entries)" + set_screen_heading(self, f"Feedback Log ({len(self.feedback_entries)} entries)") table = self.query_one("#feedback_log_table", DataTable) table.add_columns("Date", "Submitted By", "Category", "Screen", "Feedback") table.zebra_stripes = True diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py index 453220c4..edd38215 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py @@ -15,6 +15,7 @@ from textual.screen import ModalScreen from textual.widgets import DataTable, Header, Footer, Static, Label, Button from pyegeria import load_app_config, settings +from profile_utils import set_screen_heading class GenericDataViewScreen(ModalScreen): @@ -80,8 +81,6 @@ def __init__( def compose(self) -> ComposeResult: """Compose UI components for the Generic Data View screen.""" - self.title = "Shop for Data - Sample Data View" - self.sub_title = self.data_element_name yield Header(show_clock=True) yield Static( @@ -101,6 +100,7 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: """Populate the data table with Key and Value rows up to max_rows.""" + set_screen_heading(self, f"Shop for Data - Sample Data View: {self.data_element_name}") self.data_table.cursor_type = "row" self.data_table.zebra_stripes = True self.data_table.add_columns("Key", "Value") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index da006a9e..717c298f 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -14,6 +14,7 @@ from textual.widgets._option_list import Option from pyegeria import load_app_config, settings +from profile_utils import set_screen_heading class MainScreen(Screen): @@ -39,11 +40,11 @@ def __init__(self, *args, **kwargs): load_app_config() app_user = settings.User_Profile self.user_name = app_user.user_name or "garygeeke" - self.sub_title = f"User: {self.user_name}" def on_mount(self) -> None: + # The app sets the heading again once the profile, and so the karma points, have loaded + set_screen_heading(self) # self.log needs a running app, so it can't be called from __init__ - pass # self.log(f"Application Config: {settings.Environment}") # self.log(f"User Profile: {settings.User_Profile}") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py index c29ea38d..542b78cd 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py @@ -10,6 +10,7 @@ from textual.containers import ScrollableContainer, Container, Horizontal from textual.screen import ModalScreen from textual.widgets import DataTable, Header, Static, Footer, Input, Button +from profile_utils import set_screen_heading class MyBookMarksScreen(ModalScreen): @@ -30,8 +31,6 @@ class MyBookMarksScreen(ModalScreen): def __init__(self, my_bookmarks, *args, target_guid: str | None = None, **kwargs): super().__init__(id="bookmark_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = "My Bookmarks" # Rows of (name, type, GUID) self.my_bookmarks_data = list(my_bookmarks or []) # GUID of the row highlighted when the screen was opened, if any, used to pre-fill a new bookmark @@ -48,6 +47,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, "My Bookmarks") table = self.query_one("#my_bookmark_table", DataTable) table.add_columns("Name", "Type", "GUID") self._fill_table() diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py index cf6c02eb..1e9f1e12 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py @@ -10,6 +10,7 @@ from textual.containers import ScrollableContainer, Container, Horizontal from textual.screen import ModalScreen from textual.widgets import DataTable, Header, Static, Footer, Input, Button +from profile_utils import set_screen_heading class ConfirmRemovePeerScreen(ModalScreen[bool]): @@ -41,8 +42,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: - self.title = "Egeria - my_profile" - self.sub_title = "Confirm Removal - My Network" + set_screen_heading(self, "My Network - Confirm Removal") @on(Button.Pressed, "#confirm_remove_peer_btn") def handle_confirm_button(self, event: Button.Pressed) -> None: @@ -74,8 +74,6 @@ class MyNetworkScreen(ModalScreen): def __init__(self, my_peers, *args, **kwargs): super().__init__(id="my_network_screen", *args, **kwargs) - self.title = "Egeria" - self.sub_title = "My Network" # Rows of (qualified name, display name, full name, pronouns, GUID, description) self.my_peers_data = list(my_peers or []) # (display name, GUID) of the person picked from the search results to add @@ -114,6 +112,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self) -> None: + set_screen_heading(self, "My Network") self.query_one("#my_network_table", DataTable).add_columns( "Qualified Name", "Display Name", "Full Name", "Pronouns", "GUID", "Description") self.query_one("#people_found_table", DataTable).add_columns("Name", "GUID", "Qualified Name") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py index 7f7aaa8e..9dc42203 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py @@ -12,6 +12,7 @@ from textual.widgets import Header, Footer, Static, DataTable from pyegeria import Egeria, load_app_config, settings +from profile_utils import set_screen_heading class MyTeam(ModalScreen): @@ -34,7 +35,6 @@ def __init__( super().__init__(*args, **kwargs) load_app_config() app_user = settings.User_Profile - self.title = "My Team" self.my_team = my_team or [] props = list(my_team_properties) if my_team_properties is not None else ["", "", "", ""] while len(props) < 4: @@ -47,6 +47,7 @@ def __init__( self.team_table: DataTable = DataTable(id="team_table") def on_mount(self) -> None: + set_screen_heading(self, "My Team") if self.my_team: if "Selection Error" in self.my_team[0][0]: self.team_table.add_column("Error") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py index 693a4617..8aef61b1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py @@ -13,6 +13,7 @@ from textual.screen import ModalScreen from textual.widgets import Header, Footer, Static, Input, Placeholder, Button, Markdown from pyegeria import load_app_config, settings, PyegeriaException, exec_report_spec +from profile_utils import set_screen_heading class SearchForTermScreen(ModalScreen): @@ -42,8 +43,7 @@ def __init__( self.platform_url = platform_url or app_config.egeria_platform_url or "https://127.0.0.1:9443" def on_mount(self): - self.title = "Egeria" - self.sub_title = "Search for Term" + set_screen_heading(self, "Search for Term") def compose(self) -> ComposeResult: yield Header(show_clock=True) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py index db475d94..f0e6dd97 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py @@ -17,6 +17,7 @@ from textual.widgets import Tree, Header, Static, Placeholder, Footer, TextArea, Markdown from StatusScreen import StatusScreen +from profile_utils import set_screen_heading class SelectionOverviewScreen(ModalScreen): @@ -69,10 +70,11 @@ def __init__( if self.data_tree is None: self.data_tree = Tree(f"{self.category.capitalize()} Details") + def on_mount(self) -> None: + set_screen_heading(self, f"Shop for Data - Category: {self.category}") + def compose(self) -> ComposeResult: """ Compose the UI components for the SelectionOverviewScreen screen.""" - self.title = f"Shopping for Data, Data Selection:" - self.sub_title = f"Category: {self.category}" yield Header(show_clock=True) yield Static("Please select an item from the tree [blink]:[/]", id="instruction_static") yield ScrollableContainer( diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py index 94e47b91..3d5644e4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py @@ -15,6 +15,7 @@ from textual.events import Action from textual.screen import Screen from textual.widgets import DataTable, Header, Static, Footer +from profile_utils import set_screen_heading class ShopForDataScreen(Screen): @@ -107,6 +108,7 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: + set_screen_heading(self, "Shop for Data") self.header = f"Egeria Data Sources for user {self.user_name}" self.sub_header = "Shop for Data" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py index b7174353..0eddd851 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py @@ -15,6 +15,7 @@ from textual.widgets import DataTable, Header, Static, Footer, Input, Button from pyegeria import PyegeriaException, EgeriaTech, exec_report_spec, load_app_config, settings +from profile_utils import set_screen_heading class ShowCommentsScreen(ModalScreen): @@ -62,8 +63,7 @@ def __init__( def on_mount(self): """On mount, find the GUID for the Row """ - self.title = "Egeria - My Profile" - self.sub_title = "Show Comments" + set_screen_heading(self, "Show Comments") self.dt_row_selected = None self.show_comments_datatable.add_columns("Comment", "GUID", "Qualified Name", "Name") if self.selected_comment_guid: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py index 32a2de56..3c07606b 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SplashScreen.py @@ -15,6 +15,7 @@ from pyegeria import load_app_config, settings from ChangeUserScreen import ChangeUserScreen +from profile_utils import set_screen_heading class SplashScreen(ModalScreen): @@ -27,8 +28,6 @@ class SplashScreen(ModalScreen): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.title = "Egeria" - self.subtitle = "Welcome to my_egeria" load_app_config() app_config = settings.Environment app_user = settings.User_Profile @@ -37,6 +36,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.view_server = app_config.egeria_view_server self.platform_url = app_config.egeria_platform_url + def on_mount(self) -> None: + set_screen_heading(self, "Welcome to my_egeria") + def compose(self) -> ComposeResult: yield Header(show_clock=True,id="splash") # 1. Define multi-line layout with line breaks diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py index 7ddfebc6..2f07912a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py @@ -14,6 +14,7 @@ from textual.containers import ScrollableContainer from textual.screen import ModalScreen from textual.widgets import Header, TextArea, Footer, Static +from profile_utils import set_screen_heading class StatusScreen(ModalScreen): @@ -32,6 +33,9 @@ def __init__(self, status_message: str) -> None: super().__init__() self.status_message = status_message + def on_mount(self) -> None: + set_screen_heading(self, "Status") + def compose(self) -> ComposeResult: """ Compose the UI components for the StatusScreen screen.""" yield Header(show_clock=True) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py index f525b50e..09b0e5da 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py @@ -17,6 +17,7 @@ from textual.screen import ModalScreen from textual.widgets import Header, Static, OptionList, Button, Footer, Input, Tree, TextArea from textual.widgets._option_list import Option +from profile_utils import set_screen_heading class TechnologyTypeOptionsScreen(ModalScreen): @@ -88,8 +89,7 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> int: """Mount the TechnologyTypeOptions screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" + set_screen_heading(self, f"Technology Type: {self.tech_type_name}") # self.log(f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}") for widget in self.query(): @@ -296,8 +296,7 @@ def __init__( async def on_mount(self) -> None: """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" + set_screen_heading(self, f"Technology Type Processes: {self.tech_type_name}") if self.selected_t_option == "process": # self.log(f"Processing processes, with data: {self.selected_t_option_selected}") @@ -438,8 +437,7 @@ def __init__( self.node_status = "expanded" def on_mount(self) -> None: - self.title = f"User: {self.user_name}" - self.sub_title = "Select a technology type" + set_screen_heading(self, "Technology Types") def compose(self) -> ComposeResult: """ Compose and display the technology type screen""" @@ -574,8 +572,7 @@ def __init__( async def on_mount(self) -> None: """ On Mount function of the Technology_Type_Templatess screen.""" - self.title = f"User: {self.user_name}, Karma Points: {self.karma_points}" - self.sub_title = f"Technology Type: {self.tech_type_name}, Description: {self.tech_type_description}" + set_screen_heading(self, f"Technology Type Templates: {self.tech_type_name}") if self.selected_t_option == "template": # self.log(f"Processing templates, with data: {self.selected_t_option_selected}") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py index ea905418..88bffe6b 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py @@ -13,6 +13,7 @@ from typing import Any from pyegeria import load_app_config, settings +from profile_utils import set_screen_heading class UserIdentitiesScreen(ModalScreen): @@ -47,8 +48,7 @@ def on_mount(self) -> None: self.user_identities = getattr(self.app, "user_identities", None) if self.karma_points == 0 and hasattr(self, "app"): self.karma_points = getattr(self.app, "karma_points", 0) - self.title = "My_Egeria User Identities Screen" - self.sub_title = f"User Identities for {self.user_name}, Karma Points: {self.karma_points}" + set_screen_heading(self, "User Identities") # self.log(f"UserIdentitiesScreen mounted for user: {self.user_name}, Karma Points: {self.karma_points}") # self.log(f"User identity input: {self.user_identities}") try: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md index 0324922e..6c1e4d98 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md @@ -41,7 +41,7 @@ ## 3. The main dashboard (1:00 – 1:45) -**[VISUAL]** Main screen. Slowly pan or highlight each panel as it's mentioned. The header shows "Egeria — My Profile" and the clock. +**[VISUAL]** Main screen. Slowly pan or highlight each panel as it's mentioned. The header shows "Egeria — User: calliequartile, Karma Points: …" and the clock; every screen has the same header. **CALLIE:** > This is my home screen. Everything here comes live from Egeria. diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py index ed8784f1..5dd2fdf6 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py @@ -12,7 +12,7 @@ from textual.widgets import DataTable, Header, Static, Footer from pyegeria import Egeria, PyegeriaException, load_app_config, settings, print_basic_exception -from profile_utils import element_summary +from profile_utils import element_summary, set_screen_heading class ViewSubscriptionsScreen(ModalScreen): @@ -45,6 +45,7 @@ def compose(self) -> ComposeResult: yield Footer() def on_mount(self): + set_screen_heading(self, "My Subscriptions") table = self.query_one("#subscriptions_table", DataTable) table.add_columns("Name", "Status", "Description", "GUID") for row in self.get_my_subscriptions(): diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py index d8aa1dfb..d276a5bf 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/community_handler.py @@ -9,6 +9,8 @@ (https://egeria-project.org/types/1/0120-Assignment-Scopes/). The actor is either the person's own profile, or a role they are appointed to. + Creating a community makes it with create_community and, unless the user + chooses not to join, links their profile to it (as its Leader by default). Joining links the user's profile to the community with link_assignment_scope, and leaving removes that link with detach_assignment_scope. Memberships held through a role are listed but not removed here: the role may be shared, and @@ -35,10 +37,16 @@ sys.path.append(str(current_dir)) from pyegeria import Egeria, PyegeriaException -from CommunitiesScreen import CommunitiesScreen, DIRECT_LINK, JOIN_ASSIGNMENT_TYPES +from CommunitiesScreen import CommunitiesScreen, DIRECT_LINK, JOIN_ASSIGNMENT_TYPES, CREATOR_ASSIGNMENT_TYPES from CommunityScreen import comment_type_label COMMUNITY_TYPE = "Community" +PERSON_TYPE = "Person" +ROLE_TYPE = "ActorRole" +# The relationship between a role and the people appointed to it +APPOINTMENT_TYPE = "PersonRoleAppointment" +# The "Member Through" value of a person linked to a community directly, rather than through a role +MEMBER_DIRECT = "Direct" BLOG_LOG_QN = "NoteLog::{community_guid}::CommunityBlog" # How many of a community's most recent blog posts are listed LATEST_POSTS = 20 @@ -48,12 +56,20 @@ def _type_name(element: dict) -> str: return ((element.get("elementHeader") or {}).get("type") or {}).get("typeName") or "" +def _is_role(element: dict) -> bool: + """True for an ActorRole, or any of its subtypes (PersonRole, GovernanceRole, ...).""" + element_type = (element.get("elementHeader") or {}).get("type") or {} + return ROLE_TYPE == element_type.get("typeName") or ROLE_TYPE in (element_type.get("superTypeNames") or []) + + def _guid(element: dict) -> str: return (element.get("elementHeader") or {}).get("guid") or "" def _name(properties: dict) -> str: - return properties.get("displayName") or properties.get("fullName") or properties.get("qualifiedName") or "" + # Some roles have only an identifier, such as "Community Leader of Data Science special interest group" + return (properties.get("displayName") or properties.get("fullName") or properties.get("identifier") + or properties.get("qualifiedName") or "") def _created(element: dict) -> tuple[str, str]: @@ -162,12 +178,38 @@ def search_communities(self, search_string: str) -> list[tuple[str, str, str, st return rows def get_community_members(self, community_guid: str) -> list[tuple[str, str, str, str, str]] | None: - """The actors linked to a community as (name, actor type, assignment type, qualified name, GUID) rows, - or None if they could not be retrieved. An actor may be a person or a role.""" + """The people in a community as (name, assignment type, member through, qualified name, GUID) rows, + or None if they could not be retrieved. + + A person is in the community when they are linked to it directly, or appointed to a role + that is; roles themselves aren't listed. Other actors (such as teams) are left out. A person + reached through several links is one row.""" client = None try: client = self._community_client() community = client.get_community_by_guid(community_guid, output_format="JSON", graph_query_depth=1) + # (person, assignment type, member through) for each way a person belongs + memberships = [] + unread_roles = 0 + for link in (community.get("assignedActors") if isinstance(community, dict) else None) or []: + actor = link.get("relatedElement") or {} + assignment_type = (link.get("relationshipProperties") or {}).get("assignmentType") or "" + if _type_name(actor) == PERSON_TYPE: + memberships.append((actor, assignment_type, MEMBER_DIRECT)) + elif _is_role(actor): + role_through = f"Role: {_name(actor.get('properties') or {})}" + try: + # Only the appointments are needed; a role can have hundreds of other links + role = client.get_actor_role_by_guid(_guid(actor), output_format="JSON", graph_query_depth=1, + include_only_relationships=[APPOINTMENT_TYPE]) + except PyegeriaException as e: + # self.log(f"Retrieving the people in role {_guid(actor)} failed: {e}") + unread_roles += 1 + continue + for appointment in (role.get("rolePerformers") if isinstance(role, dict) else None) or []: + person = appointment.get("relatedElement") or {} + if _type_name(person) == PERSON_TYPE: + memberships.append((person, assignment_type, role_through)) except PyegeriaException as e: # self.log(f"Retrieving members of community {community_guid} failed: {e}") self.notify(f"Could not retrieve the community's members: {e}", timeout=10, severity="error") @@ -175,15 +217,25 @@ def get_community_members(self, community_guid: str) -> list[tuple[str, str, str finally: if client: client.close_session() - # self.log(f"Community {community_guid}: {community}") - rows = [] - for link in (community.get("assignedActors") if isinstance(community, dict) else None) or []: - actor = link.get("relatedElement") or {} - properties = actor.get("properties") or {} - rows.append((_name(properties), _type_name(actor), - (link.get("relationshipProperties") or {}).get("assignmentType") or "", - properties.get("qualifiedName") or "", _guid(actor))) - return rows + if unread_roles: + self.notify(f"Could not see who holds {unread_roles} of the community's roles, so some members " + f"may be missing", timeout=10, severity="warning") + + people: dict[str, dict] = {} + for person, assignment_type, through in memberships: + guid = _guid(person) + if not guid: + continue + properties = person.get("properties") or {} + entry = people.setdefault(guid, {"name": _name(properties), + "qualified_name": properties.get("qualifiedName") or "", + "types": [], "through": []}) + if assignment_type and assignment_type not in entry["types"]: + entry["types"].append(assignment_type) + if through not in entry["through"]: + entry["through"].append(through) + return sorted(((p["name"], ", ".join(p["types"]), "; ".join(p["through"]), p["qualified_name"], guid) + for guid, p in people.items()), key=lambda row: row[0].lower()) def get_community_blog_posts(self, community_guid: str) -> list[tuple[str, str, str, str, str]] | None: """The community's latest blog posts, newest first, as (title, author, posted, text, GUID) rows, @@ -303,6 +355,57 @@ def add_community_comment(self, community_guid: str, text: str, comment_type: st if client: client.close_session() + def create_community(self, name: str, description: str, mission: str = "", + assignment_type: str = CREATOR_ASSIGNMENT_TYPES[0]) -> str | None: + """Create a community, then link the user's profile to it with assignment_type (none if empty). + Returns the new community's GUID, or None if it could not be created; a failed link still + returns the GUID, because the community exists.""" + properties = { + "class": "CommunityProperties", + "typeName": COMMUNITY_TYPE, + "displayName": name, + "description": description, + } + if mission: + properties["mission"] = mission + client = None + try: + client = self._community_client() + qualified_name = client.__create_qualified_name__(COMMUNITY_TYPE, name) + # Egeria rejects a second element with the same qualified name, but only with a server error + existing = client.get_communities_by_name(filter_string=qualified_name, graph_query_depth=0, + output_format="JSON") + if isinstance(existing, list) and existing: + self.notify(f"A community called {name} already exists - find it below to join it", + timeout=10, severity="warning") + return None + properties["qualifiedName"] = qualified_name + community_guid = client.create_community(body={"class": "NewElementRequestBody", "isOwnAnchor": True, + "properties": properties}) + if not assignment_type: + self.notify(f"Community {name} created", timeout=10, severity="information") + return community_guid + try: + # same GUID order as join_community: the community is the scope, the profile the actor + client.link_assignment_scope(community_guid, self.user_GUID, { + "class": "NewRelationshipRequestBody", + "properties": {"class": "AssignmentScopeProperties", "assignmentType": assignment_type}, + }) + except PyegeriaException as e: + # self.log(f"Joining new community {community_guid} failed: {e}") + self.notify(f"Community {name} created, but joining it failed: {e}", timeout=10, severity="warning") + return community_guid + self.notify(f"Community {name} created, and you have joined it as its {assignment_type}", + timeout=10, severity="information") + return community_guid + except PyegeriaException as e: + # self.log(f"Creating community {name} failed: {e}") + self.notify(f"Creating the community failed: {e}", timeout=10, severity="error") + return None + finally: + if client: + client.close_session() + def join_community(self, community_guid: str, assignment_type: str = JOIN_ASSIGNMENT_TYPES[0]) -> bool: """Link the user's profile to the community. Returns True on success.""" body = { diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py index c8ca6263..1ec41a1e 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/feedback_handler.py @@ -49,7 +49,8 @@ async def action_feedback(self) -> None: if isinstance(current, (FeedbackScreen, FeedbackLogScreen)): return screen_name = type(current).__name__ - screen_title = str(getattr(current, "sub_title", "") or "") + # The title names the screen; every sub_title is just "User: " + screen_title = str(getattr(current, "title", "") or "") await self.push_screen(FeedbackScreen(screen_name, screen_title), callback=self.feedback_callback) async def feedback_callback(self, feedback: Any) -> None: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py index 4f0d11e1..58aa0280 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py @@ -85,6 +85,7 @@ check_request_serialization, check_egeria_access, element_summary, + set_screen_heading, ) from tech_types_handler import TechTypesMixin from shop_for_data_handler import ShopForDataMixin @@ -264,7 +265,9 @@ async def change_user(self, user_name: str, user_password: str) -> None: # ...and pyegeria clients, commands and subprocesses started from this app read the environment os.environ["EGERIA_USER"] = user_name os.environ["EGERIA_USER_PASSWORD"] = user_password - self.get_screen("main").sub_title = f"User: {user_name}" + # The old user's karma points no longer apply; the new user's arrive with their profile + self.karma_points = 0 + set_screen_heading(self.get_screen("main"), app=self) self.my_profile_data = None self.refresh_bindings() await self._load_or_create_profile() @@ -363,6 +366,7 @@ def _process_profile_data(self, profile_struct: list[dict]) -> None: self.karma_points = self.contribution_record.get("Karma Points") or 0 else: self.karma_points = 0 + set_screen_heading(self.get_screen("main"), app=self) self.my_projects_data = self.user_profile.get("Projects") or [] self.my_teams_data = self.user_profile.get("Teams") or [] self.my_communities_data = self.user_profile.get("Communities") or [] diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss index ef898b66..4e51e961 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile.tcss @@ -130,7 +130,7 @@ CommunitiesScreen { height: auto; } -#join_assignment_type { +#join_assignment_type, #creator_assignment_type { width: 20; } @@ -178,15 +178,15 @@ NewBlogPostScreen, AddCommunityCommentScreen { background: $background; } -#new_blog_post_container, #add_community_comment_container { +#new_blog_post_container, #add_community_comment_container, #create_community_container { column-span: 3; } -#new_blog_post_container Horizontal, #add_community_comment_container Horizontal { +#new_blog_post_container Horizontal, #add_community_comment_container Horizontal, #create_community_container Horizontal { height: auto; } -#blog_post_text, #community_comment_text { +#blog_post_text, #community_comment_text, #community_mission_text { height: 10; } @@ -194,7 +194,7 @@ ModalScreen > Header, ModalScreen > Footer { column-span: 3; } -#communities_title, #community_title, #new_blog_post_title, #add_community_comment_title, #confirm_leave_community_title, #my_network_title, #confirm_remove_peer_title, #edit_profile_title, #create_profile_title, #edit_teams_title, #edit_roles_title, #edit_communities_title, #edit_identities_title, #status_title, #generic_data_view_title, #feedback_title, #feedback_log_title, .span-3 { +#communities_title, #community_title, #new_blog_post_title, #add_community_comment_title, #create_community_title, #confirm_leave_community_title, #my_network_title, #confirm_remove_peer_title, #edit_profile_title, #create_profile_title, #edit_teams_title, #edit_roles_title, #edit_communities_title, #edit_identities_title, #status_title, #generic_data_view_title, #feedback_title, #feedback_log_title, .span-3 { column-span: 3; text-align: center; background: darkblue; diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py b/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py index d36bc8e8..a1f5fa81 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/profile_utils.py @@ -187,3 +187,20 @@ def row_identity(table: Any, row_key: Any) -> tuple[str, str]: elif not qualified_name and label == "Qualified Name": qualified_name = str(values[index] or "").strip() return guid, qualified_name + + +def set_screen_heading(screen: Any, context: str = "", app: Any = None) -> None: + """Give a screen the same header as the main screen: "Egeria - ", + sub-titled "User: , Karma Points: ". + + The user and karma points are the app's (the points are 0 until the profile has + loaded). A screen calls this from on_mount, where screen.app is the running app; + the app itself passes app=self. + """ + from pyegeria import settings + + app = app or screen.app + screen.title = f"Egeria - {context}" if context else "Egeria" + user_name = getattr(app, "user_name", None) or settings.User_Profile.user_name + karma_points = getattr(app, "karma_points", None) or 0 + screen.sub_title = f"User: {user_name}, Karma Points: {karma_points}" diff --git a/tests/micro-tests/my_profile/README.md b/tests/micro-tests/my_profile/README.md index 58326f4e..0d83d505 100644 --- a/tests/micro-tests/my_profile/README.md +++ b/tests/micro-tests/my_profile/README.md @@ -112,7 +112,16 @@ suitable data: `live_my_profile`, `live_team_role_name`, `live_tech_type_name`, 5. If the path is an injected error or an empty result, use `backend.always_fake(...)` and leave the `live_capable` marker off. 6. If the test enters `MyProfileApp.run_test()`, call `stub_profile_client(...)` - so `on_mount`'s profile load is mocked too. + so `on_mount`'s profile load is mocked too, and `await app._load_task` before + checking anything the load sets. A live load takes several seconds, and a + single `pilot.pause()` sees it only part done. The SDK's sync calls run nested + event loops, so the test can see, for example, the Karma Points set but not + yet the user GUID or the roles. +7. Don't make a live check depend on data a real user might not have. A user + needn't hold any roles, for example on a new system, so check + `at_least(0)` rather than `at_least(1)`. If a test genuinely needs such data, + get it from a live-discovery fixture that skips when it's missing, as + `live_team_role_name` does. The network guard will tell you if you miss one. diff --git a/tests/micro-tests/my_profile/test_community_handler.py b/tests/micro-tests/my_profile/test_community_handler.py index fc3c4e4d..045f0b2f 100644 --- a/tests/micro-tests/my_profile/test_community_handler.py +++ b/tests/micro-tests/my_profile/test_community_handler.py @@ -15,7 +15,8 @@ import community_handler from community_handler import CommunitiesMixin -from CommunitiesScreen import CommunitiesScreen, ConfirmLeaveCommunityScreen, DIRECT_LINK +from CommunitiesScreen import (CommunitiesScreen, ConfirmLeaveCommunityScreen, CreateCommunityScreen, DIRECT_LINK, + NOT_JOINING) from CommunityScreen import CommunityScreen, NewBlogPostScreen, AddCommunityCommentScreen from pyegeria import PyegeriaException @@ -37,6 +38,21 @@ def _scope(element, assignment_type): "relatedElement": element} +# The people appointed to each role, as get_actor_role_by_guid returns them, by role GUID +ROLE_PERFORMERS: dict[str, dict] = {} + + +def _role(guid, type_name, display_name, people): + """A role actor (whose type header lists its supertypes) appointed to the given people.""" + role = _element(guid, type_name, display_name) + supertypes = ["ActorRole", "Actor", "Referenceable", "OpenMetadataRoot"] + if type_name == "GovernanceRole": + supertypes.insert(0, "PersonRole") + role["elementHeader"]["type"]["superTypeNames"] = supertypes + ROLE_PERFORMERS[guid] = {**role, "rolePerformers": [{"relatedElement": person} for person in people]} + return role + + DATA_SCI = _element("ds-guid", "Community", "Data Science SIG", "Data science") SUSTAIN = _element("sus-guid", "Community", "Sustainability Community", "Green IT") @@ -80,6 +96,9 @@ def push_screen(self, screen, callback=None): @pytest.fixture def client(): with patch("community_handler.Egeria") as mock_egeria: + # MagicMock doesn't create dunder-named attributes on demand + mock_egeria.return_value.__create_qualified_name__ = MagicMock( + side_effect=lambda type_name, name: f"{type_name}::{name.replace(' ', '-')}") yield mock_egeria.return_value @@ -137,14 +156,37 @@ def test_search_error_returns_none(self, client): client.find_communities.side_effect = PyegeriaException("boom") assert DummyApp().search_communities("sig") is None - def test_members_are_people_and_roles(self, client): + def test_members_are_people_direct_and_through_roles(self, client): + gary, erin = _element("gary-guid", "Person", "Gary"), _element("erin-guid", "Person", "Erin") + lead_role = _role("lead-role", "PersonRole", "SIG Leader", [erin, gary]) + champ_role = _role("champ-role", "GovernanceRole", "SIG Champion", [erin]) client.get_community_by_guid.return_value = { - "assignedActors": [_scope(_element("gary-guid", "Person", "Gary"), "Contributor"), - _scope(_element("lead-role", "PersonRole", "SIG Leader"), "Leader")]} + "assignedActors": [_scope(gary, "Contributor"), + _scope(lead_role, "Leader"), + _scope(champ_role, "Contributor"), + _scope(_element("team-guid", "Team", "A team"), "Observer")]} + client.get_actor_role_by_guid.side_effect = lambda guid, **kwargs: ROLE_PERFORMERS[guid] rows = DummyApp().get_community_members("ds-guid") assert client.get_community_by_guid.call_args.args == ("ds-guid",) - assert rows == [("Gary", "Person", "Contributor", "Person::gary-guid", "gary-guid"), - ("SIG Leader", "PersonRole", "Leader", "PersonRole::lead-role", "lead-role")] + # Only the role's appointments are fetched + assert client.get_actor_role_by_guid.call_args.kwargs["include_only_relationships"] == [ + "PersonRoleAppointment"] + # One row per person, sorted by name; roles and teams aren't listed + assert rows == [ + ("Erin", "Leader, Contributor", "Role: SIG Leader; Role: SIG Champion", "Person::erin-guid", "erin-guid"), + ("Gary", "Contributor, Leader", "Direct; Role: SIG Leader", "Person::gary-guid", "gary-guid"), + ] + client.close_session.assert_called_once() + + def test_unreadable_role_is_skipped_with_a_warning(self, client): + gary = _element("gary-guid", "Person", "Gary") + client.get_community_by_guid.return_value = { + "assignedActors": [_scope(gary, "Contributor"), + _scope(_role("lead-role", "PersonRole", "SIG Leader", []), "Leader")]} + client.get_actor_role_by_guid.side_effect = PyegeriaException("boom") + app = DummyApp() + assert [row[4] for row in app.get_community_members("ds-guid")] == ["gary-guid"] + assert app.notices[-1][1] == "warning" def test_members_error_returns_none(self, client): client.get_community_by_guid.side_effect = PyegeriaException("boom") @@ -165,6 +207,55 @@ def test_join_error(self, client): assert app.join_community("ds-guid") is False assert app.notices[-1][1] == "error" + def test_create_and_lead(self, client): + client.create_community.return_value = "new-guid" + app = DummyApp() + assert app.create_community("Data Lovers", "People who love data", "Love data") == "new-guid" + client.__create_qualified_name__.assert_called_once_with("Community", "Data Lovers") + body = client.create_community.call_args.kwargs["body"] + assert body["class"] == "NewElementRequestBody" + assert body["properties"] == {"class": "CommunityProperties", "typeName": "Community", + "qualifiedName": "Community::Data-Lovers", "displayName": "Data Lovers", + "description": "People who love data", "mission": "Love data"} + # The creator leads the new community by default + scope, actor, link_body = client.link_assignment_scope.call_args.args + assert (scope, actor) == ("new-guid", "profile-guid") + assert link_body["properties"]["assignmentType"] == "Leader" + assert app.notices[-1][1] == "information" + client.close_session.assert_called_once() + + def test_create_without_mission_or_joining(self, client): + client.create_community.return_value = "new-guid" + assert DummyApp().create_community("Data Lovers", "People who love data", "", "") == "new-guid" + assert "mission" not in client.create_community.call_args.kwargs["body"]["properties"] + client.link_assignment_scope.assert_not_called() + + def test_create_refuses_an_existing_name(self, client): + client.get_communities_by_name.return_value = [DATA_SCI] + app = DummyApp() + assert app.create_community("Data Science SIG", "Again") is None + assert client.get_communities_by_name.call_args.kwargs["filter_string"] == "Community::Data-Science-SIG" + client.create_community.assert_not_called() + assert app.notices[-1] == ("A community called Data Science SIG already exists - find it below to join it", + "warning") + client.close_session.assert_called_once() + + def test_create_error(self, client): + client.create_community.side_effect = PyegeriaException("boom") + app = DummyApp() + assert app.create_community("Data Lovers", "People who love data") is None + client.link_assignment_scope.assert_not_called() + assert app.notices[-1][1] == "error" + client.close_session.assert_called_once() + + def test_create_with_failed_join_still_returns_the_community(self, client): + client.create_community.return_value = "new-guid" + client.link_assignment_scope.side_effect = PyegeriaException("boom") + app = DummyApp() + assert app.create_community("Data Lovers", "People who love data") == "new-guid" + assert app.notices[-1][1] == "warning" + client.close_session.assert_called_once() + def test_leave(self, client): assert DummyApp().leave_community("ds-guid") is True client.detach_assignment_scope.assert_called_once_with( @@ -289,7 +380,7 @@ def __init__(self, rows): self.join_community = MagicMock(side_effect=self._join) self.leave_community = MagicMock(side_effect=self._leave) self.search_communities = MagicMock(return_value=list(ALL_COMMUNITIES)) - self.get_community_members = MagicMock(return_value=[("Gary", "Person", "Contributor", "Person::g", "g")]) + self.get_community_members = MagicMock(return_value=[("Gary", "Contributor", "Direct", "Person::g", "g")]) self.posts = [("Second", "erin", "2026-10-20 13:27", "Again", "p2"), ("First", "gary", "2026-10-01 09:00", "Hello", "p1")] self.comments = [("Why?", "Question", "gary", "2026-10-01 09:00", "c1")] @@ -297,8 +388,15 @@ def __init__(self, rows): self.get_community_comments = MagicMock(side_effect=lambda guid: list(self.comments)) self.add_community_blog_post = MagicMock(side_effect=self._post) self.add_community_comment = MagicMock(side_effect=self._comment) + self.create_community = MagicMock(side_effect=self._create) self.dismissed = None + def _create(self, name, description, mission, assignment_type): + self.search_communities.return_value.append((name, description, f"Community::{name}", "new-guid")) + if assignment_type: + self.rows.append((name, assignment_type, DIRECT_LINK, description, "new-guid")) + return "new-guid" + def _post(self, guid, name, title, text): self.posts.insert(0, (title, "garygeeke", "2026-10-28 12:00", text, "p-new")) return True @@ -458,6 +556,88 @@ async def test_leave_with_no_communities(self): await pilot.pause() assert app.screen is app.screen_under_test + @pytest.mark.asyncio + async def test_create_and_join(self): + app = CommunitiesHostApp(MY_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + screen.query_one("#create_community_btn", Button).press() + await pilot.pause() + assert isinstance(app.screen, CreateCommunityScreen) + # The creator leads the community unless they choose otherwise + assert app.screen.query_one("#creator_assignment_type", Select).value == "Leader" + app.screen.query_one("#community_name_input", Input).value = "Data Lovers" + app.screen.query_one("#community_description_input", Input).value = "People who love data" + app.screen.query_one("#community_mission_text", TextArea).text = "Love data" + await pilot.press("ctrl+s") + await pilot.pause() + assert app.screen is screen + app.create_community.assert_called_once_with("Data Lovers", "People who love data", "Love data", "Leader") + mine = screen.query_one("#my_communities_table", DataTable) + assert mine.row_count == 3 + # The new community is highlighted + assert screen.focused is mine + assert mine.get_row_at(mine.cursor_row)[4] == "new-guid" + + @pytest.mark.asyncio + async def test_create_without_joining(self): + app = CommunitiesHostApp(MY_ROWS) + async with app.run_test() as pilot: + screen = app.screen_under_test + await pilot.press("ctrl+t") + await pilot.pause() + app.screen.query_one("#community_name_input", Input).value = "Data Lovers" + app.screen.query_one("#community_description_input", Input).value = "People who love data" + app.screen.query_one("#creator_assignment_type", Select).value = NOT_JOINING + app.screen.query_one("#submit_create_community_btn", Button).press() + await pilot.pause() + app.create_community.assert_called_once_with("Data Lovers", "People who love data", "", NOT_JOINING) + assert screen.query_one("#my_communities_table", DataTable).row_count == 2 + # ...so it is offered in the found list instead + found = screen.query_one("#communities_found_table", DataTable) + assert screen.focused is found + assert found.get_row_at(found.cursor_row)[3] == "new-guid" + + @pytest.mark.asyncio + async def test_create_needs_name_and_description(self): + app = CommunitiesHostApp(MY_ROWS) + async with app.run_test() as pilot: + app.screen_under_test.action_create_community() + await pilot.pause() + app.screen.query_one("#community_name_input", Input).value = "Data Lovers" + app.screen.action_submit() + await pilot.pause() + assert isinstance(app.screen, CreateCommunityScreen) + app.create_community.assert_not_called() + + @pytest.mark.asyncio + async def test_cancel_create(self): + app = CommunitiesHostApp(MY_ROWS) + async with app.run_test() as pilot: + app.screen_under_test.action_create_community() + await pilot.pause() + app.screen.query_one("#cancel_create_community_btn", Button).press() + await pilot.pause() + assert app.screen is app.screen_under_test + app.create_community.assert_not_called() + + @pytest.mark.asyncio + async def test_failed_create_leaves_the_lists_unchanged(self): + app = CommunitiesHostApp(MY_ROWS) + app.create_community = MagicMock(return_value=None) + async with app.run_test() as pilot: + screen = app.screen_under_test + screen.action_create_community() + await pilot.pause() + app.screen.query_one("#community_name_input", Input).value = "Data Lovers" + app.screen.query_one("#community_description_input", Input).value = "People who love data" + app.screen.action_submit() + await pilot.pause() + assert app.screen is screen + assert screen.query_one("#my_communities_table", DataTable).row_count == 2 + # Only the search made on opening; nothing was reloaded + app.search_communities.assert_called_once() + @pytest.mark.asyncio async def test_quit(self): app = CommunitiesHostApp([]) diff --git a/tests/micro-tests/my_profile/test_my_egeria.py b/tests/micro-tests/my_profile/test_my_egeria.py index 28dbfe2a..c277167c 100644 --- a/tests/micro-tests/my_profile/test_my_egeria.py +++ b/tests/micro-tests/my_profile/test_my_egeria.py @@ -96,17 +96,22 @@ async def test_app_on_mount_success( app = MyProfileApp() async with app.run_test() as pilot: + # A live load takes several seconds; checking before it finishes sees half-loaded data + await app._load_task await pilot.pause() backend.expect(app.karma_points, fake=150, live=is_int, label="karma_points") backend.expect(len(app.projects), fake=1, live=at_least(0), label="projects") backend.expect(len(app.teams), fake=1, live=at_least(0), label="teams") - backend.expect(len(app.roles), fake=1, live=at_least(1), label="roles") + # A user needn't hold any roles, e.g. a new user on a new system + backend.expect(len(app.roles), fake=1, live=at_least(0), label="roles") backend.expect(len(app.todos), fake=1, live=at_least(0), label="todos") backend.expect( app.user_GUID, fake="profile-guid-12345", live=nonempty_str, label="user_GUID" ) main_screen = app.get_screen("main") + # The heading picks up the karma points once the profile has loaded + assert main_screen.sub_title == f"User: {app.user_name}, Karma Points: {app.karma_points}" for table_id, fake_rows in ( ("#projects_table", 1), ("#my_collections_table", 1), @@ -309,7 +314,7 @@ async def test_change_user_updates_settings_for_screens(self, monkeypatch): screen = AddTodoScreen("todos_table", "user-guid-123") assert (screen.user_name, screen.user_password) == ("erinoverview", "erin-pwd") assert (os.environ["EGERIA_USER"], os.environ["EGERIA_USER_PASSWORD"]) == ("erinoverview", "erin-pwd") - assert app.get_screen("main").sub_title == "User: erinoverview" + assert app.get_screen("main").sub_title == "User: erinoverview, Karma Points: 0" app._load_or_create_profile.assert_awaited_once() finally: settings.User_Profile.user_name, settings.User_Profile.user_pwd = saved diff --git a/tests/micro-tests/my_profile/test_profile_utils.py b/tests/micro-tests/my_profile/test_profile_utils.py index d2d6d866..8573c357 100644 --- a/tests/micro-tests/my_profile/test_profile_utils.py +++ b/tests/micro-tests/my_profile/test_profile_utils.py @@ -5,6 +5,8 @@ Unit tests for profile_utils module. """ +from types import SimpleNamespace + import pytest from profile_utils import ( truncate_at_sequence, @@ -12,6 +14,7 @@ bools_to_strings, extract_glossary_terms, check_request_serialization, + set_screen_heading, ) @@ -199,3 +202,36 @@ def test_python_field_names_are_reported(self, monkeypatch): assert message is not None assert "pydantic" in message assert ".venv" in message + + +class TestSetScreenHeading: + """Tests for set_screen_heading, which gives every screen the main screen's header.""" + + def test_heading_matches_main_screen(self): + screen = SimpleNamespace(app=SimpleNamespace(user_name="erinoverview", karma_points=42)) + set_screen_heading(screen, "Edit Blogs") + assert screen.title == "Egeria - Edit Blogs" + assert screen.sub_title == "User: erinoverview, Karma Points: 42" + + def test_karma_points_are_0_before_the_profile_loads(self): + screen = SimpleNamespace(app=SimpleNamespace(user_name="erinoverview")) + set_screen_heading(screen) + assert screen.sub_title == "User: erinoverview, Karma Points: 0" + + def test_app_passed_explicitly(self): + # The app sets the main screen's heading itself, where screen.app may not be available + screen = SimpleNamespace() + set_screen_heading(screen, app=SimpleNamespace(user_name="erinoverview", karma_points=7)) + assert screen.sub_title == "User: erinoverview, Karma Points: 7" + + def test_no_context_is_plain_egeria(self): + screen = SimpleNamespace(app=SimpleNamespace(user_name="erinoverview")) + set_screen_heading(screen) + assert screen.title == "Egeria" + + def test_falls_back_to_settings_user(self, monkeypatch): + from pyegeria import settings + monkeypatch.setattr(settings.User_Profile, "user_name", "garygeeke") + screen = SimpleNamespace(app=SimpleNamespace()) + set_screen_heading(screen, "Status") + assert screen.sub_title == "User: garygeeke, Karma Points: 0" From 6ee6bba3a4f16653f46ca317a12de4e986b284af Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:48:35 -0500 Subject: [PATCH 4/6] Changes to order of additional functions on main screen Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../my_egeria/DemoCode/My_Profile/MainScreen.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 717c298f..7007cf85 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -67,16 +67,16 @@ def compose(self) -> ComposeResult: Static(f"[b]Select a function[/b]"), OptionList( # Option("[] User Identities"), - Option("[] Catalogs/Shop for Data"), - Option("[] Edit Profile"), - Option("[] Subscriptions"), - Option("[] Technology Types"), - Option("[] User Bookmarks"), Option("[] My Network"), Option("[] Work with Communities"), - Option("[] Change User"), + Option("[] User Bookmarks"), Option("[] Leave Feedback"), Option("[] Feedback Log"), + Option("[] Subscriptions"), + Option("[] Catalogs/Shop for Data"), + Option("[] Technology Types"), + Option("[] Edit Profile"), + Option("[] Change User"), id="other_function_list" ), id="other_function_container" From 61a29577c316cb68a9930315d2e654544e079fd8 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:40:10 -0500 Subject: [PATCH 5/6] Updates to my_egeria to corrent some errors in the edit profile option on the main screen Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../My_Profile/EditElementsScreens.py | 55 +++++++++++-------- .../My_Profile/elements_crud_handler.py | 2 + .../DemoCode/My_Profile/my_egeria.py | 1 + 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py index 4a10a113..c359ae24 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py @@ -374,6 +374,7 @@ def __init__( karma_points: int | None = None, user_profile: dict | None = None, user_GUID: str | None = None, + profile_element: dict | None = None, *args, **kwargs, ): @@ -388,6 +389,9 @@ def __init__( self.karma_points = karma_points if karma_points is not None else 0 self.user_profile = user_profile or {} self.user_GUID = user_GUID or "" + # The raw Person element from get_my_profile: its properties use Egeria's own field + # names (givenNames, pronouns, ...), which the My-User-MD DICT in user_profile lacks. + self.profile_element = profile_element or {} print("Platform:", self.platform_url) print("View Server:", self.view_server) @@ -408,27 +412,33 @@ def compose(self) -> ComposeResult: self.user_GUID = getattr(self.app, "user_GUID", "") if self.karma_points == 0 and hasattr(self, "app"): self.karma_points = getattr(self.app, "karma_points", 0) - self.job_title = str(self.user_profile.get("jobTitle", self.user_profile.get("Job Title", ""))) + if not self.profile_element and hasattr(self, "app"): + raw = getattr(self.app, "my_profile_raw", None) + if isinstance(raw, list) and raw: + raw = raw[0] + self.profile_element = raw if isinstance(raw, dict) else {} + props = self.profile_element.get("properties") or {} + self.job_title = str(props.get("jobTitle", self.user_profile.get("Job Title", "")) or "") # self.log(f"Job Title: {self.job_title}") - self.given_names = str(self.user_profile.get("givenNames", "")) + self.given_names = str(props.get("givenNames", "") or "") # self.log(f"Given Names: {self.given_names}") - self.surname = str(self.user_profile.get("surname", "")) + self.surname = str(props.get("surname", "") or "") # self.log(f"Surname: {self.surname}") - self.display_name = str(self.user_profile.get("displayName", "")) + self.display_name = str(props.get("displayName", "") or "") # self.log(f"Display Name: {self.display_name}") - self.pronouns = str(self.user_profile.get("pronouns", "")) + self.pronouns = str(props.get("pronouns", "") or "") # self.log(f"Pronouns: {self.pronouns}") - self.description = str(self.user_profile.get("description", "")) + self.description = str(props.get("description", "") or "") # self.log(f"Description: {self.description}") - self.employee_id = str(self.user_profile.get("employeeNumber", self.user_profile.get("employeeId", self.user_profile.get("Employee Number", "")))) + self.employee_id = str(props.get("employeeNumber", self.user_profile.get("Employee Number", "")) or "") # self.log(f"Employee ID: {self.employee_id}") - self.preferred_language = str(self.user_profile.get("preferredLanguage", "")) + self.preferred_language = str(props.get("preferredLanguage", "") or "") # self.log(f"Preferred Language: {self.preferred_language}") - self.resident_country = str(self.user_profile.get("residentCountry", "")) + self.resident_country = str(props.get("residentCountry", "") or "") # self.log(f"Resident Country: {self.resident_country}") - self.time_zone = str(self.user_profile.get("timeZone", "")) + self.time_zone = str(props.get("timeZone", "") or "") # self.log(f"Time Zone: {self.time_zone}") - self.courtesy_title = str(self.user_profile.get("courtesyTitle", "")) + self.courtesy_title = str(props.get("courtesyTitle", "") or "") yield Header(show_clock=True) yield Static("Update your profile in Egeria", classes="span-3", id="edit_profile_title") @@ -482,11 +492,6 @@ def action_create_profile(self) -> None: def create_profile(self) -> Any: """Update profile in Egeria from data provided in Input fields.""" - input_q_name = "Person" + self.query_one("#user_employee_id", Input).value + \ - self.query_one("#user_resident_country", Input).value + \ - self.query_one("#user_given_names", Input).value + \ - self.query_one("#user_family_name", Input).value - input_d_name = self.query_one("#user_preferred_name", Input).value input_c_title = self.query_one("#user_title", Input).value @@ -495,8 +500,6 @@ def create_profile(self) -> Any: input_s_name = self.query_one("#user_family_name", Input).value - input_f_name = self.query_one("#user_preferred_name", Input).value - input_pronouns = self.query_one("#user_pronouns", Input).value input_j_title = self.query_one("#user_job_title", Input).value @@ -511,7 +514,6 @@ def create_profile(self) -> Any: input_desc = self.query_one("#user_description", Input).value - # self.log(f"Input q_name:{input_q_name}") # self.log(f"Input d_name:{input_d_name}") # self.log(f"Input j_title:{input_j_title}") # self.log(f"Input e_num:{input_e_num}") @@ -520,10 +522,9 @@ def create_profile(self) -> Any: # self.log(f"Input t_z:{input_t_z}") # self.log(f"Input desc:{input_desc}") + # qualifiedName is left out so the merge update keeps the profile's existing one. props: dict = {} props.update({"class": "PersonProperties"}) - if input_q_name: - props.update({"qualifiedName": input_q_name}) if input_d_name: props.update({"displayName": input_d_name}) if input_c_title: @@ -532,8 +533,6 @@ def create_profile(self) -> Any: props.update({"givenNames": input_g_name}) if input_s_name: props.update({"surname": input_s_name}) - if input_f_name: - props.update({"fullName": input_f_name}) if input_pronouns: props.update({"pronouns": input_pronouns}) if input_j_title: @@ -555,15 +554,25 @@ def create_profile(self) -> Any: "isOwnAnchor": True, "properties": props } + # A profile loaded from a content pack (or another external source) can only be changed by a + # request naming that source, otherwise Egeria rejects it with OMAG-REPOSITORY-HANDLER-400-008. + origin = (self.profile_element.get("elementHeader") or {}).get("origin") or {} + if origin.get("originCategory") not in (None, "LOCAL_COHORT") and origin.get("homeMetadataCollectionId"): + self.update_element_request_body.update({ + "externalSourceGUID": origin.get("homeMetadataCollectionId"), + "externalSourceName": origin.get("homeMetadataCollectionName"), + }) try: new_profile_inst = Egeria(self.view_server, self.platform_url, self.user_name, self.user_password) new_profile_inst.create_egeria_bearer_token(self.user_name, self.user_password) new_profile_inst.update_actor_profile(self.user_GUID, self.update_element_request_body) # self.log(f"Profile updated for GUID: {self.user_GUID}") + self.app.notify("Profile saved to Egeria") self.dismiss(200) return (200) except PyegeriaException as e: # self.log(f"Error creating profile: {e!s} | request={self.update_element_request_body}") + self.app.notify(f"Profile not saved: {e!s}", severity="error", timeout=10) self.dismiss(401) return (401) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py index cca06fa3..5c2031e9 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py @@ -145,6 +145,8 @@ def edit_profile_callback(self, return_c: Any) -> None: if isinstance(return_c, int): if return_c == 200: self._show_main_screen() + # Re-read the profile so the main screen and the next Edit Profile show the saved values + self.call_later(self.action_refresh) else: # self.log(f"Error returned from EditProfileScreen: {return_c}") # self.log("Returning to main screen") diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py index 58aa0280..9c9558fa 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_egeria.py @@ -719,6 +719,7 @@ async def handle_option_selected(self, event: OptionList.OptionSelected) -> None karma_points=self.karma_points, user_profile=self.user_profile, user_GUID=self.user_GUID, + profile_element=getattr(self, "my_profile_raw", None), ), callback=self.edit_profile_callback, ) From ac1a263e38d02f15002b4f7e4ee5dc46bc0a9965 Mon Sep 17 00:00:00 2001 From: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:52:58 -0500 Subject: [PATCH 6/6] Updates to fix a navigation issue in the Tech Types functionality Signed-off-by: Peter Coldicott <86156876+pcoldico@users.noreply.github.com> --- .../DemoCode/My_Profile/tech_types_handler.py | 12 ++++++++++-- .../my_profile/test_tech_types_handler.py | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py index 61242701..004ba1b9 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/tech_types_handler.py @@ -49,6 +49,11 @@ async def handle_technology_types_option(self) -> None: return # self.log("Technology types fetched successfully.") # self.log("Displaying technology types...") + await self.show_technology_types_tree() + # self.log("Technology types displayed successfully.") + + async def show_technology_types_tree(self) -> None: + """Push the technology types tree from the already-fetched hierarchy.""" await self.push_screen( TechnologyTypesScreen( ttlist=self.tech_type_list, @@ -56,7 +61,6 @@ async def handle_technology_types_option(self) -> None: ), callback=self.tech_type_callback, ) - # self.log("Technology types displayed successfully.") async def tech_type_callback(self, result: Any) -> int: """Callback for Technology Types screen. @@ -153,7 +157,11 @@ async def tech_type_options_callback(self, result: Any) -> int: self._show_main_screen() return 200 elif isinstance(result, (int, str)): - if result == "back" or result == "": + if result == "back": + # Go back one level: the tree dismissed itself on selection, so re-open it + await self.show_technology_types_tree() + return 200 + elif result == "": # self.log(f"Technology Type Options screen cancelled/failed; return: {result}, exiting.") self._show_main_screen() return 200 diff --git a/tests/micro-tests/my_profile/test_tech_types_handler.py b/tests/micro-tests/my_profile/test_tech_types_handler.py index a1da2ac2..f4f1c3dc 100644 --- a/tests/micro-tests/my_profile/test_tech_types_handler.py +++ b/tests/micro-tests/my_profile/test_tech_types_handler.py @@ -233,10 +233,16 @@ async def test_tech_type_options_callback_back_or_empty(self): assert res == 200 assert app.shown_main_screen is True + # Back goes up one level, to a re-opened technology types tree, not the main screen app.shown_main_screen = False + app.tech_type_list = [{"displayName": "Root Technology Type"}] res = await app.tech_type_options_callback("back") assert res == 200 - assert app.shown_main_screen is True + assert app.shown_main_screen is False + screen, cb = app.pushed_screens[-1] + assert isinstance(screen, TechnologyTypesScreen) + assert screen.tech_type_list == app.tech_type_list + assert cb == app.tech_type_callback app.shown_main_screen = False res = await app.tech_type_options_callback([])