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