From 793667291457380a0341f2ce7f3d25934de56b5b Mon Sep 17 00:00:00 2001 From: matin Date: Mon, 21 Apr 2025 18:06:36 +0200 Subject: first try to add commentary and save functionalities by Claude --- calendar_gui.py | 776 +++++++++++++++++++++++------------------------ calendar_manager.py | 286 ++++++++--------- date_calculator.py | 306 +++++++++---------- prediction_controller.py | 133 +++++--- prediction_storage.py | 151 +++++++++ test.py | 133 +++++--- 6 files changed, 1021 insertions(+), 764 deletions(-) create mode 100644 prediction_storage.py diff --git a/calendar_gui.py b/calendar_gui.py index c111db5..ef2f359 100644 --- a/calendar_gui.py +++ b/calendar_gui.py @@ -1,389 +1,389 @@ -import sys -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QLineEdit, QPushButton, QDateEdit, QTableWidget, - QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QComboBox, - QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar) -from PyQt5.QtCore import Qt, QDate -from PyQt5.QtGui import QFont -from datetime import datetime, timedelta -from calendar_manager import CalendarManager -from date_calculator import DateCalculator -from prediction_controller import PredictionController - -class AddEventDialog(QDialog): - def __init__(self, keyword_list, parent=None): - super().__init__(parent) - self.setWindowTitle("Add New Event") - self.keyword_list = keyword_list - self.init_ui() - - def init_ui(self): - layout = QFormLayout() - - # Start date selector - self.start_date = QDateEdit() - self.start_date.setCalendarPopup(True) - self.start_date.setDate(QDate.currentDate()) - layout.addRow("Start Date:", self.start_date) - - # End date selector - self.end_date = QDateEdit() - self.end_date.setCalendarPopup(True) - self.end_date.setDate(QDate.currentDate().addDays(1)) - layout.addRow("End Date:", self.end_date) - - # Keyword selector - self.keyword = QComboBox() - self.keyword.addItems(self.keyword_list) - layout.addRow("Keyword:", self.keyword) - - # Buttons - button_layout = QHBoxLayout() - self.save_button = QPushButton("Save") - self.save_button.clicked.connect(self.accept) - self.cancel_button = QPushButton("Cancel") - self.cancel_button.clicked.connect(self.reject) - - button_layout.addWidget(self.save_button) - button_layout.addWidget(self.cancel_button) - layout.addRow("", button_layout) - - self.setLayout(layout) - - def get_data(self): - start_date = self.start_date.date().toString("yyyy-MM-dd") - end_date = self.end_date.date().toString("yyyy-MM-dd") - keyword = self.keyword.currentText() - - return start_date, end_date, keyword - - -class ModifyEventDialog(QDialog): - def __init__(self, entry, keyword_list, parent=None): - super().__init__(parent) - self.setWindowTitle("Modify Event") - self.entry = entry - self.keyword_list = keyword_list - self.init_ui() - - def init_ui(self): - layout = QFormLayout() - - # Start date selector - self.start_date = QDateEdit() - self.start_date.setCalendarPopup(True) - entry_start = QDate(self.entry.start_date.year, - self.entry.start_date.month, - self.entry.start_date.day) - self.start_date.setDate(entry_start) - layout.addRow("Start Date:", self.start_date) - - # End date selector - self.end_date = QDateEdit() - self.end_date.setCalendarPopup(True) - entry_end = QDate(self.entry.end_date.year, - self.entry.end_date.month, - self.entry.end_date.day) - self.end_date.setDate(entry_end) - layout.addRow("End Date:", self.end_date) - - # Keyword selector - self.keyword = QComboBox() - self.keyword.addItems(self.keyword_list) - current_index = self.keyword_list.index(self.entry.keyword) if self.entry.keyword in self.keyword_list else 0 - self.keyword.setCurrentIndex(current_index) - layout.addRow("Keyword:", self.keyword) - - # Buttons - button_layout = QHBoxLayout() - self.save_button = QPushButton("Save") - self.save_button.clicked.connect(self.accept) - self.cancel_button = QPushButton("Cancel") - self.cancel_button.clicked.connect(self.reject) - - button_layout.addWidget(self.save_button) - button_layout.addWidget(self.cancel_button) - layout.addRow("", button_layout) - - self.setLayout(layout) - - def get_data(self): - start_date = self.start_date.date().toString("yyyy-MM-dd") - end_date = self.end_date.date().toString("yyyy-MM-dd") - keyword = self.keyword.currentText() - - return start_date, end_date, keyword - - -class CalendarManagerGUI(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Calendar Manager") - self.setMinimumSize(800, 600) - - # Initialize backend components - self.keyword_list = ["full_project", "half_project", "event"] - self.calendar_manager = CalendarManager() - self.date_calculator = DateCalculator() - self.prediction_controller = PredictionController( - self.calendar_manager, - self.date_calculator, - self.keyword_list - ) - - self.init_ui() - self.create_menus() - - def create_menus(self): - # Create menu bar - menubar = self.menuBar() - - # File menu - file_menu = menubar.addMenu('File') - - # Save action - save_action = QAction('Save', self) - save_action.setShortcut('Ctrl+S') - save_action.triggered.connect(self.save_file) - file_menu.addAction(save_action) - - # Load action - load_action = QAction('Load', self) - load_action.setShortcut('Ctrl+O') - load_action.triggered.connect(self.load_file) - file_menu.addAction(load_action) - - # Clear action - clear_action = QAction('Clear', self) - clear_action.setShortcut('Ctrl+N') - clear_action.triggered.connect(self.clear_entries) - file_menu.addAction(clear_action) - - # Exit action - exit_action = QAction('Exit', self) - exit_action.setShortcut('Ctrl+Q') - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - def init_ui(self): - self.setWindowTitle('Calendar Manager GUI') - self.setMinimumSize(800, 600) - - central_widget = QWidget() - main_layout = QVBoxLayout() - - top_layout = QHBoxLayout() - - # Launch date input - launch_date_layout = QVBoxLayout() - launch_date_label = QLabel("Launch Date:") - self.launch_date_edit = QDateEdit() - self.launch_date_edit.setCalendarPopup(True) # Enable calendar popup - self.launch_date_edit.setDate(QDate.currentDate()) - self.launch_date_edit.dateChanged.connect(self.update_prediction) # Auto-update on change - launch_date_layout.addWidget(launch_date_label) - launch_date_layout.addWidget(self.launch_date_edit) - top_layout.addLayout(launch_date_layout) - - # Duration input - duration_layout = QVBoxLayout() - duration_label = QLabel("Duration (years):") - self.duration_spin = QSpinBox() - self.duration_spin.setRange(1, 99) - self.duration_spin.setValue(1) - self.duration_spin.valueChanged.connect(self.update_prediction) # Auto-update on change - duration_layout.addWidget(duration_label) - duration_layout.addWidget(self.duration_spin) - top_layout.addLayout(duration_layout) - - # Prediction result - prediction_result_layout = QVBoxLayout() - prediction_result_label = QLabel("Predicted Completion Date:") - self.prediction_result = QDateEdit() - self.prediction_result.setReadOnly(True) - self.prediction_result.setButtonSymbols(QDateEdit.ButtonSymbols.NoButtons) - prediction_result_layout.addWidget(prediction_result_label) - prediction_result_layout.addWidget(self.prediction_result) - - top_layout.addLayout(prediction_result_layout) - - main_layout.addLayout(top_layout) - - # Events section - events_layout = QVBoxLayout() - events_title = QLabel("

