feat: Enhance Search component with date taken filters and tag management
This commit adds new date taken filters to the Search component, allowing users to specify a date range for photos based on when they were taken. Additionally, the tagging functionality is improved with options to filter by tags during searches, including a match mode for tag selection. The UI is updated to accommodate these features, enhancing user experience and search capabilities. Documentation has been updated to reflect these changes.
This commit is contained in:
+18
-12
@@ -99,6 +99,15 @@ def search_photos(
|
||||
).first()
|
||||
return favorite is not None
|
||||
|
||||
# Parse date filters for use as additional filters (when not using date search type)
|
||||
df = date.fromisoformat(date_from) if date_from else None
|
||||
dt = date.fromisoformat(date_to) if date_to else None
|
||||
|
||||
# Parse tag filters for use as additional filters
|
||||
tag_list = None
|
||||
if tag_names:
|
||||
tag_list = [t.strip() for t in tag_names.split(",") if t.strip()]
|
||||
|
||||
if search_type == "name":
|
||||
if not person_name:
|
||||
raise HTTPException(
|
||||
@@ -106,7 +115,7 @@ def search_photos(
|
||||
detail="person_name is required for name search",
|
||||
)
|
||||
results, total = search_photos_by_name(
|
||||
db, person_name, folder_path, media_type, page, page_size
|
||||
db, person_name, folder_path, media_type, df, dt, tag_list, match_all, page, page_size
|
||||
)
|
||||
for photo, full_name in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
@@ -134,9 +143,7 @@ def search_photos(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="At least one of date_from or date_to is required",
|
||||
)
|
||||
df = date.fromisoformat(date_from) if date_from else None
|
||||
dt = date.fromisoformat(date_to) if date_to else None
|
||||
results, total = search_photos_by_date(db, df, dt, folder_path, media_type, page, page_size)
|
||||
results, total = search_photos_by_date(db, df, dt, folder_path, media_type, tag_list, match_all, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
@@ -164,14 +171,13 @@ def search_photos(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="tag_names is required for tag search",
|
||||
)
|
||||
tag_list = [t.strip() for t in tag_names.split(",") if t.strip()]
|
||||
if not tag_list:
|
||||
if not tag_list or len(tag_list) == 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="At least one tag name is required",
|
||||
)
|
||||
results, total = search_photos_by_tags(
|
||||
db, tag_list, match_all, folder_path, media_type, page, page_size
|
||||
db, tag_list, match_all, folder_path, media_type, df, dt, page, page_size
|
||||
)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
@@ -195,7 +201,7 @@ def search_photos(
|
||||
)
|
||||
)
|
||||
elif search_type == "no_faces":
|
||||
results, total = get_photos_without_faces(db, folder_path, media_type, page, page_size)
|
||||
results, total = get_photos_without_faces(db, folder_path, media_type, df, dt, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
# Convert datetime to date for date_added
|
||||
@@ -216,7 +222,7 @@ def search_photos(
|
||||
)
|
||||
)
|
||||
elif search_type == "no_tags":
|
||||
results, total = get_photos_without_tags(db, folder_path, media_type, page, page_size)
|
||||
results, total = get_photos_without_tags(db, folder_path, media_type, df, dt, page, page_size)
|
||||
for photo in results:
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
person_name_val = get_photo_person(db, photo.id)
|
||||
@@ -238,7 +244,7 @@ def search_photos(
|
||||
)
|
||||
)
|
||||
elif search_type == "processed":
|
||||
results, total = get_processed_photos(db, folder_path, media_type, page, page_size)
|
||||
results, total = get_processed_photos(db, folder_path, media_type, df, dt, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
@@ -261,7 +267,7 @@ def search_photos(
|
||||
)
|
||||
)
|
||||
elif search_type == "unprocessed":
|
||||
results, total = get_unprocessed_photos(db, folder_path, media_type, page, page_size)
|
||||
results, total = get_unprocessed_photos(db, folder_path, media_type, df, dt, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
@@ -289,7 +295,7 @@ def search_photos(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required for favorites search",
|
||||
)
|
||||
results, total = get_favorite_photos(db, username, folder_path, media_type, page, page_size)
|
||||
results, total = get_favorite_photos(db, username, folder_path, media_type, df, dt, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
|
||||
@@ -16,6 +16,10 @@ def search_photos_by_name(
|
||||
person_name: str,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
tag_names: Optional[List[str]] = None,
|
||||
match_all: bool = False,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Tuple[Photo, str]], int]:
|
||||
@@ -81,6 +85,42 @@ def search_photos_by_name(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Apply tag filter if provided
|
||||
if tag_names:
|
||||
# Find tag IDs (case-insensitive)
|
||||
tag_ids = (
|
||||
db.query(Tag.id)
|
||||
.filter(func.lower(Tag.tag_name).in_([t.lower().strip() for t in tag_names]))
|
||||
.all()
|
||||
)
|
||||
tag_ids = [tid[0] for tid in tag_ids]
|
||||
|
||||
if tag_ids:
|
||||
if match_all:
|
||||
# Photos that have ALL specified tags
|
||||
query = (
|
||||
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
|
||||
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
|
||||
.group_by(Photo.id, Person.id)
|
||||
.having(func.count(func.distinct(PhotoTagLinkage.tag_id)) == len(tag_ids))
|
||||
)
|
||||
else:
|
||||
# Photos that have ANY of the specified tags
|
||||
tagged_photo_ids_subquery = (
|
||||
db.query(PhotoTagLinkage.photo_id)
|
||||
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
|
||||
)
|
||||
query = query.filter(Photo.id.in_(tagged_photo_ids_subquery))
|
||||
else:
|
||||
# No matching tags found - return empty result
|
||||
return [], 0
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -102,6 +142,8 @@ def search_photos_by_date(
|
||||
date_to: Optional[date] = None,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
tag_names: Optional[List[str]] = None,
|
||||
match_all: bool = False,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -129,6 +171,36 @@ def search_photos_by_date(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply tag filter if provided
|
||||
if tag_names:
|
||||
# Find tag IDs (case-insensitive)
|
||||
tag_ids = (
|
||||
db.query(Tag.id)
|
||||
.filter(func.lower(Tag.tag_name).in_([t.lower().strip() for t in tag_names]))
|
||||
.all()
|
||||
)
|
||||
tag_ids = [tid[0] for tid in tag_ids]
|
||||
|
||||
if tag_ids:
|
||||
if match_all:
|
||||
# Photos that have ALL specified tags
|
||||
query = (
|
||||
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
|
||||
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
|
||||
.group_by(Photo.id)
|
||||
.having(func.count(func.distinct(PhotoTagLinkage.tag_id)) == len(tag_ids))
|
||||
)
|
||||
else:
|
||||
# Photos that have ANY of the specified tags
|
||||
tagged_photo_ids_subquery = (
|
||||
db.query(PhotoTagLinkage.photo_id)
|
||||
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
|
||||
)
|
||||
query = query.filter(Photo.id.in_(tagged_photo_ids_subquery))
|
||||
else:
|
||||
# No matching tags found - return empty result
|
||||
return [], 0
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -144,6 +216,8 @@ def search_photos_by_tags(
|
||||
match_all: bool = False,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -196,6 +270,12 @@ def search_photos_by_tags(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -209,6 +289,8 @@ def get_photos_without_faces(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -234,6 +316,12 @@ def get_photos_without_faces(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -247,6 +335,8 @@ def get_photos_without_tags(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -270,6 +360,12 @@ def get_photos_without_tags(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -312,6 +408,8 @@ def get_processed_photos(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -331,6 +429,12 @@ def get_processed_photos(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
@@ -345,6 +449,8 @@ def get_favorite_photos(
|
||||
username: str,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -368,6 +474,12 @@ def get_favorite_photos(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
total = query.count()
|
||||
|
||||
# Order by favorite date (most recent first), then date_taken
|
||||
@@ -387,6 +499,8 @@ def get_unprocessed_photos(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
media_type: Optional[str] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
@@ -406,6 +520,12 @@ def get_unprocessed_photos(
|
||||
if media_type and media_type.lower() != "all":
|
||||
query = query.filter(Photo.media_type == media_type.lower())
|
||||
|
||||
# Apply date taken filter if provided
|
||||
if date_from:
|
||||
query = query.filter(Photo.date_taken >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(Photo.date_taken <= date_to)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user