This commit is contained in:
+8
-1
@@ -468,7 +468,12 @@ async def create_task(
|
||||
)
|
||||
if parent is None:
|
||||
raise HTTPException(status_code=400, detail="父任务必须是同一清单的顶层任务")
|
||||
data = payload.model_dump()
|
||||
if payload.rrule:
|
||||
if not payload.due_at:
|
||||
raise HTTPException(status_code=422, detail="重复任务需要截止时间")
|
||||
from .mvp import parse_rrule
|
||||
parse_rrule(payload.rrule)
|
||||
data = payload.model_dump(exclude={"rrule"})
|
||||
parent_filter = Task.parent_id == payload.parent_id if payload.parent_id else Task.parent_id.is_(None)
|
||||
max_position = await db.scalar(select(func.max(Task.position)).where(
|
||||
Task.user_id == user.id,
|
||||
@@ -479,6 +484,8 @@ async def create_task(
|
||||
task = Task(user_id=user.id, position=(max_position if max_position is not None else -1) + 1, **data)
|
||||
db.add(task)
|
||||
await db.flush()
|
||||
if payload.rrule:
|
||||
db.add(RecurrenceTemplate(user_id=user.id, task_id=task.id, rrule=payload.rrule.upper(), starts_at=task.due_at))
|
||||
audit(db, user.id, "create", "task", task.id)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
|
||||
@@ -77,6 +77,7 @@ class TaskCreate(BaseModel):
|
||||
priority: int = Field(default=0, ge=0, le=3)
|
||||
due_at: datetime | None = None
|
||||
parent_id: UUID | None = None
|
||||
rrule: str | None = Field(default=None, min_length=5, max_length=1000)
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
|
||||
@@ -145,14 +145,15 @@ async function submitTaskCompose() {
|
||||
const taskTitle = composeTitle.value.trim()
|
||||
if (!taskTitle || !composeListId.value) return
|
||||
try {
|
||||
const rrule = composeRepeat.value === 'none' ? null : repeatRrule(composeRepeat.value, composeRepeatConfig.value)
|
||||
const task = await api('/tasks', { method: 'POST', body: JSON.stringify({
|
||||
title: taskTitle,
|
||||
list_id: composeListId.value,
|
||||
due_at: fromDateTimeLocal(composeDueAt.value),
|
||||
priority: composePriority.value,
|
||||
description: composeDescription.value,
|
||||
rrule,
|
||||
}) })
|
||||
if (composeRepeat.value !== 'none') await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, rrule: repeatRrule(composeRepeat.value, composeRepeatConfig.value) }) })
|
||||
if (isTaskView(activeView.value)) {
|
||||
tasks.value.push(task)
|
||||
totalTasks.value = nextTotalAfterLocalTaskAdd(totalTasks.value)
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('unified floating add interaction', () => {
|
||||
expect(app).toContain('const selectedTaskRepeat = ref')
|
||||
expect(app).toContain('重复<select v-model="composeRepeat"')
|
||||
expect(app).toContain('重复<select v-model="selectedTaskRepeat"')
|
||||
expect(app).toContain("api('/recurrences'")
|
||||
expect(app).toContain('rrule,')
|
||||
expect(app).toContain("api(`/tasks/${task.id}/recurrence`)")
|
||||
expect(app).toContain('<option value="custom">自定义…</option>')
|
||||
expect(app).toContain('class="repeat-custom-fields"')
|
||||
|
||||
@@ -29,6 +29,29 @@ def test_bootstrap_returns_navigation_and_current_user(client):
|
||||
|
||||
|
||||
|
||||
def test_creating_task_with_recurrence_is_atomic(client):
|
||||
inbox = boot(client)
|
||||
created = client.post(
|
||||
"/api/v1/tasks",
|
||||
json={
|
||||
"title": "隔周复盘",
|
||||
"list_id": inbox["id"],
|
||||
"due_at": "2026-09-07T09:00:00Z",
|
||||
"rrule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR",
|
||||
},
|
||||
)
|
||||
assert created.status_code == 201
|
||||
recurrence = client.get(f"/api/v1/tasks/{created.json()['id']}/recurrence").json()
|
||||
assert recurrence["rrule"] == "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR"
|
||||
|
||||
rejected = client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "缺少日期", "list_id": inbox["id"], "rrule": "FREQ=DAILY"},
|
||||
)
|
||||
assert rejected.status_code == 422
|
||||
assert client.get("/api/v1/tasks", params={"q": "缺少日期"}).json()["items"] == []
|
||||
|
||||
|
||||
def test_custom_recurrence_rejects_invalid_weekdays_month_days_and_until(client):
|
||||
inbox = boot(client)
|
||||
task = client.post(
|
||||
|
||||
Reference in New Issue
Block a user