feat: 安全移除本地任务记录 (#108)
This commit is contained in:
+13
-1
@@ -4,7 +4,7 @@
|
||||
``MIGRATIONS`` 末尾增加版本,不能修改已经发布的迁移。
|
||||
"""
|
||||
|
||||
SCHEMA_VERSION = 3
|
||||
SCHEMA_VERSION = 4
|
||||
|
||||
|
||||
MIGRATION_1 = (
|
||||
@@ -142,8 +142,20 @@ MIGRATION_3 = (
|
||||
)
|
||||
|
||||
|
||||
MIGRATION_4 = (
|
||||
"""
|
||||
ALTER TABLE pdd_tasks ADD COLUMN removed_at TEXT
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX idx_pdd_tasks_visible_list
|
||||
ON pdd_tasks(removed_at, updated_at DESC, id DESC)
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
MIGRATIONS = {
|
||||
1: MIGRATION_1,
|
||||
2: MIGRATION_2,
|
||||
3: MIGRATION_3,
|
||||
4: MIGRATION_4,
|
||||
}
|
||||
|
||||
+39
-9
@@ -368,6 +368,7 @@ class PDDTaskPage(QWidget):
|
||||
rerunRequested = pyqtSignal(str)
|
||||
rerunCancelRequested = pyqtSignal()
|
||||
resubmitRequested = pyqtSignal(object)
|
||||
removeRequested = pyqtSignal(object)
|
||||
openSettingsRequested = pyqtSignal()
|
||||
detailRequested = pyqtSignal(str)
|
||||
|
||||
@@ -377,6 +378,7 @@ class PDDTaskPage(QWidget):
|
||||
|
||||
self._rerun_state = "idle"
|
||||
self._resubmit_running = False
|
||||
self._remove_running = False
|
||||
self.taskModel = TaskTableModel(self)
|
||||
self._build_command_area()
|
||||
self._build_content_area()
|
||||
@@ -429,6 +431,10 @@ class PDDTaskPage(QWidget):
|
||||
self.resubmitButton.setAccessibleName("重新上报勾选任务的已有结果")
|
||||
self.resubmitButton.setEnabled(False)
|
||||
|
||||
self.removeButton = PushButton(FIF.DELETE, "删除", self)
|
||||
self.removeButton.setAccessibleName("从任务列表删除勾选的本地任务")
|
||||
self.removeButton.setEnabled(False)
|
||||
|
||||
self.commandCard = CardWidget(self)
|
||||
self.checkedCountLabel = CaptionLabel("已选 0 条", self.commandCard)
|
||||
self.checkedCountLabel.setAccessibleName("已勾选任务数量")
|
||||
@@ -446,7 +452,7 @@ class PDDTaskPage(QWidget):
|
||||
if compact == self._compact_commands and self.commandLayout.count():
|
||||
return
|
||||
self._compact_commands = compact
|
||||
for column in range(13):
|
||||
for column in range(14):
|
||||
self.commandLayout.setColumnStretch(column, 0)
|
||||
|
||||
if compact:
|
||||
@@ -459,6 +465,7 @@ class PDDTaskPage(QWidget):
|
||||
(self.checkedCountLabel, 0, 6, 1, 1),
|
||||
(self.rerunButton, 0, 7, 1, 1),
|
||||
(self.resubmitButton, 0, 8, 1, 1),
|
||||
(self.removeButton, 0, 9, 1, 1),
|
||||
(self.keywordLabel, 1, 0, 1, 1),
|
||||
(self.keywordInput, 1, 1, 1, 5),
|
||||
(self.searchButton, 1, 6, 1, 1),
|
||||
@@ -479,6 +486,7 @@ class PDDTaskPage(QWidget):
|
||||
(self.checkedCountLabel, 0, 10, 1, 1),
|
||||
(self.rerunButton, 0, 11, 1, 1),
|
||||
(self.resubmitButton, 0, 12, 1, 1),
|
||||
(self.removeButton, 0, 13, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(6, 4)
|
||||
self.commandLayout.setColumnStretch(9, 1)
|
||||
@@ -562,6 +570,7 @@ class PDDTaskPage(QWidget):
|
||||
self.refreshButton.clicked.connect(self.refreshRequested.emit)
|
||||
self.rerunButton.clicked.connect(self._request_rerun)
|
||||
self.resubmitButton.clicked.connect(self._request_resubmit)
|
||||
self.removeButton.clicked.connect(self._request_remove)
|
||||
self.clearFiltersButton.clicked.connect(self.clear_filters)
|
||||
self.taskTable.clicked.connect(self._on_table_clicked)
|
||||
self.taskTable.activated.connect(self._on_table_activated)
|
||||
@@ -634,7 +643,9 @@ class PDDTaskPage(QWidget):
|
||||
text, icon, enabled = states.get(state, states["stopped"])
|
||||
self.autoFetchButton.setText(text)
|
||||
self.autoFetchButton.setIcon(icon)
|
||||
self.autoFetchButton.setEnabled(enabled)
|
||||
self.autoFetchButton.setEnabled(
|
||||
enabled and not self._resubmit_running and not self._remove_running
|
||||
)
|
||||
self.autoFetchButton.setAccessibleName(f"{text} PDD 任务")
|
||||
|
||||
def set_engine_status(self, text: str) -> None:
|
||||
@@ -669,6 +680,12 @@ class PDDTaskPage(QWidget):
|
||||
self._resubmit_running = running
|
||||
self._update_checked_actions(self.taskModel.checked_task_ids())
|
||||
|
||||
def set_remove_running(self, running: bool) -> None:
|
||||
"""设置本地任务删除忙碌状态并防止重复操作。"""
|
||||
|
||||
self._remove_running = running
|
||||
self._update_checked_actions(self.taskModel.checked_task_ids())
|
||||
|
||||
def set_rerun_cancelling(self) -> None:
|
||||
"""显示取消请求已发出,防止用户重复点击。"""
|
||||
|
||||
@@ -686,18 +703,26 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunRequested.emit(task_ids[0])
|
||||
|
||||
def _request_resubmit(self) -> None:
|
||||
if self._resubmit_running:
|
||||
if self._resubmit_running or self._remove_running:
|
||||
return
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if task_ids:
|
||||
self.resubmitRequested.emit(task_ids)
|
||||
|
||||
def _request_remove(self) -> None:
|
||||
if self._remove_running or self._resubmit_running:
|
||||
return
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if task_ids:
|
||||
self.removeRequested.emit(task_ids)
|
||||
|
||||
def _update_rerun_button(self) -> None:
|
||||
if self._rerun_state == "running":
|
||||
self.rerunButton.setText("停止重新采集")
|
||||
self.rerunButton.setAccessibleName("停止当前重新采集")
|
||||
self.rerunButton.setEnabled(True)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
if self._rerun_state == "cancelling":
|
||||
@@ -705,19 +730,22 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setAccessibleName("正在停止重新采集")
|
||||
self.rerunButton.setEnabled(False)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
|
||||
self.rerunButton.setText("重新执行")
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.autoFetchButton.setEnabled(not self._resubmit_running)
|
||||
idle = not self._resubmit_running and not self._remove_running
|
||||
self.autoFetchButton.setEnabled(idle)
|
||||
self.rerunButton.setEnabled(
|
||||
not self._resubmit_running
|
||||
and len(self.taskModel.checked_task_ids()) == 1
|
||||
idle and len(self.taskModel.checked_task_ids()) == 1
|
||||
)
|
||||
self.resubmitButton.setEnabled(
|
||||
not self._resubmit_running
|
||||
and bool(self.taskModel.checked_task_ids())
|
||||
idle and bool(self.taskModel.checked_task_ids())
|
||||
)
|
||||
self.removeButton.setEnabled(
|
||||
idle and bool(self.taskModel.checked_task_ids())
|
||||
)
|
||||
|
||||
def _update_checked_actions(self, task_ids=None) -> None:
|
||||
@@ -728,7 +756,9 @@ class PDDTaskPage(QWidget):
|
||||
self.resubmitButton.setText(
|
||||
"正在上报…" if self._resubmit_running else "重新上报"
|
||||
)
|
||||
self.resubmitButton.setEnabled(bool(checked) and not self._resubmit_running)
|
||||
self.removeButton.setText(
|
||||
"正在删除…" if self._remove_running else "删除"
|
||||
)
|
||||
self._update_rerun_button()
|
||||
|
||||
def _apply_filters(self) -> None:
|
||||
|
||||
+158
-5
@@ -59,7 +59,11 @@ from .task_models import (
|
||||
TaskSummary,
|
||||
TaskType,
|
||||
)
|
||||
from .task_repository import CollectRerunError, TaskRepository
|
||||
from .task_repository import (
|
||||
CollectRerunError,
|
||||
TaskRemovalError,
|
||||
TaskRepository,
|
||||
)
|
||||
from .task_dispatcher import (
|
||||
TaskDispatcher,
|
||||
admin_task_to_new_claimed_task,
|
||||
@@ -322,6 +326,36 @@ class ResultResubmitWorker(QObject):
|
||||
self.completed.emit()
|
||||
|
||||
|
||||
class TaskRemoveWorker(QObject):
|
||||
"""在后台原子校验并软移除勾选任务,不访问任何界面控件。"""
|
||||
|
||||
succeeded = pyqtSignal(object)
|
||||
failed = pyqtSignal(str)
|
||||
completed = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repository: TaskRepository,
|
||||
task_ids: tuple[str, ...],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._repository = repository
|
||||
self._task_ids = task_ids
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._repository.remove_tasks_from_list(self._task_ids)
|
||||
except TaskRemovalError as exc:
|
||||
self.failed.emit(str(exc))
|
||||
except Exception:
|
||||
self.failed.emit("删除本地任务失败,请检查数据库后重试。")
|
||||
else:
|
||||
self.succeeded.emit(self._task_ids)
|
||||
finally:
|
||||
self.completed.emit()
|
||||
|
||||
|
||||
class PDDTaskPageEvent(QObject):
|
||||
"""把 PDD 页面只读操作连接到本地任务 Repository。"""
|
||||
|
||||
@@ -359,6 +393,9 @@ class PDDTaskPageEvent(QObject):
|
||||
self._resubmit_busy = False
|
||||
self._resubmit_thread: Optional[QThread] = None
|
||||
self._resubmit_worker: Optional[ResultResubmitWorker] = None
|
||||
self._remove_busy = False
|
||||
self._remove_thread: Optional[QThread] = None
|
||||
self._remove_worker: Optional[TaskRemoveWorker] = None
|
||||
self._stop_requested = False
|
||||
self._cycle_next_delay_ms: Optional[int] = None
|
||||
self._stop_status = "自动获取:已停止 · 当前没有执行中的任务"
|
||||
@@ -417,6 +454,7 @@ class PDDTaskPageEvent(QObject):
|
||||
page.rerunRequested.connect(self.request_rerun)
|
||||
page.rerunCancelRequested.connect(self.request_cancel_rerun)
|
||||
page.resubmitRequested.connect(self.request_resubmit)
|
||||
page.removeRequested.connect(self.request_remove)
|
||||
page.autoFetchRequested.connect(self._request_claim_task)
|
||||
page.detailRequested.connect(self.show_task_detail)
|
||||
page.taskModel.loadMoreRequested.connect(self._load_page)
|
||||
@@ -452,7 +490,12 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
if self._closing or not task_id:
|
||||
return
|
||||
if self._auto_fetch_running or self._claim_busy or self._resubmit_busy:
|
||||
if (
|
||||
self._auto_fetch_running
|
||||
or self._claim_busy
|
||||
or self._resubmit_busy
|
||||
or self._remove_busy
|
||||
):
|
||||
self._show_rerun_warning(
|
||||
"暂时不能重新执行",
|
||||
"自动获取或其他采集正在运行,请停止并等待当前任务结束。",
|
||||
@@ -517,7 +560,12 @@ class PDDTaskPageEvent(QObject):
|
||||
)
|
||||
if self._closing or not stable_ids:
|
||||
return
|
||||
if self._auto_fetch_running or self._claim_busy or self._resubmit_busy:
|
||||
if (
|
||||
self._auto_fetch_running
|
||||
or self._claim_busy
|
||||
or self._resubmit_busy
|
||||
or self._remove_busy
|
||||
):
|
||||
self._show_rerun_warning(
|
||||
"暂时不能重新上报",
|
||||
"自动获取、重新执行或另一批上报正在运行,请等待当前操作结束。",
|
||||
@@ -545,6 +593,98 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
self._start_resubmit_worker(stable_ids)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def request_remove(self, task_ids) -> None:
|
||||
"""确认后在后台从普通列表软移除勾选的本地任务。"""
|
||||
|
||||
stable_ids = tuple(
|
||||
dict.fromkeys(
|
||||
str(value).strip()
|
||||
for value in task_ids
|
||||
if value is not None and str(value).strip()
|
||||
)
|
||||
)
|
||||
if self._closing or not stable_ids:
|
||||
return
|
||||
if (
|
||||
self._auto_fetch_running
|
||||
or self._claim_busy
|
||||
or self._resubmit_busy
|
||||
or self._remove_busy
|
||||
):
|
||||
self._show_rerun_warning(
|
||||
"暂时不能删除",
|
||||
"自动获取、重新执行、重新上报或另一批删除正在运行,"
|
||||
"请等待当前操作结束。",
|
||||
)
|
||||
return
|
||||
|
||||
count = len(stable_ids)
|
||||
dialog = MessageBox(
|
||||
f"从列表删除 {count} 条任务?",
|
||||
"只会从普通任务列表中隐藏这些本地记录,不会删除 Admin 任务;"
|
||||
"执行记录和上报历史仍会永久保留。\n\n"
|
||||
"只有已结束、没有待上报数据且没有进入不可逆阶段的任务才允许删除。",
|
||||
self._page.window(),
|
||||
)
|
||||
dialog.yesButton.setText(f"删除 {count} 条")
|
||||
dialog.cancelButton.setText("暂不删除")
|
||||
dialog.cancelButton.setFocus()
|
||||
if not dialog.exec():
|
||||
return
|
||||
|
||||
self._start_remove_worker(stable_ids)
|
||||
|
||||
def _start_remove_worker(self, task_ids: tuple[str, ...]) -> None:
|
||||
"""启动本地任务软删除工作线程。"""
|
||||
|
||||
self._remove_busy = True
|
||||
self._page.set_remove_running(True)
|
||||
self._page.set_engine_status(
|
||||
f"正在检查并删除 {len(task_ids)} 条本地任务…"
|
||||
)
|
||||
|
||||
thread = QThread(self)
|
||||
worker = TaskRemoveWorker(self._repository, task_ids)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.succeeded.connect(self._on_remove_succeeded)
|
||||
worker.failed.connect(self._on_remove_failed)
|
||||
worker.completed.connect(thread.quit)
|
||||
worker.completed.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._on_remove_thread_finished)
|
||||
self._remove_thread = thread
|
||||
self._remove_worker = worker
|
||||
thread.start()
|
||||
|
||||
@pyqtSlot(object)
|
||||
def _on_remove_succeeded(self, task_ids) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
stable_ids = tuple(task_ids)
|
||||
self._page.taskModel.uncheck_tasks(stable_ids)
|
||||
self._page.set_engine_status(
|
||||
f"已从列表删除 {len(stable_ids)} 条本地任务;"
|
||||
"执行记录和上报历史仍会永久保留。"
|
||||
)
|
||||
self._reload()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_remove_failed(self, message: str) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
self._page.set_engine_status(f"删除未完成:{message}")
|
||||
self._show_claim_error("不能删除所选任务", message)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_remove_thread_finished(self) -> None:
|
||||
self._remove_worker = None
|
||||
self._remove_thread = None
|
||||
self._remove_busy = False
|
||||
if not self._closing:
|
||||
self._page.set_remove_running(False)
|
||||
|
||||
def _start_resubmit_worker(self, task_ids: tuple[str, ...]) -> None:
|
||||
"""启动只处理指定任务 Outbox 的工作线程。"""
|
||||
|
||||
@@ -774,10 +914,10 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
if self._closing:
|
||||
return
|
||||
if self._resubmit_busy:
|
||||
if self._resubmit_busy or self._remove_busy:
|
||||
self._show_rerun_warning(
|
||||
"暂时不能获取任务",
|
||||
"任务结果正在重新上报,请等待当前操作结束。",
|
||||
"本地任务正在重新上报或删除,请等待当前操作结束。",
|
||||
)
|
||||
return
|
||||
if self._auto_fetch_running:
|
||||
@@ -1283,6 +1423,19 @@ class PDDTaskPageEvent(QObject):
|
||||
resubmit_thread.quit()
|
||||
resubmit_thread.wait(60_000)
|
||||
|
||||
remove_worker = self._remove_worker
|
||||
remove_thread = self._remove_thread
|
||||
if remove_worker is not None:
|
||||
try:
|
||||
remove_worker.succeeded.disconnect(self._on_remove_succeeded)
|
||||
remove_worker.failed.disconnect(self._on_remove_failed)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
if remove_thread is not None and remove_thread.isRunning():
|
||||
# 删除只包含一个很短的 SQLite 事务,不中途取消,避免部分更新。
|
||||
remove_thread.quit()
|
||||
remove_thread.wait(60_000)
|
||||
|
||||
|
||||
def summary_to_row(summary: TaskSummary) -> TaskRow:
|
||||
"""把领域摘要转换成只供表格显示的轻量行。"""
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from .db import initialize_database, open_database
|
||||
@@ -39,6 +39,10 @@ class CollectRerunError(ValueError):
|
||||
"""当前任务不满足重新采集条件。"""
|
||||
|
||||
|
||||
class TaskRemovalError(ValueError):
|
||||
"""勾选任务不满足从普通列表移除的安全条件。"""
|
||||
|
||||
|
||||
def utc_now_iso() -> str:
|
||||
"""返回精确到秒的 UTC ISO 8601 时间。"""
|
||||
|
||||
@@ -145,6 +149,98 @@ class TaskRepository:
|
||||
connection.close()
|
||||
return int(row[0])
|
||||
|
||||
def remove_tasks_from_list(self, remote_task_ids: Iterable[str]) -> int:
|
||||
"""把符合安全条件的终态任务从普通列表中软移除。
|
||||
|
||||
全部任务会在同一个事务中完成校验和更新;任一任务不安全时,
|
||||
所有任务都保持原样。执行记录和 Outbox 不会被删除或修改。
|
||||
"""
|
||||
|
||||
task_ids = tuple(
|
||||
dict.fromkeys(
|
||||
str(value).strip()
|
||||
for value in remote_task_ids
|
||||
if value is not None and str(value).strip()
|
||||
)
|
||||
)
|
||||
if not task_ids:
|
||||
raise TaskRemovalError("没有可删除的任务,请重新勾选。")
|
||||
|
||||
placeholders = ", ".join("?" for _ in task_ids)
|
||||
terminal_statuses = {
|
||||
TaskStatus.SUCCEEDED.value,
|
||||
TaskStatus.FAILED.value,
|
||||
TaskStatus.CANCELLED.value,
|
||||
}
|
||||
now = utc_now_iso()
|
||||
connection = open_database(self._db_path)
|
||||
try:
|
||||
with connection:
|
||||
rows = connection.execute(
|
||||
"SELECT id, remote_task_id, status, removed_at FROM pdd_tasks"
|
||||
f" WHERE remote_task_id IN ({placeholders})",
|
||||
task_ids,
|
||||
).fetchall()
|
||||
found = {row["remote_task_id"]: row for row in rows}
|
||||
missing = [task_id for task_id in task_ids if task_id not in found]
|
||||
if missing:
|
||||
raise TaskRemovalError(
|
||||
f"本地找不到任务 {missing[0]},请刷新列表后重试。"
|
||||
)
|
||||
removed = next(
|
||||
(row for row in rows if row["removed_at"] is not None), None
|
||||
)
|
||||
if removed is not None:
|
||||
raise TaskRemovalError(
|
||||
f"任务 {removed['remote_task_id']} 已不在普通列表,请刷新后重试。"
|
||||
)
|
||||
non_terminal = next(
|
||||
(row for row in rows if row["status"] not in terminal_statuses),
|
||||
None,
|
||||
)
|
||||
if non_terminal is not None:
|
||||
raise TaskRemovalError(
|
||||
f"任务 {non_terminal['remote_task_id']} 尚未结束,不能删除。"
|
||||
)
|
||||
|
||||
irreversible = connection.execute(
|
||||
"SELECT t.remote_task_id FROM task_runs r"
|
||||
" JOIN pdd_tasks t ON t.id = r.task_id"
|
||||
f" WHERE t.remote_task_id IN ({placeholders})"
|
||||
" AND r.irreversible_action_at IS NOT NULL LIMIT 1",
|
||||
task_ids,
|
||||
).fetchone()
|
||||
if irreversible is not None:
|
||||
raise TaskRemovalError(
|
||||
f"任务 {irreversible['remote_task_id']} 已进入不可逆阶段,"
|
||||
"必须保留在列表中核对订单。"
|
||||
)
|
||||
|
||||
unsent = connection.execute(
|
||||
"SELECT t.remote_task_id, o.status FROM outbox_events o"
|
||||
" JOIN pdd_tasks t ON t.id = o.task_id"
|
||||
f" WHERE t.remote_task_id IN ({placeholders})"
|
||||
" AND o.status <> 'sent' LIMIT 1",
|
||||
task_ids,
|
||||
).fetchone()
|
||||
if unsent is not None:
|
||||
raise TaskRemovalError(
|
||||
f"任务 {unsent['remote_task_id']} 还有未发送完成的数据,"
|
||||
"请先重新上报。"
|
||||
)
|
||||
|
||||
cursor = connection.execute(
|
||||
"UPDATE pdd_tasks SET removed_at = ?, updated_at = ?"
|
||||
f" WHERE remote_task_id IN ({placeholders})"
|
||||
" AND removed_at IS NULL",
|
||||
(now, now, *task_ids),
|
||||
)
|
||||
if cursor.rowcount != len(task_ids):
|
||||
raise TaskRemovalError("任务列表已发生变化,请刷新后重试。")
|
||||
finally:
|
||||
connection.close()
|
||||
return len(task_ids)
|
||||
|
||||
def get_task(self, remote_task_id: str) -> Optional[TaskDetail]:
|
||||
"""按稳定远程编号读取完整任务;不存在时返回 None。"""
|
||||
|
||||
@@ -1594,7 +1690,7 @@ class TaskRepository:
|
||||
|
||||
@staticmethod
|
||||
def _build_where(filters: TaskFilters) -> Tuple[str, List[object]]:
|
||||
clauses = []
|
||||
clauses = ["removed_at IS NULL"]
|
||||
parameters: List[object] = []
|
||||
if filters.task_type is not None:
|
||||
clauses.append("task_type = ?")
|
||||
|
||||
Reference in New Issue
Block a user