title modification
This commit is contained in:
@@ -39,6 +39,7 @@ from .models import (
|
|||||||
AuthRequest,
|
AuthRequest,
|
||||||
AuthResponse,
|
AuthResponse,
|
||||||
CourseSummary,
|
CourseSummary,
|
||||||
|
CourseTitleUpdate,
|
||||||
SentenceBoundaryAdjust,
|
SentenceBoundaryAdjust,
|
||||||
SentenceBoundary,
|
SentenceBoundary,
|
||||||
SentenceBoundaryDocument,
|
SentenceBoundaryDocument,
|
||||||
@@ -624,6 +625,20 @@ def create_app(
|
|||||||
background_tasks.add_task(_process_safely, processor, video_hash)
|
background_tasks.add_task(_process_safely, processor, video_hash)
|
||||||
return {"video_hash": video_hash.lower(), "status": "processing_queued"}
|
return {"video_hash": video_hash.lower(), "status": "processing_queued"}
|
||||||
|
|
||||||
|
@application.put(
|
||||||
|
"/api/v1/admin/videos/{video_hash}/title",
|
||||||
|
response_model=VideoSummary,
|
||||||
|
dependencies=[Depends(require_admin)],
|
||||||
|
)
|
||||||
|
def update_course_title(
|
||||||
|
payload: CourseTitleUpdate,
|
||||||
|
video_hash: str = SHA256_PATH,
|
||||||
|
) -> VideoSummary:
|
||||||
|
video = video_repository.update_title(video_hash, payload.title)
|
||||||
|
if video is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Video was not found.")
|
||||||
|
return _video_summary(video, service_settings)
|
||||||
|
|
||||||
@application.put(
|
@application.put(
|
||||||
"/api/v1/admin/videos/{video_hash}/sentences/{sentence_index}",
|
"/api/v1/admin/videos/{video_hash}/sentences/{sentence_index}",
|
||||||
response_model=SentenceBoundary,
|
response_model=SentenceBoundary,
|
||||||
|
|||||||
@@ -92,6 +92,20 @@ class SentenceTextUpdate(BaseModel):
|
|||||||
language: Optional[str] = Field(default=None, max_length=32)
|
language: Optional[str] = Field(default=None, max_length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseTitleUpdate(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
title: str = Field(max_length=200)
|
||||||
|
|
||||||
|
@field_validator("title")
|
||||||
|
@classmethod
|
||||||
|
def validate_title(cls, value: str) -> str:
|
||||||
|
cleaned = value.strip()
|
||||||
|
if not cleaned:
|
||||||
|
raise ValueError("title cannot be empty")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetRequest(BaseModel):
|
class PasswordResetRequest(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|||||||
@@ -349,6 +349,22 @@ class VideoRepository:
|
|||||||
(remote_url, utc_now(), normalize_video_hash(video_hash)),
|
(remote_url, utc_now(), normalize_video_hash(video_hash)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_title(self, video_hash: str, title: str) -> Optional[Dict[str, Any]]:
|
||||||
|
normalized_hash = normalize_video_hash(video_hash)
|
||||||
|
cleaned_title = title.strip()[:200]
|
||||||
|
if not cleaned_title:
|
||||||
|
raise ValueError("Title cannot be empty.")
|
||||||
|
|
||||||
|
with self._connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"UPDATE videos SET title = ?, updated_at = ? WHERE video_hash = ?",
|
||||||
|
(cleaned_title, utc_now(), normalized_hash),
|
||||||
|
)
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.get_video(normalized_hash)
|
||||||
|
|
||||||
def list_videos(self) -> List[Dict[str, Any]]:
|
def list_videos(self) -> List[Dict[str, Any]]:
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
rows = connection.execute(
|
rows = connection.execute(
|
||||||
|
|||||||
@@ -114,6 +114,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="/static/admin.js?v=admin-merge-sentence-v1" defer></script>
|
<script src="/static/admin.js?v=admin-course-title-v1" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ function actionCell(video) {
|
|||||||
play.rel = "noopener";
|
play.rel = "noopener";
|
||||||
play.textContent = "播放";
|
play.textContent = "播放";
|
||||||
actions.append(play);
|
actions.append(play);
|
||||||
|
actions.append(button("改标题", () => renameVideo(video)));
|
||||||
actions.append(button("句子", () => openSentences(video)));
|
actions.append(button("句子", () => openSentences(video)));
|
||||||
actions.append(button("重新处理", () => reprocess(video.video_hash)));
|
actions.append(button("重新处理", () => reprocess(video.video_hash)));
|
||||||
const remove = button("删除", () => deleteVideo(video));
|
const remove = button("删除", () => deleteVideo(video));
|
||||||
@@ -198,6 +199,28 @@ function button(label, handler) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renameVideo(video) {
|
||||||
|
const title = window.prompt("请输入新的课程标题", video.title || "");
|
||||||
|
if (title === null) return;
|
||||||
|
const cleanedTitle = title.trim();
|
||||||
|
if (!cleanedTitle) {
|
||||||
|
elements.serviceState.textContent = "课程标题不能为空";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cleanedTitle === video.title) return;
|
||||||
|
try {
|
||||||
|
await api(`/api/v1/admin/videos/${video.video_hash}/title`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title: cleanedTitle }),
|
||||||
|
});
|
||||||
|
elements.serviceState.textContent = "课程标题已更新";
|
||||||
|
await loadVideos();
|
||||||
|
} catch (error) {
|
||||||
|
elements.serviceState.textContent = `标题修改失败: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function uploadVideo(event) {
|
function uploadVideo(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = document.querySelector("#video-file").files[0];
|
const file = document.querySelector("#video-file").files[0];
|
||||||
|
|||||||
@@ -562,3 +562,40 @@ def test_admin_boundary_adjustment_updates_linked_next_start(tmp_path):
|
|||||||
document = repository.get_document(VIDEO_HASH)
|
document = repository.get_document(VIDEO_HASH)
|
||||||
assert [item.end_ms for item in document.sentences] == [4250, 9000]
|
assert [item.end_ms for item in document.sentences] == [4250, 9000]
|
||||||
assert [item.start_ms for item in document.sentences] == [0, 4250]
|
assert [item.start_ms for item in document.sentences] == [0, 4250]
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_can_update_course_title(tmp_path):
|
||||||
|
settings = replace(Settings.from_env(), data_dir=tmp_path / "data")
|
||||||
|
settings.ensure_directories()
|
||||||
|
repository = VideoRepository(settings.database_path)
|
||||||
|
repository.upsert_upload(
|
||||||
|
video_hash=VIDEO_HASH,
|
||||||
|
title="Old course title",
|
||||||
|
filename="lesson.mp4",
|
||||||
|
stored_filename=f"{VIDEO_HASH}.mp4",
|
||||||
|
content_type="video/mp4",
|
||||||
|
size_bytes=5,
|
||||||
|
language="en",
|
||||||
|
)
|
||||||
|
client = TestClient(
|
||||||
|
create_app(
|
||||||
|
BoundaryStore(tmp_path / "boundaries.json"),
|
||||||
|
settings=settings,
|
||||||
|
repository=repository,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1/admin/videos/{VIDEO_HASH}/title",
|
||||||
|
json={"title": " New course title "},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "New course title"
|
||||||
|
assert repository.get_video(VIDEO_HASH)["title"] == "New course title"
|
||||||
|
|
||||||
|
missing_response = client.put(
|
||||||
|
f"/api/v1/admin/videos/{'b' * 64}/title",
|
||||||
|
json={"title": "Missing course"},
|
||||||
|
)
|
||||||
|
assert missing_response.status_code == 404
|
||||||
|
|||||||
Reference in New Issue
Block a user