mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
i18n and tests
This commit is contained in:
parent
317ca7bb1c
commit
b407334f56
@ -504,7 +504,14 @@ class TestHttpExport(BaseTestHttp):
|
|||||||
assert row.in_progress is False
|
assert row.in_progress is False
|
||||||
assert os.path.exists(done_video)
|
assert os.path.exists(done_video)
|
||||||
|
|
||||||
def test_batch_export_requires_case_target(self):
|
def test_batch_export_without_case_goes_to_uncategorized(self):
|
||||||
|
"""Exports without a case target go to uncategorized."""
|
||||||
|
self._insert_recording("rec-front", "front_door", 100, 400)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"frigate.api.export.start_export_job",
|
||||||
|
side_effect=lambda _config, job: job.id,
|
||||||
|
):
|
||||||
with AuthTestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/exports/batch",
|
"/exports/batch",
|
||||||
@ -519,11 +526,10 @@ class TestHttpExport(BaseTestHttp):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 422
|
assert response.status_code == 202
|
||||||
assert (
|
response_json = response.json()
|
||||||
response.json()["detail"][0]["msg"]
|
assert response_json["export_case_id"] is None
|
||||||
== "Value error, Either export_case_id or new_case_name must be provided"
|
assert ExportCase.select().count() == 0
|
||||||
)
|
|
||||||
|
|
||||||
# --- /exports/batch (item-shaped multi-export) ---------------------------
|
# --- /exports/batch (item-shaped multi-export) ---------------------------
|
||||||
|
|
||||||
@ -651,10 +657,18 @@ class TestHttpExport(BaseTestHttp):
|
|||||||
== "Value error, end_time must be after start_time"
|
== "Value error, end_time must be after start_time"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_batch_export_missing_case_target_rejected(self):
|
def test_batch_export_non_admin_without_case_goes_to_uncategorized(self):
|
||||||
|
"""Non-admin batch exports go to uncategorized."""
|
||||||
|
self._insert_recording("rec-front", "front_door", 100, 400)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"frigate.api.export.start_export_job",
|
||||||
|
side_effect=lambda _config, job: job.id,
|
||||||
|
):
|
||||||
with AuthTestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/exports/batch",
|
"/exports/batch",
|
||||||
|
headers={"remote-user": "viewer", "remote-role": "viewer"},
|
||||||
json={
|
json={
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
@ -666,11 +680,10 @@ class TestHttpExport(BaseTestHttp):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 422
|
assert response.status_code == 202
|
||||||
assert (
|
response_json = response.json()
|
||||||
response.json()["detail"][0]["msg"]
|
assert response_json["export_case_id"] is None
|
||||||
== "Value error, Either export_case_id or new_case_name must be provided"
|
assert ExportCase.select().count() == 0
|
||||||
)
|
|
||||||
|
|
||||||
def test_batch_export_camera_access_denied_fails_closed(self):
|
def test_batch_export_camera_access_denied_fails_closed(self):
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@ -1108,3 +1121,313 @@ class TestHttpExport(BaseTestHttp):
|
|||||||
|
|
||||||
assert response.status_code == 202
|
assert response.status_code == 202
|
||||||
assert response.json()["success"] is True
|
assert response.json()["success"] is True
|
||||||
|
|
||||||
|
# ── Bulk delete exports ────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_success(self):
|
||||||
|
"""All IDs exist, none in-progress → 200, all deleted."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp2",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_2",
|
||||||
|
date=200,
|
||||||
|
video_path="/tmp/exp2.mp4",
|
||||||
|
thumb_path="/tmp/exp2.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
json={"ids": ["exp1", "exp2"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["success"] is True
|
||||||
|
assert Export.select().count() == 0
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_single_item(self):
|
||||||
|
"""Regression: single-item delete via batch endpoint."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
json={"ids": ["exp1"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert Export.select().count() == 0
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_some_missing(self):
|
||||||
|
"""Some IDs don't exist → 404, nothing deleted."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
json={"ids": ["exp1", "nonexistent"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
# Nothing deleted
|
||||||
|
assert Export.select().count() == 1
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_all_missing(self):
|
||||||
|
"""All IDs don't exist → 404."""
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
json={"ids": ["nope1", "nope2"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_in_progress(self):
|
||||||
|
"""Some exports in-progress → 400, nothing deleted."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path=f"{os.environ.get('EXPORT_DIR', '/media/frigate/exports')}/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"frigate.api.export._get_files_in_use",
|
||||||
|
return_value={"exp1.mp4"},
|
||||||
|
):
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
json={"ids": ["exp1"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert Export.select().count() == 1
|
||||||
|
|
||||||
|
def test_bulk_delete_exports_non_admin_rejected(self):
|
||||||
|
"""Non-admin users cannot bulk delete."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/delete",
|
||||||
|
headers={"remote-user": "viewer", "remote-role": "viewer"},
|
||||||
|
json={"ids": ["exp1"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert Export.select().count() == 1
|
||||||
|
|
||||||
|
# ── Bulk reassign exports ──────────────────────────────────────
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_to_case(self):
|
||||||
|
"""All IDs exist, case exists → 200, all reassigned."""
|
||||||
|
ExportCase.create(
|
||||||
|
id="case1",
|
||||||
|
name="Test Case",
|
||||||
|
description="",
|
||||||
|
created_at=10,
|
||||||
|
updated_at=10,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp2",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_2",
|
||||||
|
date=200,
|
||||||
|
video_path="/tmp/exp2.mp4",
|
||||||
|
thumb_path="/tmp/exp2.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
json={"ids": ["exp1", "exp2"], "export_case_id": "case1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["success"] is True
|
||||||
|
for exp_id in ["exp1", "exp2"]:
|
||||||
|
exp = Export.get(Export.id == exp_id)
|
||||||
|
assert exp.export_case_id == "case1"
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_to_null(self):
|
||||||
|
"""Reassign to null (uncategorize) → 200."""
|
||||||
|
ExportCase.create(
|
||||||
|
id="case1",
|
||||||
|
name="Test Case",
|
||||||
|
description="",
|
||||||
|
created_at=10,
|
||||||
|
updated_at=10,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
export_case="case1",
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
json={"ids": ["exp1"], "export_case_id": None},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
exp = Export.get(Export.id == "exp1")
|
||||||
|
assert exp.export_case_id is None
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_single_item(self):
|
||||||
|
"""Regression: single-item reassign via batch endpoint."""
|
||||||
|
ExportCase.create(
|
||||||
|
id="case1",
|
||||||
|
name="Test Case",
|
||||||
|
description="",
|
||||||
|
created_at=10,
|
||||||
|
updated_at=10,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
json={"ids": ["exp1"], "export_case_id": "case1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
exp = Export.get(Export.id == "exp1")
|
||||||
|
assert exp.export_case_id == "case1"
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_some_missing(self):
|
||||||
|
"""Some IDs don't exist → 404, nothing reassigned."""
|
||||||
|
ExportCase.create(
|
||||||
|
id="case1",
|
||||||
|
name="Test Case",
|
||||||
|
description="",
|
||||||
|
created_at=10,
|
||||||
|
updated_at=10,
|
||||||
|
)
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
json={
|
||||||
|
"ids": ["exp1", "nonexistent"],
|
||||||
|
"export_case_id": "case1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
# Nothing reassigned
|
||||||
|
exp = Export.get(Export.id == "exp1")
|
||||||
|
assert exp.export_case_id is None
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_case_not_found(self):
|
||||||
|
"""Target case doesn't exist → 404."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
json={"ids": ["exp1"], "export_case_id": "nonexistent"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
exp = Export.get(Export.id == "exp1")
|
||||||
|
assert exp.export_case_id is None
|
||||||
|
|
||||||
|
def test_bulk_reassign_exports_non_admin_rejected(self):
|
||||||
|
"""Non-admin users cannot bulk reassign."""
|
||||||
|
Export.create(
|
||||||
|
id="exp1",
|
||||||
|
camera="front_door",
|
||||||
|
name="export_1",
|
||||||
|
date=100,
|
||||||
|
video_path="/tmp/exp1.mp4",
|
||||||
|
thumb_path="/tmp/exp1.jpg",
|
||||||
|
in_progress=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
headers={"remote-user": "viewer", "remote-role": "viewer"},
|
||||||
|
json={"ids": ["exp1"], "export_case_id": None},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|||||||
@ -84,6 +84,7 @@
|
|||||||
"title_one": "Export 1 review",
|
"title_one": "Export 1 review",
|
||||||
"title_other": "Export {{count}} reviews",
|
"title_other": "Export {{count}} reviews",
|
||||||
"description": "Export each selected review. All exports will be grouped under a single case.",
|
"description": "Export each selected review. All exports will be grouped under a single case.",
|
||||||
|
"descriptionNoCase": "Export each selected review.",
|
||||||
"caseNamePlaceholder": "Review export - {{date}}",
|
"caseNamePlaceholder": "Review export - {{date}}",
|
||||||
"exportButton_one": "Export 1 review",
|
"exportButton_one": "Export 1 review",
|
||||||
"exportButton_other": "Export {{count}} reviews",
|
"exportButton_other": "Export {{count}} reviews",
|
||||||
@ -91,6 +92,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"started_one": "Started 1 export. Opening the case now.",
|
"started_one": "Started 1 export. Opening the case now.",
|
||||||
"started_other": "Started {{count}} exports. Opening the case now.",
|
"started_other": "Started {{count}} exports. Opening the case now.",
|
||||||
|
"startedNoCase_one": "Started 1 export.",
|
||||||
|
"startedNoCase_other": "Started {{count}} exports.",
|
||||||
"partial": "Started {{successful}} of {{total}} exports. Failed: {{failedItems}}",
|
"partial": "Started {{successful}} of {{total}} exports. Failed: {{failedItems}}",
|
||||||
"failed": "Failed to start {{total}} exports. Failed: {{failedItems}}"
|
"failed": "Failed to start {{total}} exports. Failed: {{failedItems}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,5 +86,38 @@
|
|||||||
"addButton_one": "Add 1 Export",
|
"addButton_one": "Add 1 Export",
|
||||||
"addButton_other": "Add {{count}} Exports",
|
"addButton_other": "Add {{count}} Exports",
|
||||||
"adding": "Adding..."
|
"adding": "Adding..."
|
||||||
|
},
|
||||||
|
"selected_one": "{{count}} selected",
|
||||||
|
"selected_other": "{{count}} selected",
|
||||||
|
"bulkActions": {
|
||||||
|
"addToCase": "Add to Case",
|
||||||
|
"moveToCase": "Move to Case",
|
||||||
|
"removeFromCase": "Remove from Case",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteNow": "Delete Now"
|
||||||
|
},
|
||||||
|
"bulkDelete": {
|
||||||
|
"title": "Delete Exports",
|
||||||
|
"desc_one": "Are you sure you want to delete {{count}} export?",
|
||||||
|
"desc_other": "Are you sure you want to delete {{count}} exports?"
|
||||||
|
},
|
||||||
|
"bulkRemoveFromCase": {
|
||||||
|
"title": "Remove from Case",
|
||||||
|
"desc_one": "Remove {{count}} export from this case?",
|
||||||
|
"desc_other": "Remove {{count}} exports from this case?",
|
||||||
|
"descKeepExports": "Exports will be moved to uncategorized.",
|
||||||
|
"descDeleteExports": "Exports will be permanently deleted.",
|
||||||
|
"deleteExports": "Delete exports instead"
|
||||||
|
},
|
||||||
|
"bulkToast": {
|
||||||
|
"success": {
|
||||||
|
"delete": "Successfully deleted exports",
|
||||||
|
"reassign": "Successfully updated case assignment",
|
||||||
|
"remove": "Successfully removed exports from case"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"deleteFailed": "Failed to delete exports: {{errorMessage}}",
|
||||||
|
"reassignFailed": "Failed to update case assignment: {{errorMessage}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user