ciit-checkin 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2025 Yongshun Song
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: ciit-checkin
3
+ Version: 0.1.0
4
+ Summary: 简易签到系统:提供基于 HTTP 的签到表单、管理页面与导出 CSV 功能,适合教室点名与记录。
5
+ Author-email: Yongshun Song <ys_song@foxmail.com>
6
+ Maintainer-email: Yongshun Song <ys_song@foxmail.com>
7
+ License-Expression: MIT
8
+ Keywords: ciit,checkin,attendance,csv,admin
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # 项目说明
14
+ 简要说明项目用途:一个用于签到的轻量服务,提供前端签到页面和管理员页面,用于管理名单、保存签到记录(CSV)与重置数据。
15
+
16
+ # 运行方法
17
+ 1. 进入项目根目录(包含 `server.py`)。
18
+ 2. 推荐创建虚拟环境并安装依赖:
19
+ ```bash
20
+ python -m venv venv
21
+ source venv/Scripts/activate # Windows: venv\Scripts\activate
22
+ pip install -r requirements.txt
23
+ ```
24
+ 3. 启动服务(任选其一):
25
+ - 直接运行脚本:
26
+ ```bash
27
+ python server.py
28
+ ```
29
+ - 或使用 Flask(若适用):
30
+ ```bash
31
+ export FLASK_APP=server.py
32
+ flask run --host=0.0.0.0 --port=5000
33
+ ```
34
+ 4. 默认访问地址(按实际日志或配置调整):
35
+ - 签到页面(前端):http://localhost:5000/ 或 http://localhost:5000/checkin
36
+ - 管理页面: http://localhost:5000/admin 或 http://localhost:5000/manage
37
+
38
+ # 配置说明
39
+ - 工作目录:服务以启动时的当前工作目录为基准读取/写入文件。建议从项目根目录启动。
40
+ - 配置文件(示例路径,可根据代码调整):
41
+ - data/name.txt — 名单
42
+ - data/checkins.csv — 签到记录(CSV)
43
+ - data/room_info.txt — 房间或活动相关配置
44
+
45
+ # 文件位置与格式
46
+ - data/name.txt
47
+ - 路径示例:./data/name.txt
48
+ - 格式:UTF-8,逐行一个姓名或记录。示例:
49
+ ```
50
+ 张三
51
+ 李四
52
+ 王五
53
+ ```
54
+ - 如需额外字段(例如 id 或工号),可采用逗号分隔:`name,id`(需与代码解析方式一致)。
55
+
56
+ - data/checkins.csv
57
+ - 路径示例:./data/checkins.csv
58
+ - 推荐字段(CSV首行头部):timestamp,name,room,notes
59
+ - 示例内容:
60
+ ```
61
+ timestamp,name,room,notes
62
+ 2025-11-11T09:02:15,张三,RoomA,第一次签到
63
+ 2025-11-11T09:10:40,李四,RoomB,
64
+ ```
65
+
66
+ - data/room_info.txt
67
+ - 路径示例:./data/room_info.txt
68
+ - 常见格式(选择与代码匹配的格式):
69
+ - CSV 格式(room_id,display_name,capacity):
70
+ ```
71
+ RoomA,一号教室,30
72
+ RoomB,二号教室,25
73
+ ```
74
+ - 或 key=value 风格:
75
+ ```
76
+ RoomA.name=一号教室
77
+ RoomA.capacity=30
78
+ RoomB.name=二号教室
79
+ RoomB.capacity=25
80
+ ```
81
+
82
+ # 启动后常用操作
83
+ - 访问签到页面:在浏览器打开签到 URL,输入姓名或选择名单进行签到,界面操作通常会将记录追加到 `data/checkins.csv`。
84
+ - 访问管理页面:打开管理 URL,可查看/编辑名单、导出 CSV、或执行重置操作(具体按钮/功能与实现相关)。
85
+ - 保存操作:在管理页面点击“保存”时,名单应写入 `data/name.txt`,签到记录写入 `data/checkins.csv`。确认服务器进程有写权限。
86
+ - 重置操作:管理页面“重置”通常会清空或重命名当天的 CSV。若无界面,手动操作可:
87
+ ```bash
88
+ # 清空签到记录
89
+ > data/checkins.csv
90
+ # 或备份再清空
91
+ mv data/checkins.csv data/checkins-$(date +%F).csv
92
+ ```
93
+
94
+ # 示例 room_info.txt 片段
95
+ CSV 风格:
96
+ ```
97
+ RoomA,主会场,100
98
+ RoomB,分会场1,40
99
+ RoomC,分会场2,40
100
+ ```
101
+ key=value 风格:
102
+ ```
103
+ RoomA.name=主会场
104
+ RoomA.capacity=100
105
+ RoomB.name=分会场1
106
+ RoomB.capacity=40
107
+ ```
108
+
109
+ # 常见故障排查
110
+ - 页面无法访问
111
+ - 检查 server.py 是否在运行、控制台是否报错。
112
+ - 检查端口是否被占用或防火墙阻挡。
113
+ - 依赖缺失或导入错误
114
+ - 确认已安装 requirements.txt 中的包,使用正确的 Python 版本。
115
+ - 文件读写失败
116
+ - 检查 data 目录是否存在、服务器用户是否有读写权限。
117
+ - Windows 路径注意反斜杠和工作目录;以项目根目录启动服务。
118
+ - 中文/编码问题
119
+ - 确保所有文本文件使用 UTF-8 编码,避免 BOM 或其它编码导致解析错误。
120
+ - 名单/CSV 格式不生效
121
+ - 与代码中解析逻辑保持一致(是否期望逗号分隔、是否跳过空行、首行是否为头部)。
122
+ - 无法保存/重置
123
+ - 确认管理页面的按钮触发的 API 没有返回错误(检查浏览器开发者工具的网络请求和服务器日志)。
124
+ - 日志与调试
125
+ - 查看控制台日志以获取异常栈信息;可在开发环境开启更详细的日志级别。
126
+
127
+ 如需将以上路径或字段名与代码严格对齐,请提供 server.py 或配置片段以便精确修改 README。
@@ -0,0 +1,115 @@
1
+ # 项目说明
2
+ 简要说明项目用途:一个用于签到的轻量服务,提供前端签到页面和管理员页面,用于管理名单、保存签到记录(CSV)与重置数据。
3
+
4
+ # 运行方法
5
+ 1. 进入项目根目录(包含 `server.py`)。
6
+ 2. 推荐创建虚拟环境并安装依赖:
7
+ ```bash
8
+ python -m venv venv
9
+ source venv/Scripts/activate # Windows: venv\Scripts\activate
10
+ pip install -r requirements.txt
11
+ ```
12
+ 3. 启动服务(任选其一):
13
+ - 直接运行脚本:
14
+ ```bash
15
+ python server.py
16
+ ```
17
+ - 或使用 Flask(若适用):
18
+ ```bash
19
+ export FLASK_APP=server.py
20
+ flask run --host=0.0.0.0 --port=5000
21
+ ```
22
+ 4. 默认访问地址(按实际日志或配置调整):
23
+ - 签到页面(前端):http://localhost:5000/ 或 http://localhost:5000/checkin
24
+ - 管理页面: http://localhost:5000/admin 或 http://localhost:5000/manage
25
+
26
+ # 配置说明
27
+ - 工作目录:服务以启动时的当前工作目录为基准读取/写入文件。建议从项目根目录启动。
28
+ - 配置文件(示例路径,可根据代码调整):
29
+ - data/name.txt — 名单
30
+ - data/checkins.csv — 签到记录(CSV)
31
+ - data/room_info.txt — 房间或活动相关配置
32
+
33
+ # 文件位置与格式
34
+ - data/name.txt
35
+ - 路径示例:./data/name.txt
36
+ - 格式:UTF-8,逐行一个姓名或记录。示例:
37
+ ```
38
+ 张三
39
+ 李四
40
+ 王五
41
+ ```
42
+ - 如需额外字段(例如 id 或工号),可采用逗号分隔:`name,id`(需与代码解析方式一致)。
43
+
44
+ - data/checkins.csv
45
+ - 路径示例:./data/checkins.csv
46
+ - 推荐字段(CSV首行头部):timestamp,name,room,notes
47
+ - 示例内容:
48
+ ```
49
+ timestamp,name,room,notes
50
+ 2025-11-11T09:02:15,张三,RoomA,第一次签到
51
+ 2025-11-11T09:10:40,李四,RoomB,
52
+ ```
53
+
54
+ - data/room_info.txt
55
+ - 路径示例:./data/room_info.txt
56
+ - 常见格式(选择与代码匹配的格式):
57
+ - CSV 格式(room_id,display_name,capacity):
58
+ ```
59
+ RoomA,一号教室,30
60
+ RoomB,二号教室,25
61
+ ```
62
+ - 或 key=value 风格:
63
+ ```
64
+ RoomA.name=一号教室
65
+ RoomA.capacity=30
66
+ RoomB.name=二号教室
67
+ RoomB.capacity=25
68
+ ```
69
+
70
+ # 启动后常用操作
71
+ - 访问签到页面:在浏览器打开签到 URL,输入姓名或选择名单进行签到,界面操作通常会将记录追加到 `data/checkins.csv`。
72
+ - 访问管理页面:打开管理 URL,可查看/编辑名单、导出 CSV、或执行重置操作(具体按钮/功能与实现相关)。
73
+ - 保存操作:在管理页面点击“保存”时,名单应写入 `data/name.txt`,签到记录写入 `data/checkins.csv`。确认服务器进程有写权限。
74
+ - 重置操作:管理页面“重置”通常会清空或重命名当天的 CSV。若无界面,手动操作可:
75
+ ```bash
76
+ # 清空签到记录
77
+ > data/checkins.csv
78
+ # 或备份再清空
79
+ mv data/checkins.csv data/checkins-$(date +%F).csv
80
+ ```
81
+
82
+ # 示例 room_info.txt 片段
83
+ CSV 风格:
84
+ ```
85
+ RoomA,主会场,100
86
+ RoomB,分会场1,40
87
+ RoomC,分会场2,40
88
+ ```
89
+ key=value 风格:
90
+ ```
91
+ RoomA.name=主会场
92
+ RoomA.capacity=100
93
+ RoomB.name=分会场1
94
+ RoomB.capacity=40
95
+ ```
96
+
97
+ # 常见故障排查
98
+ - 页面无法访问
99
+ - 检查 server.py 是否在运行、控制台是否报错。
100
+ - 检查端口是否被占用或防火墙阻挡。
101
+ - 依赖缺失或导入错误
102
+ - 确认已安装 requirements.txt 中的包,使用正确的 Python 版本。
103
+ - 文件读写失败
104
+ - 检查 data 目录是否存在、服务器用户是否有读写权限。
105
+ - Windows 路径注意反斜杠和工作目录;以项目根目录启动服务。
106
+ - 中文/编码问题
107
+ - 确保所有文本文件使用 UTF-8 编码,避免 BOM 或其它编码导致解析错误。
108
+ - 名单/CSV 格式不生效
109
+ - 与代码中解析逻辑保持一致(是否期望逗号分隔、是否跳过空行、首行是否为头部)。
110
+ - 无法保存/重置
111
+ - 确认管理页面的按钮触发的 API 没有返回错误(检查浏览器开发者工具的网络请求和服务器日志)。
112
+ - 日志与调试
113
+ - 查看控制台日志以获取异常栈信息;可在开发环境开启更详细的日志级别。
114
+
115
+ 如需将以上路径或字段名与代码严格对齐,请提供 server.py 或配置片段以便精确修改 README。
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ciit-checkin"
7
+ version = "0.1.0"
8
+
9
+ authors = [
10
+ {name = "Yongshun Song", email = "ys_song@foxmail.com"}
11
+ ]
12
+ maintainers = [
13
+ {name = "Yongshun Song", email = "ys_song@foxmail.com"}
14
+ ]
15
+
16
+ description = "简易签到系统:提供基于 HTTP 的签到表单、管理页面与导出 CSV 功能,适合教室点名与记录。"
17
+
18
+ readme = "README.md"
19
+
20
+ license = "MIT"
21
+ license-files = ["LICENSE"]
22
+ keywords = ["ciit", "checkin", "attendance", "csv", "admin"]
23
+
24
+ [project.scripts]
25
+ checkin = "checkin:checkin_server"
26
+
27
+ [tool.setuptools.package-data]
28
+ checkin = ["*.html"]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ import argparse
2
+ from . import server
3
+ from typing import Optional
4
+
5
+ def checkin_server(host: str = "0.0.0.0", port: int = 8000, config: Optional[str] = None):
6
+ """Start the checkin HTTP server (blocking)."""
7
+ # ✅ 解析命令行参数(当通过 'checkin' 命令调用时)
8
+ parser = argparse.ArgumentParser(description="Start the CIIT check-in server.")
9
+ parser.add_argument("-c", "--config", type=str, help="Path to room_info.yaml (default: src/checkin/room_info.yaml)")
10
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host (default: 0.0.0.0)")
11
+ parser.add_argument("--port", type=int, default=8000, help="Server port (default: 8000)")
12
+
13
+ # 解析命令行参数(如果被作为脚本调用)
14
+ args = parser.parse_args()
15
+
16
+ # 覆盖默认参数
17
+ host = args.host
18
+ port = args.port
19
+ config = args.config or config # 允许传入 config 参数覆盖
20
+
21
+ print(f"Starting checkin server on {host}:{port} with config: {config}")
22
+ return server.run_server(host=host, port=port, room_info_path=config)
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>姓名记录管理</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ margin: 0;
11
+ padding: 20px;
12
+ background-color: #f5f5f5;
13
+ }
14
+ .container {
15
+ max-width: 800px;
16
+ margin: 0 auto;
17
+ background-color: white;
18
+ padding: 20px;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
21
+ }
22
+ h2 {
23
+ text-align: center;
24
+ color: #333;
25
+ margin-bottom: 20px;
26
+ }
27
+ table {
28
+ width: 100%;
29
+ border-collapse: collapse;
30
+ margin-top: 10px;
31
+ }
32
+ td {
33
+ padding: 8px;
34
+ text-align: center;
35
+ border: 1px solid #ddd;
36
+ }
37
+ .btn {
38
+ display: block;
39
+ width: 200px;
40
+ margin: 10px auto;
41
+ padding: 10px;
42
+ background-color: #4CAF50;
43
+ color: white;
44
+ border: none;
45
+ border-radius: 4px;
46
+ cursor: pointer;
47
+ font-size: 16px;
48
+ text-decoration: none;
49
+ text-align: center;
50
+ }
51
+ .btn:hover {
52
+ background-color: #45a049;
53
+ }
54
+ .btn-reset {
55
+ background-color: #f44336;
56
+ }
57
+ .btn-reset:hover {
58
+ background-color: #da190b;
59
+ }
60
+ .label {
61
+ text-align: center;
62
+ margin-top: 15px;
63
+ font-weight: bold;
64
+ color: #666;
65
+ font-size: 18px;
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div class="container">
71
+ <h2>签到情况</h2>
72
+ {{table_html}}
73
+ <div class="label">讲台</div>
74
+ <form method="POST" action="/{{room_number}}/save">
75
+ <input type="hidden" name="action" value="save">
76
+ <button type="submit" class="btn">保存为 CSV</button>
77
+ </form>
78
+ <form method="POST" action="/{{room_number}}/reset" onsubmit="return confirm('确定要重置所有数据吗?此操作不可恢复!');">
79
+ <input type="hidden" name="action" value="reset">
80
+ <button type="submit" class="btn btn-reset">重置</button>
81
+ </form>
82
+ </div>
83
+ </body>
84
+ </html>
@@ -0,0 +1,173 @@
1
+ import os
2
+ import html
3
+ import csv
4
+ import yaml
5
+ from datetime import datetime
6
+
7
+ def load_classroom_config(path=None, classroom_id=None):
8
+ """
9
+ Load classroom config from YAML file.
10
+ If classroom_id is provided, return its config; else return first one.
11
+ Returns: (room_number, row, column) or (None, None, None) if not found.
12
+ """
13
+ p = path or os.path.join(os.path.dirname(__file__), 'room_info.yaml')
14
+ try:
15
+ with open(p, 'r', encoding='utf-8') as f:
16
+ data = yaml.safe_load(f)
17
+ classrooms = data.get('classrooms', [])
18
+ if classroom_id:
19
+ for room in classrooms:
20
+ if room.get('id') == classroom_id:
21
+ return (
22
+ room.get('room_number', classroom_id),
23
+ room.get('row', 4),
24
+ room.get('column', 12)
25
+ )
26
+ return None, None, None
27
+ else:
28
+ # Return first classroom if no ID specified
29
+ if classrooms:
30
+ room = classrooms[0]
31
+ return (
32
+ room.get('room_number', room.get('id', '1058')),
33
+ room.get('row', 4),
34
+ room.get('column', 12)
35
+ )
36
+ return '1058', 4, 12
37
+ except Exception:
38
+ return '1058', 4, 12
39
+
40
+ def build_table_html_from_namefile(name_file="name.txt", classroom_id=None, room_info_path=None):
41
+ """
42
+ Build HTML table for a specific classroom.
43
+ """
44
+ room_number, row, column = load_classroom_config(room_info_path, classroom_id)
45
+ if room_number is None:
46
+ return "<h2>教室配置未找到</h2>"
47
+
48
+ # Ensure row and column are integers with defaults
49
+ row = row or 4
50
+ column = column or 12
51
+
52
+ # ✅ 如果传入了 classroom_id,自动使用 name-{id}.txt
53
+ if classroom_id and not name_file.startswith("name-"):
54
+ name_file = f"name-{classroom_id}.txt"
55
+
56
+ # ✅ 所有文件路径统一加 data/ 前缀
57
+ data_dir = "data"
58
+ os.makedirs(data_dir, exist_ok=True)
59
+ name_file = os.path.join(data_dir, name_file)
60
+
61
+ entries = {}
62
+ if os.path.exists(name_file):
63
+ with open(name_file, "r", encoding="utf-8") as f:
64
+ for line in f:
65
+ line = line.strip()
66
+ if not line:
67
+ continue
68
+ parts = line.split(",", 1)
69
+ if len(parts) == 2:
70
+ prefix, name = parts[0], parts[1]
71
+ entries[prefix] = name
72
+
73
+ # Build table (inverted row order for display)
74
+ table = [["" for _ in range(column)] for _ in range(row)]
75
+ for prefix_str, name in entries.items():
76
+ try:
77
+ prefix = int(prefix_str)
78
+ r_idx = (prefix - 1) // column
79
+ c_idx = (prefix - 1) % column
80
+ if 0 <= r_idx < row and 0 <= c_idx < column:
81
+ table[row - 1 - r_idx][c_idx] = name
82
+ except ValueError:
83
+ continue
84
+
85
+ table_html = "<table border='1' style='width:100%; border-collapse: collapse;'>\n"
86
+ for tr in table:
87
+ table_html += " <tr>\n"
88
+ for cell in tr:
89
+ escaped_cell = html.escape(cell) if cell else "&nbsp;"
90
+ table_html += f" <td style='padding:8px; text-align:center;'>{escaped_cell}</td>\n"
91
+ table_html += " </tr>\n"
92
+ table_html += "</table>"
93
+ return table_html
94
+
95
+ def save_csv_to_dir(name_file="name.txt", out_dir=None, classroom_id=None, room_info_path=None):
96
+ """
97
+ Save CSV for a specific classroom.
98
+ """
99
+ room_number, row, column = load_classroom_config(room_info_path, classroom_id)
100
+ if room_number is None:
101
+ return "ERROR: Classroom config not found"
102
+
103
+ # Ensure row and column are integers with defaults
104
+ row = row or 4
105
+ column = column or 12
106
+
107
+ # ✅ 如果传入了 classroom_id,自动使用 name-{id}.txt
108
+ if classroom_id and not name_file.startswith("name-"):
109
+ name_file = f"name-{classroom_id}.txt"
110
+
111
+ # ✅ 所有文件路径统一加 data/ 前缀
112
+ data_dir = "data"
113
+ os.makedirs(data_dir, exist_ok=True)
114
+ name_file = os.path.join(data_dir, name_file)
115
+
116
+ entries = {}
117
+ if os.path.exists(name_file):
118
+ with open(name_file, "r", encoding="utf-8") as f:
119
+ for line in f:
120
+ line = line.strip()
121
+ if not line:
122
+ continue
123
+ parts = line.split(",", 1)
124
+ if len(parts) == 2:
125
+ prefix, name = parts[0], parts[1]
126
+ entries[prefix] = name
127
+
128
+ table = [["" for _ in range(column)] for _ in range(row)]
129
+ for prefix_str, name in entries.items():
130
+ try:
131
+ prefix = int(prefix_str)
132
+ r_idx = (prefix - 1) // column
133
+ c_idx = (prefix - 1) % column
134
+ if 0 <= r_idx < row and 0 <= c_idx < column:
135
+ table[r_idx][c_idx] = name
136
+ except ValueError:
137
+ continue
138
+
139
+ timestamp = datetime.now().strftime("%Y%m%d%H%M")
140
+ csv_filename = f"checkin-{room_number}-{timestamp}.csv"
141
+ out_dir = out_dir or data_dir # ✅ 默认输出到 data/ 目录
142
+ csv_path = os.path.join(out_dir, csv_filename)
143
+
144
+ with open(csv_path, "w", encoding="utf-8", newline='') as csvfile:
145
+ writer = csv.writer(csvfile)
146
+ writer.writerows(table)
147
+
148
+ return csv_filename
149
+
150
+ def reset_namefile(name_file="name.txt", classroom_id=None):
151
+ # ✅ 如果传入了 classroom_id,自动使用 data/name-{id}.txt
152
+ if classroom_id:
153
+ name_file = f"name-{classroom_id}.txt"
154
+
155
+ # ✅ 所有文件路径统一加 data/ 前缀
156
+ data_dir = "data"
157
+ os.makedirs(data_dir, exist_ok=True)
158
+ name_file = os.path.join(data_dir, name_file)
159
+
160
+ if os.path.exists(name_file):
161
+ try:
162
+ print(f"Deleting: {name_file}") # ✅ 调试用
163
+ os.remove(name_file)
164
+ return True
165
+ except Exception as e:
166
+ print(f"Failed to delete {name_file}: {e}")
167
+ return False
168
+ return True
169
+
170
+ if __name__ == "__main__":
171
+ # Test: save CSV using default room_info.yaml and first classroom
172
+ fn = save_csv_to_dir(classroom_id=None, room_info_path=None)
173
+ print("Saved:", fn)
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>请输入姓名</title>
7
+ <style>
8
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
9
+ .container { max-width: 600px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
10
+ h2 { text-align: center; color: #333; margin-bottom: 20px; }
11
+ input[type="text"] { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; box-sizing: border-box; }
12
+ input[type="submit"] { width: 100%; padding: 12px; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 16px; margin-top: 10px; cursor: pointer; }
13
+ input[type="submit"]:hover { background-color: #0056b3; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div class="container">
18
+ <h2>请输入你的姓名</h2>
19
+ {{message}}
20
+ <form method="POST">
21
+ <input type="text" name="name" placeholder="输入姓名" required>
22
+ <input type="submit" value="提交">
23
+ </form>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,192 @@
1
+ import importlib.resources
2
+ from http.server import BaseHTTPRequestHandler, HTTPServer
3
+ import json
4
+ import os
5
+ import re
6
+ import urllib.parse
7
+ from . import admin
8
+
9
+ class CheckinHandler(BaseHTTPRequestHandler):
10
+ # Class-level variable to hold the config file path passed from run_server()
11
+ room_info_path = None
12
+
13
+ def _render_form(self, message=''):
14
+ try:
15
+ # ✅ 使用 importlib.resources 从包内读取文件
16
+ html_content = importlib.resources.read_text('checkin', 'checkin.html', encoding='utf-8')
17
+ except FileNotFoundError:
18
+ html_content = "<html><body><h2>页面丢失</h2></body></html>"
19
+ except Exception:
20
+ html_content = "<html><body><h2>模板加载失败</h2></body></html>"
21
+
22
+ msg_html = f'<p style="color:green">{message}</p>' if message else ''
23
+ html_content = html_content.replace('{{message}}', msg_html)
24
+ return html_content.encode('utf-8')
25
+
26
+ def _render_admin(self, table_html='', room_number=''):
27
+ try:
28
+ # ✅ 同样方式读取 admin.html
29
+ tpl_content = importlib.resources.read_text('checkin', 'admin.html', encoding='utf-8')
30
+ except FileNotFoundError:
31
+ tpl_content = "<html><body><h2>admin 模板丢失</h2></body></html>"
32
+ except Exception:
33
+ tpl_content = "<html><body><h2>模板加载失败</h2></body></html>"
34
+
35
+ rendered = tpl_content.replace('{{table_html}}', table_html).replace('{{room_number}}', room_number)
36
+ return rendered.encode('utf-8')
37
+
38
+ def do_GET(self):
39
+ path = urllib.parse.urlparse(self.path).path
40
+
41
+ # Extract classroom ID from path: e.g., /1056/admin.html → id="1056"
42
+ match = re.match(r'^/(\d{3,4})/(admin\.html|checkin-\d{2}\.html)$', path)
43
+ if match:
44
+ classroom_id = match.group(1)
45
+ page_type = match.group(2)
46
+
47
+ # Load config for this classroom
48
+ room_number, _, _ = admin.load_classroom_config(self.room_info_path, classroom_id)
49
+ if room_number is None:
50
+ self.send_response(404)
51
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
52
+ self.end_headers()
53
+ self.wfile.write("<h2>教室配置未找到</h2>".encode('utf-8'))
54
+ return
55
+
56
+ if page_type == "admin.html":
57
+ table_html = admin.build_table_html_from_namefile(classroom_id=classroom_id, room_info_path=self.room_info_path)
58
+ self.send_response(200)
59
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
60
+ self.end_headers()
61
+ self.wfile.write(self._render_admin(table_html=table_html, room_number=room_number or "未知教室"))
62
+ return
63
+
64
+ elif page_type.startswith("checkin-"):
65
+ # For checkin pages, just show the form
66
+ self.send_response(200)
67
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
68
+ self.end_headers()
69
+ self.wfile.write(self._render_form())
70
+ return
71
+
72
+ # ❌ 删除:不再支持 /admin.html 默认页(必须通过 /{id}/admin.html 访问)
73
+ # 所有访问必须带 classroom_id
74
+ self.send_response(404)
75
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
76
+ self.end_headers()
77
+ self.wfile.write("<h2>无效路径,请通过 /{教室ID}/admin.html 访问</h2>".encode('utf-8'))
78
+
79
+ def do_POST(self):
80
+ path = urllib.parse.urlparse(self.path).path
81
+
82
+ # ✅ 仅支持教室专属路径:/1056/save 和 /1056/reset
83
+ save_match = re.match(r'^/(\d{3,4})/save$', path)
84
+ reset_match = re.match(r'^/(\d{3,4})/reset$', path)
85
+
86
+ if save_match:
87
+ classroom_id = save_match.group(1)
88
+ csv_filename = admin.save_csv_to_dir(classroom_id=classroom_id, room_info_path=self.room_info_path)
89
+ redirect_url = f"/{classroom_id}/admin.html"
90
+ html_resp = f"""<!DOCTYPE html>
91
+ <html><head><meta charset="utf-8"><title>保存成功</title>
92
+ <meta http-equiv="refresh" content="2;url={redirect_url}"></head>
93
+ <body><p>签到记录已保存为 <code>{csv_filename}</code>,2秒后返回...</p></body></html>"""
94
+ self.send_response(200)
95
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
96
+ self.end_headers()
97
+ self.wfile.write(html_resp.encode('utf-8'))
98
+ return
99
+
100
+ if reset_match:
101
+ classroom_id = reset_match.group(1)
102
+ admin.reset_namefile(classroom_id=classroom_id)
103
+ redirect_url = f"/{classroom_id}/admin.html"
104
+ html_resp = f"""<!DOCTYPE html>
105
+ <html><head><meta charset="utf-8"><title>重置成功</title>
106
+ <meta http-equiv="refresh" content="1;url={redirect_url}"></head>
107
+ <body><p>数据已重置,1秒后返回...</p></body></html>"""
108
+ self.send_response(200)
109
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
110
+ self.end_headers()
111
+ self.wfile.write(html_resp.encode('utf-8'))
112
+ return
113
+
114
+ # ❌ 删除:不再支持 /save 或 /reset 全局路径
115
+ # ❌ 删除:不再支持从 Referer 推断教室 ID
116
+
117
+ # Handle check-in POST (name submission) —— 仅允许 /{id}/checkin-XX.html
118
+ content_length = int(self.headers.get('Content-Length', 0))
119
+ body = self.rfile.read(content_length) if content_length > 0 else b''
120
+ content_type = self.headers.get('Content-Type', '')
121
+
122
+ name = None
123
+ if 'application/json' in content_type:
124
+ try:
125
+ data = json.loads(body.decode('utf-8'))
126
+ name = data.get('name') or data.get('user_id')
127
+ except json.JSONDecodeError:
128
+ name = None
129
+ else:
130
+ try:
131
+ parsed = urllib.parse.parse_qs(body.decode('utf-8'))
132
+ name = parsed.get('name', [None])[0]
133
+ except Exception:
134
+ name = None
135
+
136
+ if name:
137
+ # ✅ 1. 从路径提取 classroom_id(决定写入哪个文件)
138
+ class_match = re.match(r'^/(\d{3,4})/checkin-\d{2}\.html$', path)
139
+ classroom_id = class_match.group(1) if class_match else None
140
+
141
+ # ✅ 2. 从路径提取签到序号(写入文件的第一列)
142
+ seq_match = re.match(r'^.*/checkin-(\d{2})\.html$', path)
143
+ seq = seq_match.group(1) if seq_match else '00'
144
+
145
+ if not classroom_id:
146
+ # ❌ 不再允许 fallback 到默认教室
147
+ # 所有 checkin 请求必须来自 /{id}/checkin-XX.html
148
+ self.send_response(400)
149
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
150
+ self.end_headers()
151
+ self.wfile.write("<h2>签到请求路径无效,请通过 /{教室ID}/checkin-XX.html 提交</h2>".encode('utf-8'))
152
+ return
153
+
154
+ # ✅ 3. 构造文件名:data/name-1056.txt
155
+ data_dir = "data"
156
+ os.makedirs(data_dir, exist_ok=True)
157
+ name_file = os.path.join(data_dir, f"name-{classroom_id}.txt")
158
+
159
+ # ✅ 4. 写入:签到序号,姓名 → 如 "01,张三"
160
+ with open(name_file, 'a', encoding='utf-8') as f:
161
+ f.write(f"{seq},{name}\n")
162
+
163
+ message = f"已保存: {name}"
164
+ status = 200
165
+ else:
166
+ message = "Missing name or invalid data."
167
+ status = 400
168
+
169
+ self.send_response(status)
170
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
171
+ self.end_headers()
172
+ self.wfile.write(self._render_form(message=message))
173
+
174
+
175
+ def run_server(host: str = "0.0.0.0", port: int = 8000, room_info_path=None):
176
+ """Start the checkin HTTP server (blocking)."""
177
+ # ✅ Set the room_info_path on the handler class
178
+ CheckinHandler.room_info_path = room_info_path
179
+
180
+ addr = (host, int(port))
181
+ server = HTTPServer(addr, CheckinHandler)
182
+ print(f"Serving on http://{addr[0]}:{addr[1]}/")
183
+ try:
184
+ server.serve_forever()
185
+ except KeyboardInterrupt:
186
+ print("Shutting down server...")
187
+ server.server_close()
188
+ return server
189
+
190
+
191
+ if __name__ == "__main__":
192
+ run_server() # keep CLI behavior
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: ciit-checkin
3
+ Version: 0.1.0
4
+ Summary: 简易签到系统:提供基于 HTTP 的签到表单、管理页面与导出 CSV 功能,适合教室点名与记录。
5
+ Author-email: Yongshun Song <ys_song@foxmail.com>
6
+ Maintainer-email: Yongshun Song <ys_song@foxmail.com>
7
+ License-Expression: MIT
8
+ Keywords: ciit,checkin,attendance,csv,admin
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # 项目说明
14
+ 简要说明项目用途:一个用于签到的轻量服务,提供前端签到页面和管理员页面,用于管理名单、保存签到记录(CSV)与重置数据。
15
+
16
+ # 运行方法
17
+ 1. 进入项目根目录(包含 `server.py`)。
18
+ 2. 推荐创建虚拟环境并安装依赖:
19
+ ```bash
20
+ python -m venv venv
21
+ source venv/Scripts/activate # Windows: venv\Scripts\activate
22
+ pip install -r requirements.txt
23
+ ```
24
+ 3. 启动服务(任选其一):
25
+ - 直接运行脚本:
26
+ ```bash
27
+ python server.py
28
+ ```
29
+ - 或使用 Flask(若适用):
30
+ ```bash
31
+ export FLASK_APP=server.py
32
+ flask run --host=0.0.0.0 --port=5000
33
+ ```
34
+ 4. 默认访问地址(按实际日志或配置调整):
35
+ - 签到页面(前端):http://localhost:5000/ 或 http://localhost:5000/checkin
36
+ - 管理页面: http://localhost:5000/admin 或 http://localhost:5000/manage
37
+
38
+ # 配置说明
39
+ - 工作目录:服务以启动时的当前工作目录为基准读取/写入文件。建议从项目根目录启动。
40
+ - 配置文件(示例路径,可根据代码调整):
41
+ - data/name.txt — 名单
42
+ - data/checkins.csv — 签到记录(CSV)
43
+ - data/room_info.txt — 房间或活动相关配置
44
+
45
+ # 文件位置与格式
46
+ - data/name.txt
47
+ - 路径示例:./data/name.txt
48
+ - 格式:UTF-8,逐行一个姓名或记录。示例:
49
+ ```
50
+ 张三
51
+ 李四
52
+ 王五
53
+ ```
54
+ - 如需额外字段(例如 id 或工号),可采用逗号分隔:`name,id`(需与代码解析方式一致)。
55
+
56
+ - data/checkins.csv
57
+ - 路径示例:./data/checkins.csv
58
+ - 推荐字段(CSV首行头部):timestamp,name,room,notes
59
+ - 示例内容:
60
+ ```
61
+ timestamp,name,room,notes
62
+ 2025-11-11T09:02:15,张三,RoomA,第一次签到
63
+ 2025-11-11T09:10:40,李四,RoomB,
64
+ ```
65
+
66
+ - data/room_info.txt
67
+ - 路径示例:./data/room_info.txt
68
+ - 常见格式(选择与代码匹配的格式):
69
+ - CSV 格式(room_id,display_name,capacity):
70
+ ```
71
+ RoomA,一号教室,30
72
+ RoomB,二号教室,25
73
+ ```
74
+ - 或 key=value 风格:
75
+ ```
76
+ RoomA.name=一号教室
77
+ RoomA.capacity=30
78
+ RoomB.name=二号教室
79
+ RoomB.capacity=25
80
+ ```
81
+
82
+ # 启动后常用操作
83
+ - 访问签到页面:在浏览器打开签到 URL,输入姓名或选择名单进行签到,界面操作通常会将记录追加到 `data/checkins.csv`。
84
+ - 访问管理页面:打开管理 URL,可查看/编辑名单、导出 CSV、或执行重置操作(具体按钮/功能与实现相关)。
85
+ - 保存操作:在管理页面点击“保存”时,名单应写入 `data/name.txt`,签到记录写入 `data/checkins.csv`。确认服务器进程有写权限。
86
+ - 重置操作:管理页面“重置”通常会清空或重命名当天的 CSV。若无界面,手动操作可:
87
+ ```bash
88
+ # 清空签到记录
89
+ > data/checkins.csv
90
+ # 或备份再清空
91
+ mv data/checkins.csv data/checkins-$(date +%F).csv
92
+ ```
93
+
94
+ # 示例 room_info.txt 片段
95
+ CSV 风格:
96
+ ```
97
+ RoomA,主会场,100
98
+ RoomB,分会场1,40
99
+ RoomC,分会场2,40
100
+ ```
101
+ key=value 风格:
102
+ ```
103
+ RoomA.name=主会场
104
+ RoomA.capacity=100
105
+ RoomB.name=分会场1
106
+ RoomB.capacity=40
107
+ ```
108
+
109
+ # 常见故障排查
110
+ - 页面无法访问
111
+ - 检查 server.py 是否在运行、控制台是否报错。
112
+ - 检查端口是否被占用或防火墙阻挡。
113
+ - 依赖缺失或导入错误
114
+ - 确认已安装 requirements.txt 中的包,使用正确的 Python 版本。
115
+ - 文件读写失败
116
+ - 检查 data 目录是否存在、服务器用户是否有读写权限。
117
+ - Windows 路径注意反斜杠和工作目录;以项目根目录启动服务。
118
+ - 中文/编码问题
119
+ - 确保所有文本文件使用 UTF-8 编码,避免 BOM 或其它编码导致解析错误。
120
+ - 名单/CSV 格式不生效
121
+ - 与代码中解析逻辑保持一致(是否期望逗号分隔、是否跳过空行、首行是否为头部)。
122
+ - 无法保存/重置
123
+ - 确认管理页面的按钮触发的 API 没有返回错误(检查浏览器开发者工具的网络请求和服务器日志)。
124
+ - 日志与调试
125
+ - 查看控制台日志以获取异常栈信息;可在开发环境开启更详细的日志级别。
126
+
127
+ 如需将以上路径或字段名与代码严格对齐,请提供 server.py 或配置片段以便精确修改 README。
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/checkin/__init__.py
5
+ src/checkin/admin.html
6
+ src/checkin/admin.py
7
+ src/checkin/checkin.html
8
+ src/checkin/server.py
9
+ src/ciit_checkin.egg-info/PKG-INFO
10
+ src/ciit_checkin.egg-info/SOURCES.txt
11
+ src/ciit_checkin.egg-info/dependency_links.txt
12
+ src/ciit_checkin.egg-info/entry_points.txt
13
+ src/ciit_checkin.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ checkin = checkin:checkin_server