Spaces:
Running
Running
feat: disable renaming for topics and transition to per-user title uniqueness constraints
Browse files- materials/routes.py +23 -8
- store.py +5 -2
materials/routes.py
CHANGED
|
@@ -64,7 +64,10 @@ async def _process_pdf_background(material_id: str, file_content: bytes):
|
|
| 64 |
loop = asyncio.get_event_loop()
|
| 65 |
# Skip processing if this user already has a material with this title
|
| 66 |
mat = await loop.run_in_executor(None, get_material, material_id)
|
| 67 |
-
if mat and await loop.run_in_executor(
|
|
|
|
|
|
|
|
|
|
| 68 |
logger.info(f"Skipping processing for {material_id}: duplicate title")
|
| 69 |
await loop.run_in_executor(None, update_material_status, material_id, "failed", "Duplicate title. Please rename to retry.")
|
| 70 |
return
|
|
@@ -88,7 +91,10 @@ async def _process_url_background(material_id: str, url: str):
|
|
| 88 |
loop = asyncio.get_event_loop()
|
| 89 |
# Skip if this user already has a material with this title
|
| 90 |
mat = await loop.run_in_executor(None, get_material, material_id)
|
| 91 |
-
if mat and await loop.run_in_executor(
|
|
|
|
|
|
|
|
|
|
| 92 |
logger.info(f"Skipping URL processing for {material_id}: duplicate title, waiting for rename")
|
| 93 |
return
|
| 94 |
|
|
@@ -155,8 +161,11 @@ async def scrape_url(
|
|
| 155 |
))
|
| 156 |
material_id = material["id"]
|
| 157 |
|
| 158 |
-
# Skip processing if title conflicts — user must rename first
|
| 159 |
-
is_taken = await loop.run_in_executor(
|
|
|
|
|
|
|
|
|
|
| 160 |
if not is_taken:
|
| 161 |
background_tasks.add_task(_process_url_background, material_id, input.url)
|
| 162 |
|
|
@@ -208,12 +217,18 @@ async def rename_material_endpoint(
|
|
| 208 |
raise HTTPException(404, "Material not found")
|
| 209 |
if mat.get("user_id") != user_id:
|
| 210 |
raise HTTPException(403, "Not authorized to rename this material")
|
|
|
|
|
|
|
|
|
|
| 211 |
new_title = body.title.strip()
|
| 212 |
if not new_title:
|
| 213 |
raise HTTPException(400, "Title cannot be empty")
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
| 217 |
await loop.run_in_executor(None, lambda: rename_material(material_id, new_title))
|
| 218 |
|
| 219 |
# If the material was pending due to title conflict, try processing now
|
|
@@ -233,7 +248,7 @@ async def create_topic(
|
|
| 233 |
user_id: str = Depends(get_current_user_id)
|
| 234 |
):
|
| 235 |
if is_title_taken(body.topic, user_id=user_id):
|
| 236 |
-
raise HTTPException(409, "
|
| 237 |
mat = create_material(
|
| 238 |
user_id=user_id,
|
| 239 |
title=body.topic.strip(),
|
|
|
|
| 64 |
loop = asyncio.get_event_loop()
|
| 65 |
# Skip processing if this user already has a material with this title
|
| 66 |
mat = await loop.run_in_executor(None, get_material, material_id)
|
| 67 |
+
if mat and await loop.run_in_executor(
|
| 68 |
+
None,
|
| 69 |
+
lambda: is_title_taken(mat.get("title", ""), exclude_id=material_id, user_id=mat.get("user_id"))
|
| 70 |
+
):
|
| 71 |
logger.info(f"Skipping processing for {material_id}: duplicate title")
|
| 72 |
await loop.run_in_executor(None, update_material_status, material_id, "failed", "Duplicate title. Please rename to retry.")
|
| 73 |
return
|
|
|
|
| 91 |
loop = asyncio.get_event_loop()
|
| 92 |
# Skip if this user already has a material with this title
|
| 93 |
mat = await loop.run_in_executor(None, get_material, material_id)
|
| 94 |
+
if mat and await loop.run_in_executor(
|
| 95 |
+
None,
|
| 96 |
+
lambda: is_title_taken(mat.get("title", ""), exclude_id=material_id, user_id=mat.get("user_id"))
|
| 97 |
+
):
|
| 98 |
logger.info(f"Skipping URL processing for {material_id}: duplicate title, waiting for rename")
|
| 99 |
return
|
| 100 |
|
|
|
|
| 161 |
))
|
| 162 |
material_id = material["id"]
|
| 163 |
|
| 164 |
+
# Skip processing if title conflicts within user scope — user must rename first
|
| 165 |
+
is_taken = await loop.run_in_executor(
|
| 166 |
+
None,
|
| 167 |
+
lambda: is_title_taken(input.url, exclude_id=material_id, user_id=user_id)
|
| 168 |
+
)
|
| 169 |
if not is_taken:
|
| 170 |
background_tasks.add_task(_process_url_background, material_id, input.url)
|
| 171 |
|
|
|
|
| 217 |
raise HTTPException(404, "Material not found")
|
| 218 |
if mat.get("user_id") != user_id:
|
| 219 |
raise HTTPException(403, "Not authorized to rename this material")
|
| 220 |
+
# Topics have no URL — renaming is disabled for them
|
| 221 |
+
if mat.get("source_type") == "url" and not mat.get("url"):
|
| 222 |
+
raise HTTPException(403, "Custom topic names cannot be changed")
|
| 223 |
new_title = body.title.strip()
|
| 224 |
if not new_title:
|
| 225 |
raise HTTPException(400, "Title cannot be empty")
|
| 226 |
+
is_taken = await loop.run_in_executor(
|
| 227 |
+
None,
|
| 228 |
+
lambda: is_title_taken(new_title, exclude_id=material_id, user_id=user_id)
|
| 229 |
+
)
|
| 230 |
+
if is_taken:
|
| 231 |
+
raise HTTPException(409, "You already have a material with this title")
|
| 232 |
await loop.run_in_executor(None, lambda: rename_material(material_id, new_title))
|
| 233 |
|
| 234 |
# If the material was pending due to title conflict, try processing now
|
|
|
|
| 248 |
user_id: str = Depends(get_current_user_id)
|
| 249 |
):
|
| 250 |
if is_title_taken(body.topic, user_id=user_id):
|
| 251 |
+
raise HTTPException(409, "You already have a material with this title")
|
| 252 |
mat = create_material(
|
| 253 |
user_id=user_id,
|
| 254 |
title=body.topic.strip(),
|
store.py
CHANGED
|
@@ -159,6 +159,7 @@ def list_materials(user_id: str) -> list[dict]:
|
|
| 159 |
|
| 160 |
|
| 161 |
def is_title_taken(title: str, exclude_id: Optional[str] = None, user_id: Optional[str] = None) -> bool:
|
|
|
|
| 162 |
normalized = title.strip().lower()
|
| 163 |
if not normalized:
|
| 164 |
return False
|
|
@@ -211,7 +212,7 @@ def create_material(user_id: str, source_type: str, title: str,
|
|
| 211 |
if source_type == "topic":
|
| 212 |
actual_source_type = "url"
|
| 213 |
|
| 214 |
-
# Auto-resolve duplicate titles
|
| 215 |
original_title = title
|
| 216 |
counter = 1
|
| 217 |
while is_title_taken(title, user_id=user_id):
|
|
@@ -224,8 +225,10 @@ def create_material(user_id: str, source_type: str, title: str,
|
|
| 224 |
data["file_path"] = file_path
|
| 225 |
if url:
|
| 226 |
data["url"] = url
|
|
|
|
|
|
|
| 227 |
result = _robust_execute(_table_supabase("materials").insert(data))
|
| 228 |
-
|
| 229 |
ret_data = result.data[0]
|
| 230 |
if ret_data.get("source_type") == "url" and not ret_data.get("url"):
|
| 231 |
ret_data["source_type"] = "topic"
|
|
|
|
| 159 |
|
| 160 |
|
| 161 |
def is_title_taken(title: str, exclude_id: Optional[str] = None, user_id: Optional[str] = None) -> bool:
|
| 162 |
+
"""Check whether *title* is already used by *user_id* (or globally when user_id is None)."""
|
| 163 |
normalized = title.strip().lower()
|
| 164 |
if not normalized:
|
| 165 |
return False
|
|
|
|
| 212 |
if source_type == "topic":
|
| 213 |
actual_source_type = "url"
|
| 214 |
|
| 215 |
+
# Auto-resolve duplicate titles PER USER so each user's material list
|
| 216 |
original_title = title
|
| 217 |
counter = 1
|
| 218 |
while is_title_taken(title, user_id=user_id):
|
|
|
|
| 225 |
data["file_path"] = file_path
|
| 226 |
if url:
|
| 227 |
data["url"] = url
|
| 228 |
+
|
| 229 |
+
# Insert assuming the global constraint on `title` has been replaced with a per-user one
|
| 230 |
result = _robust_execute(_table_supabase("materials").insert(data))
|
| 231 |
+
|
| 232 |
ret_data = result.data[0]
|
| 233 |
if ret_data.get("source_type") == "url" and not ret_data.get("url"):
|
| 234 |
ret_data["source_type"] = "topic"
|