"""Job creation/editing dialog""" from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QComboBox, QPushButton, QTextEdit, QSpinBox, QFileDialog, QMessageBox, QGroupBox ) from PyQt6.QtCore import Qt from core import DatabaseManager from datetime import datetime class JobDialog(QDialog): """Dialog for creating/editing jobs""" def __init__(self, parent=None, job_id=None): """ Initialize dialog. Args: parent: Parent widget job_id: If provided, edit mode. If None, create mode. """ super().__init__(parent) self.db = DatabaseManager() self.job_id = job_id self.is_edit_mode = job_id is not None self.setWindowTitle("Edit Job" if self.is_edit_mode else "New Job") self.setMinimumWidth(600) self._init_ui() if self.is_edit_mode: self._load_job_data() def _init_ui(self): """Create UI layout""" layout = QVBoxLayout() self.setLayout(layout) # Job details group details_group = QGroupBox("Job Details") details_layout = QFormLayout() self.name_edit = QLineEdit() details_layout.addRow("Name:", self.name_edit) self.type_combo = QComboBox() self.type_combo.addItems(["script", "python_module"]) details_layout.addRow("Type:", self.type_combo) # Executable path with browse button exec_layout = QHBoxLayout() self.exec_edit = QLineEdit() exec_browse_btn = QPushButton("📁 Browse") exec_browse_btn.clicked.connect(self._browse_executable) exec_layout.addWidget(self.exec_edit) exec_layout.addWidget(exec_browse_btn) details_layout.addRow("Executable:", exec_layout) # Working directory with browse button workdir_layout = QHBoxLayout() self.workdir_edit = QLineEdit() workdir_browse_btn = QPushButton("📁 Browse") workdir_browse_btn.clicked.connect(self._browse_workdir) workdir_layout.addWidget(self.workdir_edit) workdir_layout.addWidget(workdir_browse_btn) details_layout.addRow("Working Dir:", workdir_layout) self.timeout_spin = QSpinBox() self.timeout_spin.setRange(1, 86400) # 1 sec to 24 hours self.timeout_spin.setValue(3600) self.timeout_spin.setSuffix(" seconds") details_layout.addRow("Timeout:", self.timeout_spin) self.description_edit = QTextEdit() self.description_edit.setMaximumHeight(80) details_layout.addRow("Description:", self.description_edit) details_group.setLayout(details_layout) layout.addWidget(details_group) # Schedule group schedule_group = QGroupBox("Schedule (Cron Expression)") schedule_layout = QFormLayout() self.cron_edit = QLineEdit() self.cron_edit.setPlaceholderText("e.g., 0 2 * * * (daily at 2 AM)") schedule_layout.addRow("Cron:", self.cron_edit) # Cron helper text helper_text = QTextEdit() helper_text.setReadOnly(True) helper_text.setMaximumHeight(100) helper_text.setPlainText( "Cron Format: minute hour day month day_of_week\n\n" "Examples:\n" "* * * * * - Every minute\n" "0 * * * * - Every hour\n" "0 2 * * * - Daily at 2 AM\n" "0 0 * * 0 - Weekly on Sunday\n" "0 0 1 * * - Monthly on 1st" ) schedule_layout.addRow(helper_text) schedule_group.setLayout(schedule_layout) layout.addWidget(schedule_group) # Buttons button_layout = QHBoxLayout() button_layout.addStretch() save_btn = QPushButton("💾 Save") save_btn.clicked.connect(self.accept) button_layout.addWidget(save_btn) cancel_btn = QPushButton("❌ Cancel") cancel_btn.clicked.connect(self.reject) button_layout.addWidget(cancel_btn) layout.addLayout(button_layout) def _browse_executable(self): """Browse for executable file""" file_path, _ = QFileDialog.getOpenFileName( self, "Select Executable", "", "All Files (*)" ) if file_path: self.exec_edit.setText(file_path) def _browse_workdir(self): """Browse for working directory""" dir_path = QFileDialog.getExistingDirectory( self, "Select Working Directory" ) if dir_path: self.workdir_edit.setText(dir_path) def _load_job_data(self): """Load existing job data for editing""" with self.db.get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM jobs WHERE id = ?", (self.job_id,)) job = cursor.fetchone() cursor.execute(""" SELECT cron_expression FROM schedules WHERE job_id = ? AND enabled = 1 """, (self.job_id,)) schedule = cursor.fetchone() # Populate fields self.name_edit.setText(job['name']) self.type_combo.setCurrentText(job['job_type']) self.exec_edit.setText(job['executable_path']) self.workdir_edit.setText(job['working_directory'] or "") self.timeout_spin.setValue(job['timeout']) self.description_edit.setPlainText(job['description'] or "") if schedule: self.cron_edit.setText(schedule['cron_expression']) def accept(self): """Validate and save job""" # Validate inputs if not self.name_edit.text().strip(): QMessageBox.warning(self, "Validation Error", "Job name is required.") return if not self.exec_edit.text().strip(): QMessageBox.warning(self, "Validation Error", "Executable path is required.") return # Save job try: now = datetime.now().isoformat() with self.db.get_connection() as conn: cursor = conn.cursor() if self.is_edit_mode: # Update existing job cursor.execute(""" UPDATE jobs SET name = ?, job_type = ?, executable_path = ?, working_directory = ?, timeout = ?, description = ?, updated_at = ? WHERE id = ? """, ( self.name_edit.text().strip(), self.type_combo.currentText(), self.exec_edit.text().strip(), self.workdir_edit.text().strip() or None, self.timeout_spin.value(), self.description_edit.toPlainText().strip() or None, now, self.job_id )) job_id = self.job_id else: # Create new job cursor.execute(""" INSERT INTO jobs ( name, job_type, executable_path, working_directory, timeout, enabled, description, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?) """, ( self.name_edit.text().strip(), self.type_combo.currentText(), self.exec_edit.text().strip(), self.workdir_edit.text().strip() or None, self.timeout_spin.value(), self.description_edit.toPlainText().strip() or None, now, now )) job_id = cursor.lastrowid # Handle schedule cron_expr = self.cron_edit.text().strip() if cron_expr: # Delete old schedule if exists cursor.execute("DELETE FROM schedules WHERE job_id = ?", (job_id,)) # Create new schedule cursor.execute(""" INSERT INTO schedules ( job_id, cron_expression, enabled, created_at, updated_at ) VALUES (?, ?, 1, ?, ?) """, (job_id, cron_expr, now, now)) super().accept() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save job: {e}")