From 13e6a49d10fa8d09683e86774835a895b31d3a53 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Wed, 17 Dec 2025 11:11:35 -0300 Subject: [PATCH 1/7] Initial mockup of Dialog. --- src/ode/dialogs/ckan.py | 361 ++++++++++++++++++++++++++++++++++++ src/ode/dialogs/download.py | 11 ++ 2 files changed, 372 insertions(+) create mode 100644 src/ode/dialogs/ckan.py diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py new file mode 100644 index 000000000..f613286f8 --- /dev/null +++ b/src/ode/dialogs/ckan.py @@ -0,0 +1,361 @@ +import json +from typing import Dict, Any, Optional + +import requests +from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QFormLayout, QScrollArea, + QLabel, QMessageBox, QGroupBox, QTextEdit, + QCheckBox, QSpinBox, QComboBox, QDialog) +from PySide6.QtCore import Signal + + +class CKANResourceCreator(QDialog): + """Widget to create resources in CKAN instances.""" + + resource_created = Signal(dict) + + def __init__(self, parent=None): + super().__init__(parent) + self.api_token = None + self.base_url = None + self.schema = None + self.init_ui() + + def init_ui(self): + """Initialize the user interface.""" + self.setWindowTitle("CKAN Resource Creator") + self.setMinimumSize(800, 600) + + # Main layout + main_layout = QVBoxLayout() + + # Connection section + connection_group = QGroupBox("CKAN Connection") + connection_layout = QFormLayout() + + self.url_input = QLineEdit() + self.url_input.setPlaceholderText("https://demo.ckan.org") + connection_layout.addRow("CKAN URL:", self.url_input) + + self.api_token_input = QLineEdit() + self.api_token_input.setPlaceholderText("Enter your API token") + self.api_token_input.setEchoMode(QLineEdit.EchoMode.Password) + connection_layout.addRow("API Token:", self.api_token_input) + + connection_group.setLayout(connection_layout) + main_layout.addWidget(connection_group) + + # Dataset ID section + dataset_group = QGroupBox("Dataset Information") + dataset_layout = QFormLayout() + + self.dataset_id_input = QLineEdit() + self.dataset_id_input.setPlaceholderText("Enter dataset ID where resource will be added") + self.dataset_id_input.setText("testing") + dataset_layout.addRow("Dataset ID:", self.dataset_id_input) + + dataset_group.setLayout(dataset_layout) + main_layout.addWidget(dataset_group) + + # Buttons for connection + button_layout = QHBoxLayout() + + self.connect_button = QPushButton("Connect and Load Schema") + self.connect_button.clicked.connect(self.connect_and_load_schema) + button_layout.addWidget(self.connect_button) + + self.clear_button = QPushButton("Clear Form") + self.clear_button.clicked.connect(self.clear_form) + button_layout.addWidget(self.clear_button) + + main_layout.addLayout(button_layout) + + # Scroll area for dynamic form + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_content = QWidget() + self.form_layout = QFormLayout() + self.scroll_content.setLayout(self.form_layout) + self.scroll_area.setWidget(self.scroll_content) + + main_layout.addWidget(QLabel("Resource Fields:")) + main_layout.addWidget(self.scroll_area) + + # Create resource button + self.create_button = QPushButton("Create Resource") + self.create_button.clicked.connect(self.create_resource) + self.create_button.setEnabled(False) + main_layout.addWidget(self.create_button) + + # Status label + self.status_label = QLabel("Not connected") + self.status_label.setStyleSheet("color: gray;") + main_layout.addWidget(self.status_label) + + self.setLayout(main_layout) + + def clear_form(self): + """Clear all form fields.""" + # Clear dynamic form + for i in reversed(range(self.form_layout.count())): + widget = self.form_layout.itemAt(i).widget() + if widget: + widget.deleteLater() + + self.create_button.setEnabled(False) + self.status_label.setText("Not connected") + self.status_label.setStyleSheet("color: gray;") + + def connect_and_load_schema(self): + """Connect to CKAN instance and load the resource schema.""" + self.base_url = self.url_input.text().strip() + self.api_token = self.api_token_input.text().strip() + dataset_id = self.dataset_id_input.text().strip() + + if not self.base_url: + QMessageBox.warning(self, "Warning", "Please enter a CKAN URL") + return + + if not dataset_id: + QMessageBox.warning(self, "Warning", "Please enter a Dataset ID") + return + + if not self.base_url.endswith('/'): + self.base_url += '/' + + try: + self.schema = self.get_resource_schema() + + self.clear_form() + + self.build_dynamic_form() + + self.create_button.setEnabled(True) + self.status_label.setText(f"Connected to {self.base_url}") + self.status_label.setStyleSheet("color: green;") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to connect: {str(e)}") + self.status_label.setText("Connection failed") + self.status_label.setStyleSheet("color: red;") + + def get_resource_schema(self) -> list: + """ + Get the resource schema from CKAN. + + CKAN doesn't have a direct endpoint for resource schema, but we can: + 1. Try the scheming extension's schema endpoint if available + 2. Fall back to a default CKAN schema + """ + # Try to get schema from scheming extension (common in many CKAN instances) + scheming_url = f"{self.base_url}api/action/scheming_dataset_schema_show?type=dataset" + + headers = { + "Authorization": self.api_token if self.api_token else None, + "Content-Type": "application/json" + } + + try: + # First try scheming endpoint + response = requests.get(scheming_url, headers=headers, timeout=10) + if response.status_code == 200: + data = response.json() + if data.get("success"): + result = data.get("result", {}).get("resource_fields", []) + return result + else: + print(f"Failed fetching scheming endpoint: {response.content}") + except: + pass + + return self.get_default_schema() + + def get_default_schema(self) -> list: + """Return a default schema based on CKAN's standard resource fields. + + We are ignoring CKAN's url field as we are only allowing upload of ODE existing files. + """ + return [ + { + "field_name": "name", + "label": "Name", + "preset": "text", + "required": True, + "help_text": "Name of the resource" + }, + { + "field_name": "description", + "label": "Description", + "preset": "markdown", + "required": False, + "form_placeholder": "Description of the resource" + }, + { + "field_name": "format", + "label": "Format", + "preset": "select", + "choices": [ + {"value": "CSV", "label": "CSV"}, + {"value": "JSON", "label": "JSON"}, + {"value": "XML", "label": "XML"}, + {"value": "PDF", "label": "PDF"}, + {"value": "XLS", "label": "Excel"}, + ], + "required": False + }, + { + "field_name": "hash", + "label": "File Hash", + "preset": "text", + "required": False, + "help_text": "MD5 or SHA256 hash of the file" + } + ] + + def build_dynamic_form(self): + """Build form fields dynamically based on the schema.""" + if not self.schema: + return + + for field in self.schema: + field_name = field.get("field_name") + label = field.get("label", field_name) + preset = field.get("preset", "text") + required = field.get("required", False) + # help_text = field.get("help_text", "") + placeholder = field.get("form_placeholder", "") + + # Create label with required indicator + field_label = QLabel(f"{field_name}{' *' if required else ''}") + field_label.setToolTip(str(label)) # Label could be a multilingual dict so we cast it to str. + # if help_text: + # TODO: Necessary? Help text can be multilingual + # field_label.setToolTip(help_text) + + # Create appropriate widget based on preset + if preset == "text": + widget = QLineEdit() + if placeholder: + widget.setPlaceholderText(placeholder) + + elif preset == "markdown": + widget = QTextEdit() + widget.setMaximumHeight(100) + + elif preset == "number": + widget = QSpinBox() + widget.setRange(0, 1000000000) + + elif preset == "select": + widget = QComboBox() + choices = field.get("choices", []) + for choice in choices: + widget.addItem(choice.get("label", ""), choice.get("value", "")) + + elif preset == "boolean": + widget = QCheckBox() + + else: + # Default to text input + widget = QLineEdit() + if placeholder: + widget.setPlaceholderText(placeholder) + + # Store field metadata in widget + widget.setProperty("field_name", field_name) + widget.setProperty("required", required) + widget.setProperty("preset", preset) + + # Add to form + self.form_layout.addRow(field_label, widget) + + # Add a note about required fields + note_label = QLabel("* indicates required field") + note_label.setStyleSheet("color: gray; font-style: italic;") + self.form_layout.addRow(note_label) + + def create_resource(self): + """Create the resource using the CKAN API.""" + if not self.base_url or not self.api_token: + QMessageBox.warning(self, "Warning", "Please connect first") + return + + dataset_id = self.dataset_id_input.text().strip() + if not dataset_id: + QMessageBox.warning(self, "Warning", "Please enter a Dataset ID") + return + + # Collect form data + resource_data = {"package_id": dataset_id} + + for i in range(self.form_layout.rowCount() - 1): # -1 for the note label + layout_item = self.form_layout.itemAt(i, QFormLayout.ItemRole.FieldRole) + if layout_item: + widget = layout_item.widget() + if widget: + field_name = widget.property("field_name") + preset = widget.property("preset") + required = widget.property("required") + + if field_name: + if preset == "text" and isinstance(widget, QLineEdit): + value = widget.text().strip() + if required and not value: + QMessageBox.warning(self, "Warning", + f"Field '{field_name}' is required") + return + resource_data[field_name] = value + + elif preset == "markdown" and isinstance(widget, QTextEdit): + value = widget.toPlainText().strip() + if required and not value: + QMessageBox.warning(self, "Warning", + f"Field '{field_name}' is required") + return + resource_data[field_name] = value + + elif preset == "number" and isinstance(widget, QSpinBox): + resource_data[field_name] = widget.value() + + elif preset == "select" and isinstance(widget, QComboBox): + value = widget.currentData() + if not value: + value = widget.currentText() + resource_data[field_name] = value + + elif preset == "boolean" and isinstance(widget, QCheckBox): + resource_data[field_name] = widget.isChecked() + + elif preset == "resource_url_upload": + # TODO: Handle file upload + pass + + # Send to CKAN + try: + url = f"{self.base_url}api/action/resource_create" + + headers = { + "Authorization": self.api_token, + "Content-Type": "application/json" + } + + response = requests.post(url, headers=headers, + data=json.dumps(resource_data), timeout=30) + + result = response.json() + + if response.status_code == 200 and result.get("success"): + QMessageBox.information(self, "Success", "Resource created successfully!") + self.resource_created.emit(result.get("result", {})) + self.clear_form() + else: + error_msg = result.get("error", {}).get("message", "Unknown error") + QMessageBox.critical(self, "Error", + f"Failed to create resource: {error_msg}") + + except requests.exceptions.RequestException as e: + QMessageBox.critical(self, "Error", f"Network error: {str(e)}") + except json.JSONDecodeError: + QMessageBox.critical(self, "Error", "Invalid response from server") + except Exception as e: + QMessageBox.critical(self, "Error", f"Unexpected error: {str(e)}") diff --git a/src/ode/dialogs/download.py b/src/ode/dialogs/download.py index 86b681890..4b4810c63 100644 --- a/src/ode/dialogs/download.py +++ b/src/ode/dialogs/download.py @@ -5,6 +5,8 @@ from PySide6.QtCore import Qt, Signal, QStandardPaths from pathlib import Path +from ode.dialogs.ckan import CKANResourceCreator + class DownloadDialog(QDialog): """Dialog to export the file and the errors.""" @@ -38,6 +40,10 @@ def __init__(self, parent, filepath: Path, has_errors:bool) -> None: self.download_error_button.setDisabled(True) button_layout.addWidget(self.download_error_button) + self.ckan_exporter_button = QPushButton() + self.ckan_exporter_button.clicked.connect(self.ckan_exporter) + button_layout.addWidget(self.ckan_exporter_button) + layout.addLayout(button_layout) self.setLayout(layout) @@ -49,6 +55,11 @@ def retranslateUI(self) -> None: self.label.setText(self.tr("Please, select one of the following options:")) self.download_button.setText(self.tr("Download file")) self.download_error_button.setText(self.tr("Download file with errors")) + self.ckan_exporter_button.setText(self.tr("Export to CKAN Resource.")) + + def ckan_exporter(self) -> None: + dialog = CKANResourceCreator() + dialog.exec() def download_file(self): """ From 17f4b673150487880c6e991cae97698b62e260bb Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Wed, 17 Dec 2025 11:34:52 -0300 Subject: [PATCH 2/7] Fix styling and name --- src/ode/assets/style.qss | 25 +++++++++++++++---------- src/ode/dialogs/ckan.py | 6 ++---- src/ode/dialogs/download.py | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ode/assets/style.qss b/src/ode/assets/style.qss index eeb752050..3eff87c0c 100644 --- a/src/ode/assets/style.qss +++ b/src/ode/assets/style.qss @@ -6,12 +6,12 @@ so we are enforcing it by explicitly setting the main widgets and components bac */ /* Enforcing Light Mode of main elements. */ -Content, FrictionlessResourceMetadataWidget, SelectWidget, Toolbar, -FieldsForm, SingleFieldForm, SchemaForm, QWidget#fields_form_container, +Content, FrictionlessResourceMetadataWidget, SelectWidget, Toolbar, QFormLayout, +FieldsForm, SingleFieldForm, SchemaForm, QTableView, QTableView QHeaderView, QTableView QTableCornerButton, QTreeView, QDialog, QPushButton, QComboBox, QComboBox QAbstractItemView, QLineEdit, QLabel, QTabWidget, QTabBar, QScrollBar, QSpinBox, QListWidget, QTextEdit, QSrollArea, -QPlainTextEdit, QWidget#central_widget, QGroupBox { +QPlainTextEdit, QWidget#central_widget, QGroupBox, CKANExportDialog, QWidget { background: #FFF; color: #404040; font-size: 19px; /* multi-os support */ @@ -195,7 +195,8 @@ DownloadDialog, LlamaDialog, LLMWarningDialog, ColumnMetadataDialog, -LlamaDownloadingDialog { +LlamaDownloadingDialog, +CKANExportDialog { border: 2px solid #000000; } @@ -206,7 +207,8 @@ DownloadDialog QPushButton, LLMWarningDialog QPushButton, ColumnMetadataDialog QPushButton, LlamaDialog QPushButton, -LlamaDownloadDialog QPushButton { +LlamaDownloadDialog QPushButton, +CKANExportDialog QPushButton { border: 2px solid #000000; background-color: #F0F0F0; font-size: 16px; @@ -237,7 +239,8 @@ DownloadDialog QPushButton:hover, ColumnMetadataDialog QPushButton:hover, LLMWarningDialog QPushButton:hover, LlamaDialog QPushButton:hover, -LlamaDownloadDialog QPushButton:hover { +LlamaDownloadDialog QPushButton:hover, +CKANExportDialog QPushButton:hover { background: #0288D1; border-color: #0288d1; } @@ -249,7 +252,8 @@ DownloadDialog QPushButton:pressed, ColumnMetadataDialog QPushButton:pressed, LLMWarningDialog QPushButton:pressed, LlamaDialog QPushButton:pressed, -LlamaDownloadDialog QPushButton:pressed { +LlamaDownloadDialog QPushButton:pressed, +CKANExportDialog QPushButton:pressed { color: #FFFFFF; background: #000000; border-color: #000000; @@ -262,7 +266,8 @@ DownloadDialog QPushButton:disabled, ColumnMetadataDialog QPushButton:disabled, LLMWarningDialog QPushButton:disabled, LlamaDialog QPushButton:disabled, -LlamaDownloadDialog QPushButton:disabled { +LlamaDownloadDialog QPushButton:disabled, +CKANExportDialog QPushButton:disabled { background-color: #F0F0F0; border-color: #F0F0F0; color: #4C5564; @@ -353,7 +358,7 @@ QTableView::item:focus { LLMWarningDialog QCheckBox { color: #404040; - font-size: 18px; + font-size: 18px; background-color: white; } @@ -391,4 +396,4 @@ QWidget#llamaDownloadModelRow { border-radius: 4px; background-color: #f9f9f9; margin: 2px; -} \ No newline at end of file +} diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index f613286f8..4035227db 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -9,7 +9,7 @@ from PySide6.QtCore import Signal -class CKANResourceCreator(QDialog): +class CKANExportDialog(QDialog): """Widget to create resources in CKAN instances.""" resource_created = Signal(dict) @@ -73,10 +73,8 @@ def init_ui(self): # Scroll area for dynamic form self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) - self.scroll_content = QWidget() self.form_layout = QFormLayout() - self.scroll_content.setLayout(self.form_layout) - self.scroll_area.setWidget(self.scroll_content) + self.scroll_area.setLayout(self.form_layout) main_layout.addWidget(QLabel("Resource Fields:")) main_layout.addWidget(self.scroll_area) diff --git a/src/ode/dialogs/download.py b/src/ode/dialogs/download.py index 4b4810c63..9ce03793a 100644 --- a/src/ode/dialogs/download.py +++ b/src/ode/dialogs/download.py @@ -5,7 +5,7 @@ from PySide6.QtCore import Qt, Signal, QStandardPaths from pathlib import Path -from ode.dialogs.ckan import CKANResourceCreator +from ode.dialogs.ckan import CKANExportDialog class DownloadDialog(QDialog): @@ -58,7 +58,7 @@ def retranslateUI(self) -> None: self.ckan_exporter_button.setText(self.tr("Export to CKAN Resource.")) def ckan_exporter(self) -> None: - dialog = CKANResourceCreator() + dialog = CKANExportDialog(self) dialog.exec() def download_file(self): From b2ca31200cc2cf3745a3cf5562be74d7a1824956 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Wed, 17 Dec 2025 12:46:03 -0300 Subject: [PATCH 3/7] Add support for file upload. --- src/ode/dialogs/ckan.py | 42 ++++++++++++++++++++++++------------- src/ode/dialogs/download.py | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index 4035227db..d40744f3b 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -14,11 +14,12 @@ class CKANExportDialog(QDialog): resource_created = Signal(dict) - def __init__(self, parent=None): + def __init__(self, parent=None, filepath=None): super().__init__(parent) self.api_token = None self.base_url = None self.schema = None + self.filepath = filepath self.init_ui() def init_ui(self): @@ -51,7 +52,6 @@ def init_ui(self): self.dataset_id_input = QLineEdit() self.dataset_id_input.setPlaceholderText("Enter dataset ID where resource will be added") - self.dataset_id_input.setText("testing") dataset_layout.addRow("Dataset ID:", self.dataset_id_input) dataset_group.setLayout(dataset_layout) @@ -223,6 +223,11 @@ def build_dynamic_form(self): # help_text = field.get("help_text", "") placeholder = field.get("form_placeholder", "") + if field_name == "url": + # Ignore URL as we only support file uploads. + # The selected file will be appended to the POST request in create_resource + continue + # Create label with required indicator field_label = QLabel(f"{field_name}{' *' if required else ''}") field_label.setToolTip(str(label)) # Label could be a multilingual dict so we cast it to str. @@ -324,21 +329,31 @@ def create_resource(self): elif preset == "boolean" and isinstance(widget, QCheckBox): resource_data[field_name] = widget.isChecked() - elif preset == "resource_url_upload": - # TODO: Handle file upload - pass - # Send to CKAN try: url = f"{self.base_url}api/action/resource_create" - headers = { - "Authorization": self.api_token, - "Content-Type": "application/json" + headers = {"Authorization": self.api_token} + + payload = { + "package_id": dataset_id, + "url": "", # CKAN expects this field, but can be empty for uploads + "url_type": "upload", } - response = requests.post(url, headers=headers, - data=json.dumps(resource_data), timeout=30) + # Add form fields to payload + for key, value in resource_data.items(): + if key not in ["package_id", "url", "url_type"]: # Already added + payload[key] = value + + if not self.filepath: + QMessageBox.warning(self, "Warning", f"Couldn't get filepath. Aborting.") + return + + # Open the file and send + with open(str(self.filepath), 'rb') as file_obj: + files = [('upload', (self.filepath.name, file_obj))] + response = requests.post(url, headers=headers, data=payload, files=files) result = response.json() @@ -347,9 +362,8 @@ def create_resource(self): self.resource_created.emit(result.get("result", {})) self.clear_form() else: - error_msg = result.get("error", {}).get("message", "Unknown error") - QMessageBox.critical(self, "Error", - f"Failed to create resource: {error_msg}") + error_msg = result.get("error", "Unknown error.") + QMessageBox.critical(self, "Error", f"Failed to create resource: {error_msg}") except requests.exceptions.RequestException as e: QMessageBox.critical(self, "Error", f"Network error: {str(e)}") diff --git a/src/ode/dialogs/download.py b/src/ode/dialogs/download.py index 9ce03793a..5d1f88cc8 100644 --- a/src/ode/dialogs/download.py +++ b/src/ode/dialogs/download.py @@ -58,7 +58,7 @@ def retranslateUI(self) -> None: self.ckan_exporter_button.setText(self.tr("Export to CKAN Resource.")) def ckan_exporter(self) -> None: - dialog = CKANExportDialog(self) + dialog = CKANExportDialog(parent=self, filepath=self.filepath) dialog.exec() def download_file(self): From fcf974b48cd3a25aeffabe34ab1f27efce1aeb0e Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Fri, 19 Dec 2025 09:37:08 -0300 Subject: [PATCH 4/7] Fix imports --- src/ode/dialogs/ckan.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index d40744f3b..9b18e69bb 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -1,12 +1,23 @@ import json -from typing import Dict, Any, Optional import requests -from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QFormLayout, QScrollArea, - QLabel, QMessageBox, QGroupBox, QTextEdit, - QCheckBox, QSpinBox, QComboBox, QDialog) from PySide6.QtCore import Signal +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QDialog, + QFormLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QMessageBox, + QPushButton, + QScrollArea, + QSpinBox, + QTextEdit, + QVBoxLayout, +) class CKANExportDialog(QDialog): @@ -347,7 +358,7 @@ def create_resource(self): payload[key] = value if not self.filepath: - QMessageBox.warning(self, "Warning", f"Couldn't get filepath. Aborting.") + QMessageBox.warning(self, "Warning", "Couldn't get filepath. Aborting.") return # Open the file and send From 13ff029a34f29588278408f492d2a5e1b098d7a4 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Fri, 19 Dec 2025 10:40:37 -0300 Subject: [PATCH 5/7] Small style improvement. --- src/ode/assets/style.qss | 4 ++++ src/ode/dialogs/ckan.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ode/assets/style.qss b/src/ode/assets/style.qss index 3eff87c0c..3041cb264 100644 --- a/src/ode/assets/style.qss +++ b/src/ode/assets/style.qss @@ -24,6 +24,10 @@ QPlainTextEdit, QWidget#central_widget, QGroupBox, CKANExportDialog, QWidget { background: #f0f0f0; } +QGroupBox { + /* This only affects the title as is the only text in QGroupBox. */ + font: bold; +} ErrorsReportButton { border: 0; diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index 9b18e69bb..230998adb 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -87,7 +87,9 @@ def init_ui(self): self.form_layout = QFormLayout() self.scroll_area.setLayout(self.form_layout) - main_layout.addWidget(QLabel("Resource Fields:")) + self.resource_label = QLabel("Resource Fields") + self.resource_label.setStyleSheet("font-weight: bold;") + main_layout.addWidget(self.resource_label) main_layout.addWidget(self.scroll_area) # Create resource button From f628cdc7159071010a8c08b94fc517a188c11b39 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Fri, 19 Dec 2025 10:49:41 -0300 Subject: [PATCH 6/7] Change order sections CKAN exporter. --- src/ode/dialogs/ckan.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index 230998adb..bbc33371c 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -57,17 +57,6 @@ def init_ui(self): connection_group.setLayout(connection_layout) main_layout.addWidget(connection_group) - # Dataset ID section - dataset_group = QGroupBox("Dataset Information") - dataset_layout = QFormLayout() - - self.dataset_id_input = QLineEdit() - self.dataset_id_input.setPlaceholderText("Enter dataset ID where resource will be added") - dataset_layout.addRow("Dataset ID:", self.dataset_id_input) - - dataset_group.setLayout(dataset_layout) - main_layout.addWidget(dataset_group) - # Buttons for connection button_layout = QHBoxLayout() @@ -81,6 +70,17 @@ def init_ui(self): main_layout.addLayout(button_layout) + # Dataset ID section + dataset_group = QGroupBox("Dataset Information") + dataset_layout = QFormLayout() + + self.dataset_id_input = QLineEdit() + self.dataset_id_input.setPlaceholderText("Enter dataset ID where resource will be added") + dataset_layout.addRow("Dataset ID:", self.dataset_id_input) + + dataset_group.setLayout(dataset_layout) + main_layout.addWidget(dataset_group) + # Scroll area for dynamic form self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) From 83189de03414fc60343ca2fdee50b5dd57074004 Mon Sep 17 00:00:00 2001 From: Patricio Del Boca Date: Fri, 19 Dec 2025 11:29:42 -0300 Subject: [PATCH 7/7] Check if dataset exists. --- src/ode/dialogs/ckan.py | 52 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/ode/dialogs/ckan.py b/src/ode/dialogs/ckan.py index bbc33371c..be117ea8a 100644 --- a/src/ode/dialogs/ckan.py +++ b/src/ode/dialogs/ckan.py @@ -76,7 +76,11 @@ def init_ui(self): self.dataset_id_input = QLineEdit() self.dataset_id_input.setPlaceholderText("Enter dataset ID where resource will be added") + self.dataset_id_input.editingFinished.connect(self.check_dataset_exists) dataset_layout.addRow("Dataset ID:", self.dataset_id_input) + self.dataset_help_text = QLabel("No info on dataset.") + self.dataset_help_text.setStyleSheet("font-style: italic;") + dataset_layout.addRow(self.dataset_help_text) dataset_group.setLayout(dataset_layout) main_layout.addWidget(dataset_group) @@ -105,6 +109,47 @@ def init_ui(self): self.setLayout(main_layout) + def _get_base_url(self) -> str: + base_url = self.url_input.text().strip() + if not base_url.endswith('/'): + base_url += '/' + return base_url + + def check_dataset_exists(self) -> None: + self.base_url = self._get_base_url() + self.api_token = self.api_token_input.text().strip() + dataset_id = self.dataset_id_input.text().strip() + + if not self.base_url or not self.api_token: + print("doing nothing...") + return + + package_show_url = f"{self.base_url}api/action/package_show?id={dataset_id}" + + headers = { + "Authorization": self.api_token if self.api_token else None, + "Content-Type": "application/json" + } + + try: + response = requests.get(package_show_url, headers=headers, timeout=10) + if response.status_code == 200: + self.dataset_help_text.setText("Dataset found.") + self.dataset_help_text.setStyleSheet("color: green; font-style: italic;") + elif response.status_code == 403: + self.dataset_help_text.setText("API Token does not have access to the dataset.") + self.dataset_help_text.setStyleSheet("color: red; font-style: italic;") + elif response.status_code == 404: + self.dataset_help_text.setText("Dataset not found.") + self.dataset_help_text.setStyleSheet("color: red; font-style: italic;") + else: + self.dataset_help_text.setText("The CKAN instance returned an error.") + self.dataset_help_text.setStyleSheet("color: red; font-style: italic;") + except: + self.dataset_help_text.setText("Error when searching for dataset. Check connection info.") + self.dataset_help_text.setStyleSheet("color: red; font-style: italic;") + + def clear_form(self): """Clear all form fields.""" # Clear dynamic form @@ -119,7 +164,7 @@ def clear_form(self): def connect_and_load_schema(self): """Connect to CKAN instance and load the resource schema.""" - self.base_url = self.url_input.text().strip() + self.base_url = self._get_base_url() self.api_token = self.api_token_input.text().strip() dataset_id = self.dataset_id_input.text().strip() @@ -131,9 +176,6 @@ def connect_and_load_schema(self): QMessageBox.warning(self, "Warning", "Please enter a Dataset ID") return - if not self.base_url.endswith('/'): - self.base_url += '/' - try: self.schema = self.get_resource_schema() @@ -174,8 +216,6 @@ def get_resource_schema(self) -> list: if data.get("success"): result = data.get("result", {}).get("resource_fields", []) return result - else: - print(f"Failed fetching scheming endpoint: {response.content}") except: pass