diff --git a/.codespellignore b/.codespellignore index 46df8631..936966ae 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,3 +1,4 @@ aline ore jkd +te \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a45060d0..e22bd9b0 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,10 +3,10 @@ name: PyTest&Coverage on: push: branches: - - main + - main pull_request: branches: - - main + - main jobs: build: @@ -30,7 +30,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-linux.txt - pip install pytest pytest-asyncio pytest-qt pytest-qt-app pytest-mock coverage aiohttp + pip install typing_extensions pytest pytest-asyncio pytest-qt pytest-qt-app pytest-mock coverage aiohttp - name: Test with pytest run: | mkdir -p htmlcov diff --git a/.pylintrc b/.pylintrc index 0373e59b..cfda8913 100644 --- a/.pylintrc +++ b/.pylintrc @@ -11,7 +11,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=data_hierarchy_editor_dialog_base.py,create_type_dialog_base.py,terminology_lookup_dialog_base.py,completed_upload_task.py,completed_uploads_base.py,config_dialog_base.py,edit_metadata_dialog_base.py,main_dialog_base.py,primitive_compound_controlled_frame_base.py,project_item_frame_base.py,upload_config_dialog_base.py,upload_widget_base.py,edit_metadata_summary_dialog_base.py +ignore-patterns=data_hierarchy_editor_dialog_base.py,create_type_dialog_base.py,terminology_lookup_dialog_base.py,completed_upload_task.py,completed_uploads_base.py,config_dialog_base.py,edit_metadata_dialog_base.py,main_dialog_base.py,primitive_compound_controlled_frame_base.py,project_item_frame_base.py,upload_config_dialog_base.py,upload_widget_base.py,edit_metadata_summary_dialog_base.py,type_dialog_base.py # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -423,6 +423,7 @@ valid-metaclass-classmethod-first-arg=mcs # Maximum number of arguments for function / method max-args=10 #Steffen's change +max-positional-arguments=10 # Maximum number of attributes for a class (see R0902). max-attributes=25 diff --git a/pasta_eln/GUI/data_hierarchy/create_type_dialog.py b/pasta_eln/GUI/data_hierarchy/create_type_dialog.py index 7367cc4e..b253ff6a 100644 --- a/pasta_eln/GUI/data_hierarchy/create_type_dialog.py +++ b/pasta_eln/GUI/data_hierarchy/create_type_dialog.py @@ -1,115 +1,117 @@ -""" CreateTypeDialog used for the create type dialog """ +"""A dialog for creating a new data type within the application.""" # PASTA-ELN and all its sub-parts are covered by the MIT license. # -# Copyright (c) 2023 +# Copyright (c) 2024 # # Author: Jithu Murugan # Filename: create_type_dialog.py # # You should have received a copy of the license with this file. Please refer the license file for more information. - import logging -from collections.abc import Callable -from typing import Any +from typing import Any, Callable -from PySide6 import QtCore -from PySide6.QtCore import QRegularExpression -from PySide6.QtGui import QRegularExpressionValidator -from PySide6.QtWidgets import QDialog +from PySide6 import QtWidgets +from PySide6.QtWidgets import QMessageBox -from pasta_eln.GUI.data_hierarchy.create_type_dialog_base import Ui_CreateTypeDialogBase +from pasta_eln.GUI.data_hierarchy.generic_exception import GenericException +from pasta_eln.GUI.data_hierarchy.type_dialog import TypeDialog +from pasta_eln.GUI.data_hierarchy.utility_functions import generate_data_hierarchy_type, show_message -class CreateTypeDialog(Ui_CreateTypeDialogBase): - """ - Abstracted dialog for the create type +class CreateTypeDialog(TypeDialog): """ + A dialog for creating a new data type within the application. - def __new__(cls, *_: Any, **__: Any) -> Any: - """ - Instantiates the create type dialog - """ - return super(CreateTypeDialog, cls).__new__(cls) + This class extends the TypeDialog to provide functionality for adding new data types. + It initializes the dialog with specific UI elements and behavior tailored for creating new types. + + Args: + accepted_callback (Callable[[], None]): A callback function to be executed when the action is accepted. + rejected_callback (Callable[[], None]): A callback function to be executed when the action is rejected. + """ def __init__(self, accepted_callback: Callable[[], None], - rejected_callback: Callable[[], None]) -> None: + rejected_callback: Callable[[], None]): """ - Initializes the create type dialog + Initializes a new instance of the class with specified callback functions. + + This constructor sets up the instance by initializing the parent class and configuring the logger. + It also initializes an empty dictionary for data hierarchy types and sets the window title for the dialog. + Args: - accepted_callback (Callable): Accepted button parent callback. - rejected_callback (Callable): Rejected button parent callback. + accepted_callback (Callable[[], None]): A callback function to be executed when the action is accepted. + rejected_callback (Callable[[], None]): A callback function to be executed when the action is rejected. """ + super().__init__(accepted_callback, rejected_callback) self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self.next_struct_level: str | None = "" - self.instance = QDialog() - super().setupUi(self.instance) - # Restricts the title input to allow anything except x or space - # as the first character which is reserved for structural level - self.titleLineEdit.setValidator(QRegularExpressionValidator(QRegularExpression("^[^ Ax].*"))) - self.setup_slots(accepted_callback, rejected_callback) - - def setup_slots(self, - accepted_callback: Callable[[], None], - rejected_callback: Callable[[], None]) -> None: - """ - Sets up the slots for the dialog - Args: - accepted_callback (Callable): Accepted button parent callback. - rejected_callback (Callable): Rejected button parent callback. - - Returns: None + self.data_hierarchy_types: dict[str, Any] = {} + self.instance.setWindowTitle("Create new type") + def accepted_callback(self) -> None: """ - self.buttonBox.accepted.connect(accepted_callback) - self.buttonBox.rejected.connect(rejected_callback) - self.structuralLevelCheckBox.stateChanged.connect(self.structural_level_checkbox_callback) + Handles the acceptance of a new data type by validating input and updating the data hierarchy. - def structural_level_checkbox_callback(self) -> None: - """ - Callback invoked when the state changes for structuralLevelCheckBox + This method checks if the type information is valid and whether the data type already exists in the hierarchy. + If the type is valid and does not exist, it logs the creation of the new type, updates the data hierarchy, + and closes the dialog. If the type already exists, it shows a warning message. If the data hierarchy types + are null, it logs an error and raises an exception. - Returns: Nothing + Raises: + GenericException: If the data hierarchy types are null. """ - if self.structuralLevelCheckBox.isChecked(): - self.titleLineEdit.setText(self.next_struct_level.replace("x", "Structure level ") - if self.next_struct_level else "") - self.titleLineEdit.setDisabled(True) + if not self.validate_type_info(): + return + if self.data_hierarchy_types is None: + self.logger.error("Null data_hierarchy_types, erroneous app state") + raise GenericException("Null data_hierarchy_types, erroneous app state", {}) + if self.type_info.datatype in self.data_hierarchy_types: + self.logger.error( + "Type (datatype: {%s} displayed title: {%s}) cannot be added since it exists in DB already....", + self.type_info.datatype, + self.type_info.title + ) + show_message( + f"Type (datatype: {self.type_info.datatype} displayed title: {self.type_info.title}) cannot be added since it exists in DB already....", + QMessageBox.Icon.Warning) else: - self.titleLineEdit.clear() - self.titleLineEdit.setDisabled(False) - - def show(self) -> None: + self.logger.info("User created a new type and added " + "to the data_hierarchy document: Datatype: {%s}, Displayed Title: {%s}", + self.type_info.datatype, + self.type_info.title) + if isinstance(self.type_info.datatype, str): + self.data_hierarchy_types[self.type_info.datatype] = generate_data_hierarchy_type(self.type_info) + self.instance.close() + self.accepted_callback_parent() + + def rejected_callback(self) -> None: """ - Displays the dialog + Calls the parent rejection callback method. - Returns: None - - """ - self.instance.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) - self.instance.show() + This method is intended to handle the rejection of a dialog or action by invoking + the corresponding parent method that manages the rejection behavior. It does not + perform any additional logic or checks. + """ + self.rejected_callback_parent() - def clear_ui(self) -> None: + def set_data_hierarchy_types(self, data_hierarchy_types: dict[str, Any]) -> None: """ - Clear the Dialog UI + Sets the data hierarchy types for the instance. - Returns: Nothing + This method updates the internal state of the instance by assigning the provided dictionary of data hierarchy types. + It allows the instance to manage and utilize the specified types in its operations. + Args: + self: The instance of the class. + data_hierarchy_types (dict[str, Any]): A dictionary containing data hierarchy types to be set. """ - self.displayedTitleLineEdit.clear() - self.titleLineEdit.clear() - self.structuralLevelCheckBox.setChecked(False) + self.data_hierarchy_types = data_hierarchy_types - def set_structural_level_title(self, - structural_level: str | None) -> None: - """ - Set the next possible structural type level title - Args: - structural_level (str): Passed in structural level of the format (x0, x1, x2 ...) +if __name__ == "__main__": + import sys - Returns: Nothing - - """ - self.logger.info("Next structural level set: {%s}...", structural_level) - self.next_struct_level = structural_level + app = QtWidgets.QApplication(sys.argv) + ui = CreateTypeDialog(lambda: None, lambda: None) + ui.show() + sys.exit(app.exec()) diff --git a/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.py b/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.py deleted file mode 100644 index 5b372247..00000000 --- a/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.py +++ /dev/null @@ -1,91 +0,0 @@ -# Form implementation generated from reading ui file 'create_type_dialog_base.ui' -# -# Created by: PyQt6 UI code generator 6.4.2 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_CreateTypeDialogBase(object): - def setupUi(self, CreateTypeDialogBase): - CreateTypeDialogBase.setObjectName("CreateTypeDialogBase") - CreateTypeDialogBase.resize(584, 375) - self.gridLayout = QtWidgets.QGridLayout(CreateTypeDialogBase) - self.gridLayout.setObjectName("gridLayout") - self.mainVerticalLayout = QtWidgets.QVBoxLayout() - self.mainVerticalLayout.setContentsMargins(20, -1, 20, -1) - self.mainVerticalLayout.setObjectName("mainVerticalLayout") - self.tileHorizontalLayout = QtWidgets.QHBoxLayout() - self.tileHorizontalLayout.setObjectName("tileHorizontalLayout") - self.titleLabel = QtWidgets.QLabel(parent=CreateTypeDialogBase) - self.titleLabel.setMinimumSize(QtCore.QSize(120, 0)) - self.titleLabel.setObjectName("titleLabel") - self.tileHorizontalLayout.addWidget(self.titleLabel) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.tileHorizontalLayout.addItem(spacerItem) - self.titleLineEdit = QtWidgets.QLineEdit(parent=CreateTypeDialogBase) - self.titleLineEdit.setClearButtonEnabled(True) - self.titleLineEdit.setObjectName("titleLineEdit") - self.tileHorizontalLayout.addWidget(self.titleLineEdit) - self.mainVerticalLayout.addLayout(self.tileHorizontalLayout) - self.displayedTitleHorizontalLayout = QtWidgets.QHBoxLayout() - self.displayedTitleHorizontalLayout.setObjectName("displayedTitleHorizontalLayout") - self.typeLabel = QtWidgets.QLabel(parent=CreateTypeDialogBase) - self.typeLabel.setMinimumSize(QtCore.QSize(120, 0)) - self.typeLabel.setObjectName("typeLabel") - self.displayedTitleHorizontalLayout.addWidget(self.typeLabel) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.displayedTitleHorizontalLayout.addItem(spacerItem1) - self.displayedTitleLineEdit = QtWidgets.QLineEdit(parent=CreateTypeDialogBase) - self.displayedTitleLineEdit.setClearButtonEnabled(True) - self.displayedTitleLineEdit.setObjectName("displayedTitleLineEdit") - self.displayedTitleHorizontalLayout.addWidget(self.displayedTitleLineEdit) - self.mainVerticalLayout.addLayout(self.displayedTitleHorizontalLayout) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.mainVerticalLayout.addItem(spacerItem2) - self.checkBoxHorizontalLayout = QtWidgets.QHBoxLayout() - self.checkBoxHorizontalLayout.setObjectName("checkBoxHorizontalLayout") - self.structuralLevelCheckBox = QtWidgets.QCheckBox(parent=CreateTypeDialogBase) - self.structuralLevelCheckBox.setObjectName("structuralLevelCheckBox") - self.checkBoxHorizontalLayout.addWidget(self.structuralLevelCheckBox) - self.mainVerticalLayout.addLayout(self.checkBoxHorizontalLayout) - self.mainVerticalLayout.setStretch(0, 1) - self.mainVerticalLayout.setStretch(1, 1) - self.mainVerticalLayout.setStretch(2, 1) - self.mainVerticalLayout.setStretch(3, 1) - self.gridLayout.addLayout(self.mainVerticalLayout, 0, 0, 1, 1) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=CreateTypeDialogBase) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) - self.buttonBox.setObjectName("buttonBox") - self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) - - self.retranslateUi(CreateTypeDialogBase) - self.buttonBox.accepted.connect(CreateTypeDialogBase.accept) # type: ignore - self.buttonBox.rejected.connect(CreateTypeDialogBase.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(CreateTypeDialogBase) - - def retranslateUi(self, CreateTypeDialogBase): - _translate = QtCore.QCoreApplication.translate - CreateTypeDialogBase.setWindowTitle(_translate("CreateTypeDialogBase", "Create a new data type")) - self.titleLabel.setText(_translate("CreateTypeDialogBase", "Data type")) - self.titleLineEdit.setToolTip(_translate("CreateTypeDialogBase", "Exclude titles which start with \'x\' (reserved for structure level titles) or whitespace")) - self.titleLineEdit.setPlaceholderText(_translate("CreateTypeDialogBase", "Enter the data type")) - self.typeLabel.setText(_translate("CreateTypeDialogBase", "Title")) - self.displayedTitleLineEdit.setToolTip(_translate("CreateTypeDialogBase", "Enter displayed title for the new type, which can also be modified later in the main editor window")) - self.displayedTitleLineEdit.setPlaceholderText(_translate("CreateTypeDialogBase", "Enter the displayed title")) - self.structuralLevelCheckBox.setToolTip(_translate("CreateTypeDialogBase", "If this is a structural type, then title will be automatically populated as (x0, x1...xn). Next number will be chosen for xn from the existing list of structural items.")) - self.structuralLevelCheckBox.setText(_translate("CreateTypeDialogBase", "Is this a structural Type?")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - CreateTypeDialogBase = QtWidgets.QDialog() - ui = Ui_CreateTypeDialogBase() - ui.setupUi(CreateTypeDialogBase) - CreateTypeDialogBase.show() - sys.exit(app.exec()) diff --git a/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.ui b/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.ui deleted file mode 100644 index 7e8d4be2..00000000 --- a/pasta_eln/GUI/data_hierarchy/create_type_dialog_base.ui +++ /dev/null @@ -1,193 +0,0 @@ - - - CreateTypeDialogBase - - - - 0 - 0 - 584 - 375 - - - - Create a new data type - - - - - - 20 - - - 20 - - - - - - - - 120 - 0 - - - - Data type - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - Exclude titles which start with 'x' (reserved for structure level titles) or whitespace - - - Enter the data type - - - true - - - - - - - - - - - - 120 - 0 - - - - Title - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - Enter displayed title for the new type, which can also be modified later in the main editor window - - - Enter the displayed title - - - true - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - If this is a structural type, then title will be automatically populated as (x0, x1...xn). Next number will be chosen for xn from the existing list of structural items. - - - Is this a structural Type? - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - CreateTypeDialogBase - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - CreateTypeDialogBase - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog.py b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog.py index f0bb1524..918161be 100644 --- a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog.py +++ b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog.py @@ -10,36 +10,33 @@ import copy import logging -import sys import webbrowser from typing import Any from PySide6 import QtWidgets from PySide6.QtCore import QCoreApplication, QObject, Signal, Slot -from PySide6.QtWidgets import QApplication, QLineEdit, QMessageBox +from PySide6.QtWidgets import QApplication, QMessageBox from cloudant.document import Document from .attachments_tableview_data_model import AttachmentsTableViewModel from .constants import ATTACHMENT_TABLE_DELETE_COLUMN_INDEX, \ ATTACHMENT_TABLE_REORDER_COLUMN_INDEX, DATA_HIERARCHY_HELP_PAGE_URL, METADATA_TABLE_DELETE_COLUMN_INDEX, \ METADATA_TABLE_IRI_COLUMN_INDEX, METADATA_TABLE_REORDER_COLUMN_INDEX, METADATA_TABLE_REQUIRED_COLUMN_INDEX -from .create_type_dialog import CreateTypeDialog +from .create_type_dialog import CreateTypeDialog, TypeDialog from .data_hierarchy_editor_dialog_base import Ui_DataHierarchyEditorDialogBase from .delete_column_delegate import DeleteColumnDelegate from .document_null_exception import DocumentNullException +from .edit_type_dialog import EditTypeDialog from .generic_exception import GenericException from .iri_column_delegate import IriColumnDelegate from .key_not_found_exception import \ KeyNotFoundException -from .lookup_iri_action import LookupIriAction from .mandatory_column_delegate import MandatoryColumnDelegate from .metadata_tableview_data_model import MetadataTableViewModel from .reorder_column_delegate import ReorderColumnDelegate from .utility_functions import adapt_type, adjust_data_hierarchy_data_to_v4, can_delete_type, \ check_data_hierarchy_types, \ - generate_empty_type, \ - get_missing_metadata_message, get_next_possible_structural_level_title, \ - get_types_for_display, show_message + get_missing_metadata_message, get_types_for_display, show_message from ...database import Database @@ -68,7 +65,7 @@ def __init__(self, database: Database) -> None: self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.data_hierarchy_loaded: bool = False - self.data_hierarchy_types: Any = {} + self.data_hierarchy_types: dict[str, Any] = {} self.selected_type_metadata: dict[str, list[dict[str, Any]]] | Any = {} # Set up the UI elements @@ -124,7 +121,10 @@ def __init__(self, database: Database) -> None: self.typeAttachmentsTableView.horizontalHeader().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) # Create the dialog for new type creation - self.create_type_dialog = CreateTypeDialog(self.create_type_accepted_callback, self.create_type_rejected_callback) + self.create_type_dialog: TypeDialog = CreateTypeDialog(self.type_create_accepted_callback, + self.type_create_rejected_callback) + self.edit_type_dialog: TypeDialog = EditTypeDialog(self.type_edit_accepted_callback, + self.type_edit_rejected_callback) # Set up the slots for the UI items self.setup_slots() @@ -156,41 +156,18 @@ def type_combo_box_changed(self, if new_type_selected not in self.data_hierarchy_types: raise KeyNotFoundException(f"Key {new_type_selected} " f"not found in data_hierarchy_types", {}) - selected_type = self.data_hierarchy_types.get(new_type_selected) + selected_type = self.data_hierarchy_types.get(new_type_selected, {}) # Get the metadata for the selected type and store the list in selected_type_metadata - self.selected_type_metadata = selected_type.get("meta") - - # Type displayed_title is set in a line edit - self.typeDisplayedTitleLineEdit.setText(selected_type.get('title')) - - # Type IRI is set in a line edit - self.typeIriLineEdit.setText(selected_type.get('IRI')) + self.selected_type_metadata = selected_type.get("meta", {}) # Gets the attachment data from selected type and set it in table view - self.attachments_table_data_model.update(selected_type.get('attachments')) + self.attachments_table_data_model.update(selected_type.get('attachments', {})) # Reset the metadata group combo-box self.metadataGroupComboBox.addItems(list(self.selected_type_metadata.keys()) if self.selected_type_metadata else []) self.metadataGroupComboBox.setCurrentIndex(0) - def set_iri_lookup_action(self, - lookup_term: str) -> None: - """ - Sets the IRI lookup action for the IRI line edit - Args: - lookup_term (str): Default lookup term to be used by the lookup service - - Returns: Nothing - - """ - for act in self.typeIriLineEdit.actions(): - if isinstance(act, LookupIriAction): - act.deleteLater() - self.typeIriLineEdit.addAction( - LookupIriAction(parent_line_edit=self.typeIriLineEdit, lookup_term=lookup_term), - QLineEdit.ActionPosition.TrailingPosition) - def metadata_group_combo_box_changed(self, new_selected_metadata_group: Any) -> None: """ @@ -259,8 +236,7 @@ def update_type_displayed_title(self, current_type = self.typeComboBox.currentText() current_type = adapt_type(current_type) if modified_type_displayed_title is not None and current_type in self.data_hierarchy_types: - self.data_hierarchy_types.get(current_type)["title"] = modified_type_displayed_title - self.set_iri_lookup_action(modified_type_displayed_title) + self.data_hierarchy_types.get(current_type, {})["title"] = modified_type_displayed_title def update_type_iri(self, modified_iri: str) -> None: @@ -275,7 +251,7 @@ def update_type_iri(self, current_type = self.typeComboBox.currentText() current_type = adapt_type(current_type) if modified_iri is not None and current_type in self.data_hierarchy_types: - self.data_hierarchy_types.get(current_type)["IRI"] = modified_iri + self.data_hierarchy_types.get(current_type, {})["IRI"] = modified_iri def delete_selected_type(self) -> None: """ @@ -295,7 +271,7 @@ def delete_selected_type(self) -> None: self.logger.info("User deleted the selected type: {%s}", selected_type) self.data_hierarchy_types.pop(selected_type) self.typeComboBox.clear() - self.typeComboBox.addItems(get_types_for_display(self.data_hierarchy_types.keys())) + self.typeComboBox.addItems(get_types_for_display(list(self.data_hierarchy_types.keys()))) self.typeComboBox.setCurrentIndex(0) def clear_ui(self) -> None: @@ -305,38 +281,63 @@ def clear_ui(self) -> None: Returns: None """ - # Disable the signals for the line edits before clearing in order to avoid clearing the respective - # iri and displayed_titles for the selected type from data_hierarchy document - self.typeDisplayedTitleLineEdit.textChanged[str].disconnect() - self.typeIriLineEdit.textChanged[str].disconnect() - self.typeDisplayedTitleLineEdit.clear() - self.typeIriLineEdit.clear() - self.typeDisplayedTitleLineEdit.textChanged[str].connect(self.update_type_displayed_title) - self.typeIriLineEdit.textChanged[str].connect(self.update_type_iri) - self.metadataGroupComboBox.clear() self.addMetadataGroupLineEdit.clear() self.typeMetadataTableView.model().update([]) self.typeAttachmentsTableView.model().update([]) - def create_type_accepted_callback(self) -> None: + def type_create_accepted_callback(self) -> None: """ - Callback for the OK button of CreateTypeDialog to create a new type in the data_hierarchy data set + Handles the acceptance of a new type creation in the data hierarchy. - Returns: Nothing + This method is called when the user confirms the creation of a new type. + It checks if the data hierarchy types are loaded, updates the type combo box with the available types, + and clears the user interface of the create type dialog. + + Args: + self: The instance of the class. """ - title = self.create_type_dialog.next_struct_level \ - if self.create_type_dialog.structuralLevelCheckBox.isChecked() \ - else self.create_type_dialog.titleLineEdit.text() - displayed_title = self.create_type_dialog.displayedTitleLineEdit.text() + if not isinstance(self.data_hierarchy_types, dict): + show_message("Load the data hierarchy data first....", QMessageBox.Icon.Warning) + return + self.typeComboBox.clear() + self.typeComboBox.addItems(get_types_for_display(list(self.data_hierarchy_types.keys()))) + self.typeComboBox.setCurrentIndex(len(self.data_hierarchy_types) - 1) self.create_type_dialog.clear_ui() - self.create_new_type(title, displayed_title) - def create_type_rejected_callback(self) -> None: + def type_create_rejected_callback(self) -> None: """ - Callback for the cancel button of CreateTypeDialog + Handles the cancellation of the type creation process. - Returns: Nothing + This method is called when the user cancels the creation of a new type in the dialog. + It clears the user interface elements of the create type dialog to reset its state. + + Args: + self: The instance of the class. + """ + self.create_type_dialog.clear_ui() + + def type_edit_accepted_callback(self) -> None: + """ + Handles the acceptance of the type editing process. + + This method is called when the user confirms the changes made to a type in the dialog. + It clears the user interface elements of the create type dialog to prepare for a new input. + + Args: + self: The instance of the class. + """ + self.create_type_dialog.clear_ui() + + def type_edit_rejected_callback(self) -> None: + """ + Handles the cancellation of the type editing process. + + This method is called when the user cancels the editing of a type in the dialog. + It clears the user interface elements of the create type dialog to reset its state. + + Args: + self: The instance of the class. """ self.create_type_dialog.clear_ui() @@ -345,12 +346,25 @@ def show_create_type_dialog(self) -> None: Opens a dialog which allows the user to enter the details to create a new type (structural or normal) Returns: Nothing """ - if self.data_hierarchy_types is not None and self.data_hierarchy_loaded: - structural_title = get_next_possible_structural_level_title(self.data_hierarchy_types.keys()) - self.create_type_dialog.set_structural_level_title(structural_title) - self.create_type_dialog.show() - else: + if self.data_hierarchy_types is None or not self.data_hierarchy_loaded: + show_message("Load the data hierarchy data first...", QMessageBox.Icon.Warning) + return + self.create_type_dialog.set_data_hierarchy_types(self.data_hierarchy_types) + self.create_type_dialog.show() + + def show_edit_type_dialog(self) -> None: + """ + Opens a dialog which allows the user to enter the details to create a new type (structural or normal) + Returns: Nothing + """ + if self.data_hierarchy_types is None or not self.data_hierarchy_loaded: show_message("Load the data hierarchy data first...", QMessageBox.Icon.Warning) + return + current_type = self.typeComboBox.currentText() + current_type = adapt_type(current_type) + self.edit_type_dialog.set_selected_data_hierarchy_type_name(current_type) + self.edit_type_dialog.set_selected_data_hierarchy_type(self.data_hierarchy_types.get(current_type, {})) + self.edit_type_dialog.show() def setup_slots(self) -> None: """ @@ -366,6 +380,7 @@ def setup_slots(self) -> None: self.deleteMetadataGroupPushButton.clicked.connect(self.delete_selected_metadata_group) self.deleteTypePushButton.clicked.connect(self.delete_selected_type) self.addTypePushButton.clicked.connect(self.show_create_type_dialog) + self.editTypePushButton.clicked.connect(self.show_edit_type_dialog) self.cancelPushButton.clicked.connect(self.instance.close) self.helpPushButton.clicked.connect(lambda: webbrowser.open(DATA_HIERARCHY_HELP_PAGE_URL)) self.attachmentsShowHidePushButton.clicked.connect(self.show_hide_attachments_table) @@ -374,17 +389,16 @@ def setup_slots(self) -> None: self.typeComboBox.currentTextChanged.connect(self.type_combo_box_changed) self.metadataGroupComboBox.currentTextChanged.connect(self.metadata_group_combo_box_changed) - # Slots for line edits - self.typeDisplayedTitleLineEdit.textChanged[str].connect(self.update_type_displayed_title) - self.typeIriLineEdit.textChanged[str].connect(self.update_type_iri) - # Slots for the delegates - self.delete_column_delegate_metadata_table.delete_clicked_signal.connect(self.metadata_table_data_model.delete_data) - self.reorder_column_delegate_metadata_table.re_order_signal.connect(self.metadata_table_data_model.re_order_data) + self.delete_column_delegate_metadata_table.delete_clicked_signal.connect( + self.metadata_table_data_model.delete_data) + self.reorder_column_delegate_metadata_table.re_order_signal.connect( + self.metadata_table_data_model.re_order_data) self.delete_column_delegate_attach_table.delete_clicked_signal.connect( self.attachments_table_data_model.delete_data) - self.reorder_column_delegate_attach_table.re_order_signal.connect(self.attachments_table_data_model.re_order_data) + self.reorder_column_delegate_attach_table.re_order_signal.connect( + self.attachments_table_data_model.re_order_data) self.type_changed_signal.connect(self.check_and_disable_delete_button) @@ -406,7 +420,7 @@ def load_data_hierarchy_data(self) -> None: # Set the types in the type selector combo-box self.typeComboBox.clear() - self.typeComboBox.addItems(get_types_for_display(self.data_hierarchy_types.keys())) + self.typeComboBox.addItems(get_types_for_display(list(self.data_hierarchy_types.keys()))) self.typeComboBox.setCurrentIndex(0) def save_data_hierarchy(self) -> None: @@ -445,38 +459,6 @@ def save_data_hierarchy(self) -> None: self.database.initDocTypeViews(16) self.instance.close() - def create_new_type(self, - title: str, - displayed_title: str) -> None: - """ - Add a new type to the loaded data_hierarchy_data from the db - Args: - title (str): The new key entry used for the data_hierarchy_data - displayed_title (str): The new displayed_title set for the new type entry in data_hierarchy_data - - Returns: - - """ - if self.data_hierarchy_document is None or self.data_hierarchy_types is None: - self.logger.error("Null data_hierarchy_document/data_hierarchy_types, erroneous app state") - raise GenericException("Null data_hierarchy_document/data_hierarchy_types, erroneous app state", {}) - if title in self.data_hierarchy_types: - show_message( - f"Type (title: {title} displayed title: {displayed_title}) cannot be added since it exists in DB already....", - QMessageBox.Icon.Warning) - else: - if not title: - self.logger.warning("Enter non-null/valid title!!.....") - show_message("Enter non-null/valid title!!.....", QMessageBox.Icon.Warning) - return - self.logger.info("User created a new type and added " - "to the data_hierarchy document: Title: {%s}, Displayed Title: {%s}", title, displayed_title) - empty_type = generate_empty_type(displayed_title) - self.data_hierarchy_types[title] = empty_type - self.typeComboBox.clear() - self.typeComboBox.addItems(get_types_for_display(self.data_hierarchy_types.keys())) - self.typeComboBox.setCurrentIndex(len(self.data_hierarchy_types) - 1) - def show_hide_attachments_table(self) -> None: """ Show/hide the attachment table and the add attachment button @@ -496,9 +478,7 @@ def check_and_disable_delete_button(self, Returns: Nothing """ - (self.deleteTypePushButton - .setEnabled(can_delete_type(self.data_hierarchy_types.keys(), - selected_type))) + self.deleteTypePushButton.setEnabled(can_delete_type(adapt_type(selected_type))) def get_gui(database: Database) -> tuple[ @@ -510,6 +490,7 @@ def get_gui(database: Database) -> tuple[ Returns: """ + import sys instance = QApplication.instance() application = QApplication(sys.argv) if instance is None else instance data_hierarchy_form: DataHierarchyEditorDialog = DataHierarchyEditorDialog(database) diff --git a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.py b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.py index acf86d0d..7e0332ae 100644 --- a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.py +++ b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.py @@ -1,293 +1,358 @@ -# Form implementation generated from reading ui file 'data_hierarchy_editor_dialog_base.ui' -# -# Created by: PyQt6 UI code generator 6.4.2 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. +# -*- coding: utf-8 -*- +################################################################################ +## Form generated from reading UI file 'data_hierarchy_editor_dialog_base.ui' +## +## Created by: Qt User Interface Compiler version 6.7.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ -from PySide6 import QtCore, QtGui, QtWidgets - +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QComboBox, QGridLayout, QHBoxLayout, + QHeaderView, QLabel, QLineEdit, QPushButton, + QSizePolicy, QSpacerItem, QTableView, QWidget) class Ui_DataHierarchyEditorDialogBase(object): - def setupUi(self, DataHierarchyEditorDialogBase): - DataHierarchyEditorDialogBase.setObjectName("DataHierarchyEditorDialogBase") - DataHierarchyEditorDialogBase.resize(1271, 845) - DataHierarchyEditorDialogBase.setToolTip("") - self.gridLayout = QtWidgets.QGridLayout(DataHierarchyEditorDialogBase) - self.gridLayout.setContentsMargins(10, 10, 10, 10) - self.gridLayout.setObjectName("gridLayout") - self.mainWidget = QtWidgets.QWidget(parent=DataHierarchyEditorDialogBase) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mainWidget.sizePolicy().hasHeightForWidth()) - self.mainWidget.setSizePolicy(sizePolicy) - self.mainWidget.setObjectName("mainWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.mainWidget) - self.gridLayout_2.setObjectName("gridLayout_2") - self.mainGridLayout = QtWidgets.QGridLayout() - self.mainGridLayout.setContentsMargins(30, -1, 30, -1) - self.mainGridLayout.setObjectName("mainGridLayout") - self.typeMetadataTableView = QtWidgets.QTableView(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeMetadataTableView.sizePolicy().hasHeightForWidth()) - self.typeMetadataTableView.setSizePolicy(sizePolicy) - self.typeMetadataTableView.setSortingEnabled(False) - self.typeMetadataTableView.setObjectName("typeMetadataTableView") - self.typeMetadataTableView.horizontalHeader().setCascadingSectionResizes(True) - self.typeMetadataTableView.horizontalHeader().setSortIndicatorShown(False) - self.typeMetadataTableView.horizontalHeader().setStretchLastSection(False) - self.typeMetadataTableView.verticalHeader().setCascadingSectionResizes(True) - self.typeMetadataTableView.verticalHeader().setSortIndicatorShown(False) - self.typeMetadataTableView.verticalHeader().setStretchLastSection(False) - self.mainGridLayout.addWidget(self.typeMetadataTableView, 6, 0, 1, 1) - self.attachmentsHeaderHorizontalLayout = QtWidgets.QHBoxLayout() - self.attachmentsHeaderHorizontalLayout.setContentsMargins(0, 5, 0, 5) - self.attachmentsHeaderHorizontalLayout.setObjectName("attachmentsHeaderHorizontalLayout") - self.attachmentsShowHidePushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.attachmentsShowHidePushButton.sizePolicy().hasHeightForWidth()) - self.attachmentsShowHidePushButton.setSizePolicy(sizePolicy) - self.attachmentsShowHidePushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.attachmentsShowHidePushButton.setObjectName("attachmentsShowHidePushButton") - self.attachmentsHeaderHorizontalLayout.addWidget(self.attachmentsShowHidePushButton) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.attachmentsHeaderHorizontalLayout.addItem(spacerItem) - self.mainGridLayout.addLayout(self.attachmentsHeaderHorizontalLayout, 10, 0, 1, 1) - self.metadataGroupHorizontalLayout = QtWidgets.QHBoxLayout() - self.metadataGroupHorizontalLayout.setContentsMargins(0, 5, 0, 5) - self.metadataGroupHorizontalLayout.setObjectName("metadataGroupHorizontalLayout") - self.metadataGroupLabel = QtWidgets.QLabel(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.metadataGroupLabel.sizePolicy().hasHeightForWidth()) - self.metadataGroupLabel.setSizePolicy(sizePolicy) - self.metadataGroupLabel.setMinimumSize(QtCore.QSize(130, 0)) - self.metadataGroupLabel.setObjectName("metadataGroupLabel") - self.metadataGroupHorizontalLayout.addWidget(self.metadataGroupLabel) - self.metadataGroupComboBox = QtWidgets.QComboBox(parent=self.mainWidget) - self.metadataGroupComboBox.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.metadataGroupComboBox.sizePolicy().hasHeightForWidth()) - self.metadataGroupComboBox.setSizePolicy(sizePolicy) - self.metadataGroupComboBox.setMinimumSize(QtCore.QSize(200, 0)) - self.metadataGroupComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents) - self.metadataGroupComboBox.setObjectName("metadataGroupComboBox") - self.metadataGroupHorizontalLayout.addWidget(self.metadataGroupComboBox) - self.addMetadataGroupLineEdit = QtWidgets.QLineEdit(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.addMetadataGroupLineEdit.sizePolicy().hasHeightForWidth()) - self.addMetadataGroupLineEdit.setSizePolicy(sizePolicy) - self.addMetadataGroupLineEdit.setMinimumSize(QtCore.QSize(0, 0)) - self.addMetadataGroupLineEdit.setClearButtonEnabled(True) - self.addMetadataGroupLineEdit.setObjectName("addMetadataGroupLineEdit") - self.metadataGroupHorizontalLayout.addWidget(self.addMetadataGroupLineEdit) - self.addMetadataGroupPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.addMetadataGroupPushButton.sizePolicy().hasHeightForWidth()) - self.addMetadataGroupPushButton.setSizePolicy(sizePolicy) - self.addMetadataGroupPushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.addMetadataGroupPushButton.setStatusTip("") - self.addMetadataGroupPushButton.setObjectName("addMetadataGroupPushButton") - self.metadataGroupHorizontalLayout.addWidget(self.addMetadataGroupPushButton) - self.deleteMetadataGroupPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.deleteMetadataGroupPushButton.sizePolicy().hasHeightForWidth()) - self.deleteMetadataGroupPushButton.setSizePolicy(sizePolicy) - self.deleteMetadataGroupPushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.deleteMetadataGroupPushButton.setObjectName("deleteMetadataGroupPushButton") - self.metadataGroupHorizontalLayout.addWidget(self.deleteMetadataGroupPushButton) - self.mainGridLayout.addLayout(self.metadataGroupHorizontalLayout, 3, 0, 1, 1) - self.datatypeHorizontalLayout = QtWidgets.QHBoxLayout() - self.datatypeHorizontalLayout.setContentsMargins(0, 5, 0, 5) - self.datatypeHorizontalLayout.setObjectName("datatypeHorizontalLayout") - self.typeLabel = QtWidgets.QLabel(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeLabel.sizePolicy().hasHeightForWidth()) - self.typeLabel.setSizePolicy(sizePolicy) - self.typeLabel.setMinimumSize(QtCore.QSize(130, 0)) - self.typeLabel.setObjectName("typeLabel") - self.datatypeHorizontalLayout.addWidget(self.typeLabel) - self.typeComboBox = QtWidgets.QComboBox(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeComboBox.sizePolicy().hasHeightForWidth()) - self.typeComboBox.setSizePolicy(sizePolicy) - self.typeComboBox.setMinimumSize(QtCore.QSize(200, 0)) - self.typeComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents) - self.typeComboBox.setObjectName("typeComboBox") - self.datatypeHorizontalLayout.addWidget(self.typeComboBox) - self.typeDisplayedTitleLineEdit = QtWidgets.QLineEdit(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeDisplayedTitleLineEdit.sizePolicy().hasHeightForWidth()) - self.typeDisplayedTitleLineEdit.setSizePolicy(sizePolicy) - self.typeDisplayedTitleLineEdit.setMinimumSize(QtCore.QSize(0, 0)) - self.typeDisplayedTitleLineEdit.setText("") - self.typeDisplayedTitleLineEdit.setClearButtonEnabled(True) - self.typeDisplayedTitleLineEdit.setObjectName("typeDisplayedTitleLineEdit") - self.datatypeHorizontalLayout.addWidget(self.typeDisplayedTitleLineEdit) - self.typeIriLineEdit = QtWidgets.QLineEdit(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeIriLineEdit.sizePolicy().hasHeightForWidth()) - self.typeIriLineEdit.setSizePolicy(sizePolicy) - self.typeIriLineEdit.setClearButtonEnabled(True) - self.typeIriLineEdit.setObjectName("typeIriLineEdit") - self.datatypeHorizontalLayout.addWidget(self.typeIriLineEdit) - self.addTypePushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.addTypePushButton.sizePolicy().hasHeightForWidth()) - self.addTypePushButton.setSizePolicy(sizePolicy) - self.addTypePushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.addTypePushButton.setObjectName("addTypePushButton") - self.datatypeHorizontalLayout.addWidget(self.addTypePushButton) - self.deleteTypePushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.deleteTypePushButton.sizePolicy().hasHeightForWidth()) - self.deleteTypePushButton.setSizePolicy(sizePolicy) - self.deleteTypePushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.deleteTypePushButton.setObjectName("deleteTypePushButton") - self.datatypeHorizontalLayout.addWidget(self.deleteTypePushButton) - self.mainGridLayout.addLayout(self.datatypeHorizontalLayout, 1, 0, 1, 1) - self.metadataTableButtonHorizontalLayout = QtWidgets.QHBoxLayout() - self.metadataTableButtonHorizontalLayout.setContentsMargins(0, 5, 0, 5) - self.metadataTableButtonHorizontalLayout.setObjectName("metadataTableButtonHorizontalLayout") - self.addMetadataRowPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.addMetadataRowPushButton.sizePolicy().hasHeightForWidth()) - self.addMetadataRowPushButton.setSizePolicy(sizePolicy) - self.addMetadataRowPushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.addMetadataRowPushButton.setObjectName("addMetadataRowPushButton") - self.metadataTableButtonHorizontalLayout.addWidget(self.addMetadataRowPushButton) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.metadataTableButtonHorizontalLayout.addItem(spacerItem1) - self.mainGridLayout.addLayout(self.metadataTableButtonHorizontalLayout, 8, 0, 1, 1) - self.attachmentTableButtonsHorizontalLayout = QtWidgets.QHBoxLayout() - self.attachmentTableButtonsHorizontalLayout.setContentsMargins(0, 5, 0, 5) - self.attachmentTableButtonsHorizontalLayout.setObjectName("attachmentTableButtonsHorizontalLayout") - self.addAttachmentPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - self.addAttachmentPushButton.setMinimumSize(QtCore.QSize(200, 0)) - self.addAttachmentPushButton.setObjectName("addAttachmentPushButton") - self.attachmentTableButtonsHorizontalLayout.addWidget(self.addAttachmentPushButton) - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.attachmentTableButtonsHorizontalLayout.addItem(spacerItem2) - self.mainGridLayout.addLayout(self.attachmentTableButtonsHorizontalLayout, 13, 0, 1, 1) - self.metadataTableHeaderHorizontalLayout = QtWidgets.QHBoxLayout() - self.metadataTableHeaderHorizontalLayout.setContentsMargins(-1, 5, -1, 5) - self.metadataTableHeaderHorizontalLayout.setObjectName("metadataTableHeaderHorizontalLayout") - self.metadataTableHeaderLabel = QtWidgets.QLabel(parent=self.mainWidget) - font = QtGui.QFont() - font.setBold(True) - self.metadataTableHeaderLabel.setFont(font) - self.metadataTableHeaderLabel.setObjectName("metadataTableHeaderLabel") - self.metadataTableHeaderHorizontalLayout.addWidget(self.metadataTableHeaderLabel) - self.mainGridLayout.addLayout(self.metadataTableHeaderHorizontalLayout, 4, 0, 1, 1) - self.typeAttachmentsTableView = QtWidgets.QTableView(parent=self.mainWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.typeAttachmentsTableView.sizePolicy().hasHeightForWidth()) - self.typeAttachmentsTableView.setSizePolicy(sizePolicy) - self.typeAttachmentsTableView.setObjectName("typeAttachmentsTableView") - self.typeAttachmentsTableView.horizontalHeader().setStretchLastSection(False) - self.mainGridLayout.addWidget(self.typeAttachmentsTableView, 12, 0, 1, 1) - self.headerHorizontalLayout = QtWidgets.QHBoxLayout() - self.headerHorizontalLayout.setContentsMargins(0, 20, 0, 20) - self.headerHorizontalLayout.setObjectName("headerHorizontalLayout") - self.headerLabel = QtWidgets.QLabel(parent=self.mainWidget) - font = QtGui.QFont() - font.setPointSize(14) - font.setBold(True) - self.headerLabel.setFont(font) - self.headerLabel.setObjectName("headerLabel") - self.headerHorizontalLayout.addWidget(self.headerLabel) - spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.headerHorizontalLayout.addItem(spacerItem3) - self.saveDataHierarchyPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - self.saveDataHierarchyPushButton.setObjectName("saveDataHierarchyPushButton") - self.headerHorizontalLayout.addWidget(self.saveDataHierarchyPushButton) - self.helpPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - self.helpPushButton.setObjectName("helpPushButton") - self.headerHorizontalLayout.addWidget(self.helpPushButton) - self.cancelPushButton = QtWidgets.QPushButton(parent=self.mainWidget) - self.cancelPushButton.setObjectName("cancelPushButton") - self.headerHorizontalLayout.addWidget(self.cancelPushButton) - self.mainGridLayout.addLayout(self.headerHorizontalLayout, 0, 0, 1, 1) - self.gridLayout_2.addLayout(self.mainGridLayout, 0, 0, 1, 1) - self.gridLayout.addWidget(self.mainWidget, 0, 1, 1, 1) - - self.retranslateUi(DataHierarchyEditorDialogBase) - QtCore.QMetaObject.connectSlotsByName(DataHierarchyEditorDialogBase) - - def retranslateUi(self, DataHierarchyEditorDialogBase): - _translate = QtCore.QCoreApplication.translate - DataHierarchyEditorDialogBase.setWindowTitle(_translate("DataHierarchyEditorDialogBase", "Data Hierarchy Editor")) - self.typeMetadataTableView.setToolTip(_translate("DataHierarchyEditorDialogBase", "Table for all metadata associated with the Data Type. Add \"comment\" or \"content\" for editable text fields, \"image\" for image support, or enter another Data Type to enable links.")) - self.attachmentsShowHidePushButton.setText(_translate("DataHierarchyEditorDialogBase", "Show/Hide Attachments")) - self.metadataGroupLabel.setText(_translate("DataHierarchyEditorDialogBase", "Metadata Group")) - self.metadataGroupComboBox.setToolTip(_translate("DataHierarchyEditorDialogBase", "Select the group of metadata to be listed below in the table")) - self.addMetadataGroupLineEdit.setToolTip(_translate("DataHierarchyEditorDialogBase", "Enter the new group to be added to the data type")) - self.addMetadataGroupLineEdit.setPlaceholderText(_translate("DataHierarchyEditorDialogBase", "Enter the new group to be added")) - self.addMetadataGroupPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Add a new group of metadata to the data type, table below will be reset to empty list!")) - self.addMetadataGroupPushButton.setText(_translate("DataHierarchyEditorDialogBase", "+ Add")) - self.deleteMetadataGroupPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Delete the selected group in the group combobox")) - self.deleteMetadataGroupPushButton.setText(_translate("DataHierarchyEditorDialogBase", "- Delete")) - self.typeLabel.setText(_translate("DataHierarchyEditorDialogBase", "Data Type")) - self.typeComboBox.setToolTip(_translate("DataHierarchyEditorDialogBase", "Select the type from the loaded data hierarchy types")) - self.typeDisplayedTitleLineEdit.setToolTip(_translate("DataHierarchyEditorDialogBase", "Modify the displayed title property of the type")) - self.typeDisplayedTitleLineEdit.setPlaceholderText(_translate("DataHierarchyEditorDialogBase", "Modify the type displayed title here")) - self.typeIriLineEdit.setToolTip(_translate("DataHierarchyEditorDialogBase", "Enter the link/iri to be associated with this data-type")) - self.typeIriLineEdit.setPlaceholderText(_translate("DataHierarchyEditorDialogBase", "Enter the IRI for the type")) - self.addTypePushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Add a new type (structural or normal type) to the data hierarchy data set.")) - self.addTypePushButton.setText(_translate("DataHierarchyEditorDialogBase", "+ Add")) - self.deleteTypePushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Delete the type with the full metadata and attachments completely")) - self.deleteTypePushButton.setText(_translate("DataHierarchyEditorDialogBase", "- Delete")) - self.addMetadataRowPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Add a new metadata row to the above table with empty values")) - self.addMetadataRowPushButton.setText(_translate("DataHierarchyEditorDialogBase", "+ Add Metadata")) - self.addAttachmentPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Add a new attachment row to the above table with empty values")) - self.addAttachmentPushButton.setText(_translate("DataHierarchyEditorDialogBase", "+ Add Attachment")) - self.metadataTableHeaderLabel.setText(_translate("DataHierarchyEditorDialogBase", "Metadata")) - self.typeAttachmentsTableView.setToolTip(_translate("DataHierarchyEditorDialogBase", "Table which displays the attachments for the above selected data type")) - self.headerLabel.setText(_translate("DataHierarchyEditorDialogBase", "Edit the data hierarchy for the PASTA-ELN projects")) - self.saveDataHierarchyPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Save loaded data hierarchy in local database")) - self.saveDataHierarchyPushButton.setText(_translate("DataHierarchyEditorDialogBase", "Save")) - self.helpPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Navigate to the help page")) - self.helpPushButton.setText(_translate("DataHierarchyEditorDialogBase", "Help")) - self.cancelPushButton.setToolTip(_translate("DataHierarchyEditorDialogBase", "Close the editor")) - self.cancelPushButton.setText(_translate("DataHierarchyEditorDialogBase", "Cancel")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - DataHierarchyEditorDialogBase = QtWidgets.QWidget() - ui = Ui_DataHierarchyEditorDialogBase() - ui.setupUi(DataHierarchyEditorDialogBase) - DataHierarchyEditorDialogBase.show() - sys.exit(app.exec()) + def setupUi(self, DataHierarchyEditorDialogBase): + if not DataHierarchyEditorDialogBase.objectName(): + DataHierarchyEditorDialogBase.setObjectName(u"DataHierarchyEditorDialogBase") + DataHierarchyEditorDialogBase.resize(1254, 750) + self.gridLayout = QGridLayout(DataHierarchyEditorDialogBase) + self.gridLayout.setObjectName(u"gridLayout") + self.gridLayout.setContentsMargins(10, 10, 10, 10) + self.mainWidget = QWidget(DataHierarchyEditorDialogBase) + self.mainWidget.setObjectName(u"mainWidget") + sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.mainWidget.sizePolicy().hasHeightForWidth()) + self.mainWidget.setSizePolicy(sizePolicy) + self.gridLayout_2 = QGridLayout(self.mainWidget) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.mainGridLayout = QGridLayout() + self.mainGridLayout.setObjectName(u"mainGridLayout") + self.mainGridLayout.setContentsMargins(30, -1, 30, -1) + self.typeMetadataTableView = QTableView(self.mainWidget) + self.typeMetadataTableView.setObjectName(u"typeMetadataTableView") + sizePolicy.setHeightForWidth(self.typeMetadataTableView.sizePolicy().hasHeightForWidth()) + self.typeMetadataTableView.setSizePolicy(sizePolicy) + self.typeMetadataTableView.setSortingEnabled(False) + self.typeMetadataTableView.horizontalHeader().setCascadingSectionResizes(True) + self.typeMetadataTableView.horizontalHeader().setProperty("showSortIndicator", False) + self.typeMetadataTableView.horizontalHeader().setStretchLastSection(False) + self.typeMetadataTableView.verticalHeader().setCascadingSectionResizes(True) + self.typeMetadataTableView.verticalHeader().setProperty("showSortIndicator", False) + self.typeMetadataTableView.verticalHeader().setStretchLastSection(False) + + self.mainGridLayout.addWidget(self.typeMetadataTableView, 6, 0, 1, 1) + + self.attachmentsHeaderHorizontalLayout = QHBoxLayout() + self.attachmentsHeaderHorizontalLayout.setObjectName(u"attachmentsHeaderHorizontalLayout") + self.attachmentsHeaderHorizontalLayout.setContentsMargins(0, 5, 0, 5) + self.attachmentsShowHidePushButton = QPushButton(self.mainWidget) + self.attachmentsShowHidePushButton.setObjectName(u"attachmentsShowHidePushButton") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.attachmentsShowHidePushButton.sizePolicy().hasHeightForWidth()) + self.attachmentsShowHidePushButton.setSizePolicy(sizePolicy1) + self.attachmentsShowHidePushButton.setMinimumSize(QSize(200, 0)) + + self.attachmentsHeaderHorizontalLayout.addWidget(self.attachmentsShowHidePushButton) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.attachmentsHeaderHorizontalLayout.addItem(self.horizontalSpacer_2) + + + self.mainGridLayout.addLayout(self.attachmentsHeaderHorizontalLayout, 10, 0, 1, 1) + + self.metadataGroupHorizontalLayout = QHBoxLayout() + self.metadataGroupHorizontalLayout.setObjectName(u"metadataGroupHorizontalLayout") + self.metadataGroupHorizontalLayout.setContentsMargins(0, 5, 0, 5) + self.metadataGroupLabel = QLabel(self.mainWidget) + self.metadataGroupLabel.setObjectName(u"metadataGroupLabel") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.metadataGroupLabel.sizePolicy().hasHeightForWidth()) + self.metadataGroupLabel.setSizePolicy(sizePolicy2) + self.metadataGroupLabel.setMinimumSize(QSize(130, 0)) + + self.metadataGroupHorizontalLayout.addWidget(self.metadataGroupLabel) + + self.metadataGroupComboBox = QComboBox(self.mainWidget) + self.metadataGroupComboBox.setObjectName(u"metadataGroupComboBox") + self.metadataGroupComboBox.setEnabled(True) + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.metadataGroupComboBox.sizePolicy().hasHeightForWidth()) + self.metadataGroupComboBox.setSizePolicy(sizePolicy3) + self.metadataGroupComboBox.setMinimumSize(QSize(200, 0)) + self.metadataGroupComboBox.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) + + self.metadataGroupHorizontalLayout.addWidget(self.metadataGroupComboBox) + + self.addMetadataGroupLineEdit = QLineEdit(self.mainWidget) + self.addMetadataGroupLineEdit.setObjectName(u"addMetadataGroupLineEdit") + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.addMetadataGroupLineEdit.sizePolicy().hasHeightForWidth()) + self.addMetadataGroupLineEdit.setSizePolicy(sizePolicy4) + self.addMetadataGroupLineEdit.setMinimumSize(QSize(0, 0)) + self.addMetadataGroupLineEdit.setClearButtonEnabled(True) + + self.metadataGroupHorizontalLayout.addWidget(self.addMetadataGroupLineEdit) + + self.addMetadataGroupPushButton = QPushButton(self.mainWidget) + self.addMetadataGroupPushButton.setObjectName(u"addMetadataGroupPushButton") + sizePolicy3.setHeightForWidth(self.addMetadataGroupPushButton.sizePolicy().hasHeightForWidth()) + self.addMetadataGroupPushButton.setSizePolicy(sizePolicy3) + self.addMetadataGroupPushButton.setMinimumSize(QSize(200, 0)) + + self.metadataGroupHorizontalLayout.addWidget(self.addMetadataGroupPushButton) + + self.deleteMetadataGroupPushButton = QPushButton(self.mainWidget) + self.deleteMetadataGroupPushButton.setObjectName(u"deleteMetadataGroupPushButton") + sizePolicy3.setHeightForWidth(self.deleteMetadataGroupPushButton.sizePolicy().hasHeightForWidth()) + self.deleteMetadataGroupPushButton.setSizePolicy(sizePolicy3) + self.deleteMetadataGroupPushButton.setMinimumSize(QSize(200, 0)) + + self.metadataGroupHorizontalLayout.addWidget(self.deleteMetadataGroupPushButton) + + + self.mainGridLayout.addLayout(self.metadataGroupHorizontalLayout, 3, 0, 1, 1) + + self.datatypeHorizontalLayout = QHBoxLayout() + self.datatypeHorizontalLayout.setObjectName(u"datatypeHorizontalLayout") + self.datatypeHorizontalLayout.setContentsMargins(0, 5, 0, 5) + self.typeLabel = QLabel(self.mainWidget) + self.typeLabel.setObjectName(u"typeLabel") + sizePolicy2.setHeightForWidth(self.typeLabel.sizePolicy().hasHeightForWidth()) + self.typeLabel.setSizePolicy(sizePolicy2) + self.typeLabel.setMinimumSize(QSize(130, 0)) + + self.datatypeHorizontalLayout.addWidget(self.typeLabel) + + self.typeComboBox = QComboBox(self.mainWidget) + self.typeComboBox.setObjectName(u"typeComboBox") + sizePolicy5 = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed) + sizePolicy5.setHorizontalStretch(0) + sizePolicy5.setVerticalStretch(0) + sizePolicy5.setHeightForWidth(self.typeComboBox.sizePolicy().hasHeightForWidth()) + self.typeComboBox.setSizePolicy(sizePolicy5) + self.typeComboBox.setMinimumSize(QSize(400, 0)) + self.typeComboBox.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) + + self.datatypeHorizontalLayout.addWidget(self.typeComboBox) + + self.addTypePushButton = QPushButton(self.mainWidget) + self.addTypePushButton.setObjectName(u"addTypePushButton") + sizePolicy3.setHeightForWidth(self.addTypePushButton.sizePolicy().hasHeightForWidth()) + self.addTypePushButton.setSizePolicy(sizePolicy3) + self.addTypePushButton.setMinimumSize(QSize(200, 0)) + + self.datatypeHorizontalLayout.addWidget(self.addTypePushButton) + + self.editTypePushButton = QPushButton(self.mainWidget) + self.editTypePushButton.setObjectName(u"editTypePushButton") + sizePolicy3.setHeightForWidth(self.editTypePushButton.sizePolicy().hasHeightForWidth()) + self.editTypePushButton.setSizePolicy(sizePolicy3) + self.editTypePushButton.setMinimumSize(QSize(200, 0)) + + self.datatypeHorizontalLayout.addWidget(self.editTypePushButton) + + self.deleteTypePushButton = QPushButton(self.mainWidget) + self.deleteTypePushButton.setObjectName(u"deleteTypePushButton") + sizePolicy3.setHeightForWidth(self.deleteTypePushButton.sizePolicy().hasHeightForWidth()) + self.deleteTypePushButton.setSizePolicy(sizePolicy3) + self.deleteTypePushButton.setMinimumSize(QSize(200, 0)) + + self.datatypeHorizontalLayout.addWidget(self.deleteTypePushButton) + + + self.mainGridLayout.addLayout(self.datatypeHorizontalLayout, 1, 0, 1, 1) + + self.metadataTableButtonHorizontalLayout = QHBoxLayout() + self.metadataTableButtonHorizontalLayout.setObjectName(u"metadataTableButtonHorizontalLayout") + self.metadataTableButtonHorizontalLayout.setContentsMargins(0, 5, 0, 5) + self.addMetadataRowPushButton = QPushButton(self.mainWidget) + self.addMetadataRowPushButton.setObjectName(u"addMetadataRowPushButton") + sizePolicy1.setHeightForWidth(self.addMetadataRowPushButton.sizePolicy().hasHeightForWidth()) + self.addMetadataRowPushButton.setSizePolicy(sizePolicy1) + self.addMetadataRowPushButton.setMinimumSize(QSize(200, 0)) + + self.metadataTableButtonHorizontalLayout.addWidget(self.addMetadataRowPushButton) + + self.metadataButtonLayoutHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.metadataTableButtonHorizontalLayout.addItem(self.metadataButtonLayoutHorizontalSpacer) + + + self.mainGridLayout.addLayout(self.metadataTableButtonHorizontalLayout, 8, 0, 1, 1) + + self.attachmentTableButtonsHorizontalLayout = QHBoxLayout() + self.attachmentTableButtonsHorizontalLayout.setObjectName(u"attachmentTableButtonsHorizontalLayout") + self.attachmentTableButtonsHorizontalLayout.setContentsMargins(0, 5, 0, 5) + self.addAttachmentPushButton = QPushButton(self.mainWidget) + self.addAttachmentPushButton.setObjectName(u"addAttachmentPushButton") + self.addAttachmentPushButton.setMinimumSize(QSize(200, 0)) + + self.attachmentTableButtonsHorizontalLayout.addWidget(self.addAttachmentPushButton) + + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.attachmentTableButtonsHorizontalLayout.addItem(self.horizontalSpacer) + + + self.mainGridLayout.addLayout(self.attachmentTableButtonsHorizontalLayout, 13, 0, 1, 1) + + self.metadataTableHeaderHorizontalLayout = QHBoxLayout() + self.metadataTableHeaderHorizontalLayout.setObjectName(u"metadataTableHeaderHorizontalLayout") + self.metadataTableHeaderHorizontalLayout.setContentsMargins(-1, 5, -1, 5) + self.metadataTableHeaderLabel = QLabel(self.mainWidget) + self.metadataTableHeaderLabel.setObjectName(u"metadataTableHeaderLabel") + font = QFont() + font.setBold(True) + self.metadataTableHeaderLabel.setFont(font) + + self.metadataTableHeaderHorizontalLayout.addWidget(self.metadataTableHeaderLabel) + + + self.mainGridLayout.addLayout(self.metadataTableHeaderHorizontalLayout, 4, 0, 1, 1) + + self.typeAttachmentsTableView = QTableView(self.mainWidget) + self.typeAttachmentsTableView.setObjectName(u"typeAttachmentsTableView") + sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + sizePolicy6.setHorizontalStretch(0) + sizePolicy6.setVerticalStretch(0) + sizePolicy6.setHeightForWidth(self.typeAttachmentsTableView.sizePolicy().hasHeightForWidth()) + self.typeAttachmentsTableView.setSizePolicy(sizePolicy6) + self.typeAttachmentsTableView.horizontalHeader().setStretchLastSection(False) + + self.mainGridLayout.addWidget(self.typeAttachmentsTableView, 12, 0, 1, 1) + + self.headerHorizontalLayout = QHBoxLayout() + self.headerHorizontalLayout.setObjectName(u"headerHorizontalLayout") + self.headerHorizontalLayout.setContentsMargins(0, 20, 0, 20) + self.headerLabel = QLabel(self.mainWidget) + self.headerLabel.setObjectName(u"headerLabel") + font1 = QFont() + font1.setPointSize(14) + font1.setBold(True) + self.headerLabel.setFont(font1) + self.headerLabel.setMargin(0) + + self.headerHorizontalLayout.addWidget(self.headerLabel) + + self.headerHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.headerHorizontalLayout.addItem(self.headerHorizontalSpacer) + + self.saveDataHierarchyPushButton = QPushButton(self.mainWidget) + self.saveDataHierarchyPushButton.setObjectName(u"saveDataHierarchyPushButton") + + self.headerHorizontalLayout.addWidget(self.saveDataHierarchyPushButton) + + self.helpPushButton = QPushButton(self.mainWidget) + self.helpPushButton.setObjectName(u"helpPushButton") + + self.headerHorizontalLayout.addWidget(self.helpPushButton) + + self.cancelPushButton = QPushButton(self.mainWidget) + self.cancelPushButton.setObjectName(u"cancelPushButton") + + self.headerHorizontalLayout.addWidget(self.cancelPushButton) + + + self.mainGridLayout.addLayout(self.headerHorizontalLayout, 0, 0, 1, 1) + + + self.gridLayout_2.addLayout(self.mainGridLayout, 0, 0, 1, 1) + + + self.gridLayout.addWidget(self.mainWidget, 0, 1, 1, 1) + + + self.retranslateUi(DataHierarchyEditorDialogBase) + + QMetaObject.connectSlotsByName(DataHierarchyEditorDialogBase) + # setupUi + + def retranslateUi(self, DataHierarchyEditorDialogBase): + DataHierarchyEditorDialogBase.setWindowTitle(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Data Hierarchy Editor", None)) +#if QT_CONFIG(tooltip) + DataHierarchyEditorDialogBase.setToolTip("") +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(tooltip) + self.typeMetadataTableView.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Table for all metadata associated with the Data Type. Add \"comment\" or \"content\" for editable text fields, \"image\" for image support, or enter another Data Type to enable links.", None)) +#endif // QT_CONFIG(tooltip) + self.attachmentsShowHidePushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Show/Hide Attachments", None)) + self.metadataGroupLabel.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Metadata Group", None)) +#if QT_CONFIG(tooltip) + self.metadataGroupComboBox.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Select the group of metadata to be listed below in the table", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(tooltip) + self.addMetadataGroupLineEdit.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Enter the new group to be added to the data type", None)) +#endif // QT_CONFIG(tooltip) + self.addMetadataGroupLineEdit.setPlaceholderText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Enter the new group to be added", None)) +#if QT_CONFIG(tooltip) + self.addMetadataGroupPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Add a new group of metadata to the data type, table below will be reset to empty list!", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(statustip) + self.addMetadataGroupPushButton.setStatusTip("") +#endif // QT_CONFIG(statustip) + self.addMetadataGroupPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"+ Add", None)) +#if QT_CONFIG(tooltip) + self.deleteMetadataGroupPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Delete the selected group in the group combobox", None)) +#endif // QT_CONFIG(tooltip) + self.deleteMetadataGroupPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"- Delete", None)) + self.typeLabel.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Data Type", None)) +#if QT_CONFIG(tooltip) + self.typeComboBox.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Select the type from the loaded data hierarchy types", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(tooltip) + self.addTypePushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Add a new type (structural or normal type) to the data hierarchy data set.", None)) +#endif // QT_CONFIG(tooltip) + self.addTypePushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"+ Add", None)) + self.editTypePushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"* Edit", None)) +#if QT_CONFIG(tooltip) + self.deleteTypePushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Delete the type with the full metadata and attachments completely", None)) +#endif // QT_CONFIG(tooltip) + self.deleteTypePushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"- Delete", None)) +#if QT_CONFIG(tooltip) + self.addMetadataRowPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Add a new metadata row to the above table with empty values", None)) +#endif // QT_CONFIG(tooltip) + self.addMetadataRowPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"+ Add Metadata", None)) +#if QT_CONFIG(tooltip) + self.addAttachmentPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Add a new attachment row to the above table with empty values", None)) +#endif // QT_CONFIG(tooltip) + self.addAttachmentPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"+ Add Attachment", None)) + self.metadataTableHeaderLabel.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Metadata", None)) +#if QT_CONFIG(tooltip) + self.typeAttachmentsTableView.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Table which displays the attachments for the above selected data type", None)) +#endif // QT_CONFIG(tooltip) + self.headerLabel.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Edit the data hierarchy for the PASTA-ELN projects", None)) +#if QT_CONFIG(tooltip) + self.saveDataHierarchyPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Save loaded data hierarchy in local database", None)) +#endif // QT_CONFIG(tooltip) + self.saveDataHierarchyPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Save", None)) +#if QT_CONFIG(tooltip) + self.helpPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Navigate to the help page", None)) +#endif // QT_CONFIG(tooltip) + self.helpPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Help", None)) +#if QT_CONFIG(tooltip) + self.cancelPushButton.setToolTip(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Close the editor", None)) +#endif // QT_CONFIG(tooltip) + self.cancelPushButton.setText(QCoreApplication.translate("DataHierarchyEditorDialogBase", u"Cancel", None)) + # retranslateUi + diff --git a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.ui b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.ui index debe2d92..c9dd645c 100644 --- a/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.ui +++ b/pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.ui @@ -6,8 +6,8 @@ 0 0 - 1271 - 845 + 1254 + 750 @@ -116,7 +116,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -182,7 +182,7 @@ Select the group of metadata to be listed below in the table - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -296,14 +296,14 @@ - + 0 0 - 200 + 400 0 @@ -311,59 +311,34 @@ Select the type from the loaded data hierarchy types - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents - + - + 0 0 - 0 + 200 0 - Modify the displayed title property of the type + Add a new type (structural or normal type) to the data hierarchy data set. - - - - Modify the type displayed title here - - - true - - - - - - - - 0 - 0 - - - - Enter the link/iri to be associated with this data-type - - - Enter the IRI for the type - - - true + + Add - + 0 @@ -376,11 +351,8 @@ 0 - - Add a new type (structural or normal type) to the data hierarchy data set. - - + Add + * Edit @@ -447,10 +419,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Expanding + QSizePolicy::Policy::Expanding @@ -495,10 +467,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Expanding + QSizePolicy::Policy::Expanding @@ -581,7 +553,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/pasta_eln/GUI/data_hierarchy/data_type_info.py b/pasta_eln/GUI/data_hierarchy/data_type_info.py new file mode 100644 index 00000000..a7c14746 --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/data_type_info.py @@ -0,0 +1,236 @@ +""" Represents data type information. """ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: data_type_info.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. + +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# +# Author: Jithu Murugan +# Filename: data_type_info.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from typing import Any, Generator + +from pasta_eln.dataverse.incorrect_parameter_error import IncorrectParameterError + + +class DataTypeInfo: + """ + Represents metadata information for a data type. + + Explanation: + This class encapsulates various attributes related to a data type, including its datatype, title, IRI, icon, and shortcut. + It provides properties to access and modify these attributes while ensuring type safety. + It provides properties to access and modify these attributes while ensuring type safety. + + Attributes: + datatype (str | None): The type of the data. + title (str | None): The title of the data type. + iri (str | None): The Internationalized Resource Identifier for the data type. + icon (str | None): The icon associated with the data type. + shortcut (str | None): The shortcut for the data type. + + Methods: + __iter__(): Iterates over the attributes of the object and yields key-value pairs. + """ + + def __init__(self) -> None: + """ + Initializes a new instance of the DataTypeInfo class. + + Explanation: + This constructor sets up the initial state of the DataTypeInfo instance by initializing its attributes. + The attributes include datatype, title, IRI, icon, and shortcut, all set to their default values. + """ + self._datatype: str | None = "" + self._title: str | None = "" + self._iri: str | None = "" + self._icon: str | None = "" + self._shortcut: str | None = "" + + @property + def datatype(self) -> str | None: + """ + Retrieves the data type of the information. + + Explanation: + This property provides access to the internal attribute that stores the data type. + It allows users to retrieve the current data type without modifying it. + + Returns: + str: The current data type. + """ + return self._datatype + + @datatype.setter + def datatype(self, datatype: str | None) -> None: + """ + Sets the data type of the information. + + Explanation: + This setter method allows the user to update the internal data type attribute. + It ensures that the provided value is a string; if not, it raises an IncorrectParameterError. + + Args: + datatype (str): The new data type to set. + + Raises: + IncorrectParameterError: If the provided datatype is not a string. + """ + if isinstance(datatype, str): + self._datatype = datatype + else: + raise IncorrectParameterError(f"Expected string type for datatype but got {type(datatype)}") + + @property + def title(self) -> str | None: + """ + Retrieves the title of the data type. + + Explanation: + This property provides access to the internal attribute that stores the title of the data type. + It allows users to retrieve the current title without modifying it. + + Returns: + str | None: The current title of the data type, or None if not set. + """ + return self._title + + @title.setter + def title(self, title: str | None) -> None: + """ + Sets the title of the data type. + + Explanation: + This setter method allows the user to update the internal title attribute of the data type. + It ensures that the provided value is either a string or None; if not, it raises an IncorrectParameterError. + + Args: + title (str | None): The new title to set for the data type. + + Raises: + IncorrectParameterError: If the provided title is not a string or None. + """ + if isinstance(title, str | None): + self._title = title + else: + raise IncorrectParameterError(f"Expected string type for displayed title but got {type(title)}") + + @property + def iri(self) -> str | None: + """ + Retrieves the Internationalized Resource Identifier (IRI) of the data type. + + Explanation: + This property provides access to the internal attribute that stores the IRI of the data type. + It allows users to retrieve the current IRI without modifying it. + + Returns: + str | None: The current IRI of the data type, or None if not set. + """ + return self._iri + + @iri.setter + def iri(self, iri: str | None) -> None: + """ + Sets the Internationalized Resource Identifier (IRI) of the data type. + + Explanation: + This setter method allows the user to update the internal IRI attribute of the data type. + It ensures that the provided value is either a string or None; if not, it raises an IncorrectParameterError. + + Args: + iri (str | None): The new IRI to set for the data type. + + Raises: + IncorrectParameterError: If the provided iri is not a string or None. + """ + if isinstance(iri, str | None): + self._iri = iri + else: + raise IncorrectParameterError(f"Expected string type for iri but got {type(iri)}") + + @property + def icon(self) -> str | None: + """ + Retrieves the icon associated with the data type. + + Explanation: + This property provides access to the internal attribute that stores the icon of the data type. + It allows users to retrieve the current icon without modifying it. + + Returns: + str | None: The current icon of the data type, or None if not set. + """ + return self._icon + + @icon.setter + def icon(self, icon: str | None) -> None: + """ + Sets the icon associated with the data type. + + Explanation: + This setter method allows the user to update the internal icon attribute of the data type. + It ensures that the provided value is either a string or None; if not, it raises an IncorrectParameterError. + + Args: + icon (str | None): The new icon to set for the data type. + + Raises: + IncorrectParameterError: If the provided icon is not a string or None. + """ + if isinstance(icon, str | None): + self._icon = icon + else: + raise IncorrectParameterError(f"Expected string type for icon but got {type(icon)}") + + @property + def shortcut(self) -> str | None: + """ + Retrieves the shortcut associated with the data type. + + Explanation: + This property provides access to the internal attribute that stores the shortcut for the data type. + It allows users to retrieve the current shortcut without modifying it. + + Returns: + str | None: The current shortcut of the data type, or None if not set. + """ + return self._shortcut + + @shortcut.setter + def shortcut(self, shortcut: str | None) -> None: + """ + Sets the shortcut associated with the data type. + + Explanation: + This setter method allows the user to update the internal shortcut attribute of the data type. + It ensures that the provided value is either a string or None; if not, it raises an IncorrectParameterError. + + Args: + shortcut (str | None): The new shortcut to set for the data type. + + Raises: + IncorrectParameterError: If the provided shortcut is not a string or None. + """ + if isinstance(shortcut, str | None): + self._shortcut = shortcut + else: + raise IncorrectParameterError(f"Expected string type for shortcut but got {type(shortcut)}") + + def __iter__(self) -> Generator[tuple[str, Any], None, None]: + """ + Iterates over the attributes of the object and yields key-value pairs. + + Yields: + tuple[str, Any]: A tuple containing the attribute name and its corresponding value. + + """ + for key in self.__dict__: + yield key[1:], getattr(self, key) diff --git a/pasta_eln/GUI/data_hierarchy/data_type_info_validator.py b/pasta_eln/GUI/data_hierarchy/data_type_info_validator.py new file mode 100644 index 00000000..2a253807 --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/data_type_info_validator.py @@ -0,0 +1,46 @@ +""" Provides methods to validate the properties of DataTypeInfo instances. """ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: data_type_info_validator.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from pasta_eln.GUI.data_hierarchy.data_type_info import DataTypeInfo + + +class DataTypeInfoValidator: + """ + Validator for DataTypeInfo instances. + + This class provides methods to validate the properties of DataTypeInfo objects. It ensures that + the required fields are present and correctly formatted, raising exceptions when validation fails. + + Methods: + validate(data_type_info): Validates the provided DataTypeInfo instance. + + """ + + @staticmethod + def validate(data_type_info: DataTypeInfo) -> None: + """ + Validate the provided DataTypeInfo instance to ensure it meets required criteria. + + This static method checks if the input is an instance of DataTypeInfo and verifies that + both the datatype and title properties are set. If any of these conditions are not met, + appropriate exceptions are raised to indicate the specific validation failure. + + Args: + data_type_info (DataTypeInfo): The DataTypeInfo instance to validate. + + Raises: + TypeError: If data_type_info is not an instance of DataTypeInfo. + ValueError: If the datatype or title properties are missing. + """ + if not isinstance(data_type_info, DataTypeInfo): + raise TypeError(f"Expected DataTypeInfo type for data_type_info but got {type(data_type_info)}!") + if not data_type_info.datatype: + raise ValueError("Data type property is required!") + if not data_type_info.title: + raise ValueError("Displayed title property is required!") diff --git a/pasta_eln/GUI/data_hierarchy/edit_type_dialog.py b/pasta_eln/GUI/data_hierarchy/edit_type_dialog.py new file mode 100644 index 00000000..813c0d13 --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/edit_type_dialog.py @@ -0,0 +1,176 @@ +""" A dialog for editing an existing data type within the application. """ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: edit_type_dialog.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from typing import Any, Callable + +import qtawesome as qta +from PySide6 import QtWidgets +from PySide6.QtWidgets import QMessageBox + +from pasta_eln.GUI.data_hierarchy.type_dialog import TypeDialog +from pasta_eln.GUI.data_hierarchy.utility_functions import generate_data_hierarchy_type, show_message + + +class EditTypeDialog(TypeDialog): + """ + A dialog for editing an existing data type within the application. + + This class extends the TypeDialog to provide functionality for modifying existing data types. + It initializes the dialog with specific UI elements and behavior tailored for editing, including disabling certain fields and setting tooltips. + + Args: + accepted_callback (Callable[[], None]): A callback function to be executed when the action is accepted. + rejected_callback (Callable[[], None]): A callback function to be executed when the action is rejected. + + Attributes: + selected_data_hierarchy_type (dict[str, Any]): The currently selected data hierarchy type. + selected_data_hierarchy_type_name (str): The name of the currently selected data hierarchy type. + """ + + def __init__(self, + accepted_callback: Callable[[], None], + rejected_callback: Callable[[], None]): + """ + Initializes the dialog for editing an existing data type. + + This constructor sets up the dialog by initializing the parent class and configuring the user interface elements. + It disables the type title field to prevent modifications and sets tooltips to guide the user on how to interact with the dialog. + + Args: + self: The instance of the class. + accepted_callback (Callable[[], None]): A callback function to be executed when the action is accepted. + rejected_callback (Callable[[], None]): A callback function to be executed when the action is rejected. + """ + super().__init__(accepted_callback, rejected_callback) + self.selected_data_hierarchy_type: dict[str, Any] = {} + self.selected_data_hierarchy_type_name: str = "" + self.instance.setWindowTitle("Edit existing type") + self.typeLineEdit.setDisabled(True) + self.typeLineEdit.setToolTip("Changing type title disabled for edits!") + self.typeDisplayedTitleLineEdit.setToolTip(self.typeDisplayedTitleLineEdit.toolTip().replace("Enter", "Modify")) + self.iriLineEdit.setToolTip(self.iriLineEdit.toolTip().replace("Enter", "Modify")) + self.shortcutLineEdit.setToolTip(self.shortcutLineEdit.toolTip().replace("Enter", "Modify")) + self.iconComboBox.currentIndexChanged[int].connect(self.set_icon) + self.typeLineEdit.textChanged[str].connect(self.type_changed) + + def show(self) -> None: + """ + Displays the dialog for editing the selected data type. + + This method initializes the dialog's user interface elements with the current values of the selected data hierarchy type. + It sets the text fields and combo boxes to reflect the properties of the selected type, allowing the user to view and modify the data. + """ + super().show() + self.typeLineEdit.setText(self.selected_data_hierarchy_type_name) + if not self.selected_data_hierarchy_type: + self.logger.warning("Invalid data type: {%s}", self.selected_data_hierarchy_type) + return + self.iriLineEdit.setText(self.selected_data_hierarchy_type.get("IRI") or "") + self.typeDisplayedTitleLineEdit.setText(self.selected_data_hierarchy_type.get("title") or "") + self.shortcutLineEdit.setText(self.selected_data_hierarchy_type.get("shortcut") or "") + icon = self.selected_data_hierarchy_type.get("icon") or "" + self.iconFontCollectionComboBox.setCurrentText(icon.split(".")[0] if icon else "") + self.iconComboBox.setCurrentText(self.selected_data_hierarchy_type.get("icon") or "No value") + + def accepted_callback(self) -> None: + """ + Handles the acceptance of updates to the selected data type. + + This method validates the type information and checks if the selected data hierarchy type exists. + If the type information is valid and the type exists, it logs the update, generates the updated type, + and closes the dialog. If the type does not exist, it displays a warning message to the user. + """ + if not self.validate_type_info(): + return + if not self.selected_data_hierarchy_type: + show_message( + f"Error update scenario: Type (datatype: {self.type_info.datatype} " + f"displayed title: {self.type_info.title}) does not exists!!....", + QMessageBox.Icon.Warning) + else: + self.logger.info("User updated the existing type: Datatype: {%s}, Displayed Title: {%s}", + self.type_info.datatype, + self.type_info.title) + updated_type = generate_data_hierarchy_type(self.type_info) + self.selected_data_hierarchy_type.update(updated_type) + self.instance.close() + self.accepted_callback_parent() + + def set_icon(self, new_index: int) -> None: + """ + Sets the icon for the specified index in the icon combo box. + + This method updates the icon at the given index if the index is valid and the current icon name is not "No value". + If the index is negative, it logs a warning and does not perform any updates. + + Args: + self: The instance of the class. + new_index (int): The index at which to set the icon. + """ + if new_index < 0: + self.logger.warning("Invalid index: {%s}", new_index) + return + new_icon_name = self.iconComboBox.currentText() + if new_icon_name and new_icon_name != "No value": + self.iconComboBox.setItemIcon(new_index, qta.icon(new_icon_name)) + + def type_changed(self, new_data_type: str) -> None: + """ + Updates the state of the dialog based on the selected data type. + + This method disables or enables certain UI components depending on whether the provided data type is structural. + If the data type is empty or None, it logs a warning and does not change the state of the components. + + Args: + self: The instance of the class. + new_data_type (str): The new data type selected by the user. + """ + if not new_data_type: + self.logger.warning("Invalid data type: {%s}", new_data_type) + return + disable_if_structural = new_data_type in {"x0", "x1"} + self.shortcutLineEdit.setDisabled(disable_if_structural) + self.iconComboBox.setDisabled(disable_if_structural) + self.iconFontCollectionComboBox.setDisabled(disable_if_structural) + + def set_selected_data_hierarchy_type(self, data_hierarchy_type: dict[str, Any]) -> None: + """ + Updates the selected data hierarchy type for the instance. + + This method assigns the provided dictionary to the instance's selected data hierarchy type, + allowing the instance to keep track of the currently selected type for further operations. + + Args: + self: The instance of the class. + data_hierarchy_type (dict[str, Any]): A dictionary representing the selected data hierarchy type. + """ + self.selected_data_hierarchy_type = data_hierarchy_type + + def set_selected_data_hierarchy_type_name(self, datatype: str) -> None: + """ + Sets the name of the selected data hierarchy type. + + This method updates the internal state of the instance by assigning the provided data type name + to the selected data hierarchy type name attribute. It allows the instance to keep track of the + currently selected type name for further operations. + + Args: + self: The instance of the class. + datatype (str): The name of the selected data hierarchy type. + """ + self.selected_data_hierarchy_type_name = datatype + + +if __name__ == "__main__": + import sys + + app = QtWidgets.QApplication(sys.argv) + ui = EditTypeDialog(lambda: None, lambda: None) + ui.show() + sys.exit(app.exec()) diff --git a/pasta_eln/GUI/data_hierarchy/qtaicons_factory.py b/pasta_eln/GUI/data_hierarchy/qtaicons_factory.py new file mode 100644 index 00000000..5019b3e3 --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/qtaicons_factory.py @@ -0,0 +1,168 @@ +""" QTAIconsFactory class. """ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: icon_names_singleton.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +import logging +from typing import Any + +import qtawesome as qta + +from pasta_eln.dataverse.incorrect_parameter_error import IncorrectParameterError + + +class QTAIconsFactory: + """ + Singleton class for managing QTA icons. + + This class ensures that there is only one instance of QTAIconsFactory throughout the application. It initializes and manages icon names and font collections for use in the UI. + + Attributes: + logger (logging.Logger): Logger for the class. + _icon_names (dict[str, list[str]]): Dictionary to store icon names categorized by font collections. + _font_collections (list[str]): List of font collections used for icons. + _icons_initialized (bool): Flag indicating whether the icons have been initialized. + + Methods: + get_instance(): Returns the singleton instance of the class. + set_icon_names(): Initializes the icon names based on the available font collections. + font_collections: Property to get or set the font collections. + icon_names: Property to get or set the icon names. + """ + _instance: Any = None + + @classmethod + def get_instance(cls) -> Any: + """ + Returns the singleton instance of the class. This method ensures that only one instance of the class is created and returned. + + This class method checks if an instance already exists. If not, it creates a new instance and stores it in a class attribute. Subsequent calls to this method will return the existing instance. + + Args: + cls: The class itself. + + Returns: + An instance of the class. + + Examples: + instance1 = YourClassName.get_instance() + instance2 = YourClassName.get_instance() + assert instance1 is instance2 # Both calls return the same instance. + """ + if (not hasattr(cls, '_instance') + or not getattr(cls, '_instance')): + cls._instance = cls() + return cls._instance + + def __init__(self) -> None: + """ + Initializes the QTAIconsFactory instance. + + Explanation: + This method sets up the logger for the class and initializes the icon names and font collections. + It also calls the method to set the icon names, ensuring that the instance is ready for use. + """ + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + self._icon_names: dict[str, list[str]] = {} + self._font_collections = ['fa', 'fa5', 'fa5s', 'fa5b', 'ei', 'mdi', 'mdi6', 'ph', 'ri', 'msc'] + self._icons_initialized = False + self.set_icon_names() + + def set_icon_names(self) -> None: + """ + Initializes the icon names based on the available font collections. + + Explanation: + This method sets up the icon names for each font collection by retrieving the font maps from the qta resource. + If the icons have already been initialized, a warning is logged, and the method exits early. + The method populates the _icon_names dictionary with the available icons for each font collection. + """ + self._icon_names = {fc: [] for fc in self.font_collections} + if self._icons_initialized: + self.logger.warning("Icons already initialized!") + return + qta._instance() # pylint: disable=W0212 + font_maps = qta._resource['iconic'].charmap # pylint: disable=W0212 + if font_maps is None or not font_maps: + self.logger.warning("font_maps could not be found!") + return + for fc in self._font_collections: + self._icon_names[fc].append("No value") + for iconName in font_maps[fc]: + icon_name = f'{fc}.{iconName}' + self._icon_names[fc].append(icon_name) + self._icons_initialized = True + + @property + def font_collections(self) -> list[str]: + """ + Retrieves the list of font collections used for icons. + + Explanation: + This property returns the internal list of font collections that are available for use in the icon management system. + It provides access to the font collections without allowing modification of the internal state. + + Returns: + list[str]: The list of font collections. + """ + return self._font_collections + + @font_collections.setter + def font_collections(self, font_collections: list[str]) -> None: + """ + Sets the list of font collections used for icons. + + Explanation: + This setter method allows the user to update the internal list of font collections. + It ensures that the provided value is a list; if not, it raises an IncorrectParameterError. + + Args: + font_collections (list[str]): The new list of font collections to set. + + Raises: + IncorrectParameterError: If the provided font_collections is not a list. + """ + if isinstance(font_collections, list): + self._font_collections = font_collections + else: + raise IncorrectParameterError(f"Expected list type for font_collections but got {type(font_collections)}") + + @property + def icon_names(self) -> dict[str, list[str]]: + """ + Retrieves the dictionary of icon names categorized by font collections. + + Explanation: + This property checks if the icons have been initialized; if not, it calls the method to set the icon names. + It returns the internal dictionary containing the icon names for each font collection. + + Returns: + dict[str, list[str]]: A dictionary where the keys are font collection names and the values are lists of icon names. + """ + if not self._icons_initialized: + self.set_icon_names() + return self._icon_names + + @icon_names.setter + def icon_names(self, icon_names: dict[str, list[str]]) -> None: + """ + Sets the dictionary of icon names categorized by font collections. + + Explanation: + This setter method allows the user to update the internal dictionary of icon names. + It ensures that the provided value is a dictionary; if not, it raises an IncorrectParameterError. + + Args: + icon_names (dict[str, list[str]]): The new dictionary of icon names to set. + + Raises: + IncorrectParameterError: If the provided icon_names is not a dictionary. + """ + if isinstance(icon_names, dict): + self._icon_names = icon_names + else: + raise IncorrectParameterError(f"Expected list type for icon_names but got {type(icon_names)}") diff --git a/pasta_eln/GUI/data_hierarchy/terminology_lookup_service.py b/pasta_eln/GUI/data_hierarchy/terminology_lookup_service.py index 6125ba91..f9694ea5 100644 --- a/pasta_eln/GUI/data_hierarchy/terminology_lookup_service.py +++ b/pasta_eln/GUI/data_hierarchy/terminology_lookup_service.py @@ -105,7 +105,7 @@ def parse_web_result(self, results = reduce(lambda d, key: d.get(key) if d else None, result_keys, # type: ignore[arg-type, return-value] web_result) for item in results or []: - description = reduce(lambda d, key: d.get(key) if d else None, desc_keys, item) # type: ignore[attr-defined] + description = reduce(lambda d, key: d.get(key) if d else "", desc_keys, item) # type: ignore[attr-defined] is_duplicate = (item[duplicate_ontology_key] # type: ignore[operator] in duplicate_ontology_names) if duplicate_ontology_key else False if (description diff --git a/pasta_eln/GUI/data_hierarchy/type_dialog.py b/pasta_eln/GUI/data_hierarchy/type_dialog.py new file mode 100644 index 00000000..0e95f499 --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/type_dialog.py @@ -0,0 +1,347 @@ +""" Type dialog for the data hierarchy. """ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2023 +# +# Author: Jithu Murugan +# Filename: create_type_dialog.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. + +import logging +from typing import Any, Callable + +import qtawesome as qta +from PySide6 import QtCore, QtWidgets +from PySide6.QtCore import QRegularExpression +from PySide6.QtGui import QRegularExpressionValidator +from PySide6.QtWidgets import QDialog, QLineEdit, QMessageBox + +from pasta_eln.GUI.data_hierarchy.data_type_info import DataTypeInfo +from pasta_eln.GUI.data_hierarchy.data_type_info_validator import DataTypeInfoValidator +from pasta_eln.GUI.data_hierarchy.lookup_iri_action import LookupIriAction +from pasta_eln.GUI.data_hierarchy.qtaicons_factory import QTAIconsFactory +from pasta_eln.GUI.data_hierarchy.type_dialog_base import Ui_TypeDialogBase +from pasta_eln.GUI.data_hierarchy.utility_functions import show_message + + +class TypeDialog(Ui_TypeDialogBase): + """ + Represents a dialog for creating or modifying a data type. + + Explanation: + This class initializes a dialog that allows users to input and modify various attributes of a data type, + including its title, IRI, shortcut, and icon. It connects UI elements to their respective callback functions + and manages the interaction between the user and the underlying data model. + + Args: + accepted_callback (Callable[[], None]): Callback function to be called when the dialog is accepted. + rejected_callback (Callable[[], None]): Callback function to be called when the dialog is rejected. + + Attributes: + logger (logging.Logger): Logger for the class. + accepted_callback_parent (Callable[[], None]): Parent callback for accepted action. + rejected_callback_parent (Callable[[], None]): Parent callback for rejected action. + type_info (DataTypeInfo): DataTypeInfo instance to hold the data type information. + instance (QDialog): The dialog instance. + qta_icons (QTAIconsFactory): Singleton instance for managing icons. + + Methods: + setup_slots(): Connects UI elements to their respective callback functions. + set_data_type(datatype: str): Sets the data type in the type_info. + set_type_title(title: str): Sets the title in the type_info. + set_type_iri(iri: str): Sets the IRI in the type_info. + set_type_shortcut(shortcut: str): Sets the shortcut in the type_info. + set_type_icon(icon: str): Sets the icon in the type_info. + set_iri_lookup_action(lookup_term: str): Sets the IRI lookup action for the IRI line edit. + icon_font_collection_changed(font_collection: str): Updates the icon combo box based on the selected font collection. + populate_icons(font_collection: str): Populates the icon combo box with icons from the specified font collection. + show(): Displays the dialog. + clear_ui(): Clears the dialog UI. + validate_type_info(): Validates the data type information. + title_modified(new_title: str): Updates the IRI lookup action based on the modified title. + """ + + def __new__(cls, *_: Any, **__: Any) -> Any: + """ + Create a new instance of the TypeDialog class. + """ + return super(TypeDialog, cls).__new__(cls) + + def __init__(self, + accepted_callback: Callable[[], None], + rejected_callback: Callable[[], None]) -> None: + """ + Initializes the create type dialog + """ + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + self.accepted_callback_parent = accepted_callback + self.rejected_callback_parent = rejected_callback + self.type_info: DataTypeInfo = DataTypeInfo() + self.instance = QDialog() + super().setupUi(self.instance) + + self.qta_icons = QTAIconsFactory.get_instance() + self.iconFontCollectionComboBox.addItems(self.qta_icons.font_collections) + self.populate_icons(self.qta_icons.font_collections[0]) + + self.setup_slots() + + # Restricts the title input to allow anything except x or space + # as the first character which is reserved for structural level + self.typeLineEdit.setValidator(QRegularExpressionValidator(QRegularExpression("(?=^[^Ax])(?=[^ ]*)"))) + self.iconComboBox.completer().setCompletionMode(QtWidgets.QCompleter.CompletionMode.PopupCompletion) + self.set_iri_lookup_action("") + + def accepted_callback(self) -> None: + """ + Callback function to be executed when the dialog is accepted. + + Explanation: + This method serves as a placeholder for actions that should be taken when the user confirms their input + in the dialog. Currently, it does not perform any operations but can be extended in the future. + """ + return + + def rejected_callback(self) -> None: + """ + Callback function to be executed when the dialog is rejected. + + Explanation: + This method serves as a placeholder for actions that should be taken when the user cancels their input + in the dialog. Currently, it does not perform any operations but can be extended in the future. + """ + return + + def setup_slots(self) -> None: + """ + Connects UI elements to their respective callback functions. + + Explanation: + This method sets up the signal-slot connections for various UI components in the dialog. + It ensures that changes in the UI elements trigger the appropriate methods to handle those changes. + """ + self.iconFontCollectionComboBox.currentTextChanged[str].connect(self.icon_font_collection_changed) + self.typeDisplayedTitleLineEdit.textChanged[str].connect(self.title_modified) + self.typeDisplayedTitleLineEdit.textChanged[str].connect(self.set_type_title) + self.typeLineEdit.textChanged[str].connect(self.set_data_type) + self.iriLineEdit.textChanged[str].connect(self.set_type_iri) + self.shortcutLineEdit.textChanged[str].connect(self.set_type_shortcut) + self.iconComboBox.currentTextChanged[str].connect(self.set_type_icon) + self.buttonBox.rejected.connect(self.rejected_callback) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).clicked.connect(self.accepted_callback) + self.buttonBox.accepted.disconnect() + + def set_data_type(self, datatype: str) -> None: + """ + Sets the data type for the type information. + + Explanation: + This method updates the internal data type attribute of the type_info object. + It allows the user to specify the data type associated with the current context. + + Args: + datatype (str): The new data type to set. + """ + self.type_info.datatype = datatype + + def set_type_title(self, title: str) -> None: + """ + Sets the title for the type information. + + Explanation: + This method updates the internal title attribute of the type_info object. + It allows the user to specify the title associated with the current data type context. + + Args: + title (str): The new title to set. + """ + self.type_info.title = title + + def set_type_iri(self, iri: str) -> None: + """ + Sets the IRI (Internationalized Resource Identifier) for the type information. + + Explanation: + This method updates the internal IRI attribute of the type_info object. + It allows the user to specify the IRI associated with the current data type context. + + Args: + iri (str): The new IRI to set. + """ + self.type_info.iri = iri + + def set_type_shortcut(self, shortcut: str) -> None: + """ + Sets the shortcut for the type information. + + Explanation: + This method updates the internal shortcut attribute of the type_info object. + It allows the user to specify the shortcut associated with the current data type context. + + Args: + shortcut (str): The new shortcut to set. + """ + self.type_info.shortcut = shortcut + + def set_type_icon(self, icon: str) -> None: + """ + Sets the icon for the type information. + + Explanation: + This method updates the internal icon attribute of the type_info object. + It allows the user to specify the icon associated with the current data type context. + + Args: + icon (str): The new icon to set. + """ + self.type_info.icon = icon + + def set_iri_lookup_action(self, + lookup_term: str) -> None: + """ + Sets the IRI lookup action for the IRI line edit + Args: + lookup_term (str): Default lookup term to be used by the lookup service + + Returns: Nothing + + """ + for act in self.iriLineEdit.actions(): + if isinstance(act, LookupIriAction): + act.deleteLater() + self.iriLineEdit.addAction( + LookupIriAction(parent_line_edit=self.iriLineEdit, lookup_term=lookup_term), + QLineEdit.ActionPosition.TrailingPosition) + + def icon_font_collection_changed(self, font_collection: str) -> None: + """ + Updates the icon combo box based on the selected font collection. + + Explanation: + This method is triggered when the font collection changes. + It clears the current items in the icon combo box and populates it with icons from the newly selected font collection. + + Args: + font_collection (str): The name of the font collection that has been selected. + """ + self.iconComboBox.clear() + self.populate_icons(font_collection) + + def populate_icons(self, font_collection: str) -> None: + """ + Populates the icon combo box with icons from the specified font collection. + + Explanation: + This method checks if the provided font collection is valid. + If valid, it adds the icons associated with that collection to the icon combo box; otherwise, it logs a warning. + + Args: + font_collection (str): The name of the font collection from which to populate icons. + """ + if not font_collection or font_collection not in self.qta_icons.icon_names: + self.logger.warning("Invalid font collection!") + return + self.iconComboBox.addItem(self.qta_icons.icon_names[font_collection][0]) + for item in self.qta_icons.icon_names[font_collection][1:]: + self.iconComboBox.addItem(qta.icon(item), item) + + def show(self) -> None: + """ + Displays the dialog + + Returns: None + + """ + self.instance.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) + self.instance.show() + + def clear_ui(self) -> None: + """ + Clear the Dialog UI + + Returns: Nothing + + """ + self.typeLineEdit.clear() + self.typeDisplayedTitleLineEdit.clear() + self.iriLineEdit.clear() + self.shortcutLineEdit.clear() + self.iconFontCollectionComboBox.setCurrentIndex(0) + self.iconComboBox.setCurrentIndex(0) + + def validate_type_info(self) -> bool: + """ + Validates the type information stored in the type_info attribute. + + Explanation: + This method checks the validity of the data type information using the DataTypeInfoValidator. + If the validation fails, it displays an error message and logs the error, returning False; otherwise, it returns True. + + Returns: + bool: True if the type information is valid, False otherwise. + """ + valid_type = True + try: + DataTypeInfoValidator.validate(self.type_info) + except (TypeError, ValueError) as e: + show_message(str(e), QMessageBox.Icon.Warning) + valid_type = False + self.logger.error(str(e)) + return valid_type + + def title_modified(self, new_title: str) -> None: + """ + Updates the IRI lookup action based on the modified title. + + Explanation: + This method is called when the title is modified. + If the new title is not empty, it sets the IRI lookup action using the new title. + + Args: + new_title (str): The new title that has been set. + """ + if new_title: + self.set_iri_lookup_action(new_title) + + def set_selected_data_hierarchy_type(self, data_hierarchy_type: dict[str, Any]) -> None: + """ + Sets the selected data hierarchy type for the instance. + + This method is intended to update the internal state of the instance by assigning the provided + dictionary representing the selected data hierarchy type. It allows the instance to manage and + utilize the specified type in its operations. + + Args: + self: The instance of the class. + data_hierarchy_type (dict[str, Any]): A dictionary representing the selected data hierarchy type. + """ + return + + def set_selected_data_hierarchy_type_name(self, datatype: str) -> None: + """ + Sets the name of the selected data hierarchy type. + + This method updates the internal state of the instance by assigning the provided data type name + to the selected data hierarchy type name attribute. It allows the instance to keep track of the + currently selected type name for further operations. + + Args: + self: The instance of the class. + datatype (str): The name of the selected data hierarchy type. + """ + return + + def set_data_hierarchy_types(self, data_hierarchy_types: dict[str, Any]) -> None: + """ + Sets the data hierarchy types for the instance. + + This method updates the internal state of the instance by assigning the provided dictionary + of data hierarchy types. It allows the instance to manage and utilize the specified types + in its operations. + + Args: + self: The instance of the class. + data_hierarchy_types (dict[str, Any]): A dictionary containing data hierarchy types to be set. + """ + return diff --git a/pasta_eln/GUI/data_hierarchy/type_dialog_base.py b/pasta_eln/GUI/data_hierarchy/type_dialog_base.py new file mode 100644 index 00000000..5ae442da --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/type_dialog_base.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'type_dialog_base.ui' +## +## Created by: Qt User Interface Compiler version 6.7.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, + QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, + QLineEdit, QSizePolicy, QSpacerItem, QVBoxLayout, + QWidget) + +class Ui_TypeDialogBase(object): + def setupUi(self, TypeDialogBase): + if not TypeDialogBase.objectName(): + TypeDialogBase.setObjectName(u"TypeDialogBase") + TypeDialogBase.resize(733, 351) + self.gridLayout = QGridLayout(TypeDialogBase) + self.gridLayout.setObjectName(u"gridLayout") + self.mainVerticalLayout = QVBoxLayout() + self.mainVerticalLayout.setObjectName(u"mainVerticalLayout") + self.mainVerticalLayout.setContentsMargins(20, -1, 20, -1) + self.tileHorizontalLayout = QHBoxLayout() + self.tileHorizontalLayout.setObjectName(u"tileHorizontalLayout") + self.typeLabel = QLabel(TypeDialogBase) + self.typeLabel.setObjectName(u"typeLabel") + self.typeLabel.setMinimumSize(QSize(120, 0)) + + self.tileHorizontalLayout.addWidget(self.typeLabel) + + self.titleHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.tileHorizontalLayout.addItem(self.titleHorizontalSpacer) + + self.typeLineEdit = QLineEdit(TypeDialogBase) + self.typeLineEdit.setObjectName(u"typeLineEdit") + self.typeLineEdit.setClearButtonEnabled(True) + + self.tileHorizontalLayout.addWidget(self.typeLineEdit) + + + self.mainVerticalLayout.addLayout(self.tileHorizontalLayout) + + self.verticalSpacer1 = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.mainVerticalLayout.addItem(self.verticalSpacer1) + + self.displayedTitleHorizontalLayout = QHBoxLayout() + self.displayedTitleHorizontalLayout.setObjectName(u"displayedTitleHorizontalLayout") + self.typeDisplayedTitleLabel = QLabel(TypeDialogBase) + self.typeDisplayedTitleLabel.setObjectName(u"typeDisplayedTitleLabel") + self.typeDisplayedTitleLabel.setMinimumSize(QSize(120, 0)) + + self.displayedTitleHorizontalLayout.addWidget(self.typeDisplayedTitleLabel) + + self.displayedTitleHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.displayedTitleHorizontalLayout.addItem(self.displayedTitleHorizontalSpacer) + + self.typeDisplayedTitleLineEdit = QLineEdit(TypeDialogBase) + self.typeDisplayedTitleLineEdit.setObjectName(u"typeDisplayedTitleLineEdit") + self.typeDisplayedTitleLineEdit.setClearButtonEnabled(True) + + self.displayedTitleHorizontalLayout.addWidget(self.typeDisplayedTitleLineEdit) + + + self.mainVerticalLayout.addLayout(self.displayedTitleHorizontalLayout) + + self.verticalSpacer2 = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.mainVerticalLayout.addItem(self.verticalSpacer2) + + self.iriHorizontalLayout = QHBoxLayout() + self.iriHorizontalLayout.setObjectName(u"iriHorizontalLayout") + self.iriLabel = QLabel(TypeDialogBase) + self.iriLabel.setObjectName(u"iriLabel") + self.iriLabel.setMinimumSize(QSize(120, 0)) + + self.iriHorizontalLayout.addWidget(self.iriLabel) + + self.iriHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.iriHorizontalLayout.addItem(self.iriHorizontalSpacer) + + self.iriLineEdit = QLineEdit(TypeDialogBase) + self.iriLineEdit.setObjectName(u"iriLineEdit") + + self.iriHorizontalLayout.addWidget(self.iriLineEdit) + + + self.mainVerticalLayout.addLayout(self.iriHorizontalLayout) + + self.verticalSpacer3 = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.mainVerticalLayout.addItem(self.verticalSpacer3) + + self.shortcutHorizontalLayout = QHBoxLayout() + self.shortcutHorizontalLayout.setObjectName(u"shortcutHorizontalLayout") + self.shortcutLabel = QLabel(TypeDialogBase) + self.shortcutLabel.setObjectName(u"shortcutLabel") + self.shortcutLabel.setMinimumSize(QSize(120, 0)) + + self.shortcutHorizontalLayout.addWidget(self.shortcutLabel) + + self.shortcutHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.shortcutHorizontalLayout.addItem(self.shortcutHorizontalSpacer) + + self.shortcutLineEdit = QLineEdit(TypeDialogBase) + self.shortcutLineEdit.setObjectName(u"shortcutLineEdit") + + self.shortcutHorizontalLayout.addWidget(self.shortcutLineEdit) + + + self.mainVerticalLayout.addLayout(self.shortcutHorizontalLayout) + + self.verticalSpacer4 = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.mainVerticalLayout.addItem(self.verticalSpacer4) + + self.iconHorizontalLayout = QHBoxLayout() + self.iconHorizontalLayout.setObjectName(u"iconHorizontalLayout") + self.iconLabel = QLabel(TypeDialogBase) + self.iconLabel.setObjectName(u"iconLabel") + self.iconLabel.setMinimumSize(QSize(120, 0)) + + self.iconHorizontalLayout.addWidget(self.iconLabel) + + self.iconHorizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.iconHorizontalLayout.addItem(self.iconHorizontalSpacer) + + self.iconFontCollectionComboBox = QComboBox(TypeDialogBase) + self.iconFontCollectionComboBox.setObjectName(u"iconFontCollectionComboBox") + sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.iconFontCollectionComboBox.sizePolicy().hasHeightForWidth()) + self.iconFontCollectionComboBox.setSizePolicy(sizePolicy) + self.iconFontCollectionComboBox.setMinimumSize(QSize(100, 0)) + + self.iconHorizontalLayout.addWidget(self.iconFontCollectionComboBox) + + self.iconComboBox = QComboBox(TypeDialogBase) + self.iconComboBox.setObjectName(u"iconComboBox") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.iconComboBox.sizePolicy().hasHeightForWidth()) + self.iconComboBox.setSizePolicy(sizePolicy1) + self.iconComboBox.setEditable(True) + self.iconComboBox.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) + + self.iconHorizontalLayout.addWidget(self.iconComboBox) + + + self.mainVerticalLayout.addLayout(self.iconHorizontalLayout) + + self.verticalSpacer5 = QSpacerItem(20, 90, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.mainVerticalLayout.addItem(self.verticalSpacer5) + + self.mainVerticalLayout.setStretch(0, 1) + self.mainVerticalLayout.setStretch(1, 1) + self.mainVerticalLayout.setStretch(2, 1) + self.mainVerticalLayout.setStretch(3, 1) + self.mainVerticalLayout.setStretch(4, 1) + self.mainVerticalLayout.setStretch(5, 1) + self.mainVerticalLayout.setStretch(6, 1) + self.mainVerticalLayout.setStretch(7, 1) + self.mainVerticalLayout.setStretch(8, 1) + + self.gridLayout.addLayout(self.mainVerticalLayout, 0, 0, 1, 1) + + self.buttonBox = QDialogButtonBox(TypeDialogBase) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setOrientation(Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) + + self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) + + + self.retranslateUi(TypeDialogBase) + self.buttonBox.accepted.connect(TypeDialogBase.accept) + self.buttonBox.rejected.connect(TypeDialogBase.reject) + + QMetaObject.connectSlotsByName(TypeDialogBase) + # setupUi + + def retranslateUi(self, TypeDialogBase): + TypeDialogBase.setWindowTitle(QCoreApplication.translate("TypeDialogBase", u"data type", None)) + self.typeLabel.setText(QCoreApplication.translate("TypeDialogBase", u"Data type", None)) +#if QT_CONFIG(tooltip) + self.typeLineEdit.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Enter the new type title, exclude title which contains whitespace.", None)) +#endif // QT_CONFIG(tooltip) + self.typeLineEdit.setPlaceholderText(QCoreApplication.translate("TypeDialogBase", u"Enter the data type", None)) + self.typeDisplayedTitleLabel.setText(QCoreApplication.translate("TypeDialogBase", u"Title", None)) +#if QT_CONFIG(tooltip) + self.typeDisplayedTitleLineEdit.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Enter the displayed title property of the type", None)) +#endif // QT_CONFIG(tooltip) + self.typeDisplayedTitleLineEdit.setPlaceholderText(QCoreApplication.translate("TypeDialogBase", u"Enter the displayed title", None)) + self.iriLabel.setText(QCoreApplication.translate("TypeDialogBase", u"IRI", None)) +#if QT_CONFIG(tooltip) + self.iriLineEdit.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Enter the Internationalized Resource Identifier for the type", None)) +#endif // QT_CONFIG(tooltip) + self.iriLineEdit.setPlaceholderText(QCoreApplication.translate("TypeDialogBase", u"Enter the IRI", None)) + self.shortcutLabel.setText(QCoreApplication.translate("TypeDialogBase", u"Shortcut", None)) +#if QT_CONFIG(tooltip) + self.shortcutLineEdit.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Enter the shortcut key combination for the type", None)) +#endif // QT_CONFIG(tooltip) + self.shortcutLineEdit.setPlaceholderText(QCoreApplication.translate("TypeDialogBase", u"Enter the shortcut key combination", None)) + self.iconLabel.setText(QCoreApplication.translate("TypeDialogBase", u"Icon", None)) +#if QT_CONFIG(tooltip) + self.iconFontCollectionComboBox.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Select the icon font collection for this type", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(tooltip) + self.iconComboBox.setToolTip(QCoreApplication.translate("TypeDialogBase", u"Select the icon used for this type", None)) +#endif // QT_CONFIG(tooltip) + # retranslateUi + diff --git a/pasta_eln/GUI/data_hierarchy/type_dialog_base.ui b/pasta_eln/GUI/data_hierarchy/type_dialog_base.ui new file mode 100644 index 00000000..e4ea56ba --- /dev/null +++ b/pasta_eln/GUI/data_hierarchy/type_dialog_base.ui @@ -0,0 +1,388 @@ + + + TypeDialogBase + + + + 0 + 0 + 733 + 351 + + + + data type + + + + + + 20 + + + 20 + + + + + + + + 120 + 0 + + + + Data type + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Minimum + + + + 40 + 20 + + + + + + + + Enter the new type title, exclude title which contains whitespace. + + + Enter the data type + + + true + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 10 + + + + + + + + + + + 120 + 0 + + + + Title + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Minimum + + + + 40 + 20 + + + + + + + + Enter the displayed title property of the type + + + Enter the displayed title + + + true + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 10 + + + + + + + + + + + 120 + 0 + + + + IRI + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Minimum + + + + 40 + 20 + + + + + + + + Enter the Internationalized Resource Identifier for the type + + + Enter the IRI + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 10 + + + + + + + + + + + 120 + 0 + + + + Shortcut + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Minimum + + + + 40 + 20 + + + + + + + + Enter the shortcut key combination for the type + + + Enter the shortcut key combination + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 10 + + + + + + + + + + + 120 + 0 + + + + Icon + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Minimum + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Select the icon font collection for this type + + + + + + + + 0 + 0 + + + + Select the icon used for this type + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 90 + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + TypeDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TypeDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/pasta_eln/GUI/data_hierarchy/utility_functions.py b/pasta_eln/GUI/data_hierarchy/utility_functions.py index cf04b7fb..1628d7aa 100644 --- a/pasta_eln/GUI/data_hierarchy/utility_functions.py +++ b/pasta_eln/GUI/data_hierarchy/utility_functions.py @@ -10,7 +10,6 @@ import copy import logging -import re from typing import Any from PySide6.QtCore import QEvent @@ -18,6 +17,8 @@ from PySide6.QtWidgets import QMessageBox, QStyleOptionViewItem from cloudant import CouchDB +from pasta_eln.GUI.data_hierarchy.data_type_info import DataTypeInfo + def is_click_within_bounds(event: QEvent, option: QStyleOptionViewItem) -> bool: @@ -101,25 +102,6 @@ def show_message(message: str, return None -def get_next_possible_structural_level_title(existing_type_titles: Any) -> str | None: - """ - Get the title for the next possible structural type level - Args: - existing_type_titles (Any): The list of titles existing in the data hierarchy document - - Returns (str|None): - The next possible name is returned with the decimal part greater than the existing largest one - """ - if existing_type_titles is None: - return None - if len(existing_type_titles) <= 0: - return "x0" - titles = [int(title.replace('x', '').replace('X', '')) - for title in existing_type_titles if is_structural_level(title)] - new_level = max(titles, default=-1) - return f"x{new_level + 1}" - - def get_db(db_name: str, db_user: str, db_pass: str, @@ -176,9 +158,7 @@ def adapt_type(title: str) -> str: Returns: Adapted title in the needed format """ - return title.replace("Structure level ", "x") \ - if title and title.startswith("Structure level ") \ - else title + return {"Structure level 0": "x0", "Structure level 1": "x1"}.get(title, title) def is_structural_level(title: str) -> bool: @@ -190,21 +170,23 @@ def is_structural_level(title: str) -> bool: Returns: True/False """ - return re.compile(r'^[Xx][0-9]+$').match(title) is not None + return title in {"x0", "x1"} -def generate_empty_type(displayed_title: str) -> dict[str, Any]: +def generate_data_hierarchy_type(type_info: DataTypeInfo) -> dict[str, Any]: """ Generate an empty type for creating a new data hierarchy type Args: - displayed_title (str): displayed_title of the new type + type_info (DataTypeInfo): type_info of the new type Returns: Dictionary representing a bare new type """ return { - "IRI": "", - "title": displayed_title, + "IRI": type_info.iri, + "title": type_info.title, + "icon": type_info.icon, + "shortcut": type_info.shortcut, "meta": { "default": generate_required_metadata() }, @@ -481,26 +463,15 @@ def get_duplicate_metadata_formatted_message(message: str, return message -def can_delete_type(existing_types: list[str], - selected_type: str) -> bool: +def can_delete_type(selected_type: str) -> bool: """ Check if the selected type can be deleted - Non-structural types can be deleted always - Structural type 'x0' cannot be deleted at all, the other structural types can only be deleted if they are the last one in the hierarchy Args: - existing_types (list[str]): List of existing types selected_type (str): Selected type to be deleted Returns (bool): True/False depending on whether the selected type can be deleted """ - if not selected_type: - return False - existing_types = [t for t in existing_types if t] - if not is_structural_level(selected_type): - return True - if selected_type == 'x0': - return False - structural_types = list(filter(is_structural_level, existing_types)) - return (False if selected_type not in structural_types else - max(structural_types) == selected_type) + return not is_structural_level(selected_type) if selected_type else False diff --git a/pasta_eln/fixedStringsJson.py b/pasta_eln/fixedStringsJson.py index a610c4a6..377c214a 100644 --- a/pasta_eln/fixedStringsJson.py +++ b/pasta_eln/fixedStringsJson.py @@ -20,13 +20,6 @@ {"name": "-tags", "query": "What are the tags associated with the task?", "mandatory": True}, {"name": "comment", "query": "#tags comments remarks :field:value:"} ]}}, - "x2": {"IRI": "", "attachments": [], "title": "Folders", "icon": "", "shortcut": "", - "meta": {"default": [ - {"name": "-name", "query": "What is the name of subtask?", "mandatory": True}, - {"name": "-tags", "query": "What are the tags associated with the subtask?", "mandatory": True}, - {"name": "comment", "query": "#tags comments remarks :field:value:"} - ]}}, - "measurement": {"IRI": "", "attachments": [], "title": "Measurements", "icon": "fa5s.thermometer-half", "shortcut": "m", "meta": {"default": [ @@ -94,9 +87,9 @@ ["dark_amber", "dark_blue", "dark_cyan", "dark_lightgreen", "dark_pink", "dark_purple", "dark_red", \ "dark_teal", "dark_yellow", "light_amber", "light_blue", "light_cyan", "light_lightgreen", \ "light_pink", "light_purple", "light_red", "light_teal", "light_yellow", "none"]], - "loggingLevel": ["Logging level (more->less)", "INFO", ["DEBUG", "INFO", "WARNING", "ERROR"]], - "autosave": ["Autosave entries in form", "No", ["Yes", "No"]], - "showProjectBtn":["Show project button on top-left", "Yes", ["Yes", "No"]] + "loggingLevel": ["Logging level (more->less)", "INFO", ["DEBUG", "INFO", "WARNING", "ERROR"]], + "autosave": ["Autosave entries in form", "No", ["Yes", "No"]], + "showProjectBtn": ["Show project button on top-left", "Yes", ["Yes", "No"]] }, "dimensions": { "sidebarWidth": ["Sidebar width", 280, [220, 280, 340]], @@ -228,179 +221,105 @@ """ -allIcons = [ - "fa5s.address-book", "fa5s.address-card", "fa5s.adjust", "fa5s.align-center", "fa5s.align-justify", "fa5s.align-left", - "fa5s.align-right", "fa5s.allergies", "fa5s.ambulance", "fa5s.american-sign-language-interpreting", "fa5s.anchor", - "fa5s.angle-double-down", "fa5s.angle-double-left", "fa5s.angle-double-right", "fa5s.angle-double-up", - "fa5s.angle-down", "fa5s.angle-left", "fa5s.angle-right", "fa5s.angle-up", "fa5s.archive", - "fa5s.arrow-alt-circle-down", "fa5s.arrow-alt-circle-left", "fa5s.arrow-alt-circle-right", "fa5s.arrow-alt-circle-up", - "fa5s.arrow-circle-down", "fa5s.arrow-circle-left", "fa5s.arrow-circle-right", "fa5s.arrow-circle-up", - "fa5s.arrow-down", "fa5s.arrow-left", "fa5s.arrow-right", "fa5s.arrow-up", "fa5s.arrows-alt", "fa5s.arrows-alt-h", - "fa5s.arrows-alt-v", "fa5s.assistive-listening-systems", "fa5s.asterisk", "fa5s.at", "fa5s.audio-description", - "fa5s.backward", "fa5s.balance-scale", "fa5s.ban", "fa5s.band-aid", "fa5s.barcode", "fa5s.bars", "fa5s.baseball-ball", - "fa5s.basketball-ball", "fa5s.bath", "fa5s.battery-empty", "fa5s.battery-full", "fa5s.battery-half", - "fa5s.battery-quarter", "fa5s.battery-three-quarters", "fa5s.bed", "fa5s.beer", "fa5s.bell", "fa5s.bell-slash", - "fa5s.bicycle", "fa5s.binoculars", "fa5s.birthday-cake", "fa5s.blind", "fa5s.bold", "fa5s.bolt", "fa5s.bomb", - "fa5s.book", "fa5s.bookmark", "fa5s.bowling-ball", "fa5s.box", "fa5s.box-open", "fa5s.boxes", "fa5s.braille", - "fa5s.briefcase", "fa5s.briefcase-medical", "fa5s.bug", "fa5s.building", "fa5s.bullhorn", "fa5s.bullseye", - "fa5s.burn", "fa5s.bus", "fa5s.calculator", "fa5s.calendar", "fa5s.calendar-alt", "fa5s.calendar-check", - "fa5s.calendar-minus", "fa5s.calendar-plus", "fa5s.calendar-times", "fa5s.camera", "fa5s.camera-retro", - "fa5s.capsules", "fa5s.car", "fa5s.caret-down", "fa5s.caret-left", "fa5s.caret-right", "fa5s.caret-square-down", - "fa5s.caret-square-left", "fa5s.caret-square-right", "fa5s.caret-square-up", "fa5s.caret-up", "fa5s.cart-arrow-down", - "fa5s.cart-plus", "fa5s.certificate", "fa5s.chart-area", "fa5s.chart-bar", "fa5s.chart-line", "fa5s.chart-pie", - "fa5s.check", "fa5s.check-circle", "fa5s.check-square", "fa5s.chess", "fa5s.chess-bishop", "fa5s.chess-board", - "fa5s.chess-king", "fa5s.chess-knight", "fa5s.chess-pawn", "fa5s.chess-queen", "fa5s.chess-rook", - "fa5s.chevron-circle-down", "fa5s.chevron-circle-left", "fa5s.chevron-circle-right", "fa5s.chevron-circle-up", - "fa5s.chevron-down", "fa5s.chevron-left", "fa5s.chevron-right", "fa5s.chevron-up", "fa5s.child", "fa5s.circle", - "fa5s.circle-notch", "fa5s.clipboard", "fa5s.clipboard-check", "fa5s.clipboard-list", "fa5s.clock", "fa5s.clone", - "fa5s.closed-captioning", "fa5s.cloud", "fa5s.cloud-download-alt", "fa5s.cloud-upload-alt", "fa5s.code", - "fa5s.code-branch", "fa5s.coffee", "fa5s.cog", "fa5s.cogs", "fa5s.columns", "fa5s.comment", "fa5s.comment-alt", - "fa5s.comment-dots", "fa5s.comment-slash", "fa5s.comments", "fa5s.compass", "fa5s.compress", "fa5s.copy", - "fa5s.copyright", "fa5s.couch", "fa5s.credit-card", "fa5s.crop", "fa5s.crosshairs", "fa5s.cube", "fa5s.cubes", - "fa5s.cut", "fa5s.database", "fa5s.deaf", "fa5s.desktop", "fa5s.diagnoses", "fa5s.dna", "fa5s.dollar-sign", - "fa5s.dolly", "fa5s.dolly-flatbed", "fa5s.donate", "fa5s.dot-circle", "fa5s.dove", "fa5s.download", "fa5s.edit", - "fa5s.eject", "fa5s.ellipsis-h", "fa5s.ellipsis-v", "fa5s.envelope", "fa5s.envelope-open", "fa5s.envelope-square", - "fa5s.eraser", "fa5s.euro-sign", "fa5s.exchange-alt", "fa5s.exclamation", "fa5s.exclamation-circle", - "fa5s.exclamation-triangle", "fa5s.expand", "fa5s.expand-arrows-alt", "fa5s.external-link-alt", - "fa5s.external-link-square-alt", "fa5s.eye", "fa5s.eye-dropper", "fa5s.eye-slash", "fa5s.fast-backward", - "fa5s.fast-forward", "fa5s.fax", "fa5s.female", "fa5s.fighter-jet", "fa5s.file", "fa5s.file-alt", "fa5s.file-archive", - "fa5s.file-audio", "fa5s.file-code", "fa5s.file-excel", "fa5s.file-image", "fa5s.file-medical", - "fa5s.file-medical-alt", "fa5s.file-pdf", "fa5s.file-powerpoint", "fa5s.file-video", "fa5s.file-word", "fa5s.film", - "fa5s.filter", "fa5s.fire", "fa5s.fire-extinguisher", "fa5s.first-aid", "fa5s.flag", "fa5s.flag-checkered", - "fa5s.flask", "fa5s.folder", "fa5s.folder-open", "fa5s.font", "fa5s.football-ball", "fa5s.forward", "fa5s.frown", - "fa5s.futbol", "fa5s.gamepad", "fa5s.gavel", "fa5s.gem", "fa5s.genderless", "fa5s.gift", "fa5s.glass-martini", - "fa5s.globe", "fa5s.golf-ball", "fa5s.graduation-cap", "fa5s.h-square", "fa5s.hand-holding", - "fa5s.hand-holding-heart", "fa5s.hand-holding-usd", "fa5s.hand-lizard", "fa5s.hand-paper", "fa5s.hand-peace", - "fa5s.hand-point-down", "fa5s.hand-point-left", "fa5s.hand-point-right", "fa5s.hand-point-up", "fa5s.hand-pointer", - "fa5s.hand-rock", "fa5s.hand-scissors", "fa5s.hand-spock", "fa5s.hands", "fa5s.hands-helping", "fa5s.handshake", - "fa5s.hashtag", "fa5s.hdd", "fa5s.heading", "fa5s.headphones", "fa5s.heart", "fa5s.heartbeat", "fa5s.history", - "fa5s.hockey-puck", "fa5s.home", "fa5s.hospital", "fa5s.hospital-alt", "fa5s.hospital-symbol", "fa5s.hourglass", - "fa5s.hourglass-end", "fa5s.hourglass-half", "fa5s.hourglass-start", "fa5s.i-cursor", "fa5s.id-badge", "fa5s.id-card", - "fa5s.id-card-alt", "fa5s.image", "fa5s.images", "fa5s.inbox", "fa5s.indent", "fa5s.industry", "fa5s.info", - "fa5s.info-circle", "fa5s.italic", "fa5s.key", "fa5s.keyboard", "fa5s.language", "fa5s.laptop", "fa5s.leaf", - "fa5s.lemon", "fa5s.level-down-alt", "fa5s.level-up-alt", "fa5s.life-ring", "fa5s.lightbulb", "fa5s.link", - "fa5s.lira-sign", "fa5s.list", "fa5s.list-alt", "fa5s.list-ol", "fa5s.list-ul", "fa5s.location-arrow", "fa5s.lock", - "fa5s.lock-open", "fa5s.long-arrow-alt-down", "fa5s.long-arrow-alt-left", "fa5s.long-arrow-alt-right", - "fa5s.long-arrow-alt-up", "fa5s.low-vision", "fa5s.magic", "fa5s.magnet", "fa5s.male", "fa5s.map", "fa5s.map-marker", - "fa5s.map-marker-alt", "fa5s.map-pin", "fa5s.map-signs", "fa5s.mars", "fa5s.mars-double", "fa5s.mars-stroke", - "fa5s.mars-stroke-h", "fa5s.mars-stroke-v", "fa5s.medkit", "fa5s.meh", "fa5s.mercury", "fa5s.microchip", - "fa5s.microphone", "fa5s.microphone-slash", "fa5s.minus", "fa5s.minus-circle", "fa5s.minus-square", "fa5s.mobile", - "fa5s.mobile-alt", "fa5s.money-bill-alt", "fa5s.moon", "fa5s.motorcycle", "fa5s.mouse-pointer", "fa5s.music", - "fa5s.neuter", "fa5s.newspaper", "fa5s.notes-medical", "fa5s.object-group", "fa5s.object-ungroup", "fa5s.outdent", - "fa5s.paint-brush", "fa5s.pallet", "fa5s.paper-plane", "fa5s.paperclip", "fa5s.parachute-box", "fa5s.paragraph", - "fa5s.paste", "fa5s.pause", "fa5s.pause-circle", "fa5s.paw", "fa5s.pen-square", "fa5s.pencil-alt", - "fa5s.people-carry", "fa5s.percent", "fa5s.phone", "fa5s.phone-slash", "fa5s.phone-square", "fa5s.phone-volume", - "fa5s.piggy-bank", "fa5s.pills", "fa5s.plane", "fa5s.play", "fa5s.play-circle", "fa5s.plug", "fa5s.plus", - "fa5s.plus-circle", "fa5s.plus-square", "fa5s.podcast", "fa5s.poo", "fa5s.pound-sign", "fa5s.power-off", - "fa5s.prescription-bottle", "fa5s.prescription-bottle-alt", "fa5s.print", "fa5s.procedures", "fa5s.puzzle-piece", - "fa5s.qrcode", "fa5s.question", "fa5s.question-circle", "fa5s.quidditch", "fa5s.quote-left", "fa5s.quote-right", - "fa5s.random", "fa5s.recycle", "fa5s.redo", "fa5s.redo-alt", "fa5s.registered", "fa5s.reply", "fa5s.reply-all", - "fa5s.retweet", "fa5s.ribbon", "fa5s.road", "fa5s.rocket", "fa5s.rss", "fa5s.rss-square", "fa5s.ruble-sign", - "fa5s.rupee-sign", "fa5s.save", "fa5s.search", "fa5s.search-minus", "fa5s.search-plus", "fa5s.seedling", - "fa5s.server", "fa5s.share", "fa5s.share-alt", "fa5s.share-alt-square", "fa5s.share-square", "fa5s.shekel-sign", - "fa5s.shield-alt", "fa5s.ship", "fa5s.shipping-fast", "fa5s.shopping-bag", "fa5s.shopping-basket", - "fa5s.shopping-cart", "fa5s.shower", "fa5s.sign", "fa5s.sign-in-alt", "fa5s.sign-language", "fa5s.sign-out-alt", - "fa5s.signal", "fa5s.sitemap", "fa5s.sliders-h", "fa5s.smile", "fa5s.smoking", "fa5s.snowflake", "fa5s.sort", - "fa5s.sort-alpha-down", "fa5s.sort-alpha-up", "fa5s.sort-amount-down", "fa5s.sort-amount-up", "fa5s.sort-down", - "fa5s.sort-numeric-down", "fa5s.sort-numeric-up", "fa5s.sort-up", "fa5s.space-shuttle", "fa5s.spinner", "fa5s.square", - "fa5s.square-full", "fa5s.star", "fa5s.star-half", "fa5s.step-backward", "fa5s.step-forward", "fa5s.stethoscope", - "fa5s.sticky-note", "fa5s.stop", "fa5s.stop-circle", "fa5s.stopwatch", "fa5s.street-view", "fa5s.strikethrough", - "fa5s.subscript", "fa5s.subway", "fa5s.suitcase", "fa5s.sun", "fa5s.superscript", "fa5s.sync", "fa5s.sync-alt", - "fa5s.syringe", "fa5s.table", "fa5s.table-tennis", "fa5s.tablet", "fa5s.tablet-alt", "fa5s.tablets", - "fa5s.tachometer-alt", "fa5s.tag", "fa5s.tags", "fa5s.tape", "fa5s.tasks", "fa5s.taxi", "fa5s.terminal", - "fa5s.text-height", "fa5s.text-width", "fa5s.th", "fa5s.th-large", "fa5s.th-list", "fa5s.thermometer", - "fa5s.thermometer-empty", "fa5s.thermometer-full", "fa5s.thermometer-half", "fa5s.thermometer-quarter", - "fa5s.thermometer-three-quarters", "fa5s.thumbs-down", "fa5s.thumbs-up", "fa5s.thumbtack", "fa5s.ticket-alt", - "fa5s.times", "fa5s.times-circle", "fa5s.tint", "fa5s.toggle-off", "fa5s.toggle-on", "fa5s.trademark", "fa5s.train", - "fa5s.transgender", "fa5s.transgender-alt", "fa5s.trash", "fa5s.trash-alt", "fa5s.tree", "fa5s.trophy", "fa5s.truck", - "fa5s.truck-loading", "fa5s.truck-moving", "fa5s.tty", "fa5s.tv", "fa5s.umbrella", "fa5s.underline", "fa5s.undo", - "fa5s.undo-alt", "fa5s.universal-access", "fa5s.university", "fa5s.unlink", "fa5s.unlock", "fa5s.unlock-alt", - "fa5s.upload", "fa5s.user", "fa5s.user-circle", "fa5s.user-md", "fa5s.user-plus", "fa5s.user-secret", - "fa5s.user-times", "fa5s.users", "fa5s.utensil-spoon", "fa5s.utensils", "fa5s.venus", "fa5s.venus-double", - "fa5s.venus-mars", "fa5s.vial", "fa5s.vials", "fa5s.video", "fa5s.video-slash", "fa5s.volleyball-ball", - "fa5s.volume-down", "fa5s.volume-off", "fa5s.volume-up", "fa5s.warehouse", "fa5s.weight", "fa5s.wheelchair", - "fa5s.wifi", "fa5s.window-close", "fa5s.window-maximize", "fa5s.window-minimize", "fa5s.window-restore", - "fa5s.wine-glass", "fa5s.won-sign", "fa5s.wrench", "fa5s.x-ray", "fa5s.yen-sign", "far fa-address-book", - "far fa-address-card", "far fa-arrow-alt-circle-down", "far fa-arrow-alt-circle-left", - "far fa-arrow-alt-circle-right", "far fa-arrow-alt-circle-up", "far fa-bell", "far fa-bell-slash", "far fa-bookmark", - "far fa-building", "far fa-calendar", "far fa-calendar-alt", "far fa-calendar-check", "far fa-calendar-minus", - "far fa-calendar-plus", "far fa-calendar-times", "far fa-caret-square-down", "far fa-caret-square-left", - "far fa-caret-square-right", "far fa-caret-square-up", "far fa-chart-bar", "far fa-check-circle", - "far fa-check-square", "far fa-circle", "far fa-clipboard", "far fa-clock", "far fa-clone", - "far fa-closed-captioning", "far fa-comment", "far fa-comment-alt", "far fa-comments", "far fa-compass", - "far fa-copy", "far fa-copyright", "far fa-credit-card", "far fa-dot-circle", "far fa-edit", "far fa-envelope", - "far fa-envelope-open", "far fa-eye-slash", "far fa-file", "far fa-file-alt", "far fa-file-archive", - "far fa-file-audio", "far fa-file-code", "far fa-file-excel", "far fa-file-image", "far fa-file-pdf", - "far fa-file-powerpoint", "far fa-file-video", "far fa-file-word", "far fa-flag", "far fa-folder", - "far fa-folder-open", "far fa-frown", "far fa-futbol", "far fa-gem", "far fa-hand-lizard", "far fa-hand-paper", - "far fa-hand-peace", "far fa-hand-point-down", "far fa-hand-point-left", "far fa-hand-point-right", - "far fa-hand-point-up", "far fa-hand-pointer", "far fa-hand-rock", "far fa-hand-scissors", "far fa-hand-spock", - "far fa-handshake", "far fa-hdd", "far fa-heart", "far fa-hospital", "far fa-hourglass", "far fa-id-badge", - "far fa-id-card", "far fa-image", "far fa-images", "far fa-keyboard", "far fa-lemon", "far fa-life-ring", - "far fa-lightbulb", "far fa-list-alt", "far fa-map", "far fa-meh", "far fa-minus-square", "far fa-money-bill-alt", - "far fa-moon", "far fa-newspaper", "far fa-object-group", "far fa-object-ungroup", "far fa-paper-plane", - "far fa-pause-circle", "far fa-play-circle", "far fa-plus-square", "far fa-question-circle", "far fa-registered", - "far fa-save", "far fa-share-square", "far fa-smile", "far fa-snowflake", "far fa-square", "far fa-star", - "far fa-star-half", "far fa-sticky-note", "far fa-stop-circle", "far fa-sun", "far fa-thumbs-down", - "far fa-thumbs-up", "far fa-times-circle", "far fa-trash-alt", "far fa-user", "far fa-user-circle", - "far fa-window-close", "far fa-window-maximize", "far fa-window-minimize", "far fa-window-restore", "fab fa-500px", - "fab fa-accessible-icon", "fab fa-accusoft", "fab fa-adn", "fab fa-adversal", "fab fa-affiliatetheme", - "fab fa-algolia", "fab fa-amazon", "fab fa-amazon-pay", "fab fa-amilia", "fab fa-android", "fab fa-angellist", - "fab fa-angrycreative", "fab fa-angular", "fab fa-app-store", "fab fa-app-store-ios", "fab fa-apper", "fab fa-apple", - "fab fa-apple-pay", "fab fa-asymmetrik", "fab fa-audible", "fab fa-autoprefixer", "fab fa-avianex", "fab fa-aviato", - "fab fa-aws", "fab fa-bandcamp", "fab fa-behance", "fab fa-behance-square", "fab fa-bimobject", "fab fa-bitbucket", - "fab fa-bitcoin", "fab fa-bity", "fab fa-black-tie", "fab fa-blackberry", "fab fa-blogger", "fab fa-blogger-b", - "fab fa-bluetooth", "fab fa-bluetooth-b", "fab fa-btc", "fab fa-buromobelexperte", "fab fa-buysellads", - "fab fa-cc-amazon-pay", "fab fa-cc-amex", "fab fa-cc-apple-pay", "fab fa-cc-diners-club", "fab fa-cc-discover", - "fab fa-cc-jcb", "fab fa-cc-mastercard", "fab fa-cc-paypal", "fab fa-cc-stripe", "fab fa-cc-visa", - "fab fa-centercode", "fab fa-chrome", "fab fa-cloudscale", "fab fa-cloudsmith", "fab fa-cloudversify", - "fab fa-codepen", "fab fa-codiepie", "fab fa-connectdevelop", "fab fa-contao", "fab fa-cpanel", - "fab fa-creative-commons", "fab fa-css3", "fab fa-css3-alt", "fab fa-cuttlefish", "fab fa-d-and-d", "fab fa-dashcube", - "fab fa-delicious", "fab fa-deploydog", "fab fa-deskpro", "fab fa-deviantart", "fab fa-digg", "fab fa-digital-ocean", - "fab fa-discord", "fab fa-discourse", "fab fa-dochub", "fab fa-docker", "fab fa-draft2digital", "fab fa-dribbble", - "fab fa-dribbble-square", "fab fa-dropbox", "fab fa-drupal", "fab fa-dyalog", "fab fa-earlybirds", "fab fa-edge", - "fab fa-elementor", "fab fa-ember", "fab fa-empire", "fab fa-envira", "fab fa-erlang", "fab fa-ethereum", - "fab fa-etsy", "fab fa-expeditedssl", "fab fa-facebook", "fab fa-facebook-f", "fab fa-facebook-messenger", - "fab fa-facebook-square", "fab fa-firefox", "fab fa-first-order", "fab fa-firstdraft", "fab fa-flickr", - "fab fa-flipboard", "fab fa-fly", "fab fa-font-awesome", "fab fa-font-awesome-alt", "fab fa-font-awesome-flag", - "fab fa-fonticons", "fab fa-fonticons-fi", "fab fa-fort-awesome", "fab fa-fort-awesome-alt", "fab fa-forumbee", - "fab fa-foursquare", "fab fa-free-code-camp", "fab fa-freebsd", "fab fa-get-pocket", "fab fa-gg", "fab fa-gg-circle", - "fab fa-git", "fab fa-git-square", "fab fa-github", "fab fa-github-alt", "fab fa-github-square", "fab fa-gitkraken", - "fab fa-gitlab", "fab fa-gitter", "fab fa-glide", "fab fa-glide-g", "fab fa-gofore", "fab fa-goodreads", - "fab fa-goodreads-g", "fab fa-google", "fab fa-google-drive", "fab fa-google-play", "fab fa-google-plus", - "fab fa-google-plus-g", "fab fa-google-plus-square", "fab fa-google-wallet", "fab fa-gratipay", "fab fa-grav", - "fab fa-gripfire", "fab fa-grunt", "fab fa-gulp", "fab fa-hacker-news", "fab fa-hacker-news-square", "fab fa-hips", - "fab fa-hire-a-helper", "fab fa-hooli", "fab fa-hotjar", "fab fa-houzz", "fab fa-html5", "fab fa-hubspot", - "fab fa-imdb", "fab fa-instagram", "fab fa-internet-explorer", "fab fa-ioxhost", "fab fa-itunes", - "fab fa-itunes-note", "fab fa-jenkins", "fab fa-joget", "fab fa-joomla", "fab fa-js", "fab fa-js-square", - "fab fa-jsfiddle", "fab fa-keycdn", "fab fa-kickstarter", "fab fa-kickstarter-k", "fab fa-korvue", "fab fa-laravel", - "fab fa-lastfm", "fab fa-lastfm-square", "fab fa-leanpub", "fab fa-less", "fab fa-line", "fab fa-linkedin", - "fab fa-linkedin-in", "fab fa-linode", "fab fa-linux", "fab fa-lyft", "fab fa-magento", "fab fa-maxcdn", - "fab fa-medapps", "fab fa-medium", "fab fa-medium-m", "fab fa-medrt", "fab fa-meetup", "fab fa-microsoft", - "fab fa-mix", "fab fa-mixcloud", "fab fa-mizuni", "fab fa-modx", "fab fa-monero", "fab fa-napster", - "fab fa-nintendo-switch", "fab fa-node", "fab fa-node-js", "fab fa-npm", "fab fa-ns8", "fab fa-nutritionix", - "fab fa-odnoklassniki", "fab fa-odnoklassniki-square", "fab fa-opencart", "fab fa-openid", "fab fa-opera", - "fab fa-optin-monster", "fab fa-osi", "fab fa-page4", "fab fa-pagelines", "fab fa-palfed", "fab fa-patreon", - "fab fa-paypal", "fab fa-periscope", "fab fa-phabricator", "fab fa-phoenix-framework", "fab fa-php", - "fab fa-pied-piper", "fab fa-pied-piper-alt", "fab fa-pied-piper-pp", "fab fa-pinterest", "fab fa-pinterest-p", - "fab fa-pinterest-square", "fab fa-playstation", "fab fa-product-hunt", "fab fa-pushed", "fab fa-python", "fab fa-qq", - "fab fa-quinscape", "fab fa-quora", "fab fa-ravelry", "fab fa-react", "fab fa-readme", "fab fa-rebel", - "fab fa-red-river", "fab fa-reddit", "fab fa-reddit-alien", "fab fa-reddit-square", "fab fa-rendact", "fab fa-renren", - "fab fa-replyd", "fab fa-resolving", "fab fa-rocketchat", "fab fa-rockrms", "fab fa-safari", "fab fa-sass", - "fab fa-schlix", "fab fa-scribd", "fab fa-searchengin", "fab fa-sellcast", "fab fa-sellsy", "fab fa-servicestack", - "fab fa-shirtsinbulk", "fab fa-simplybuilt", "fab fa-sistrix", "fab fa-skyatlas", "fab fa-skype", "fab fa-slack", - "fab fa-slack-hash", "fab fa-slideshare", "fab fa-snapchat", "fab fa-snapchat-ghost", "fab fa-snapchat-square", - "fab fa-soundcloud", "fab fa-speakap", "fab fa-spotify", "fab fa-stack-exchange", "fab fa-stack-overflow", - "fab fa-staylinked", "fab fa-steam", "fab fa-steam-square", "fab fa-steam-symbol", "fab fa-sticker-mule", - "fab fa-strava", "fab fa-stripe", "fab fa-stripe-s", "fab fa-studiovinari", "fab fa-stumbleupon", - "fab fa-stumbleupon-circle", "fab fa-superpowers", "fab fa-supple", "fab fa-telegram", "fab fa-telegram-plane", - "fab fa-tencent-weibo", "fab fa-themeisle", "fab fa-trello", "fab fa-tripadvisor", "fab fa-tumblr", - "fab fa-tumblr-square", "fab fa-twitch", "fab fa-twitter", "fab fa-twitter-square", "fab fa-typo3", "fab fa-uber", - "fab fa-uikit", "fab fa-uniregistry", "fab fa-untappd", "fab fa-usb", "fab fa-ussunnah", "fab fa-vaadin", - "fab fa-viacoin", "fab fa-viadeo", "fab fa-viadeo-square", "fab fa-viber", "fab fa-vimeo", "fab fa-vimeo-square", - "fab fa-vimeo-v", "fab fa-vine", "fab fa-vk", "fab fa-vnv", "fab fa-vuejs", "fab fa-weibo", "fab fa-weixin", - "fab fa-whatsapp", "fab fa-whatsapp-square", "fab fa-whmcs", "fab fa-wikipedia-w", "fab fa-windows", - "fab fa-wordpress", "fab fa-wordpress-simple", "fab fa-wpbeginner", "fab fa-wpexplorer", "fab fa-wpforms", - "fab fa-xbox", "fab fa-xing", "fab fa-xing-square", "fab fa-y-combinator", "fab fa-yahoo", "fab fa-yandex", - "fab fa-yandex-international", "fab fa-yelp", "fab fa-yoast", "fab fa-youtube", "fab fa-youtube-square" -] +allIcons = ['fa5s.address-book', 'fa5s.address-card', 'fa5s.adjust', 'fa5s.align-center', 'fa5s.align-justify', + 'fa5s.align-left', 'fa5s.align-right', 'fa5s.allergies', 'fa5s.ambulance', + 'fa5s.american-sign-language-interpreting', 'fa5s.anchor', 'fa5s.angle-double-down', + 'fa5s.angle-double-left', 'fa5s.angle-double-right', 'fa5s.angle-double-up', 'fa5s.angle-down', + 'fa5s.angle-left', 'fa5s.angle-right', 'fa5s.angle-up', 'fa5s.archive', 'fa5s.arrow-alt-circle-down', + 'fa5s.arrow-alt-circle-left', 'fa5s.arrow-alt-circle-right', 'fa5s.arrow-alt-circle-up', + 'fa5s.arrow-circle-down', 'fa5s.arrow-circle-left', 'fa5s.arrow-circle-right', 'fa5s.arrow-circle-up', + 'fa5s.arrow-down', 'fa5s.arrow-left', 'fa5s.arrow-right', 'fa5s.arrow-up', 'fa5s.arrows-alt', + 'fa5s.arrows-alt-h', 'fa5s.arrows-alt-v', 'fa5s.assistive-listening-systems', 'fa5s.asterisk', 'fa5s.at', + 'fa5s.audio-description', 'fa5s.backward', 'fa5s.balance-scale', 'fa5s.ban', 'fa5s.band-aid', + 'fa5s.barcode', 'fa5s.bars', 'fa5s.baseball-ball', 'fa5s.basketball-ball', 'fa5s.bath', + 'fa5s.battery-empty', 'fa5s.battery-full', 'fa5s.battery-half', 'fa5s.battery-quarter', + 'fa5s.battery-three-quarters', 'fa5s.bed', 'fa5s.beer', 'fa5s.bell', 'fa5s.bell-slash', 'fa5s.bicycle', + 'fa5s.binoculars', 'fa5s.birthday-cake', 'fa5s.blind', 'fa5s.bold', 'fa5s.bolt', 'fa5s.bomb', 'fa5s.book', + 'fa5s.bookmark', 'fa5s.bowling-ball', 'fa5s.box', 'fa5s.box-open', 'fa5s.boxes', 'fa5s.braille', + 'fa5s.briefcase', 'fa5s.briefcase-medical', 'fa5s.bug', 'fa5s.building', 'fa5s.bullhorn', 'fa5s.bullseye', + 'fa5s.burn', 'fa5s.bus', 'fa5s.calculator', 'fa5s.calendar', 'fa5s.calendar-alt', 'fa5s.calendar-check', + 'fa5s.calendar-minus', 'fa5s.calendar-plus', 'fa5s.calendar-times', 'fa5s.camera', 'fa5s.camera-retro', + 'fa5s.capsules', 'fa5s.car', 'fa5s.caret-down', 'fa5s.caret-left', 'fa5s.caret-right', + 'fa5s.caret-square-down', 'fa5s.caret-square-left', 'fa5s.caret-square-right', 'fa5s.caret-square-up', + 'fa5s.caret-up', 'fa5s.cart-arrow-down', 'fa5s.cart-plus', 'fa5s.certificate', 'fa5s.chart-area', + 'fa5s.chart-bar', 'fa5s.chart-line', 'fa5s.chart-pie', 'fa5s.check', 'fa5s.check-circle', + 'fa5s.check-square', 'fa5s.chess', 'fa5s.chess-bishop', 'fa5s.chess-board', 'fa5s.chess-king', + 'fa5s.chess-knight', 'fa5s.chess-pawn', 'fa5s.chess-queen', 'fa5s.chess-rook', 'fa5s.chevron-circle-down', + 'fa5s.chevron-circle-left', 'fa5s.chevron-circle-right', 'fa5s.chevron-circle-up', 'fa5s.chevron-down', + 'fa5s.chevron-left', 'fa5s.chevron-right', 'fa5s.chevron-up', 'fa5s.child', 'fa5s.circle', + 'fa5s.circle-notch', 'fa5s.clipboard', 'fa5s.clipboard-check', 'fa5s.clipboard-list', 'fa5s.clock', + 'fa5s.clone', 'fa5s.closed-captioning', 'fa5s.cloud', 'fa5s.cloud-download-alt', 'fa5s.cloud-upload-alt', + 'fa5s.code', 'fa5s.code-branch', 'fa5s.coffee', 'fa5s.cog', 'fa5s.cogs', 'fa5s.columns', 'fa5s.comment', + 'fa5s.comment-alt', 'fa5s.comment-dots', 'fa5s.comment-slash', 'fa5s.comments', 'fa5s.compass', + 'fa5s.compress', 'fa5s.copy', 'fa5s.copyright', 'fa5s.couch', 'fa5s.credit-card', 'fa5s.crop', + 'fa5s.crosshairs', 'fa5s.cube', 'fa5s.cubes', 'fa5s.cut', 'fa5s.database', 'fa5s.deaf', 'fa5s.desktop', + 'fa5s.diagnoses', 'fa5s.dna', 'fa5s.dollar-sign', 'fa5s.dolly', 'fa5s.dolly-flatbed', 'fa5s.donate', + 'fa5s.dot-circle', 'fa5s.dove', 'fa5s.download', 'fa5s.edit', 'fa5s.eject', 'fa5s.ellipsis-h', + 'fa5s.ellipsis-v', 'fa5s.envelope', 'fa5s.envelope-open', 'fa5s.envelope-square', 'fa5s.eraser', + 'fa5s.euro-sign', 'fa5s.exchange-alt', 'fa5s.exclamation', 'fa5s.exclamation-circle', + 'fa5s.exclamation-triangle', 'fa5s.expand', 'fa5s.expand-arrows-alt', 'fa5s.external-link-alt', + 'fa5s.external-link-square-alt', 'fa5s.eye', 'fa5s.eye-dropper', 'fa5s.eye-slash', 'fa5s.fast-backward', + 'fa5s.fast-forward', 'fa5s.fax', 'fa5s.female', 'fa5s.fighter-jet', 'fa5s.file', 'fa5s.file-alt', + 'fa5s.file-archive', 'fa5s.file-audio', 'fa5s.file-code', 'fa5s.file-excel', 'fa5s.file-image', + 'fa5s.file-medical', 'fa5s.file-medical-alt', 'fa5s.file-pdf', 'fa5s.file-powerpoint', 'fa5s.file-video', + 'fa5s.file-word', 'fa5s.film', 'fa5s.filter', 'fa5s.fire', 'fa5s.fire-extinguisher', 'fa5s.first-aid', + 'fa5s.flag', 'fa5s.flag-checkered', 'fa5s.flask', 'fa5s.folder', 'fa5s.folder-open', 'fa5s.font', + 'fa5s.football-ball', 'fa5s.forward', 'fa5s.frown', 'fa5s.futbol', 'fa5s.gamepad', 'fa5s.gavel', 'fa5s.gem', + 'fa5s.genderless', 'fa5s.gift', 'fa5s.glass-martini', 'fa5s.globe', 'fa5s.golf-ball', 'fa5s.graduation-cap', + 'fa5s.h-square', 'fa5s.hand-holding', 'fa5s.hand-holding-heart', 'fa5s.hand-holding-usd', + 'fa5s.hand-lizard', 'fa5s.hand-paper', 'fa5s.hand-peace', 'fa5s.hand-point-down', 'fa5s.hand-point-left', + 'fa5s.hand-point-right', 'fa5s.hand-point-up', 'fa5s.hand-pointer', 'fa5s.hand-rock', 'fa5s.hand-scissors', + 'fa5s.hand-spock', 'fa5s.hands', 'fa5s.hands-helping', 'fa5s.handshake', 'fa5s.hashtag', 'fa5s.hdd', + 'fa5s.heading', 'fa5s.headphones', 'fa5s.heart', 'fa5s.heartbeat', 'fa5s.history', 'fa5s.hockey-puck', + 'fa5s.home', 'fa5s.hospital', 'fa5s.hospital-alt', 'fa5s.hospital-symbol', 'fa5s.hourglass', + 'fa5s.hourglass-end', 'fa5s.hourglass-half', 'fa5s.hourglass-start', 'fa5s.i-cursor', 'fa5s.id-badge', + 'fa5s.id-card', 'fa5s.id-card-alt', 'fa5s.image', 'fa5s.images', 'fa5s.inbox', 'fa5s.indent', + 'fa5s.industry', 'fa5s.info', 'fa5s.info-circle', 'fa5s.italic', 'fa5s.key', 'fa5s.keyboard', + 'fa5s.language', 'fa5s.laptop', 'fa5s.leaf', 'fa5s.lemon', 'fa5s.level-down-alt', 'fa5s.level-up-alt', + 'fa5s.life-ring', 'fa5s.lightbulb', 'fa5s.link', 'fa5s.lira-sign', 'fa5s.list', 'fa5s.list-alt', + 'fa5s.list-ol', 'fa5s.list-ul', 'fa5s.location-arrow', 'fa5s.lock', 'fa5s.lock-open', + 'fa5s.long-arrow-alt-down', 'fa5s.long-arrow-alt-left', 'fa5s.long-arrow-alt-right', + 'fa5s.long-arrow-alt-up', 'fa5s.low-vision', 'fa5s.magic', 'fa5s.magnet', 'fa5s.male', 'fa5s.map', + 'fa5s.map-marker', 'fa5s.map-marker-alt', 'fa5s.map-pin', 'fa5s.map-signs', 'fa5s.mars', 'fa5s.mars-double', + 'fa5s.mars-stroke', 'fa5s.mars-stroke-h', 'fa5s.mars-stroke-v', 'fa5s.medkit', 'fa5s.meh', 'fa5s.mercury', + 'fa5s.microchip', 'fa5s.microphone', 'fa5s.microphone-slash', 'fa5s.minus', 'fa5s.minus-circle', + 'fa5s.minus-square', 'fa5s.mobile', 'fa5s.mobile-alt', 'fa5s.money-bill-alt', 'fa5s.moon', + 'fa5s.motorcycle', 'fa5s.mouse-pointer', 'fa5s.music', 'fa5s.neuter', 'fa5s.newspaper', + 'fa5s.notes-medical', 'fa5s.object-group', 'fa5s.object-ungroup', 'fa5s.outdent', 'fa5s.paint-brush', + 'fa5s.pallet', 'fa5s.paper-plane', 'fa5s.paperclip', 'fa5s.parachute-box', 'fa5s.paragraph', 'fa5s.paste', + 'fa5s.pause', 'fa5s.pause-circle', 'fa5s.paw', 'fa5s.pen-square', 'fa5s.pencil-alt', 'fa5s.people-carry', + 'fa5s.percent', 'fa5s.phone', 'fa5s.phone-slash', 'fa5s.phone-square', 'fa5s.phone-volume', + 'fa5s.piggy-bank', 'fa5s.pills', 'fa5s.plane', 'fa5s.play', 'fa5s.play-circle', 'fa5s.plug', 'fa5s.plus', + 'fa5s.plus-circle', 'fa5s.plus-square', 'fa5s.podcast', 'fa5s.poo', 'fa5s.pound-sign', 'fa5s.power-off', + 'fa5s.prescription-bottle', 'fa5s.prescription-bottle-alt', 'fa5s.print', 'fa5s.procedures', + 'fa5s.puzzle-piece', 'fa5s.qrcode', 'fa5s.question', 'fa5s.question-circle', 'fa5s.quidditch', + 'fa5s.quote-left', 'fa5s.quote-right', 'fa5s.random', 'fa5s.recycle', 'fa5s.redo', 'fa5s.redo-alt', + 'fa5s.registered', 'fa5s.reply', 'fa5s.reply-all', 'fa5s.retweet', 'fa5s.ribbon', 'fa5s.road', + 'fa5s.rocket', 'fa5s.rss', 'fa5s.rss-square', 'fa5s.ruble-sign', 'fa5s.rupee-sign', 'fa5s.save', + 'fa5s.search', 'fa5s.search-minus', 'fa5s.search-plus', 'fa5s.seedling', 'fa5s.server', 'fa5s.share', + 'fa5s.share-alt', 'fa5s.share-alt-square', 'fa5s.share-square', 'fa5s.shekel-sign', 'fa5s.shield-alt', + 'fa5s.ship', 'fa5s.shipping-fast', 'fa5s.shopping-bag', 'fa5s.shopping-basket', 'fa5s.shopping-cart', + 'fa5s.shower', 'fa5s.sign', 'fa5s.sign-in-alt', 'fa5s.sign-language', 'fa5s.sign-out-alt', 'fa5s.signal', + 'fa5s.sitemap', 'fa5s.sliders-h', 'fa5s.smile', 'fa5s.smoking', 'fa5s.snowflake', 'fa5s.sort', + 'fa5s.sort-alpha-down', 'fa5s.sort-alpha-up', 'fa5s.sort-amount-down', 'fa5s.sort-amount-up', + 'fa5s.sort-down', 'fa5s.sort-numeric-down', 'fa5s.sort-numeric-up', 'fa5s.sort-up', 'fa5s.space-shuttle', + 'fa5s.spinner', 'fa5s.square', 'fa5s.square-full', 'fa5s.star', 'fa5s.star-half', 'fa5s.step-backward', + 'fa5s.step-forward', 'fa5s.stethoscope', 'fa5s.sticky-note', 'fa5s.stop', 'fa5s.stop-circle', + 'fa5s.stopwatch', 'fa5s.street-view', 'fa5s.strikethrough', 'fa5s.subscript', 'fa5s.subway', + 'fa5s.suitcase', 'fa5s.sun', 'fa5s.superscript', 'fa5s.sync', 'fa5s.sync-alt', 'fa5s.syringe', 'fa5s.table', + 'fa5s.table-tennis', 'fa5s.tablet', 'fa5s.tablet-alt', 'fa5s.tablets', 'fa5s.tachometer-alt', 'fa5s.tag', + 'fa5s.tags', 'fa5s.tape', 'fa5s.tasks', 'fa5s.taxi', 'fa5s.terminal', 'fa5s.text-height', 'fa5s.text-width', + 'fa5s.th', 'fa5s.th-large', 'fa5s.th-list', 'fa5s.thermometer', 'fa5s.thermometer-empty', + 'fa5s.thermometer-full', 'fa5s.thermometer-half', 'fa5s.thermometer-quarter', + 'fa5s.thermometer-three-quarters', 'fa5s.thumbs-down', 'fa5s.thumbs-up', 'fa5s.thumbtack', + 'fa5s.ticket-alt', 'fa5s.times', 'fa5s.times-circle', 'fa5s.tint', 'fa5s.toggle-off', 'fa5s.toggle-on', + 'fa5s.trademark', 'fa5s.train', 'fa5s.transgender', 'fa5s.transgender-alt', 'fa5s.trash', 'fa5s.trash-alt', + 'fa5s.tree', 'fa5s.trophy', 'fa5s.truck', 'fa5s.truck-loading', 'fa5s.truck-moving', 'fa5s.tty', 'fa5s.tv', + 'fa5s.umbrella', 'fa5s.underline', 'fa5s.undo', 'fa5s.undo-alt', 'fa5s.universal-access', 'fa5s.university', + 'fa5s.unlink', 'fa5s.unlock', 'fa5s.unlock-alt', 'fa5s.upload', 'fa5s.user', 'fa5s.user-circle', + 'fa5s.user-md', 'fa5s.user-plus', 'fa5s.user-secret', 'fa5s.user-times', 'fa5s.users', 'fa5s.utensil-spoon', + 'fa5s.utensils', 'fa5s.venus', 'fa5s.venus-double', 'fa5s.venus-mars', 'fa5s.vial', 'fa5s.vials', + 'fa5s.video', 'fa5s.video-slash', 'fa5s.volleyball-ball', 'fa5s.volume-down', 'fa5s.volume-off', + 'fa5s.volume-up', 'fa5s.warehouse', 'fa5s.weight', 'fa5s.wheelchair', 'fa5s.wifi', 'fa5s.window-close', + 'fa5s.window-maximize', 'fa5s.window-minimize', 'fa5s.window-restore', 'fa5s.wine-glass', 'fa5s.won-sign', + 'fa5s.wrench', 'fa5s.x-ray', 'fa5s.yen-sign'] diff --git a/pasta_eln/guiStyle.py b/pasta_eln/guiStyle.py index 397cd8fc..248180bd 100644 --- a/pasta_eln/guiStyle.py +++ b/pasta_eln/guiStyle.py @@ -148,7 +148,7 @@ def __init__(self, data:str, layout:Optional[QLayout], width:int=-1, height:int= byteArr = QByteArray.fromBase64(bytearray(data[22:] if data[21]==',' else data[23:], encoding='utf-8')) imageW = QImage() imageType = data[11:15].upper() - imageW.loadFromData(byteArr, format=imageType[:-1] if imageType.endswith(';') else imageType) + imageW.loadFromData(byteArr, format=imageType[:-1] if imageType.endswith(';') else imageType) #type: ignore[arg-type] pixmap = QPixmap.fromImage(imageW) if height>0: pixmap = pixmap.scaledToHeight(height) diff --git a/pasta_eln/miscTools.py b/pasta_eln/miscTools.py index 8cc37938..e28f37e4 100644 --- a/pasta_eln/miscTools.py +++ b/pasta_eln/miscTools.py @@ -382,7 +382,7 @@ def _flatten(_d:Union[Mapping[Any, Any], list[Any]], depth:int, parent:object=No """ Recursive function """ key_value_iterable = (enumerate(_d) if isinstance(_d, enumerate_types) else _d.items()) has_item = False - for key, value in key_value_iterable: + for key, value in key_value_iterable: # type: ignore[union-attr] has_item = True flat_key = dot_reducer(parent, key) if isinstance(value, flatten_types): diff --git a/requirements-linux.txt b/requirements-linux.txt index b34740ac..8a7d6abc 100644 --- a/requirements-linux.txt +++ b/requirements-linux.txt @@ -1,5 +1,5 @@ #This file is autogenerated by commit.py from setup.cfg. Change content there -pyside6 +pyside6==6.7.3 qt-material QtAwesome cloudant diff --git a/requirements-windows.txt b/requirements-windows.txt index 8b71f0ab..4910da57 100644 --- a/requirements-windows.txt +++ b/requirements-windows.txt @@ -1,5 +1,5 @@ #This file is autogenerated by commit.py from setup.cfg. Change content there -pyside6 +pyside6==6.7.3 qt-material QtAwesome cloudant diff --git a/setup.cfg b/setup.cfg index 710e60ff..5bf13971 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = python_requires = >= 3.10 # https://setuptools.pypa.io/en/latest/userguide/dependency_management.html install_requires = - pyside6 + pyside6==6.7.3 qt-material QtAwesome cloudant @@ -81,6 +81,7 @@ exclude = (?x)( pasta_eln/GUI/data_hierarchy/create_type_dialog_base.py |pasta_eln/GUI/data_hierarchy/data_hierarchy_editor_dialog_base.py |pasta_eln/GUI/data_hierarchy/terminology_lookup_dialog_base.py + |pasta_eln/GUI/data_hierarchy/type_dialog_base.py |pasta_eln/GUI/dataverse/completed_upload_task.py |pasta_eln/GUI/dataverse/completed_uploads_base.py |pasta_eln/GUI/dataverse/config_dialog_base.py diff --git a/tests/common/fixtures.py b/tests/common/fixtures.py index 7f5e721c..bb678e45 100644 --- a/tests/common/fixtures.py +++ b/tests/common/fixtures.py @@ -18,7 +18,7 @@ from pytestqt.qtbot import QtBot from pasta_eln.GUI.data_hierarchy.attachments_tableview_data_model import AttachmentsTableViewModel -from pasta_eln.GUI.data_hierarchy.create_type_dialog import CreateTypeDialog +from pasta_eln.GUI.data_hierarchy.create_type_dialog import TypeDialog from pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog import DataHierarchyEditorDialog, get_gui from pasta_eln.GUI.data_hierarchy.delete_column_delegate import DeleteColumnDelegate from pasta_eln.GUI.data_hierarchy.document_null_exception import DocumentNullException @@ -40,15 +40,17 @@ @fixture() -def create_type_dialog_mock(mocker) -> CreateTypeDialog: +def create_type_dialog_mock(mocker) -> TypeDialog: mock_callable_1 = mocker.patch('typing.Callable') mock_callable_2 = mocker.patch('typing.Callable') - mocker.patch.object(CreateTypeDialog, 'setup_slots') + mocker.patch.object(TypeDialog, 'setup_slots') mocker.patch('pasta_eln.GUI.data_hierarchy.create_type_dialog.logging.getLogger') - mocker.patch('pasta_eln.GUI.data_hierarchy.create_type_dialog_base.Ui_CreateTypeDialogBase.setupUi') - mocker.patch.object(QDialog, '__new__') - mocker.patch.object(CreateTypeDialog, 'titleLineEdit', create=True) - return CreateTypeDialog(mock_callable_1, mock_callable_2) + mocker.patch('pasta_eln. GUI. data_hierarchy. type_dialog.DataTypeInfo') + mocker.patch('pasta_eln.GUI.data_hierarchy.create_type_dialog.QDialog') + mocker.patch('pasta_eln.GUI.data_hierarchy.create_type_dialog.QTAIconsFactory') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog_base.Ui_TypeDialogBase.setupUi') + mocker.patch.object(TypeDialog, 'titleLineEdit', create=True) + return TypeDialog(mock_callable_1, mock_callable_2) @fixture() @@ -111,7 +113,6 @@ def configuration_extended(mocker) -> DataHierarchyEditorDialog: mocker.patch( 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog_base.Ui_DataHierarchyEditorDialogBase.setupUi') mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.adjust_data_hierarchy_data_to_v4') - mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.LookupIriAction') mocker.patch.object(QDialog, '__new__') mocker.patch.object(MetadataTableViewModel, '__new__') mocker.patch.object(AttachmentsTableViewModel, '__new__') @@ -127,18 +128,23 @@ def configuration_extended(mocker) -> DataHierarchyEditorDialog: mocker.patch.object(DataHierarchyEditorDialog, 'deleteMetadataGroupPushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'deleteTypePushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'addTypePushButton', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'editTypePushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'cancelPushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'helpPushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'attachmentsShowHidePushButton', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'typeComboBox', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'metadataGroupComboBox', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'metadata_table_data_model', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'attachments_table_data_model', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'webbrowser', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'instance', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'typeDisplayedTitleLineEdit', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'typeIriLineEdit', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'delete_column_delegate_metadata_table', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'reorder_column_delegate_metadata_table', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'delete_column_delegate_attach_table', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'reorder_column_delegate_attach_table', create=True) - mocker.patch.object(CreateTypeDialog, '__new__') + mocker.patch.object(TypeDialog, '__new__') return DataHierarchyEditorDialog(mock_pasta_db) diff --git a/tests/component_tests/test_data_hierarchy_editor_dialog.py b/tests/component_tests/test_data_hierarchy_editor_dialog.py index b40e22cb..959bbab1 100644 --- a/tests/component_tests/test_data_hierarchy_editor_dialog.py +++ b/tests/component_tests/test_data_hierarchy_editor_dialog.py @@ -23,8 +23,7 @@ class TestDataHierarchyEditorDialog(object): def test_component_launch_should_display_all_ui_elements(self, pasta_db_mock: pasta_db_mock, # Added to import fixture by other tests - data_hierarchy_editor_gui: tuple[ - QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot]): + data_hierarchy_editor_gui: data_hierarchy_editor_gui): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui assert ui_form.headerLabel is not None, "Header not loaded!" assert ui_form.typeLabel is not None, "Data type label not loaded!" @@ -37,22 +36,28 @@ def test_component_launch_should_display_all_ui_elements(self, pasta_db_mock: pa assert ui_form.addMetadataRowPushButton is not None, "Add metadata row button not loaded!" assert ui_form.addMetadataGroupPushButton is not None, "Add metadata Group button not loaded!" assert ui_form.cancelPushButton is not None, "Cancel button not loaded!" - assert ui_form.typeDisplayedTitleLineEdit is not None, "Data type line edit not loaded!" - assert ui_form.typeIriLineEdit is not None, "Data type IRI line edit not loaded!" assert ui_form.addMetadataGroupLineEdit is not None, "metadata Group line edit not loaded!" assert ui_form.typeComboBox is not None, "Data type combo box not loaded!" assert ui_form.metadataGroupComboBox is not None, "metadata Group combo box not loaded!" assert ui_form.typeAttachmentsTableView.isHidden() is True, "Type attachments table view should not be shown!" assert ui_form.addAttachmentPushButton.isHidden() is True, "addAttachmentPushButton should not be shown!" + assert ui_form.addTypePushButton.isHidden() is False, "addTypePushButton should be shown!" + assert ui_form.addMetadataRowPushButton.isHidden() is False, "addMetadataRowPushButton should be shown!" + assert ui_form.addMetadataGroupPushButton.isHidden() is False, "addMetadataGroupPushButton should be shown!" + assert ui_form.addMetadataGroupLineEdit.isHidden() is False, "addMetadataGroupLineEdit should be shown!" + assert ui_form.typeComboBox.currentText() == 'Structure level 0', "Type combo box not selected!" + assert ui_form.metadataGroupComboBox.currentText() == 'default', "Metadata group combo box not selected!" + assert ui_form.typeMetadataTableView.model().rowCount() == 5, "metadata table should be filled!" + assert ui_form.typeAttachmentsTableView.model().rowCount() == 0, "Type attachments table should be empty!" + assert ui_form.addAttachmentPushButton.isEnabled() is True, "addAttachmentPushButton should be enabled!" + assert ui_form.editTypePushButton.isEnabled() is True, "editTypePushButton should be enabled!" + assert ui_form.editTypePushButton.isHidden() is False, "editTypePushButton should be shown!" @pytest.mark.parametrize("type_to_select, metadata_group_selected, metadata", [('Structure level 0', 'default', ['-name', 'status', 'objective', '-tags', 'comment']), ('Structure level 1', 'default', ['-name', '-tags', 'comment']), - ('Structure level 2', 'default', ['-name', '-tags', 'comment']), ('measurement', 'default', - ['-name', '-tags', - 'comment', '-type', - 'image', '#_curated', - 'sample', 'procedure']), + ('measurement', 'default', + ['-name', '-tags', 'comment', '-type', 'image', '#_curated', 'sample', 'procedure']), ('sample', 'default', ['-name', 'chemistry', '-tags', 'comment', 'qrCode']), ('procedure', 'default', ['-name', '-tags', 'comment', 'content']), ('instrument', 'default', ['-name', '-tags', 'comment', 'vendor'])]) @@ -78,10 +83,7 @@ def test_component_launch_should_load_data_hierarchy_data(self, data_hierarchy_e assert (adapt_type(ui_form.typeComboBox.currentText()) == data_hierarchy_doc_mock.types_list()[ 0]), "Type combo box should be selected to first item" selected_type = data_hierarchy_doc_mock.types()[adapt_type(ui_form.typeComboBox.currentText())] - assert (ui_form.typeDisplayedTitleLineEdit.text() == selected_type[ - "title"]), "Data type displayedTitle line edit not loaded!" - assert (ui_form.typeIriLineEdit.text() == selected_type["IRI"]), "Data type IRI line edit not loaded!" - + assert (ui_form.editTypePushButton.text() == "* Edit"), "editTypePushButton text not loaded!" categories = list(selected_type["meta"].keys()) assert ([ui_form.metadataGroupComboBox.itemText(i) for i in range(ui_form.metadataGroupComboBox.count())] == categories), "metadataGroupComboBox not loaded!" @@ -159,10 +161,23 @@ def test_component_delete_selected_type_with_loaded_data_hierarchy_should_delete assert adapt_type(ui_form.typeComboBox.currentText()) == data_hierarchy_doc_mock.types_list()[ 0], "Type combo box should be selected to first structural item" selected_type = data_hierarchy_doc_mock.types()[adapt_type(ui_form.typeComboBox.currentText())] - assert ui_form.typeDisplayedTitleLineEdit.text() == selected_type[ - "title"], "Type title line edit should be selected to first structural item" - assert ui_form.typeIriLineEdit.text() == selected_type[ - "IRI"], "Type IRI line edit should be selected to selected type IRI" + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" + assert ui_form.edit_type_dialog.buttonBox.isVisible() is True, "Create new type dialog should be shown!" + assert ui_form.edit_type_dialog.typeLineEdit.text() == "x0", "Type title line edit should be selected to first structural item" + assert ui_form.edit_type_dialog.iriLineEdit.text() == selected_type.get("IRI", + ""), "Type IRI line edit should be selected to selected type IRI" + assert ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.text() == selected_type.get("title", + ""), "Type displayedTitle line edit should be selected to selected type displayedTitle" + assert ui_form.edit_type_dialog.shortcutLineEdit.text() == selected_type.get("shortcut", + ""), "Type shortcut line edit should be selected to selected type shortcut" + assert ui_form.edit_type_dialog.iconFontCollectionComboBox.currentText() == \ + selected_type.get("icon", "").split(".")[ + 0], "icon font collection combo box should be selected to selected type icon font collection" + assert ui_form.edit_type_dialog.iconComboBox.currentText() == selected_type.get("icon", + ""), "icon combo box should be selected to selected type icon" + qtbot.mouseClick(ui_form.edit_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is False, "Create new type dialog should be closed!" assert ui_form.metadataGroupComboBox.currentText() == list(selected_type["meta"].keys())[ 0], "Type metadata group combo box should be selected to first item in the selected type" self.check_table_contents(attachments_column_names, metadata_column_names, selected_type, ui_form) @@ -181,13 +196,35 @@ def test_component_add_new_type_button_click_should_display_create_new_type_wind assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog not shown!" - def test_component_create_new_type_structural_type_should_add_new_type_with_displayed_title(self, - data_hierarchy_editor_gui: - tuple[ - QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], - data_hierarchy_doc_mock: data_hierarchy_doc_mock, - metadata_column_names: metadata_column_names, - attachments_column_names: attachments_column_names): + @pytest.mark.parametrize( + "new_type, new_title, expected_type, expected_title", + [ + # Success path tests + ("x0", "Structure level 0", "0", "Structure level 0"), # non-structural type + ("x 23", "Structure level 23", "23", "Structure level 23"), + (" x 23 ", "Structure level 23", "23", "Structure level 23"), + ("x %%", "Structure level %%", "%%", "Structure level %%"), + (" x §)(/$ x34 %% ", "Structure level §)(/$x34%%", "§)(/$x34%%", "Structure level §)(/$x34%%"), + ], + ids=[ + "case_1", + "case_with_spaces", + "case_with_ending_spaces", + "case_with_special_characters", + "case_with_special_characters_and_spaces" + ] + ) + def test_component_create_new_type_structural_type_should_do_expected(self, + data_hierarchy_editor_gui: + tuple[ + QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], + data_hierarchy_doc_mock: data_hierarchy_doc_mock, + metadata_column_names: metadata_column_names, + attachments_column_names: attachments_column_names, + new_type, + new_title, + expected_type, + expected_title): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" @@ -196,15 +233,22 @@ def test_component_create_new_type_structural_type_should_add_new_type_with_disp with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=500): assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - ui_form.create_type_dialog.structuralLevelCheckBox.setChecked(True) - ui_form.create_type_dialog.displayedTitleLineEdit.setText("test") - assert ui_form.create_type_dialog.titleLineEdit.text() == ui_form.create_type_dialog.next_struct_level.replace( - 'x', 'Structure level '), "title should be set to 'Structure level 3'" + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, new_type) + qtbot.keyClicks(ui_form.create_type_dialog.typeDisplayedTitleLineEdit, new_title) qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - assert ui_form.typeComboBox.currentText() == "Structure level 3", "Data type combo box should be newly added structural item" - assert ui_form.typeDisplayedTitleLineEdit.text() == "test", "Data type displayedTitle should be newly added displayedTitle" + assert ui_form.typeComboBox.currentText() == expected_type, "Data type combo box should be newly added structural item" + + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.edit_type_dialog.instance, timeout=500): + assert ui_form.edit_type_dialog.typeLineEdit.text() == expected_type, "Type title line edit should be selected to first structural item" + assert ui_form.edit_type_dialog.iriLineEdit.text() == "", "Type IRI line edit should be selected to selected type IRI" + assert ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.text() == expected_title, "Type displayedTitle line edit should be selected to selected type displayedTitle" + assert ui_form.edit_type_dialog.shortcutLineEdit.text() == "", "Type shortcut line edit should be selected to selected type shortcut" + assert ui_form.edit_type_dialog.iconComboBox.currentText() == 'No value', "icon combo box should be selected to selected type icon" + qtbot.mouseClick(ui_form.edit_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is False, "Create new type dialog should be closed!" def test_component_create_new_type_normal_type_should_add_new_type_with_displayed_title(self, data_hierarchy_editor_gui: @@ -221,24 +265,39 @@ def test_component_create_new_type_normal_type_should_add_new_type_with_displaye with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - assert ui_form.create_type_dialog.structuralLevelCheckBox.isChecked() is False, "structuralLevelCheckBox should be unchecked" - ui_form.create_type_dialog.titleLineEdit.setText("Title") - ui_form.create_type_dialog.displayedTitleLineEdit.setText("Displayed Title") + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, "new_type") + qtbot.keyClicks(ui_form.create_type_dialog.typeDisplayedTitleLineEdit, "new_title") + qtbot.keyClicks(ui_form.create_type_dialog.iriLineEdit, "new_iri") + qtbot.keyClicks(ui_form.create_type_dialog.shortcutLineEdit, "new_shortcut") + ui_form.create_type_dialog.iconFontCollectionComboBox.setCurrentText("mdi6") + ui_form.create_type_dialog.iconComboBox.setCurrentText("mdi6.zodiac-sagittarius") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) - assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - assert ui_form.typeComboBox.currentText() == "Title", "Data type combo box should be newly added type title" - assert ui_form.typeDisplayedTitleLineEdit.text() == "Displayed Title", "Data type combo box should be newly added type displayedTitle" - def test_component_create_new_type_normal_type_with_empty_title_should_warn_user(self, mocker, - data_hierarchy_editor_gui: tuple[ - QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], - data_hierarchy_doc_mock: data_hierarchy_doc_mock, - metadata_column_names: metadata_column_names, - attachments_column_names: attachments_column_names): + assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" + assert ui_form.typeComboBox.currentText() == "new_type", "Data type combo box should be newly added type title" + + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.edit_type_dialog.instance, timeout=500): + assert ui_form.edit_type_dialog.typeLineEdit.text() == "new_type", "Type title line edit should be selected to first structural item" + assert ui_form.edit_type_dialog.iriLineEdit.text() == "new_iri", "Type IRI line edit should be selected to selected type IRI" + assert ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.text() == "new_title", "Type displayedTitle line edit should be selected to selected type displayedTitle" + assert ui_form.edit_type_dialog.shortcutLineEdit.text() == "new_shortcut", "Type shortcut line edit should be selected to selected type shortcut" + assert ui_form.edit_type_dialog.iconFontCollectionComboBox.currentText() == 'mdi6', "icon combo box should be selected to selected type icon" + assert ui_form.edit_type_dialog.iconComboBox.currentText() == 'mdi6.zodiac-sagittarius', "icon combo box should be selected to selected type icon" + qtbot.mouseClick(ui_form.edit_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is False, "Create new type dialog should be closed!" + + def test_component_create_new_type_with_empty_type_title_should_warn_user(self, mocker, + data_hierarchy_editor_gui: tuple[ + QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], + data_hierarchy_doc_mock: data_hierarchy_doc_mock, + metadata_column_names: metadata_column_names, + attachments_column_names: attachments_column_names): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui - mocker.patch.object(ui_form.logger, 'warning') + mocker.patch.object(ui_form.logger, 'error') + mocker.patch.object(ui_form.create_type_dialog.logger, 'error') # Checking with empty title assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" @@ -247,41 +306,37 @@ def test_component_create_new_type_normal_type_with_empty_title_should_warn_user with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - assert ui_form.create_type_dialog.structuralLevelCheckBox.isChecked() is False, "structuralLevelCheckBox should be unchecked" - ui_form.create_type_dialog.titleLineEdit.setText("") - ui_form.create_type_dialog.displayedTitleLineEdit.setText("title") + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, "") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) - assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - ui_form.logger.warning.assert_called_once_with("Enter non-null/valid title!!.....") - ui_form.message_box.setText.assert_called_once_with('Enter non-null/valid title!!.....') + assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should still be shown!" + ui_form.create_type_dialog.logger.error.assert_called_once_with("Data type property is required!") + ui_form.message_box.setText.assert_called_once_with('Data type property is required!') ui_form.message_box.exec.assert_called_once_with() ui_form.message_box.setIcon.assert_called_once_with(QtWidgets.QMessageBox.Warning) assert ui_form.typeComboBox.currentText() != "", "Data type combo box should not be empty title" - assert ui_form.typeDisplayedTitleLineEdit.text() != "displayedTitle", "Data type combo box should not be newly added type displayedTitle" + qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), + Qt.LeftButton) # Checking with None title + mocker.resetall() assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is False, "Create new type dialog button box should not be shown!" qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - assert ui_form.create_type_dialog.structuralLevelCheckBox.isChecked() is False, "structuralLevelCheckBox should be unchecked" - ui_form.create_type_dialog.titleLineEdit.setText(None) - ui_form.create_type_dialog.displayedTitleLineEdit.setText("displayedTitle") + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, "test") + qtbot.keyClicks(ui_form.create_type_dialog.typeDisplayedTitleLineEdit, "") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) - assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - ui_form.logger.warning.assert_has_calls( - [mocker.call("Enter non-null/valid title!!....."), mocker.call("Enter non-null/valid title!!.....")]) - ui_form.message_box.setText.assert_has_calls( - [mocker.call("Enter non-null/valid title!!....."), mocker.call("Enter non-null/valid title!!.....")]) - ui_form.message_box.exec.assert_has_calls([mocker.call(), mocker.call()]) - ui_form.message_box.setIcon.assert_has_calls( - [mocker.call(QtWidgets.QMessageBox.Warning), mocker.call(QtWidgets.QMessageBox.Warning)]) - assert ui_form.typeComboBox.currentText() != None, "Data type combo box should not be None" - assert ui_form.typeDisplayedTitleLineEdit.text() != "displayedTitle", "Data type combo box should not be newly added type displayedTitle" + assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" + ui_form.create_type_dialog.logger.error.assert_called_once_with("Displayed title property is required!") + ui_form.message_box.setText.assert_called_once_with('Displayed title property is required!') + ui_form.message_box.exec.assert_called_once_with() + ui_form.message_box.setIcon.assert_called_once_with(QtWidgets.QMessageBox.Warning) + assert ui_form.typeComboBox.currentText() != "", "Data type combo box should not be empty title" + assert ui_form.create_type_dialog.typeDisplayedTitleLineEdit.text() == "", "Data type displayed title line edit should be empty" def test_component_create_new_type_reject_should_not_add_new_type_with_displayed_title(self, data_hierarchy_editor_gui: @@ -298,14 +353,88 @@ def test_component_create_new_type_reject_should_not_add_new_type_with_displayed with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=300): assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - assert ui_form.create_type_dialog.structuralLevelCheckBox.isChecked() is False, "structuralLevelCheckBox should be unchecked" - ui_form.create_type_dialog.titleLineEdit.setText("title") - ui_form.create_type_dialog.displayedTitleLineEdit.setText("displayedTitle") + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, "test") + qtbot.keyClicks(ui_form.create_type_dialog.typeDisplayedTitleLineEdit, "test") + qtbot.keyClicks(ui_form.create_type_dialog.iriLineEdit, "test") + qtbot.keyClicks(ui_form.create_type_dialog.shortcutLineEdit, "test") + ui_form.create_type_dialog.iconFontCollectionComboBox.setCurrentText("mdi6") + ui_form.create_type_dialog.iconComboBox.setCurrentText("mdi6.zodiac-sagittarius") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - assert ui_form.typeComboBox.currentText() != "title", "Data type combo box should not be newly added type title" - assert ui_form.typeDisplayedTitleLineEdit.text() != "displayedTitle", "Data type combo box should not be newly added type displayedTitle" + assert ui_form.typeComboBox.currentText() != "test", "Data type combo box should not be newly added type title" + + # Check if the dialog is cleared + qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=300): + assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" + assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" + assert ui_form.create_type_dialog.typeLineEdit.text() == "" + assert ui_form.create_type_dialog.typeDisplayedTitleLineEdit.text() == "" + assert ui_form.create_type_dialog.iriLineEdit.text() == "" + assert ui_form.create_type_dialog.shortcutLineEdit.text() == "" + assert ui_form.create_type_dialog.iconFontCollectionComboBox.currentText() == "fa" + assert ui_form.create_type_dialog.iconComboBox.currentText() == "No value" + qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Cancel), + Qt.LeftButton) + + def test_component_edit_existing_type_should_save_edited_contents(self, + data_hierarchy_editor_gui: + tuple[ + QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], + data_hierarchy_doc_mock: data_hierarchy_doc_mock, + metadata_column_names: metadata_column_names, + attachments_column_names: attachments_column_names): + + app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui + assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" + assert ui_form.create_type_dialog.buttonBox.isVisible() is False, "Create new type dialog button box should not be shown!" + + # Add new type + qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): + assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" + assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" + qtbot.keyClicks(ui_form.create_type_dialog.typeLineEdit, "new_type") + qtbot.keyClicks(ui_form.create_type_dialog.typeDisplayedTitleLineEdit, "new_title") + qtbot.keyClicks(ui_form.create_type_dialog.iriLineEdit, "new_iri") + qtbot.keyClicks(ui_form.create_type_dialog.shortcutLineEdit, "new_shortcut") + ui_form.create_type_dialog.iconFontCollectionComboBox.setCurrentText("mdi6") + ui_form.create_type_dialog.iconComboBox.setCurrentText("mdi6.zodiac-sagittarius") + qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), + Qt.LeftButton) + + assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" + assert ui_form.typeComboBox.currentText() == "new_type", "Data type combo box should be newly added type title" + + # Edit existing type + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.edit_type_dialog.instance, timeout=500): + assert ui_form.edit_type_dialog.typeLineEdit.text() == "new_type", "Type title line edit should be selected to first structural item" + assert ui_form.edit_type_dialog.iriLineEdit.text() == "new_iri", "Type IRI line edit should be selected to selected type IRI" + ui_form.edit_type_dialog.iriLineEdit.setText("new_iri_modified") + assert ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.text() == "new_title", "Type displayedTitle line edit should be selected to selected type displayedTitle" + ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.setText("new_title_modified") + assert ui_form.edit_type_dialog.shortcutLineEdit.text() == "new_shortcut", "Type shortcut line edit should be selected to selected type shortcut" + ui_form.edit_type_dialog.shortcutLineEdit.setText("new_shortcut_modified") + assert ui_form.edit_type_dialog.iconFontCollectionComboBox.currentText() == 'mdi6', "icon combo box should be selected to selected type icon" + assert ui_form.edit_type_dialog.iconComboBox.currentText() == 'mdi6.zodiac-sagittarius', "icon combo box should be selected to selected type icon" + ui_form.edit_type_dialog.iconFontCollectionComboBox.setCurrentText("ph") + ui_form.edit_type_dialog.iconComboBox.setCurrentText("ph.wifi-slash-light") + qtbot.mouseClick(ui_form.edit_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is False, "Create new type dialog should be closed!" + + # Check if the edited contents are saved + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + with qtbot.waitExposed(ui_form.edit_type_dialog.instance, timeout=500): + assert ui_form.edit_type_dialog.typeLineEdit.text() == "new_type", "Type title line edit should be selected to first structural item" + assert ui_form.edit_type_dialog.iriLineEdit.text() == "new_iri_modified", "Type IRI line edit should be selected to selected type IRI" + assert ui_form.edit_type_dialog.typeDisplayedTitleLineEdit.text() == "new_title_modified", "Type displayedTitle line edit should be selected to selected type displayedTitle" + assert ui_form.edit_type_dialog.shortcutLineEdit.text() == "new_shortcut_modified", "Type shortcut line edit should be selected to selected type shortcut" + assert ui_form.edit_type_dialog.iconFontCollectionComboBox.currentText() == 'ph', "icon combo box should be selected to selected type icon" + assert ui_form.edit_type_dialog.iconComboBox.currentText() == 'ph.wifi-slash-light', "icon combo box should be selected to selected type icon" + qtbot.mouseClick(ui_form.edit_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) + assert ui_form.edit_type_dialog.instance.isVisible() is False, "Create new type dialog should be closed!" def test_component_cancel_button_click_after_delete_group_should_not_modify_data_hierarchy_document_data(self, data_hierarchy_editor_gui: @@ -326,46 +455,6 @@ def test_component_cancel_button_click_after_delete_group_should_not_modify_data qtbot.mouseClick(ui_form.cancelPushButton, Qt.LeftButton) assert data_hierarchy_doc_mock.types() != ui_form.data_hierarchy_types, "Data Hierarchy Document should not be modified!" - def test_component_delete_type_after_creation_of_new_structural_type_should_succeed(self, - data_hierarchy_editor_gui: tuple[ - QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], - data_hierarchy_doc_mock: data_hierarchy_doc_mock, - metadata_column_names: metadata_column_names, - attachments_column_names: attachments_column_names): - app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui - assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - assert ui_form.create_type_dialog.buttonBox.isVisible() is False, "Create new type dialog button box should not be shown!" - qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) - with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): - assert ui_form.create_type_dialog.instance.isVisible() is True, "Create new type dialog should be shown!" - assert ui_form.create_type_dialog.buttonBox.isVisible() is True, "Create new type dialog button box should be shown!" - ui_form.create_type_dialog.structuralLevelCheckBox.setChecked(True) - ui_form.create_type_dialog.displayedTitleLineEdit.setText("test") - assert ui_form.create_type_dialog.titleLineEdit.text() == ui_form.create_type_dialog.next_struct_level.replace( - 'x', 'Structure level '), "title should be set to 'Structure level 3'" - qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), - Qt.LeftButton) - assert ui_form.create_type_dialog.instance.isVisible() is False, "Create new type dialog should not be shown!" - assert ui_form.typeComboBox.currentText() == "Structure level 3", "Data type combo box should be newly added structural item" - assert ui_form.typeDisplayedTitleLineEdit.text() == "test", "Data type displayedTitle should be newly added displayedTitle" - current_selected_type = ui_form.typeComboBox.currentText() - previous_types_count = ui_form.typeComboBox.count() - qtbot.mouseClick(ui_form.deleteTypePushButton, Qt.LeftButton) - assert (current_selected_type not in [ui_form.typeComboBox.itemText(i) for i in range( - ui_form.typeComboBox.count())]), f"Deleted type:{current_selected_type} should not exist in combo list!" - assert ( - previous_types_count - 1 == ui_form.typeComboBox.count()), f"Combo list should have {previous_types_count - 1} items!" - assert adapt_type(ui_form.typeComboBox.currentText()) == data_hierarchy_doc_mock.types_list()[ - 0], "Type combo box should be selected to first structural item" - selected_type = data_hierarchy_doc_mock.types()[adapt_type(ui_form.typeComboBox.currentText())] - assert ui_form.typeDisplayedTitleLineEdit.text() == selected_type[ - "title"], "Type displayedTitle line edit should be selected to first structural item" - assert ui_form.typeIriLineEdit.text() == selected_type[ - "IRI"], "Type IRI line edit should be selected to IRI in selected type" - assert ui_form.metadataGroupComboBox.currentText() == list(selected_type["meta"].keys())[ - 0], "Type metadata group combo box should be selected to first metadata group" - self.check_table_contents(attachments_column_names, metadata_column_names, selected_type, ui_form) - def test_component_save_button_click_after_delete_group_should_modify_data_hierarchy_document_data(self, mocker, data_hierarchy_editor_gui: tuple[ @@ -397,12 +486,13 @@ def test_component_iri_lookup_button_click_should_show_data_hierarchy_lookup_dia metadata_column_names: metadata_column_names, attachments_column_names: attachments_column_names): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui - assert ui_form.typeIriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value" + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + assert ui_form.edit_type_dialog.iriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value" iri_lookup_action = None - for act in ui_form.typeIriLineEdit.actions(): + for act in ui_form.edit_type_dialog.iriLineEdit.actions(): if isinstance(act, LookupIriAction): iri_lookup_action = act - act.trigger() + iri_lookup_action.trigger() lookup_dialog = iri_lookup_action.terminology_lookup_dialog assert lookup_dialog.selected_iris == [], "Selected IRIs should be empty" with qtbot.waitExposed(lookup_dialog.instance, timeout=500): @@ -421,7 +511,7 @@ def test_component_iri_lookup_button_click_should_show_data_hierarchy_lookup_dia qtbot.mouseClick(lookup_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) assert lookup_dialog.instance.isVisible() is False, "Data Hierarchy lookup dialog should be accepted and closed" assert len(lookup_dialog.selected_iris) >= 5, "IRIs should be set" - assert ui_form.typeIriLineEdit.text() == " ".join( + assert ui_form.edit_type_dialog.iriLineEdit.text() == " ".join( lookup_dialog.selected_iris), "typeIriLineEdit should contain all selected IRIs" def test_component_iri_lookup_button_click_should_show_data_hierarchy_lookup_dialog_and_should_not_set_iris_on_cancel( @@ -429,12 +519,13 @@ def test_component_iri_lookup_button_click_should_show_data_hierarchy_lookup_dia data_hierarchy_doc_mock: data_hierarchy_doc_mock, metadata_column_names: metadata_column_names, attachments_column_names: attachments_column_names): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui - assert ui_form.typeIriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value" + qtbot.mouseClick(ui_form.editTypePushButton, Qt.LeftButton) + assert ui_form.edit_type_dialog.iriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value" iri_lookup_action = None - for act in ui_form.typeIriLineEdit.actions(): + for act in ui_form.edit_type_dialog.iriLineEdit.actions(): if isinstance(act, LookupIriAction): iri_lookup_action = act - act.trigger() + iri_lookup_action.trigger() lookup_dialog = iri_lookup_action.terminology_lookup_dialog assert lookup_dialog.selected_iris == [], "Selected IRIs should be empty" with qtbot.waitExposed(lookup_dialog.instance, timeout=500): @@ -453,7 +544,7 @@ def test_component_iri_lookup_button_click_should_show_data_hierarchy_lookup_dia qtbot.mouseClick(lookup_dialog.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) assert lookup_dialog.instance.isVisible() is False, "data_hierarchy lookup dialog should be cancelled and closed" assert lookup_dialog.selected_iris == [], "IRIs should not be set" - assert ui_form.typeIriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value after the cancellation" + assert ui_form.edit_type_dialog.iriLineEdit.text() == 'http://url.com', "typeIriLineEdit should be default test value after the cancellation" def test_delete_type_button_must_be_disabled_for_every_structural_level_except_the_last(self, data_hierarchy_editor_gui: @@ -470,7 +561,7 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t loaded_types.append(ui_form.typeComboBox.itemText(i)) enabled_structural_type = max(filter(lambda k: 'Structure level' in k, loaded_types)) ui_form.typeComboBox.setCurrentText(enabled_structural_type) - assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for only enabled structural type: '{enabled_structural_type}'" + assert ui_form.deleteTypePushButton.isEnabled() is False, f"Delete type button must be disabled for structural type: '{enabled_structural_type}'" loaded_types.remove(enabled_structural_type) for loaded_type in loaded_types: ui_form.typeComboBox.setCurrentText(loaded_type) @@ -479,11 +570,11 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t else: assert ui_form.deleteTypePushButton.isEnabled() is True, "Delete type button must be enabled for normal types" - # Add a new structural type and check if the delete button is disabled for the previously enabled type + # Add a new type and check if the delete button is disabled for the previously enabled type qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=1000): - ui_form.create_type_dialog.structuralLevelCheckBox.setChecked(True) - ui_form.create_type_dialog.displayedTitleLineEdit.setText("test") + ui_form.create_type_dialog.typeLineEdit.setText("test") + ui_form.create_type_dialog.typeDisplayedTitleLineEdit.setText("new type") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) ui_form.typeComboBox.setCurrentText(enabled_structural_type) @@ -495,7 +586,7 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t loaded_types.append(ui_form.typeComboBox.itemText(i)) enabled_structural_type = max(filter(lambda k: 'Structure level' in k, loaded_types)) ui_form.typeComboBox.setCurrentText(enabled_structural_type) - assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for only enabled structural type: '{enabled_structural_type}'" + assert ui_form.deleteTypePushButton.isEnabled() is False, f"Delete type button must be disabled for structural type: '{enabled_structural_type}'" loaded_types.remove(enabled_structural_type) for loaded_type in loaded_types: ui_form.typeComboBox.setCurrentText(loaded_type) @@ -507,8 +598,8 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t # Add a normal type and check if the delete button is disabled correctly for the structural types qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=200): - ui_form.create_type_dialog.titleLineEdit.setText("new type") - ui_form.create_type_dialog.displayedTitleLineEdit.setText("test") + ui_form.create_type_dialog.typeLineEdit.setText("new type") + ui_form.create_type_dialog.typeDisplayedTitleLabel.setText("test") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) @@ -516,10 +607,6 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t loaded_types.clear() for i in range(ui_form.typeComboBox.count()): loaded_types.append(ui_form.typeComboBox.itemText(i)) - enabled_structural_type = max(filter(lambda k: 'Structure level' in k, loaded_types)) - ui_form.typeComboBox.setCurrentText(enabled_structural_type) - assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for only enabled structural type: '{enabled_structural_type}'" - loaded_types.remove(enabled_structural_type) for loaded_type in loaded_types: ui_form.typeComboBox.setCurrentText(loaded_type) if "Structure level" in loaded_type: @@ -527,22 +614,22 @@ def test_delete_type_button_must_be_disabled_for_every_structural_level_except_t else: assert ui_form.deleteTypePushButton.isEnabled() is True, "Delete type button must be enabled for normal types'" - def test_delete_of_structural_type_possible_from_xn_to_x1_must_succeed_and_x0_delete_disabled(self, - data_hierarchy_editor_gui: - tuple[ - QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], - data_hierarchy_doc_mock: data_hierarchy_doc_mock, - metadata_column_names: metadata_column_names, - attachments_column_names: attachments_column_names): + def test_delete_of_all_types_possible_except_structural_ones(self, + data_hierarchy_editor_gui: + tuple[ + QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], + data_hierarchy_doc_mock: data_hierarchy_doc_mock, + metadata_column_names: metadata_column_names, + attachments_column_names: attachments_column_names): app, ui_dialog, ui_form, qtbot = data_hierarchy_editor_gui assert ui_form.typeComboBox.currentText() == "Structure level 0", "Initial loaded type must be 'Structure level 0'" assert ui_form.deleteTypePushButton.isEnabled() is False, "Delete type button must be disabled for 'Structure level 0'" - # Add 5 structural types - for _ in range(5): + # Add 5 types + for i in range(5): qtbot.mouseClick(ui_form.addTypePushButton, Qt.LeftButton) with qtbot.waitExposed(ui_form.create_type_dialog.instance, timeout=300): - ui_form.create_type_dialog.structuralLevelCheckBox.setChecked(True) - ui_form.create_type_dialog.displayedTitleLineEdit.setText("test") + ui_form.create_type_dialog.typeLineEdit.setText(f"test{i}") + ui_form.create_type_dialog.typeDisplayedTitleLineEdit.setText(f"test{i}") qtbot.mouseClick(ui_form.create_type_dialog.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) @@ -551,7 +638,7 @@ def test_delete_of_structural_type_possible_from_xn_to_x1_must_succeed_and_x0_de normal_types = list(filter(lambda k: 'Structure level' not in k, loaded_types)) for normal_type in normal_types: ui_form.typeComboBox.setCurrentText(normal_type) - assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for only enabled structural type: '{normal_type}'" + assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for type: '{normal_type}'" qtbot.mouseClick(ui_form.deleteTypePushButton, Qt.LeftButton) for i in range(ui_form.typeComboBox.count()): assert ui_form.typeComboBox.itemText( @@ -560,25 +647,11 @@ def test_delete_of_structural_type_possible_from_xn_to_x1_must_succeed_and_x0_de structural_types = sorted(filter(lambda k: 'Structure level' in k, loaded_types)) assert structural_types == loaded_types, "All normal types must be deleted from UI, hence only structural types are left!" - for _ in range(len(structural_types)): - enabled_structural_type = max(structural_types) - if enabled_structural_type == 'Structure level 0': - break - for structural_type in list(structural_types): - if structural_type == enabled_structural_type: - ui_form.typeComboBox.setCurrentText(structural_type) - assert ui_form.deleteTypePushButton.isEnabled() is True, f"Delete type button must be enabled for only enabled structural type: '{structural_type}'" - qtbot.mouseClick(ui_form.deleteTypePushButton, Qt.LeftButton) - for j in range(ui_form.typeComboBox.count()): - assert ui_form.typeComboBox.itemText( - j) != structural_type, f"Deleted type:{structural_type} should not exist in combo list!" - structural_types.remove(structural_type) - loaded_types.remove(structural_type) - else: - ui_form.typeComboBox.setCurrentText(structural_type) - assert ui_form.deleteTypePushButton.isEnabled() is False, f"Delete type button must be disabled for '{structural_type}'" - assert structural_types == loaded_types == [ - "Structure level 0"], "All structural types must be deleted from UI except 'Structure level 0'" + for structural_type in list(structural_types): + ui_form.typeComboBox.setCurrentText(structural_type) + assert ui_form.deleteTypePushButton.isEnabled() is False, f"Delete type button must be disabled for '{structural_type}'" + assert structural_types == loaded_types == ['Structure level 0', 'Structure level 1'], \ + "All types must be deleted from UI except ['Structure level 0','Structure level 1']" def test_hide_show_attachments_table_should_do_as_expected(self, data_hierarchy_editor_gui: tuple[ QApplication, QtWidgets.QDialog, DataHierarchyEditorDialog, QtBot], diff --git a/tests/test_data/data_hierarchy_document.json b/tests/test_data/data_hierarchy_document.json index 13fce598..8c188293 100644 --- a/tests/test_data/data_hierarchy_document.json +++ b/tests/test_data/data_hierarchy_document.json @@ -5,6 +5,8 @@ "x0": { "IRI": "http://url.com", "title": "Projects", + "icon": "fa5s.align-center", + "shortcut": "space", "meta": { "default": [ { @@ -62,6 +64,8 @@ "x1": { "IRI": "", "title": "Tasks", + "icon": "fa5s.bold", + "shortcut": "d+c", "meta": { "default": [ { @@ -80,30 +84,11 @@ }, "attachments": [] }, - "x2": { - "IRI": "", - "title": "Subtasks", - "meta": { - "default": [ - { - "name": "-name", - "query": "What is the name of subtask?" - }, - { - "name": "-tags", - "query": "What are the tags associated with this subtask?" - }, - { - "name": "comment", - "query": "#tags comments remarks :field:value:" - } - ] - }, - "attachments": [] - }, "measurement": { "IRI": "", "title": "Measurements", + "icon": "fa5s.save", + "shortcut": "d+m", "meta": { "default": [ { @@ -147,6 +132,8 @@ "sample": { "IRI": "", "title": "Samples", + "icon": "mdi6.shopping-search-outline", + "shortcut": "d+s", "meta": { "default": [ { @@ -175,6 +162,8 @@ "procedure": { "IRI": "", "title": "Procedures", + "icon": "fa5s.expand-arrows-alt", + "shortcut": "d+p", "meta": { "default": [ { @@ -200,6 +189,8 @@ "instrument": { "IRI": "", "title": "Instruments", + "icon": "mdi6.abacus", + "shortcut": "d+i", "meta": { "default": [ { diff --git a/tests/unit_tests/test_data_hierarchy_create_type_dialog.py b/tests/unit_tests/test_data_hierarchy_create_type_dialog.py index c2002882..6a5fcdaf 100644 --- a/tests/unit_tests/test_data_hierarchy_create_type_dialog.py +++ b/tests/unit_tests/test_data_hierarchy_create_type_dialog.py @@ -6,64 +6,164 @@ # Filename: test_data_hierarchy_create_type_dialog.py # # You should have received a copy of the license with this file. Please refer the license file for more information. +import copy +from unittest.mock import MagicMock import pytest -from PySide6.QtCore import Qt - -from tests.common.fixtures import create_type_dialog_mock - - -class TestDataHierarchyCreateTypeDialog(object): - - @pytest.mark.parametrize("checked, next_level", [(True, "x0"), (False, "x1")]) - def test_structural_level_checkbox_callback_should_do_expected(self, mocker, - create_type_dialog_mock: create_type_dialog_mock, - checked, next_level): - mock_check_box = mocker.patch('PySide6.QtWidgets.QCheckBox') - mock_line_edit = mocker.patch('PySide6.QtWidgets.QLineEdit') - mocker.patch.object(mock_check_box, 'isChecked', return_value=checked) - mocker.patch.object(create_type_dialog_mock, 'structuralLevelCheckBox', mock_check_box, create=True) - mocker.patch.object(create_type_dialog_mock, 'titleLineEdit', mock_line_edit, create=True) - mocker.patch.object(create_type_dialog_mock, 'next_struct_level', next_level, create=True) - set_text_line_edit_spy = mocker.spy(mock_line_edit, 'setText') - set_disabled_line_edit_spy = mocker.spy(mock_line_edit, 'setDisabled') - clear_line_edit_spy = mocker.spy(mock_line_edit, 'clear') - assert create_type_dialog_mock.structural_level_checkbox_callback() is None, "create_type_dialog_mock.structural_level_checkbox_callback() should return None" - if checked: - set_text_line_edit_spy.assert_called_once_with(next_level.replace("x", "Structure level ")) - set_disabled_line_edit_spy.assert_called_once_with(True) +from PySide6.QtWidgets import QMessageBox +from _pytest.mark import param + +from pasta_eln.GUI.data_hierarchy.create_type_dialog import CreateTypeDialog +from pasta_eln.GUI.data_hierarchy.generic_exception import GenericException + + +@pytest.fixture +def type_dialog(mocker): + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.logging.getLogger') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconFontCollectionComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeDisplayedTitleLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iriLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.shortcutLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.buttonBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QDialog') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.LookupIriAction') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.show_message') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog_base.Ui_TypeDialogBase.setupUi') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.DataTypeInfo') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpression') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpressionValidator') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + return CreateTypeDialog(MagicMock(), MagicMock()) + + +class TestDataHierarchyCreateTypeDialog: + @pytest.mark.parametrize( + "validate_type_info, data_hierarchy_types, type_info_datatype, expected_log, expected_exception", + [ + # Success path: valid type info, new datatype + (True, {}, "new_type", + "User created a new type and added to the data_hierarchy document: Datatype: {%s}, Displayed Title: {%s}", + None), + + # Edge case: type info not valid + (False, {}, "new_type", "Type info not valid, please check and try again....", None), + + # Error case: data_hierarchy_types is None + (True, None, "new_type", "Null data_hierarchy_types, erroneous app state", GenericException), + + # Error case: datatype already exists + (True, {"existing_type": "some_value"}, "existing_type", + "Type (datatype: {%s} displayed title: {%s}) cannot be added since it exists in DB already....", + None), + ], + ids=[ + "success_path_new_type", + "edge_case_invalid_type_info", + "error_case_null_data_hierarchy_types", + "error_case_existing_datatype", + ] + ) + def test_accepted_callback(self, mocker, type_dialog, validate_type_info, data_hierarchy_types, type_info_datatype, + expected_log, + expected_exception): + # Arrange + mock_show_message = mocker.patch('pasta_eln.GUI.data_hierarchy.create_type_dialog.show_message') + data_hierarchy_types_actual = copy.deepcopy(data_hierarchy_types) + type_dialog.validate_type_info = MagicMock() + type_dialog.validate_type_info.return_value = validate_type_info + type_dialog.data_hierarchy_types = data_hierarchy_types + type_dialog.type_info.datatype = type_info_datatype + type_dialog.type_info.title = "New Type" if type_info_datatype == "new_type" else "Existing Type" + + # Act + if expected_exception: + with pytest.raises(expected_exception): + type_dialog.accepted_callback() else: - clear_line_edit_spy.assert_called_once_with() - set_disabled_line_edit_spy.assert_called_once_with(False) - - def test_show_callback_should_do_expected(self, mocker, create_type_dialog_mock): - set_window_modality_spy = mocker.spy(create_type_dialog_mock.instance, 'setWindowModality') - show_spy = mocker.spy(create_type_dialog_mock.instance, 'show') - assert create_type_dialog_mock.show() is None, "create_type_dialog_mock.show() should return None" - set_window_modality_spy.assert_called_once_with(Qt.ApplicationModal) - assert show_spy.call_count == 1, "show() should be called once" - - def test_clear_ui_callback_should_do_expected(self, mocker, create_type_dialog_mock): - mock_check_box = mocker.patch('PySide6.QtWidgets.QCheckBox') - mock_title_line_edit = mocker.patch('PySide6.QtWidgets.QLineEdit') - mock_label_line_edit = mocker.patch('PySide6.QtWidgets.QLineEdit') - mocker.patch.object(create_type_dialog_mock, 'structuralLevelCheckBox', mock_check_box, create=True) - mocker.patch.object(create_type_dialog_mock, 'titleLineEdit', mock_title_line_edit, create=True) - mocker.patch.object(create_type_dialog_mock, 'displayedTitleLineEdit', mock_label_line_edit, create=True) - title_line_edit_clear_spy = mocker.spy(mock_title_line_edit, 'clear') - label_line_edit_clear_spy = mocker.spy(mock_label_line_edit, 'clear') - check_box_set_checked_spy = mocker.spy(mock_check_box, 'setChecked') - assert create_type_dialog_mock.clear_ui() is None, "create_type_dialog_mock.clear_ui() should return None" - assert title_line_edit_clear_spy.call_count == 1, "titleLineEdit.clear() should be called once" - assert label_line_edit_clear_spy.call_count == 1, "displayedTitleLineEdit.clear() should be called once" - check_box_set_checked_spy.assert_called_once_with(False) - - @pytest.mark.parametrize("next_level", ["x0", "x1"]) - def test_set_structural_level_title_should_do_expected(self, mocker, create_type_dialog_mock, next_level): - next_set = None - mocker.patch.object(create_type_dialog_mock, 'next_struct_level', next_set, create=True) - logger_info_spy = mocker.spy(create_type_dialog_mock.logger, 'info') - assert create_type_dialog_mock.set_structural_level_title( - next_level) is None, "set_structural_level_title() should return None" - logger_info_spy.assert_called_once_with("Next structural level set: {%s}...", next_level) - assert create_type_dialog_mock.next_struct_level == next_level, "next_struct_level should be set to next_level" + type_dialog.accepted_callback() + + # Assert + if validate_type_info and data_hierarchy_types_actual is not None: + if type_info_datatype in data_hierarchy_types_actual: + type_dialog.logger.error.assert_called_with( + expected_log, + type_dialog.type_info.datatype, + type_dialog.type_info.title + ) + mock_show_message.assert_called_once_with("Type (datatype: existing_type displayed title: Existing Type)" + " cannot be added since it exists in DB already....", + QMessageBox.Icon.Warning) + else: + type_dialog.logger.info.assert_called_with(expected_log, type_info_datatype, type_dialog.type_info.title) + type_dialog.instance.close.assert_called_once() + type_dialog.accepted_callback_parent.assert_called_once() + elif not validate_type_info: + pass + elif data_hierarchy_types is None: + type_dialog.logger.error.assert_called_with(expected_log) + + @pytest.mark.parametrize( + "mock_return_value, expected_call_count", + [ + # Happy path test cases + (None, 1), # ID: happy_path_none + ("some_value", 1), # ID: happy_path_some_value + + # Edge cases + (0, 1), # ID: edge_case_zero + ("", 1), # ID: edge_case_empty_string + + # Error cases + (Exception("error"), 1), # ID: error_case_exception + ], + ids=[ + "success_path_none", + "success_path_some_value", + "edge_case_zero", + "edge_case_empty_string", + "error_case_exception", + ] + ) + def test_rejected_callback(self, type_dialog, mock_return_value, expected_call_count): + # Arrange + type_dialog.rejected_callback_parent.return_value = mock_return_value + + # Act + type_dialog.rejected_callback() + + # Assert + type_dialog.rejected_callback_parent.assert_called_once() + assert type_dialog.rejected_callback_parent.call_count == expected_call_count + + @pytest.mark.parametrize( + "data_hierarchy_types, expected", + [ + # Success path tests + param({"type1": "value1", "type2": "value2"}, {"type1": "value1", "type2": "value2"}, + id="success_path_multiple_types"), + param({"type1": 123, "type2": [1, 2, 3]}, {"type1": 123, "type2": [1, 2, 3]}, id="success_path_mixed_values"), + param({"type1": None}, {"type1": None}, id="success_path_none_value"), + + # Edge cases + param({}, {}, id="edge_case_empty_dict"), + param({"": "empty_key"}, {"": "empty_key"}, id="edge_case_empty_string_key"), + param({"type1": ""}, {"type1": ""}, id="edge_case_empty_string_value"), + + # Error cases + param(None, None, id="error_case_none_input"), + ], + ids=lambda x: x[2] + ) + def test_set_data_hierarchy_types(self, type_dialog, data_hierarchy_types, expected): + # Arrange + + # Act + type_dialog.set_data_hierarchy_types(data_hierarchy_types) + + # Assert + assert type_dialog.data_hierarchy_types == expected diff --git a/tests/unit_tests/test_data_hierarchy_data_type_info.py b/tests/unit_tests/test_data_hierarchy_data_type_info.py new file mode 100644 index 00000000..31461177 --- /dev/null +++ b/tests/unit_tests/test_data_hierarchy_data_type_info.py @@ -0,0 +1,111 @@ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: test_data_hierarchy_data_type_info.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +import pytest + +from pasta_eln.GUI.data_hierarchy.data_type_info import DataTypeInfo +from pasta_eln.dataverse.incorrect_parameter_error import IncorrectParameterError + + +class TestDataHierarchyDataTypeInfo: + def test_init(self): + data_type_info = DataTypeInfo() + assert data_type_info.datatype == "" + assert data_type_info.title == "" + assert data_type_info.iri == "" + assert data_type_info.icon == "" + assert data_type_info.shortcut == "" + + @pytest.mark.parametrize( + "datatype, title, iri, icon, shortcut", + [ + ("type1", "Title1", "http://example.com/iri1", "icon1", "shortcut1"), + ("type2", "Title2", "http://example.com/iri2", "icon2", "shortcut2"), + ("type3", "Title3", "http://example.com/iri3", "icon3", "shortcut3"), + ], + ids=["case1", "case2", "case3"] + ) + def test_data_type_info_happy_path(self, datatype, title, iri, icon, shortcut): + # Arrange + data_type_info = DataTypeInfo() + + # Act + data_type_info.datatype = datatype + data_type_info.title = title + data_type_info.iri = iri + data_type_info.icon = icon + data_type_info.shortcut = shortcut + + # Assert + assert data_type_info.datatype == datatype + assert data_type_info.title == title + assert data_type_info.iri == iri + assert data_type_info.icon == icon + assert data_type_info.shortcut == shortcut + + @pytest.mark.parametrize( + "attribute, value, expected_exception", + [ + ("datatype", 123, IncorrectParameterError), + ("title", 123, IncorrectParameterError), + ("iri", 123, IncorrectParameterError), + ("icon", 123, IncorrectParameterError), + ("shortcut", 123, IncorrectParameterError), + ], + ids=["datatype_int", "title_int", "iri_int", "icon_int", "shortcut_int"] + ) + def test_data_type_info_error_cases(self, attribute, value, expected_exception): + # Arrange + data_type_info = DataTypeInfo() + + # Act & Assert + with pytest.raises(expected_exception): + setattr(data_type_info, attribute, value) + + @pytest.mark.parametrize( + "attribute, value", + [ + ("datatype", ""), + ("title", None), + ("iri", None), + ("icon", None), + ("shortcut", None), + ], + ids=["empty_datatype", "none_title", "none_iri", "none_icon", "none_shortcut"] + ) + def test_data_type_info_edge_cases(self, attribute, value): + # Arrange + data_type_info = DataTypeInfo() + + # Act + setattr(data_type_info, attribute, value) + + # Assert + assert getattr(data_type_info, attribute) == value + + def test_data_type_info_iteration(self): + # Arrange + data_type_info = DataTypeInfo() + data_type_info.datatype = "type1" + data_type_info.title = "Title1" + data_type_info.iri = "http://example.com/iri1" + data_type_info.icon = "icon1" + data_type_info.shortcut = "shortcut1" + + # Act + items = list(data_type_info) + + # Assert + expected_items = [ + ("datatype", "type1"), + ("title", "Title1"), + ("iri", "http://example.com/iri1"), + ("icon", "icon1"), + ("shortcut", "shortcut1"), + ] + assert items == expected_items diff --git a/tests/unit_tests/test_data_hierarchy_data_type_info_validator.py b/tests/unit_tests/test_data_hierarchy_data_type_info_validator.py new file mode 100644 index 00000000..e4d95b24 --- /dev/null +++ b/tests/unit_tests/test_data_hierarchy_data_type_info_validator.py @@ -0,0 +1,58 @@ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: test_data_hierarchy_data_type_info_validator.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +import pytest + +from pasta_eln.GUI.data_hierarchy.data_type_info import DataTypeInfo +from pasta_eln.GUI.data_hierarchy.data_type_info_validator import DataTypeInfoValidator + + +class TestDataHierarchyDataTypeInfoValidator: + + @pytest.mark.parametrize( + "data_type_info, expected_exception, test_id", + [ + # Success path tests + ({"datatype": "text", "title": "Sample Title"}, None, "valid_data_type_info"), + + # Edge cases + ({"datatype": "", "title": "Sample Title"}, ValueError, "missing_datatype"), + ({"datatype": "text", "title": ""}, ValueError, "missing_title"), + + # Error cases + (None, TypeError, "none_data_type_info"), + ("invalid_type", TypeError, "string_data_type_info"), + (123, TypeError, "integer_data_type_info"), + ], + ids=[ + "valid_data_type_info", + "missing_datatype", + "missing_title", + "none_data_type_info", + "string_data_type_info", + "integer_data_type_info", + ] + ) + def test_validate(self, data_type_info, expected_exception, test_id): + # Arrange + if isinstance(data_type_info, dict): + data_type = DataTypeInfo() + for key, value in data_type_info.items(): + setattr(data_type, key, value) + data_type_info = data_type + + # Act + if expected_exception: + # Assert + with pytest.raises(expected_exception): + DataTypeInfoValidator.validate(data_type_info) + else: + # Act + DataTypeInfoValidator.validate(data_type_info) + # Assert + # No exception should be raised diff --git a/tests/unit_tests/test_data_hierarchy_edit_type_dialog.py b/tests/unit_tests/test_data_hierarchy_edit_type_dialog.py new file mode 100644 index 00000000..b9ff422a --- /dev/null +++ b/tests/unit_tests/test_data_hierarchy_edit_type_dialog.py @@ -0,0 +1,445 @@ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: test_data_hierarchy_edit_type_dialog.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from unittest.mock import MagicMock, patch + +import pytest +from PySide6.QtWidgets import QMessageBox + +from pasta_eln.GUI.data_hierarchy.edit_type_dialog import EditTypeDialog + + +@pytest.fixture +def patch_dependencies(mocker): + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.logging.getLogger') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconFontCollectionComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeDisplayedTitleLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iriLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.shortcutLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.buttonBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QDialog') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.LookupIriAction') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.show_message') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog_base.Ui_TypeDialogBase.setupUi') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.DataTypeInfo') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpression') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpressionValidator') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + + +@pytest.fixture +def edit_type_dialog(patch_dependencies): + return EditTypeDialog(MagicMock(), MagicMock()) + + +class TestDataHierarchyEditTypeDialog: + @pytest.mark.parametrize( + "accepted_callback, rejected_callback, expected_title, expected_tooltip", + [ + # Happy path test case + pytest.param( + MagicMock(), MagicMock(), "Edit existing type", "Changing type title disabled for edits!", + id="success_path" + ), + # Edge case: Empty callbacks + pytest.param( + None, None, "Edit existing type", "Changing type title disabled for edits!", + id="empty_callbacks" + ), + # Edge case: Callbacks with side effects + pytest.param( + lambda: print("Accepted"), lambda: print("Rejected"), "Edit existing type", + "Changing type title disabled for edits!", + id="callbacks_with_side_effects" + ), + ] + ) + def test_edit_type_dialog_initialization(self, patch_dependencies, accepted_callback, rejected_callback, + expected_title, expected_tooltip): + # Act + dialog = EditTypeDialog(accepted_callback, rejected_callback) + + # Assert + assert dialog.selected_data_hierarchy_type == {} + assert dialog.selected_data_hierarchy_type_name == "" + dialog.instance.setWindowTitle.assert_called_once_with("Edit existing type") + dialog.typeLineEdit.setDisabled.assert_called_once_with(True) + dialog.typeLineEdit.setToolTip.assert_called_once_with("Changing type title disabled for edits!") + dialog.typeDisplayedTitleLineEdit.toolTip.return_value.replace.assert_called_once_with("Enter", "Modify") + dialog.typeDisplayedTitleLineEdit.setToolTip.assert_called_once_with( + dialog.typeDisplayedTitleLineEdit.toolTip.return_value.replace.return_value) + dialog.iriLineEdit.toolTip.return_value.replace.assert_called_once_with("Enter", "Modify") + dialog.iriLineEdit.setToolTip.assert_called_once_with(dialog.iriLineEdit.toolTip.return_value.replace.return_value) + dialog.shortcutLineEdit.toolTip.return_value.replace.assert_called_once_with("Enter", "Modify") + dialog.shortcutLineEdit.setToolTip.assert_called_once_with( + dialog.shortcutLineEdit.toolTip.return_value.replace.return_value) + dialog.iconComboBox.currentIndexChanged[int].connect.assert_any_call(dialog.set_icon) + dialog.typeLineEdit.textChanged[str].connect.assert_any_call(dialog.type_changed) + + @pytest.mark.parametrize( + "selected_data_hierarchy_type, expected_type, expected_iri, expected_title, expected_shortcut, expected_icon_font, expected_icon", + [ + # Happy path test cases + pytest.param( + {"IRI": "http://example.com/iri", "title": "Example Title", "shortcut": "Ex", "icon": "icon.png"}, + "Example Type", "http://example.com/iri", "Example Title", "Ex", "icon", "icon.png", + id="success_path_with_icon" + ), + pytest.param( + {"IRI": "http://example.com/iri", "title": "Example Title", "shortcut": "Ex", "icon": ""}, + "Example Type", "http://example.com/iri", "Example Title", "Ex", "", "No value", + id="success_path_no_icon" + ), + # Edge case test cases + pytest.param( + {"IRI": "", "title": "", "shortcut": "", "icon": ""}, + "Empty Type", "", "", "", "", "No value", + id="edge_case_empty_values" + ), + # Error case test cases + pytest.param( + {"IRI": None, "title": None, "shortcut": None, "icon": None}, + "None Type", "", "", "", "", "No value", + id="error_case_none_values" + ), + ] + ) + def test_show_v1(self, edit_type_dialog, selected_data_hierarchy_type, expected_type, expected_iri, expected_title, + expected_shortcut, expected_icon_font, expected_icon): + # Arrange + edit_type_dialog.selected_data_hierarchy_type_name = expected_type + edit_type_dialog.selected_data_hierarchy_type = selected_data_hierarchy_type + + # Act + edit_type_dialog.show() + + # Assert + edit_type_dialog.typeLineEdit.setText.assert_called_once_with(expected_type) + edit_type_dialog.iriLineEdit.setText.assert_called_once_with(expected_iri) + edit_type_dialog.typeDisplayedTitleLineEdit.setText.assert_called_once_with(expected_title) + edit_type_dialog.shortcutLineEdit.setText.assert_called_once_with(expected_shortcut) + edit_type_dialog.iconFontCollectionComboBox.setCurrentText.assert_called_once_with(expected_icon_font) + edit_type_dialog.iconComboBox.setCurrentText.assert_called_once_with(expected_icon) + + @pytest.mark.parametrize("test_input", [ + # Happy path with all fields provided + pytest.param( + { + "selected_data_hierarchy_type_name": "Type A", + "selected_data_hierarchy_type": { + "IRI": "http://example.com/typeA", + "title": "Type A Title", + "shortcut": "TA", + "icon": "iconA.png" + } + }, + id="happy_path_all_fields" + ), + # Edge case with missing IRI + pytest.param( + { + "selected_data_hierarchy_type_name": "Type B", + "selected_data_hierarchy_type": { + "IRI": None, + "title": "Type B Title", + "shortcut": "TB", + "icon": "iconB.png" + } + }, + id="edge_case_missing_IRI" + ), + # Edge case with missing title + pytest.param( + { + "selected_data_hierarchy_type_name": "Type C", + "selected_data_hierarchy_type": { + "IRI": "http://example.com/typeC", + "title": None, + "shortcut": "TC", + "icon": "iconC.png" + } + }, + id="edge_case_missing_title" + ), + # Edge case with missing shortcut + pytest.param( + { + "selected_data_hierarchy_type_name": "Type D", + "selected_data_hierarchy_type": { + "IRI": "http://example.com/typeD", + "title": "Type D Title", + "shortcut": None, + "icon": "iconD.png" + } + }, + id="edge_case_missing_shortcut" + ), + # Edge case with missing icon + pytest.param( + { + "selected_data_hierarchy_type_name": "Type E", + "selected_data_hierarchy_type": { + "IRI": "http://example.com/typeE", + "title": "Type E Title", + "shortcut": "TE", + "icon": None + } + }, + id="edge_case_missing_icon" + ), + # Error case with invalid data type + pytest.param( + { + "selected_data_hierarchy_type_name": "Type F", + "selected_data_hierarchy_type": None + }, + id="error_case_invalid_data_type" + ), + ]) + def test_show_v2(self, mocker, edit_type_dialog, test_input): + # Arrange + mocker.resetall() + edit_type_dialog.selected_data_hierarchy_type_name = test_input["selected_data_hierarchy_type_name"] + edit_type_dialog.selected_data_hierarchy_type = test_input["selected_data_hierarchy_type"] + + # Act + edit_type_dialog.show() + + # Assert + if edit_type_dialog.selected_data_hierarchy_type is None: + edit_type_dialog.logger.warning.assert_called_once_with( + "Invalid data type: {%s}", edit_type_dialog.selected_data_hierarchy_type) + else: + edit_type_dialog.typeLineEdit.setText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type_name) + edit_type_dialog.iriLineEdit.setText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type["IRI"] or "") + edit_type_dialog.typeDisplayedTitleLineEdit.setText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type["title"] or "") + edit_type_dialog.shortcutLineEdit.setText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type["shortcut"] or "") + edit_type_dialog.iconFontCollectionComboBox.setCurrentText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type["icon"].split(".")[0] if + edit_type_dialog.selected_data_hierarchy_type[ + "icon"] else "") + edit_type_dialog.iconComboBox.setCurrentText.assert_called_once_with( + edit_type_dialog.selected_data_hierarchy_type["icon"] or "No value") + + @pytest.mark.parametrize( + "validate_type_info, selected_data_hierarchy_type, expected_log, expected_message, test_id", + [ + # Happy path + (True, {"key": "value"}, + "User updated the existing type: Datatype: {test_datatype}, Displayed Title: {test_title}", None, + "success_path"), + + # Edge case: No selected_data_hierarchy_type + (True, None, None, + "Error update scenario: Type (datatype: test_datatype displayed title: test_title) does not exists!!....", + "no_selected_type"), + + # Error case: Validation fails + (False, {"key": "value"}, None, None, "validation_fails"), + ], + ids=["success_path", "no_selected_type", "validation_fails"] + ) + def test_accepted_callback(self, edit_type_dialog, validate_type_info, selected_data_hierarchy_type, expected_log, + expected_message, test_id): + # Arrange + edit_type_dialog.type_info = MagicMock(datatype="test_datatype", title="test_title") + edit_type_dialog.validate_type_info = MagicMock() + edit_type_dialog.validate_type_info.return_value = validate_type_info + if selected_data_hierarchy_type: + edit_type_dialog.selected_data_hierarchy_type = MagicMock() + edit_type_dialog.selected_data_hierarchy_type.__getitem__.side_effect = selected_data_hierarchy_type.__getitem__ + else: + edit_type_dialog.selected_data_hierarchy_type = None + + with patch('pasta_eln.GUI.data_hierarchy.edit_type_dialog.show_message') as mock_show_message, \ + patch('pasta_eln.GUI.data_hierarchy.edit_type_dialog.generate_data_hierarchy_type', + return_value={"key": "updated_value"}) as mock_generate_data_hierarchy_type: + # Act + edit_type_dialog.accepted_callback() + + # Assert + if expected_log: + edit_type_dialog.logger.info.assert_called_once_with( + "User updated the existing type: Datatype: {%s}, Displayed Title: {%s}", + edit_type_dialog.type_info.datatype, + edit_type_dialog.type_info.title + ) + mock_generate_data_hierarchy_type.assert_called_once_with(edit_type_dialog.type_info) + edit_type_dialog.selected_data_hierarchy_type.update.assert_called_once_with({"key": "updated_value"}) + edit_type_dialog.instance.close.assert_called_once() + edit_type_dialog.accepted_callback_parent.assert_called_once() + else: + edit_type_dialog.logger.info.assert_not_called() + mock_generate_data_hierarchy_type.assert_not_called() + if selected_data_hierarchy_type: + edit_type_dialog.selected_data_hierarchy_type.update.assert_not_called() + edit_type_dialog.instance.close.assert_not_called() + edit_type_dialog.accepted_callback_parent.assert_not_called() + + if expected_message: + mock_show_message.assert_called_once_with( + expected_message, + QMessageBox.Icon.Warning + ) + else: + mock_show_message.assert_not_called() + + @pytest.mark.parametrize( + "new_index, current_text, expected_warning, expected_icon_call", + [ + # Happy path tests + (1, "icon1", None, True), # valid index and icon name + (2, "icon2", None, True), # another valid index and icon name + + # Edge cases + (0, "icon3", None, True), # boundary index value + (1, "No value", None, False), # valid index but "No value" as icon name + + # Error cases + (-1, "icon4", "Invalid index: {%s}", False), # negative index + (-10, "icon5", "Invalid index: {%s}", False), # another negative index + ], + ids=[ + "valid_index_icon1", + "valid_index_icon2", + "boundary_index_icon3", + "valid_index_no_value", + "negative_index_icon4", + "negative_index_icon5", + ] + ) + def test_set_icon(self, mocker, edit_type_dialog, new_index, current_text, expected_warning, expected_icon_call): + # Arrange + mocker.resetall() + mock_icon = mocker.patch('pasta_eln.GUI.data_hierarchy.edit_type_dialog.qta.icon') + edit_type_dialog.iconComboBox.currentText.return_value = current_text + + # Act + edit_type_dialog.set_icon(new_index) + + # Assert + if expected_warning: + edit_type_dialog.logger.warning.assert_called_once_with(expected_warning, new_index) + else: + edit_type_dialog.logger.warning.assert_not_called() + + if expected_icon_call: + mock_icon.assert_called_once_with(current_text) + edit_type_dialog.iconComboBox.setItemIcon.assert_called_once_with(new_index, mock_icon.return_value) + else: + edit_type_dialog.iconComboBox.setItemIcon.assert_not_called() + + @pytest.mark.parametrize( + "new_data_type, expected_disabled, log_warning_called, test_id", + [ + ("x0", True, False, "disable_structural_x0"), + ("x1", True, False, "disable_structural_x1"), + ("x2", False, False, "enable_non_structural_x2"), + ("", False, True, "empty_data_type"), + (None, False, True, "none_data_type"), + ], + ids=[ + "disable_structural_x0", + "disable_structural_x1", + "enable_non_structural_x2", + "empty_data_type", + "none_data_type", + ] + ) + def test_type_changed(self, mocker, edit_type_dialog, new_data_type, expected_disabled, log_warning_called, test_id): + # Arrange + mocker.resetall() + # Act + edit_type_dialog.type_changed(new_data_type) + + # Assert + assert edit_type_dialog.logger.warning.called == log_warning_called + if log_warning_called: + edit_type_dialog.logger.warning.assert_called_with("Invalid data type: {%s}", new_data_type) + edit_type_dialog.shortcutLineEdit.setDisabled.assert_not_called() + edit_type_dialog.iconComboBox.setDisabled.assert_not_called() + edit_type_dialog.iconFontCollectionComboBox.setDisabled.assert_not_called() + else: + edit_type_dialog.shortcutLineEdit.setDisabled.assert_called_with(expected_disabled) + edit_type_dialog.iconComboBox.setDisabled.assert_called_with(expected_disabled) + edit_type_dialog.iconFontCollectionComboBox.setDisabled.assert_called_with(expected_disabled) + + @pytest.mark.parametrize( + "data_hierarchy_type, expected, test_id", + [ + # Happy path tests + ({"type": "folder", "description": "A folder type"}, {"type": "folder", "description": "A folder type"}, + "success_path_folder"), + ({"type": "file", "description": "A file type"}, {"type": "file", "description": "A file type"}, + "success_path_file"), + + # Edge cases + ({}, {}, "empty_dict"), + ({"type": None}, {"type": None}, "none_type"), + ({"type": "folder", "extra": "unexpected"}, {"type": "folder", "extra": "unexpected"}, "extra_key"), + + # Error cases + (None, None, "none_input"), + ("not_a_dict", "not_a_dict", "string_input"), + (123, 123, "integer_input"), + ], + ids=[ + "success_path_folder", + "success_path_file", + "empty_dict", + "none_type", + "extra_key", + "none_input", + "string_input", + "integer_input", + ] + ) + def test_set_selected_data_hierarchy_type(self, edit_type_dialog, data_hierarchy_type, expected, test_id): + # Act + edit_type_dialog.set_selected_data_hierarchy_type(data_hierarchy_type) + + # Assert + assert edit_type_dialog.selected_data_hierarchy_type == expected + + @pytest.mark.parametrize( + "datatype, expected, test_id", + [ + ("TypeA", "TypeA", "success_path_type_a"), + ("TypeB", "TypeB", "success_path_type_b"), + ("", "", "edge_case_empty_string"), + (None, None, "edge_case_none"), + (123, 123, "edge_case_integer"), + ([], [], "edge_case_empty_list"), + (["TypeC"], ["TypeC"], "edge_case_list_with_one_element"), + ], + ids=[ + "success_path_type_a", + "success_path_type_b", + "edge_case_empty_string", + "edge_case_none", + "edge_case_integer", + "edge_case_empty_list", + "edge_case_list_with_one_element", + ] + ) + def test_set_selected_data_hierarchy_type_name(self, edit_type_dialog, datatype, expected, test_id): + + # Act + edit_type_dialog.set_selected_data_hierarchy_type_name(datatype) + + # Assert + assert edit_type_dialog.selected_data_hierarchy_type_name == expected diff --git a/tests/unit_tests/test_data_hierarchy_editor_dialog.py b/tests/unit_tests/test_data_hierarchy_editor_dialog.py index f76f9ea8..824f5529 100644 --- a/tests/unit_tests/test_data_hierarchy_editor_dialog.py +++ b/tests/unit_tests/test_data_hierarchy_editor_dialog.py @@ -6,15 +6,18 @@ # Filename: test_data_hierarchy_editor_dialog.py # # You should have received a copy of the license with this file. Please refer the license file for more information. +from functools import reduce +from unittest.mock import MagicMock, patch import pytest from PySide6 import QtWidgets -from PySide6.QtWidgets import QApplication, QDialog, QLineEdit, QMessageBox +from PySide6.QtWidgets import QApplication, QDialog, QMessageBox +from _pytest.mark import param from pasta_eln.GUI.data_hierarchy.constants import ATTACHMENT_TABLE_DELETE_COLUMN_INDEX, \ ATTACHMENT_TABLE_REORDER_COLUMN_INDEX, METADATA_TABLE_DELETE_COLUMN_INDEX, \ METADATA_TABLE_IRI_COLUMN_INDEX, METADATA_TABLE_REORDER_COLUMN_INDEX, METADATA_TABLE_REQUIRED_COLUMN_INDEX -from pasta_eln.GUI.data_hierarchy.create_type_dialog import CreateTypeDialog +from pasta_eln.GUI.data_hierarchy.create_type_dialog import CreateTypeDialog, TypeDialog from pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog import DataHierarchyEditorDialog, get_gui from pasta_eln.GUI.data_hierarchy.delete_column_delegate import DeleteColumnDelegate from pasta_eln.GUI.data_hierarchy.document_null_exception import DocumentNullException @@ -23,8 +26,9 @@ KeyNotFoundException from pasta_eln.GUI.data_hierarchy.mandatory_column_delegate import MandatoryColumnDelegate from pasta_eln.GUI.data_hierarchy.reorder_column_delegate import ReorderColumnDelegate -from pasta_eln.GUI.data_hierarchy.utility_functions import generate_empty_type, get_types_for_display -from tests.common.fixtures import configuration_extended, data_hierarchy_doc_mock +from pasta_eln.GUI.data_hierarchy.utility_functions import get_types_for_display +from pasta_eln.database import Database +from tests.common.fixtures import configuration_extended class TestDataHierarchyEditorDialog(object): @@ -36,7 +40,6 @@ def test_instantiation_should_succeed(self, mock_setup_ui = mocker.patch( 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog_base.Ui_DataHierarchyEditorDialogBase.setupUi') mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.adjust_data_hierarchy_data_to_v4') - mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.LookupIriAction') mock_metadata_table_view_model = mocker.MagicMock() mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.MetadataTableViewModel', lambda: mock_metadata_table_view_model) @@ -54,8 +57,11 @@ def test_instantiation_should_succeed(self, mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.MandatoryColumnDelegate', lambda: mock_required_column_delegate) mock_create_type_dialog = mocker.MagicMock() + mock_edit_type_dialog = mocker.MagicMock() mock_create = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.CreateTypeDialog', return_value=mock_create_type_dialog) + mock_edit = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.EditTypeDialog', + return_value=mock_edit_type_dialog) mock_delete_column_delegate = mocker.MagicMock() mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.DeleteColumnDelegate', lambda: mock_delete_column_delegate) @@ -92,10 +98,14 @@ def test_instantiation_should_succeed(self, mocker.patch.object(DataHierarchyEditorDialog, 'reorder_column_delegate_metadata_table', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'delete_column_delegate_attach_table', create=True) mocker.patch.object(DataHierarchyEditorDialog, 'reorder_column_delegate_attach_table', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'type_create_accepted_callback', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'type_create_rejected_callback', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'type_edit_accepted_callback', create=True) + mocker.patch.object(DataHierarchyEditorDialog, 'type_edit_rejected_callback', create=True) mock_setup_slots = mocker.patch.object(DataHierarchyEditorDialog, 'setup_slots', create=True) mock_load_data_hierarchy_data = mocker.patch.object(DataHierarchyEditorDialog, 'load_data_hierarchy_data', create=True) - mocker.patch.object(CreateTypeDialog, '__new__') + mocker.patch.object(TypeDialog, '__new__') config_instance = DataHierarchyEditorDialog(mock_database) assert config_instance, "DataHierarchyEditorDialog should be created" assert config_instance.type_changed_signal == mock_signal, "Signal should be created" @@ -145,9 +155,12 @@ def test_instantiation_should_succeed(self, config_instance.typeAttachmentsTableView.setColumnWidth.assert_any_call(column_index, width) config_instance.typeAttachmentsTableView.horizontalHeader().setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) - mock_create.assert_called_once_with(config_instance.create_type_accepted_callback, - config_instance.create_type_rejected_callback) + mock_create.assert_called_once_with(config_instance.type_create_accepted_callback, + config_instance.type_create_rejected_callback) assert config_instance.create_type_dialog == mock_create_type_dialog, "CreateTypeDialog should be set" + mock_edit.assert_called_once_with(config_instance.type_edit_accepted_callback, + config_instance.type_edit_rejected_callback) + assert config_instance.edit_type_dialog == mock_edit_type_dialog, "EditTypeDialog should be set" mock_setup_slots.assert_called_once_with() config_instance.addAttachmentPushButton.hide.assert_called_once_with() @@ -174,139 +187,59 @@ def test_instantiation_with_database_with_null_document_should_throw_exception(s with pytest.raises(DocumentNullException, match="Null data_hierarchy document in db instance"): DataHierarchyEditorDialog(mock_db) - @pytest.mark.parametrize("new_type_selected, mock_data_hierarchy_types", [ - ("x0", { - "x0": { - "title": "x0", - "IRI": "url", - "meta": { - "default": [ - { - "key": "key", - "value": "value"} - ], - "metadata group1": [ - { - "key": "key", - "value": "value" - } - ] - }, - "attachments": [] - }, - "x1": { - "title": "x0", - "IRI": "url", - "meta": { - "default": [ - { - "key": "key", - "value": "value"} - ], - "metadata group1": [ - { - "key": "key", - "value": "value" - } - ] - }, - "attachments": [] - } - }), - ("x1", { - "x0": { - "title": "x0", - "IRI": "url", - "meta": { - "default": [ - { - "key": "key", - "value": "value"} - ], - "metadata group1": [ - { - "key": "key", - "value": "value" - } - ] - }, - "attachments": [] - }, - "x1": { - "title": "x0", - "IRI": "url", - "meta": { - "default": [ - { - "key": "key", - "value": "value"} - ], - "metadata group1": [ - { - "key": "key", - "value": "value" - } - ] - }, - "attachments": [] - } - }), - (None, {}), - ("x0", {}), - ("x0", {"x1": {}}), - ("x0", {"x0": {}}), - ("x0", {"x0": {"title": None, "IRI": None, "meta": None, "attachments": None}}), - ("x0", {"x0": {"title": None, "IRI": None, "meta": {"": None}, "attachments": [{"": None}]}}), - ("x0", {"x0": {"": None, "§": None, "meta": {"": None}, "attachment": [{"": None}]}}) - ]) - def test_type_combo_box_changed_should_do_expected(self, - mocker, - configuration_extended: configuration_extended, - new_type_selected, - mock_data_hierarchy_types): - logger_info_spy = mocker.spy(configuration_extended.logger, 'info') - mock_signal = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.DataHierarchyEditorDialog.type_changed_signal') - mocker.patch.object(configuration_extended, 'addMetadataGroupLineEdit', create=True) - mocker.patch.object(configuration_extended, 'data_hierarchy_types', mock_data_hierarchy_types, create=True) - mocker.patch.object(configuration_extended, 'typeDisplayedTitleLineEdit', create=True) - mocker.patch.object(configuration_extended, 'typeIriLineEdit', create=True) - mocker.patch.object(configuration_extended, 'attachments_table_data_model', create=True) - mocker.patch.object(configuration_extended, 'metadataGroupComboBox', create=True) - mocker.patch.object(configuration_extended, 'type_changed_signal', mock_signal, create=True) - set_text_displayed_title_line_edit_spy = mocker.spy(configuration_extended.typeDisplayedTitleLineEdit, 'setText') - set_text_iri_line_edit_spy = mocker.spy(configuration_extended.typeIriLineEdit, 'setText') - set_current_index_metadata_group_combo_box_spy = mocker.spy(configuration_extended.metadataGroupComboBox, - 'setCurrentIndex') - clear_add_metadata_metadata_group_line_edit_spy = mocker.spy(configuration_extended.addMetadataGroupLineEdit, - 'clear') - clear_metadata_group_combo_box_spy = mocker.spy(configuration_extended.metadataGroupComboBox, 'clear') - add_items_metadata_group_combo_box_spy = mocker.spy(configuration_extended.metadataGroupComboBox, 'addItems') - update_attachment_table_model_spy = mocker.spy(configuration_extended.attachments_table_data_model, 'update') - if mock_data_hierarchy_types is not None and len( - mock_data_hierarchy_types) > 0 and new_type_selected not in mock_data_hierarchy_types: - with pytest.raises(KeyNotFoundException, - match=f"Key {new_type_selected} not found in data_hierarchy_types"): - assert configuration_extended.type_combo_box_changed( - new_type_selected) is not None, "Nothing should be returned" - - if (mock_data_hierarchy_types - and new_type_selected - and new_type_selected in mock_data_hierarchy_types): - assert configuration_extended.type_combo_box_changed(new_type_selected) is None, "Nothing should be returned" - mock_signal.emit.assert_called_once_with(new_type_selected) - logger_info_spy.assert_called_once_with("New type selected in UI: {%s}", new_type_selected) - clear_add_metadata_metadata_group_line_edit_spy.assert_called_once_with() - set_text_displayed_title_line_edit_spy.assert_called_once_with( - mock_data_hierarchy_types.get(new_type_selected).get('title')) - set_text_iri_line_edit_spy.assert_called_once_with(mock_data_hierarchy_types.get(new_type_selected).get('IRI')) - set_current_index_metadata_group_combo_box_spy.assert_called_once_with(0) - clear_metadata_group_combo_box_spy.assert_called_once_with() - add_items_metadata_group_combo_box_spy.assert_called_once_with( - list(mock_data_hierarchy_types.get(new_type_selected).get("meta").keys()) - if mock_data_hierarchy_types.get(new_type_selected).get("meta") else []) - update_attachment_table_model_spy.assert_called_once_with( - mock_data_hierarchy_types.get(new_type_selected).get('attachments')) + @pytest.mark.parametrize( + "new_type_selected, data_hierarchy_types, expected_metadata_keys, expected_attachments, test_id", + [ + # Success path test cases + ("type1", {"type1": {"meta": {"key1": "value1"}, "attachments": ["attachment1"]}}, ["key1"], ["attachment1"], + "success_path_type1"), + ("type2", {"type2": {"meta": {"key2": "value2"}, "attachments": ["attachment2"]}}, ["key2"], ["attachment2"], + "success_path_type2"), + + # Edge case: Empty metadata + ("type3", {"type3": {"meta": {}, "attachments": []}}, [], [], "edge_case_empty_metadata"), + + # Error case: Key not found + ("missing_type", {"type1": {"meta": {"key1": "value1"}, "attachments": ["attachment1"]}}, None, None, + "error_case_key_not_found"), + ], + ids=[ + "success_path_type1", + "success_path_type2", + "edge_case_empty_metadata", + "error_case_key_not_found" + ] + ) + def test_type_combo_box_changed(self, + mocker, + configuration_extended: configuration_extended, + new_type_selected, + data_hierarchy_types, + expected_metadata_keys, + expected_attachments, + test_id): + # Arrange + mocker.resetall() + configuration_extended.data_hierarchy_types = data_hierarchy_types + configuration_extended.clear_ui = mocker.MagicMock() + configuration_extended.type_changed_signal = mocker.MagicMock() + configuration_extended.attachments_table_data_model = mocker.MagicMock() + configuration_extended.metadataGroupComboBox = mocker.MagicMock() + + # Act + if test_id == "error_case_key_not_found": + with pytest.raises(KeyNotFoundException): + configuration_extended.type_combo_box_changed(new_type_selected) + else: + configuration_extended.type_combo_box_changed(new_type_selected) + + # Assert + configuration_extended.logger.info.assert_called_once_with("New type selected in UI: {%s}", new_type_selected) + configuration_extended.clear_ui.assert_called_once() + configuration_extended.type_changed_signal.emit.assert_called_once_with(new_type_selected) + configuration_extended.attachments_table_data_model.update.assert_called_once_with(expected_attachments) + configuration_extended.metadataGroupComboBox.addItems.assert_called_once_with(expected_metadata_keys) + configuration_extended.metadataGroupComboBox.setCurrentIndex.assert_called_once_with(0) @pytest.mark.parametrize("new_selected_metadata_group, selected_type_metadata", [ (None, {}), @@ -494,9 +427,8 @@ def test_update_structure_displayed_title_should_do_expected(self, assert configuration_extended.update_type_displayed_title( modified_type_displayed_title) is None, "Nothing should be returned" if data_hierarchy_types is not None and current_type in data_hierarchy_types: - get_data_hierarchy_types_spy.assert_called_once_with(current_type) + get_data_hierarchy_types_spy.assert_called_once_with(current_type, {}) assert data_hierarchy_types[current_type]["title"] == modified_type_displayed_title - configuration_extended.set_iri_lookup_action.assert_called_once_with(modified_type_displayed_title) @pytest.mark.parametrize("modified_type_iri, current_type, data_hierarchy_types", [ (None, None, None), @@ -529,7 +461,7 @@ def test_update_type_iri_should_do_expected(self, if modified_type_iri: assert configuration_extended.update_type_iri(modified_type_iri) is None, "Nothing should be returned" if data_hierarchy_types is not None and current_type in data_hierarchy_types: - get_data_hierarchy_types_spy.assert_called_once_with(current_type) + get_data_hierarchy_types_spy.assert_called_once_with(current_type, {}) assert data_hierarchy_types[current_type]["IRI"] == modified_type_iri @pytest.mark.parametrize("selected_type, data_hierarchy_types, data_hierarchy_document", [ @@ -611,169 +543,406 @@ def test_delete_selected_type_should_do_expected(self, add_items_selected_spy.assert_not_called() set_current_index_type_combo_box_spy.assert_not_called() - @pytest.mark.parametrize("new_title, new_displayed_title, is_structure_level", [ - (None, None, False), - ("x0", None, True), - (None, "x2", True), - ("x3", "x3", True), - ("instrument", "new Instrument", False) - ]) - def test_create_type_accepted_callback_should_do_expected(self, - mocker, - configuration_extended: configuration_extended, - new_title, - new_displayed_title, - is_structure_level): - mocker.patch.object(configuration_extended, 'create_type_dialog', create=True) - mocker.patch.object(configuration_extended.create_type_dialog, 'titleLineEdit', create=True) - mocker.patch.object(configuration_extended.create_type_dialog, 'next_struct_level', new_title, create=True) - mock_check_box = mocker.patch.object(configuration_extended.create_type_dialog, 'structuralLevelCheckBox', - create=True) - mocker.patch.object(mock_check_box, 'isChecked', return_value=is_structure_level, create=True) - mocker.patch.object(configuration_extended.create_type_dialog.titleLineEdit, 'text', return_value=new_title) - mocker.patch.object(configuration_extended.create_type_dialog, 'displayedTitleLineEdit', create=True) - mocker.patch.object(configuration_extended.create_type_dialog.displayedTitleLineEdit, 'text', - return_value=new_displayed_title) - clear_ui_spy = mocker.patch.object(configuration_extended.create_type_dialog, 'clear_ui', create=True) - create_new_type_spy = mocker.patch.object(configuration_extended, 'create_new_type', create=True) - text_title_line_edit_text_spy = mocker.spy(configuration_extended.create_type_dialog.titleLineEdit, 'text') - text_displayed_title_line_edit_text_spy = mocker.spy( - configuration_extended.create_type_dialog.displayedTitleLineEdit, 'text') - - assert configuration_extended.create_type_accepted_callback() is None, "Nothing should be returned" - if not is_structure_level: - text_title_line_edit_text_spy.assert_called_once_with() - text_displayed_title_line_edit_text_spy.assert_called_once_with() - clear_ui_spy.assert_called_once_with() - create_new_type_spy.assert_called_once_with( - new_title, new_displayed_title - ) + @pytest.mark.parametrize( + "data_hierarchy_types, expected_items, expected_index", + [ + # Happy path with multiple types + pytest.param( + {"type1": {}, "type2": {}, "type3": {}}, + ["type1", "type2", "type3"], + 2, + id="happy_path_multiple_types" + ), + # Edge case with a single type + pytest.param( + {"type1": {}}, + ["type1"], + 0, + id="edge_case_single_type" + ), + # Edge case with no types + pytest.param( + {}, + [], + -1, + id="edge_case_no_types" + ), + ] + ) + def test_type_create_accepted_callback(self, + mocker, + configuration_extended: configuration_extended, + data_hierarchy_types, expected_items, expected_index): + # Arrange + mocker.resetall() + configuration_extended.data_hierarchy_types = data_hierarchy_types - def test_create_type_rejected_callback_should_do_expected(self, - mocker, - configuration_extended: configuration_extended): - mocker.patch.object(configuration_extended, 'create_type_dialog', create=True) - clear_ui_spy = mocker.patch.object(configuration_extended.create_type_dialog, 'clear_ui', create=True) - assert configuration_extended.create_type_rejected_callback() is None, "Nothing should be returned" - clear_ui_spy.assert_called_once_with() - - @pytest.mark.parametrize("new_structural_title, data_hierarchy_types", [ - (None, None), - ("x0", None), - (None, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - ("x3", {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - ("x7", {"x0": {"IRI": "x0"}, "instrument": {"IRI": "x1"}}), - ("x6", {"x0": {"IRI": "x0"}, "subtask5": {"IRI": "x1"}}) - ]) - def test_show_create_type_dialog_should_do_expected(self, - mocker, - configuration_extended: configuration_extended, - new_structural_title, - data_hierarchy_types): - mocker.patch.object(configuration_extended, 'create_type_dialog', create=True) - mocker.patch.object(configuration_extended, 'data_hierarchy_types', create=True) - set_structural_level_title_spy = mocker.patch.object(configuration_extended.create_type_dialog, - 'set_structural_level_title', create=True) - mocker.patch.object(configuration_extended, 'data_hierarchy_loaded', create=True) - show_create_type_dialog_spy = mocker.patch.object(configuration_extended.create_type_dialog, 'show', create=True) - show_message_spy = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') - get_next_possible_structural_level_title_spy = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.get_next_possible_structural_level_title', - return_value=new_structural_title) - if data_hierarchy_types is not None: - configuration_extended.data_hierarchy_types.__setitem__.side_effect = data_hierarchy_types.__setitem__ - configuration_extended.data_hierarchy_types.__getitem__.side_effect = data_hierarchy_types.__getitem__ - configuration_extended.data_hierarchy_types.__iter__.side_effect = data_hierarchy_types.__iter__ - configuration_extended.data_hierarchy_types.__contains__.side_effect = data_hierarchy_types.__contains__ - configuration_extended.data_hierarchy_types.get.side_effect = data_hierarchy_types.get - configuration_extended.data_hierarchy_types.keys.side_effect = data_hierarchy_types.keys - configuration_extended.data_hierarchy_types.pop.side_effect = data_hierarchy_types.pop + # Act + configuration_extended.type_create_accepted_callback() + + # Assert + configuration_extended.typeComboBox.clear.assert_called_once() + configuration_extended.typeComboBox.addItems.assert_called_once_with(expected_items) + configuration_extended.typeComboBox.setCurrentIndex.assert_called_once_with(expected_index) + configuration_extended.create_type_dialog.clear_ui.assert_called_once() + + @pytest.mark.parametrize( + "data_hierarchy_types, expected_message, test_id", + [ + (None, "Load the data hierarchy data first....", "error_case_none"), + ("not_a_dict", "Load the data hierarchy data first....", "error_case_not_a_dict"), + ], + ids=[ + "error_case_none", + "error_case_not_a_dict" + ] + ) + def test_type_create_accepted_callback_error_cases(self, + mocker, + configuration_extended: configuration_extended, + data_hierarchy_types, expected_message, test_id): + # Arrange + mocker.resetall() + configuration_extended.data_hierarchy_types = data_hierarchy_types + + with patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') as mock_show_message: + # Act + configuration_extended.type_create_accepted_callback() + + # Assert + mock_show_message.assert_called_once_with(expected_message, QMessageBox.Icon.Warning) + configuration_extended.typeComboBox.clear.assert_not_called() + configuration_extended.typeComboBox.addItems.assert_not_called() + configuration_extended.typeComboBox.setCurrentIndex.assert_not_called() + configuration_extended.create_type_dialog.clear_ui.assert_not_called() + + @pytest.mark.parametrize( + "clear_ui_side_effect, expected_clear_ui_calls, test_id", + [ + (None, 1, "success_path"), # Happy path: clear_ui works without issues + (Exception("Clear UI failed"), 1, "clear_ui_exception"), # Edge case: clear_ui raises an exception + ], + ids=[ + "success_path", + "clear_ui_exception" + ] + ) + def test_type_create_rejected_callback(self, + configuration_extended: configuration_extended, + clear_ui_side_effect, expected_clear_ui_calls, test_id): + # Arrange + configuration_extended.create_type_dialog.clear_ui.side_effect = clear_ui_side_effect + + # Act + try: + configuration_extended.type_create_rejected_callback() + except Exception: + pass # We are testing if the method handles exceptions gracefully + + # Assert + assert configuration_extended.create_type_dialog.clear_ui.call_count == expected_clear_ui_calls + + @pytest.mark.parametrize( + "mock_dialog, expected_clear_call", + [ + # Happy path test case + pytest.param(MagicMock(spec=CreateTypeDialog), True, id="success_path"), + + # Edge case: dialog already cleared + pytest.param(MagicMock(spec=CreateTypeDialog, clear_ui=MagicMock()), True, id="already_cleared"), + + # Error case: dialog is None + pytest.param(None, False, id="dialog_none") + ] + ) + def test_type_edit_accepted_callback(self, + configuration_extended: configuration_extended, mock_dialog, + expected_clear_call): + # Arrange + configuration_extended.create_type_dialog = mock_dialog + + # Act + if mock_dialog is not None: + configuration_extended.type_edit_accepted_callback() + + # Assert + if expected_clear_call: + mock_dialog.clear_ui.assert_called_once() else: - mocker.patch.object(configuration_extended, 'data_hierarchy_types', None) + if mock_dialog is not None: + mock_dialog.clear_ui.assert_not_called() + + @pytest.mark.parametrize( + "clear_ui_side_effect, expected_clear_ui_call_count, test_id", + [ + (None, 1, "success_path"), # Happy path: clear_ui works without issues + (Exception("Clear UI failed"), 1, "clear_ui_exception"), # Edge case: clear_ui raises an exception + ], + ids=[ + "success_path", + "clear_ui_exception" + ] # Use the test_id as the parameterized test ID + ) + def test_type_edit_rejected_callback(self, + configuration_extended: configuration_extended, clear_ui_side_effect, + expected_clear_ui_call_count, + test_id): + # Arrange + configuration_extended.create_type_dialog.clear_ui.side_effect = clear_ui_side_effect + + # Act + try: + configuration_extended.type_edit_rejected_callback() + except Exception: + pass # Ignore exceptions for this test case + + # Assert + assert configuration_extended.create_type_dialog.clear_ui.call_count == expected_clear_ui_call_count + + @pytest.mark.parametrize( + "data_hierarchy_types, data_hierarchy_loaded, expected_message, test_id", + [ + # Happy path: data hierarchy is loaded and types are available + (["type1", "type2"], True, None, "success_path_with_types"), + + # Edge case: data hierarchy is loaded but no types are available + ([], True, None, "edge_case_no_types"), - assert configuration_extended.show_create_type_dialog() is None, "Nothing should be returned" - if data_hierarchy_types is not None: - get_next_possible_structural_level_title_spy.assert_called_once_with(data_hierarchy_types.keys()) - set_structural_level_title_spy.assert_called_once_with(new_structural_title) - show_create_type_dialog_spy.assert_called_once_with() + # Error case: data hierarchy is not loaded + (None, False, "Load the data hierarchy data first...", "error_case_not_loaded"), + + # Error case: data hierarchy is loaded but types are None + (None, True, "Load the data hierarchy data first...", "error_case_types_none"), + ], + ids=[ + "success_path_with_types", + "edge_case_no_types", + "error_case_not_loaded", + "error_case_types_none" + ] + ) + def test_show_create_type_dialog(self, + configuration_extended: configuration_extended, data_hierarchy_types, + data_hierarchy_loaded, expected_message, test_id): + # Arrange + configuration_extended.data_hierarchy_types = data_hierarchy_types + configuration_extended.data_hierarchy_loaded = data_hierarchy_loaded + + with patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') as mock_show_message: + # Act + configuration_extended.show_create_type_dialog() + + # Assert + if expected_message: + mock_show_message.assert_called_once_with(expected_message, QMessageBox.Icon.Warning) + configuration_extended.create_type_dialog.set_data_hierarchy_types.assert_not_called() + configuration_extended.create_type_dialog.show.assert_not_called() + else: + mock_show_message.assert_not_called() + configuration_extended.create_type_dialog.set_data_hierarchy_types.assert_called_once_with(data_hierarchy_types) + configuration_extended.create_type_dialog.show.assert_called_once() + + @pytest.mark.parametrize("data_hierarchy_types, data_hierarchy_loaded, current_text, expected_message, test_id", [ + # Happy path + ({'type1': 'Type 1 Data'}, True, 'type1', None, "happy_path_type1"), + ({'type2': 'Type 2 Data'}, True, 'type2', None, "happy_path_type2"), + + # Edge cases + ({}, True, '', None, "edge_case_empty_type"), + ({'type1': 'Type 1 Data'}, True, '', None, "edge_case_no_selection"), + + # Error cases + (None, True, 'type1', "Load the data hierarchy data first...", "error_case_no_data_hierarchy"), + ({'type1': 'Type 1 Data'}, False, 'type1', "Load the data hierarchy data first...", "error_case_not_loaded"), + ]) + def test_show_edit_type_dialog(self, + mocker, + configuration_extended: configuration_extended, + data_hierarchy_types, data_hierarchy_loaded, + current_text, expected_message, test_id): + # Arrange + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.adapt_type', side_effect=lambda x: x) + mock_show_message = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') + configuration_extended.data_hierarchy_types = data_hierarchy_types + configuration_extended.data_hierarchy_loaded = data_hierarchy_loaded + configuration_extended.typeComboBox.currentText.return_value = current_text + + # Act + configuration_extended.show_edit_type_dialog() + + # Assert + if expected_message: + mock_show_message.assert_called_once_with(expected_message, QMessageBox.Icon.Warning) else: - show_message_spy.assert_called_once_with("Load the data hierarchy data first...", QMessageBox.Warning) - get_next_possible_structural_level_title_spy.assert_not_called() - set_structural_level_title_spy.assert_not_called() - show_create_type_dialog_spy.assert_not_called() - - def test_initialize_should_setup_slots_and_should_do_expected(self, - configuration_extended: configuration_extended): - configuration_extended.logger.info.assert_any_call("Setting up slots for the editor..") - configuration_extended.logger.info.assert_any_call("User loaded the data hierarchy data in UI") - configuration_extended.addMetadataRowPushButton.clicked.connect.assert_called_once_with( - configuration_extended.metadata_table_data_model.add_data_row) - configuration_extended.addAttachmentPushButton.clicked.connect.assert_called_once_with( - configuration_extended.attachments_table_data_model.add_data_row) - configuration_extended.saveDataHierarchyPushButton.clicked.connect.assert_called_once_with( - configuration_extended.save_data_hierarchy) - configuration_extended.addMetadataGroupPushButton.clicked.connect.assert_called_once_with( - configuration_extended.add_new_metadata_group) - configuration_extended.deleteMetadataGroupPushButton.clicked.connect.assert_called_once_with( - configuration_extended.delete_selected_metadata_group) - configuration_extended.deleteTypePushButton.clicked.connect.assert_called_once_with( - configuration_extended.delete_selected_type) - configuration_extended.addTypePushButton.clicked.connect.assert_called_once_with( - configuration_extended.show_create_type_dialog) - - # Slots for the combo-boxes - configuration_extended.typeComboBox.currentTextChanged.connect.assert_called_once_with( - configuration_extended.type_combo_box_changed) - configuration_extended.metadataGroupComboBox.currentTextChanged.connect.assert_called_once_with( - configuration_extended.metadata_group_combo_box_changed) - - # Slots for line edits - configuration_extended.typeDisplayedTitleLineEdit.textChanged[str].connect.assert_called_once_with( - configuration_extended.update_type_displayed_title) - configuration_extended.typeIriLineEdit.textChanged[str].connect.assert_called_once_with( - configuration_extended.update_type_iri) - - # Slots for the delegates - configuration_extended.delete_column_delegate_metadata_table.delete_clicked_signal.connect.assert_called_once_with( - configuration_extended.metadata_table_data_model.delete_data) - configuration_extended.reorder_column_delegate_metadata_table.re_order_signal.connect.assert_called_once_with( - configuration_extended.metadata_table_data_model.re_order_data) - - configuration_extended.delete_column_delegate_attach_table.delete_clicked_signal.connect.assert_called_once_with( - configuration_extended.attachments_table_data_model.delete_data) - configuration_extended.reorder_column_delegate_attach_table.re_order_signal.connect.assert_called_once_with( - configuration_extended.attachments_table_data_model.re_order_data) - - @pytest.mark.parametrize("data_hierarchy_document", [ - 'data_hierarchy_doc_mock', - None, - {"x0": {"IRI": "x0"}, "": {"IRI": "x1"}}, - {"x0": {"IRI": "x0"}, "": {"IRI": "x1"}, 23: "test", "__id": "test"}, - {"test": ["test1", "test2", "test3"]} + configuration_extended.edit_type_dialog.set_selected_data_hierarchy_type_name.assert_called_once_with( + current_text) + configuration_extended.edit_type_dialog.set_selected_data_hierarchy_type.assert_called_once_with( + data_hierarchy_types.get(current_text, {})) + configuration_extended.edit_type_dialog.show.assert_called_once() + + @pytest.mark.parametrize("button_name, method_name", [ + ("addMetadataRowPushButton", "metadata_table_data_model.add_data_row"), + ("addAttachmentPushButton", "attachments_table_data_model.add_data_row"), + ("saveDataHierarchyPushButton", "save_data_hierarchy"), + ("addMetadataGroupPushButton", "add_new_metadata_group"), + ("deleteMetadataGroupPushButton", "delete_selected_metadata_group"), + ("deleteTypePushButton", "delete_selected_type"), + ("addTypePushButton", "show_create_type_dialog"), + ("editTypePushButton", "show_edit_type_dialog"), + ("cancelPushButton", "instance.close"), + ("attachmentsShowHidePushButton", "show_hide_attachments_table"), + ], ids=[ + "Add Metadata Row Button", + "Add Attachment Button", + "Save Data Hierarchy Button", + "Add Metadata Group Button", + "Delete Metadata Group Button", + "Delete Type Button", + "Add Type Button", + "Edit Type Button", + "Cancel Button", + "Show/Hide Attachments Button" ]) - def test_load_data_hierarchy_data_should_with_variant_types_of_doc_should_do_expected(self, - mocker, - data_hierarchy_document, - configuration_extended: configuration_extended, - request): - doc = request.getfixturevalue(data_hierarchy_document) \ - if data_hierarchy_document and type(data_hierarchy_document) is str \ - else data_hierarchy_document - mocker.patch.object(configuration_extended, 'data_hierarchy_document', doc, create=True) + def test_button_connections(self, mocker, configuration_extended: configuration_extended, button_name, method_name): + # Arrange + mocker.resetall() + button = getattr(configuration_extended, button_name) + method = reduce(getattr, method_name.split("."), configuration_extended) + + # Act + configuration_extended.setup_slots() + + # Assert + button.clicked.connect.assert_called_once_with(method) + + @pytest.mark.parametrize("combo_box_name, method_name", [ + ("typeComboBox", "type_combo_box_changed"), + ("metadataGroupComboBox", "metadata_group_combo_box_changed"), + ], ids=[ + "Type ComboBox", + "Metadata Group ComboBox" + ]) + def test_combobox_connections(self, mocker, configuration_extended: configuration_extended, combo_box_name, + method_name): + # Arrange + mocker.resetall() + combo_box = getattr(configuration_extended, combo_box_name) + method = getattr(configuration_extended, method_name) + + # Act + configuration_extended.setup_slots() + + # Assert + combo_box.currentTextChanged.connect.assert_called_once_with(method) + + @pytest.mark.parametrize("delegate_name, signal_name, method_name", [ + ("delete_column_delegate_metadata_table", "delete_clicked_signal", "metadata_table_data_model.delete_data"), + ("reorder_column_delegate_metadata_table", "re_order_signal", "metadata_table_data_model.re_order_data"), + ("delete_column_delegate_attach_table", "delete_clicked_signal", "attachments_table_data_model.delete_data"), + ("reorder_column_delegate_attach_table", "re_order_signal", "attachments_table_data_model.re_order_data"), + ], ids=[ + "Delete Column Delegate Metadata Table", + "Reorder Column Delegate Metadata Table", + "Delete Column Delegate Attach Table", + "Reorder Column Delegate Attach Table" + ]) + def test_delegate_connections(self, mocker, configuration_extended: configuration_extended, delegate_name, + signal_name, method_name): + # Arrange + mocker.resetall() + delegate = getattr(configuration_extended, delegate_name) + signal = getattr(delegate, signal_name) + method = reduce(getattr, method_name.split("."), configuration_extended) + + # Act + configuration_extended.setup_slots() + + # Assert + signal.connect.assert_any_call(method) + + def test_help_button_connection(self, mocker, configuration_extended: configuration_extended): + # Arrange + mocker.resetall() + # Act + configuration_extended.setup_slots() + configuration_extended.helpPushButton.clicked.emit() + + # Assert + assert configuration_extended.helpPushButton.clicked.connect.call_count == 1 + + def test_type_changed_signal_connection(self, mocker, configuration_extended: configuration_extended): + # Arrange + mocker.resetall() + configuration_extended.type_changed_signal = mocker.MagicMock() + method = configuration_extended.check_and_disable_delete_button + + # Act + configuration_extended.setup_slots() + + # Assert + configuration_extended.type_changed_signal.connect.assert_called_once_with(method) + + @pytest.mark.parametrize( + "data_hierarchy_document, expected_types, expected_items, test_id", + [ + # Success path with realistic data + ({"type1": {"key": "value"}, "type2": {"key": "value"}}, + {"type1": {"key": "value"}, "type2": {"key": "value"}}, + ["type1", "type2"], + "success_path_multiple_types"), + + # Edge case: empty document + ({}, + {}, + [], + "edge_case_empty_document"), + + # Edge case: single type + ({"type1": {"key": "value"}}, + {"type1": {"key": "value"}}, + ["type1"], + "edge_case_single_type"), + + # Error case: None document + (None, + None, + None, + "error_case_none_document"), + ], + ids=[ + "success_path_multiple_types", + "edge_case_single_type", + "edge_case_empty_document", + "error_case_none_document" + ] + ) + def test_load_data_hierarchy_data(self, mocker, configuration_extended: configuration_extended, + data_hierarchy_document, expected_types, expected_items, test_id): + # Arrange + mocker.resetall() + configuration_extended.data_hierarchy_document = data_hierarchy_document + if data_hierarchy_document is None: - with pytest.raises(GenericException, match="Null data_hierarchy_document, erroneous app state"): - assert configuration_extended.load_data_hierarchy_data() is None, "Nothing should be returned" - return - assert configuration_extended.load_data_hierarchy_data() is None, "Nothing should be returned" - assert configuration_extended.typeComboBox.clear.call_count == 2, "Clear should be called twice" - assert configuration_extended.typeComboBox.addItems.call_count == 2, "addItems should be called twice" - configuration_extended.typeComboBox.addItems.assert_called_with( - get_types_for_display(configuration_extended.data_hierarchy_types.keys())) - assert configuration_extended.typeComboBox.setCurrentIndex.call_count == 2, "setCurrentIndex should be called twice" - configuration_extended.typeComboBox.setCurrentIndex.assert_called_with(0) - for data in data_hierarchy_document: - if type(data) is dict: - assert data in configuration_extended.data_hierarchy_types, "Data should be loaded" + # Act and Assert + with pytest.raises(GenericException) as exec_info: + configuration_extended.load_data_hierarchy_data() + assert "Null data_hierarchy_document, erroneous app state" in str(exec_info.value) + else: + # Act + with patch( + 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.adjust_data_hierarchy_data_to_v4') as mock_adjust, \ + patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.get_types_for_display', + return_value=expected_items) as mock_get_types: + configuration_extended.load_data_hierarchy_data() + + # Assert + assert configuration_extended.data_hierarchy_types == expected_types + assert configuration_extended.data_hierarchy_loaded is True + configuration_extended.typeComboBox.clear.assert_called_once() + configuration_extended.typeComboBox.addItems.assert_called_once_with(expected_items) + configuration_extended.typeComboBox.setCurrentIndex.assert_called_once_with(0) + mock_adjust.assert_called_once_with(expected_types) + mock_get_types.assert_called_once_with(list(expected_types.keys())) @pytest.mark.parametrize("data_hierarchy_document", [None, @@ -870,7 +1039,7 @@ def test_cancel_save_data_hierarchy_should_do_expected(self, mock_is_instance.assert_not_called() if isinstance(doc[item], dict): configuration_extended.data_hierarchy_document.__delitem__.assert_not_called() - for item in configuration_extended.data_hierarchy_types: + for _ in configuration_extended.data_hierarchy_types: configuration_extended.data_hierarchy_document.__setitem__.assert_not_called() mock_check_data_hierarchy_types.assert_called_once_with(configuration_extended.data_hierarchy_types) configuration_extended.logger.info.assert_called_once_with("User clicked the save button..") @@ -881,162 +1050,156 @@ def test_cancel_save_data_hierarchy_should_do_expected(self, QMessageBox.No | QMessageBox.Yes, QMessageBox.Yes) - def test_save_data_hierarchy_with_missing_metadata_should_skip_save_and_show_message(self, - mocker, - data_hierarchy_doc_mock, - configuration_extended: configuration_extended): - mocker.patch.object(configuration_extended, 'data_hierarchy_types', create=True) - configuration_extended.data_hierarchy_document.__setitem__.side_effect = data_hierarchy_doc_mock.__setitem__ - configuration_extended.data_hierarchy_document.__getitem__.side_effect = data_hierarchy_doc_mock.__getitem__ - configuration_extended.data_hierarchy_document.__iter__.side_effect = data_hierarchy_doc_mock.__iter__ - configuration_extended.data_hierarchy_document.__contains__.side_effect = data_hierarchy_doc_mock.__contains__ + @pytest.mark.parametrize( + "types_with_missing_metadata, types_with_null_name_metadata, types_with_duplicate_metadata, expected_message, expected_log_warning, expected_log_info", + [ + # Happy path: No missing, null, or duplicate metadata + param([], [], [], "Save will close the tool and restart the Pasta Application (Yes/No?)", False, True, + id="no_missing_null_duplicate_metadata"), - log_info_spy = mocker.patch.object(configuration_extended.logger, 'info') - log_warn_spy = mocker.patch.object(configuration_extended.logger, 'warning') - mock_show_message = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') - missing_metadata = ({ - 'Structure level 0': {'metadata group1': ['-tags']}, - 'Structure level 1': {'default': ['-tags']}, - 'Structure level 2': {'default': ['-tags']}, - 'instrument': {'default': ['-tags']} - }, - { - 'Structure level 0': ['metadata group1', '-tags'], - 'instrument': ['metadata group1', '-tags'] - }, - { - 'Structure level 2': { - '-tags': ['group2', 'group3', 'default', 'group1'], - 'duplicate1': ['group2', 'group3', 'default', 'group1'], - 'duplicate2': ['group2', 'group3', 'default'], - 'duplicate3': ['group3', 'default', 'group4'] - } - }) - mock_check_data_hierarchy_document_types = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.check_data_hierarchy_types', - return_value=missing_metadata) + # Edge case: Missing metadata + param(["type1"], [], [], "Missing metadata for types: type1", True, False, id="missing_metadata"), + + # Edge case: Null name metadata + param([], ["type2"], [], "Null name metadata for types: type2", True, False, id="null_name_metadata"), + + # Edge case: Duplicate metadata + param([], [], ["type3"], "Duplicate metadata for types: type3", True, False, id="duplicate_metadata"), + + # Error case: All types of metadata issues + param(["type1"], ["type2"], ["type3"], + "Missing metadata for types: type1\nNull name metadata for types: type2\nDuplicate metadata for types: type3", + True, False, id="all_metadata_issues"), + ] + ) + def test_save_data_hierarchy_metadata_issues(self, + mocker, + configuration_extended: configuration_extended, + types_with_missing_metadata, + types_with_null_name_metadata, + types_with_duplicate_metadata, + expected_message, + expected_log_warning, + expected_log_info): + mock_check_data_hierarchy_types = mocker.patch( + 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.check_data_hierarchy_types') mock_get_missing_metadata_message = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.get_missing_metadata_message', - return_value="Missing message") - assert configuration_extended.save_data_hierarchy() is None, "Nothing should be returned" - log_info_spy.assert_called_once_with("User clicked the save button..") - mock_check_data_hierarchy_document_types.assert_called_once_with(configuration_extended.data_hierarchy_types) - mock_get_missing_metadata_message.assert_called_once_with(missing_metadata[0], missing_metadata[1], - missing_metadata[2]) - mock_show_message.assert_called_once_with("Missing message", QMessageBox.Warning) - log_warn_spy.assert_called_once_with("Missing message") - - @pytest.mark.parametrize("new_title, new_displayed_title, data_hierarchy_document, data_hierarchy_types", [ - (None, None, None, None), - (None, None, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - ("x0", None, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - (None, "x1", {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - ("x0", "x1", {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}, {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}), - ("x0", "x1", None, None), - ("instrument", "new Instrument", {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}, - {"x0": {"IRI": "x0"}, "x1": {"IRI": "x1"}}) - ]) - def test_create_new_type_should_do_expected(self, - mocker, - new_title, - new_displayed_title, - data_hierarchy_document, - data_hierarchy_types, - configuration_extended: configuration_extended): - mocker.patch.object(configuration_extended, 'data_hierarchy_document', create=True) - mocker.patch.object(configuration_extended, 'data_hierarchy_types', create=True) - mock_show_message = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') - mock_log_info = mocker.patch.object(configuration_extended.logger, 'info') - mock_log_error = mocker.patch.object(configuration_extended.logger, 'error') - mock_log_warn = mocker.patch.object(configuration_extended.logger, 'warning') - if data_hierarchy_document: - configuration_extended.data_hierarchy_document.__setitem__.side_effect = data_hierarchy_document.__setitem__ - configuration_extended.data_hierarchy_document.__getitem__.side_effect = data_hierarchy_document.__getitem__ - configuration_extended.data_hierarchy_document.__iter__.side_effect = data_hierarchy_document.__iter__ - configuration_extended.data_hierarchy_document.__contains__.side_effect = data_hierarchy_document.__contains__ - configuration_extended.data_hierarchy_document.get.side_effect = data_hierarchy_document.get - configuration_extended.data_hierarchy_document.keys.side_effect = data_hierarchy_document.keys - configuration_extended.data_hierarchy_document.pop.side_effect = data_hierarchy_document.pop - if data_hierarchy_types is not None: - configuration_extended.data_hierarchy_types.__setitem__.side_effect = data_hierarchy_types.__setitem__ - configuration_extended.data_hierarchy_types.__getitem__.side_effect = data_hierarchy_types.__getitem__ - configuration_extended.data_hierarchy_types.__iter__.side_effect = data_hierarchy_types.__iter__ - configuration_extended.data_hierarchy_types.__contains__.side_effect = data_hierarchy_types.__contains__ - configuration_extended.data_hierarchy_types.get.side_effect = data_hierarchy_types.get - configuration_extended.data_hierarchy_types.keys.side_effect = data_hierarchy_types.keys - configuration_extended.data_hierarchy_types.pop.side_effect = data_hierarchy_types.pop - configuration_extended.data_hierarchy_types.__len__.side_effect = data_hierarchy_types.__len__ + 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.get_missing_metadata_message') + mock_show_message = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') + mock_check_data_hierarchy_types.return_value = ( + types_with_missing_metadata, types_with_null_name_metadata, types_with_duplicate_metadata) + mock_get_missing_metadata_message.return_value = expected_message - if data_hierarchy_document is None: - mocker.patch.object(configuration_extended, 'data_hierarchy_document', None, create=True) - if data_hierarchy_types is None: - mocker.patch.object(configuration_extended, 'data_hierarchy_types', None, create=True) - - if data_hierarchy_document is None or data_hierarchy_types is None or new_title in data_hierarchy_document: - if data_hierarchy_document is None or data_hierarchy_types is None: - with pytest.raises(GenericException, - match="Null data_hierarchy_document/data_hierarchy_types, erroneous app state"): - assert configuration_extended.create_new_type(new_title, - new_displayed_title) is None, "Nothing should be returned" - mock_log_error.assert_called_once_with( - "Null data_hierarchy_document/data_hierarchy_types, erroneous app state") + # Act + configuration_extended.save_data_hierarchy() + + # Assert + if expected_message: + if expected_log_info: + mock_show_message.assert_called_once_with(expected_message, QMessageBox.Icon.Question, + QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.Yes) else: - assert configuration_extended.create_new_type(new_title, - new_displayed_title) is None, "Nothing should be returned" - mock_show_message.assert_called_once_with(f"Type (title: {new_title} " - f"displayed title: {new_displayed_title}) cannot be added " - f"since it exists in DB already....", QMessageBox.Warning) + mock_show_message.assert_called_once_with(expected_message, QMessageBox.Icon.Warning) + if expected_log_warning: + configuration_extended.logger.warning.assert_called_once_with(expected_message) else: - if new_title is None: - assert configuration_extended.create_new_type(None, new_displayed_title) is None, "Nothing should be returned" - mock_show_message.assert_called_once_with("Enter non-null/valid title!!.....", QMessageBox.Warning) - mock_log_warn.assert_called_once_with("Enter non-null/valid title!!.....") - else: - assert configuration_extended.create_new_type(new_title, - new_displayed_title) is None, "Nothing should be returned" - mock_log_info.assert_called_once_with("User created a new type and added " - "to the data_hierarchy document: Title: {%s}, Displayed Title: {%s}", - new_title, - new_displayed_title) - - (configuration_extended.data_hierarchy_types - .__setitem__.assert_called_once_with(new_title, generate_empty_type(new_displayed_title))) - assert configuration_extended.typeComboBox.clear.call_count == 2, "ComboBox should be cleared twice" - assert configuration_extended.typeComboBox.addItems.call_count == 2, "ComboBox addItems should be called twice" - configuration_extended.typeComboBox.addItems.assert_called_with( - get_types_for_display(configuration_extended.data_hierarchy_types.keys())) - - @pytest.mark.parametrize("instance_exists", [True, False]) - def test_get_gui_should_do_expected(self, - mocker, - configuration_extended: configuration_extended, - instance_exists): - mock_form = mocker.MagicMock() - mock_sys_argv = mocker.patch( - "pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.sys.argv") - mock_new_app_inst = mocker.patch("PySide6.QtWidgets.QApplication") - mock_exist_app_inst = mocker.patch("PySide6.QtWidgets.QApplication") - mock_form_instance = mocker.patch("PySide6.QtWidgets.QDialog") - mock_database = mocker.patch("pasta_eln.database.Database") - - mocker.patch.object(QApplication, 'instance', return_value=mock_exist_app_inst if instance_exists else None) - mocker.patch.object(mock_form, 'instance', mock_form_instance, create=True) - spy_new_app_inst = mocker.patch.object(QApplication, '__new__', return_value=mock_new_app_inst) - spy_form_inst = mocker.patch.object(DataHierarchyEditorDialog, '__new__', return_value=mock_form) - - (app, form_inst, form) = get_gui(mock_database) - spy_form_inst.assert_called_once_with(DataHierarchyEditorDialog, mock_database) - if instance_exists: - assert app is mock_exist_app_inst, "Should return existing instance" - assert form_inst is mock_form_instance, "Should return existing instance" - assert form is mock_form, "Should return existing instance" + mock_show_message.assert_not_called() + configuration_extended.logger.warning.assert_not_called() + + @pytest.mark.parametrize( + "user_response, expected_save_call", + [ + # Success path: User chooses to save + param(QMessageBox.StandardButton.Yes, True, id="user_confirms_save"), + + # Edge case: User chooses not to save + param(QMessageBox.StandardButton.No, False, id="user_declines_save"), + ] + ) + def test_save_data_hierarchy_user_confirmation(self, + mocker, + configuration_extended: configuration_extended, + user_response, + expected_save_call): + mock_show_message = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.show_message') + mock_show_message.return_value = user_response + + # Act + configuration_extended.save_data_hierarchy() + + # Assert + if expected_save_call: + configuration_extended.data_hierarchy_document.save.assert_called_once() + configuration_extended.database.initDocTypeViews.assert_called_once_with(16) + configuration_extended.instance.close.assert_called_once() else: - spy_new_app_inst.assert_called_once_with(QApplication, mock_sys_argv) - assert app is mock_new_app_inst, "Should return new instance" - assert form_inst is mock_form_instance, "Should return existing instance" - assert form is mock_form, "Should return existing instance" + configuration_extended.data_hierarchy_document.save.assert_not_called() + configuration_extended.database.initDocTypeViews.assert_not_called() + configuration_extended.instance.close.assert_not_called() + + @pytest.mark.parametrize( + "existing_instance, expected_instance_type, test_id", + [ + (None, QApplication, "no_existing_qapplication"), + (MagicMock(spec=QApplication), QApplication, "existing_qapplication"), + ], + ids=[ + "no_existing_qapplication", + "existing_qapplication" + ] + ) + def test_get_gui_success_path(self, + mocker, + configuration_extended: configuration_extended, + existing_instance, + expected_instance_type, + test_id): + # Arrange + database = mocker.MagicMock(spec=Database) + app_instance = mocker.MagicMock(spec=QApplication) + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.QApplication', + return_value=app_instance) + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.QApplication.instance', + return_value=existing_instance) + mock_dialog = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.DataHierarchyEditorDialog', + return_value=mocker.MagicMock(instance=mocker.MagicMock())) + + # Act + application, dialog_instance, data_hierarchy_form = get_gui(database) + + # Assert + assert isinstance(application, expected_instance_type) + assert dialog_instance == mock_dialog.return_value.instance + assert data_hierarchy_form == mock_dialog.return_value + + @pytest.mark.parametrize( + "database, test_id", + [ + (None, "none_database"), + ("invalid_database", "invalid_database_type"), + ], + ids=[ + "none_database", + "invalid_database_type" + ] + ) + def test_get_gui_error_cases(self, + mocker, + configuration_extended: configuration_extended, + database, test_id): + # Arrange + app_instance = mocker.MagicMock(spec=QApplication) + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.QApplication', + return_value=app_instance) + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.QApplication.instance', + return_value=None) + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.DataHierarchyEditorDialog', + side_effect=TypeError) + + # Act & Assert + with pytest.raises(TypeError): + get_gui(database) @pytest.mark.parametrize("hidden", [True, False]) def test_show_hide_attachments_table_do_expected(self, @@ -1056,33 +1219,32 @@ def test_show_hide_attachments_table_do_expected(self, spy_add_attachment_set_visible.assert_called_once_with(not hidden) spy_add_attachment_is_visible.assert_called_once_with() - def test_set_iri_lookup_action_do_expected(self, - mocker, - configuration_extended: configuration_extended): - mock_actions = [mocker.MagicMock(), mocker.MagicMock()] - configuration_extended.typeIriLineEdit.actions.return_value = mock_actions - mock_is_instance = mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.isinstance', - return_value=True) - mock_is_lookup_iri_action = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.LookupIriAction') - - assert configuration_extended.set_iri_lookup_action("default") is None, "Nothing should be returned" - mock_is_instance.assert_has_calls( - [mocker.call(mock_actions[0], mock_is_lookup_iri_action), - mocker.call(mock_actions[1], mock_is_lookup_iri_action)]) - mock_is_lookup_iri_action.assert_called_once_with(parent_line_edit=configuration_extended.typeIriLineEdit, - lookup_term="default") - configuration_extended.typeIriLineEdit.addAction.assert_called_once_with(mock_is_lookup_iri_action.return_value, - QLineEdit.TrailingPosition) - - def test_check_and_disable_delete_button_should_do_expected(self, - mocker, - configuration_extended: configuration_extended): - mock_can_delete_type = mocker.patch( - 'pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.can_delete_type', return_value=True) - mock_data_hierarchy_types = mocker.MagicMock() - mocker.patch.object(configuration_extended, 'data_hierarchy_types', mock_data_hierarchy_types) - mock_data_hierarchy_types.keys.return_value = ['one', 'two'] - assert configuration_extended.check_and_disable_delete_button("three") is None, "Nothing should be returned" - configuration_extended.deleteTypePushButton.setEnabled.assert_called_once_with(True) - mock_can_delete_type.assert_called_once_with(['one', 'two'], "three") + @pytest.mark.parametrize( + "selected_type, can_delete, expected_enabled, test_id", + [ + ("type1", True, True, "success_path_type1"), + ("type2", False, False, "success_path_type2"), + ("", False, False, "edge_case_empty_string"), + ("nonexistent_type", False, False, "edge_case_nonexistent_type"), + ("type_with_special_chars!@#", True, True, "edge_case_special_chars"), + ], + ids=[ + "success_path_type1", + "success_path_type2", + "edge_case_empty_string", + "edge_case_nonexistent_type", + "edge_case_special_chars", + ] + ) + def test_check_and_disable_delete_button(self, + mocker, + configuration_extended: configuration_extended, + selected_type, can_delete, expected_enabled, test_id): + # Arrange + mocker.patch('pasta_eln.GUI.data_hierarchy.data_hierarchy_editor_dialog.can_delete_type', return_value=can_delete) + + # Act + configuration_extended.check_and_disable_delete_button(selected_type) + + # Assert + configuration_extended.deleteTypePushButton.setEnabled.assert_called_once_with(expected_enabled) diff --git a/tests/unit_tests/test_data_hierarchy_qtaicons_factory.py b/tests/unit_tests/test_data_hierarchy_qtaicons_factory.py new file mode 100644 index 00000000..f7557ca6 --- /dev/null +++ b/tests/unit_tests/test_data_hierarchy_qtaicons_factory.py @@ -0,0 +1,181 @@ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: test_data_hierarchy_icon_names.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from unittest.mock import MagicMock, patch + +import pytest + +from pasta_eln.GUI.data_hierarchy.qtaicons_factory import QTAIconsFactory +from pasta_eln.dataverse.incorrect_parameter_error import IncorrectParameterError + + +@pytest.fixture +def qta_icons_factory(mocker): + mocker.patch("pasta_eln.GUI.data_hierarchy.qtaicons_factory.logging") + iconic_mock = mocker.MagicMock() + iconic_mock.charmap = { + 'fa': ['value1', 'value2'], + 'fa5': ['value3', 'value4'], + 'fa5s': ['value5', 'value13'], + 'fa5b': ['value6', 'value14'], + 'ei': ['value7', 'value15'], + 'mdi': ['value8', 'value16'], + 'mdi6': ['value9', 'value17'], + 'ph': ['value10', 'value18'], + 'ri': ['value11', 'value19'], + 'msc': ['value12', 'value20'], + } + mocker.patch("pasta_eln.GUI.data_hierarchy.qtaicons_factory.qta._resource", {"iconic": iconic_mock}) + return QTAIconsFactory.get_instance() + + +class TestDataHierarchyQTAIconsFactory: + + @pytest.mark.parametrize("description", [ + pytest.param("First call to get_instance", id="first_call"), + pytest.param("Subsequent call to get_instance", id="subsequent_call"), + ]) + def test_get_instance_singleton_behavior(self, description): + # Act + instance1 = QTAIconsFactory.get_instance() + instance2 = QTAIconsFactory.get_instance() + + # Assert + assert instance1 is instance2, "get_instance should return the same instance on subsequent calls" + + @pytest.mark.parametrize("description", [ + pytest.param("Check instance type", id="check_instance_type"), + ]) + def test_get_instance_type(self, description): + # Act + instance = QTAIconsFactory.get_instance() + + # Assert + assert isinstance(instance, QTAIconsFactory), "get_instance should return an instance of YourClassName" + + @pytest.mark.parametrize("description", [ + pytest.param("Check instance attribute existence", id="check_instance_attribute"), + ]) + def test_get_instance_attribute_existence(self, description): + # Arrange + QTAIconsFactory.get_instance() + + # Act + has_instance_attr = hasattr(QTAIconsFactory, '_instance') + + # Assert + assert has_instance_attr, "Class should have '_instance' attribute after get_instance is called" + + @pytest.mark.parametrize( + "has_instance", + [ + (True), + (False) + ], + ids=["instance_attribute_exists_but_not_initialized", "instance_attribute_does_not_exist"] + ) + def test_init(self, mocker, has_instance): + mocker.resetall() + if has_instance: + QTAIconsFactory._instance = None + else: + delattr(QTAIconsFactory, "_instance") + mock_logging = mocker.patch("pasta_eln.GUI.data_hierarchy.qtaicons_factory.logging") + mock_set_icon_names = mocker.patch( + "pasta_eln.GUI.data_hierarchy.qtaicons_factory.QTAIconsFactory.set_icon_names") + + instance = QTAIconsFactory.get_instance() + mock_logging.getLogger.assert_called_once_with("pasta_eln.GUI.data_hierarchy.qtaicons_factory.QTAIconsFactory") + mock_set_icon_names.assert_called_once() + + assert instance.icon_names == {} + assert instance._icons_initialized == False + assert instance._font_collections == ['fa', 'fa5', 'fa5s', 'fa5b', 'ei', 'mdi', 'mdi6', 'ph', 'ri', 'msc'] + + def test_only_one_instance_created(self, qta_icons_factory): + for _ in range(5): + instance = QTAIconsFactory.get_instance() + assert instance is qta_icons_factory + + @pytest.mark.parametrize( + "font_collections, expected", + [ + (['fa', 'fa5'], {'fa': ['No value', 'fa.value1', 'fa.value2'], 'fa5': ['No value', 'fa5.value3', 'fa5.value4']}), + (['mdi'], {'mdi': ['No value', 'mdi.value8', 'mdi.value16']}), + ([], {}), + ], + ids=["two_collections", "one_collection", "empty_collection"] + ) + def test_set_icon_names(self, qta_icons_factory, font_collections, expected): + # Arrange + qta_icons_factory._icons_initialized = False + qta_icons_factory.font_collections = font_collections + + # Act + qta_icons_factory.set_icon_names() + + # Assert + assert qta_icons_factory.icon_names == expected + assert qta_icons_factory._icons_initialized + + @pytest.mark.parametrize( + "font_collections, expected_exception", + [ + (None, IncorrectParameterError), + ("not_a_list", IncorrectParameterError), + ], + ids=["None_type", "string_type"] + ) + def test_font_collections_setter_invalid(self, qta_icons_factory, font_collections, expected_exception): + # Act & Assert + with pytest.raises(expected_exception): + qta_icons_factory.font_collections = font_collections + + @pytest.mark.parametrize( + "icon_names, expected_exception", + [ + (None, IncorrectParameterError), + ("not_a_dict", IncorrectParameterError), + ], + ids=["None_type", "string_type"] + ) + def test_icon_names_setter_invalid(self, qta_icons_factory, icon_names, expected_exception): + # Act & Assert + with pytest.raises(expected_exception): + qta_icons_factory.icon_names = icon_names + + def test_icon_names_property_initialization(self, qta_icons_factory): + # Act + icon_names = qta_icons_factory.icon_names + + # Assert + assert qta_icons_factory._icons_initialized + assert icon_names == qta_icons_factory._icon_names + + def test_set_icon_names_already_initialized(self, qta_icons_factory): + # Arrange + qta_icons_factory._icons_initialized = True + + # Act + with patch.object(qta_icons_factory.logger, 'warning') as mock_warning: + qta_icons_factory.set_icon_names() + + # Assert + mock_warning.assert_called_once_with("Icons already initialized!") + + def test_set_icon_names_no_font_maps(self, mocker, qta_icons_factory): + # Arrange + qta_icons_factory.logger = mocker.MagicMock() + qta_icons_factory._icons_initialized = False + mocker.patch("pasta_eln.GUI.data_hierarchy.qtaicons_factory.qta._resource", {"iconic": MagicMock(charmap=None)}) + + # Act + qta_icons_factory.set_icon_names() + + # Assert + qta_icons_factory.logger.warning.assert_called_once_with("font_maps could not be found!") diff --git a/tests/unit_tests/test_data_hierarchy_type_dialog.py b/tests/unit_tests/test_data_hierarchy_type_dialog.py new file mode 100644 index 00000000..80633192 --- /dev/null +++ b/tests/unit_tests/test_data_hierarchy_type_dialog.py @@ -0,0 +1,420 @@ +# PASTA-ELN and all its sub-parts are covered by the MIT license. +# +# Copyright (c) 2024 +# +# Author: Jithu Murugan +# Filename: test_data_hierarchy_type_dialog.py +# +# You should have received a copy of the license with this file. Please refer the license file for more information. +from unittest.mock import MagicMock, patch + +import pytest +from PySide6 import QtWidgets +from PySide6.QtWidgets import QDialogButtonBox, QLineEdit, QMessageBox +from _pytest.mark import param + +from pasta_eln.GUI.data_hierarchy.data_type_info_validator import DataTypeInfoValidator +from pasta_eln.GUI.data_hierarchy.lookup_iri_action import LookupIriAction +from pasta_eln.GUI.data_hierarchy.type_dialog import TypeDialog + + +@pytest.fixture +def type_dialog(mocker): + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.logging.getLogger') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconFontCollectionComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeDisplayedTitleLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iriLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.shortcutLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.buttonBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QDialog') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.LookupIriAction') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.show_message') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog_base.Ui_TypeDialogBase.setupUi') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.DataTypeInfo') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpression') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpressionValidator') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + return TypeDialog(MagicMock(), MagicMock()) + + +class TestDataHierarchyTypeDialog: + + def test_init(self, mocker): + # Arrange + accepted_callback = MagicMock() + rejected_callback = MagicMock() + mock_get_logger = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.logging.getLogger') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconFontCollectionComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iconComboBox', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeDisplayedTitleLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.typeLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.iriLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.shortcutLineEdit', create=True) + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.buttonBox', create=True) + mock_setup_slots = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.setup_slots') + mock_q_dialog = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QDialog') + mock_data_type_info = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.DataTypeInfo') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.LookupIriAction') + mock_regular_expression_validator = mocker.patch( + 'pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpressionValidator') + mock_regular_expression = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QRegularExpression') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.show_message') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.populate_icons') + mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog.set_iri_lookup_action') + mock_qta_icons_singleton = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog.QTAIconsFactory', + MagicMock(font_collections=['Font1', 'Font2'])) + mock_setup_ui = mocker.patch('pasta_eln.GUI.data_hierarchy.type_dialog_base.Ui_TypeDialogBase.setupUi') + + # Act + type_dialog = TypeDialog(accepted_callback, rejected_callback) + + # Assert + mock_get_logger.assert_called_with('pasta_eln.GUI.data_hierarchy.type_dialog.TypeDialog') + mock_data_type_info.assert_called_once() + mock_q_dialog.assert_called_once() + mock_setup_ui.assert_called_once_with(mock_q_dialog.return_value) + mock_qta_icons_singleton.get_instance.assert_called_once() + mock_setup_slots.assert_called_once() + assert type_dialog.accepted_callback_parent == accepted_callback + assert type_dialog.rejected_callback_parent == rejected_callback + + type_dialog.iconFontCollectionComboBox.addItems.assert_called_once_with(type_dialog.qta_icons.font_collections) + type_dialog.populate_icons.assert_called_once_with(type_dialog.qta_icons.font_collections[0]) + mock_regular_expression.assert_called_once_with('(?=^[^Ax])(?=[^ ]*)') + mock_regular_expression_validator.assert_called_once_with(mock_regular_expression.return_value) + type_dialog.typeLineEdit.setValidator.assert_called_once_with(mock_regular_expression_validator.return_value) + type_dialog.iconComboBox.completer().setCompletionMode.assert_called_once_with( + QtWidgets.QCompleter.CompletionMode.PopupCompletion) + type_dialog.set_iri_lookup_action.assert_called_once_with("") + + @pytest.mark.parametrize( + "type_info, validator_side_effect, expected_valid, log_error_called", + [ + # Success path tests + param({"key": "value"}, None, True, False, id="valid_type_info"), + param({"another_key": "another_value"}, None, True, False, id="another_valid_type_info"), + + # Edge cases + param({}, None, True, False, id="empty_type_info"), + param({"key": None}, None, True, False, id="none_value_in_type_info"), + + # Error cases + param({"key": "value"}, TypeError("Invalid type"), False, True, id="type_error"), + param({"key": "value"}, ValueError("Invalid value"), False, True, id="value_error"), + ], + ids=lambda x: x[-1] + ) + def test_validate_type_info(self, type_info, type_dialog, validator_side_effect, expected_valid, log_error_called): + # Arrange + type_dialog.type_info = type_info + + with patch.object(DataTypeInfoValidator, 'validate', side_effect=validator_side_effect): + with patch('pasta_eln.GUI.data_hierarchy.type_dialog.show_message') as mock_show_message: + + # Act + result = type_dialog.validate_type_info() + + # Assert + assert result == expected_valid + if validator_side_effect: + mock_show_message.assert_called_once_with(str(validator_side_effect), QMessageBox.Icon.Warning) + type_dialog.logger.error.assert_called_once_with(str(validator_side_effect)) + else: + mock_show_message.assert_not_called() + type_dialog.logger.error.assert_not_called() + + def test_setup_slots(self, type_dialog): + # Act + type_dialog.setup_slots() + + # Assert + assert type_dialog.iconFontCollectionComboBox.currentTextChanged[str].isConnected() + assert type_dialog.typeDisplayedTitleLineEdit.textChanged[str].isConnected() + assert type_dialog.typeLineEdit.textChanged[str].isConnected() + assert type_dialog.iriLineEdit.textChanged[str].isConnected() + assert type_dialog.shortcutLineEdit.textChanged[str].isConnected() + assert type_dialog.iconComboBox.currentTextChanged[str].isConnected() + assert type_dialog.buttonBox.rejected.isConnected() + assert type_dialog.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.isConnected() + + @pytest.mark.parametrize( + "lookup_term, existing_actions, expected_action_count", + [ + ("http://example.com/iri1", [], 1), # success path with no existing actions + ("http://example.com/iri2", + [MagicMock(spec=LookupIriAction, lookup_term=None, parent_line_edit="http://example.com/iri1")], 1), + # success path with one existing action + ("", [], 1), # edge case with empty lookup term + ("http://example.com/iri3", [MagicMock()], 2), # edge case with non-LookupIriAction existing action + ], + ids=[ + "success_path_no_existing_actions", + "success_path_with_existing_action", + "edge_case_empty_lookup_term", + "edge_case_non_lookupiri_action_existing" + ] + ) + def test_set_iri_lookup_action(self, mocker, type_dialog, lookup_term, existing_actions, expected_action_count): + # Arrange + mocker.resetall() + mock_lookup_action = mocker.patch("pasta_eln.GUI.data_hierarchy.type_dialog.LookupIriAction") + mock_isinstance = mocker.patch("pasta_eln.GUI.data_hierarchy.type_dialog.isinstance", return_value=True) + + type_dialog.iriLineEdit.actions.return_value = existing_actions + + # Act + type_dialog.set_iri_lookup_action(lookup_term) + + # Assert + type_dialog.iriLineEdit.actions.assert_called_once() + if expected_action_count == 1: + mock_lookup_action.assert_called_once_with( + parent_line_edit=type_dialog.iriLineEdit, + lookup_term=lookup_term + ) + (type_dialog.iriLineEdit.addAction + .assert_called_once_with(mock_lookup_action.return_value, + QLineEdit.ActionPosition.TrailingPosition)) + for act in existing_actions: + mock_isinstance.assert_called_with(act, mock_lookup_action) + act.deleteLater.assert_called_once() + + @pytest.mark.parametrize( + "lookup_term, existing_actions", + [ + ("http://example.com/iri4", None), # error case with None as existing actions + ], + ids=[ + "error_case_none_existing_actions" + ] + ) + def test_set_iri_lookup_action_errors(self, type_dialog, lookup_term, existing_actions): + # Arrange + type_dialog.iriLineEdit.actions.return_value = existing_actions + + # Act and Assert + with pytest.raises(TypeError): + type_dialog.set_iri_lookup_action(lookup_term) + + def test_clear_ui(self, type_dialog): + + # Act + type_dialog.clear_ui() + + # Assert + type_dialog.typeLineEdit.clear.assert_called_once() + type_dialog.typeDisplayedTitleLineEdit.clear.assert_called_once() + type_dialog.iriLineEdit.clear.assert_called_once() + type_dialog.shortcutLineEdit.clear.assert_called_once() + type_dialog.iconFontCollectionComboBox.setCurrentIndex.assert_called_once_with(0) + type_dialog.iconComboBox.setCurrentIndex.assert_called_once_with(0) + + def test_show(self, type_dialog): + # Act + with patch.object(type_dialog.instance, 'show') as mock_show: + type_dialog.show() + + # Assert + mock_show.assert_called_once() + + def test_title_modified(self, type_dialog): + # Arrange + new_title = "New Title" + + # Act + with patch.object(type_dialog, 'set_iri_lookup_action') as mock_set_iri_lookup_action: + type_dialog.title_modified(new_title) + + # Assert + mock_set_iri_lookup_action.assert_called_once_with(new_title) + + @pytest.mark.parametrize("test_id, font_collection", [ + ("success_path_1", "font_collection_1"), + ("success_path_2", "font_collection_2"), + ("empty_font_collection", ""), + ("special_characters", "!@#$%^&*()"), + ("long_string", "a" * 1000), + ("none_font_collection", None), + ], ids=["success_path_1", "success_path_2", "empty_font_collection", "special_characters", "long_string", + "none_font_collection"]) + def test_icon_font_collection_changed(self, type_dialog, test_id, font_collection): + # Arrange + type_dialog.populate_icons = MagicMock() + # Act + type_dialog.icon_font_collection_changed(font_collection) + + # Assert + type_dialog.iconComboBox.clear.assert_called_once() + type_dialog.populate_icons.assert_called_once_with(font_collection) + + @pytest.mark.parametrize( + "font_collection, expected_calls, log_warning", + [ + ("font1", [("icon1",), ('icon2', 'icon2'), ('icon3', 'icon3')], False), + ("font2", [("iconA",), ('iconB', 'iconB')], False), + ("", [], True), + (None, [], True), + ("nonexistent_font", [], True), + ], + ids=[ + "valid_font1", + "valid_font2", + "empty_string", + "none_value", + "nonexistent_font" + ] + ) + def test_populate_icons(self, mocker, type_dialog, font_collection, expected_calls, log_warning): + # Arrange + mocker.resetall() + mock_qta = mocker.patch("pasta_eln.GUI.data_hierarchy.type_dialog.qta") + type_dialog.qta_icons.icon_names = {"font1": ["icon1", "icon2", "icon3"], "font2": ["iconA", "iconB"]} + expected_calls_modified = [] + for item in expected_calls: + if len(item) == 2: + expected_calls_modified.append(mocker.call(mock_qta.icon(item[0]), item[1])) + else: + expected_calls_modified.append(mocker.call(item[0])) + + # Act + type_dialog.populate_icons(font_collection) + + # Assert + if log_warning: + type_dialog.logger.warning.assert_called_once_with('Invalid font collection!') + else: + type_dialog.logger.warning.assert_not_called() + + assert type_dialog.iconComboBox.addItem.call_count == len(expected_calls) + for call, expected_call in zip(type_dialog.iconComboBox.addItem.call_args_list, expected_calls_modified): + assert call == expected_call + + @pytest.mark.parametrize( + "datatype, expected", + [ + ("text", "text"), # happy path with a common string + ("123", "123"), # happy path with numeric string + ("", ""), # edge case with empty string + ("a" * 1000, "a" * 1000), # edge case with very long string + ], + ids=[ + "common_string", + "numeric_string", + "empty_string", + "very_long_string" + ] + ) + def test_set_data_type(self, type_dialog, datatype, expected): + # Act + if datatype is None: + with pytest.raises(TypeError): + type_dialog.set_data_type(datatype) + else: + type_dialog.set_data_type(datatype) + + # Assert + if datatype is not None: + assert type_dialog.type_info.datatype == expected + + @pytest.mark.parametrize( + "title, expected_title", + [ + ("Sample Title", "Sample Title"), # happy path + ("", ""), # edge case: empty string + ("A" * 1000, "A" * 1000), # edge case: very long string + ("1234567890", "1234567890"), # edge case: numeric string + ("!@#$%^&*()", "!@#$%^&*()"), # edge case: special characters + ], + ids=[ + "happy_path_sample_title", + "edge_case_empty_string", + "edge_case_very_long_string", + "edge_case_numeric_string", + "edge_case_special_characters", + ] + ) + def test_set_type_title(self, type_dialog, title, expected_title): + # Arrange + + # Act + type_dialog.set_type_title(title) + + # Assert + assert type_dialog.type_info.title == expected_title + + @pytest.mark.parametrize( + "iri, expected_iri", + [ + param("http://example.com/type1", "http://example.com/type1", id="success_path_1"), + param("http://example.com/type2", "http://example.com/type2", id="success_path_2"), + param("", "", id="edge_case_empty_string"), + param("http://example.com/very/long/iri/with/many/segments", + "http://example.com/very/long/iri/with/many/segments", id="edge_case_long_iri"), + param(None, None, id="error_case_none"), + ], + ids=lambda x: x[-1] + ) + def test_set_type_iri(self, type_dialog, iri, expected_iri): + # Arrange + + # Act + type_dialog.set_type_iri(iri) + + # Assert + if iri is not None: + assert type_dialog.type_info.iri == expected_iri + + @pytest.mark.parametrize( + "shortcut, expected", + [ + ("ctrl+c", "ctrl+c"), # happy path with common shortcut + ("ctrl+v", "ctrl+v"), # happy path with another common shortcut + ("", ""), # edge case with empty string + ("a" * 1000, "a" * 1000), # edge case with very long string + ("ctrl+shift+alt+del", "ctrl+shift+alt+del"), # edge case with complex shortcut + ], + ids=[ + "common-shortcut-copy", + "common-shortcut-paste", + "empty-string", + "very-long-string", + "complex-shortcut" + ] + ) + def test_set_type_shortcut(self, type_dialog, shortcut, expected): + # Arrange + + # Act + type_dialog.set_type_shortcut(shortcut) + + # Assert + assert type_dialog.type_info.shortcut == expected + + @pytest.mark.parametrize( + "icon, expected_icon", + [ + ("icon1.png", "icon1.png"), # happy path with a typical icon name + ("", ""), # edge case with an empty string + ("a" * 255, "a" * 255), # edge case with a very long string + ("icon_with_special_chars_!@#.png", "icon_with_special_chars_!@#.png"), # edge case with special characters + ], + ids=[ + "typical_icon_name", + "empty_string", + "very_long_string", + "special_characters" + ] + ) + def test_set_type_icon(self, type_dialog, icon, expected_icon): + # Arrange + + # Act + type_dialog.set_type_icon(icon) + + # Assert + assert type_dialog.type_info.icon == expected_icon diff --git a/tests/unit_tests/test_data_hierarchy_utility_functions.py b/tests/unit_tests/test_data_hierarchy_utility_functions.py index 4d3d3543..f2469319 100644 --- a/tests/unit_tests/test_data_hierarchy_utility_functions.py +++ b/tests/unit_tests/test_data_hierarchy_utility_functions.py @@ -8,6 +8,7 @@ # You should have received a copy of the license with this file. Please refer the license file for more information. import logging +from typing import Any import pytest from PySide6.QtCore import QEvent, Qt @@ -16,12 +17,23 @@ from cloudant import CouchDB from pasta_eln.GUI.data_hierarchy.utility_functions import adjust_data_hierarchy_data_to_v4, can_delete_type, \ - check_data_hierarchy_types, get_db, get_missing_metadata_message, get_next_possible_structural_level_title, \ + check_data_hierarchy_types, get_db, get_missing_metadata_message, \ is_click_within_bounds, set_types_missing_required_metadata, set_types_with_duplicate_metadata, \ set_types_without_name_in_metadata, show_message from tests.common.test_utils import are_json_equal +def get_db_with_right_arguments_returns_valid_db_instance(db_user: str, + db_instances: dict, + mock_client: Any, + dbs_call_count: int): + assert (get_db(db_user, "test", "test", "test", None) + is db_instances[db_user]), "get_db should return valid db instance" + assert mock_client.all_dbs.call_count == dbs_call_count, "get_db should call all_dbs" + assert (mock_client.__getitem__.call_count == dbs_call_count + ), "get_db should call __getitem__" + + class TestDataHierarchyUtilityFunctions(object): def test_is_click_within_bounds_when_null_arguments_returns_false(self, mocker): @@ -112,19 +124,6 @@ def create_mock_doc(contents, mocker): mock_doc.__setitem__.side_effect = contents.__setitem__ return mock_doc - def test_get_next_possible_structural_level_title_when_null_arg_returns_none(self): - assert get_next_possible_structural_level_title( - None) is None, "get_next_possible_structural_level_title should return True" - - @pytest.mark.parametrize("existing_list, expected_next_level", - [(None, None), ([], "x0"), (["x0", "x2"], "x3"), (["x0", "xa", "x3", "x-1", "x10"], "x11"), - (["x0", "xa", "x3", "x-1", "a10", "X23"], "x24"), (["a"], "x0")]) - def test_get_next_possible_structural_level_displayed_title_when_valid_list_arg_returns_right_result(self, mocker, - existing_list, - expected_next_level): - assert get_next_possible_structural_level_title( - existing_list) == expected_next_level, "get_next_possible_structural_level_displayed_title should return as expected" - def test_get_db_with_right_arguments_returns_valid_db_instance(self, mocker): mock_client = mocker.MagicMock(spec=CouchDB) mock_client.all_dbs.return_value = ["db_name1", "db_name2"] @@ -135,16 +134,10 @@ def test_get_db_with_right_arguments_returns_valid_db_instance(self, mocker): mocker.patch.object(CouchDB, "__new__", lambda s, user, auth_token, url, connect: mock_client) mocker.patch.object(CouchDB, "__init__", lambda s, user, auth_token, url, connect: None) - assert get_db("db_name1", "test", "test", "test", None) is db_instances[ - "db_name1"], "get_db should return valid db instance" - assert mock_client.all_dbs.call_count == 1, "get_db should call all_dbs" - assert mock_client.__getitem__.call_count == 1, "get_db should call __getitem__" - - assert get_db("db_name2", "test", "test", "test", None) is db_instances[ - "db_name2"], "get_db should return valid db instance" - assert mock_client.all_dbs.call_count == 2, "get_db should call all_dbs" - assert mock_client.__getitem__.call_count == 2, "get_db should call __getitem__" - + get_db_with_right_arguments_returns_valid_db_instance( + "db_name1", db_instances, mock_client, 1) + get_db_with_right_arguments_returns_valid_db_instance( + "db_name2", db_instances, mock_client, 2) assert get_db("db_name3", "test", "test", "test", None) is created_db_instance, "get_db should return created db instance" assert mock_client.all_dbs.call_count == 3, "get_db should call all_dbs" @@ -621,19 +614,41 @@ def test_get_formatted_missing_or_duplicate_metadata_message_returns_expected_me assert (get_missing_metadata_message(missing_metadata, missing_names, duplicate_metadata) == expected_message), "get_missing_metadata_message should return expected" - @pytest.mark.parametrize("existing_types, selected_type, expected_result", - [(["x0", "x1", "x3", "samples", "instruments"], "test", True), - (["x0", "x1", "x3", "x4", "instruments"], "x5", False), - (["x0", "x1", "x3", "x4", "instruments"], "x0", False), - (["x0", "x1", "x3", "x4", "instruments"], "x4", True), - (["x0", "x1", "x3", "x4", "instruments"], "x3", False), - (["x0", "x1", "x3", "x4", "instruments"], "x0", False), - (["x0", "x1", "x3", "x4", "instruments"], "x1", False), - (["x0", "x1", "x3", "x4", "instruments"], "instruments", True), - (["x2", "x3", "x5", "x0", "instruments"], "x5", True), - (["x2", "x3", "x5", "x0", "x7", "", "samples"], "x7", True), - (["x2", "x3", "x5", "x0", "x7", "", "samples"], "x8", False), - (["x2", "x3", "x5", "x0", "x7", "", None], "x8", False), - (["x2", "x3", "x5", "x0", "x7", "", None], None, False)]) - def test_can_delete_type_returns_expected(self, existing_types, selected_type, expected_result): - assert can_delete_type(existing_types, selected_type) == expected_result, "can_delete_type should return expected" + @pytest.mark.parametrize( + "selected_type, expected_result", + [ + # Success path tests + ("non_structural", True), # non-structural type + ("x1", False), # structural type but not 'x0' + ("x0", False), # structural type 'x0' + + # Edge cases + ("", False), # empty string + (None, False), # None as input + + # Error cases + ("x0", False), # structural type 'x0' + ("x1", False), # structural type 'x1' + ("non_structural", True), # non-structural type + ], + ids=[ + "non_structural_type", + "structural_type_x1", + "structural_type_x0", + "empty_string", + "none_input", + "structural_type_x0_error", + "structural_type_x1_error", + "non_structural_type_error", + ] + ) + def test_can_delete_type(self, mocker, selected_type, expected_result, monkeypatch): + # Arrange + mocker.patch("pasta_eln.GUI.data_hierarchy.utility_functions.is_structural_level", + side_effect=lambda title: title in {"x0", "x1"}) + + # Act + result = can_delete_type(selected_type) + + # Assert + assert result == expected_result