Calendar Events

") - events_layout.addWidget(events_title) - - # Add event button - add_event_button = QPushButton("Add Event") - add_event_button.clicked.connect(self.add_event) - events_layout.addWidget(add_event_button) - - # Events table - self.events_table = QTableWidget() - self.events_table.setColumnCount(7) # ID (hidden), Start, End, Keyword, CorrectedStart, CorrectedEnd, Actions - self.events_table.setHorizontalHeaderLabels(["ID", "Start Date", "End Date", "Keyword", "Corrected Start", "Corrected End", "Actions"]) - self.events_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.events_table.setColumnHidden(0, True) # Hide ID column - events_layout.addWidget(self.events_table) - - main_layout.addLayout(events_layout) - - # Set main layout - central_widget.setLayout(main_layout) - self.setCentralWidget(central_widget) - - # Load initial data - self.update_events_table() - self.update_prediction() # Calculate initial prediction - - def update_prediction(self): - """Update prediction whenever needed""" - try: - launch_date = self.launch_date_edit.date().toString("yyyy-MM-dd") - duration_years = self.duration_spin.value() - - self.prediction_controller.make_prediction(launch_date, duration_years) - prediction_date = self.prediction_controller.get_prediction() - - if prediction_date: - self.prediction_result.setDate(prediction_date) - - # Update table to show corrected dates - self.update_events_table() - - except Exception as e: - self.prediction_result.setText("Error in calculation") - print(f"Error calculating prediction: {str(e)}") - - def add_event(self): - dialog = AddEventDialog(self.keyword_list, self) - if dialog.exec_(): - start_date, end_date, keyword = dialog.get_data() - try: - self.calendar_manager.add_entry(start_date, end_date, keyword) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error adding event: {str(e)}") - - def modify_event(self, event_id): - entry = self.calendar_manager.get_entry_by_id(event_id) - if entry: - dialog = ModifyEventDialog(entry, self.keyword_list, self) - if dialog.exec_(): - start_date, end_date, keyword = dialog.get_data() - try: - self.calendar_manager.modify_entry(event_id, - datetime.fromisoformat(start_date), - datetime.fromisoformat(end_date), - keyword) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error modifying event: {str(e)}") - - def delete_event(self, event_id): - reply = QMessageBox.question(self, "Delete Event", - "Are you sure you want to delete this event?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.calendar_manager.delete_entry(event_id) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error deleting event: {str(e)}") - - def update_events_table(self): - # Clear table - self.events_table.setRowCount(0) - - # Add entries to table - entries = self.calendar_manager.list_entries() - for i, entry in enumerate(entries): - self.events_table.insertRow(i) - - # Set item data - self.events_table.setItem(i, 0, QTableWidgetItem(entry.id)) - self.events_table.setItem(i, 1, QTableWidgetItem(entry.start_date.strftime("%Y-%m-%d"))) - self.events_table.setItem(i, 2, QTableWidgetItem(entry.end_date.strftime("%Y-%m-%d"))) - self.events_table.setItem(i, 3, QTableWidgetItem(entry.keyword)) - - # Corrected dates - corrected_start = entry.corrected_start_date.strftime("%Y-%m-%d") if entry.corrected_start_date else "" - corrected_end = entry.corrected_end_date.strftime("%Y-%m-%d") if entry.corrected_end_date else "" - self.events_table.setItem(i, 4, QTableWidgetItem(corrected_start)) - self.events_table.setItem(i, 5, QTableWidgetItem(corrected_end)) - - # Action buttons - actions_widget = QWidget() - actions_layout = QHBoxLayout() - actions_layout.setContentsMargins(0, 0, 0, 0) - - modify_button = QPushButton("Modify") - delete_button = QPushButton("Delete") - - # Use lambda with default argument to capture the correct event_id - modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid)) - delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid)) - - actions_layout.addWidget(modify_button) - actions_layout.addWidget(delete_button) - actions_widget.setLayout(actions_layout) - - self.events_table.setCellWidget(i, 6, actions_widget) - - def save_file(self): - """Save calendar entries to a JSON file""" - if not self.calendar_manager.filename: - file_path, _ = QFileDialog.getSaveFileName(self, "Save Calendar", "", "JSON Files (*.json)") - if not file_path: - return - self.calendar_manager.switch_file(file_path) - - try: - self.calendar_manager.save_entries() - QMessageBox.information(self, "Save Successful", f"Calendar saved to {self.calendar_manager.filename}") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to save calendar: {str(e)}") - - def load_file(self): - """Load calendar entries from a JSON file""" - file_path, _ = QFileDialog.getOpenFileName(self, "Load Calendar", "", "JSON Files (*.json)") - if not file_path: - return - - try: - self.calendar_manager.load_file(file_path) - self.update_events_table() - self.update_prediction() # Auto-update prediction - QMessageBox.information(self, "Load Successful", f"Calendar loaded from {file_path}") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to load calendar: {str(e)}") - - def clear_entries(self): - """Clear all calendar entries""" - reply = QMessageBox.question(self, "Clear Calendar", - "Are you sure you want to clear all events?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.calendar_manager.clear_entries() - self.update_events_table() - self.update_prediction() # Auto-update prediction - QMessageBox.information(self, "Success", "Calendar cleared successfully") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to clear calendar: {str(e)}") - - -def main(): - app = QApplication(sys.argv) - app.setStyle('Fusion') - window = CalendarManagerGUI() - window.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": +import sys +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QDateEdit, QTableWidget, + QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QComboBox, + QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar) +from PyQt5.QtCore import Qt, QDate +from PyQt5.QtGui import QFont +from datetime import datetime, timedelta +from calendar_manager import CalendarManager +from date_calculator import DateCalculator +from prediction_controller import PredictionController + +class AddEventDialog(QDialog): + def __init__(self, keyword_list, parent=None): + super().__init__(parent) + self.setWindowTitle("Add New Event") + self.keyword_list = keyword_list + self.init_ui() + + def init_ui(self): + layout = QFormLayout() + + # Start date selector + self.start_date = QDateEdit() + self.start_date.setCalendarPopup(True) + self.start_date.setDate(QDate.currentDate()) + layout.addRow("Start Date:", self.start_date) + + # End date selector + self.end_date = QDateEdit() + self.end_date.setCalendarPopup(True) + self.end_date.setDate(QDate.currentDate().addDays(1)) + layout.addRow("End Date:", self.end_date) + + # Keyword selector + self.keyword = QComboBox() + self.keyword.addItems(self.keyword_list) + layout.addRow("Keyword:", self.keyword) + + # Buttons + button_layout = QHBoxLayout() + self.save_button = QPushButton("Save") + self.save_button.clicked.connect(self.accept) + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.reject) + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.cancel_button) + layout.addRow("", button_layout) + + self.setLayout(layout) + + def get_data(self): + start_date = self.start_date.date().toString("yyyy-MM-dd") + end_date = self.end_date.date().toString("yyyy-MM-dd") + keyword = self.keyword.currentText() + + return start_date, end_date, keyword + + +class ModifyEventDialog(QDialog): + def __init__(self, entry, keyword_list, parent=None): + super().__init__(parent) + self.setWindowTitle("Modify Event") + self.entry = entry + self.keyword_list = keyword_list + self.init_ui() + + def init_ui(self): + layout = QFormLayout() + + # Start date selector + self.start_date = QDateEdit() + self.start_date.setCalendarPopup(True) + entry_start = QDate(self.entry.start_date.year, + self.entry.start_date.month, + self.entry.start_date.day) + self.start_date.setDate(entry_start) + layout.addRow("Start Date:", self.start_date) + + # End date selector + self.end_date = QDateEdit() + self.end_date.setCalendarPopup(True) + entry_end = QDate(self.entry.end_date.year, + self.entry.end_date.month, + self.entry.end_date.day) + self.end_date.setDate(entry_end) + layout.addRow("End Date:", self.end_date) + + # Keyword selector + self.keyword = QComboBox() + self.keyword.addItems(self.keyword_list) + current_index = self.keyword_list.index(self.entry.keyword) if self.entry.keyword in self.keyword_list else 0 + self.keyword.setCurrentIndex(current_index) + layout.addRow("Keyword:", self.keyword) + + # Buttons + button_layout = QHBoxLayout() + self.save_button = QPushButton("Save") + self.save_button.clicked.connect(self.accept) + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.reject) + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.cancel_button) + layout.addRow("", button_layout) + + self.setLayout(layout) + + def get_data(self): + start_date = self.start_date.date().toString("yyyy-MM-dd") + end_date = self.end_date.date().toString("yyyy-MM-dd") + keyword = self.keyword.currentText() + + return start_date, end_date, keyword + + +class CalendarManagerGUI(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Calendar Manager") + self.setMinimumSize(800, 600) + + # Initialize backend components + self.keyword_list = ["full_project", "half_project", "event"] + self.calendar_manager = CalendarManager() + self.date_calculator = DateCalculator() + self.prediction_controller = PredictionController( + self.calendar_manager, + self.date_calculator, + self.keyword_list + ) + + self.init_ui() + self.create_menus() + + def create_menus(self): + # Create menu bar + menubar = self.menuBar() + + # File menu + file_menu = menubar.addMenu('File') + + # Save action + save_action = QAction('Save', self) + save_action.setShortcut('Ctrl+S') + save_action.triggered.connect(self.save_file) + file_menu.addAction(save_action) + + # Load action + load_action = QAction('Load', self) + load_action.setShortcut('Ctrl+O') + load_action.triggered.connect(self.load_file) + file_menu.addAction(load_action) + + # Clear action + clear_action = QAction('Clear', self) + clear_action.setShortcut('Ctrl+N') + clear_action.triggered.connect(self.clear_entries) + file_menu.addAction(clear_action) + + # Exit action + exit_action = QAction('Exit', self) + exit_action.setShortcut('Ctrl+Q') + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + def init_ui(self): + self.setWindowTitle('Calendar Manager GUI') + self.setMinimumSize(800, 600) + + central_widget = QWidget() + main_layout = QVBoxLayout() + + top_layout = QHBoxLayout() + + # Launch date input + launch_date_layout = QVBoxLayout() + launch_date_label = QLabel("Launch Date:") + self.launch_date_edit = QDateEdit() + self.launch_date_edit.setCalendarPopup(True) # Enable calendar popup + self.launch_date_edit.setDate(QDate.currentDate()) + self.launch_date_edit.dateChanged.connect(self.update_prediction) # Auto-update on change + launch_date_layout.addWidget(launch_date_label) + launch_date_layout.addWidget(self.launch_date_edit) + top_layout.addLayout(launch_date_layout) + + # Duration input + duration_layout = QVBoxLayout() + duration_label = QLabel("Duration (years):") + self.duration_spin = QSpinBox() + self.duration_spin.setRange(1, 99) + self.duration_spin.setValue(1) + self.duration_spin.valueChanged.connect(self.update_prediction) # Auto-update on change + duration_layout.addWidget(duration_label) + duration_layout.addWidget(self.duration_spin) + top_layout.addLayout(duration_layout) + + # Prediction result + prediction_result_layout = QVBoxLayout() + prediction_result_label = QLabel("Predicted Completion Date:") + self.prediction_result = QDateEdit() + self.prediction_result.setReadOnly(True) + self.prediction_result.setButtonSymbols(QDateEdit.ButtonSymbols.NoButtons) + prediction_result_layout.addWidget(prediction_result_label) + prediction_result_layout.addWidget(self.prediction_result) + + top_layout.addLayout(prediction_result_layout) + + main_layout.addLayout(top_layout) + + # Events section + events_layout = QVBoxLayout() + events_title = QLabel("

Calendar Events

") + events_layout.addWidget(events_title) + + # Add event button + add_event_button = QPushButton("Add Event") + add_event_button.clicked.connect(self.add_event) + events_layout.addWidget(add_event_button) + + # Events table + self.events_table = QTableWidget() + self.events_table.setColumnCount(7) # ID (hidden), Start, End, Keyword, CorrectedStart, CorrectedEnd, Actions + self.events_table.setHorizontalHeaderLabels(["ID", "Start Date", "End Date", "Keyword", "Corrected Start", "Corrected End", "Actions"]) + self.events_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.events_table.setColumnHidden(0, True) # Hide ID column + events_layout.addWidget(self.events_table) + + main_layout.addLayout(events_layout) + + # Set main layout + central_widget.setLayout(main_layout) + self.setCentralWidget(central_widget) + + # Load initial data + self.update_events_table() + self.update_prediction() # Calculate initial prediction + + def update_prediction(self): + """Update prediction whenever needed""" + try: + launch_date = self.launch_date_edit.date().toString("yyyy-MM-dd") + duration_years = self.duration_spin.value() + + self.prediction_controller.make_prediction(launch_date, duration_years) + prediction_date = self.prediction_controller.get_prediction() + + if prediction_date: + self.prediction_result.setDate(prediction_date) + + # Update table to show corrected dates + self.update_events_table() + + except Exception as e: + self.prediction_result.setText("Error in calculation") + print(f"Error calculating prediction: {str(e)}") + + def add_event(self): + dialog = AddEventDialog(self.keyword_list, self) + if dialog.exec_(): + start_date, end_date, keyword = dialog.get_data() + try: + self.calendar_manager.add_entry(start_date, end_date, keyword) + self.update_events_table() + self.update_prediction() # Auto-update prediction + except Exception as e: + QMessageBox.critical(self, "Error", f"Error adding event: {str(e)}") + + def modify_event(self, event_id): + entry = self.calendar_manager.get_entry_by_id(event_id) + if entry: + dialog = ModifyEventDialog(entry, self.keyword_list, self) + if dialog.exec_(): + start_date, end_date, keyword = dialog.get_data() + try: + self.calendar_manager.modify_entry(event_id, + datetime.fromisoformat(start_date), + datetime.fromisoformat(end_date), + keyword) + self.update_events_table() + self.update_prediction() # Auto-update prediction + except Exception as e: + QMessageBox.critical(self, "Error", f"Error modifying event: {str(e)}") + + def delete_event(self, event_id): + reply = QMessageBox.question(self, "Delete Event", + "Are you sure you want to delete this event?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + try: + self.calendar_manager.delete_entry(event_id) + self.update_events_table() + self.update_prediction() # Auto-update prediction + except Exception as e: + QMessageBox.critical(self, "Error", f"Error deleting event: {str(e)}") + + def update_events_table(self): + # Clear table + self.events_table.setRowCount(0) + + # Add entries to table + entries = self.calendar_manager.list_entries() + for i, entry in enumerate(entries): + self.events_table.insertRow(i) + + # Set item data + self.events_table.setItem(i, 0, QTableWidgetItem(entry.id)) + self.events_table.setItem(i, 1, QTableWidgetItem(entry.start_date.strftime("%Y-%m-%d"))) + self.events_table.setItem(i, 2, QTableWidgetItem(entry.end_date.strftime("%Y-%m-%d"))) + self.events_table.setItem(i, 3, QTableWidgetItem(entry.keyword)) + + # Corrected dates + corrected_start = entry.corrected_start_date.strftime("%Y-%m-%d") if entry.corrected_start_date else "" + corrected_end = entry.corrected_end_date.strftime("%Y-%m-%d") if entry.corrected_end_date else "" + self.events_table.setItem(i, 4, QTableWidgetItem(corrected_start)) + self.events_table.setItem(i, 5, QTableWidgetItem(corrected_end)) + + # Action buttons + actions_widget = QWidget() + actions_layout = QHBoxLayout() + actions_layout.setContentsMargins(0, 0, 0, 0) + + modify_button = QPushButton("Modify") + delete_button = QPushButton("Delete") + + # Use lambda with default argument to capture the correct event_id + modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid)) + delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid)) + + actions_layout.addWidget(modify_button) + actions_layout.addWidget(delete_button) + actions_widget.setLayout(actions_layout) + + self.events_table.setCellWidget(i, 6, actions_widget) + + def save_file(self): + """Save calendar entries to a JSON file""" + if not self.calendar_manager.filename: + file_path, _ = QFileDialog.getSaveFileName(self, "Save Calendar", "", "JSON Files (*.json)") + if not file_path: + return + self.calendar_manager.switch_file(file_path) + + try: + self.calendar_manager.save_entries() + QMessageBox.information(self, "Save Successful", f"Calendar saved to {self.calendar_manager.filename}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save calendar: {str(e)}") + + def load_file(self): + """Load calendar entries from a JSON file""" + file_path, _ = QFileDialog.getOpenFileName(self, "Load Calendar", "", "JSON Files (*.json)") + if not file_path: + return + + try: + self.calendar_manager.load_file(file_path) + self.update_events_table() + self.update_prediction() # Auto-update prediction + QMessageBox.information(self, "Load Successful", f"Calendar loaded from {file_path}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load calendar: {str(e)}") + + def clear_entries(self): + """Clear all calendar entries""" + reply = QMessageBox.question(self, "Clear Calendar", + "Are you sure you want to clear all events?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + try: + self.calendar_manager.clear_entries() + self.update_events_table() + self.update_prediction() # Auto-update prediction + QMessageBox.information(self, "Success", "Calendar cleared successfully") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to clear calendar: {str(e)}") + + +def main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') + window = CalendarManagerGUI() + window.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/calendar_manager.py b/calendar_manager.py index b62cf5d..be19b2f 100644 --- a/calendar_manager.py +++ b/calendar_manager.py @@ -1,136 +1,152 @@ -# calendar_manager.py -import json -import uuid -from datetime import datetime - -class CalendarEntry: - def __init__(self, start_date, end_date, keyword, entry_id=None, corrected_start_date=None, corrected_end_date=None): - self.id = entry_id if entry_id else str(uuid.uuid4()) - # Convert string dates to datetime if necessary - self.start_date = ( - datetime.fromisoformat(start_date) - if isinstance(start_date, str) - else start_date - ) - self.end_date = ( - datetime.fromisoformat(end_date) - if isinstance(end_date, str) - else end_date - ) - self.keyword = keyword - self.corrected_start_date = corrected_start_date - self.corrected_end_date = corrected_end_date - def to_dict(self): - return { - 'id': self.id, - 'start_date': self.start_date.isoformat(), - 'end_date': self.end_date.isoformat(), - 'keyword': self.keyword, - 'corrected_start_date': self.corrected_start_date.isoformat() if self.corrected_start_date else None, - 'corrected_end_date': self.corrected_end_date.isoformat() if self.corrected_end_date else None, - } - - @classmethod - def from_dict(cls, data): - return cls( - start_date=datetime.fromisoformat(data['start_date']), - end_date=datetime.fromisoformat(data['end_date']), - keyword=data['keyword'], - entry_id=data['id'], - corrected_start_date=datetime.fromisoformat(data['corrected_start_date']) if data.get('corrected_start_date') else None, - corrected_end_date=datetime.fromisoformat(data['corrected_end_date']) if data.get('corrected_end_date') else None, - ) - - def __repr__(self): - return (f"CalendarEntry(id={self.id}, start_date={self.start_date}, " - f"end_date={self.end_date}, keyword='{self.keyword}', " - f"corrected_start_date={self.corrected_start_date}, corrected_end_date={self.corrected_end_date})") - -class CalendarManager: - def __init__(self, filename=None): - self.filename = filename - self.entries = [] - if filename: - self.entries = self.add_entries_from_file(filename) - - def switch_file(self, new_filename): - """Switch to a new file.""" - self.filename = new_filename - - def load_file(self, new_filename): - """Clears current entries and loads entries from new file.""" - self.clear_entries() - self.add_entries_from_file(new_filename) - self.switch_file(new_filename) - - def add_entries_from_file(self, file_path): - """Add events from another JSON file to the current calendar.""" - try: - with open(file_path, 'r') as f: - data = json.load(f) - new_entries = [CalendarEntry.from_dict(entry) for entry in data] - self.entries.extend(new_entries) - self.save_entries() - return new_entries - except (FileNotFoundError, json.JSONDecodeError) as e: - print(f"Error reading file {file_path}: {e}") - return [] - - def clear_entries(self): - self.entries = [] - - def save_entries(self): - """Save the current list of events to the file.""" - if self.filename: - try: - with open(self.filename, 'w') as f: - json.dump([entry.to_dict() for entry in self.entries], f, indent=4) - except Exception as e: - print(f"Error writing to file {self.filename}: {e}") - - def add_entry(self, start_date, end_date, keyword): - """Add a new event to the calendar.""" - new_entry = CalendarEntry(start_date, end_date, keyword) - self.entries.append(new_entry) - self.save_entries() - return new_entry - - def modify_entry(self, entry_id, start_date=None, end_date=None, keyword=None): - """Modify an existing event by ID.""" - entry = self.get_entry_by_id(entry_id) - if entry: - if start_date: - entry.start_date = start_date - if end_date: - entry.end_date = end_date - if keyword: - entry.keyword = keyword - self.save_entries() - return entry - return None - - def delete_entry(self, entry_id): - """Delete an event by ID.""" - entry = self.get_entry_by_id(entry_id) - if entry: - self.entries.remove(entry) - self.save_entries() - return entry - return None - - def get_entry_by_id(self, entry_id): - """Get an event by its ID.""" - return next((entry for entry in self.entries if entry.id == entry_id), None) - - def list_entries(self): - """List all calendar entries.""" - return self.entries - - def correct_dates(self, list_of_events): - for start_date, end_date, original_id in list_of_events: - entry = self.get_entry_by_id(original_id) - entry.corrected_start_date = start_date - entry.corrected_end_date = end_date - - +# calendar_manager.py +import json +import uuid +from datetime import datetime + +class CalendarEntry: + def __init__(self, start_date, end_date, keyword, entry_id=None, corrected_start_date=None, corrected_end_date=None, commentary=None): + self.id = entry_id if entry_id else str(uuid.uuid4()) + # Convert string dates to datetime if necessary + self.start_date = ( + datetime.fromisoformat(start_date) + if isinstance(start_date, str) + else start_date + ) + self.end_date = ( + datetime.fromisoformat(end_date) + if isinstance(end_date, str) + else end_date + ) + self.keyword = keyword + self.corrected_start_date = corrected_start_date + self.corrected_end_date = corrected_end_date + self.commentary = commentary + + def to_dict(self): + return { + 'id': self.id, + 'start_date': self.start_date.isoformat(), + 'end_date': self.end_date.isoformat(), + 'keyword': self.keyword, + 'corrected_start_date': self.corrected_start_date.isoformat() if self.corrected_start_date else None, + 'corrected_end_date': self.corrected_end_date.isoformat() if self.corrected_end_date else None, + 'commentary': self.commentary, + } + + @classmethod + def from_dict(cls, data): + return cls( + start_date=datetime.fromisoformat(data['start_date']), + end_date=datetime.fromisoformat(data['end_date']), + keyword=data['keyword'], + entry_id=data['id'], + corrected_start_date=datetime.fromisoformat(data['corrected_start_date']) if data.get('corrected_start_date') else None, + corrected_end_date=datetime.fromisoformat(data['corrected_end_date']) if data.get('corrected_end_date') else None, + commentary=data.get('commentary'), + ) + + def __repr__(self): + return (f"CalendarEntry(id={self.id}, start_date={self.start_date}, " + f"end_date={self.end_date}, keyword='{self.keyword}', " + f"corrected_start_date={self.corrected_start_date}, corrected_end_date={self.corrected_end_date}, " + f"commentary={repr(self.commentary)})") + +class CalendarManager: + def __init__(self, filename=None): + self.filename = filename + self.entries = [] + if filename: + self.entries = self.add_entries_from_file(filename) + + def switch_file(self, new_filename): + """Switch to a new file.""" + self.filename = new_filename + + def load_file(self, new_filename): + """Clears current entries and loads entries from new file.""" + self.clear_entries() + self.add_entries_from_file(new_filename) + self.switch_file(new_filename) + + def add_entries_from_file(self, file_path): + """Add events from another JSON file to the current calendar.""" + try: + with open(file_path, 'r') as f: + data = json.load(f) + new_entries = [CalendarEntry.from_dict(entry) for entry in data] + self.entries.extend(new_entries) + self.save_entries() + return new_entries + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error reading file {file_path}: {e}") + return [] + + def clear_entries(self): + self.entries = [] + + def save_entries(self): + """Save the current list of events to the file.""" + if self.filename: + try: + with open(self.filename, 'w') as f: + json.dump([entry.to_dict() for entry in self.entries], f, indent=4) + except Exception as e: + print(f"Error writing to file {self.filename}: {e}") + + def add_entry(self, start_date, end_date, keyword, commentary=None): + """Add a new event to the calendar.""" + new_entry = CalendarEntry(start_date, end_date, keyword, commentary=commentary) + self.entries.append(new_entry) + self.save_entries() + return new_entry + + def modify_entry(self, entry_id, start_date=None, end_date=None, keyword=None, commentary=None): + """Modify an existing event by ID.""" + entry = self.get_entry_by_id(entry_id) + if entry: + if start_date: + entry.start_date = start_date + if end_date: + entry.end_date = end_date + if keyword: + entry.keyword = keyword + if commentary is not None: # Allow setting empty string as commentary + entry.commentary = commentary + self.save_entries() + return entry + return None + + def add_commentary(self, entry_id, commentary): + """Add or update commentary for an existing entry.""" + entry = self.get_entry_by_id(entry_id) + if entry: + entry.commentary = commentary + self.save_entries() + return entry + return None + + def delete_entry(self, entry_id): + """Delete an event by ID.""" + entry = self.get_entry_by_id(entry_id) + if entry: + self.entries.remove(entry) + self.save_entries() + return entry + return None + + def get_entry_by_id(self, entry_id): + """Get an event by its ID.""" + return next((entry for entry in self.entries if entry.id == entry_id), None) + + def list_entries(self): + """List all calendar entries.""" + return self.entries + + def correct_dates(self, list_of_events): + for start_date, end_date, original_id in list_of_events: + entry = self.get_entry_by_id(original_id) + entry.corrected_start_date = start_date + entry.corrected_end_date = end_date + + \ No newline at end of file diff --git a/date_calculator.py b/date_calculator.py index e6c20fc..402a58c 100644 --- a/date_calculator.py +++ b/date_calculator.py @@ -1,154 +1,154 @@ -# date_calculator.py -from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta -import math - -class DateCalculator: - @staticmethod - def truncate_periods(periods, launch, prediction): - considered_periods = [] - for start, end, id in periods: - # print(start) - # print(launch) - truncated_start = max(start, launch) - truncated_end = min(end, prediction) - if truncated_start <= end and truncated_start <= prediction: - considered_periods.append((truncated_start, truncated_end, id)) - return considered_periods - - @staticmethod - def round_periods(periods): - rounded_periods = [] - total_months = 0 - - for start, end, id in periods: - year_diff = end.year - start.year - month_diff = end.month - start.month - months = year_diff * 12 + month_diff - if end.day >= start.day: - months += 1 - rounded_end = start + relativedelta(months=months) - timedelta(days=1) - - rounded_periods.append((start, rounded_end, id)) - total_months += months - - return rounded_periods, total_months - - @staticmethod - def merge_periods(periods): - if not periods: - return [] - - periods.sort() - merged_periods = [] - - current_start, current_end, current_id = periods[0] - - for start, end, id in periods[1:]: - if start <= current_end + timedelta(days=1): - current_end = max(current_end, end) - else: - merged_periods.append((current_start, current_end, current_id)) - current_start, current_end, current_id = start, end, id - - merged_periods.append((current_start, current_end, current_id)) - - return merged_periods - - @staticmethod - def find_non_overlapping_periods(existing_periods, test_period): - existing_periods.sort(key=lambda x: x[0]) - - test_start, test_end, id = test_period - non_overlapping_periods = [] - - for start, end, _ in existing_periods: - if test_end < start: - non_overlapping_periods.append((test_start, test_end, id)) - return non_overlapping_periods - - elif test_start > end: - continue - - else: - if test_start < start: - non_overlapping_periods.append((test_start, start - timedelta(days=1), id)) - - test_start = end + timedelta(days=1) - - if test_start <= test_end: - non_overlapping_periods.append((test_start, test_end, id)) - - return non_overlapping_periods - - def calculate_prediction(self, launch_date, duration, **kwargs): - total_days = 0 - total_months = 0 - updated = True - prediction_start = launch_date + duration - timedelta(days = 1) - prediction = prediction_start - - events = [] - half_projects = [] - full_projects = [] - other_kwargs = {} - - for k, v in kwargs.items(): - if k == "event": - events.extend(v) - elif k == "half_project": - half_projects.extend(v) - elif k == "full_project": - full_projects.extend(v) - elif k == "two_years": - full_projects.extend(v) - else: - other_kwargs[k] = v - - while updated: - updated = False - considered_events = self.truncate_periods(events, launch_date, prediction) - considered_full_projects = self.truncate_periods(full_projects, launch_date, prediction) - considered_half_projects = self.truncate_periods(half_projects, launch_date, prediction) - - considered_full_projects_merged = self.merge_periods(considered_full_projects) - considered_full_projects_rounded, months = self.round_periods(considered_full_projects_merged) - considered_full_projects_merged2 = self.merge_periods(considered_full_projects_rounded) - considered_full_projects_rounded2, months = self.round_periods(considered_full_projects_merged2) - - non_overlapping_half_projects = [] - for test_interval in considered_half_projects: - non_overlapping_half_projects.extend( - self.find_non_overlapping_periods(considered_full_projects_rounded2, test_interval) - ) - - considered_half_projects_merged = self.merge_periods(non_overlapping_half_projects) - considered_half_projects_rounded, months2 = self.round_periods(considered_half_projects_merged) - considered_half_projects_merged2 = self.merge_periods(considered_half_projects_rounded) - considered_half_projects_rounded2, months2 = self.round_periods(considered_half_projects_merged2) - - all_projects_merged = self.merge_periods( - considered_full_projects_rounded2 + considered_half_projects_rounded2 - ) - merged_event_periods = self.merge_periods(considered_events) - - non_overlapping_event_periods = [] - for test_interval in merged_event_periods: - non_overlapping_event_periods.extend( - self.find_non_overlapping_periods(all_projects_merged, test_interval) - ) - - new_total_months = months + math.ceil(months2 / 2) - new_total_days = sum((end - start).days + 1 for start, end, _ in non_overlapping_event_periods) - - if new_total_days != total_days or new_total_months != total_months: - total_days = new_total_days - total_months = new_total_months - updated = True - prediction = launch_date + duration + relativedelta(months=total_months) + timedelta(days=total_days-1) - - # print(prediction, prediction_start + relativedelta(years = 6)) - if prediction > prediction_start + relativedelta(years = 6): - prediction = prediction_start + relativedelta(years = 6) - +# date_calculator.py +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import math + +class DateCalculator: + @staticmethod + def truncate_periods(periods, launch, prediction): + considered_periods = [] + for start, end, id in periods: + # print(start) + # print(launch) + truncated_start = max(start, launch) + truncated_end = min(end, prediction) + if truncated_start <= end and truncated_start <= prediction: + considered_periods.append((truncated_start, truncated_end, id)) + return considered_periods + + @staticmethod + def round_periods(periods): + rounded_periods = [] + total_months = 0 + + for start, end, id in periods: + year_diff = end.year - start.year + month_diff = end.month - start.month + months = year_diff * 12 + month_diff + if end.day >= start.day: + months += 1 + rounded_end = start + relativedelta(months=months) - timedelta(days=1) + + rounded_periods.append((start, rounded_end, id)) + total_months += months + + return rounded_periods, total_months + + @staticmethod + def merge_periods(periods): + if not periods: + return [] + + periods.sort() + merged_periods = [] + + current_start, current_end, current_id = periods[0] + + for start, end, id in periods[1:]: + if start <= current_end + timedelta(days=1): + current_end = max(current_end, end) + else: + merged_periods.append((current_start, current_end, current_id)) + current_start, current_end, current_id = start, end, id + + merged_periods.append((current_start, current_end, current_id)) + + return merged_periods + + @staticmethod + def find_non_overlapping_periods(existing_periods, test_period): + existing_periods.sort(key=lambda x: x[0]) + + test_start, test_end, id = test_period + non_overlapping_periods = [] + + for start, end, _ in existing_periods: + if test_end < start: + non_overlapping_periods.append((test_start, test_end, id)) + return non_overlapping_periods + + elif test_start > end: + continue + + else: + if test_start < start: + non_overlapping_periods.append((test_start, start - timedelta(days=1), id)) + + test_start = end + timedelta(days=1) + + if test_start <= test_end: + non_overlapping_periods.append((test_start, test_end, id)) + + return non_overlapping_periods + + def calculate_prediction(self, launch_date, duration, **kwargs): + total_days = 0 + total_months = 0 + updated = True + prediction_start = launch_date + duration - timedelta(days = 1) + prediction = prediction_start + + events = [] + half_projects = [] + full_projects = [] + other_kwargs = {} + + for k, v in kwargs.items(): + if k == "event": + events.extend(v) + elif k == "half_project": + half_projects.extend(v) + elif k == "full_project": + full_projects.extend(v) + elif k == "two_years": + full_projects.extend(v) + else: + other_kwargs[k] = v + + while updated: + updated = False + considered_events = self.truncate_periods(events, launch_date, prediction) + considered_full_projects = self.truncate_periods(full_projects, launch_date, prediction) + considered_half_projects = self.truncate_periods(half_projects, launch_date, prediction) + + considered_full_projects_merged = self.merge_periods(considered_full_projects) + considered_full_projects_rounded, months = self.round_periods(considered_full_projects_merged) + considered_full_projects_merged2 = self.merge_periods(considered_full_projects_rounded) + considered_full_projects_rounded2, months = self.round_periods(considered_full_projects_merged2) + + non_overlapping_half_projects = [] + for test_interval in considered_half_projects: + non_overlapping_half_projects.extend( + self.find_non_overlapping_periods(considered_full_projects_rounded2, test_interval) + ) + + considered_half_projects_merged = self.merge_periods(non_overlapping_half_projects) + considered_half_projects_rounded, months2 = self.round_periods(considered_half_projects_merged) + considered_half_projects_merged2 = self.merge_periods(considered_half_projects_rounded) + considered_half_projects_rounded2, months2 = self.round_periods(considered_half_projects_merged2) + + all_projects_merged = self.merge_periods( + considered_full_projects_rounded2 + considered_half_projects_rounded2 + ) + merged_event_periods = self.merge_periods(considered_events) + + non_overlapping_event_periods = [] + for test_interval in merged_event_periods: + non_overlapping_event_periods.extend( + self.find_non_overlapping_periods(all_projects_merged, test_interval) + ) + + new_total_months = months + math.ceil(months2 / 2) + new_total_days = sum((end - start).days + 1 for start, end, _ in non_overlapping_event_periods) + + if new_total_days != total_days or new_total_months != total_months: + total_days = new_total_days + total_months = new_total_months + updated = True + prediction = launch_date + duration + relativedelta(months=total_months) + timedelta(days=total_days-1) + + # print(prediction, prediction_start + relativedelta(years = 6)) + if prediction > prediction_start + relativedelta(years = 6): + prediction = prediction_start + relativedelta(years = 6) + return prediction, considered_full_projects_rounded2 + considered_half_projects_rounded2 + non_overlapping_event_periods \ No newline at end of file diff --git a/prediction_controller.py b/prediction_controller.py index 4d5c1ed..0188e2e 100644 --- a/prediction_controller.py +++ b/prediction_controller.py @@ -1,48 +1,85 @@ -# prediction_controller.py -from dateutil.relativedelta import relativedelta -from datetime import datetime, timedelta -import math - -class PredictionController: - def __init__(self, calendar_manager, date_calculator, keyword_list): - self.calendar_manager = calendar_manager - self.date_calculator = date_calculator - self.launch_date = None - self.duration = None - self.prediction = None - self.keyword_list = keyword_list - for keyword in keyword_list: - self.keyword = [] - - def set_parameters(self, launch_date, duration_years): - self.launch_date = datetime.fromisoformat(launch_date) - self.duration = relativedelta(years=duration_years) - - def make_prediction(self, launch_date, duration_years): - self.set_parameters(launch_date, duration_years) - - prediction = self.launch_date + self.duration - timedelta(days=1) - - keyword_args = {} - - for entry in self.calendar_manager.entries: - for keyword in self.keyword_list: - if entry.keyword == keyword: - if keyword not in keyword_args: - keyword_args[keyword] = [] - keyword_args[keyword].append((entry.start_date, entry.end_date, entry.id)) - break - - prediction, corrected_events = self.date_calculator.calculate_prediction(self.launch_date, self.duration, **keyword_args) - self.prediction = prediction - self.calendar_manager.correct_dates(corrected_events) - - def get_launch_date(self): - return self.launch_date - - def get_duration(self): - return self.duration - - def get_prediction(self): - return self.prediction - +# prediction_controller.py +from dateutil.relativedelta import relativedelta +from datetime import datetime, timedelta +import math +from prediction_storage import PredictionStorage + +class PredictionController: + def __init__(self, calendar_manager, date_calculator, keyword_list, prediction_storage=None): + self.calendar_manager = calendar_manager + self.date_calculator = date_calculator + self.launch_date = None + self.duration = None + self.prediction = None + self.keyword_list = keyword_list + self.prediction_storage = prediction_storage + for keyword in keyword_list: + self.keyword = [] + + def set_parameters(self, launch_date, duration_years): + self.launch_date = datetime.fromisoformat(launch_date) if isinstance(launch_date, str) else launch_date + self.duration = relativedelta(years=duration_years) + + def make_prediction(self, launch_date, duration_years, description=None): + self.set_parameters(launch_date, duration_years) + + prediction = self.launch_date + self.duration - timedelta(days=1) + + keyword_args = {} + + for entry in self.calendar_manager.entries: + for keyword in self.keyword_list: + if entry.keyword == keyword: + if keyword not in keyword_args: + keyword_args[keyword] = [] + keyword_args[keyword].append((entry.start_date, entry.end_date, entry.id)) + break + + prediction, corrected_events = self.date_calculator.calculate_prediction(self.launch_date, self.duration, **keyword_args) + self.prediction = prediction + self.calendar_manager.correct_dates(corrected_events) + + # Store the prediction if we have a storage object + if self.prediction_storage: + self.prediction_storage.add_prediction( + launch_date=self.launch_date, + duration_years=duration_years, + predicted_date=prediction, + keyword_args=keyword_args, + description=description + ) + + return prediction + + def get_launch_date(self): + return self.launch_date + + def get_duration(self): + return self.duration + + def get_prediction(self): + return self.prediction + + def get_all_predictions(self): + """Get all stored predictions.""" + if self.prediction_storage: + return self.prediction_storage.list_predictions() + return [] + + def get_prediction_by_id(self, prediction_id): + """Get a specific prediction by ID.""" + if self.prediction_storage: + return self.prediction_storage.get_prediction_by_id(prediction_id) + return None + + def search_predictions(self, start_date=None, end_date=None, keyword=None): + """Search predictions by date range or keyword.""" + if self.prediction_storage: + return self.prediction_storage.search_predictions(start_date, end_date, keyword) + return [] + + def update_prediction_description(self, prediction_id, description): + """Update a prediction's description.""" + if self.prediction_storage: + return self.prediction_storage.update_prediction_description(prediction_id, description) + return None \ No newline at end of file diff --git a/prediction_storage.py b/prediction_storage.py new file mode 100644 index 0000000..95688c9 --- /dev/null +++ b/prediction_storage.py @@ -0,0 +1,151 @@ +# prediction_storage.py +import json +import uuid +from datetime import datetime + +class Prediction: + def __init__(self, launch_date, duration_years, predicted_date, keyword_args=None, prediction_id=None, timestamp=None, description=None): + self.id = prediction_id if prediction_id else str(uuid.uuid4()) + self.launch_date = launch_date if isinstance(launch_date, datetime) else datetime.fromisoformat(launch_date) + self.duration_years = duration_years + self.predicted_date = predicted_date if isinstance(predicted_date, datetime) else datetime.fromisoformat(predicted_date) + self.keyword_args = keyword_args or {} + self.timestamp = timestamp if timestamp else datetime.now() + self.description = description + + def to_dict(self): + # Convert keyword_args dates to ISO format strings + serialized_keyword_args = {} + for keyword, events in self.keyword_args.items(): + serialized_events = [] + for event in events: + # Assuming each event is a tuple of (start_date, end_date, id) + start_date = event[0].isoformat() if isinstance(event[0], datetime) else event[0] + end_date = event[1].isoformat() if isinstance(event[1], datetime) else event[1] + serialized_events.append((start_date, end_date, event[2])) + serialized_keyword_args[keyword] = serialized_events + + return { + 'id': self.id, + 'launch_date': self.launch_date.isoformat(), + 'duration_years': self.duration_years, + 'predicted_date': self.predicted_date.isoformat(), + 'keyword_args': serialized_keyword_args, + 'timestamp': self.timestamp.isoformat(), + 'description': self.description + } + + @classmethod + def from_dict(cls, data): + # Deserialize keyword_args dates from ISO format strings + keyword_args = {} + for keyword, events in data.get('keyword_args', {}).items(): + deserialized_events = [] + for event in events: + # Convert string dates back to datetime objects + start_date = datetime.fromisoformat(event[0]) if isinstance(event[0], str) else event[0] + end_date = datetime.fromisoformat(event[1]) if isinstance(event[1], str) else event[1] + deserialized_events.append((start_date, end_date, event[2])) + keyword_args[keyword] = deserialized_events + + return cls( + launch_date=data['launch_date'], + duration_years=data['duration_years'], + predicted_date=data['predicted_date'], + keyword_args=keyword_args, + prediction_id=data['id'], + timestamp=datetime.fromisoformat(data['timestamp']), + description=data.get('description') + ) + + def __repr__(self): + return (f"Prediction(id={self.id}, launch_date={self.launch_date}, " + f"duration_years={self.duration_years}, predicted_date={self.predicted_date}, " + f"timestamp={self.timestamp}, description={repr(self.description)})") + +class PredictionStorage: + def __init__(self, filename=None): + self.filename = filename + self.predictions = [] + if filename: + self.load_predictions() + + def load_predictions(self): + """Load predictions from file.""" + if not self.filename: + return + + try: + with open(self.filename, 'r') as f: + data = json.load(f) + self.predictions = [Prediction.from_dict(pred) for pred in data] + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error reading predictions file {self.filename}: {e}") + self.predictions = [] + + def save_predictions(self): + """Save predictions to file.""" + if not self.filename: + return + + try: + with open(self.filename, 'w') as f: + json.dump([pred.to_dict() for pred in self.predictions], f, indent=4) + except Exception as e: + print(f"Error writing to predictions file {self.filename}: {e}") + + def add_prediction(self, launch_date, duration_years, predicted_date, keyword_args=None, description=None): + """Add a new prediction.""" + prediction = Prediction( + launch_date=launch_date, + duration_years=duration_years, + predicted_date=predicted_date, + keyword_args=keyword_args, + description=description + ) + self.predictions.append(prediction) + self.save_predictions() + return prediction + + def get_prediction_by_id(self, prediction_id): + """Get a prediction by its ID.""" + return next((pred for pred in self.predictions if pred.id == prediction_id), None) + + def list_predictions(self): + """List all predictions.""" + return self.predictions + + def search_predictions(self, start_date=None, end_date=None, keyword=None): + """Search predictions by date range or keyword.""" + results = self.predictions + + if start_date: + start_dt = start_date if isinstance(start_date, datetime) else datetime.fromisoformat(start_date) + results = [p for p in results if p.predicted_date >= start_dt] + + if end_date: + end_dt = end_date if isinstance(end_date, datetime) else datetime.fromisoformat(end_date) + results = [p for p in results if p.predicted_date <= end_dt] + + if keyword: + results = [p for p in results if keyword in p.keyword_args] + + return results + + def delete_prediction(self, prediction_id): + """Delete a prediction by ID.""" + prediction = self.get_prediction_by_id(prediction_id) + if prediction: + self.predictions.remove(prediction) + self.save_predictions() + return prediction + return None + + def update_prediction_description(self, prediction_id, description): + """Update a prediction's description.""" + prediction = self.get_prediction_by_id(prediction_id) + if prediction: + prediction.description = description + self.save_predictions() + return prediction + return None \ No newline at end of file diff --git a/test.py b/test.py index a2812c2..73225ce 100644 --- a/test.py +++ b/test.py @@ -1,40 +1,93 @@ -# test.py -from date_calculator import DateCalculator -from calendar_manager import CalendarManager -from prediction_controller import PredictionController - - - -def test_calculate_prediction(): - # Input event dates as strings - event1_str = ["2023-01-01", "2023-01-10"] - event2_str = ["2023-01-05", "2023-01-15"] - - # Input full project dates as strings - project1_str = ["2025-01-02", "2025-02-11"] - - # Input half project dates as strings - project2_str = ["2025-02-01", "2025-06-30"] - project3_str = ["2024-05-05", "2024-06-07"] - - date_calculator = DateCalculator() - calendar_manager = CalendarManager() - prediction_controller = PredictionController(calendar_manager, date_calculator, ["full_project", "half_project", "event"]) - - calendar_manager.add_entry(event1_str[0], event1_str[1], "event") - calendar_manager.add_entry(event2_str[0], event2_str[1], "event") - calendar_manager.add_entry(project1_str[0], project1_str[1], "full_project") - calendar_manager.add_entry(project2_str[0], project2_str[1], "half_project") - calendar_manager.add_entry(project3_str[0], project3_str[1], "half_project") - - # Set launch date and duration - prediction_controller.make_prediction("2023-01-01", 2) - - prediction = prediction_controller.get_prediction() - - # Output the result - print(f"Predicted completion date: {prediction}") - print(calendar_manager.list_entries()) - -# Run the test -test_calculate_prediction() \ No newline at end of file +# test.py +from date_calculator import DateCalculator +from calendar_manager import CalendarManager +from prediction_controller import PredictionController +from prediction_storage import PredictionStorage + +def test_updated_functionality(): + # Create necessary objects + date_calculator = DateCalculator() + calendar_manager = CalendarManager() + prediction_storage = PredictionStorage("predictions.json") + prediction_controller = PredictionController( + calendar_manager, + date_calculator, + ["full_project", "half_project", "event"], + prediction_storage + ) + + # Add calendar entries with commentary + calendar_manager.add_entry( + "2023-01-01", + "2023-01-10", + "event", + commentary="Annual company meeting" + ) + + calendar_manager.add_entry( + "2023-01-05", + "2023-01-15", + "event", + commentary="Product launch conference" + ) + + calendar_manager.add_entry( + "2025-01-02", + "2025-02-11", + "full_project", + commentary="Website redesign project" + ) + + # Test adding commentary to existing entry + entries = calendar_manager.list_entries() + project_entry = entries[2] # Get the third entry (full_project) + calendar_manager.add_commentary(project_entry.id, "Updated: Major redesign with new branding") + + # Make prediction with description + prediction_controller.make_prediction( + "2023-01-01", + 2, + description="Initial project timeline prediction" + ) + + # Make another prediction + prediction_controller.make_prediction( + "2023-02-01", + 1, + description="Revised timeline after scope changes" + ) + + # Retrieve and display stored predictions + all_predictions = prediction_controller.get_all_predictions() + print("\nAll stored predictions:") + for pred in all_predictions: + print(f"- {pred.id}: {pred.launch_date.strftime('%Y-%m-%d')} to {pred.predicted_date.strftime('%Y-%m-%d')} ({pred.duration_years} years)") + print(f" Description: {pred.description}") + + # Search predictions + if all_predictions: + # Update description of first prediction + first_pred_id = all_predictions[0].id + prediction_controller.update_prediction_description( + first_pred_id, + "Updated: Initial project timeline with adjusted parameters" + ) + + # Search by date range + date_filtered = prediction_controller.search_predictions( + start_date="2024-01-01", + end_date="2025-12-31" + ) + print("\nPredictions ending in 2024-2025:") + for pred in date_filtered: + print(f"- {pred.id}: ends on {pred.predicted_date.strftime('%Y-%m-%d')}") + + # Display calendar entries with commentary + print("\nCalendar entries with commentary:") + for entry in calendar_manager.list_entries(): + print(f"- {entry.keyword}: {entry.start_date.strftime('%Y-%m-%d')} to {entry.end_date.strftime('%Y-%m-%d')}") + if entry.commentary: + print(f" Commentary: {entry.commentary}") + +if __name__ == "__main__": + test_updated_functionality() \ No newline at end of file -- cgit v1.1