-
-
Notifications
You must be signed in to change notification settings - Fork 618
Solves : Implement the Memories page #723 Open #774
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
786ec3d
Step count progress bar overflow fixed
ritigya03 103c245
Fix non-functional back button in onboarding flow
ritigya03 5c14420
Fix onboarding step display and index safety
ritigya03 adb2a36
landing page tasks done
ritigya03 a3d4bc2
memory page added
ritigya03 1595647
fixed date issue
ritigya03 4b6d3ba
document updated
ritigya03 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,372 @@ | ||
| """ | ||
| Database operations for Memories feature. | ||
| Handles creation, retrieval, and management of photo memories. | ||
| """ | ||
|
|
||
| import sqlite3 | ||
| from typing import List, Optional, Dict, Any, Tuple | ||
| from datetime import datetime | ||
| from app.config.settings import DATABASE_PATH | ||
| from app.logging.setup_logging import get_logger | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
| # Type aliases | ||
| MemoryId = str | ||
| ImageId = str | ||
|
|
||
|
|
||
| def _connect() -> sqlite3.Connection: | ||
| """Create database connection with foreign key enforcement.""" | ||
| conn = sqlite3.connect(DATABASE_PATH) | ||
| conn.execute("PRAGMA foreign_keys = ON") | ||
| return conn | ||
|
|
||
|
|
||
| def db_create_memories_table() -> None: | ||
| """Create the memories table and related junction table.""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| # Main memories table | ||
| cursor.execute( | ||
| """ | ||
| CREATE TABLE IF NOT EXISTS memories ( | ||
| id TEXT PRIMARY KEY, | ||
| title TEXT NOT NULL, | ||
| memory_type TEXT NOT NULL, | ||
| start_date TEXT NOT NULL, | ||
| end_date TEXT NOT NULL, | ||
| location TEXT, | ||
| latitude REAL, | ||
| longitude REAL, | ||
| cover_image_id TEXT, | ||
| total_photos INTEGER DEFAULT 0, | ||
| created_at TEXT NOT NULL, | ||
| FOREIGN KEY (cover_image_id) REFERENCES images(id) ON DELETE SET NULL | ||
| ) | ||
| """ | ||
| ) | ||
|
|
||
| # Junction table for memory-image relationships | ||
| cursor.execute( | ||
| """ | ||
| CREATE TABLE IF NOT EXISTS memory_images ( | ||
| memory_id TEXT, | ||
| image_id TEXT, | ||
| is_representative BOOLEAN DEFAULT 0, | ||
| PRIMARY KEY (memory_id, image_id), | ||
| FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE, | ||
| FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE | ||
| ) | ||
| """ | ||
| ) | ||
|
|
||
| # Index for faster queries | ||
| cursor.execute( | ||
| """ | ||
| CREATE INDEX IF NOT EXISTS idx_memories_dates | ||
| ON memories(start_date, end_date) | ||
| """ | ||
| ) | ||
|
|
||
| cursor.execute( | ||
| """ | ||
| CREATE INDEX IF NOT EXISTS idx_memories_type | ||
| ON memories(memory_type) | ||
| """ | ||
| ) | ||
|
|
||
| cursor.execute( | ||
| """ | ||
| CREATE INDEX IF NOT EXISTS idx_memory_images_representative | ||
| ON memory_images(memory_id, is_representative) | ||
| """ | ||
| ) | ||
|
|
||
| conn.commit() | ||
| logger.info("Memories tables created successfully") | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error creating memories tables: {e}") | ||
| conn.rollback() | ||
| raise | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_insert_memory( | ||
| memory_id: str, | ||
| title: str, | ||
| memory_type: str, | ||
| start_date: str, | ||
| end_date: str, | ||
| location: Optional[str] = None, | ||
| latitude: Optional[float] = None, | ||
| longitude: Optional[float] = None, | ||
| cover_image_id: Optional[str] = None, | ||
| total_photos: int = 0, | ||
| ) -> bool: | ||
| """Insert a new memory into the database.""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| created_at = datetime.now().isoformat() | ||
|
|
||
| cursor.execute( | ||
| """ | ||
| INSERT INTO memories | ||
| (id, title, memory_type, start_date, end_date, location, | ||
| latitude, longitude, cover_image_id, total_photos, created_at) | ||
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| """, | ||
| ( | ||
| memory_id, | ||
| title, | ||
| memory_type, | ||
| start_date, | ||
| end_date, | ||
| location, | ||
| latitude, | ||
| longitude, | ||
| cover_image_id, | ||
| total_photos, | ||
| created_at, | ||
| ), | ||
| ) | ||
|
|
||
| conn.commit() | ||
| logger.info(f"Memory '{title}' created successfully with ID: {memory_id}") | ||
| return True | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error inserting memory: {e}") | ||
| conn.rollback() | ||
| return False | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_insert_memory_images( | ||
| memory_id: str, | ||
| image_ids: List[str], | ||
| representative_ids: List[str] = None | ||
| ) -> bool: | ||
| """ | ||
| Link images to a memory. | ||
|
|
||
| Args: | ||
| memory_id: ID of the memory | ||
| image_ids: List of all image IDs in this memory | ||
| representative_ids: List of image IDs to mark as representative (for cards) | ||
| """ | ||
| if not image_ids: | ||
| return True | ||
|
|
||
| representative_set = set(representative_ids) if representative_ids else set() | ||
|
|
||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| records = [ | ||
| (memory_id, img_id, img_id in representative_set) | ||
| for img_id in image_ids | ||
| ] | ||
|
|
||
| cursor.executemany( | ||
| """ | ||
| INSERT OR IGNORE INTO memory_images | ||
| (memory_id, image_id, is_representative) | ||
| VALUES (?, ?, ?) | ||
| """, | ||
| records, | ||
| ) | ||
|
|
||
| conn.commit() | ||
| return True | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error inserting memory images: {e}") | ||
| conn.rollback() | ||
| return False | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_get_all_memories() -> List[Dict[str, Any]]: | ||
| """Retrieve all memories with their representative images.""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| cursor.execute( | ||
| """ | ||
| SELECT | ||
| m.id, | ||
| m.title, | ||
| m.memory_type, | ||
| m.start_date, | ||
| m.end_date, | ||
| m.location, | ||
| m.latitude, | ||
| m.longitude, | ||
| m.cover_image_id, | ||
| m.total_photos, | ||
| m.created_at, | ||
| GROUP_CONCAT( | ||
| CASE WHEN mi.is_representative = 1 | ||
| THEN i.thumbnailPath END | ||
| ) as representative_thumbnails | ||
| FROM memories m | ||
| LEFT JOIN memory_images mi ON m.id = mi.memory_id | ||
| LEFT JOIN images i ON mi.image_id = i.id | ||
| GROUP BY m.id | ||
| ORDER BY m.start_date DESC | ||
| """ | ||
| ) | ||
|
|
||
| results = cursor.fetchall() | ||
|
|
||
| memories = [] | ||
| for row in results: | ||
| thumbnails = row[11].split(',') if row[11] else [] | ||
| # Filter out None values | ||
| thumbnails = [t for t in thumbnails if t] | ||
|
|
||
| memories.append({ | ||
| "id": row[0], | ||
| "title": row[1], | ||
| "memory_type": row[2], | ||
| "start_date": row[3], | ||
| "end_date": row[4], | ||
| "location": row[5], | ||
| "latitude": row[6], | ||
| "longitude": row[7], | ||
| "cover_image_id": row[8], | ||
| "total_photos": row[9], | ||
| "created_at": row[10], | ||
| "representative_thumbnails": thumbnails, | ||
| }) | ||
|
|
||
| return memories | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error retrieving memories: {e}") | ||
| return [] | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_get_memory_by_id(memory_id: str) -> Optional[Dict[str, Any]]: | ||
| """Get a specific memory with all its images.""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| # Get memory details | ||
| cursor.execute( | ||
| """ | ||
| SELECT | ||
| id, title, memory_type, start_date, end_date, | ||
| location, latitude, longitude, cover_image_id, | ||
| total_photos, created_at | ||
| FROM memories | ||
| WHERE id = ? | ||
| """, | ||
| (memory_id,), | ||
| ) | ||
|
|
||
| memory_row = cursor.fetchone() | ||
| if not memory_row: | ||
| return None | ||
|
|
||
| # Get all images in this memory | ||
| cursor.execute( | ||
| """ | ||
| SELECT | ||
| i.id, i.path, i.thumbnailPath, i.metadata, | ||
| mi.is_representative | ||
| FROM memory_images mi | ||
| JOIN images i ON mi.image_id = i.id | ||
| WHERE mi.memory_id = ? | ||
| ORDER BY i.path | ||
| """, | ||
| (memory_id,), | ||
| ) | ||
|
|
||
| images = [] | ||
| for img_row in cursor.fetchall(): | ||
| from app.utils.images import image_util_parse_metadata | ||
|
|
||
| images.append({ | ||
| "id": img_row[0], | ||
| "path": img_row[1], | ||
| "thumbnailPath": img_row[2], | ||
| "metadata": image_util_parse_metadata(img_row[3]), | ||
| "is_representative": bool(img_row[4]), | ||
| }) | ||
|
|
||
| return { | ||
| "id": memory_row[0], | ||
| "title": memory_row[1], | ||
| "memory_type": memory_row[2], | ||
| "start_date": memory_row[3], | ||
| "end_date": memory_row[4], | ||
| "location": memory_row[5], | ||
| "latitude": memory_row[6], | ||
| "longitude": memory_row[7], | ||
| "cover_image_id": memory_row[8], | ||
| "total_photos": memory_row[9], | ||
| "created_at": memory_row[10], | ||
| "images": images, | ||
| } | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error retrieving memory {memory_id}: {e}") | ||
| return None | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_delete_memory(memory_id: str) -> bool: | ||
| """Delete a memory (cascade will remove memory_images entries).""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| cursor.execute("DELETE FROM memories WHERE id = ?", (memory_id,)) | ||
| conn.commit() | ||
|
|
||
| if cursor.rowcount > 0: | ||
| logger.info(f"Memory {memory_id} deleted successfully") | ||
| return True | ||
| return False | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error deleting memory {memory_id}: {e}") | ||
| conn.rollback() | ||
| return False | ||
| finally: | ||
| conn.close() | ||
|
|
||
|
|
||
| def db_clear_all_memories() -> bool: | ||
| """Clear all memories from the database.""" | ||
| conn = _connect() | ||
| cursor = conn.cursor() | ||
|
|
||
| try: | ||
| cursor.execute("DELETE FROM memories") | ||
| conn.commit() | ||
| logger.info("All memories cleared from database") | ||
| return True | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error clearing memories: {e}") | ||
| conn.rollback() | ||
| return False | ||
| finally: | ||
| conn.close() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Move import outside the loop for efficiency.
The import statement inside the
forloop executes on every iteration. While Python caches imports, placing it inside a loop is unnecessary overhead and unconventional.Move the import to the top of the function or module:
def db_get_memory_by_id(memory_id: str) -> Optional[Dict[str, Any]]: """Get a specific memory with all its images.""" + from app.utils.images import image_util_parse_metadata + conn = _connect() cursor = conn.cursor() ... images = [] for img_row in cursor.fetchall(): - from app.utils.images import image_util_parse_metadata - images.append({🤖 Prompt for AI Agents