Compare commits

...
Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda ca0b8c554c Change the import to avoid importing by the folder 2025-07-21 01:22:05 -03:00
Gustavo Henrique Santos Souza de Miranda 8119ae74f6 Create the test for the database class 2025-07-20 01:50:12 -03:00
Gustavo Henrique Miranda 78e45e26fd Update pylint_sonarqube.yml
Fix the ruff error on the sonarcloud scan
2025-07-19 19:06:53 -03:00
Gustavo Henrique Miranda cc68bbcf2e Update pylint_sonarqube.yml
Add the missing parameters for SonarQube
2025-07-19 18:56:19 -03:00
Gustavo Henrique Miranda 9a698596b1 Update and rename pylint.yml to pylint_sonarqube.yml
Add RUFF and SonarQube scanning
2025-07-19 18:52:53 -03:00
Gustavo Henrique Miranda 0cbcbd3a8d Update CHANGELOG.md
Revert the markdown format missing from the last update
2025-07-19 17:40:01 -03:00
Gustavo Henrique Miranda a1e718c1fc Update CHANGELOG.md
Added info for the next alpha version 0.0.4
2025-07-19 17:34:05 -03:00
Gustavo Henrique Miranda 8919e158bc Merge pull request #60 from gmbrax/fix/Importing-duplicate-images
Fix the bug that collides the hash if identical images
2025-07-19 03:23:54 -03:00
Gustavo Henrique Santos Souza de Miranda 9e4b08234f Fix the bug that collides the hash if identical images 2025-07-19 00:16:29 -03:00
Gustavo Henrique Miranda 44ff390695 Merge pull request #59 from gmbrax/fix/remove-stale-hashing-methods
Refactor the edit and add photo modal and remove the hashing methods out of the modals
2025-07-18 02:41:29 -03:00
Gustavo Henrique Santos Souza de Miranda ca53ac7778 Remove the unused import and unused variable 2025-07-18 02:37:07 -03:00
Gustavo Henrique Santos Souza de Miranda 0567495ff9 Refactor the edit and add photo modal and remove the hashing methods out of the modals 2025-07-18 02:27:16 -03:00
Gustavo Henrique Miranda 18ff157eb7 Merge pull request #58 from gmbrax/fix/edit-photo-not-working
Fix the photo edit to properly pass the original photo hash that was missing
2025-07-18 01:39:13 -03:00
Gustavo Henrique Santos Souza de Miranda a9d39f415a Fix the photo edit to properly pass the original photo hash that was missing 2025-07-18 00:03:13 -03:00
Gustavo Henrique Miranda b14d775d87 Merge pull request #56 from gmbrax/fix/sidebar-layout
Improve the CSS and the code for the photo sidebar
2025-07-17 21:43:31 -03:00
Gustavo Henrique Santos Souza de Miranda 1d08f4e40d Change the CSS to better distribute the elements of the sidebar 2025-07-17 21:39:05 -03:00
10 changed files with 192 additions and 92 deletions
-24
View File
@@ -1,24 +0,0 @@
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')
+63
View File
@@ -0,0 +1,63 @@
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
+47 -30
View File
@@ -1,51 +1,68 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. 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.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## Planned
* Installation Method 1 (repository compilation)
* Organization of trips by date, location, or theme
* Enhanced photo management features
* Search functionality
* Export features
* Testing implementation
### Planned ## [0.0.4] - 2025-07-19
- Installation Method 1 (repository compilation) ### Added
- Organization of trips by date, location, or theme * Support for creating new diaries asynchronously, with an option to automatically open the newly created diary
- Enhanced photo management features * Unified "Enter" key support for saving or creating diaries across relevant modals
- Search functionality * Automatic diary list refresh when returning to the diary screen
- Export features * Application configuration management with a new centralized config system
- Testing implementation * 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
## [0.0.3] - 2025-07-07 ## [0.0.3] - 2025-07-07
### Changed ### 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 ## [0.0.2] - 2025-07-07
### Changed ### Changed
- Changed the license in pyproject.toml to BSD * Changed the license in pyproject.toml to BSD
## [0.0.1] - 2025-07-06 ## [0.0.1] - 2025-07-06
### Added ### Added
* Initial alpha release of Pilgrim travel diary application
- Initial alpha release of Pilgrim travel diary application * Create and edit travel diaries
- Create and edit travel diaries * Create and edit diary entries
- Create and edit diary entries * Photo ingestion system
- Photo ingestion system * Photo addition and reference via sidebar
- Photo addition and reference via sidebar * Text User Interface (TUI) built with Textual framework
- Text User Interface (TUI) built with Textual framework * Pre-compiled binary installation method (Method 2)
- Pre-compiled binary installation method (Method 2) * Support for Linux operating systems
- Support for Linux operating systems * Basic project documentation (README)
- Basic project documentation (README)
### Known Issues ### Known Issues
* Installation Method 1 not yet implemented
- Installation Method 1 not yet implemented * No testing suite implemented yet
- No testing suite implemented yet * Some features may be unstable in an alpha version
- 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
+4
View File
@@ -4,6 +4,7 @@ from pathlib import Path
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import Index
from pilgrim.models.photo_in_entry import photo_entry_association from pilgrim.models.photo_in_entry import photo_entry_association
from ..database import Base from ..database import Base
@@ -24,6 +25,9 @@ class Photo(Base):
) )
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False) fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False)
__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): def __init__(self, filepath, name, photo_hash, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
super().__init__(**kw) super().__init__(**kw)
+12 -5
View File
@@ -14,8 +14,9 @@ class PhotoService:
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
def _hash_file(self, filepath: Path) -> str: @staticmethod
"""Calculate hash of a file using SHA3-384.""" def hash_file(filepath: Path) -> str:
"""Calculate the hash of a file using SHA3-384."""
hash_func = hashlib.new('sha3_384') hash_func = hashlib.new('sha3_384')
with open(filepath, 'rb') as f: with open(filepath, 'rb') as f:
while chunk := f.read(8192): while chunk := f.read(8192):
@@ -64,10 +65,18 @@ class PhotoService:
return dest_path 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: 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() travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first()
if not travel_diary: if not travel_diary:
return None 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 # Copy photo to diary's images directory
copied_path = self._copy_photo_to_diary(filepath, travel_diary) copied_path = self._copy_photo_to_diary(filepath, travel_diary)
@@ -79,8 +88,6 @@ class PhotoService:
except ValueError: except ValueError:
addition_date = None addition_date = None
# Calculate hash from the copied file
photo_hash = self._hash_file(copied_path)
new_photo = Photo( new_photo = Photo(
filepath=str(copied_path), # Store the path to the copied file filepath=str(copied_path), # Store the path to the copied file
@@ -118,7 +125,7 @@ class PhotoService:
old_path.unlink() old_path.unlink()
original.filepath = str(new_path) original.filepath = str(new_path)
# Update hash based on the new copied file # 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.name = photo_dst.name
original.addition_date = photo_dst.addition_date original.addition_date = photo_dst.addition_date
+2 -1
View File
@@ -611,7 +611,8 @@ class EditEntryScreen(Screen):
addition_date=original_photo.addition_date, addition_date=original_photo.addition_date,
caption=photo_data["caption"], caption=photo_data["caption"],
entries=original_photo.entries if original_photo.entries is not None else [], entries=original_photo.entries if original_photo.entries is not None else [],
id=original_photo.id id=original_photo.id,
photo_hash=original_photo.photo_hash,
) )
result = photo_service.update(original_photo, updated_photo) result = photo_service.update(original_photo, updated_photo)
@@ -15,12 +15,6 @@ class AddPhotoModal(Screen):
self.result = None self.result = None
self.created_photo = 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: def compose(self) -> ComposeResult:
yield Container( yield Container(
@@ -69,10 +63,16 @@ class AddPhotoModal(Screen):
async def _async_create_photo(self, photo_data: dict): async def _async_create_photo(self, photo_data: dict):
"""Creates a new photo asynchronously using PhotoService""" """Creates a new photo asynchronously using PhotoService"""
try: try:
service_manager = self.app.service_manager service_manager = self.app.service_manager
photo_service = service_manager.get_photo_service() 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( new_photo = photo_service.create(
filepath=Path(photo_data["filepath"]), filepath=Path(photo_data["filepath"]),
name=photo_data["name"], name=photo_data["name"],
@@ -82,13 +82,9 @@ class AddPhotoModal(Screen):
if new_photo: if new_photo:
self.created_photo = 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: {photo_hash}\nReference: \\[\\[photo:{new_photo.name}:{photo_hash}\\]\\]", 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]}\\]\\]",
severity="information", timeout=5) severity="information", timeout=5)
# Return the created photo data to the calling screen # Return the created photo data to the calling screen
@@ -97,7 +93,7 @@ class AddPhotoModal(Screen):
"name": photo_data["name"], "name": photo_data["name"],
"caption": photo_data["caption"], "caption": photo_data["caption"],
"photo_id": new_photo.id, "photo_id": new_photo.id,
"hash": photo_hash "hash": new_photo.photo_hash
} }
self.dismiss(self.result) self.dismiss(self.result)
else: else:
@@ -3,7 +3,7 @@ from textual.screen import Screen
from textual.widgets import Static, Input, Button from textual.widgets import Static, Input, Button
from textual.containers import Container, Horizontal from textual.containers import Container, Horizontal
from pilgrim.models.photo import Photo from pilgrim.models.photo import Photo
import hashlib
class EditPhotoModal(Screen): class EditPhotoModal(Screen):
"""Modal for editing an existing photo (name and caption only)""" """Modal for editing an existing photo (name and caption only)"""
@@ -12,15 +12,11 @@ class EditPhotoModal(Screen):
self.photo = photo self.photo = photo
self.result = None 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: def compose(self) -> ComposeResult:
# Generate hash for this photo # Generate hash for this photo
photo_hash = self._generate_photo_hash(self.photo)
yield Container( yield Container(
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"), Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
@@ -45,10 +41,9 @@ class EditPhotoModal(Screen):
id="caption-input", id="caption-input",
classes="EditPhotoModal-Input" classes="EditPhotoModal-Input"
), ),
Static(f"🔗 Photo Hash: {photo_hash}", classes="EditPhotoModal-Hash"), Static(f"🔗 Photo Hash: {self.photo.photo_hash[:8]}", classes="EditPhotoModal-Hash"),
Static("Reference formats:", classes="EditPhotoModal-Label"), Static("Reference formats:", classes="EditPhotoModal-Label"),
Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"), Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
Horizontal( Horizontal(
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"), Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"), Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
+10 -9
View File
@@ -391,7 +391,7 @@ Screen.-modal {
.EditEntryScreen-sidebar{ .EditEntryScreen-sidebar{
background: $primary-darken-1; background: $primary-darken-1;
width: 35%; width: 45%;
} }
.EditEntryScreen-sidebar-title { .EditEntryScreen-sidebar-title {
text-align: center; text-align: center;
@@ -405,23 +405,24 @@ Screen.-modal {
.EditEntryScreen-sidebar-content { .EditEntryScreen-sidebar-content {
height: 1fr; height: 1fr;
layout: vertical; layout: vertical;
margin-left: 2; margin-left: 1;
margin-right: 2; margin-right: 1;
} }
.EditEntryScreen-sidebar-photo-list { .EditEntryScreen-sidebar-photo-list {
height: 0.5fr; /* Pega o espaço restante disponível */ height: 2fr; /* Pega o espaço restante disponível */
min-height: 8; /* Garante altura mínima para não sumir */ min-height: 5; /* Garante altura mínima para não sumir */
border: solid $accent; border: solid $accent;
margin-bottom: 1; margin-bottom: 1;
overflow-y: auto; /* Adiciona scroll vertical se necessário */ overflow-y: auto; /* Adiciona scroll vertical se necessário */
} }
.EditEntryScreen-sidebar-photo-info { .EditEntryScreen-sidebar-photo-info {
height: 0.5fr; height: 1fr;
max-height: 13; /* Limita altura máxima */ max-height: 15; /* Limita altura máxima */
min-height: 5; min-height: 13;
padding: 1;
border: solid $warning; border: solid $warning;
margin-bottom: 1; margin-bottom: 1;
background: $surface-darken-1; background: $surface-darken-1;
@@ -430,7 +431,7 @@ Screen.-modal {
.EditEntryScreen-sidebar-help { .EditEntryScreen-sidebar-help {
height: 1fr; height: 1fr;
max-height: 12; /* Altura máxima menor que info */ max-height: 10; /* Altura máxima menor que info */
min-height: 8; /* Altura mínima menor */ min-height: 8; /* Altura mínima menor */
border: solid $success; border: solid $success;
padding: 1; padding: 1;
+40
View File
@@ -0,0 +1,40 @@
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()