flashexam 2.0.1__py3-none-any.whl
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.
- flashexam/__init__.py +198 -0
- flashexam/admin.py +198 -0
- flashexam/answer_preservation_system.py +770 -0
- flashexam/api.py +392 -0
- flashexam/auth.py +337 -0
- flashexam/cli.py +186 -0
- flashexam/code_executor.py +209 -0
- flashexam/database.py +2591 -0
- flashexam/encoding_detector.py +294 -0
- flashexam/english_questions.py +574 -0
- flashexam/exam_reliability_system.py +728 -0
- flashexam/grading_engine.py +769 -0
- flashexam/improved_grading_system.py +690 -0
- flashexam/intelligent_grading_enhanced.py +127 -0
- flashexam/intelligent_grading_ollama.py +718 -0
- flashexam/intelligent_grading_qwen.py +1218 -0
- flashexam/keyword_focused_grader.py +84 -0
- flashexam/main.py +34 -0
- flashexam/models.py +82 -0
- flashexam/new_exam_creation_api.py +347 -0
- flashexam/new_question_system.py +386 -0
- flashexam/quick_test.py +160 -0
- flashexam/reading_passage_database.py +377 -0
- flashexam/reading_passage_models.py +98 -0
- flashexam/reading_passages.py +343 -0
- flashexam/redis_store.py +245 -0
- flashexam/reset_system.py +351 -0
- flashexam/run.py +74 -0
- flashexam/setup_database.py +370 -0
- flashexam/smart_import_manager.py +463 -0
- flashexam/static/css/app.css +240 -0
- flashexam/static/css/libs/bootstrap/bootstrap-5.1.3.min.css +7 -0
- flashexam/static/css/libs/bootstrap/bootstrap-5.3.0.min.css +6 -0
- flashexam/static/css/libs/bootstrap/bootstrap-5.3.2.min.css +6 -0
- flashexam/static/css/libs/bootstrap/bootstrap.min.css +796 -0
- flashexam/static/css/libs/bootstrap-icons/bootstrap-icons-1.10.0.css +2018 -0
- flashexam/static/css/libs/bootstrap-icons/bootstrap-icons-1.11.0.css +2078 -0
- flashexam/static/css/libs/bootstrap-icons/bootstrap-icons-1.7.2.css +1556 -0
- flashexam/static/css/libs/bootstrap-icons/bootstrap-icons-1.8.1.css +1704 -0
- flashexam/static/css/libs/bootstrap-icons/bootstrap-icons.min.css +77 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.10.0/bootstrap-icons.woff +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.10.0/bootstrap-icons.woff2 +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.11.0/bootstrap-icons.woff +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.11.0/bootstrap-icons.woff2 +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.7.2/bootstrap-icons.woff +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.7.2/bootstrap-icons.woff2 +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.8.1/bootstrap-icons.woff +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/1.8.1/bootstrap-icons.woff2 +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- flashexam/static/css/libs/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- flashexam/static/css/libs/prism/prism-tomorrow.min.css +1 -0
- flashexam/static/css/libs/prism/prism.min.css +1 -0
- flashexam/static/css/libs/sweetalert2.min.css +1 -0
- flashexam/static/css/student_info_display.css +207 -0
- flashexam/static/js/app.js +161 -0
- flashexam/static/js/exam_monitoring.js +198 -0
- flashexam/static/js/exam_optimized.js +576 -0
- flashexam/static/js/libs/bootstrap/bootstrap-5.1.3.bundle.min.js +7 -0
- flashexam/static/js/libs/bootstrap/bootstrap-5.3.0.bundle.min.js +7 -0
- flashexam/static/js/libs/bootstrap/bootstrap-5.3.2.bundle.min.js +7 -0
- flashexam/static/js/libs/bootstrap/bootstrap.bundle.min.js +391 -0
- flashexam/static/js/libs/bootstrap-shim.js +59 -0
- flashexam/static/js/libs/chart/chart.js +20 -0
- flashexam/static/js/libs/chart/chart.min.js +20 -0
- flashexam/static/js/libs/init.js +156 -0
- flashexam/static/js/libs/jquery/jquery-3.7.1.min.js +2 -0
- flashexam/static/js/libs/jquery/jquery.min.js +2 -0
- flashexam/static/js/libs/prism/components/prism-bash.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-c.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-clike.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-core.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-cpp.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-csharp.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-css.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-go.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-java.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-javascript.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-json.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-markdown.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-markup.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-php.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-python.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-ruby.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-rust.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-sql.min.js +1 -0
- flashexam/static/js/libs/prism/components/prism-typescript.min.js +1 -0
- flashexam/static/js/libs/prism/plugins/autoloader/prism-autoloader.min.js +1 -0
- flashexam/static/js/libs/prism/prism-core.min.js +1 -0
- flashexam/static/js/libs/sweetalert2/sweetalert2.min.js +5 -0
- flashexam/static/js/offline-detection.js +68 -0
- flashexam/static/js/prism-init.js +53 -0
- flashexam/static/js/prism-local-config.js +16 -0
- flashexam/storage/.gitkeep +0 -0
- flashexam/stress_test_submission.py +163 -0
- flashexam/student.py +1670 -0
- flashexam/teacher.py +8947 -0
- flashexam/teacher_fix.py +1 -0
- flashexam/templates/admin/index.html +180 -0
- flashexam/templates/admin/settings.html +214 -0
- flashexam/templates/admin/teachers.html +354 -0
- flashexam/templates/auth/login.html +267 -0
- flashexam/templates/auth/login_conflict.html +83 -0
- flashexam/templates/auth/setup_password.html +339 -0
- flashexam/templates/base.html +217 -0
- flashexam/templates/errors/404.html +25 -0
- flashexam/templates/errors/500.html +25 -0
- flashexam/templates/index.html +236 -0
- flashexam/templates/student/dashboard.html +315 -0
- flashexam/templates/student/exam_continue_choice.html +131 -0
- flashexam/templates/student/exam_result.html +412 -0
- flashexam/templates/student/exam_result_detail.html +244 -0
- flashexam/templates/student/exam_result_detail_secure.html +427 -0
- flashexam/templates/student/exams.html +211 -0
- flashexam/templates/student/profile.html +224 -0
- flashexam/templates/student/results.html +293 -0
- flashexam/templates/student/take_exam.html +964 -0
- flashexam/templates/student/take_exam_new.html +2243 -0
- flashexam/templates/student/take_exam_tpo.html +1025 -0
- flashexam/templates/teacher/dashboard.html +375 -0
- flashexam/templates/teacher/english_questions.html +1227 -0
- flashexam/templates/teacher/exam_edit.html +1009 -0
- flashexam/templates/teacher/exam_monitoring_config.html +288 -0
- flashexam/templates/teacher/exam_submissions.html +1334 -0
- flashexam/templates/teacher/exams.html +2534 -0
- flashexam/templates/teacher/grading.html +372 -0
- flashexam/templates/teacher/grading_detail.html +1967 -0
- flashexam/templates/teacher/new_exam_interface.html +2105 -0
- flashexam/templates/teacher/questions.html +1609 -0
- flashexam/templates/teacher/reading_passages.html +592 -0
- flashexam/templates/teacher/results.html +895 -0
- flashexam/templates/teacher/student_exam_details.html +295 -0
- flashexam/templates/teacher/students.html +1360 -0
- flashexam/templates/teacher/test_export.html +127 -0
- flashexam/templates/teacher/test_update_score.html +179 -0
- flashexam/templates/teacher/unified_grading.html +276 -0
- flashexam/tools.py +1 -0
- flashexam/user.py +172 -0
- flashexam/utils.py +1397 -0
- flashexam/verify_submission_queue.py +86 -0
- flashexam-2.0.1.dist-info/METADATA +402 -0
- flashexam-2.0.1.dist-info/RECORD +145 -0
- flashexam-2.0.1.dist-info/WHEEL +5 -0
- flashexam-2.0.1.dist-info/entry_points.txt +2 -0
- flashexam-2.0.1.dist-info/licenses/LICENSE +20 -0
- flashexam-2.0.1.dist-info/top_level.txt +1 -0
flashexam/__init__.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""FlashExam - Intelligent Online Examination System with AI Grading."""
|
|
2
|
+
|
|
3
|
+
__version__ = "2.0.1"
|
|
4
|
+
|
|
5
|
+
from flask import Flask
|
|
6
|
+
from flask_login import LoginManager
|
|
7
|
+
from flask_session import Session
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import timedelta
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
sys.path.insert(0, current_dir)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import psutil
|
|
19
|
+
PSUTIL_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
PSUTIL_AVAILABLE = False
|
|
22
|
+
print("[WARNING] psutil未安装,将使用模拟数据")
|
|
23
|
+
|
|
24
|
+
_app_start_time = time.time()
|
|
25
|
+
_request_count = 0
|
|
26
|
+
_active_connections = 0
|
|
27
|
+
|
|
28
|
+
def load_development_config(app, static_folder):
|
|
29
|
+
"""加载开发环境配置"""
|
|
30
|
+
app.config.update({
|
|
31
|
+
'SECRET_KEY': os.environ.get('SECRET_KEY') or 'flashexam-dev-key-2024',
|
|
32
|
+
'UPLOAD_FOLDER': os.path.join(static_folder, 'uploads'),
|
|
33
|
+
'MAX_CONTENT_LENGTH': 16 * 1024 * 1024,
|
|
34
|
+
'TEMPLATES_AUTO_RELOAD': True,
|
|
35
|
+
'JSON_AS_ASCII': False,
|
|
36
|
+
'DEBUG': True
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
def load_production_config(app, static_folder):
|
|
40
|
+
"""加载生产环境配置"""
|
|
41
|
+
is_https = os.environ.get('HTTPS', '').lower() in ('1', 'true', 'on')
|
|
42
|
+
|
|
43
|
+
app.config.update({
|
|
44
|
+
'SECRET_KEY': os.environ.get('SECRET_KEY') or 'flashexam-prod-key-2024',
|
|
45
|
+
'UPLOAD_FOLDER': os.path.join(static_folder, 'uploads'),
|
|
46
|
+
'MAX_CONTENT_LENGTH': 32 * 1024 * 1024,
|
|
47
|
+
'TEMPLATES_AUTO_RELOAD': False,
|
|
48
|
+
'JSON_AS_ASCII': False,
|
|
49
|
+
'SEND_FILE_MAX_AGE_DEFAULT': 43200,
|
|
50
|
+
'SESSION_COOKIE_SECURE': is_https,
|
|
51
|
+
'SESSION_COOKIE_HTTPONLY': True,
|
|
52
|
+
'SESSION_COOKIE_SAMESITE': 'Lax',
|
|
53
|
+
'DEBUG': False
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
def load_high_performance_config(app, static_folder):
|
|
57
|
+
"""加载高性能环境配置"""
|
|
58
|
+
is_https = os.environ.get('HTTPS', '').lower() in ('1', 'true', 'on')
|
|
59
|
+
|
|
60
|
+
app.config.update({
|
|
61
|
+
'SECRET_KEY': os.environ.get('SECRET_KEY') or 'flashexam-hp-key-2024',
|
|
62
|
+
'UPLOAD_FOLDER': os.path.join(static_folder, 'uploads'),
|
|
63
|
+
'MAX_CONTENT_LENGTH': 64 * 1024 * 1024,
|
|
64
|
+
'TEMPLATES_AUTO_RELOAD': False,
|
|
65
|
+
'JSON_AS_ASCII': False,
|
|
66
|
+
'SEND_FILE_MAX_AGE_DEFAULT': 86400,
|
|
67
|
+
'SESSION_COOKIE_SECURE': is_https,
|
|
68
|
+
'SESSION_COOKIE_HTTPONLY': True,
|
|
69
|
+
'SESSION_COOKIE_SAMESITE': 'Lax',
|
|
70
|
+
'PERMANENT_SESSION_LIFETIME': timedelta(hours=8),
|
|
71
|
+
'DEBUG': False
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
def create_app(config_name='development'):
|
|
75
|
+
"""
|
|
76
|
+
Flask应用工厂函数
|
|
77
|
+
支持多种配置模式:development, production, high-performance
|
|
78
|
+
"""
|
|
79
|
+
template_folder = os.path.join(os.path.dirname(__file__), 'templates')
|
|
80
|
+
static_folder = os.path.join(os.path.dirname(__file__), 'static')
|
|
81
|
+
|
|
82
|
+
app = Flask(__name__,
|
|
83
|
+
template_folder=template_folder,
|
|
84
|
+
static_folder=static_folder)
|
|
85
|
+
|
|
86
|
+
if config_name == 'production':
|
|
87
|
+
load_production_config(app, static_folder)
|
|
88
|
+
elif config_name == 'high-performance':
|
|
89
|
+
load_high_performance_config(app, static_folder)
|
|
90
|
+
else:
|
|
91
|
+
load_development_config(app, static_folder)
|
|
92
|
+
|
|
93
|
+
app.config['SESSION_TYPE'] = 'filesystem'
|
|
94
|
+
app.config['SESSION_PERMANENT'] = True
|
|
95
|
+
if 'PERMANENT_SESSION_LIFETIME' not in app.config:
|
|
96
|
+
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)
|
|
97
|
+
app.config['SESSION_USE_SIGNER'] = True
|
|
98
|
+
app.config['SESSION_KEY_PREFIX'] = 'flashexam:'
|
|
99
|
+
app.config['SESSION_FILE_DIR'] = os.path.join(os.path.dirname(__file__), 'storage', 'sessions')
|
|
100
|
+
app.config['DATABASE_PATH'] = os.path.join(os.path.dirname(__file__), 'storage', 'exam_database.db')
|
|
101
|
+
|
|
102
|
+
Session(app)
|
|
103
|
+
|
|
104
|
+
login_manager = LoginManager()
|
|
105
|
+
login_manager.init_app(app)
|
|
106
|
+
login_manager.login_view = 'auth.login'
|
|
107
|
+
login_manager.login_message = '请先登录系统'
|
|
108
|
+
login_manager.login_message_category = 'warning'
|
|
109
|
+
|
|
110
|
+
app.jinja_env.globals['hasattr'] = hasattr
|
|
111
|
+
|
|
112
|
+
@app.before_request
|
|
113
|
+
def before_request():
|
|
114
|
+
global _request_count, _active_connections
|
|
115
|
+
_request_count += 1
|
|
116
|
+
_active_connections += 1
|
|
117
|
+
|
|
118
|
+
@app.after_request
|
|
119
|
+
def after_request(response):
|
|
120
|
+
global _active_connections
|
|
121
|
+
_active_connections = max(0, _active_connections - 1)
|
|
122
|
+
return response
|
|
123
|
+
|
|
124
|
+
@app.template_filter('from_json')
|
|
125
|
+
def from_json_filter(value):
|
|
126
|
+
try:
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
return json.loads(value)
|
|
129
|
+
return value
|
|
130
|
+
except (json.JSONDecodeError, TypeError):
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
@app.template_filter('rjust')
|
|
134
|
+
def rjust_filter(value, width, fillchar=' '):
|
|
135
|
+
try:
|
|
136
|
+
return str(value).rjust(int(width), str(fillchar))
|
|
137
|
+
except (ValueError, TypeError):
|
|
138
|
+
return str(value)
|
|
139
|
+
|
|
140
|
+
@app.template_filter('ljust')
|
|
141
|
+
def ljust_filter(value, width, fillchar=' '):
|
|
142
|
+
try:
|
|
143
|
+
return str(value).ljust(int(width), str(fillchar))
|
|
144
|
+
except (ValueError, TypeError):
|
|
145
|
+
return str(value)
|
|
146
|
+
|
|
147
|
+
@app.template_filter('zfill')
|
|
148
|
+
def zfill_filter(value, width):
|
|
149
|
+
try:
|
|
150
|
+
return str(value).zfill(int(width))
|
|
151
|
+
except (ValueError, TypeError):
|
|
152
|
+
return str(value)
|
|
153
|
+
|
|
154
|
+
@login_manager.user_loader
|
|
155
|
+
def load_user(user_id):
|
|
156
|
+
from .user import User
|
|
157
|
+
return User.get_by_id(user_id)
|
|
158
|
+
|
|
159
|
+
from .auth import auth_bp
|
|
160
|
+
from .teacher import teacher_bp
|
|
161
|
+
from .student import student_bp
|
|
162
|
+
from .api import api_bp
|
|
163
|
+
from .main import main_bp
|
|
164
|
+
from .admin import admin_bp
|
|
165
|
+
|
|
166
|
+
app.register_blueprint(auth_bp, url_prefix='/auth')
|
|
167
|
+
app.register_blueprint(teacher_bp, url_prefix='/teacher')
|
|
168
|
+
app.register_blueprint(student_bp, url_prefix='/student')
|
|
169
|
+
app.register_blueprint(api_bp, url_prefix='/api')
|
|
170
|
+
app.register_blueprint(main_bp)
|
|
171
|
+
app.register_blueprint(admin_bp, url_prefix='/admin')
|
|
172
|
+
|
|
173
|
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
174
|
+
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
|
|
175
|
+
|
|
176
|
+
@app.errorhandler(404)
|
|
177
|
+
def not_found_error(error):
|
|
178
|
+
try:
|
|
179
|
+
from flask import render_template
|
|
180
|
+
return render_template('errors/404.html'), 404
|
|
181
|
+
except:
|
|
182
|
+
return '<h1>404 - 页面不存在</h1><p>抱歉,您访问的页面不存在。</p><a href="/">返回首页</a>', 404
|
|
183
|
+
|
|
184
|
+
@app.errorhandler(500)
|
|
185
|
+
def internal_error(error):
|
|
186
|
+
try:
|
|
187
|
+
from flask import render_template
|
|
188
|
+
return render_template('errors/500.html'), 500
|
|
189
|
+
except:
|
|
190
|
+
return '<h1>500 - 服务器错误</h1><p>抱歉,服务器遇到了错误。</p><a href="/">返回首页</a>', 500
|
|
191
|
+
|
|
192
|
+
print("[SUCCESS] FlashExam 初始化完成")
|
|
193
|
+
return app
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"__version__",
|
|
197
|
+
"create_app",
|
|
198
|
+
]
|
flashexam/admin.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
管理员管理模块
|
|
3
|
+
提供教师账户管理和系统管理功能
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
|
7
|
+
from flask_login import login_required, current_user
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from .database import Database
|
|
12
|
+
|
|
13
|
+
admin_bp = Blueprint('admin', __name__)
|
|
14
|
+
|
|
15
|
+
def require_admin():
|
|
16
|
+
"""检查是否为管理员(教师角色)"""
|
|
17
|
+
if not current_user.is_authenticated or not current_user.is_teacher():
|
|
18
|
+
return False
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
@admin_bp.route('/')
|
|
22
|
+
@login_required
|
|
23
|
+
def index():
|
|
24
|
+
"""管理面板首页"""
|
|
25
|
+
if not require_admin():
|
|
26
|
+
flash('需要管理员权限', 'error')
|
|
27
|
+
return redirect(url_for('auth.login'))
|
|
28
|
+
|
|
29
|
+
db = Database()
|
|
30
|
+
|
|
31
|
+
stats = {
|
|
32
|
+
'total_students': len(db.get_all_students()),
|
|
33
|
+
'total_exams': len(db.get_exams()),
|
|
34
|
+
'active_exams': len([e for e in db.get_exams() if e.is_active]),
|
|
35
|
+
'total_questions': len(db.get_questions())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return render_template('admin/index.html', stats=stats)
|
|
39
|
+
|
|
40
|
+
@admin_bp.route('/teachers')
|
|
41
|
+
@login_required
|
|
42
|
+
def teachers():
|
|
43
|
+
"""教师账户列表"""
|
|
44
|
+
if not require_admin():
|
|
45
|
+
return jsonify({'success': False, 'message': '需要管理员权限'}), 403
|
|
46
|
+
|
|
47
|
+
db = Database()
|
|
48
|
+
|
|
49
|
+
return render_template('admin/teachers.html')
|
|
50
|
+
|
|
51
|
+
@admin_bp.route('/teachers/add', methods=['POST'])
|
|
52
|
+
@login_required
|
|
53
|
+
def add_teacher():
|
|
54
|
+
"""添加教师账户"""
|
|
55
|
+
if not require_admin():
|
|
56
|
+
return jsonify({'success': False, 'message': '需要管理员权限'}), 403
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = request.get_json() if request.is_json else request.form
|
|
60
|
+
username = data.get('username', '').strip()
|
|
61
|
+
password = data.get('password', '').strip()
|
|
62
|
+
name = data.get('name', '').strip()
|
|
63
|
+
email = data.get('email', '').strip()
|
|
64
|
+
|
|
65
|
+
if not all([username, password, name]):
|
|
66
|
+
return jsonify({'success': False, 'message': '请填写所有必填字段'}), 400
|
|
67
|
+
|
|
68
|
+
if len(password) < 6:
|
|
69
|
+
return jsonify({'success': False, 'message': '密码长度至少6位'}), 400
|
|
70
|
+
|
|
71
|
+
db = Database()
|
|
72
|
+
|
|
73
|
+
if db.get_teacher_info(username):
|
|
74
|
+
return jsonify({'success': False, 'message': '用户名已存在'}), 400
|
|
75
|
+
|
|
76
|
+
success = db.set_teacher_password(username, password, name, email)
|
|
77
|
+
|
|
78
|
+
if success:
|
|
79
|
+
return jsonify({'success': True, 'message': '教师账户添加成功'})
|
|
80
|
+
else:
|
|
81
|
+
return jsonify({'success': False, 'message': '添加失败,请重试'}), 500
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return jsonify({'success': False, 'message': str(e)}), 500
|
|
85
|
+
|
|
86
|
+
@admin_bp.route('/teachers/<username>/password', methods=['PUT'])
|
|
87
|
+
@login_required
|
|
88
|
+
def update_teacher_password(username):
|
|
89
|
+
"""更新教师密码"""
|
|
90
|
+
if not require_admin():
|
|
91
|
+
return jsonify({'success': False, 'message': '需要管理员权限'}), 403
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
data = request.get_json()
|
|
95
|
+
new_password = data.get('new_password', '').strip()
|
|
96
|
+
|
|
97
|
+
if not new_password:
|
|
98
|
+
return jsonify({'success': False, 'message': '新密码不能为空'}), 400
|
|
99
|
+
|
|
100
|
+
if len(new_password) < 6:
|
|
101
|
+
return jsonify({'success': False, 'message': '密码长度至少6位'}), 400
|
|
102
|
+
|
|
103
|
+
db = Database()
|
|
104
|
+
|
|
105
|
+
if not db.get_teacher_info(username):
|
|
106
|
+
return jsonify({'success': False, 'message': '用户不存在'}), 404
|
|
107
|
+
|
|
108
|
+
success = db.update_teacher_password(username, new_password)
|
|
109
|
+
|
|
110
|
+
if success:
|
|
111
|
+
return jsonify({'success': True, 'message': '密码更新成功'})
|
|
112
|
+
else:
|
|
113
|
+
return jsonify({'success': False, 'message': '更新失败,请重试'}), 500
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return jsonify({'success': False, 'message': str(e)}), 500
|
|
117
|
+
|
|
118
|
+
@admin_bp.route('/students')
|
|
119
|
+
@login_required
|
|
120
|
+
def manage_students():
|
|
121
|
+
"""学生管理页面"""
|
|
122
|
+
if not require_admin():
|
|
123
|
+
flash('需要管理员权限', 'error')
|
|
124
|
+
return redirect(url_for('auth.login'))
|
|
125
|
+
|
|
126
|
+
return redirect(url_for('teacher.students'))
|
|
127
|
+
|
|
128
|
+
@admin_bp.route('/settings')
|
|
129
|
+
@login_required
|
|
130
|
+
def settings():
|
|
131
|
+
"""系统设置"""
|
|
132
|
+
if not require_admin():
|
|
133
|
+
flash('需要管理员权限', 'error')
|
|
134
|
+
return redirect(url_for('auth.login'))
|
|
135
|
+
|
|
136
|
+
return render_template('admin/settings.html')
|
|
137
|
+
|
|
138
|
+
@admin_bp.route('/api/teachers')
|
|
139
|
+
@login_required
|
|
140
|
+
def api_get_teachers():
|
|
141
|
+
"""获取教师列表API"""
|
|
142
|
+
if not require_admin():
|
|
143
|
+
return jsonify({'success': False, 'message': '需要管理员权限'}), 403
|
|
144
|
+
|
|
145
|
+
db = Database()
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
db.conn.row_factory = None
|
|
149
|
+
cursor = db.conn.execute('''
|
|
150
|
+
SELECT username, name, email, created_at
|
|
151
|
+
FROM teachers
|
|
152
|
+
ORDER BY created_at DESC
|
|
153
|
+
''')
|
|
154
|
+
rows = cursor.fetchall()
|
|
155
|
+
|
|
156
|
+
teachers_list = []
|
|
157
|
+
for row in rows:
|
|
158
|
+
teachers_list.append({
|
|
159
|
+
'username': row[0],
|
|
160
|
+
'name': row[1],
|
|
161
|
+
'email': row[2] or '',
|
|
162
|
+
'created_at': row[3] or ''
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return jsonify({
|
|
166
|
+
'success': True,
|
|
167
|
+
'teachers': teachers_list,
|
|
168
|
+
'count': len(teachers_list)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return jsonify({'success': False, 'message': str(e)}), 500
|
|
173
|
+
|
|
174
|
+
@admin_bp.route('/api/stats')
|
|
175
|
+
@login_required
|
|
176
|
+
def api_get_stats():
|
|
177
|
+
"""获取系统统计API"""
|
|
178
|
+
if not require_admin():
|
|
179
|
+
return jsonify({'success': False, 'message': '需要管理员权限'}), 403
|
|
180
|
+
|
|
181
|
+
db = Database()
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
stats = {
|
|
185
|
+
'total_students': len(db.get_all_students()),
|
|
186
|
+
'total_exams': len(db.get_exams()),
|
|
187
|
+
'active_exams': len([e for e in db.get_exams() if e.is_active]),
|
|
188
|
+
'total_questions': len(db.get_questions()),
|
|
189
|
+
'database_size': os.path.getsize(db.db_path) if hasattr(db, 'db_path') else 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return jsonify({
|
|
193
|
+
'success': True,
|
|
194
|
+
'stats': stats
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return jsonify({'success': False, 'message': str(e)}), 500
|