Compare commits

...
Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda 7274d670ec Remove the backup diary option on the diary settings screen 2025-07-24 02:03:52 -03:00
Gustavo Henrique Santos Souza de Miranda 62e084efb3 Add the feature to delete all the photos from a travel diary and also remove all the references of photos inside the entry 2025-07-24 01:54:24 -03:00
Gustavo Henrique Santos Souza de Miranda c5edb835b6 Add the feature to delete all the entries from a travel diary 2025-07-23 08:45:50 -03:00
Gustavo Henrique Santos Souza de Miranda ed89669431 Fix a circular import and passed the proper travel diary id 2025-07-23 08:44:32 -03:00
Gustavo Henrique Santos Souza de Miranda 8efda52d15 Add the delete all entries method the travel diary service 2025-07-23 08:42:00 -03:00
Gustavo Henrique Santos Souza de Miranda 4d6c92b4d0 Add the feature to delete the whole diary 2025-07-23 07:56:42 -03:00
Gustavo Henrique Santos Souza de Miranda 83fdb885a9 Add the feature to set the auto-open diary in the toml file 2025-07-23 07:27:35 -03:00
Gustavo Henrique Santos Souza de Miranda 0c6de1ea2e Add the call to the diary settings screen 2025-07-23 07:26:23 -03:00
Gustavo Henrique Santos Souza de Miranda ed8619fbf9 Add the get auto open diary method to the config manager 2025-07-23 07:18:50 -03:00
Gustavo Henrique Santos Souza de Miranda d9753a375f Add the Diary Settings screen and the CSS for it 2025-07-23 04:52:22 -03:00
Gustavo Henrique Santos Souza de Miranda d1afd3a7ef Add the css for the new modals 2025-07-23 04:22:51 -03:00
Gustavo Henrique Santos Souza de Miranda 40a2e727f8 Add the modals to delete the diary and the diary contents 2025-07-23 03:00:49 -03:00
Gustavo Henrique Miranda 08a5030846 Merge pull request #75 from gmbrax/test/application-test
Add the tests for the application class
2025-07-21 19:41:05 -03:00
Gustavo Henrique Miranda 050939b4b8 Merge pull request #76 from gmbrax/test/command-tests
Add the tests to the command file
2025-07-21 19:40:40 -03:00
Gustavo Henrique Santos Souza de Miranda 8bd03bcaca Add the tests to the command file 2025-07-21 19:36:37 -03:00
Gustavo Henrique Santos Souza de Miranda bcb76fbb7d Add the tests for the application class 2025-07-21 19:30:52 -03:00
Gustavo Henrique Miranda 0ab1e38726 Merge pull request #74 from gmbrax/test/photoservice-unit-tests
Add the tests for the update on photo service
2025-07-21 19:01:23 -03:00
Gustavo Henrique Santos Souza de Miranda 668e2825d9 Add the tests for the update on photo service 2025-07-21 18:55:28 -03:00
Gustavo Henrique Miranda 5c50cad207 Merge pull request #73 from gmbrax/test/directory-manager-unit-tests
Add the tests for the directory manager
2025-07-21 18:07:18 -03:00
Gustavo Henrique Miranda 3212607f70 Merge pull request #72 from gmbrax/test/config-manager-unit-tests
Add the tests for the config manager
2025-07-21 18:07:01 -03:00
Gustavo Henrique Miranda eafd9b9522 Merge pull request #71 from gmbrax/test/service-manager-unit-tests
Add the tests for the service manager
2025-07-21 18:06:36 -03:00
Gustavo Henrique Miranda 3e644a415f Merge pull request #70 from gmbrax/test/photoservice-unit-tests
Test/photoservice unit tests
2025-07-21 18:05:52 -03:00
Gustavo Henrique Santos Souza de Miranda ec0c8a6f71 Add the tests for the config manager 2025-07-21 17:43:39 -03:00
Gustavo Henrique Santos Souza de Miranda bf64255fe2 Add the tests for the service manager 2025-07-21 16:58:27 -03:00
Gustavo Henrique Santos Souza de Miranda 32f363f15a Add the tests for the delete photo service and also fixed the missing import 2025-07-21 16:48:09 -03:00
Gustavo Henrique Santos Souza de Miranda 65cf7b04f3 Merge remote-tracking branch 'origin/test/photoservice-unit-tests' into test/photoservice-unit-tests
# Conflicts:
#	tests/conftest.py
#	tests/service/test_photo_service.py
2025-07-21 16:31:59 -03:00
Gustavo Henrique Santos Souza de Miranda 44824cd690 Add the tests for the update photo service and also improved the fixture 2025-07-21 16:30:54 -03:00
Gustavo Henrique Santos Souza de Miranda 32b1c24846 Add the tests for the read methods for the photo service 2025-07-21 16:19:10 -03:00
Gustavo Henrique Santos Souza de Miranda 5d0d4fc5ac Add the tests for the hash methods and the create methods 2025-07-21 16:19:09 -03:00
Gustavo Henrique Santos Souza de Miranda 5f20bd3624 Add the tests for the read methods for the photo service 2025-07-21 02:48:04 -03:00
Gustavo Henrique Santos Souza de Miranda 2f81e4ff06 Add the tests for the hash methods and the create methods 2025-07-21 02:28:04 -03:00
17 changed files with 1254 additions and 4 deletions
+22
View File
@@ -1,3 +1,4 @@
import re
from datetime import datetime from datetime import datetime
from typing import List from typing import List
@@ -54,3 +55,24 @@ class EntryService:
self.session.commit() self.session.commit()
return excluded return excluded
return None return None
def delete_references_for_specific_photo(self, entry: Entry, photo_hash: str) -> Entry:
regex = r"\[\[photo::" + re.escape(photo_hash) + r"\]\]"
entry.text = re.sub(regex, lambda match: ' ' * len(match.group(0)), entry.text)
self.session.commit()
self.session.refresh(entry)
return entry
def delete_all_photo_references(self, entry: Entry, commit=True) -> Entry:
if not entry.photos:
return entry
photo_hashes = {photo.photo_hash[:8] for photo in entry.photos}
regex = r"\[\[photo::(" + "|".join(re.escape(h) for h in photo_hashes) + r")\]\]"
entry.text = re.sub(regex, lambda match: ' ' * len(match.group(0)), entry.text)
if commit:
self.session.commit()
self.session.refresh(entry)
return entry
+2 -1
View File
@@ -141,7 +141,7 @@ class PhotoService:
return original return original
return None return None
def delete(self, photo_src: Photo) -> Photo | None: def delete(self, photo_src: Photo, commit=True) -> Photo | None:
excluded = self.read_by_id(photo_src.id) excluded = self.read_by_id(photo_src.id)
if excluded: if excluded:
# Store photo data before deletion # Store photo data before deletion
@@ -162,6 +162,7 @@ class PhotoService:
file_path.unlink() file_path.unlink()
self.session.delete(excluded) self.session.delete(excluded)
if commit:
self.session.commit() self.session.commit()
return deleted_photo return deleted_photo
@@ -3,12 +3,16 @@ import re
import shutil import shutil
from pathlib import Path from pathlib import Path
from pilgrim.models.entry import Entry
from pilgrim.utils import DirectoryManager from pilgrim.utils import DirectoryManager
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from pilgrim.models.travel_diary import TravelDiary from pilgrim.models.travel_diary import TravelDiary
from unidecode import unidecode from unidecode import unidecode
from pilgrim.service.photo_service import PhotoService
from pilgrim.service.entry_service import EntryService
class TravelDiaryService: class TravelDiaryService:
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
@@ -147,3 +151,32 @@ class TravelDiaryService:
self.session.rollback() self.session.rollback()
raise ValueError(f"Could not delete diary: {str(e)}") raise ValueError(f"Could not delete diary: {str(e)}")
return None return None
def delete_all_entries(self,travel_diary: TravelDiary):
diary = self.read_by_id(travel_diary.id)
if diary is not None:
diary.entries = []
self.session.commit()
return True
return False
def delete_all_photos(self,travel_diary: TravelDiary):
diary = self.read_by_id(travel_diary.id)
photo_service = PhotoService(self.session)
entry_service = EntryService(self.session)
if diary is not None:
for entry in list(diary.entries):
entry_service.delete_all_photo_references(entry,commit=False)
for photo in list(diary.photos):
photo_service.delete(photo,commit=False)
self.session.commit()
return True
return False
@@ -9,6 +9,7 @@ from textual.containers import Vertical, Container, Horizontal
from pilgrim.models.travel_diary import TravelDiary from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.about_screen import AboutScreen from pilgrim.ui.screens.about_screen import AboutScreen
from pilgrim.ui.screens.diary_settings_screen import SettingsScreen
from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal
from pilgrim.ui.screens.new_diary_modal import NewDiaryModal from pilgrim.ui.screens.new_diary_modal import NewDiaryModal
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
@@ -23,6 +24,7 @@ class DiaryListScreen(Screen):
Binding("enter", "open_selected_diary", "Open diary"), Binding("enter", "open_selected_diary", "Open diary"),
Binding("e", "edit_selected_diary", "Edit diary"), Binding("e", "edit_selected_diary", "Edit diary"),
Binding("r", "force_refresh", "Force refresh"), Binding("r", "force_refresh", "Force refresh"),
Binding("s", "diary_settings", "Open The Selected Diary Settings"),
] ]
def __init__(self): def __init__(self):
@@ -286,3 +288,13 @@ class DiaryListScreen(Screen):
def action_quit(self): def action_quit(self):
"""Action to quit the application""" """Action to quit the application"""
self.app.exit() self.app.exit()
def action_diary_settings(self):
if self.selected_diary_index is not None:
diary_id = self.diary_id_map.get(self.selected_diary_index)
if diary_id:
self.app.push_screen(SettingsScreen(diary_id=diary_id))
else:
self.notify("Invalid diary ID")
else:
self.notify("Select a diary to open the settings")
@@ -0,0 +1,274 @@
from textual.widgets import Static
from textual.containers import Container
from textual.widgets import Header, Footer, Label, Button,Checkbox,Input
from textual.screen import Screen
from textual.reactive import reactive
from textual.binding import Binding
from textual import on
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.modals.delete_all_entries_from_diary_modal import DeleteAllEntriesModal
from pilgrim.ui.screens.modals.delete_all_photos_from_diary_modal import DeleteAllPhotosModal
from pilgrim.ui.screens.modals.delete_diary_modal import DeleteDiaryModal
class SettingsScreen(Screen):
is_changed = reactive(False)
BINDINGS = [
Binding("escape","cancel","Cancel"),
]
def __init__(self,diary_id:int):
super().__init__()
self.current_diary = self.app.service_manager.get_travel_diary_service().read_by_id(diary_id)
self.header = Header()
self.footer = Footer()
self.title = "Settings"
self.diary_name = Static(self.current_diary.name,id="DiarySettingsScreen-DiaryName")
self.notify(str(self.app.config_manager))
self.is_the_diary_set_to_auto_open = self.app.config_manager.get_auto_open_diary() == self.current_diary.name
self.diary_entry_count = Static(str(len(self.current_diary.entries)))
self.diary_photo_count = Static(str(len(self.current_diary.photos)))
self.save_button = Button("Save",id="DiarySettingsScreen-SaveButton" )
self.cancel_button = Button("Cancel",id="DiarySettingsScreen-cancel_button")
self.apply_button = Button("Apply",id="DiarySettingsScreen-ApplyButton")
self.delete_diary_button = Button("Delete Diary",id="DiarySettingsScreen-DeleteDiaryButton")
self.delete_all_entries_button = Button("Delete All Entries",id="DiarySettingsScreen-DeleteAllEntriesButton")
self.delete_all_photos_button = Button("Delete All Photos",id="DiarySettingsScreen-DeleteAllPhotosButton")
self.set_auto_open_to_this_diary = Checkbox(id="set_auto_open_to_this_diary",value=self.is_the_diary_set_to_auto_open)
self.delete_diary_button_container = Container(
Label("Delete Diary:"),
self.delete_diary_button,
id="DiarySettingsScreen-DeleteDiaryButtonContainer",
classes="DiarySettingsScreen-DeleteDiaryButtonContainer Button_Container"
)
self.delete_all_entries_button_container = Container(
Label("Delete All Entries:"),
self.delete_all_entries_button,
id="DiarySettingsScreen-DeleteAllEntriesButtonContainer",
classes="DiarySettingsScreen-DeleteAllEntriesButtonContainer Button_Container"
)
self.delete_all_photos_button_container = Container(
Label("Delete All Photos:"),
self.delete_all_photos_button,
id="DiarySettingsScreen-DeleteAllPhotosButtonContainer",
classes="DiarySettingsScreen-DeleteAllPhotosButtonContainer Button_Container"
)
self.diary_name_container = Container(
Label("Diary Name:"),
self.diary_name,
id="DiarySettingsScreen-DiaryNameContainer",
classes="DiarySettingsScreen-DiaryNameContainer Data_Container"
)
self.diary_entry_count_container = Container(
Label("Diary Entries:"),
self.diary_entry_count,
id="DiarySettingsScreen-DiaryEntryCountContainer",
classes="DiarySettingsScreen-DiaryEntryCountContainer Data_Container"
)
self.set_auto_open_to_this_diary_container = Container(
Label("Set Open This Diary On App Start?:"),
self.set_auto_open_to_this_diary,
id="DiarySettingsScreen-SetAutoOpenToThisDiaryContainer",
classes="DiarySettingsScreen-SetAutoOpenToThisDiaryContainer Data_Container"
)
self.diary_photo_count_container = Container(
Label("Diary Photos:"),
self.diary_photo_count,
id="DiarySettingsScreen-DiaryPhotoCountContainer",
classes="DiarySettingsScreen-DiaryPhotoCountContainer Data_Container"
)
self.diary_info_container = Container(
self.diary_name_container,
self.diary_entry_count_container,
self.diary_photo_count_container,
self.set_auto_open_to_this_diary_container,
id="DiarySettingsScreen-DiaryInfoContainer",
classes="DiarySettingsScreen-DiaryInfoContainer",
)
self.diary_denger_zone_container = Container(
self.backup_diary_button_container,
self.delete_diary_button_container,
self.delete_all_entries_button_container,
self.delete_all_photos_button_container,
id="DiarySettingsScreen-DiaryDengerZoneContainer",
classes="DiarySettingsScreen-DiaryDengerZoneContainer"
)
self.button_container = Container(
self.save_button,
self.apply_button,
self.cancel_button,
id="DiarySettingsScreen-ButtonContainer",
classes="DiarySettingsScreen-ButtonContainer"
)
self.main = Container(
self.diary_info_container,
self.diary_denger_zone_container,
self.button_container,
id="DiarySettingsScreen-MainContainer",
classes="DiarySettingsScreen-MainContainer"
)
self.diary_info_container.border_title = "Diary Info"
self.diary_denger_zone_container.border_title = "Denger Zone"
@on(Checkbox.Changed, "#set_auto_open_to_this_diary")
def on_checkbox_changed(self, event):
self.is_changed = not self.is_changed
@on(Button.Pressed, "#DiarySettingsScreen-cancel_button")
def on_cancel_button_pressed(self, event):
self.action_cancel()
@on(Button.Pressed, "#DiarySettingsScreen-DeleteDiaryButton")
def on_delete_diary_button_pressed(self, event):
self.app.push_screen(DeleteDiaryModal(diary_id=self.current_diary.id,diary_name=self.current_diary.name))
@on(Button.Pressed, "#DiarySettingsScreen-DeleteAllEntriesButton")
def on_delete_all_entries_button_pressed(self, event):
self.app.push_screen(DeleteAllEntriesModal(diary_id=self.current_diary.id))
@on(Button.Pressed, "#DiarySettingsScreen-DeleteAllPhotosButton")
def on_delete_all_photos_button_pressed(self, event):
self.app.push_screen(DeleteAllPhotosModal(diary_id=self.current_diary.id))
def action_cancel(self):
if self.is_changed:
self.notify("Cancel button pressed, but changes are not saved",severity="error")
return
self.dismiss()
@on(Button.Pressed, "#DiarySettingsScreen-SaveButton")
def on_save_button_pressed(self, event):
self.action_save()
@on(Button.Pressed, "#DiarySettingsScreen-ApplyButton")
def on_apply_button_pressed(self, event):
self.action_apply()
def watch_is_changed(self, value):
label = self.set_auto_open_to_this_diary_container.query_one(Label)
if value:
label.add_class("DiarySettingsScreen-SetAutoOpenToThisDiaryContainer-Not-Saved-Label")
else:
label.remove_class("DiarySettingsScreen-SetAutoOpenToThisDiaryContainer-Not-Saved-Label")
def compose(self):
yield Header()
yield self.main
yield Footer()
def on_mount(self):
if self.app.config_manager.get_auto_open_diary() == self.current_diary.name:
self.call_after_refresh(self.set_checkbox_state)
def set_checkbox_state(self):
self.set_auto_open_to_this_diary.value = True
def _set_auto_open_diary(self,value):
self.app.config_manager.set_auto_open_diary(value)
self.app.config_manager.save_config()
self.is_changed = False
def _get_auto_open_diary(self):
return self.app.config_manager.get_auto_open_diary()
def _make_auto_open_diary_value(self):
value = None
if self.set_auto_open_to_this_diary.value:
value = self.current_diary.name
return value
def action_save(self):
if not self.is_changed:
self.dismiss()
return
value = self._make_auto_open_diary_value()
current_auto_open = self._get_auto_open_diary()
if current_auto_open is None:
self._set_auto_open_diary(value)
self.notify("Settings saved")
self.dismiss()
return
if current_auto_open == self.current_diary.name:
if value is None:
self._set_auto_open_diary(None)
self.notify("Auto-open disabled")
else:
self.is_changed = False
self.notify("No changes made")
self.dismiss()
return
if value is not None:
self._set_auto_open_diary(value)
self.notify(f"Auto-open changed from '{current_auto_open}' to '{self.current_diary.name}'")
self.dismiss()
else:
self.is_changed = False
self.notify("No changes made")
self.dismiss()
def action_apply(self):
if not self.is_changed:
return
value = self._make_auto_open_diary_value()
current_auto_open = self._get_auto_open_diary()
if current_auto_open is None:
self._set_auto_open_diary(value)
self.notify("Settings applied")
return
if current_auto_open == self.current_diary.name:
if value is None:
self._set_auto_open_diary(None)
self.notify("Auto-open disabled")
else:
self.is_changed = False
self.notify("No changes made")
return
if value is not None:
self._set_auto_open_diary(value)
self.notify(f"Auto-open changed from '{current_auto_open}' to '{self.current_diary.name}'")
else:
self.is_changed = False
self.notify("No changes made")
@@ -0,0 +1,33 @@
from textual.widgets import Button
from textual import on
from pilgrim.ui.screens.modals.delete_yes_confirmation_modal import DeleteYesConfirmationModal
class DeleteAllEntriesModal(DeleteYesConfirmationModal):
def __init__(self,diary_id:int):
super().__init__(diary_id)
self.head_text.update("Are you sure you want to delete all entries from this diary?")
self.delete_button.add_class("DeleteDiaryModal-DeleteButton")
@on(Button.Pressed, ".DeleteDiaryModal-DeleteButton")
def on_delete_button_pressed(self, event):
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
self.result = True
self._delete_entries()
self.dismiss()
self.app.push_screen(DiaryListScreen())
def _delete_entries(self):
diary = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id)
if self.app.service_manager.get_travel_diary_service().delete_all_entries(diary):
self.notify("All entries deleted successfully")
else:
self.notify("Failed to delete all entries")
@@ -0,0 +1,32 @@
from textual.widgets import Button
from textual import on
from pilgrim.ui.screens.modals.delete_yes_confirmation_modal import DeleteYesConfirmationModal
class DeleteAllPhotosModal(DeleteYesConfirmationModal):
def __init__(self,diary_id:int):
super().__init__(diary_id)
self.head_text.update("Are you sure you want to delete all photos from this diary?")
self.delete_button.add_class("DeleteDiaryModal-DeleteButton")
@on(Button.Pressed, ".DeleteDiaryModal-DeleteButton")
def on_delete_button_pressed(self, event):
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
self.result = True
self._delete_all_photo()
self.dismiss()
self.app.push_screen(DiaryListScreen())
def _delete_all_photo(self):
diary = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id)
if self.app.service_manager.get_travel_diary_service().delete_all_photos(diary):
self.notify("All photos deleted successfully")
else:
self.notify("Failed to delete all photos")
@@ -0,0 +1,77 @@
from textual.containers import Container
from textual.widgets import Header, Footer, Label, Button,Input
from textual.screen import Screen
from textual.binding import Binding
from textual import on
class DeleteDiaryModal(Screen):
BINDINGS = [
Binding("escape","cancel","Cancel"),
]
def __init__(self, diary_id: int,diary_name:str):
super().__init__()
self.diary_id = diary_id
self.diary_name = diary_name
self.user_input = Input(placeholder=f"Type diary name to confirm: ({self.diary_name})",id="DeleteDiaryModal-UserInput")
self.delete_button = Button("Delete Diary",id="DeleteDiaryModal-DeleteButton",disabled=True)
self.cancel_button = Button("Cancel",id="DeleteDiaryModal-CancelButton")
self.result = None
def compose(self):
yield Header()
yield Container(
Label("Are you sure you want to delete this diary?"),
self.user_input,
Container(
self.delete_button,
self.cancel_button,
id="DeleteDiaryModal-ButtonContainer",
classes="DeleteDiaryModal-ButtonContainer"
),
id="DeleteDiaryModal-MainContainer",
classes="DeleteDiaryModal-MainContainer"
)
yield Footer()
@on(Input.Changed,"#DeleteDiaryModal-UserInput")
def on_user_input_changed(self, event):
input_text = event.value.strip()
if input_text == self.diary_name:
self.delete_button.disabled = False
else:
self.delete_button.disabled = True
@on(Button.Pressed,"#DeleteDiaryModal-DeleteButton")
def on_delete_button_pressed(self, event):
self.result = True
self._delete_diary()
self.dismiss()
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
self.app.push_screen(DiaryListScreen())
@on(Button.Pressed,"#DeleteDiaryModal-CancelButton")
def on_cancel_button_pressed(self, event):
self.action_cancel()
def action_cancel(self):
self.dismiss()
def _delete_diary(self):
diary = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id)
self.app.service_manager.get_travel_diary_service().delete(diary)
if self.app.config_manager.get_auto_open_diary() == self.diary_name:
self.app.config_manager.set_auto_open_diary(None)
self.app.config_manager.save_config()
@@ -0,0 +1,57 @@
from textual.containers import Container
from textual.widgets import Header, Footer, Label, Button,Input
from textual.screen import Screen
from textual.binding import Binding
from textual import on
class DeleteYesConfirmationModal(Screen):
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
]
def __init__(self,diary_id:int):
super().__init__()
self.diary_id = diary_id
self.user_input = Input(placeholder="Type 'Yes, I do ' to confirm",id="DeleteYesConfirmationModal-UserInput")
self.delete_button = Button("Delete",id="DeleteYesConfirmationModal-DeleteButton",disabled=True)
self.cancel_button = Button("Cancel",id="DeleteYesConfirmationModal-CancelButton")
self.head_text = Label("Are you sure you want to delete this diary?",id="DeleteYesConfirmationModal-HeadText")
self.second_head_text = Label("This action cannot be undone.",id="DeleteYesConfirmationModal-SecondHeadText")
self.delete_modal_container = Container(
self.head_text,
self.second_head_text,
self.user_input,
Container(
self.delete_button,
self.cancel_button,
id="DeleteYesConfirmationModal-DeleteButtonContainer",
classes="DeleteYesConfirmationModal-DeleteButtonContainer"
),
id="DeleteYesConfirmationModal-DeleteModalContainer",
classes="DeleteYesConfirmationModal-DeleteModalContainer"
)
self.result = None
@on(Input.Changed,"#DeleteYesConfirmationModal-UserInput")
def on_user_input_changed(self, event):
input_text = event.value.strip()
if input_text == "Yes, I do":
self.delete_button.disabled = False
else:
self.delete_button.disabled = True
@on(Button.Pressed,"#DeleteYesConfirmationModal-CancelButton")
def on_cancel_button_pressed(self, event):
self.action_cancel()
def action_cancel(self):
from pilgrim.ui.screens.diary_settings_screen import SettingsScreen
self.dismiss()
self.app.push_screen(SettingsScreen(self.diary_id))
def compose(self):
yield Header()
yield Footer()
yield self.delete_modal_container
+263 -1
View File
@@ -1,7 +1,8 @@
Screen { Screen {
layout: vertical; layout: vertical;
background: $surface-darken-1; background: $primary-background-darken-3;
align: center middle; align: center middle;
hatch: right $secondary-background-darken-3;
} }
.EditEntryScreen-sub-header { .EditEntryScreen-sub-header {
@@ -624,3 +625,264 @@ Screen.-modal {
margin: 0 1; margin: 0 1;
width: 1fr; width: 1fr;
} }
.DeleteYesConfirmationModal-DeleteModalContainer,
.DeleteDiaryModal-MainContainer {
align: center middle;
layout: vertical;
margin: 2;
padding: 2;
background: $primary-background;
height: auto;
width: auto;
min-width: 80%;
max-width: 95%;
border: solid $primary;
}
/* Labels de texto */
.DeleteYesConfirmationModal-DeleteModalContainer > Label,
.DeleteDiaryModal-MainContainer > Label {
margin: 1 0;
padding: 0 1;
color: $error;
text-align: center;
width: 100%;
}
/* Input fields */
.DeleteYesConfirmationModal-DeleteModalContainer > Input,
.DeleteDiaryModal-MainContainer > Input {
margin: 1 0;
width: 100%;
height: 3;
}
/* Container dos botões */
.DeleteYesConfirmationModal-DeleteButtonContainer,
.DeleteDiaryModal-ButtonContainer {
layout: horizontal;
align: center middle;
margin: 2 0 0 0;
height: auto;
width: 100%;
padding: 0;
}
/* Botões individuais */
.DeleteYesConfirmationModal-DeleteButtonContainer > Button,
.DeleteDiaryModal-ButtonContainer > Button {
width: 45%;
margin: 0 1;
height: 3;
}
/* Botão de delete (primeiro botão) */
.DeleteYesConfirmationModal-DeleteButtonContainer > Button:first-child,
.DeleteDiaryModal-ButtonContainer > Button:first-child {
background: $error-darken-1;
}
/* Botão de cancel */
.DeleteYesConfirmationModal-DeleteButtonContainer > Button:last-child,
.DeleteDiaryModal-ButtonContainer > Button:last-child {
background: $surface;
}
/* Estados disabled para botões de delete */
.DeleteYesConfirmationModal-DeleteButtonContainer > Button:first-child:disabled,
.DeleteDiaryModal-ButtonContainer > Button:first-child:disabled {
background: $surface-lighten-1;
color: $text-muted;
}
/* Espaçamento específico para labels secundários */
#DeleteYesConfirmationModal-SecondHeadText {
margin: 1 0;
padding: 0 1;
color: $warning;
text-align: center;
text-style: italic;
}
.Data_Container{
width: 100%;
layout: grid;
grid-size: 2 1; /* 2 colunas, 1 linha */
height:auto;
}
.Button_Container{
width: 100%;
layout: grid;
grid-size: 2 2;
height: auto;
}
.DiarySettingsScreen-DeleteDiaryButtonContainer{
grid-size: 2 1;
padding: 0 1;
content-align: center middle;
padding-bottom:1;
}
.DiarySettingsScreen-DeleteAllEntriesButtonContainer{
margin:0;
padding: 0 1;
height:auto;
padding-bottom:1;
}
.DiarySettingsScreen-DeleteAllPhotosButtonContainer{
margin:0;
padding: 0 1;
height:auto;
padding-bottom:1;
}
.DiarySettingsScreen-DeleteAllPhotosButtonContainer > Label,
.DiarySettingsScreen-DeleteAllEntriesButtonContainer > Label{
color: $error-lighten-3;
padding:1;
}
.DiarySettingsScreen-BackupDiaryButtonContainer{
margin:0;
padding:0 1;
padding-bottom:1
}
.DiarySettingsScreen-BackupDiaryButtonContainer > Label{
padding:1;
color: $success-darken-1;
}
.DiarySettingsScreen-BackupDiaryButtonContainer > Button{
background: $success;
}
.DiarySettingsScreen-DeleteDiaryButtonContainer > Label{
color: $error-lighten-3;
content-align: left middle;
padding:1
}
.DiarySettingsScreen-DeleteAllPhotosButtonContainer > Button,
.DiarySettingsScreen-DeleteAllEntriesButtonContainer > Button,
.DiarySettingsScreen-DeleteDiaryButtonContainer Button{
background: $error;
}
.DiarySettingsScreen-MainContainer{
align: center top;
layout: vertical;
margin:1;
padding:1;
background: $primary-background
}
.DiarySettingsScreen-DiaryInfoContainer{
padding:1 2;
border:round grey;
height:auto;
width: 90%;
padding-bottom: 0;
}
.DiarySettingsScreen-ButtonContainer{
height:auto;
layout: grid;
grid-size: 3 1;
grid-gutter:2;
dock:bottom;
}
.DiarySettingsScreen-ButtonContainer > Button:first-child {
margin-left:2
}
.DiarySettingsScreen-ButtonContainer > Button:last-child {
margin-right:2
}
.DiarySettingsScreen-ButtonContainer > Button {
width: 100%;
}
#DiarySettingsScreen-DiaryPhotoCountContainer > Static:first-child,
#DiarySettingsScreen-DiaryEntryCountContainer > Static:first-child,
#DiarySettingsScreen-DiaryNameContainer > Static:first-child{
text-align: left;
padding: 0 1;
padding-bottom:1
}
#DiarySettingsScreen-DiaryPhotoCountContainer > Static:last-child,
#DiarySettingsScreen-DiaryEntryCountContainer > Static:last-child,
#DiarySettingsScreen-DiaryNameContainer > Static:last-child{
text-align: right;
padding:0 1
}
.DiarySettingsScreen-SetAutoOpenToThisDiaryContainer > Checkbox{
margin:0;
padding:1;
background: $primary-background;
border:none;
}
.DiarySettingsScreen-SetAutoOpenToThisDiaryContainer-Not-Saved-Label{
text-style:bold;
color:$warning-lighten-2;
}
.DiarySettingsScreen-DiaryDengerZoneContainer{
border: round $error-darken-1;
width: 90%;
padding: 0 1;
height: auto;
padding-bottom: 0;
}
+5
View File
@@ -46,6 +46,8 @@ class ConfigManager(metaclass=SingletonMeta):
if self.__data["settings"]["diary"]["auto_open_diary_on_startup"] == "": if self.__data["settings"]["diary"]["auto_open_diary_on_startup"] == "":
self.auto_open_diary = None self.auto_open_diary = None
else:
self.auto_open_diary = self.__data["settings"]["diary"]["auto_open_diary_on_startup"]
self.auto_open_new_diary = self.__data["settings"]["diary"]["auto_open_on_creation"] self.auto_open_new_diary = self.__data["settings"]["diary"]["auto_open_on_creation"]
else: else:
print("Error: config.toml not found.") print("Error: config.toml not found.")
@@ -103,5 +105,8 @@ class ConfigManager(metaclass=SingletonMeta):
def set_auto_open_diary(self, value: str): def set_auto_open_diary(self, value: str):
self.auto_open_diary = value self.auto_open_diary = value
def get_auto_open_diary(self):
return self.auto_open_diary
def set_auto_open_new_diary(self, value: bool): def set_auto_open_new_diary(self, value: bool):
self.auto_open_new_diary = value self.auto_open_new_diary = value
+28
View File
@@ -4,6 +4,7 @@ from sqlalchemy.orm import sessionmaker
from pilgrim.database import Base from pilgrim.database import Base
from pilgrim.models.travel_diary import TravelDiary from pilgrim.models.travel_diary import TravelDiary
from pilgrim.models.photo import Photo
# Todos os imports necessários para as fixtures devem estar aqui # Todos os imports necessários para as fixtures devem estar aqui
# ... # ...
@@ -34,3 +35,30 @@ def session_with_one_diary(db_session):
db_session.commit() db_session.commit()
db_session.refresh(diary) db_session.refresh(diary)
return db_session, diary return db_session, diary
@pytest.fixture
def session_with_photos(session_with_one_diary):
session, diary = session_with_one_diary
# Usamos a mesma raiz de diretório que o mock do teste espera
diaries_root = "/fake/diaries_root"
photo1 = Photo(
# CORREÇÃO: O caminho agora inclui a raiz e a subpasta 'images'
filepath=f"{diaries_root}/{diary.directory_name}/images/p1.jpg",
name="Foto 1",
photo_hash="hash1",
fk_travel_diary_id=diary.id
)
photo2 = Photo(
filepath=f"{diaries_root}/{diary.directory_name}/images/p2.jpg",
name="Foto 2",
photo_hash="hash2",
fk_travel_diary_id=diary.id
)
session.add_all([photo1, photo2])
session.commit()
return session, [photo1, photo2]
+249
View File
@@ -0,0 +1,249 @@
import pytest
from pathlib import Path
from pilgrim import TravelDiary
from pilgrim.service.photo_service import PhotoService
import hashlib
from unittest.mock import patch
from pilgrim.models.photo import Photo
from pilgrim.utils import DirectoryManager
@patch.object(PhotoService, '_copy_photo_to_diary')
@patch.object(PhotoService, 'hash_file', return_value="fake_hash_123")
def test_create_photo_successfully(mock_hash, mock_copy, session_with_one_diary):
session, diary = session_with_one_diary
service = PhotoService(session)
fake_source_path = Path("/path/original/imagem.jpg")
fake_copied_path = Path(f"~/.pilgrim/diaries/{diary.directory_name}/images/imagem.jpg")
mock_copy.return_value = fake_copied_path
new_photo = service.create(
filepath=fake_source_path,
name="Foto da Praia",
travel_diary_id=diary.id,
caption="Pôr do sol")
mock_hash.assert_called_once_with(fake_source_path)
mock_copy.assert_called_once_with(fake_source_path, diary)
assert new_photo is not None
assert new_photo.name == "Foto da Praia"
assert new_photo.photo_hash == "fake_hash_123"
assert new_photo.filepath == str(fake_copied_path)
def test_hash_file_generates_correct_hash(tmp_path: Path):
original_content_bytes = b"um conteudo de teste para o hash"
file_on_disk = tmp_path / "test.jpg"
file_on_disk.write_bytes(original_content_bytes)
hash_from_file = PhotoService.hash_file(file_on_disk)
expected_hash_func = hashlib.new('sha3_384')
expected_hash_func.update(original_content_bytes)
hash_from_memory = expected_hash_func.hexdigest()
assert hash_from_file == hash_from_memory
@patch.object(PhotoService, '_copy_photo_to_diary')
@patch.object(PhotoService, 'hash_file', return_value="hash_ja_existente")
def test_create_photo_returns_none_if_hash_exists(mock_hash, mock_copy, session_with_one_diary):
session, diary = session_with_one_diary
existing_photo = Photo(
filepath="/path/existente.jpg", name="Foto Antiga",
photo_hash="hash_ja_existente", fk_travel_diary_id=diary.id
)
session.add(existing_photo)
session.commit()
service = PhotoService(session)
new_photo = service.create(
filepath=Path("/path/novo/arquivo.jpg"),
name="Foto Nova",
travel_diary_id=diary.id
)
assert new_photo is None
mock_copy.assert_not_called()
def test_read_by_id_successfully(session_with_photos):
session, photos = session_with_photos
service = PhotoService(session)
photo_to_find_id = photos[0].id
found_photo = service.read_by_id(photo_to_find_id)
assert found_photo is not None
assert found_photo.id == photo_to_find_id
assert found_photo.name == "Foto 1"
def test_read_by_id_returns_none_for_invalid_id(db_session):
service = PhotoService(db_session)
result = service.read_by_id(999)
assert result is None
def test_read_all_returns_all_photos(session_with_photos):
session, _ = session_with_photos
service = PhotoService(session)
all_photos = service.read_all()
assert isinstance(all_photos, list)
assert len(all_photos) == 2
assert all_photos[0].name == "Foto 1"
assert all_photos[1].name == "Foto 2"
def test_read_all_returns_empty_list_for_empty_db(db_session):
service = PhotoService(db_session)
all_photos = service.read_all()
assert isinstance(all_photos, list)
assert len(all_photos) == 0
def test_check_photo_by_hash_finds_existing_photo(session_with_photos):
session, photos = session_with_photos
service = PhotoService(session)
existing_photo = photos[0]
hash_to_find = existing_photo.photo_hash # "hash1"
diary_id = existing_photo.fk_travel_diary_id # 1
found_photo = service.check_photo_by_hash(hash_to_find, diary_id)
assert found_photo is not None
assert found_photo.id == existing_photo.id
assert found_photo.photo_hash == hash_to_find
def test_check_photo_by_hash_returns_none_when_not_found(session_with_photos):
session, photos = session_with_photos
service = PhotoService(session)
existing_hash = photos[0].photo_hash # "hash1"
existing_diary_id = photos[0].fk_travel_diary_id # 1
result1 = service.check_photo_by_hash("hash_inexistente", existing_diary_id)
assert result1 is None
invalid_diary_id = 999
result2 = service.check_photo_by_hash(existing_hash, invalid_diary_id)
assert result2 is None
def test_update_photo_metadata_successfully(session_with_photos):
session, photos = session_with_photos
service = PhotoService(session)
photo_to_update = photos[0]
photo_with_new_data = Photo(
filepath=photo_to_update.filepath,
name="Novo Nome da Foto",
caption="Nova legenda para a foto.",
photo_hash=photo_to_update.photo_hash, # O hash não muda
addition_date=photo_to_update.addition_date,
fk_travel_diary_id=photo_to_update.fk_travel_diary_id
)
updated_photo = service.update(photo_to_update, photo_with_new_data)
assert updated_photo is not None
assert updated_photo.name == "Novo Nome da Foto"
assert updated_photo.caption == "Nova legenda para a foto."
assert updated_photo.photo_hash == photo_to_update.photo_hash
photo_in_db = session.query(Photo).get(photo_to_update.id)
assert photo_in_db.name == "Novo Nome da Foto"
@patch.object(PhotoService, 'hash_file')
@patch('pathlib.Path.unlink')
@patch('pathlib.Path.exists')
@patch.object(PhotoService, '_copy_photo_to_diary')
@patch.object(DirectoryManager, 'get_diaries_root', return_value="/fake/diaries_root")
def test_update_photo_with_new_file_successfully(
mock_get_root, mock_copy, mock_exists, mock_unlink, mock_hash, session_with_photos
):
session, photos = session_with_photos
service = PhotoService(session)
photo_to_update = photos[0]
new_source_path = Path("/path/para/nova_imagem.jpg")
new_copied_path = Path(f"/fake/diaries_root/{photo_to_update.travel_diary.directory_name}/images/nova_imagem.jpg")
mock_copy.return_value = new_copied_path
mock_exists.return_value = True
mock_hash.return_value = "novo_hash_calculado"
photo_with_new_file = Photo(
filepath=new_source_path,
name=photo_to_update.name,
photo_hash="hash_antigo",
fk_travel_diary_id=photo_to_update.fk_travel_diary_id
)
updated_photo = service.update(photo_to_update, photo_with_new_file)
mock_copy.assert_called_once_with(new_source_path, photo_to_update.travel_diary)
mock_unlink.assert_called_once()
mock_hash.assert_called_once_with(new_copied_path)
assert updated_photo.filepath == str(new_copied_path)
assert updated_photo.photo_hash == "novo_hash_calculado"
def test_update_photo_returns_none_if_photo_does_not_exist(db_session):
service = PhotoService(db_session)
non_existent_photo_src = Photo(
filepath="/fake/path.jpg", name="dummy",
photo_hash="dummy", fk_travel_diary_id=1
)
non_existent_photo_src.id = 999
photo_with_new_data = Photo(
filepath="/fake/new.jpg", name="new dummy",
photo_hash="new_dummy", fk_travel_diary_id=1
)
result = service.update(non_existent_photo_src, photo_with_new_data)
assert result is None
@patch.object(PhotoService, 'hash_file')
@patch('pathlib.Path.unlink')
@patch('pathlib.Path.exists')
@patch.object(PhotoService, '_copy_photo_to_diary')
@patch.object(DirectoryManager, 'get_diaries_root', return_value="/fake/diaries_root")
def test_update_photo_with_new_file_successfully(
mock_get_root, mock_copy, mock_exists, mock_unlink, mock_hash, session_with_photos
):
session, photos = session_with_photos
service = PhotoService(session)
photo_to_update = photos[0]
new_source_path = Path("/path/para/nova_imagem.jpg")
new_copied_path = Path(f"/fake/diaries_root/{photo_to_update.travel_diary.directory_name}/images/nova_imagem.jpg")
mock_copy.return_value = new_copied_path
mock_exists.return_value = True
mock_hash.return_value = "novo_hash_calculado"
photo_with_new_file = Photo(
filepath=new_source_path, name=photo_to_update.name,
photo_hash="hash_antigo", fk_travel_diary_id=photo_to_update.fk_travel_diary_id
)
updated_photo = service.update(photo_to_update, photo_with_new_file)
mock_copy.assert_called_once_with(new_source_path, photo_to_update.travel_diary)
mock_unlink.assert_called_once()
mock_hash.assert_called_once_with(new_copied_path)
assert updated_photo.filepath == str(new_copied_path)
assert updated_photo.photo_hash == "novo_hash_calculado"
@patch.object(DirectoryManager, 'get_diary_images_directory')
def test_copy_photo_to_diary_handles_name_collision_with_patch(mock_get_images_dir, db_session, tmp_path: Path):
images_dir = tmp_path / "images"
images_dir.mkdir()
mock_get_images_dir.return_value = images_dir
source_file = tmp_path / "foto.jpg"
source_file.touch()
(images_dir / "foto.jpg").touch()
service = PhotoService(db_session)
fake_diary = TravelDiary(name="test",directory_name="fake_diary")
copied_path = service._copy_photo_to_diary(source_file, fake_diary)
assert copied_path.name == "foto_1.jpg"
assert copied_path.exists()
@patch('pathlib.Path.unlink')
@patch('pathlib.Path.exists')
@patch.object(DirectoryManager, 'get_diaries_root', return_value="/fake/diaries_root")
def test_delete_photo_successfully(mock_get_root, mock_exists, mock_unlink, session_with_photos):
session, photos = session_with_photos
service = PhotoService(session)
photo_to_delete = photos[0]
photo_id = photo_to_delete.id
mock_exists.return_value = True
deleted_photo_data = service.delete(photo_to_delete)
mock_unlink.assert_called_once()
assert deleted_photo_data is not None
assert deleted_photo_data.id == photo_id
photo_in_db = service.read_by_id(photo_id)
assert photo_in_db is None
@patch('pathlib.Path.unlink')
def test_delete_returns_none_for_non_existent_photo(mock_unlink, db_session):
service = PhotoService(db_session)
non_existent_photo = Photo(
filepath="/fake/path.jpg", name="dummy",
photo_hash="dummy_hash", fk_travel_diary_id=1
)
non_existent_photo.id = 999
result = service.delete(non_existent_photo)
assert result is None
mock_unlink.assert_not_called()
+33
View File
@@ -0,0 +1,33 @@
from pilgrim.service.servicemanager import ServiceManager
from unittest.mock import patch, MagicMock
def test_initial_state_is_none():
manager = ServiceManager()
assert manager.get_session() is None
assert manager.get_entry_service() is None
assert manager.get_photo_service() is None
assert manager.get_travel_diary_service() is None
@patch('pilgrim.service.servicemanager.TravelDiaryService')
@patch('pilgrim.service.servicemanager.PhotoService')
@patch('pilgrim.service.servicemanager.EntryService')
def test_get_services_instantiates_with_correct_session(
mock_entry_service, mock_photo_service, mock_travel_diary_service
):
manager = ServiceManager()
mock_session = MagicMock()
manager.set_session(mock_session)
entry_service_instance = manager.get_entry_service()
photo_service_instance = manager.get_photo_service()
travel_diary_service_instance = manager.get_travel_diary_service()
mock_entry_service.assert_called_once()
mock_photo_service.assert_called_once()
mock_travel_diary_service.assert_called_once()
mock_entry_service.assert_called_once_with(mock_session)
mock_photo_service.assert_called_once_with(mock_session)
mock_travel_diary_service.assert_called_once_with(mock_session)
assert entry_service_instance == mock_entry_service.return_value
assert photo_service_instance == mock_photo_service.return_value
assert travel_diary_service_instance == mock_travel_diary_service.return_value
+56
View File
@@ -0,0 +1,56 @@
from unittest.mock import patch, MagicMock
from pilgrim.application import Application
@patch('pilgrim.application.UIApp')
@patch('pilgrim.application.ServiceManager')
@patch('pilgrim.application.Database')
@patch('pilgrim.application.ConfigManager')
def test_application_initialization_wires_dependencies(
MockConfigManager, MockDatabase, MockServiceManager, MockUIApp
):
mock_config_instance = MockConfigManager.return_value
mock_db_instance = MockDatabase.return_value
mock_session_instance = mock_db_instance.session.return_value
mock_service_manager_instance = MockServiceManager.return_value
app = Application()
MockConfigManager.assert_called_once()
MockDatabase.assert_called_once_with(mock_config_instance)
MockServiceManager.assert_called_once()
MockUIApp.assert_called_once_with(mock_service_manager_instance, mock_config_instance)
mock_config_instance.read_config.assert_called_once()
mock_db_instance.session.assert_called_once()
mock_service_manager_instance.set_session.assert_called_once_with(mock_session_instance)
@patch('pilgrim.application.UIApp')
@patch('pilgrim.application.ServiceManager')
@patch('pilgrim.application.Database')
@patch('pilgrim.application.ConfigManager')
def test_application_run_calls_methods(
MockConfigManager, MockDatabase, MockServiceManager, MockUIApp
):
app = Application()
mock_db_instance = app.database
mock_ui_instance = app.ui
app.run()
mock_db_instance.create.assert_called_once()
mock_ui_instance.run.assert_called_once()
@patch('pilgrim.application.UIApp')
@patch('pilgrim.application.ServiceManager')
@patch('pilgrim.application.Database')
@patch('pilgrim.application.ConfigManager')
def test_get_service_manager_creates_and_configures_new_instance(
MockConfigManager, MockDatabase, MockServiceManager, MockUIApp
):
app = Application()
mock_db_instance = app.database
fake_session = MagicMock()
mock_db_instance.session.return_value = fake_session
mock_db_instance.reset_mock()
MockServiceManager.reset_mock()
returned_manager = app.get_service_manager()
mock_db_instance.session.assert_called_once()
MockServiceManager.assert_called_once()
returned_manager.set_session.assert_called_once_with(fake_session)
assert returned_manager is MockServiceManager.return_value
+9
View File
@@ -0,0 +1,9 @@
from unittest.mock import patch
from pilgrim.command import main
@patch('pilgrim.command.Application')
def test_main_function_runs_application(MockApplication):
mock_app_instance = MockApplication.return_value
main()
MockApplication.assert_called_once()
mock_app_instance.run.assert_called_once()
+67
View File
@@ -0,0 +1,67 @@
import pytest
import tomli
from pathlib import Path
from unittest.mock import patch
from pilgrim.utils.config_manager import ConfigManager, SingletonMeta
from pilgrim.utils.directory_manager import DirectoryManager
@pytest.fixture
def clean_singleton():
SingletonMeta._instances = {}
@patch('pilgrim.utils.config_manager.DirectoryManager.get_config_directory')
def test_create_default_config_if_not_exists_with_decorator(mock_get_config_dir, tmp_path: Path, clean_singleton):
mock_get_config_dir.return_value = str(tmp_path)
manager = ConfigManager()
config_file = tmp_path / "config.toml"
assert not config_file.exists()
manager.read_config()
assert config_file.exists()
assert manager.database_type == "sqlite"
@patch('pilgrim.utils.config_manager.DirectoryManager.get_config_directory')
def test_read_existing_config_with_decorator(mock_get_config_dir, tmp_path: Path, clean_singleton):
mock_get_config_dir.return_value = str(tmp_path)
custom_config_content = """
[database]
url = "/custom/path/to/db.sqlite"
type = "custom_sqlite"
[settings.diary]
auto_open_diary_on_startup = "MyCustomDiary"
auto_open_on_creation = true
"""
config_file = tmp_path / "config.toml"
config_file.write_text(custom_config_content)
manager = ConfigManager()
manager.read_config()
assert manager.database_url == "/custom/path/to/db.sqlite"
assert manager.database_type == "custom_sqlite"
@patch('pilgrim.utils.config_manager.DirectoryManager.get_config_directory')
def test_save_config_writes_changes_to_file_with_decorator(mock_get_config_dir, tmp_path: Path, clean_singleton):
mock_get_config_dir.return_value = str(tmp_path)
manager = ConfigManager()
manager.read_config()
manager.set_database_url("/novo/caminho.db")
manager.set_auto_open_new_diary(True)
manager.save_config()
config_file = tmp_path / "config.toml"
with open(config_file, "rb") as f:
data = tomli.load(f)
assert data["database"]["url"] == "/novo/caminho.db"
assert data["settings"]["diary"]["auto_open_on_creation"] is True
@patch('pilgrim.utils.config_manager.DirectoryManager.get_config_directory')
def test_read_config_raises_error_on_malformed_toml(mock_get_config_dir, tmp_path: Path, clean_singleton):
mock_get_config_dir.return_value = str(tmp_path)
invalid_toml_content = """
[database]
url = /caminho/sem/aspas
"""
config_file = tmp_path / "config.toml"
config_file.write_text(invalid_toml_content)
manager = ConfigManager()
with pytest.raises(ValueError, match="Invalid TOML configuration"):
manager.read_config()