Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7590995e4 | |||
| d7b4b48cc7 | |||
| 3f5284e067 | |||
| 50d2e62694 | |||
| 70dee494f3 | |||
| 4b2a94002b | |||
| 9eb872391e | |||
| 784830bd62 | |||
| 252bda696f | |||
| ef822d0eba | |||
| cc54fd52c6 | |||
| e4115b8baa | |||
| b99e3e88c9 | |||
| 35198a8edb | |||
| c9142c3f8a | |||
| 9797ddf3f7 | |||
| b92239fb1b | |||
| 105f4d5492 | |||
| e92349e111 | |||
| 0912d658b8 | |||
| 8e63db4424 | |||
| 0335937312 | |||
| 6a0f8d7196 | |||
| 407a63f3cf | |||
| eec8b477be | |||
| 489e95d677 | |||
| 47cbcb25bc | |||
| 527c411d87 | |||
| c8aecaeb03 | |||
| bf63610510 | |||
| 7af3a7f21d | |||
| 4783e9d88e | |||
| ccbd24be11 | |||
| 16ed5459fa |
1819
logs/app.log
1819
logs/app.log
File diff suppressed because it is too large
Load Diff
Binary file not shown.
2300
xian_favor/api.py
2300
xian_favor/api.py
File diff suppressed because it is too large
Load Diff
338
xian_favor/db.py
338
xian_favor/db.py
@@ -131,6 +131,19 @@ class Database:
|
||||
)
|
||||
""")
|
||||
|
||||
# 文件夹表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
parent_id INTEGER DEFAULT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES folders(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_type ON items(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_status ON items(status)")
|
||||
@@ -163,6 +176,42 @@ class Database:
|
||||
except sqlite3.OperationalError:
|
||||
cursor.execute("ALTER TABLE items ADD COLUMN deleted_at TEXT")
|
||||
|
||||
# 检查并添加 folder_id 字段(兼容旧数据库)
|
||||
try:
|
||||
cursor.execute("SELECT folder_id FROM items LIMIT 1")
|
||||
except sqlite3.OperationalError:
|
||||
cursor.execute("ALTER TABLE items ADD COLUMN folder_id INTEGER DEFAULT NULL")
|
||||
|
||||
# 创建 folder 相关索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_folder ON items(folder_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_type ON folders(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id)")
|
||||
|
||||
# 检查并添加 is_pinned 字段(置顶功能,兼容旧数据库)
|
||||
try:
|
||||
cursor.execute("SELECT is_pinned FROM items LIMIT 1")
|
||||
except sqlite3.OperationalError:
|
||||
cursor.execute("ALTER TABLE items ADD COLUMN is_pinned INTEGER DEFAULT 0")
|
||||
|
||||
# 创建 is_pinned 索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_pinned ON items(is_pinned)")
|
||||
|
||||
# 待办事务表(关联到文本类别的items)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS todo_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
remaining_days INTEGER DEFAULT 1,
|
||||
is_completed INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_todo_events_item ON todo_events(item_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_todo_events_created ON todo_events(created_at)")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# ============ Item 操作 ============
|
||||
@@ -170,7 +219,8 @@ class Database:
|
||||
def create_item(self, type: str = "text", title: str = None, content: str = None,
|
||||
url: str = None, source: str = None, status: str = "pending",
|
||||
priority: str = "medium", due_date: str = None, note: str = None,
|
||||
tags: List[str] = None, is_starred: bool = False) -> int:
|
||||
tags: List[str] = None, is_starred: bool = False,
|
||||
folder_id: int = None) -> int:
|
||||
"""创建新条目"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
@@ -184,9 +234,9 @@ class Database:
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, now, now))
|
||||
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, folder_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, folder_id, now, now))
|
||||
item_id = cursor.lastrowid
|
||||
|
||||
# 添加标签
|
||||
@@ -210,12 +260,14 @@ class Database:
|
||||
return item
|
||||
|
||||
def list_items(self, type: str = None, status: str = None, tag: str = None,
|
||||
keyword: str = None, starred: bool = None, sort_by: str = None,
|
||||
sort_order: str = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
keyword: str = None, starred: bool = None, folder_id: int = None,
|
||||
sort_by: str = None, sort_order: str = None,
|
||||
limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""列出条目
|
||||
|
||||
sort_by: created_at, updated_at
|
||||
sort_order: desc, asc
|
||||
folder_id: 文件夹ID,None表示不限制,-1表示未分类(folder_id为null)
|
||||
"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -245,6 +297,15 @@ class Database:
|
||||
conditions.append("i.is_starred = ?")
|
||||
params.append(1 if starred else 0)
|
||||
|
||||
# 文件夹过滤
|
||||
if folder_id is not None:
|
||||
if folder_id == -1:
|
||||
# -1 表示未分类(folder_id 为 null)
|
||||
conditions.append("i.folder_id IS NULL")
|
||||
else:
|
||||
conditions.append("i.folder_id = ?")
|
||||
params.append(folder_id)
|
||||
|
||||
if keyword:
|
||||
conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)")
|
||||
keyword_pattern = f"%{keyword}%"
|
||||
@@ -253,18 +314,16 @@ class Database:
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
# 排序逻辑
|
||||
# 排序逻辑:置顶 > 关注 > 创建时间
|
||||
if sort_by == 'updated_at':
|
||||
order_field = 'i.updated_at'
|
||||
elif sort_by == 'created_at':
|
||||
order_field = 'i.created_at'
|
||||
else:
|
||||
# 默认:重点关注优先 + 创建时间降序
|
||||
# 默认:置顶优先 + 关注优先 + 创建时间降序
|
||||
order_field = 'i.created_at'
|
||||
|
||||
order_dir = 'DESC' if (sort_order == 'asc' or sort_order is None) else 'ASC'
|
||||
# 这里反转逻辑:用户选择"降序"时用DESC,选择"升序"时用ASC
|
||||
|
||||
# 确定排序方向
|
||||
if sort_order == 'asc':
|
||||
order_dir = 'ASC'
|
||||
elif sort_order == 'desc':
|
||||
@@ -272,12 +331,12 @@ class Database:
|
||||
else:
|
||||
order_dir = 'DESC' # 默认降序
|
||||
|
||||
# 如果有指定排序字段,按该字段排序;否则默认重点关注优先
|
||||
# 如果有指定排序字段,按该字段排序,但置顶始终优先
|
||||
if sort_by:
|
||||
query += f" ORDER BY {order_field} {order_dir} LIMIT ? OFFSET ?"
|
||||
query += f" ORDER BY i.is_pinned DESC, {order_field} {order_dir} LIMIT ? OFFSET ?"
|
||||
else:
|
||||
# 默认:重点关注优先,然后创建时间降序
|
||||
query += f" ORDER BY i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
|
||||
# 默认:置顶 > 关注 > 创建时间降序
|
||||
query += f" ORDER BY i.is_pinned DESC, i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
|
||||
params.extend([limit, offset])
|
||||
|
||||
cursor.execute(query, params)
|
||||
@@ -290,7 +349,7 @@ class Database:
|
||||
return items
|
||||
|
||||
def count_items(self, type: str = None, status: str = None, tag: str = None,
|
||||
keyword: str = None, starred: bool = None) -> int:
|
||||
keyword: str = None, starred: bool = None, folder_id: int = None) -> int:
|
||||
"""计算符合条件的条目总数"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -322,6 +381,15 @@ class Database:
|
||||
keyword_pattern = f"%{keyword}%"
|
||||
params.extend([keyword_pattern, keyword_pattern, keyword_pattern])
|
||||
|
||||
# 文件夹过滤
|
||||
if folder_id is not None:
|
||||
if folder_id == -1:
|
||||
# 未分类:folder_id为NULL
|
||||
conditions.append("i.folder_id IS NULL")
|
||||
else:
|
||||
conditions.append("i.folder_id = ?")
|
||||
params.append(folder_id)
|
||||
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
@@ -330,7 +398,7 @@ class Database:
|
||||
|
||||
def update_item(self, item_id: int, **kwargs) -> bool:
|
||||
"""更新条目"""
|
||||
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note', 'is_starred']
|
||||
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note', 'is_starred', 'folder_id']
|
||||
update_fields = {k: v for k, v in kwargs.items() if k in allowed_fields}
|
||||
|
||||
# 只有 tags 变化也算有效更新
|
||||
@@ -456,6 +524,31 @@ class Database:
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def toggle_pin(self, item_id: int) -> bool:
|
||||
"""切换置顶状态"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
# 先获取当前状态
|
||||
cursor.execute("SELECT is_pinned FROM items WHERE id = ?", (item_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return False
|
||||
|
||||
new_status = 0 if row['is_pinned'] else 1
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("UPDATE items SET is_pinned = ?, updated_at = ? WHERE id = ?", (new_status, now, item_id))
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
def set_pin(self, item_id: int, pinned: bool = True) -> bool:
|
||||
"""设置置顶状态"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("UPDATE items SET is_pinned = ?, updated_at = ? WHERE id = ?", (1 if pinned else 0, now, item_id))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def increment_views(self, item_id: int) -> bool:
|
||||
"""增加阅读数"""
|
||||
with self.get_conn() as conn:
|
||||
@@ -464,6 +557,116 @@ class Database:
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def move_item_to_folder(self, item_id: int, folder_id: int) -> bool:
|
||||
"""将条目移动到文件夹"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("UPDATE items SET folder_id = ?, updated_at = ? WHERE id = ?", (folder_id, now, item_id))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
# ============ Folder 文件夹操作 ============
|
||||
|
||||
def create_folder(self, name: str, type: str, parent_id: int = None) -> int:
|
||||
"""创建文件夹
|
||||
|
||||
Args:
|
||||
name: 文件夹名称
|
||||
type: 类别类型(text/link/column/todo)
|
||||
parent_id: 父文件夹ID(目前不支持嵌套,预留)
|
||||
"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO folders (name, type, parent_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (name, type, parent_id, now, now))
|
||||
folder_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
return folder_id
|
||||
|
||||
def get_folder(self, folder_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取文件夹信息"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM folders WHERE id = ?", (folder_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
folder = dict(row)
|
||||
# 获取文件夹内条目数量
|
||||
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder_id,))
|
||||
folder['item_count'] = cursor.fetchone()[0]
|
||||
return folder
|
||||
|
||||
def list_folders(self, type: str = None) -> List[Dict[str, Any]]:
|
||||
"""列出文件夹
|
||||
|
||||
Args:
|
||||
type: 按类型过滤,None表示列出所有
|
||||
"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
if type:
|
||||
cursor.execute("SELECT * FROM folders WHERE type = ? ORDER BY created_at DESC", (type,))
|
||||
else:
|
||||
cursor.execute("SELECT * FROM folders ORDER BY created_at DESC")
|
||||
|
||||
folders = []
|
||||
for row in cursor.fetchall():
|
||||
folder = dict(row)
|
||||
# 获取文件夹内条目数量
|
||||
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder['id'],))
|
||||
folder['item_count'] = cursor.fetchone()[0]
|
||||
folders.append(folder)
|
||||
|
||||
return folders
|
||||
|
||||
def update_folder(self, folder_id: int, name: str = None) -> bool:
|
||||
"""更新文件夹"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
if name:
|
||||
cursor.execute("UPDATE folders SET name = ?, updated_at = ? WHERE id = ?", (name, now, folder_id))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
return False
|
||||
|
||||
def delete_folder(self, folder_id: int, move_items_to_root: bool = True) -> bool:
|
||||
"""删除文件夹
|
||||
|
||||
Args:
|
||||
folder_id: 文件夹ID
|
||||
move_items_to_root: 是否将条目移出文件夹(到未分类),True则移动,False则一起删除
|
||||
"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
if move_items_to_root:
|
||||
# 将条目移出文件夹(folder_id设为null)
|
||||
cursor.execute("UPDATE items SET folder_id = NULL WHERE folder_id = ?", (folder_id,))
|
||||
else:
|
||||
# 删除文件夹内的所有条目
|
||||
cursor.execute("DELETE FROM items WHERE folder_id = ?", (folder_id,))
|
||||
|
||||
# 删除文件夹
|
||||
cursor.execute("DELETE FROM folders WHERE id = ?", (folder_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def count_items_by_folder(self, folder_id: int) -> int:
|
||||
"""统计文件夹内的条目数量"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder_id,))
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
# ============ Draft 草稿操作 ============
|
||||
|
||||
def save_draft(self, type: str = "text", title: str = None, content: str = None,
|
||||
@@ -748,15 +951,23 @@ class Database:
|
||||
stats = {}
|
||||
|
||||
# 总数
|
||||
cursor.execute("SELECT COUNT(*) as count FROM items")
|
||||
cursor.execute("SELECT COUNT(*) as count FROM items WHERE is_deleted = 0")
|
||||
stats['total'] = cursor.fetchone()['count']
|
||||
|
||||
# 按类型统计
|
||||
cursor.execute("SELECT type, COUNT(*) as count FROM items GROUP BY type")
|
||||
cursor.execute("SELECT type, COUNT(*) as count FROM items WHERE is_deleted = 0 GROUP BY type")
|
||||
stats['by_type'] = {row['type']: row['count'] for row in cursor.fetchall()}
|
||||
|
||||
# 未读数量统计(views = 0)
|
||||
cursor.execute("SELECT COUNT(*) as count FROM items WHERE is_deleted = 0 AND (views IS NULL OR views = 0)")
|
||||
stats['unread'] = cursor.fetchone()['count']
|
||||
|
||||
# 按类型统计未读数量
|
||||
cursor.execute("SELECT type, COUNT(*) as count FROM items WHERE is_deleted = 0 AND (views IS NULL OR views = 0) GROUP BY type")
|
||||
stats['unread_by_type'] = {row['type']: row['count'] for row in cursor.fetchall()}
|
||||
|
||||
# 待办状态统计
|
||||
cursor.execute("SELECT status, COUNT(*) as count FROM items WHERE type = 'todo' GROUP BY status")
|
||||
cursor.execute("SELECT status, COUNT(*) as count FROM items WHERE type = 'todo' AND is_deleted = 0 GROUP BY status")
|
||||
stats['todo_status'] = {row['status']: row['count'] for row in cursor.fetchall()}
|
||||
|
||||
# 标签数
|
||||
@@ -1011,6 +1222,93 @@ class Database:
|
||||
# 删除
|
||||
self.delete_backup(backup['name'])
|
||||
|
||||
# ============ Todo Events 待办事务操作 ============
|
||||
|
||||
def create_todo_event(self, item_id: int, content: str, remaining_days: int = 1) -> int:
|
||||
"""创建待办事务"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO todo_events (item_id, content, remaining_days, is_completed, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 0, ?, ?)
|
||||
""", (item_id, content, remaining_days, now, now))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def list_todo_events(self, item_id: int, limit: int = 10, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""列出待办事务(按时间倒序)"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM todo_events
|
||||
WHERE item_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (item_id, limit, offset))
|
||||
rows = cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def count_todo_events(self, item_id: int) -> int:
|
||||
"""计算待办事务总数"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as count FROM todo_events WHERE item_id = ?", (item_id,))
|
||||
return cursor.fetchone()['count']
|
||||
|
||||
def get_todo_event(self, event_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取单个待办事务"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM todo_events WHERE id = ?", (event_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def update_todo_event(self, event_id: int, content: str = None, remaining_days: int = None, is_completed: bool = None) -> bool:
|
||||
"""更新待办事务"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取当前数据
|
||||
cursor.execute("SELECT * FROM todo_events WHERE id = ?", (event_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return False
|
||||
|
||||
# 更新字段
|
||||
new_content = content if content is not None else row['content']
|
||||
new_days = remaining_days if remaining_days is not None else row['remaining_days']
|
||||
new_completed = 1 if is_completed is True else (0 if is_completed is False else row['is_completed'])
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE todo_events
|
||||
SET content = ?, remaining_days = ?, is_completed = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""", (new_content, new_days, new_completed, now, event_id))
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
def delete_todo_event(self, event_id: int) -> bool:
|
||||
"""删除待办事务"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM todo_events WHERE id = ?", (event_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
# 全局数据库实例
|
||||
db = Database()
|
||||
Reference in New Issue
Block a user