feat: improve report submission form

This commit is contained in:
Codex 2026-05-07 22:24:04 +08:00
parent 3c7b7096dd
commit 5434d1b283
7 changed files with 345 additions and 80 deletions

View File

@ -41,6 +41,7 @@ class Database:
feishu_user_id TEXT NOT NULL,
employee_name TEXT NOT NULL,
report_date TEXT NOT NULL,
report_status TEXT NOT NULL DEFAULT 'normal',
today_done TEXT NOT NULL,
tomorrow_plan TEXT NOT NULL,
blockers TEXT NOT NULL DEFAULT '',
@ -56,6 +57,13 @@ class Database:
except sqlite3.OperationalError as error:
if "duplicate column name" not in str(error).lower():
raise
try:
self.connection.execute(
"ALTER TABLE daily_reports ADD COLUMN report_status TEXT NOT NULL DEFAULT 'normal'"
)
except sqlite3.OperationalError as error:
if "duplicate column name" not in str(error).lower():
raise
self.connection.commit()
def load_employees(self, employee_seed_path: Path) -> None:
@ -129,15 +137,16 @@ class Database:
self.connection.execute(
"""
INSERT INTO daily_reports (
feishu_user_id, employee_name, report_date, today_done, tomorrow_plan,
feishu_user_id, employee_name, report_date, report_status, today_done, tomorrow_plan,
blockers, help_needed, submitted_at, updated_at
)
VALUES (
:feishu_user_id, :employee_name, :report_date, :today_done, :tomorrow_plan,
:feishu_user_id, :employee_name, :report_date, :report_status, :today_done, :tomorrow_plan,
:blockers, :help_needed, :submitted_at, :updated_at
)
ON CONFLICT(feishu_user_id, report_date) DO UPDATE SET
employee_name = excluded.employee_name,
report_status = excluded.report_status,
today_done = excluded.today_done,
tomorrow_plan = excluded.tomorrow_plan,
blockers = excluded.blockers,
@ -151,7 +160,7 @@ class Database:
with self._lock:
rows = self.connection.execute(
"""
SELECT id, feishu_user_id, employee_name, report_date, today_done,
SELECT id, feishu_user_id, employee_name, report_date, report_status, today_done,
tomorrow_plan, blockers, help_needed, submitted_at, updated_at
FROM daily_reports
WHERE report_date = ?
@ -165,7 +174,7 @@ class Database:
with self._lock:
rows = self.connection.execute(
"""
SELECT id, feishu_user_id, employee_name, report_date, today_done,
SELECT id, feishu_user_id, employee_name, report_date, report_status, today_done,
tomorrow_plan, blockers, help_needed, submitted_at, updated_at
FROM daily_reports
WHERE feishu_user_id = ?
@ -175,3 +184,18 @@ class Database:
(feishu_user_id, limit),
).fetchall()
return [dict(row) for row in rows]
def find_previous_report(self, feishu_user_id: str, before_date: str) -> dict[str, Any] | None:
with self._lock:
row = self.connection.execute(
"""
SELECT id, feishu_user_id, employee_name, report_date, report_status, today_done,
tomorrow_plan, blockers, help_needed, submitted_at, updated_at
FROM daily_reports
WHERE feishu_user_id = ? AND report_date < ?
ORDER BY report_date DESC, updated_at DESC
LIMIT 1
""",
(feishu_user_id, before_date),
).fetchone()
return dict(row) if row else None

View File

@ -22,6 +22,14 @@ def _optional_text(value: Any) -> str:
return str(value or "").strip()
def _report_status(value: Any) -> str:
status = str(value or "normal").strip()
allowed = {"normal", "risk", "need_help"}
if status not in allowed:
raise ValueError("report_status is invalid")
return status
class ReportService:
def __init__(self, database: Database, clock: Clock | None = None):
self.database = database
@ -42,6 +50,7 @@ class ReportService:
"feishu_user_id": feishu_user_id,
"employee_name": employee["name"],
"report_date": report_date,
"report_status": _report_status(data.get("report_status")),
"today_done": today_done,
"tomorrow_plan": tomorrow_plan,
"blockers": _optional_text(data.get("blockers")),
@ -81,6 +90,7 @@ class ReportService:
"tomorrow_plan",
"blockers",
"help_needed",
"report_status",
"submitted_at",
]
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
@ -99,6 +109,17 @@ class ReportService:
"reports": self.database.list_reports_for_employee(user_id, limit),
}
def previous_report_reference(self, feishu_user_id: str, before_date: str) -> dict[str, Any]:
user_id = _required_text(feishu_user_id, "feishu_user_id")
date = _required_text(before_date, "report_date")
employee = self.database.find_employee(user_id)
if not employee or employee.get("active") != 1:
raise ValueError("employee is not active")
return {
"employee": employee,
"report": self.database.find_previous_report(user_id, date),
}
def is_admin(self, feishu_user_id: str) -> bool:
user_id = _required_text(feishu_user_id, "feishu_user_id")
employee = self.database.find_employee(user_id)

View File

@ -1,9 +1,25 @@
const state = { data: null };
const statusLabels = {
normal: "正常",
risk: "有风险",
need_help: "需要支持"
};
function text(value) {
return String(value || "");
}
function escapeHtml(value) {
return text(value).replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
}[char]));
}
function render() {
const employeeQuery = document.querySelector("#employee-filter").value.trim().toLowerCase();
const keywordQuery = document.querySelector("#keyword-filter").value.trim().toLowerCase();
@ -18,22 +34,33 @@ function render() {
const reports = data.reports.filter((report) => {
const employeeMatch = report.employee_name.toLowerCase().includes(employeeQuery);
const searchable = [report.today_done, report.tomorrow_plan, report.blockers, report.help_needed].join(" ").toLowerCase();
const searchable = [
report.today_done,
report.tomorrow_plan,
report.blockers,
report.help_needed,
statusLabels[report.report_status]
].join(" ").toLowerCase();
const keywordMatch = searchable.includes(keywordQuery);
const blockerMatch = !onlyBlockers || report.blockers || report.help_needed;
const blockerMatch = !onlyBlockers || report.blockers || report.help_needed || report.report_status === "need_help";
return employeeMatch && keywordMatch && blockerMatch;
});
document.querySelector("#reports").innerHTML = reports.length ? reports.map((report) => `
<article class="report-card">
<h3>${report.employee_name}</h3>
<div class="report-grid">
<div><div class="field-title">今日完成</div><div>${text(report.today_done)}</div></div>
<div><div class="field-title">明日计划</div><div>${text(report.tomorrow_plan)}</div></div>
<div><div class="field-title">遇到的问题</div><div>${text(report.blockers) || ""}</div></div>
<div><div class="field-title">需要协助</div><div>${text(report.help_needed) || ""}</div></div>
<div class="report-head">
<h3>${escapeHtml(report.employee_name)}</h3>
<span class="status-badge status-${escapeHtml(report.report_status || "normal")}">
${statusLabels[report.report_status] || "正常"}
</span>
</div>
<p>提交时间${report.updated_at}</p>
<div class="report-grid">
<div><div class="field-title">今日完成</div><pre>${escapeHtml(report.today_done)}</pre></div>
<div><div class="field-title">明日计划</div><pre>${escapeHtml(report.tomorrow_plan)}</pre></div>
<div><div class="field-title">遇到的问题</div><pre>${escapeHtml(report.blockers || "")}</pre></div>
<div><div class="field-title">需要协助</div><pre>${escapeHtml(report.help_needed || "")}</pre></div>
</div>
<p>提交时间${escapeHtml(report.updated_at)}</p>
</article>
`).join("") : "<p>当前筛选下没有日报。</p>";

View File

@ -32,6 +32,66 @@ button {
border: 0;
cursor: pointer;
}
.identity-box {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid #dbeafe;
border-radius: 6px;
background: #eff6ff;
}
.status-options {
display: flex;
flex-wrap: wrap;
gap: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
}
.status-options legend { font-weight: 700; }
.status-options label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 400;
}
.item-group {
display: grid;
gap: 8px;
}
.item-group h2 { margin: 0; }
.item-group [data-list] {
display: grid;
gap: 8px;
}
.previous-plan {
display: grid;
gap: 10px;
margin-top: 18px;
background: #f8fafc;
}
.previous-plan[hidden] { display: none; }
.previous-plan h2, .previous-plan p { margin: 0; }
.previous-plan button { justify-self: start; }
.report-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.status-badge {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 4px 10px;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
}
.status-normal { background: #dcfce7; color: #166534; }
.status-risk { background: #fef3c7; color: #92400e; }
.status-need_help { background: #fee2e2; color: #991b1b; }
.filters {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
@ -68,6 +128,6 @@ pre {
.message.success { color: #047857; }
.message.error { color: #b91c1c; }
@media (max-width: 760px) {
.topbar, .stats, .history-head { display: grid; }
.topbar, .stats, .history-head, .report-head { display: grid; }
.filters, .report-grid { grid-template-columns: 1fr; }
}

View File

@ -43,7 +43,6 @@ def page(title: str, body: str, scripts: str = "") -> bytes:
def submit_page(current_date: str, session: dict[str, str] | None = None) -> bytes:
numbered_template = "1. \n2. \n3. \n4. "
if session:
identity_html = f"""
<div class="identity-box">
@ -52,38 +51,102 @@ def submit_page(current_date: str, session: dict[str, str] | None = None) -> byt
</div>
<input id="feishu-user-id" type="hidden" name="feishu_user_id" value="{escape(session["feishu_user_id"])}">
"""
history_hint = "系统已自动识别你的飞书身份,可以查看自己最近提交的记录"
history_hint = "系统已自动识别你的飞书身份,这里会显示你最近提交过的日报"
else:
identity_html = '<label>员工 ID<input id="feishu-user-id" name="feishu_user_id" required placeholder="例如 u_alice"></label>'
history_hint = "输入员工 ID 后可以查看自己最近提交的记录"
history_hint = "输入员工 ID 后,可以查看自己最近提交过的日报"
return page(
"每日报告",
f"""
<main class="shell narrow">
<h1>每日</h1>
<h1>每日工作汇</h1>
<form id="report-form" class="panel">
{identity_html}
<label>日期<input name="report_date" type="date" required value="{escape(current_date)}"></label>
<label>今日完成<textarea name="today_done" required rows="5">{numbered_template}</textarea></label>
<label>明日计划<textarea name="tomorrow_plan" required rows="4">{numbered_template}</textarea></label>
<label>遇到的问题<textarea name="blockers" rows="3"></textarea></label>
<label>需要协助<textarea name="help_needed" rows="3"></textarea></label>
<label>日期<input id="report-date" name="report_date" type="date" required value="{escape(current_date)}"></label>
<fieldset class="status-options">
<legend>今日状态</legend>
<label><input type="radio" name="report_status" value="normal" checked> 正常</label>
<label><input type="radio" name="report_status" value="risk"> 有风险</label>
<label><input type="radio" name="report_status" value="need_help"> 需要支持</label>
</fieldset>
<section class="item-group">
<h2>今日完成</h2>
<div data-list="today_done">
<input placeholder="1.">
<input placeholder="2.">
<input placeholder="3.">
<input placeholder="4.">
</div>
</section>
<section class="item-group">
<h2>明日计划</h2>
<div data-list="tomorrow_plan">
<input placeholder="1.">
<input placeholder="2.">
<input placeholder="3.">
<input placeholder="4.">
</div>
</section>
<label>遇到的问题<textarea name="blockers" rows="3" placeholder="没有可以不填"></textarea></label>
<label>需要协助<textarea name="help_needed" rows="3" placeholder="没有可以不填"></textarea></label>
<button type="submit">提交日报</button>
<p id="form-message" class="message"></p>
</form>
<section id="previous-plan" class="panel previous-plan" hidden>
<div>
<h2>上次明日计划参考</h2>
<p id="previous-plan-date"></p>
</div>
<pre id="previous-plan-content"></pre>
<button id="use-previous-plan" type="button">填入今日完成</button>
</section>
<section class="panel history-panel">
<div class="history-head">
<div>
<h2>我的历史日报</h2>
<p>{history_hint}</p>
</div>
<button id="load-history" type="button">查看历史</button>
<button id="load-history" type="button">刷新历史</button>
</div>
<div id="history-list" class="history-list">暂无历史记录</div>
</section>
</main>
<script>
const statusLabels = {{
normal: "正常",
risk: "有风险",
need_help: "需要支持"
}};
function escapeHtml(value) {{
return String(value || "").replace(/[&<>"']/g, (char) => ({{
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
}}[char]));
}}
function collectItems(name) {{
const values = Array.from(document.querySelectorAll(`[data-list="${{name}}"] input`))
.map((input) => input.value.trim())
.filter(Boolean);
return values.map((value, index) => `${{index + 1}}. ${{value}}`).join("\\n");
}}
function fillItemInputs(name, text) {{
const inputs = Array.from(document.querySelectorAll(`[data-list="${{name}}"] input`));
const lines = String(text || "")
.split(/\\n+/)
.map((line) => line.replace(/^\\s*\\d+[\\.)]\\s*/, "").trim())
.filter(Boolean);
inputs.forEach((input, index) => {{
input.value = lines[index] || "";
}});
}}
function renderHistory(data) {{
const container = document.querySelector("#history-list");
if (!data.reports.length) {{
@ -92,16 +155,16 @@ function renderHistory(data) {{
}}
container.innerHTML = data.reports.map((report) => `
<article class="history-card">
<h3>${{report.report_date}}</h3>
<div><strong>今日完成</strong><pre>${{report.today_done}}</pre></div>
<div><strong>明日计划</strong><pre>${{report.tomorrow_plan}}</pre></div>
<div><strong>问题</strong><pre>${{report.blockers || ""}}</pre></div>
<div><strong>协助</strong><pre>${{report.help_needed || ""}}</pre></div>
<h3>${{escapeHtml(report.report_date)}} · ${{statusLabels[report.report_status] || "正常"}}</h3>
<div><strong>今日完成</strong><pre>${{escapeHtml(report.today_done)}}</pre></div>
<div><strong>明日计划</strong><pre>${{escapeHtml(report.tomorrow_plan)}}</pre></div>
<div><strong>问题</strong><pre>${{escapeHtml(report.blockers || "")}}</pre></div>
<div><strong>协助</strong><pre>${{escapeHtml(report.help_needed || "")}}</pre></div>
</article>
`).join("");
}}
document.querySelector("#load-history").addEventListener("click", async () => {{
async function loadHistory() {{
const userId = document.querySelector("#feishu-user-id").value.trim();
const container = document.querySelector("#history-list");
if (!userId) {{
@ -115,11 +178,42 @@ document.querySelector("#load-history").addEventListener("click", async () => {{
const result = await response.json();
container.textContent = result.error || "历史记录加载失败。";
}}
}}
async function loadPreviousPlan() {{
const userId = document.querySelector("#feishu-user-id").value.trim();
const reportDate = document.querySelector("#report-date").value;
const panel = document.querySelector("#previous-plan");
if (!userId || !reportDate) {{
panel.hidden = true;
return;
}}
const response = await fetch(`/api/reports/previous?feishu_user_id=${{encodeURIComponent(userId)}}&date=${{encodeURIComponent(reportDate)}}`);
if (!response.ok) {{
panel.hidden = true;
return;
}}
const result = await response.json();
if (!result.report) {{
panel.hidden = true;
return;
}}
document.querySelector("#previous-plan-date").textContent = `${{result.report.report_date}} 提交`;
document.querySelector("#previous-plan-content").textContent = result.report.tomorrow_plan;
panel.hidden = false;
}}
document.querySelector("#load-history").addEventListener("click", loadHistory);
document.querySelector("#report-date").addEventListener("change", loadPreviousPlan);
document.querySelector("#use-previous-plan").addEventListener("click", () => {{
fillItemInputs("today_done", document.querySelector("#previous-plan-content").textContent);
}});
document.querySelector("#report-form").addEventListener("submit", async (event) => {{
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
data.today_done = collectItems("today_done");
data.tomorrow_plan = collectItems("tomorrow_plan");
const response = await fetch("/api/reports", {{
method: "POST",
headers: {{ "content-type": "application/json" }},
@ -129,13 +223,17 @@ document.querySelector("#report-form").addEventListener("submit", async (event)
if (response.ok) {{
message.textContent = "提交成功。";
message.className = "message success";
document.querySelector("#load-history").click();
loadHistory();
loadPreviousPlan();
}} else {{
const result = await response.json();
message.textContent = result.error || "提交失败。";
message.className = "message error";
}}
}});
loadHistory();
loadPreviousPlan();
</script>""",
)
@ -177,7 +275,7 @@ def forbidden_page() -> bytes:
<main class="shell narrow">
<section class="panel">
<h1>无权限</h1>
<p>你没有查看日报汇总的权限请联系管理员开通</p>
<p>你没有查看日报汇总的权限请联系管理员开通</p>
<p><a href="/submit">返回日报填写页</a></p>
</section>
</main>""",
@ -300,6 +398,14 @@ class DailyReportHandler(BaseHTTPRequestHandler):
except ValueError as error:
self._json(400, {"error": str(error)})
return
if parsed.path == "/api/reports/previous":
user_id = query.get("feishu_user_id", [""])[0]
report_date = query.get("date", [today_string()])[0]
try:
self._json(200, self.report_service.previous_report_reference(user_id, report_date))
except ValueError as error:
self._json(400, {"error": str(error)})
return
if parsed.path == "/api/reports/export":
if not self._require_admin():
return

View File

@ -42,7 +42,7 @@ class ReportServiceTest(unittest.TestCase):
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"today_done": "",
"tomorrow_plan": "管理页",
"tomorrow_plan": "管理页",
}
)
finally:
@ -56,29 +56,32 @@ class ReportServiceTest(unittest.TestCase):
{
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"today_done": "完成原型初稿",
"tomorrow_plan": "和主管评审",
"report_status": "risk",
"today_done": "1. 完成原型初稿",
"tomorrow_plan": "1. 和主管评审",
}
)
service.upsert_report(
{
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"today_done": "完成原型初稿并调整文案",
"tomorrow_plan": "和主管评审",
"report_status": "need_help",
"today_done": "1. 完成原型初稿\n2. 调整文案",
"tomorrow_plan": "1. 和主管评审",
"blockers": "需要确认 logo",
"help_needed": "确认视觉方向",
}
)
result = service.list_reports_for_date("2026-05-07")
self.assertEqual(len(result["reports"]), 1)
self.assertEqual(result["reports"][0]["today_done"], "完成原型初稿并调整文案")
self.assertEqual(result["reports"][0]["report_status"], "need_help")
self.assertEqual(result["reports"][0]["today_done"], "1. 完成原型初稿\n2. 调整文案")
self.assertEqual(result["reports"][0]["blockers"], "需要确认 logo")
self.assertEqual(len(result["missing"]), 0)
finally:
db.close()
def test_returns_missing_active_employees_and_csv_export(self) -> None:
def test_returns_missing_active_employees_csv_and_previous_reference(self) -> None:
db = make_database(
[
{"feishu_user_id": "u_1", "name": "Lin", "department": "Design", "active": True},
@ -87,12 +90,20 @@ class ReportServiceTest(unittest.TestCase):
)
service = ReportService(db, clock=lambda: datetime(2026, 5, 7, 10, tzinfo=timezone.utc))
try:
service.upsert_report(
{
"feishu_user_id": "u_1",
"report_date": "2026-05-06",
"today_done": "1. 完成上线检查清单",
"tomorrow_plan": "1. 准备发布",
}
)
service.upsert_report(
{
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"today_done": "完成上线检查清单",
"tomorrow_plan": "准备发布",
"today_done": "1. 准备发布",
"tomorrow_plan": "1. 复盘问题",
}
)
result = service.list_reports_for_date("2026-05-07")
@ -100,8 +111,12 @@ class ReportServiceTest(unittest.TestCase):
self.assertEqual(result["missing"][0]["name"], "Chen")
csv_text = service.export_reports_csv("2026-05-07")
self.assertIn("employee_name,report_date,today_done,tomorrow_plan,blockers,help_needed,submitted_at", csv_text)
self.assertIn("Lin,2026-05-07,完成上线检查清单,准备发布", csv_text)
self.assertIn("employee_name,report_date,today_done,tomorrow_plan,blockers,help_needed,report_status,submitted_at", csv_text)
self.assertIn("Lin,2026-05-07,1. 准备发布,1. 复盘问题,,,normal,", csv_text)
reference = service.previous_report_reference("u_1", "2026-05-07")
self.assertEqual(reference["report"]["report_date"], "2026-05-06")
self.assertEqual(reference["report"]["tomorrow_plan"], "1. 准备发布")
finally:
db.close()

View File

@ -4,6 +4,7 @@ import json
import tempfile
import threading
import unittest
import urllib.error
import urllib.request
from pathlib import Path
@ -52,19 +53,23 @@ def get_with_cookie(url: str, cookie: str) -> tuple[int, str]:
return response.status, response.read().decode("utf-8")
def admin_cookie(user_id: str = "u_1", name: str = "Lin") -> str:
return "daily_report_session=" + create_session_cookie(
{"feishu_user_id": user_id, "name": name}, "session-secret"
)
class WebTest(unittest.TestCase):
def test_serves_submit_and_manager_pages(self) -> None:
server, db, base_url = make_server()
try:
status, submit = get(f"{base_url}/submit")
self.assertEqual(status, 200)
self.assertIn("每日报告", submit)
self.assertIn("1. \n2. \n3. \n4.", submit)
self.assertIn("每日工作汇报", submit)
self.assertIn("今日状态", submit)
self.assertIn('data-list="today_done"', submit)
self.assertIn("我的历史日报", submit)
admin_cookie = "daily_report_session=" + create_session_cookie(
{"feishu_user_id": "u_1", "name": "Lin"}, "session-secret"
)
db.upsert_employee(
{
"feishu_user_id": "u_1",
@ -75,7 +80,7 @@ class WebTest(unittest.TestCase):
"role": "admin",
}
)
status, manager = get_with_cookie(f"{base_url}/manager", admin_cookie)
status, manager = get_with_cookie(f"{base_url}/manager", admin_cookie())
self.assertEqual(status, 200)
self.assertIn("日报浏览", manager)
finally:
@ -83,24 +88,42 @@ class WebTest(unittest.TestCase):
server.server_close()
db.close()
def test_accepts_report_submission_and_returns_summary(self) -> None:
def test_accepts_report_submission_and_returns_summary_and_previous_reference(self) -> None:
server, db, base_url = make_server()
try:
request = urllib.request.Request(
first_request = urllib.request.Request(
f"{base_url}/api/reports",
data=json.dumps(
{
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"today_done": "完成 API",
"tomorrow_plan": "完善界面",
"report_date": "2026-05-06",
"today_done": "1. 完成 API",
"tomorrow_plan": "1. 完善界面",
},
ensure_ascii=False,
).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=5) as response:
with urllib.request.urlopen(first_request, timeout=5) as response:
self.assertEqual(response.status, 200)
second_request = urllib.request.Request(
f"{base_url}/api/reports",
data=json.dumps(
{
"feishu_user_id": "u_1",
"report_date": "2026-05-07",
"report_status": "need_help",
"today_done": "1. 完善界面",
"tomorrow_plan": "1. 上线测试",
},
ensure_ascii=False,
).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
with urllib.request.urlopen(second_request, timeout=5) as response:
self.assertEqual(response.status, 200)
db.upsert_employee(
@ -113,20 +136,24 @@ class WebTest(unittest.TestCase):
"role": "admin",
}
)
admin_cookie = "daily_report_session=" + create_session_cookie(
{"feishu_user_id": "u_1", "name": "Lin"}, "session-secret"
)
status, body = get_with_cookie(f"{base_url}/api/reports?date=2026-05-07", admin_cookie)
status, body = get_with_cookie(f"{base_url}/api/reports?date=2026-05-07", admin_cookie())
summary = json.loads(body)
self.assertEqual(status, 200)
self.assertEqual(summary["submittedCount"], 1)
self.assertEqual(summary["reports"][0]["today_done"], "完成 API")
self.assertEqual(summary["reports"][0]["today_done"], "1. 完善界面")
self.assertEqual(summary["reports"][0]["report_status"], "need_help")
status, history_body = get(f"{base_url}/api/reports/history?feishu_user_id=u_1")
history = json.loads(history_body)
self.assertEqual(status, 200)
self.assertEqual(history["employee"]["name"], "Lin")
self.assertEqual(history["reports"][0]["tomorrow_plan"], "完善界面")
self.assertEqual(history["reports"][0]["tomorrow_plan"], "1. 上线测试")
status, reference_body = get(f"{base_url}/api/reports/previous?feishu_user_id=u_1&date=2026-05-07")
reference = json.loads(reference_body)
self.assertEqual(status, 200)
self.assertEqual(reference["report"]["report_date"], "2026-05-06")
self.assertEqual(reference["report"]["tomorrow_plan"], "1. 完善界面")
finally:
server.shutdown()
server.server_close()
@ -135,18 +162,8 @@ class WebTest(unittest.TestCase):
def test_submit_page_uses_logged_in_feishu_identity(self) -> None:
server, db, base_url = make_server()
try:
opener = urllib.request.build_opener()
opener.addheaders = [
(
"Cookie",
"daily_report_session="
+ create_session_cookie({"feishu_user_id": "u_1", "name": "Lin"}, "session-secret"),
)
]
with opener.open(f"{base_url}/submit", timeout=5) as response:
body = response.read().decode("utf-8")
self.assertEqual(response.status, 200)
status, body = get_with_cookie(f"{base_url}/submit", admin_cookie())
self.assertEqual(status, 200)
self.assertIn("Lin", body)
self.assertIn('type="hidden" name="feishu_user_id" value="u_1"', body)
self.assertNotIn("员工 ID<input", body)
@ -163,17 +180,12 @@ class WebTest(unittest.TestCase):
]
)
try:
staff_cookie = "daily_report_session=" + create_session_cookie(
{"feishu_user_id": "u_staff", "name": "Staff"}, "session-secret"
)
staff_cookie = admin_cookie("u_staff", "Staff")
with self.assertRaises(urllib.error.HTTPError) as error:
get_with_cookie(f"{base_url}/manager", staff_cookie)
self.assertEqual(error.exception.code, 403)
admin_cookie = "daily_report_session=" + create_session_cookie(
{"feishu_user_id": "u_admin", "name": "Admin"}, "session-secret"
)
status, body = get_with_cookie(f"{base_url}/manager", admin_cookie)
status, body = get_with_cookie(f"{base_url}/manager", admin_cookie("u_admin", "Admin"))
self.assertEqual(status, 200)
self.assertIn("日报浏览", body)
finally: