mirror of
https://github.com/gmbrax/Pilgrim.git
synced 2026-11-10 20:00:16 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d648470a | ||
|
|
8f36e5a770 | ||
|
|
11e9cf6e75 |
@@ -0,0 +1,24 @@
|
||||
name: Pylint
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
pip install pylint
|
||||
- name: Analysing the code with pylint
|
||||
run: |
|
||||
pylint --disable=C0114,C0115,C0116 --exit-zero $(git ls-files '*.py')
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Pylint and SonarCloud
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master, staging ]
|
||||
pull_request:
|
||||
branches: [ main, master, staging ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Necessário para SonarCloud
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
pip install pylint ruff coverage pytest
|
||||
|
||||
- name: Analysing the code with pylint (console output)
|
||||
run: |
|
||||
pylint --disable=C0114,C0115,C0116 --exit-zero $(git ls-files '*.py') || true
|
||||
|
||||
- name: Generate Pylint report for SonarCloud
|
||||
run: |
|
||||
pylint --disable=C0114,C0115,C0116 --output-format=json --exit-zero $(git ls-files '*.py') > pylint-report.json || true
|
||||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --output-format=json . > ruff-report.json || true
|
||||
|
||||
- name: Run tests with coverage (if you have tests)
|
||||
run: |
|
||||
if [ -d "tests" ] || [ -f "test_*.py" ] || [ -f "*_test.py" ]; then
|
||||
coverage run -m pytest || true
|
||||
coverage xml || true
|
||||
else
|
||||
echo "No tests found, skipping coverage"
|
||||
fi
|
||||
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.projectKey=gmbrax_Pilgrim
|
||||
-Dsonar.organization=gmbrax
|
||||
-Dsonar.python.pylint.reportPaths=pylint-report.json
|
||||
-Dsonar.python.ruff.reportPaths=ruff-report.json
|
||||
-Dsonar.python.coverage.reportPaths=coverage.xml
|
||||
+30
-47
@@ -1,68 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
|
||||
|
||||
## Unreleased
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Planned
|
||||
* Installation Method 1 (repository compilation)
|
||||
* Organization of trips by date, location, or theme
|
||||
* Enhanced photo management features
|
||||
* Search functionality
|
||||
* Export features
|
||||
* Testing implementation
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.4] - 2025-07-19
|
||||
### Planned
|
||||
|
||||
### Added
|
||||
* Support for creating new diaries asynchronously, with an option to automatically open the newly created diary
|
||||
* Unified "Enter" key support for saving or creating diaries across relevant modals
|
||||
* Automatic diary list refresh when returning to the diary screen
|
||||
* Application configuration management with a new centralized config system
|
||||
* Database location and initialization now configurable via the new config manager
|
||||
* Automatic migration of database file to the configuration directory
|
||||
* Display of database URL on application startup for transparency
|
||||
* Duplicate photo detection before photo creation to prevent redundant entries
|
||||
* Photo hash indexing to improve photo lookup performance
|
||||
|
||||
### Changed
|
||||
* Enhanced feedback and validation when editing or creating diary names
|
||||
* Streamlined and unified save logic for diary modals, reducing duplicated behavior
|
||||
* About screen now displays the actual installed application version dynamically
|
||||
* Sidebar and photo-related UI text updated to remove emoji icons for a cleaner appearance
|
||||
* Sidebar layout and scrolling behavior improved for better usability
|
||||
* Photo hash generation now relies on existing service-provided hashes instead of local computation
|
||||
|
||||
### Improved
|
||||
* Enhanced feedback and validation when editing or creating diary names
|
||||
* Streamlined and unified save logic for diary modals, reducing duplicated behavior
|
||||
* Sidebar layout and scrolling behavior for better usability
|
||||
- Installation Method 1 (repository compilation)
|
||||
- Organization of trips by date, location, or theme
|
||||
- Enhanced photo management features
|
||||
- Search functionality
|
||||
- Export features
|
||||
- Testing implementation
|
||||
|
||||
## [0.0.3] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
* Removed the dependency on textual-dev from pyproject.toml
|
||||
- Removed the dependency on textual-dev from pyproject.toml
|
||||
|
||||
## [0.0.2] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
* Changed the license in pyproject.toml to BSD
|
||||
- Changed the license in pyproject.toml to BSD
|
||||
|
||||
## [0.0.1] - 2025-07-06
|
||||
|
||||
### Added
|
||||
* Initial alpha release of Pilgrim travel diary application
|
||||
* Create and edit travel diaries
|
||||
* Create and edit diary entries
|
||||
* Photo ingestion system
|
||||
* Photo addition and reference via sidebar
|
||||
* Text User Interface (TUI) built with Textual framework
|
||||
* Pre-compiled binary installation method (Method 2)
|
||||
* Support for Linux operating systems
|
||||
* Basic project documentation (README)
|
||||
|
||||
- Initial alpha release of Pilgrim travel diary application
|
||||
- Create and edit travel diaries
|
||||
- Create and edit diary entries
|
||||
- Photo ingestion system
|
||||
- Photo addition and reference via sidebar
|
||||
- Text User Interface (TUI) built with Textual framework
|
||||
- Pre-compiled binary installation method (Method 2)
|
||||
- Support for Linux operating systems
|
||||
- Basic project documentation (README)
|
||||
|
||||
### Known Issues
|
||||
* Installation Method 1 not yet implemented
|
||||
* No testing suite implemented yet
|
||||
* Some features may be unstable in an alpha version
|
||||
|
||||
- Installation Method 1 not yet implemented
|
||||
- No testing suite implemented yet
|
||||
- Some features may be unstable in an alpha version
|
||||
|
||||
[Unreleased]: https://github.com/username/pilgrim/compare/v0.0.1...HEAD
|
||||
|
||||
[0.0.1]: https://github.com/username/pilgrim/releases/tag/v0.0.1
|
||||
|
||||
+9
-8
@@ -2,9 +2,10 @@
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[project]
|
||||
name = "pilgrim"
|
||||
version = "0.0.4"
|
||||
name = "Pilgrim"
|
||||
version = "0.0.3"
|
||||
authors = [
|
||||
{ name="Gustavo Henrique Santos Souza de Miranda", email="gustavohssmiranda@gmail.com" }
|
||||
]
|
||||
@@ -19,14 +20,14 @@ classifiers = [
|
||||
dependencies = [
|
||||
"sqlalchemy",
|
||||
"textual",
|
||||
"tomli",
|
||||
"tomli_w",
|
||||
"unidecode"
|
||||
]
|
||||
|
||||
]
|
||||
[template.plugins.default]
|
||||
src-layout = true
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/gmbrax/Pilgrim/"
|
||||
Issues = "https://github.com/gmbrax/Pilgrim/issues"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/pilgrim"]
|
||||
[project.scripts]
|
||||
pilgrim = "pilgrim.command:main"
|
||||
pilgrim = "pilgrim:main"
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
from pilgrim.database import Database
|
||||
from pilgrim.service.servicemanager import ServiceManager
|
||||
from pilgrim.ui.ui import UIApp
|
||||
from pilgrim.utils import ConfigManager
|
||||
from pilgrim.utils import DirectoryManager
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self):
|
||||
self.config_manager = ConfigManager()
|
||||
self.config_manager.read_config() # Chamar antes de criar o Database
|
||||
self.database = Database(self.config_manager)
|
||||
self.config_dir = DirectoryManager.get_config_directory()
|
||||
self.database = Database()
|
||||
session = self.database.session()
|
||||
session_manager = ServiceManager()
|
||||
session_manager.set_session(session)
|
||||
self.ui = UIApp(session_manager, self.config_manager)
|
||||
self.ui = UIApp(session_manager)
|
||||
|
||||
def run(self):
|
||||
print(f"URL do banco: {self.config_manager.database_url}")
|
||||
self.database.create()
|
||||
self.ui.run()
|
||||
|
||||
|
||||
+27
-13
@@ -1,28 +1,42 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from pilgrim.utils import ConfigManager
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_database_path() -> Path:
|
||||
"""
|
||||
Get the database file path following XDG Base Directory specification.
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
# Get home directory
|
||||
home = Path.home()
|
||||
|
||||
# Create .pilgrim directory if it doesn't exist
|
||||
pilgrim_dir = home / ".pilgrim"
|
||||
pilgrim_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Database file path
|
||||
db_path = pilgrim_dir / "database.db"
|
||||
|
||||
# If database doesn't exist in new location but exists in current directory,
|
||||
# migrate it
|
||||
if not db_path.exists():
|
||||
current_db = Path("database.db")
|
||||
if current_db.exists():
|
||||
shutil.copy2(current_db, db_path)
|
||||
print(f"Database migrated from {current_db} to {db_path}")
|
||||
|
||||
return db_path
|
||||
|
||||
class Database:
|
||||
|
||||
def __init__(self, config_manager: ConfigManager):
|
||||
self.db_path = config_manager.database_url
|
||||
|
||||
# Garantir que o diretório existe
|
||||
db_dir = os.path.dirname(self.db_path)
|
||||
if not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
def __init__(self):
|
||||
db_path = get_database_path()
|
||||
self.engine = create_engine(
|
||||
f"sqlite:///{self.db_path}",
|
||||
f"sqlite:///{db_path}",
|
||||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
|
||||
@@ -4,16 +4,15 @@ from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pilgrim.database import Base
|
||||
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Entry(Base):
|
||||
__tablename__ = "entries"
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String,nullable=False)
|
||||
title = Column(String)
|
||||
text = Column(String)
|
||||
date = Column(DateTime,nullable=False)
|
||||
date = Column(DateTime)
|
||||
photos = relationship(
|
||||
"Photo",
|
||||
secondary=photo_entry_association,
|
||||
|
||||
@@ -4,11 +4,9 @@ from pathlib import Path
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql.schema import Index
|
||||
|
||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
from pilgrim.database import Base
|
||||
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Photo(Base):
|
||||
@@ -26,10 +24,6 @@ class Photo(Base):
|
||||
)
|
||||
|
||||
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False)
|
||||
travel_diary = relationship("TravelDiary", back_populates="photos")
|
||||
__table_args__ = (
|
||||
Index('idx_photo_hash_diary', 'hash', 'fk_travel_diary_id'),
|
||||
)
|
||||
|
||||
def __init__(self, filepath, name, photo_hash, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey
|
||||
|
||||
from pilgrim.database import Base
|
||||
from ..database import Base
|
||||
|
||||
photo_entry_association = Table('photo_entry_association', Base.metadata,
|
||||
Column('id', Integer, primary_key=True, autoincrement=True),
|
||||
|
||||
@@ -3,17 +3,15 @@ from typing import Any
|
||||
from sqlalchemy import Column, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pilgrim.database import Base
|
||||
from .. import database
|
||||
|
||||
|
||||
|
||||
class TravelDiary(Base):
|
||||
class TravelDiary(database.Base):
|
||||
__tablename__ = "travel_diaries"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
directory_name = Column(String, nullable=False, unique=True)
|
||||
entries = relationship("Entry", back_populates="travel_diary", cascade="all, delete-orphan")
|
||||
photos = relationship("Photo", back_populates="travel_diary", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('directory_name', name='uq_travel_diary_directory_name'),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from pilgrim.models.entry import Entry
|
||||
from pilgrim.models.travel_diary import TravelDiary
|
||||
from pilgrim.models.photo import Photo # ✨ Importe o modelo Photo
|
||||
from ..models.entry import Entry
|
||||
from ..models.travel_diary import TravelDiary
|
||||
from ..models.photo import Photo # ✨ Importe o modelo Photo
|
||||
|
||||
|
||||
class EntryService:
|
||||
|
||||
@@ -14,9 +14,8 @@ class PhotoService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
@staticmethod
|
||||
def hash_file(filepath: Path) -> str:
|
||||
"""Calculate the hash of a file using SHA3-384."""
|
||||
def _hash_file(self, filepath: Path) -> str:
|
||||
"""Calculate hash of a file using SHA3-384."""
|
||||
hash_func = hashlib.new('sha3_384')
|
||||
with open(filepath, 'rb') as f:
|
||||
while chunk := f.read(8192):
|
||||
@@ -65,18 +64,10 @@ class PhotoService:
|
||||
|
||||
return dest_path
|
||||
|
||||
def check_photo_by_hash(self, photohash:str, traveldiaryid:int):
|
||||
photo = (self.session.query(Photo).filter(Photo.photo_hash == photohash,Photo.fk_travel_diary_id == traveldiaryid)
|
||||
.first())
|
||||
return photo
|
||||
|
||||
def create(self, filepath: Path, name: str, travel_diary_id: int, caption=None, addition_date=None) -> Photo | None:
|
||||
travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first()
|
||||
if not travel_diary:
|
||||
return None
|
||||
photo_hash = self.hash_file(filepath)
|
||||
if self.check_photo_by_hash(photo_hash, travel_diary_id):
|
||||
return None
|
||||
|
||||
# Copy photo to diary's images directory
|
||||
copied_path = self._copy_photo_to_diary(filepath, travel_diary)
|
||||
@@ -88,6 +79,8 @@ class PhotoService:
|
||||
except ValueError:
|
||||
addition_date = None
|
||||
|
||||
# Calculate hash from the copied file
|
||||
photo_hash = self._hash_file(copied_path)
|
||||
|
||||
new_photo = Photo(
|
||||
filepath=str(copied_path), # Store the path to the copied file
|
||||
@@ -125,7 +118,7 @@ class PhotoService:
|
||||
old_path.unlink()
|
||||
original.filepath = str(new_path)
|
||||
# Update hash based on the new copied file
|
||||
original.photo_hash = self.hash_file(new_path)
|
||||
original.photo_hash = self._hash_file(new_path)
|
||||
|
||||
original.name = photo_dst.name
|
||||
original.addition_date = photo_dst.addition_date
|
||||
|
||||
@@ -6,8 +6,8 @@ from pathlib import Path
|
||||
from pilgrim.utils import DirectoryManager
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from pilgrim.models.travel_diary import TravelDiary
|
||||
from unidecode import unidecode
|
||||
from ..models.travel_diary import TravelDiary
|
||||
|
||||
|
||||
class TravelDiaryService:
|
||||
def __init__(self, session):
|
||||
@@ -20,10 +20,8 @@ class TravelDiaryService:
|
||||
- Replaces spaces with underscores
|
||||
- Ensures name is unique by adding a suffix if needed
|
||||
"""
|
||||
transliterated_name = unidecode(name)
|
||||
|
||||
# Remove special characters and replace spaces
|
||||
safe_name = re.sub(r'[^\w\s-]', '', transliterated_name)
|
||||
safe_name = re.sub(r'[^\w\s-]', '', name)
|
||||
safe_name = safe_name.strip().replace(' ', '_').lower()
|
||||
|
||||
# Ensure we have a valid name
|
||||
|
||||
@@ -3,7 +3,7 @@ from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Footer, Button, Label, TextArea
|
||||
from textual.containers import Container
|
||||
from importlib.metadata import version
|
||||
|
||||
class AboutScreen(Screen[bool]):
|
||||
"""Screen to display application information."""
|
||||
|
||||
@@ -20,7 +20,7 @@ class AboutScreen(Screen[bool]):
|
||||
self.app_title = Label("Pilgrim", id="AboutScreen_AboutTitle",classes="AboutScreen_AboutTitle")
|
||||
self.content = Label("A TUI Based Travel Diary Application", id="AboutScreen_AboutContent",
|
||||
classes="AboutScreen_AboutContent")
|
||||
self.version = Label(f"Version: {version('Pilgrim')}", id="AboutScreen_AboutVersion",
|
||||
self.version = Label("Version: 0.0.1", id="AboutScreen_AboutVersion",
|
||||
classes="AboutScreen_AboutVersion")
|
||||
self.developer = Label("Developed By: Gustavo Henrique Miranda ", id="AboutScreen_AboutAuthor")
|
||||
self.contact = Label("git.gustavomiranda.xyz", id="AboutScreen_AboutContact",
|
||||
|
||||
@@ -216,7 +216,6 @@ class DiaryListScreen(Screen):
|
||||
self.notify(f"Creation canceled...")
|
||||
|
||||
def _on_screen_resume(self) -> None:
|
||||
super()._on_screen_resume()
|
||||
self.refresh_diaries()
|
||||
|
||||
def action_edit_selected_diary(self):
|
||||
|
||||
@@ -87,7 +87,7 @@ class EditEntryScreen(Screen):
|
||||
self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry")
|
||||
|
||||
# Sidebar widgets
|
||||
self.sidebar_title = Static("Photos", classes="EditEntryScreen-sidebar-title")
|
||||
self.sidebar_title = Static("📸 Photos", classes="EditEntryScreen-sidebar-title")
|
||||
self.photo_list = OptionList(id="photo_list", classes="EditEntryScreen-sidebar-photo-list")
|
||||
self.photo_info = Static("", classes="EditEntryScreen-sidebar-photo-info")
|
||||
self.help_text = Static("", classes="EditEntryScreen-sidebar-help")
|
||||
@@ -296,28 +296,30 @@ class EditEntryScreen(Screen):
|
||||
|
||||
if not self.cached_photos:
|
||||
self.photo_info.update("No photos found for this diary")
|
||||
self.help_text.update("No photos available\n\nUse Photo Manager to add photos")
|
||||
self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos")
|
||||
return
|
||||
|
||||
# Add photos to the list with hash
|
||||
for photo in self.cached_photos:
|
||||
# Show name and hash in the list
|
||||
photo_hash = str(photo.photo_hash)[:8]
|
||||
self.photo_list.add_option(f"{photo.name} \\[{photo_hash}\]")
|
||||
self.photo_list.add_option(f"📷 {photo.name} \\[{photo_hash}\]")
|
||||
|
||||
self.photo_info.update(f"{len(self.cached_photos)} photos in diary")
|
||||
self.photo_info.update(f"📸 {len(self.cached_photos)} photos in diary")
|
||||
|
||||
# Updated help a text with hash information
|
||||
help_text = (
|
||||
"[b]Sidebar Shortcuts[/b]\n"
|
||||
"[b]⌨️ Sidebar Shortcuts[/b]\n"
|
||||
"[b][green]i[/green][/b]: Insert photo into entry\n"
|
||||
"[b][green]n[/green][/b]: Add new photo\n"
|
||||
"[b][green]d[/green][/b]: Delete selected photo\n"
|
||||
"[b][green]e[/green][/b]: Edit selected photo\n"
|
||||
"[b][yellow]Tab[/yellow][/b]: Back to editor\n"
|
||||
"[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n"
|
||||
"[b][yellow]F9[/yellow][/b]: Switch focus (if needed)\n\n"
|
||||
"[b]📝 Photo References[/b]\n"
|
||||
"\\[\\[photo::hash\\]\\]"
|
||||
"Use: \\[\\[photo:name:hash\\]\\]\n"
|
||||
"Or: \\[\\[photo::hash\\]\\]"
|
||||
)
|
||||
self.help_text.update(help_text)
|
||||
except Exception as e:
|
||||
@@ -427,7 +429,9 @@ class EditEntryScreen(Screen):
|
||||
photo_details += f"🔗 {photo_hash}\n"
|
||||
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||
photo_details += f"💬 {selected_photo.caption or 'No caption'}\n"
|
||||
photo_details += f"📁 {selected_photo.filepath}\n\n"
|
||||
photo_details += f"[b]Reference formats:[/b]\n"
|
||||
photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
|
||||
|
||||
self.photo_info.update(photo_details)
|
||||
@@ -611,8 +615,7 @@ class EditEntryScreen(Screen):
|
||||
addition_date=original_photo.addition_date,
|
||||
caption=photo_data["caption"],
|
||||
entries=original_photo.entries if original_photo.entries is not None else [],
|
||||
id=original_photo.id,
|
||||
photo_hash=original_photo.photo_hash,
|
||||
id=original_photo.id
|
||||
)
|
||||
|
||||
result = photo_service.update(original_photo, updated_photo)
|
||||
@@ -739,15 +742,15 @@ class EditEntryScreen(Screen):
|
||||
self.notify(f"Selected photo: {selected_photo.name} \\[{photo_hash}\\]")
|
||||
|
||||
# Update photo info with details including hash
|
||||
photo_details = f"Name: {selected_photo.name}\n"
|
||||
photo_details += f"Hash: {photo_hash}\n"
|
||||
photo_details += f"Date: {selected_photo.addition_date}\n"
|
||||
photo_details = f"📷 {selected_photo.name}\n"
|
||||
photo_details += f"🔗 {photo_hash}\n"
|
||||
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||
if selected_photo.caption:
|
||||
photo_details += f"Caption: {selected_photo.caption}\n"
|
||||
else:
|
||||
photo_details += f"Caption: No Caption\n"
|
||||
photo_details += f"💬 {selected_photo.caption}\n"
|
||||
photo_details += f"📁 {selected_photo.filepath}\n\n"
|
||||
photo_details += f"[b]Reference formats:[/b]\n"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}]]"
|
||||
photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
|
||||
|
||||
self.photo_info.update(photo_details)
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ class AddPhotoModal(Screen):
|
||||
self.result = None
|
||||
self.created_photo = None
|
||||
|
||||
def _generate_photo_hash(self, photo_data: dict) -> str:
|
||||
"""Generate a short, unique hash for a photo"""
|
||||
# Use temporary data for hash generation
|
||||
unique_string = f"{photo_data['name']}_{photo_data.get('photo_id', 0)}_new"
|
||||
hash_object = hashlib.md5(unique_string.encode())
|
||||
return hash_object.hexdigest()[:8]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
@@ -63,16 +69,10 @@ class AddPhotoModal(Screen):
|
||||
|
||||
async def _async_create_photo(self, photo_data: dict):
|
||||
"""Creates a new photo asynchronously using PhotoService"""
|
||||
|
||||
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
if photo_service.check_photo_by_hash(photo_service.hash_file(photo_data["filepath"]),self.diary_id):
|
||||
self.notify("Photo already exists in database", severity="error")
|
||||
return
|
||||
|
||||
new_photo = photo_service.create(
|
||||
filepath=Path(photo_data["filepath"]),
|
||||
name=photo_data["name"],
|
||||
@@ -82,9 +82,13 @@ class AddPhotoModal(Screen):
|
||||
|
||||
if new_photo:
|
||||
self.created_photo = new_photo
|
||||
# Generate hash for the new photo
|
||||
photo_hash = self._generate_photo_hash({
|
||||
"name": new_photo.name,
|
||||
"photo_id": new_photo.id
|
||||
})
|
||||
|
||||
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!\nHash: {new_photo.photo_hash[:8]}\nReference: \\[\\[photo:{new_photo.name}:{new_photo.photo_hash[:8]}\\]\\]",
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!\nHash: {photo_hash}\nReference: \\[\\[photo:{new_photo.name}:{photo_hash}\\]\\]",
|
||||
severity="information", timeout=5)
|
||||
|
||||
# Return the created photo data to the calling screen
|
||||
@@ -93,7 +97,7 @@ class AddPhotoModal(Screen):
|
||||
"name": photo_data["name"],
|
||||
"caption": photo_data["caption"],
|
||||
"photo_id": new_photo.id,
|
||||
"hash": new_photo.photo_hash
|
||||
"hash": photo_hash
|
||||
}
|
||||
self.dismiss(self.result)
|
||||
else:
|
||||
|
||||
@@ -3,7 +3,7 @@ from textual.screen import Screen
|
||||
from textual.widgets import Static, Input, Button
|
||||
from textual.containers import Container, Horizontal
|
||||
from pilgrim.models.photo import Photo
|
||||
|
||||
import hashlib
|
||||
|
||||
class EditPhotoModal(Screen):
|
||||
"""Modal for editing an existing photo (name and caption only)"""
|
||||
@@ -12,11 +12,15 @@ class EditPhotoModal(Screen):
|
||||
self.photo = photo
|
||||
self.result = None
|
||||
|
||||
|
||||
def _generate_photo_hash(self, photo: Photo) -> str:
|
||||
"""Generate a short, unique hash for a photo"""
|
||||
unique_string = f"{photo.name}_{photo.id}_{photo.addition_date}"
|
||||
hash_object = hashlib.md5(unique_string.encode())
|
||||
return hash_object.hexdigest()[:8]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
# Generate hash for this photo
|
||||
|
||||
photo_hash = self._generate_photo_hash(self.photo)
|
||||
|
||||
yield Container(
|
||||
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
|
||||
@@ -41,9 +45,10 @@ class EditPhotoModal(Screen):
|
||||
id="caption-input",
|
||||
classes="EditPhotoModal-Input"
|
||||
),
|
||||
Static(f"🔗 Photo Hash: {self.photo.photo_hash[:8]}", classes="EditPhotoModal-Hash"),
|
||||
Static(f"🔗 Photo Hash: {photo_hash}", classes="EditPhotoModal-Hash"),
|
||||
Static("Reference formats:", classes="EditPhotoModal-Label"),
|
||||
Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Horizontal(
|
||||
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
|
||||
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
|
||||
|
||||
@@ -12,9 +12,9 @@ class NewDiaryModal(ModalScreen[str]):
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "create_diary", "Create",priority=True),
|
||||
]
|
||||
def __init__(self):
|
||||
def __init__(self,autoopen=True):
|
||||
super().__init__()
|
||||
self.auto_open = self.app.config_manager.auto_open_new_diary
|
||||
self.auto_open = autoopen
|
||||
self.name_input = Input(id="NewDiaryModal-NameInput",classes="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
@@ -390,9 +390,21 @@ Screen.-modal {
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar {
|
||||
background: $primary-darken-1;
|
||||
width: 45%;
|
||||
width: 40;
|
||||
min-height: 10;
|
||||
border-left: solid green;
|
||||
padding: 1;
|
||||
background: $surface-darken-2;
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
content-align: left top;
|
||||
}
|
||||
|
||||
.EditEntryScreen-content-container {
|
||||
layout: horizontal;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
@@ -405,39 +417,30 @@ Screen.-modal {
|
||||
.EditEntryScreen-sidebar-content {
|
||||
height: 1fr;
|
||||
layout: vertical;
|
||||
margin-left: 1;
|
||||
margin-right: 1;
|
||||
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-list {
|
||||
height: 2fr; /* Pega o espaço restante disponível */
|
||||
min-height: 5; /* Garante altura mínima para não sumir */
|
||||
height: 1fr;
|
||||
border: solid $accent;
|
||||
margin-bottom: 1;
|
||||
overflow-y: auto; /* Adiciona scroll vertical se necessário */
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-info {
|
||||
height: 1fr;
|
||||
max-height: 15; /* Limita altura máxima */
|
||||
min-height: 13;
|
||||
padding: 1;
|
||||
height: auto;
|
||||
min-height: 3;
|
||||
border: solid $warning;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
background: $surface-darken-1;
|
||||
overflow-y: auto; /* Adiciona scroll se exceder max-height */
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-help {
|
||||
height: 1fr;
|
||||
max-height: 10; /* Altura máxima menor que info */
|
||||
min-height: 8; /* Altura mínima menor */
|
||||
height: auto;
|
||||
min-height: 8;
|
||||
border: solid $success;
|
||||
padding: 1;
|
||||
background: $surface-darken-1;
|
||||
text-style: italic;
|
||||
overflow-y: auto; /* Adiciona scroll se exceder max-height */
|
||||
}
|
||||
|
||||
/* Photo Modal Styles */
|
||||
|
||||
@@ -9,7 +9,6 @@ from pilgrim.service.servicemanager import ServiceManager
|
||||
from pilgrim.ui.screens.about_screen import AboutScreen
|
||||
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
|
||||
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
|
||||
from pilgrim.utils import ConfigManager
|
||||
|
||||
CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
|
||||
|
||||
@@ -17,10 +16,9 @@ CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
|
||||
class UIApp(App):
|
||||
CSS_PATH = CSS_FILE_PATH
|
||||
|
||||
def __init__(self,service_manager: ServiceManager, config_manager: ConfigManager, **kwargs):
|
||||
def __init__(self,service_manager: ServiceManager, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.service_manager = service_manager
|
||||
self.config_manager = config_manager
|
||||
|
||||
|
||||
def on_mount(self) -> None:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .directory_manager import DirectoryManager
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
__all__ = ['DirectoryManager', 'ConfigManager']
|
||||
__all__ = ['DirectoryManager']
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import os.path
|
||||
from os import PathLike
|
||||
from threading import Lock
|
||||
|
||||
import tomli
|
||||
import tomli_w
|
||||
|
||||
from pilgrim.utils import DirectoryManager
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
_instances = {}
|
||||
_lock: Lock = Lock()
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
with cls._lock:
|
||||
if cls not in cls._instances:
|
||||
instance = super().__call__(*args, **kwargs)
|
||||
cls._instances[cls] = instance
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class ConfigManager(metaclass=SingletonMeta):
|
||||
def __init__(self):
|
||||
self.database_url = None
|
||||
self.database_type = None
|
||||
self.auto_open_diary = None
|
||||
self.auto_open_new_diary = None
|
||||
self.config_dir = DirectoryManager.get_config_directory()
|
||||
self.__data = None
|
||||
|
||||
def read_config(self):
|
||||
if os.path.exists(f"{DirectoryManager.get_config_directory()}/config.toml"):
|
||||
try:
|
||||
with open(f"{DirectoryManager.get_config_directory()}/config.toml", "rb") as f:
|
||||
data = tomli.load(f)
|
||||
|
||||
except tomli.TOMLDecodeError as e:
|
||||
raise ValueError(f"Invalid TOML configuration: {e}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error reading configuration: {e}")
|
||||
|
||||
self.__data = data
|
||||
self.database_url = self.__data["database"]["url"]
|
||||
self.database_type = self.__data["database"]["type"]
|
||||
|
||||
if self.__data["settings"]["diary"]["auto_open_diary_on_startup"] == "":
|
||||
self.auto_open_diary = None
|
||||
self.auto_open_new_diary = self.__data["settings"]["diary"]["auto_open_on_creation"]
|
||||
else:
|
||||
print("Error: config.toml not found.")
|
||||
self.create_config()
|
||||
self.read_config()
|
||||
|
||||
def create_config(self, config: dict = None):
|
||||
# Garantir que o diretório de configuração existe
|
||||
config_dir = DirectoryManager.get_config_directory()
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
default = {
|
||||
"database": {
|
||||
"url": f"{config_dir}/database.db",
|
||||
"type": "sqlite"
|
||||
},
|
||||
"settings": {
|
||||
"diary": {
|
||||
"auto_open_diary_on_startup": "",
|
||||
"auto_open_on_creation": False
|
||||
}
|
||||
}
|
||||
}
|
||||
if config is None:
|
||||
config = default
|
||||
|
||||
try:
|
||||
with open(f"{config_dir}/config.toml", "wb") as f:
|
||||
tomli_w.dump(config, f)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error creating configuration: {e}")
|
||||
|
||||
def save_config(self):
|
||||
if self.__data is None:
|
||||
self.read_config()
|
||||
if self.__data is None:
|
||||
raise RuntimeError("Error reading configuration.")
|
||||
|
||||
self.__data["database"]["url"] = self.database_url
|
||||
self.__data["database"]["type"] = self.database_type
|
||||
self.__data["settings"]["diary"]["auto_open_diary_on_startup"] = self.auto_open_diary or ""
|
||||
self.__data["settings"]["diary"]["auto_open_on_creation"] = self.auto_open_new_diary
|
||||
try:
|
||||
self.create_config(self.__data)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error saving configuration: {e}")
|
||||
|
||||
def set_config_dir(self, value):
|
||||
self.config_dir = value
|
||||
|
||||
def set_database_url(self, value: str):
|
||||
self.database_url = value
|
||||
|
||||
def set_auto_open_diary(self, value: str):
|
||||
self.auto_open_diary = value
|
||||
|
||||
def set_auto_open_new_diary(self, value: bool):
|
||||
self.auto_open_new_diary = value
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -38,26 +37,3 @@ class DirectoryManager:
|
||||
def get_diary_images_directory(directory_name: str) -> Path:
|
||||
"""Returns the images directory path for a specific diary."""
|
||||
return DirectoryManager.get_diary_data_directory(directory_name) / "images"
|
||||
|
||||
@staticmethod
|
||||
def get_database_path() -> Path:
|
||||
"""
|
||||
Get the database file path following XDG Base Directory specification.
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
pilgrim_dir = DirectoryManager.get_config_directory()
|
||||
db_path = pilgrim_dir / "database.db"
|
||||
|
||||
# If database doesn't exist in new location but exists in current directory,
|
||||
# migrate it
|
||||
if not db_path.exists():
|
||||
current_db = Path("database.db")
|
||||
if current_db.exists():
|
||||
try:
|
||||
shutil.copy2(current_db, db_path)
|
||||
# Consider using logging instead of print
|
||||
print(f"Database migrated from {current_db} to {db_path}")
|
||||
except (OSError, shutil.Error) as e:
|
||||
raise RuntimeError(f"Failed to migrate database: {e}")
|
||||
|
||||
return db_path
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from pilgrim.database import Base
|
||||
from pilgrim.models.travel_diary import TravelDiary
|
||||
from pilgrim.models.photo import Photo
|
||||
|
||||
# Todos os imports necessários para as fixtures devem estar aqui
|
||||
# ...
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_session():
|
||||
"""Esta fixture agora está disponível para TODOS os testes."""
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
@pytest.fixture
|
||||
def populated_db_session(db_session):
|
||||
"""Esta também fica disponível para todos."""
|
||||
travel_diary = TravelDiary(name="My Travel Diary", directory_name="viagem-teste")
|
||||
db_session.add(travel_diary)
|
||||
db_session.commit()
|
||||
return db_session
|
||||
|
||||
@pytest.fixture
|
||||
def session_with_one_diary(db_session):
|
||||
diary = TravelDiary(name="Diário de Teste", directory_name="diario_de_teste")
|
||||
db_session.add(diary)
|
||||
db_session.commit()
|
||||
db_session.refresh(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]
|
||||
@@ -1,269 +0,0 @@
|
||||
from re import search
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from pilgrim.database import Base
|
||||
from pilgrim.models.travel_diary import TravelDiary
|
||||
from pilgrim.models.entry import Entry
|
||||
from pilgrim.models.photo import Photo
|
||||
|
||||
from pilgrim.service.entry_service import EntryService
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_with_an_entry(populated_db_session):
|
||||
session = populated_db_session
|
||||
initial_entry = Entry(
|
||||
title="Título Original",
|
||||
text="Texto original.",
|
||||
date=datetime(2025, 1, 1),
|
||||
travel_diary_id=1
|
||||
)
|
||||
session.add(initial_entry)
|
||||
session.commit()
|
||||
return session, initial_entry.id
|
||||
|
||||
@pytest.fixture
|
||||
def session_with_multiple_entries(populated_db_session):
|
||||
"""Fixture que cria um diário e duas entradas para ele."""
|
||||
session = populated_db_session
|
||||
|
||||
entry1 = Entry(title="Entrada 1", text="Texto 1", date=datetime(2025, 1, 1), travel_diary_id=1)
|
||||
entry2 = Entry(title="Entrada 2", text="Texto 2", date=datetime(2025, 1, 2), travel_diary_id=1)
|
||||
|
||||
session.add_all([entry1, entry2])
|
||||
session.commit()
|
||||
|
||||
return session
|
||||
|
||||
def test_create_entry_successfully(populated_db_session):
|
||||
session = populated_db_session
|
||||
service = EntryService(session)
|
||||
diary_id = 1 # Sabemos que o ID é 1 por causa da nossa fixture
|
||||
title = "Primeiro Dia na Praia"
|
||||
text = "O dia foi ensolarado e o mar estava ótimo."
|
||||
date = datetime(2025, 7, 20)
|
||||
photos = [Photo(filepath="/path/to/photo1.jpg",name="Photo 1",photo_hash="hash_12345678",fk_travel_diary_id=diary_id), Photo(filepath="/path/to/photo2.jpg",name="Photo 1",photo_hash="hash_87654321",fk_travel_diary_id=diary_id)]
|
||||
created_entry = service.create(
|
||||
travel_diary_id=diary_id,
|
||||
title=title,
|
||||
text=text,
|
||||
date=date,
|
||||
photos=photos
|
||||
)
|
||||
assert created_entry is not None
|
||||
assert created_entry.id is not None # Garante que foi salvo no BD e tem um ID
|
||||
assert created_entry.title == title
|
||||
assert created_entry.text == text
|
||||
assert len(created_entry.photos) == 2
|
||||
assert created_entry.photos[0].filepath == "/path/to/photo1.jpg"
|
||||
|
||||
entry_in_db = session.query(Entry).filter_by(id=created_entry.id).one()
|
||||
assert entry_in_db.title == "Primeiro Dia na Praia"
|
||||
|
||||
def test_create_entry_fails_when_diary_id_is_invalid(db_session):
|
||||
session = db_session
|
||||
service = EntryService(session)
|
||||
invalid_id = 666
|
||||
|
||||
result = service.create(
|
||||
travel_diary_id=invalid_id,
|
||||
title="Título de Teste",
|
||||
text="Texto de Teste",
|
||||
date=datetime(2025, 7, 20),
|
||||
photos=[]
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_create_entry_successfully_without_photo(populated_db_session):
|
||||
session = populated_db_session
|
||||
service = EntryService(session)
|
||||
diary_id = 1 # Sabemos que o ID é 1 por causa da nossa fixture
|
||||
title = "Primeiro Dia na Praia"
|
||||
text = "O dia foi ensolarado e o mar estava ótimo."
|
||||
date = datetime(2025, 7, 20)
|
||||
photos = []
|
||||
created_entry = service.create(
|
||||
travel_diary_id=diary_id,
|
||||
title=title,
|
||||
text=text,
|
||||
date=date,
|
||||
photos=photos
|
||||
)
|
||||
assert created_entry is not None
|
||||
assert created_entry.id is not None # Garante que foi salvo no BD e tem um ID
|
||||
assert created_entry.title == title
|
||||
assert created_entry.text == text
|
||||
assert len(created_entry.photos) == 0
|
||||
entry_in_db = session.query(Entry).filter_by(id=created_entry.id).one()
|
||||
assert entry_in_db.title == "Primeiro Dia na Praia"
|
||||
|
||||
def test_create_entry_fails_with_null_title(populated_db_session):
|
||||
session = populated_db_session
|
||||
service = EntryService(session)
|
||||
diary_id = 1
|
||||
with pytest.raises(IntegrityError):
|
||||
service.create(
|
||||
travel_diary_id=diary_id,
|
||||
title=None,
|
||||
text="Um texto qualquer.",
|
||||
date=datetime.now(),
|
||||
photos=[]
|
||||
)
|
||||
|
||||
def test_create_entry_fails_with_null_date(populated_db_session):
|
||||
session = populated_db_session
|
||||
service = EntryService(session)
|
||||
diary_id = 1
|
||||
with pytest.raises(IntegrityError):
|
||||
service.create(
|
||||
travel_diary_id=diary_id,
|
||||
title="Sabado de sol",
|
||||
text="Um texto qualquer.",
|
||||
date=None,
|
||||
photos=[]
|
||||
)
|
||||
|
||||
def test_create_entry_fails_with_null_diary_id(populated_db_session):
|
||||
session = populated_db_session
|
||||
service = EntryService(session)
|
||||
diary_id = 1
|
||||
result = service.create(
|
||||
travel_diary_id=None,
|
||||
title="Sabado de sol",
|
||||
text="Um texto qualquer.",
|
||||
date=datetime.now(),
|
||||
photos=[]
|
||||
)
|
||||
assert result is None
|
||||
def test_ready_by_id_successfully(session_with_an_entry):
|
||||
session,_ = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
search_id = 1
|
||||
result = service.read_by_id(search_id)
|
||||
assert result is not None
|
||||
def test_ready_by_id_fails_when_id_is_invalid(db_session):
|
||||
session = db_session
|
||||
service = EntryService(session)
|
||||
invalid_id = 666
|
||||
result = service.read_by_id(invalid_id)
|
||||
assert result is None
|
||||
|
||||
def test_read_all_returns_all_entries(session_with_multiple_entries):
|
||||
session = session_with_multiple_entries
|
||||
service = EntryService(session)
|
||||
all_entries = service.read_all()
|
||||
assert isinstance(all_entries, list)
|
||||
assert len(all_entries) == 2
|
||||
assert all_entries[0].title == "Entrada 1"
|
||||
assert all_entries[1].title == "Entrada 2"
|
||||
|
||||
def test_read_all_returns_empty_list_on_empty_db(db_session):
|
||||
session = db_session
|
||||
service = EntryService(session)
|
||||
all_entries = service.read_all()
|
||||
assert isinstance(all_entries, list)
|
||||
assert len(all_entries) == 0
|
||||
|
||||
def test_update_entry_successfully(session_with_an_entry):
|
||||
session, entry_id = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
entry_src = session.query(Entry).filter_by(id=entry_id).one()
|
||||
new_date = datetime(2025, 1, 2)
|
||||
entry_dst = Entry(
|
||||
title="Título Atualizado",
|
||||
text="Texto atualizado.",
|
||||
date=new_date,
|
||||
travel_diary_id=1, # Mantemos o mesmo travel_diary_id
|
||||
photos=[]
|
||||
)
|
||||
updated_entry = service.update(entry_src, entry_dst)
|
||||
assert updated_entry is not None
|
||||
assert updated_entry.id == entry_id
|
||||
assert updated_entry.title == "Título Atualizado"
|
||||
assert updated_entry.text == "Texto atualizado."
|
||||
entry_in_db = session.query(Entry).filter_by(id=entry_id).one()
|
||||
assert entry_in_db.title == "Título Atualizado"
|
||||
|
||||
def test_update_entry_fails_if_entry_does_not_exist(db_session):
|
||||
service = EntryService(db_session)
|
||||
non_existent_entry = Entry(
|
||||
title="dummy",
|
||||
text="dummy",
|
||||
date=datetime.now(),
|
||||
travel_diary_id=1)
|
||||
non_existent_entry.id = 999
|
||||
entry_with_new_data = Entry(title="Novo Título", text="Novo Texto", date=datetime.now(), travel_diary_id=1)
|
||||
result = service.update(non_existent_entry, entry_with_new_data)
|
||||
assert result is None
|
||||
|
||||
def test_update_fails_with_null_title(session_with_an_entry):
|
||||
session, entry_id = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
entry_src = session.query(Entry).filter_by(id=entry_id).one()
|
||||
entry_dst = Entry(
|
||||
title=None,
|
||||
text="Texto atualizado.",
|
||||
date=datetime.now(),
|
||||
travel_diary_id=1,
|
||||
photos=[]
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
service.update(entry_src, entry_dst)
|
||||
|
||||
def test_update_fails_with_null_date(session_with_an_entry):
|
||||
session, entry_id = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
entry_src = session.query(Entry).filter_by(id=entry_id).one()
|
||||
entry_dst = Entry(
|
||||
title=entry_src.title,
|
||||
text="Texto atualizado.",
|
||||
date=None,
|
||||
travel_diary_id=1,
|
||||
photos=[]
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
service.update(entry_src, entry_dst)
|
||||
|
||||
def test_update_fails_with_null_diary_id(session_with_an_entry):
|
||||
session, entry_id = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
entry_src = session.query(Entry).filter_by(id=entry_id).one()
|
||||
entry_dst = Entry(
|
||||
title=entry_src.title,
|
||||
text="Texto atualizado.",
|
||||
date=datetime.now(),
|
||||
travel_diary_id=None,
|
||||
photos=[]
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
service.update(entry_src, entry_dst)
|
||||
|
||||
def test_delete_successfully_removes_entry(session_with_an_entry):
|
||||
session, entry_id = session_with_an_entry
|
||||
service = EntryService(session)
|
||||
entry_to_delete = service.read_by_id(entry_id)
|
||||
assert entry_to_delete is not None
|
||||
deleted_entry = service.delete(entry_to_delete)
|
||||
assert deleted_entry is not None
|
||||
assert deleted_entry.id == entry_id
|
||||
entry_in_db = service.read_by_id(entry_id)
|
||||
assert entry_in_db is None
|
||||
|
||||
def test_delete_returns_none_if_entry_does_not_exist(db_session):
|
||||
service = EntryService(db_session)
|
||||
non_existent_entry = Entry(
|
||||
title="dummy",
|
||||
text="dummy",
|
||||
date=datetime.now(),
|
||||
travel_diary_id=1)
|
||||
non_existent_entry.id = 999
|
||||
result = service.delete(non_existent_entry)
|
||||
assert result is None
|
||||
@@ -1,249 +0,0 @@
|
||||
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()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
@@ -1,143 +0,0 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pilgrim import TravelDiary
|
||||
from pilgrim.service.travel_diary_service import TravelDiaryService
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
@pytest.mark.asyncio # Marca o teste para rodar código assíncrono
|
||||
async def test_create_diary_successfully(mock_ensure_dir, db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
new_diary = await service.async_create("Viagem para a Serra")
|
||||
assert new_diary is not None
|
||||
assert new_diary.id is not None
|
||||
assert new_diary.name == "Viagem para a Serra"
|
||||
assert new_diary.directory_name == "viagem_para_a_serra"
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
@patch.object(TravelDiaryService, '_sanitize_directory_name', return_value="nome_existente")
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_diary_handles_integrity_error(mock_sanitize, mock_ensure_dir, db_session):
|
||||
existing_diary = TravelDiary(name="Diário Antigo", directory_name="nome_existente")
|
||||
db_session.add(existing_diary)
|
||||
db_session.commit()
|
||||
service = TravelDiaryService(db_session)
|
||||
with pytest.raises(ValueError, match="Could not create diary"):
|
||||
await service.async_create("Qualquer Nome Novo")
|
||||
mock_ensure_dir.assert_not_called()
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
def test_read_by_id_successfully(mock_ensure_dir, session_with_one_diary):
|
||||
session, diary_to_find = session_with_one_diary
|
||||
service = TravelDiaryService(session)
|
||||
found_diary = service.read_by_id(diary_to_find.id)
|
||||
assert found_diary is not None
|
||||
assert found_diary.id == diary_to_find.id
|
||||
assert found_diary.name == "Diário de Teste"
|
||||
mock_ensure_dir.assert_called_once_with(found_diary)
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
def test_read_by_id_returns_none_for_invalid_id(mock_ensure_dir, db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
result = service.read_by_id(999)
|
||||
assert result is None
|
||||
mock_ensure_dir.assert_not_called()
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
def test_read_all_returns_all_diaries(mock_ensure_dir, db_session):
|
||||
d1 = TravelDiary(name="Diário 1", directory_name="d1")
|
||||
d2 = TravelDiary(name="Diário 2", directory_name="d2")
|
||||
db_session.add_all([d1, d2])
|
||||
db_session.commit()
|
||||
service = TravelDiaryService(db_session)
|
||||
diaries = service.read_all()
|
||||
assert isinstance(diaries, list)
|
||||
assert len(diaries) == 2
|
||||
assert mock_ensure_dir.call_count == 2
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
def test_read_all_returns_empty_list_for_empty_db(mock_ensure_dir, db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
diaries = service.read_all()
|
||||
assert isinstance(diaries, list)
|
||||
assert len(diaries) == 0
|
||||
mock_ensure_dir.assert_not_called()
|
||||
|
||||
@patch.object(TravelDiaryService, '_ensure_diary_directory')
|
||||
@patch('pathlib.Path.rename')
|
||||
@patch.object(TravelDiaryService, '_get_diary_directory')
|
||||
def test_update_diary_successfully(mock_get_dir, mock_path_rename, mock_ensure, session_with_one_diary):
|
||||
session, diary_to_update = session_with_one_diary
|
||||
service = TravelDiaryService(session)
|
||||
old_path = MagicMock(spec=Path) # Um mock que se parece com um objeto Path
|
||||
old_path.exists.return_value = True # Dizemos que o diretório antigo "existe"
|
||||
new_path = Path("/fake/path/diario_atualizado")
|
||||
mock_get_dir.side_effect = [old_path, new_path]
|
||||
updated_diary = service.update(diary_to_update.id, "Diário Atualizado")
|
||||
assert updated_diary is not None
|
||||
assert updated_diary.name == "Diário Atualizado"
|
||||
assert updated_diary.directory_name == "diario_atualizado"
|
||||
old_path.rename.assert_called_once_with(new_path)
|
||||
|
||||
def test_update_returns_none_for_invalid_id(db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
result = service.update(travel_diary_id=999, name="Nome Novo")
|
||||
assert result is None
|
||||
|
||||
@patch.object(TravelDiaryService, '_cleanup_diary_directory')
|
||||
def test_delete_diary_successfully(mock_cleanup, session_with_one_diary):
|
||||
session, diary_to_delete = session_with_one_diary
|
||||
service = TravelDiaryService(session)
|
||||
result = service.delete(diary_to_delete)
|
||||
assert result is not None
|
||||
assert result.id == diary_to_delete.id
|
||||
mock_cleanup.assert_called_once_with(diary_to_delete)
|
||||
diary_in_db = service.read_by_id(diary_to_delete.id)
|
||||
assert diary_in_db is None
|
||||
|
||||
@patch.object(TravelDiaryService, '_cleanup_diary_directory')
|
||||
def test_delete_returns_none_for_non_existent_diary(mock_cleanup, db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
non_existent_diary = TravelDiary(name="dummy", directory_name="dummy")
|
||||
non_existent_diary.id = 999
|
||||
result = service.delete(non_existent_diary)
|
||||
assert result is None
|
||||
mock_cleanup.assert_not_called()
|
||||
|
||||
@patch.object(TravelDiaryService, '_sanitize_directory_name')
|
||||
def test_update_raises_value_error_on_name_collision(mock_sanitize, db_session):
|
||||
|
||||
d1 = TravelDiary(name="Diário A", directory_name="diario_a")
|
||||
d2 = TravelDiary(name="Diário B", directory_name="diario_b")
|
||||
db_session.add_all([d1, d2])
|
||||
db_session.commit()
|
||||
db_session.refresh(d1)
|
||||
mock_sanitize.return_value = "diario_b"
|
||||
service = TravelDiaryService(db_session)
|
||||
with pytest.raises(ValueError, match="Could not update diary"):
|
||||
service.update(d1.id, "Diário B")
|
||||
|
||||
def test_sanitize_directory_name_formats_string_correctly(db_session):
|
||||
service = TravelDiaryService(db_session)
|
||||
name1 = "Minha Primeira Viagem"
|
||||
assert service._sanitize_directory_name(name1) == "minha_primeira_viagem"
|
||||
name2 = "Viagem para o #Rio de Janeiro! @2025"
|
||||
assert service._sanitize_directory_name(name2) == "viagem_para_o_rio_de_janeiro_2025"
|
||||
name3 = " Mochilão na Europa "
|
||||
assert service._sanitize_directory_name(name3) == "mochilao_na_europa"
|
||||
|
||||
def test_sanitize_directory_name_handles_uniqueness(db_session):
|
||||
existing_diary = TravelDiary(name="Viagem para a Praia", directory_name="viagem_para_a_praia")
|
||||
db_session.add(existing_diary)
|
||||
db_session.commit()
|
||||
service = TravelDiaryService(db_session)
|
||||
new_sanitized_name = service._sanitize_directory_name("Viagem para a Praia")
|
||||
assert new_sanitized_name == "viagem_para_a_praia_1"
|
||||
another_diary = TravelDiary(name="Outra Viagem", directory_name="viagem_para_a_praia_1")
|
||||
db_session.add(another_diary)
|
||||
db_session.commit()
|
||||
|
||||
third_sanitized_name = service._sanitize_directory_name("Viagem para a Praia")
|
||||
assert third_sanitized_name == "viagem_para_a_praia_2"
|
||||
@@ -1,9 +0,0 @@
|
||||
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()
|
||||
@@ -1,40 +0,0 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock # A ferramenta para criar nosso "dublê"
|
||||
from pathlib import Path
|
||||
from sqlalchemy import inspect, Column, Integer, String
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from pilgrim.database import Database,Base
|
||||
|
||||
class MockUser(Base):
|
||||
__tablename__ = 'mock_users'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
|
||||
@pytest.fixture
|
||||
def db_instance(tmp_path: Path):
|
||||
fake_db_path = tmp_path / "test_pilgrim.db"
|
||||
mock_config_manager = Mock()
|
||||
mock_config_manager.database_url = str(fake_db_path)
|
||||
db = Database(mock_config_manager)
|
||||
return db, fake_db_path
|
||||
|
||||
def test_create_database(db_instance):
|
||||
db, fake_db_path = db_instance
|
||||
db.create()
|
||||
assert fake_db_path.exists()
|
||||
|
||||
def test_table_creation(db_instance):
|
||||
db, _ = db_instance
|
||||
db.create()
|
||||
inspector = inspect(db.engine)
|
||||
assert "mock_users" in inspector.get_table_names()
|
||||
|
||||
def test_session_returned_corectly(db_instance):
|
||||
db, _ = db_instance
|
||||
session = db.session()
|
||||
assert isinstance(session, Session)
|
||||
session.close()
|
||||
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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()
|
||||
@@ -1,71 +0,0 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from pilgrim.utils.directory_manager import DirectoryManager
|
||||
|
||||
@patch('os.chmod')
|
||||
@patch('pathlib.Path.home')
|
||||
def test_get_config_directory_creates_dir_in_fake_home(mock_home, mock_chmod, tmp_path: Path):
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
expected_config_dir = tmp_path / ".pilgrim"
|
||||
assert not expected_config_dir.exists()
|
||||
result_path = DirectoryManager.get_config_directory()
|
||||
assert result_path == expected_config_dir
|
||||
assert expected_config_dir.exists()
|
||||
mock_chmod.assert_called_once_with(expected_config_dir, 0o700)
|
||||
|
||||
@patch('shutil.copy2')
|
||||
@patch('pathlib.Path.home')
|
||||
def test_get_database_path_no_migration(mock_home, mock_copy, tmp_path: Path):
|
||||
mock_home.return_value = tmp_path
|
||||
expected_db_path = tmp_path / ".pilgrim" / "database.db"
|
||||
result_path = DirectoryManager.get_database_path()
|
||||
assert result_path == expected_db_path
|
||||
mock_copy.assert_not_called()
|
||||
|
||||
@patch('shutil.copy2')
|
||||
@patch('pathlib.Path.home')
|
||||
def test_get_database_path_with_migration(mock_home, mock_copy, tmp_path: Path, monkeypatch):
|
||||
fake_home_dir = tmp_path / "home"
|
||||
fake_project_dir = tmp_path / "project"
|
||||
fake_home_dir.mkdir()
|
||||
fake_project_dir.mkdir()
|
||||
|
||||
(fake_project_dir / "database.db").touch()
|
||||
mock_home.return_value = fake_home_dir
|
||||
monkeypatch.chdir(fake_project_dir)
|
||||
result_path = DirectoryManager.get_database_path()
|
||||
expected_db_path = fake_home_dir / ".pilgrim" / "database.db"
|
||||
assert result_path == expected_db_path
|
||||
|
||||
mock_copy.assert_called_once_with(
|
||||
Path("database.db"),
|
||||
expected_db_path
|
||||
)
|
||||
|
||||
@patch('os.chmod')
|
||||
@patch('pathlib.Path.home')
|
||||
def test_diary_path_methods_construct_correctly(mock_home, mock_chmod, tmp_path: Path):
|
||||
mock_home.return_value = tmp_path
|
||||
images_path = DirectoryManager.get_diary_images_directory("minha-viagem")
|
||||
expected_path = tmp_path / ".pilgrim" / "diaries" / "minha-viagem" / "data" / "images"
|
||||
assert images_path == expected_path
|
||||
assert (tmp_path / ".pilgrim" / "diaries").exists()
|
||||
|
||||
@patch('shutil.copy2')
|
||||
@patch('pathlib.Path.home')
|
||||
def test_get_database_path_handles_migration_error(mock_home, mock_copy, tmp_path: Path, monkeypatch):
|
||||
fake_home_dir = tmp_path / "home"
|
||||
fake_project_dir = tmp_path / "project"
|
||||
fake_home_dir.mkdir()
|
||||
fake_project_dir.mkdir()
|
||||
(fake_project_dir / "database.db").touch()
|
||||
mock_home.return_value = fake_home_dir
|
||||
mock_copy.side_effect = shutil.Error("O disco está cheio!")
|
||||
monkeypatch.chdir(fake_project_dir)
|
||||
with pytest.raises(RuntimeError, match="Failed to migrate database"):
|
||||
DirectoryManager.get_database_path()
|
||||
mock_copy.assert_called_once()
|
||||
Reference in New Issue
Block a user