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
Gustavo Henrique Santos Souza de Miranda 165bc10a63 Change the CSS to fit the text better and also limited the width of the sidebar 2025-07-17 20:48:59 -03:00
10 changed files with 190 additions and 89 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
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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## [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)
- Organization of trips by date, location, or theme
- Enhanced photo management features
- Search functionality
- Export features
- Testing implementation
### 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
## [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
[Unreleased]: https://github.com/username/pilgrim/compare/v0.0.1...HEAD
[0.0.1]: https://github.com/username/pilgrim/releases/tag/v0.0.1
* Installation Method 1 not yet implemented
* No testing suite implemented yet
* Some features may be unstable in an alpha version
+4
View File
@@ -4,6 +4,7 @@ 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 ..database import Base
@@ -24,6 +25,9 @@ class Photo(Base):
)
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):
super().__init__(**kw)
+12 -5
View File
@@ -14,8 +14,9 @@ class PhotoService:
def __init__(self, session):
self.session = session
def _hash_file(self, filepath: Path) -> str:
"""Calculate hash of a file using SHA3-384."""
@staticmethod
def hash_file(filepath: Path) -> str:
"""Calculate the hash of a file using SHA3-384."""
hash_func = hashlib.new('sha3_384')
with open(filepath, 'rb') as f:
while chunk := f.read(8192):
@@ -64,10 +65,18 @@ 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)
@@ -79,8 +88,6 @@ 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
@@ -118,7 +125,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
+2 -1
View File
@@ -611,7 +611,8 @@ 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
id=original_photo.id,
photo_hash=original_photo.photo_hash,
)
result = photo_service.update(original_photo, updated_photo)
@@ -15,12 +15,6 @@ 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(
@@ -69,10 +63,16 @@ 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,13 +82,9 @@ 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: {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)
# Return the created photo data to the calling screen
@@ -97,7 +93,7 @@ class AddPhotoModal(Screen):
"name": photo_data["name"],
"caption": photo_data["caption"],
"photo_id": new_photo.id,
"hash": photo_hash
"hash": new_photo.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,15 +12,11 @@ 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"),
@@ -45,10 +41,9 @@ class EditPhotoModal(Screen):
id="caption-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(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
Horizontal(
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
+8 -6
View File
@@ -391,6 +391,7 @@ Screen.-modal {
.EditEntryScreen-sidebar{
background: $primary-darken-1;
width: 45%;
}
.EditEntryScreen-sidebar-title {
text-align: center;
@@ -404,23 +405,24 @@ Screen.-modal {
.EditEntryScreen-sidebar-content {
height: 1fr;
layout: vertical;
margin-left: 2;
margin-right: 2;
margin-left: 1;
margin-right: 1;
}
.EditEntryScreen-sidebar-photo-list {
height: 0.5fr; /* Pega o espaço restante disponível */
min-height: 10; /* Garante altura mínima para não sumir */
height: 2fr; /* Pega o espaço restante disponível */
min-height: 5; /* Garante altura mínima para não sumir */
border: solid $accent;
margin-bottom: 1;
overflow-y: auto; /* Adiciona scroll vertical se necessário */
}
.EditEntryScreen-sidebar-photo-info {
height: 0.5fr;
height: 1fr;
max-height: 15; /* Limita altura máxima */
min-height: 6;
min-height: 13;
padding: 1;
border: solid $warning;
margin-bottom: 1;
background: $surface-darken-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()