1-version upload system
This commit is contained in:
parent
6129cd506d
commit
cc4f4c253c
22
.env
22
.env
@ -1,4 +1,24 @@
|
||||
SECRET_KEY=6c9d28c778888d3fc7c459155e6593f8
|
||||
|
||||
# 邮箱配置
|
||||
EMAIL_HOST=mail.sq0715.com
|
||||
EMAIL_PORT=465
|
||||
MAIL_USE_SSL=True
|
||||
EMAIL_USERNAME=vip@sq0715.com
|
||||
EMAIL_PASSWORD=Lsq12350501.
|
||||
EMAIL_FROM_NAME=Qin
|
||||
EMAIL_FROM=vip@sq0715.com
|
||||
|
||||
# AWS 配置
|
||||
AWS_ACCESS_KEY_ID=AKIAZQ3DT3KLI6N5LQUM
|
||||
AWS_SECRET_ACCESS_KEY=R5eLA2TjGLZT77xgACiEP39Y7JnnbPjLzL64v0tT
|
||||
AWS_REGION=ap-northeast-1
|
||||
S3_BUCKET_NAME=sure-ae-upload
|
||||
S3_BUCKET_NAME=qin-test-file-system
|
||||
|
||||
# MySQL 配置
|
||||
MYSQL_HOST=8.218.165.242
|
||||
MYSQL_USER=sure_001
|
||||
MYSQL_PASSWORD=EKKWLMmrGmG7sdPf
|
||||
MYSQL_DB=sure_001
|
||||
|
||||
|
||||
|
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
app.py
|
2
.idea/AWS-sure.iml
generated
2
.idea/AWS-sure.iml
generated
@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv/lib/python3.10/site-packages" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (AWS-sure)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||
</project>
|
BIN
__pycache__/app_function.cpython-311.pyc
Normal file
BIN
__pycache__/app_function.cpython-311.pyc
Normal file
Binary file not shown.
247
app.py
247
app.py
@ -1,223 +1,50 @@
|
||||
from flask import Flask, request, jsonify, send_from_directory, make_response, send_file
|
||||
from flask import Flask, request, jsonify, redirect, url_for, render_template, session, make_response
|
||||
from flask_cors import CORS
|
||||
import boto3
|
||||
from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError
|
||||
import os
|
||||
from flask_mail import Mail
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_session import Session
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import logging
|
||||
import datetime
|
||||
import pytz
|
||||
from botocore.client import Config
|
||||
import csv
|
||||
import pandas as pd
|
||||
import app_function # 引入新的文件
|
||||
|
||||
# 载入环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 初始化 Flask 应用
|
||||
app = Flask(__name__, static_url_path='', static_folder='.')
|
||||
CORS(app, resources={r"/*": {"origins": "*", "methods": "GET,POST,PUT,DELETE,OPTIONS"}})
|
||||
bcrypt = Bcrypt(app)
|
||||
|
||||
# 确保 SECRET_KEY 被设置
|
||||
app.secret_key = os.getenv('SECRET_KEY', 'you_will_never_guess')
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.config['SESSION_PERMANENT'] = False
|
||||
Session(app)
|
||||
|
||||
|
||||
|
||||
# 配置邮件
|
||||
app.config.update(
|
||||
MAIL_SERVER=os.getenv('EMAIL_HOST'),
|
||||
MAIL_PORT=os.getenv('EMAIL_PORT'),
|
||||
MAIL_USE_SSL=True,
|
||||
MAIL_USERNAME=os.getenv('EMAIL_USERNAME'),
|
||||
MAIL_PASSWORD=os.getenv('EMAIL_PASSWORD'),
|
||||
MAIL_DEFAULT_SENDER=(os.getenv('EMAIL_FROM_NAME'), os.getenv('EMAIL_FROM'))
|
||||
)
|
||||
mail = Mail(app)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
load_dotenv() # 从.env文件加载环境变量
|
||||
|
||||
app = Flask(__name__, static_url_path='', static_folder='.')
|
||||
CORS(app, resources={r"/*": {"origins": "*", "methods": "GET,POST,PUT,DELETE,OPTIONS"}}) # 添加 CORS 支持
|
||||
|
||||
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
region_name = os.getenv('AWS_REGION')
|
||||
bucket_name = os.getenv('S3_BUCKET_NAME')
|
||||
|
||||
# 打印环境变量 (仅用于调试,生产环境中请移除)
|
||||
print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
|
||||
print(f"AWS_SECRET_ACCESS_KEY: {'*' * len(aws_secret_access_key) if aws_secret_access_key else 'Not set'}")
|
||||
print(f"AWS_REGION: {region_name}")
|
||||
print(f"S3_BUCKET_NAME: {bucket_name}")
|
||||
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name,
|
||||
config=Config(signature_version='s3v4') # 使用 S3v4 签名版本
|
||||
)
|
||||
|
||||
# 跟踪学生提交信息
|
||||
submissions_file = 'submissions.csv'
|
||||
|
||||
# 创建或者加载提交文件
|
||||
if not os.path.exists(submissions_file):
|
||||
with open(submissions_file, 'w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件'])
|
||||
|
||||
|
||||
def add_submission(student, student_id, assignment, filename):
|
||||
with open(submissions_file, 'a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, filename])
|
||||
|
||||
|
||||
def generate_presigned_url(object_key, content_type, expiration=3600):
|
||||
try:
|
||||
current_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Current UTC time before generating URL: {current_time}")
|
||||
|
||||
response = s3_client.generate_presigned_url('put_object',
|
||||
Params={
|
||||
'Bucket': bucket_name,
|
||||
'Key': object_key,
|
||||
'ContentType': content_type
|
||||
},
|
||||
ExpiresIn=expiration,
|
||||
HttpMethod='PUT'
|
||||
)
|
||||
logging.info(f"Generated presigned URL: {response}")
|
||||
return response
|
||||
except (NoCredentialsError, ClientError, EndpointConnectionError) as e:
|
||||
logging.error(f"Error generating presigned URL: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@app.route('/generate-url', methods=['GET'])
|
||||
def get_presigned_url():
|
||||
student = request.args.get('student')
|
||||
student_id = request.args.get('student_id')
|
||||
filename = request.args.get('filename')
|
||||
content_type = request.args.get('content_type', 'application/octet-stream')
|
||||
assignment = request.args.get('assignment')
|
||||
logging.info(
|
||||
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}")
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
logging.warning("Missing student, student_id, assignment or filename parameter")
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
new_filename = f'{student}_{student_id}_{filename}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
|
||||
url = generate_presigned_url(object_key, content_type)
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
return jsonify({'url': url, 'content_type': content_type})
|
||||
|
||||
|
||||
@app.route('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student = data.get('student')
|
||||
student_id = data.get('student_id')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
add_submission(student, student_id, assignment, filename)
|
||||
|
||||
return jsonify({'status': 'success'})
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def serve_index():
|
||||
return send_from_directory('.', 'index.html')
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
logging.info("Health check initiated")
|
||||
try:
|
||||
local_time = datetime.datetime.now()
|
||||
utc_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Local time: {local_time}, UTC time: {utc_time}")
|
||||
|
||||
logging.info("Attempting to list S3 buckets")
|
||||
response = s3_client.list_buckets()
|
||||
logging.info(f"Successfully listed buckets: {[bucket['Name'] for bucket in response['Buckets']]}")
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'message': 'AWS credentials are valid',
|
||||
'local_time': local_time.isoformat(),
|
||||
'utc_time': utc_time.isoformat()
|
||||
}), 200
|
||||
except NoCredentialsError:
|
||||
logging.error("AWS credentials not found", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': 'AWS credentials not found'}), 500
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
error_message = e.response['Error']['Message']
|
||||
logging.error(f"AWS client error: {error_code} - {error_message}", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': f'AWS client error: {error_code} - {error_message}'}), 500
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during health check: {str(e)}", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': f'Unexpected error: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.route('/download-submissions')
|
||||
def download_submissions():
|
||||
try:
|
||||
# 使用 csv 模块读取文件,这样可以处理不一致的行
|
||||
with open(submissions_file, 'r', newline='') as file:
|
||||
csv_reader = csv.reader(file)
|
||||
headers = next(csv_reader) # 读取标题行
|
||||
data = list(csv_reader) # 读取所有数据行
|
||||
|
||||
# 确定最大列数
|
||||
max_columns = max(len(headers), max(len(row) for row in data))
|
||||
|
||||
# 创建一个新的数据列表,确保每行都有相同数量的列
|
||||
normalized_data = []
|
||||
for row in data:
|
||||
normalized_data.append(row + [''] * (max_columns - len(row)))
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers)))
|
||||
|
||||
# 重命名列(如果需要)
|
||||
expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件']
|
||||
df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))]
|
||||
|
||||
# 删除完全为空的行和列
|
||||
df = df.dropna(how='all', axis=0).dropna(how='all', axis=1)
|
||||
|
||||
output_file = 'submissions.xlsx'
|
||||
df.to_excel(output_file, index=False)
|
||||
return send_file(output_file, as_attachment=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# 添加路由
|
||||
app_function.add_admin_routes(app, mail, bcrypt)
|
||||
app_function.add_teacher_routes(app, mail, bcrypt)
|
||||
|
||||
@app.before_request
|
||||
def before_request_func():
|
||||
if request.method == 'OPTIONS':
|
||||
return _build_cors_preflight_response()
|
||||
|
||||
|
||||
def _build_cors_preflight_response():
|
||||
response = make_response()
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type")
|
||||
return response
|
||||
|
||||
return app_function._build_cors_preflight_response()
|
||||
|
||||
if __name__ == '__main__':
|
||||
local_time = datetime.datetime.now()
|
||||
utc_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Application starting. Local time: {local_time}, UTC time: {utc_time}")
|
||||
|
||||
try:
|
||||
logging.info("Validating AWS credentials on startup")
|
||||
sts = boto3.client('sts',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name)
|
||||
response = sts.get_caller_identity()
|
||||
logging.info(f"AWS credentials validated successfully. Account ID: {response['Account']}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to validate AWS credentials: {str(e)}", exc_info=True)
|
||||
# 如果你想在凭证验证失败时退出程序,取消注释下面两行
|
||||
# import sys
|
||||
# sys.exit(1)
|
||||
|
||||
app.run(debug=True)
|
||||
|
223
app.txt
223
app.txt
@ -1,223 +0,0 @@
|
||||
from flask import Flask, request, jsonify, send_from_directory, make_response, send_file
|
||||
from flask_cors import CORS
|
||||
import boto3
|
||||
from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
import datetime
|
||||
import pytz
|
||||
from botocore.client import Config
|
||||
import csv
|
||||
import pandas as pd
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
load_dotenv() # 从.env文件加载环境变量
|
||||
|
||||
app = Flask(__name__, static_url_path='', static_folder='.')
|
||||
CORS(app, resources={r"/*": {"origins": "*", "methods": "GET,POST,PUT,DELETE,OPTIONS"}}) # 添加 CORS 支持
|
||||
|
||||
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
region_name = os.getenv('AWS_REGION')
|
||||
bucket_name = os.getenv('S3_BUCKET_NAME')
|
||||
|
||||
# 打印环境变量 (仅用于调试,生产环境中请移除)
|
||||
print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
|
||||
print(f"AWS_SECRET_ACCESS_KEY: {'*' * len(aws_secret_access_key) if aws_secret_access_key else 'Not set'}")
|
||||
print(f"AWS_REGION: {region_name}")
|
||||
print(f"S3_BUCKET_NAME: {bucket_name}")
|
||||
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name,
|
||||
config=Config(signature_version='s3v4') # 使用 S3v4 签名版本
|
||||
)
|
||||
|
||||
# 跟踪学生提交信息
|
||||
submissions_file = 'submissions.csv'
|
||||
|
||||
# 创建或者加载提交文件
|
||||
if not os.path.exists(submissions_file):
|
||||
with open(submissions_file, 'w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件'])
|
||||
|
||||
|
||||
def add_submission(student, student_id, assignment, filename):
|
||||
with open(submissions_file, 'a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, filename])
|
||||
|
||||
|
||||
def generate_presigned_url(object_key, content_type, expiration=3600):
|
||||
try:
|
||||
current_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Current UTC time before generating URL: {current_time}")
|
||||
|
||||
response = s3_client.generate_presigned_url('put_object',
|
||||
Params={
|
||||
'Bucket': bucket_name,
|
||||
'Key': object_key,
|
||||
'ContentType': content_type
|
||||
},
|
||||
ExpiresIn=expiration,
|
||||
HttpMethod='PUT'
|
||||
)
|
||||
logging.info(f"Generated presigned URL: {response}")
|
||||
return response
|
||||
except (NoCredentialsError, ClientError, EndpointConnectionError) as e:
|
||||
logging.error(f"Error generating presigned URL: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@app.route('/generate-url', methods=['GET'])
|
||||
def get_presigned_url():
|
||||
student = request.args.get('student')
|
||||
student_id = request.args.get('student_id')
|
||||
filename = request.args.get('filename')
|
||||
content_type = request.args.get('content_type', 'application/octet-stream')
|
||||
assignment = request.args.get('assignment')
|
||||
logging.info(
|
||||
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}")
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
logging.warning("Missing student, student_id, assignment or filename parameter")
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
new_filename = f'{student}_{student_id}_{filename}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
|
||||
url = generate_presigned_url(object_key, content_type)
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
return jsonify({'url': url, 'content_type': content_type})
|
||||
|
||||
|
||||
@app.route('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student = data.get('student')
|
||||
student_id = data.get('student_id')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
add_submission(student, student_id, assignment, filename)
|
||||
|
||||
return jsonify({'status': 'success'})
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def serve_index():
|
||||
return send_from_directory('.', 'index.html')
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
logging.info("Health check initiated")
|
||||
try:
|
||||
local_time = datetime.datetime.now()
|
||||
utc_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Local time: {local_time}, UTC time: {utc_time}")
|
||||
|
||||
logging.info("Attempting to list S3 buckets")
|
||||
response = s3_client.list_buckets()
|
||||
logging.info(f"Successfully listed buckets: {[bucket['Name'] for bucket in response['Buckets']]}")
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'message': 'AWS credentials are valid',
|
||||
'local_time': local_time.isoformat(),
|
||||
'utc_time': utc_time.isoformat()
|
||||
}), 200
|
||||
except NoCredentialsError:
|
||||
logging.error("AWS credentials not found", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': 'AWS credentials not found'}), 500
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
error_message = e.response['Error']['Message']
|
||||
logging.error(f"AWS client error: {error_code} - {error_message}", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': f'AWS client error: {error_code} - {error_message}'}), 500
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during health check: {str(e)}", exc_info=True)
|
||||
return jsonify({'status': 'unhealthy', 'message': f'Unexpected error: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.route('/download-submissions')
|
||||
def download_submissions():
|
||||
try:
|
||||
# 使用 csv 模块读取文件,这样可以处理不一致的行
|
||||
with open(submissions_file, 'r', newline='') as file:
|
||||
csv_reader = csv.reader(file)
|
||||
headers = next(csv_reader) # 读取标题行
|
||||
data = list(csv_reader) # 读取所有数据行
|
||||
|
||||
# 确定最大列数
|
||||
max_columns = max(len(headers), max(len(row) for row in data))
|
||||
|
||||
# 创建一个新的数据列表,确保每行都有相同数量的列
|
||||
normalized_data = []
|
||||
for row in data:
|
||||
normalized_data.append(row + [''] * (max_columns - len(row)))
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers)))
|
||||
|
||||
# 重命名列(如果需要)
|
||||
expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件']
|
||||
df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))]
|
||||
|
||||
# 删除完全为空的行和列
|
||||
df = df.dropna(how='all', axis=0).dropna(how='all', axis=1)
|
||||
|
||||
output_file = 'submissions.xlsx'
|
||||
df.to_excel(output_file, index=False)
|
||||
return send_file(output_file, as_attachment=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request_func():
|
||||
if request.method == 'OPTIONS':
|
||||
return _build_cors_preflight_response()
|
||||
|
||||
|
||||
def _build_cors_preflight_response():
|
||||
response = make_response()
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type")
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
local_time = datetime.datetime.now()
|
||||
utc_time = datetime.datetime.now(pytz.UTC)
|
||||
logging.info(f"Application starting. Local time: {local_time}, UTC time: {utc_time}")
|
||||
|
||||
try:
|
||||
logging.info("Validating AWS credentials on startup")
|
||||
sts = boto3.client('sts',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name)
|
||||
response = sts.get_caller_identity()
|
||||
logging.info(f"AWS credentials validated successfully. Account ID: {response['Account']}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to validate AWS credentials: {str(e)}", exc_info=True)
|
||||
# 如果你想在凭证验证失败时退出程序,取消注释下面两行
|
||||
# import sys
|
||||
# sys.exit(1)
|
||||
|
||||
app.run(debug=True)
|
781
app_function.py
Normal file
781
app_function.py
Normal file
@ -0,0 +1,781 @@
|
||||
from flask import Flask, request, jsonify, redirect, url_for, render_template, session, make_response, flash
|
||||
from flask_mail import Mail, Message
|
||||
from flask_bcrypt import Bcrypt
|
||||
import mysql.connector
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
import pandas as pd
|
||||
from dotenv import load_dotenv
|
||||
from flask import send_file
|
||||
import io
|
||||
from functools import wraps
|
||||
|
||||
load_dotenv()
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
# 配置 AWS S3
|
||||
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
region_name = os.getenv('AWS_REGION')
|
||||
bucket_name = os.getenv('S3_BUCKET_NAME')
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name,
|
||||
config=Config(signature_version='s3v4')
|
||||
)
|
||||
|
||||
# 数据库连接函数
|
||||
def get_db_connection():
|
||||
return mysql.connector.connect(
|
||||
host=os.getenv('MYSQL_HOST'),
|
||||
user=os.getenv('MYSQL_USER'),
|
||||
password=os.getenv('MYSQL_PASSWORD'),
|
||||
database=os.getenv('MYSQL_DB')
|
||||
)
|
||||
|
||||
# 验证学生身份
|
||||
def validate_student(student_id, password, bcrypt):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM students WHERE id = %s', (student_id,))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
if student and bcrypt.check_password_hash(student['password'], password):
|
||||
return student
|
||||
return None
|
||||
|
||||
# 验证管理员身份
|
||||
def validate_admin(username, password):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM administrators WHERE username = %s', (username,))
|
||||
admin = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if admin and bcrypt.check_password_hash(admin['password'], password):
|
||||
return admin
|
||||
return None
|
||||
|
||||
# 验证教师身份
|
||||
def validate_teacher(email, password, bcrypt):
|
||||
conn = get_db_connection() # 或者其他方法来获取数据库连接
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM teachers WHERE email = %s', (email,))
|
||||
teacher = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if teacher and bcrypt.check_password_hash(teacher['password'], password):
|
||||
return teacher
|
||||
return None
|
||||
|
||||
def fetch_all_departments():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM departments')
|
||||
departments = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return departments
|
||||
|
||||
def fetch_all_grades():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM grades')
|
||||
grades = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return grades
|
||||
|
||||
def fetch_all_classes():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM classes')
|
||||
classes = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return classes
|
||||
|
||||
def fetch_all_teachers():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM teachers')
|
||||
teachers = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return teachers
|
||||
|
||||
def fetch_teacher_classes(teacher_id):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 假设每个教师和班级的关联存储在一个中间表 `class_teacher` 中
|
||||
cursor.execute('''
|
||||
SELECT c.id, c.name FROM classes c
|
||||
JOIN class_teacher ct ON c.id = ct.class_id
|
||||
WHERE ct.teacher_id = %s
|
||||
''', (teacher_id,))
|
||||
|
||||
classes = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return classes
|
||||
|
||||
def fetch_class_assignments(class_id):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
pivot_query = '''
|
||||
SELECT * FROM assignments
|
||||
WHERE class_id = %s
|
||||
'''
|
||||
cursor.execute(pivot_query, (class_id,))
|
||||
assignments = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return assignments
|
||||
|
||||
|
||||
def fetch_class_students(class_id):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 查询属于给定class_id的所有学生
|
||||
cursor.execute('''
|
||||
SELECT * FROM students
|
||||
WHERE class_id = %s
|
||||
''', (class_id,))
|
||||
|
||||
students = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return students
|
||||
|
||||
|
||||
# 添加到提交历史表
|
||||
def add_to_submission_history(student_id, assignment_id, filename):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
'INSERT INTO submission_history (student_id, assignment_id, filename, submit_date) VALUES (%s, %s, %s, NOW())',
|
||||
(student_id, assignment_id, filename)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 添加或更新作业提交记录
|
||||
def add_or_update_submission(student_id, assignment_id, filename, code_verified=False):
|
||||
add_to_submission_history(student_id, assignment_id, filename) # 保留历史记录
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
if code_verified:
|
||||
cursor.execute(
|
||||
'UPDATE submissions SET submit_date = NOW(), filename = %s WHERE student_id = %s AND assignment_id = %s',
|
||||
(filename, student_id, assignment_id))
|
||||
else:
|
||||
cursor.execute(
|
||||
'INSERT INTO submissions (student_id, assignment_id, filename, submit_date) VALUES (%s, %s, %s, NOW())',
|
||||
(student_id, assignment_id, filename))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 生成预签名URL
|
||||
def generate_presigned_url(object_key, content_type, expiration=3600):
|
||||
try:
|
||||
url = s3_client.generate_presigned_url(
|
||||
'put_object',
|
||||
Params={'Bucket': bucket_name, 'Key': object_key, 'ContentType': content_type},
|
||||
ExpiresIn=expiration
|
||||
)
|
||||
return url
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to generate presigned URL: {str(e)}")
|
||||
return None
|
||||
|
||||
# 添加管理员路由
|
||||
def add_admin_routes(app, mail, bcrypt):
|
||||
# 管理员登录装饰器
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'admin_id' not in session:
|
||||
return redirect(url_for('admin_login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@app.route('/admin/add_department', methods=['POST'])
|
||||
@admin_required
|
||||
def add_department():
|
||||
name = request.form['name']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO departments (name) VALUES (%s)', (name,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Department added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_grade', methods=['POST'])
|
||||
@admin_required
|
||||
def add_grade():
|
||||
year = request.form['year']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO grades (year) VALUES (%s)', (year,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Grade added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_class', methods=['POST'])
|
||||
@admin_required
|
||||
def add_class():
|
||||
name = request.form['name']
|
||||
department_id = request.form['department_id']
|
||||
grade_id = request.form['grade_id']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO classes (name, department_id, grade_id) VALUES (%s, %s, %s)',
|
||||
(name, department_id, grade_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Class added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_teacher', methods=['POST'])
|
||||
@admin_required
|
||||
def add_teacher():
|
||||
name = request.form['name']
|
||||
email = request.form['email']
|
||||
password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8')
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO teachers (name, email, password) VALUES (%s, %s, %s)',
|
||||
(name, email, password))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Teacher added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_administrator', methods=['POST'])
|
||||
@admin_required
|
||||
def add_administrator():
|
||||
username = request.form['username']
|
||||
password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8')
|
||||
teacher_id = request.form['teacher_id']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO administrators (username, password, teacher_id) VALUES (%s, %s, %s)',
|
||||
(username, password, teacher_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Administrator added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/assign_teacher', methods=['POST'])
|
||||
@admin_required
|
||||
def assign_teacher():
|
||||
class_id = request.form['class_id']
|
||||
teacher_id = request.form['teacher_id']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO class_teacher (class_id, teacher_id) VALUES (%s, %s)', (class_id, teacher_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Teacher assigned to class successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||
def admin_login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
admin = validate_admin(username, password)
|
||||
if admin:
|
||||
session['admin_id'] = admin['id']
|
||||
return redirect(url_for('admin_panel'))
|
||||
else:
|
||||
flash('Invalid credentials', 'error')
|
||||
return render_template('admin_login.html')
|
||||
|
||||
@app.route('/admin/logout')
|
||||
def admin_logout():
|
||||
session.pop('admin_id', None)
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
@app.route('/admin/panel')
|
||||
@admin_required
|
||||
def admin_panel():
|
||||
# Retrieve necessary data (e.g., departments, grades, classes, etc.)
|
||||
departments = fetch_all_departments()
|
||||
grades = fetch_all_grades()
|
||||
classes = fetch_all_classes()
|
||||
teachers = fetch_all_teachers()
|
||||
return render_template('admin_panel.html', departments=departments, grades=grades, classes=classes, teachers=teachers)
|
||||
|
||||
@app.route('/admin/add_assignment', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_add_assignment():
|
||||
value = request.form['value']
|
||||
name = request.form['name']
|
||||
deadline = request.form['deadline']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO assignments (value, name, deadline) VALUES (%s, %s, %s)', (value, name, deadline))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Assignment added successfully', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
student_id = request.form.get('student_id')
|
||||
password = request.form.get('password')
|
||||
student = validate_student(student_id, password, bcrypt)
|
||||
if student:
|
||||
session['student_id'] = student['id']
|
||||
session['student_name'] = student['name']
|
||||
return redirect(url_for('serve_index'))
|
||||
else:
|
||||
return render_template('login.html', error='学号或密码错误')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/reset-password', methods=['GET', 'POST'])
|
||||
def reset_password():
|
||||
if request.method == 'POST':
|
||||
student_id = request.form.get('student_id')
|
||||
email = request.form.get('email')
|
||||
code = request.form.get('code')
|
||||
new_password = request.form.get('new_password')
|
||||
if code:
|
||||
if session.get('reset_code') == code and session.get('reset_student_id') == student_id:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
||||
cursor.execute('UPDATE students SET password = %s WHERE id = %s', (hashed_password, student_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return render_template('login.html', success='密码已成功重置,请使用新密码登录')
|
||||
else:
|
||||
return render_template('reset_password.html', error='验证码错误')
|
||||
else:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM students WHERE id = %s AND email = %s', (student_id, email))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
if student:
|
||||
reset_code = ''.join(random.choices('0123456789', k=6))
|
||||
session['reset_code'] = reset_code
|
||||
session['reset_student_id'] = student_id
|
||||
try:
|
||||
msg = Message('重置密码验证码', recipients=[email])
|
||||
msg.body = f'您用于重置密码的验证码是: {reset_code}'
|
||||
mail.send(msg)
|
||||
return render_template('reset_password.html',
|
||||
success='验证码已发送到您的邮箱,请检查并输入验证码')
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending email: {str(e)}")
|
||||
return render_template('reset_password.html', error='发送验证码失败,请稍后再试')
|
||||
else:
|
||||
return render_template('reset_password.html', error='学号和邮箱不匹配')
|
||||
return render_template('reset_password.html')
|
||||
|
||||
@app.route('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student_id = session.get('student_id')
|
||||
student_name = session.get('student_name')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student_id or not filename or not assignment:
|
||||
return jsonify({'error': 'Student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM submissions WHERE student_id = %s AND assignment_id = %s',
|
||||
(student_id, assignment))
|
||||
submission = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if submission:
|
||||
session['filename'] = filename # 将文件名保存到session
|
||||
return jsonify({'error': '作业已提交过,需要验证码'}), 401
|
||||
|
||||
# 生成 pre-signed URL 并记录提交
|
||||
new_filename = f'{student_id}_{student_name}_{assignment}'
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
url = generate_presigned_url(object_key, 'application/octet-stream')
|
||||
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
add_or_update_submission(student_id, assignment, filename, code_verified=False)
|
||||
return jsonify({'status': 'success', 'upload_url': url})
|
||||
|
||||
@app.route('/generate-code', methods=['POST'])
|
||||
def generate_code():
|
||||
student_id = session.get('student_id')
|
||||
assignment = request.json.get('assignment')
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT email FROM students WHERE id = %s', (student_id,))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if not student:
|
||||
return jsonify({'error': '学生信息未找到'}), 404
|
||||
|
||||
email = student['email']
|
||||
reset_code = ''.join(random.choices('0123456789', k=6))
|
||||
session['submission_code'] = reset_code
|
||||
session['submission_student_id'] = student_id
|
||||
session['submission_assignment'] = assignment
|
||||
|
||||
try:
|
||||
msg = Message('提交作业验证码', recipients=[email])
|
||||
msg.body = f'您用于提交作业的验证码是: {reset_code}'
|
||||
mail.send(msg)
|
||||
return jsonify({'status': '验证码已发送到您的邮箱,请检查并输入验证码'})
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending email: {str(e)}")
|
||||
return jsonify({'error': '发送验证码失败,请稍后再试'}), 500
|
||||
|
||||
@app.route('/validate-code', methods=['POST'])
|
||||
def validate_code():
|
||||
request_data = request.json
|
||||
student_id = session.get('student_id')
|
||||
assignment = request_data.get('assignment')
|
||||
code = request_data.get('code')
|
||||
|
||||
if (code == session.get('submission_code') and
|
||||
student_id == session.get('submission_student_id') and
|
||||
assignment == session.get('submission_assignment')):
|
||||
|
||||
filename = session.pop('filename', None)
|
||||
new_filename = f'{student_id}_{session.get("student_name")}_{assignment}'
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
|
||||
logging.info(f"Generated object_key: {object_key}")
|
||||
|
||||
url = generate_presigned_url(object_key, 'application/octet-stream')
|
||||
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
# 调用 add_or_update_submission 函数来更新数据库记录
|
||||
add_or_update_submission(student_id, assignment, filename, code_verified=True)
|
||||
|
||||
# 删除session中的凭据
|
||||
session.pop('submission_code', None)
|
||||
session.pop('submission_student_id', None)
|
||||
session.pop('submission_assignment', None)
|
||||
session['validated_assignment'] = assignment
|
||||
session['validation_presigned_url'] = url
|
||||
return jsonify({'status': '成功', 'upload_url': url})
|
||||
else:
|
||||
return jsonify({'error': '验证码错误'}), 400
|
||||
|
||||
@app.route('/api/submissions')
|
||||
def get_submissions():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
sh.submit_date AS 时间,
|
||||
sh.filename AS 提交的文件,
|
||||
s.name AS 姓名,
|
||||
s.id AS 学号,
|
||||
sh.assignment_id AS 作业
|
||||
FROM submission_history sh
|
||||
JOIN students s ON sh.student_id = s.id
|
||||
ORDER BY sh.submit_date DESC
|
||||
""")
|
||||
submissions = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化日期时间
|
||||
for sub in submissions:
|
||||
sub['时间'] = sub['时间'].strftime('%Y-%m-%d %H:%M:%S') if sub['时间'] else None
|
||||
|
||||
return jsonify(submissions)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/assignment-status')
|
||||
def get_assignment_status():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id AS 学号,
|
||||
s.name AS 姓名,
|
||||
a.assignment_id AS 作业,
|
||||
CASE WHEN sub.id IS NOT NULL THEN '已提交' ELSE '未提交' END AS 提交情况,
|
||||
COALESCE(sub.filename, '') AS 提交的文件
|
||||
FROM students s
|
||||
CROSS JOIN (SELECT DISTINCT assignment_id FROM submissions) a
|
||||
LEFT JOIN submissions sub ON s.id = sub.student_id AND a.assignment_id = sub.assignment_id
|
||||
ORDER BY s.id, a.assignment_id
|
||||
""")
|
||||
status_data = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify(status_data)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_assignment_status: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/download-submissions')
|
||||
def download_submissions():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
sh.submit_date AS 时间,
|
||||
sh.filename AS 提交的项目,
|
||||
s.name AS 姓名,
|
||||
s.id AS 学号,
|
||||
sh.assignment_id AS 作业
|
||||
FROM submission_history sh
|
||||
JOIN students s ON sh.student_id = s.id
|
||||
ORDER BY sh.submit_date DESC
|
||||
""")
|
||||
submissions = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(submissions)
|
||||
|
||||
# 格式化日期时间
|
||||
df['时间'] = pd.to_datetime(df['时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 确保列的顺序
|
||||
df = df[['时间', '提交的项目', '姓名', '学号', '作业']]
|
||||
|
||||
# 创建一个 BytesIO 对象,用于存储 Excel 文件
|
||||
output = io.BytesIO()
|
||||
|
||||
# 使用 ExcelWriter 来设置列宽
|
||||
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
||||
df.to_excel(writer, sheet_name='提交记录', index=False)
|
||||
worksheet = writer.sheets['提交记录']
|
||||
|
||||
# 设置列宽
|
||||
worksheet.set_column('A:A', 20) # 时间
|
||||
worksheet.set_column('B:B', 30) # 提交的项目
|
||||
worksheet.set_column('C:C', 15) # 姓名
|
||||
worksheet.set_column('D:D', 15) # 学号
|
||||
worksheet.set_column('E:E', 20) # 作业
|
||||
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
as_attachment=True,
|
||||
download_name='submissions.xlsx',
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/download-assignment-status')
|
||||
def download_assignment_status():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id AS 学号,
|
||||
s.name AS 姓名,
|
||||
COALESCE(sub.assignment_id, 'No assignment') AS 作业,
|
||||
CASE WHEN sub.id IS NOT NULL THEN '已提交' ELSE '未提交' END AS 作业提交情况,
|
||||
COALESCE(sub.filename, '') AS 提交的项目,
|
||||
COALESCE(sub.submit_date, '') AS 提交时间
|
||||
FROM students s
|
||||
LEFT JOIN submissions sub ON s.id = sub.student_id
|
||||
ORDER BY s.id, sub.assignment_id
|
||||
""")
|
||||
status_data = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
df = pd.DataFrame(status_data)
|
||||
|
||||
# 格式化日期时间
|
||||
df['提交时间'] = pd.to_datetime(df['提交时间']).dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 确保列的顺序
|
||||
df = df[['学号', '姓名', '作业', '作业提交情况', '提交的项目', '提交时间']]
|
||||
|
||||
output = io.BytesIO()
|
||||
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
||||
df.to_excel(writer, sheet_name='作业提交情况', index=False)
|
||||
worksheet = writer.sheets['作业提交情况']
|
||||
|
||||
# 设置列宽
|
||||
worksheet.set_column('A:A', 15) # 学号
|
||||
worksheet.set_column('B:B', 10) # 姓名
|
||||
worksheet.set_column('C:C', 15) # 作业
|
||||
worksheet.set_column('D:D', 12) # 作业提交情况
|
||||
worksheet.set_column('E:E', 30) # 提交的项目
|
||||
worksheet.set_column('F:F', 20) # 提交时间
|
||||
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
as_attachment=True,
|
||||
download_name='assignment_status.xlsx',
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_assignment_status: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/')
|
||||
def serve_index():
|
||||
if 'student_id' not in session:
|
||||
return redirect(url_for('login'))
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM assignments ORDER BY deadline')
|
||||
assignments = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return render_template('index.html', assignments=assignments)
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
def _build_cors_preflight_response():
|
||||
response = make_response()
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type")
|
||||
return response
|
||||
|
||||
# 教师管理路由
|
||||
def add_teacher_routes(app, mail, bcrypt):
|
||||
# 教师登录装饰器
|
||||
def teacher_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'teacher_id' not in session:
|
||||
return redirect(url_for('teacher_login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Teacher Login Route
|
||||
@app.route('/teacher/login', methods=['GET', 'POST'])
|
||||
def teacher_login():
|
||||
if request.method == 'POST':
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
teacher = validate_teacher(email, password, bcrypt)
|
||||
if teacher:
|
||||
session['teacher_id'] = teacher['id']
|
||||
return redirect(url_for('teacher_panel'))
|
||||
else:
|
||||
flash('Invalid credentials', 'error')
|
||||
return render_template('teacher_login.html')
|
||||
|
||||
@app.route('/teacher/logout')
|
||||
def teacher_logout():
|
||||
session.pop('teacher_id', None)
|
||||
return redirect(url_for('teacher_login'))
|
||||
|
||||
@app.route('/teacher/panel')
|
||||
@teacher_required
|
||||
def teacher_panel():
|
||||
# Fetch the classes this teacher is responsible for
|
||||
teacher_id = session['teacher_id']
|
||||
classes = fetch_teacher_classes(teacher_id)
|
||||
return render_template('teacher_panel.html', classes=classes)
|
||||
|
||||
@app.route('/teacher/class/<int:class_id>')
|
||||
@teacher_required
|
||||
def view_class(class_id):
|
||||
# Fetch the assignments and students of the class
|
||||
assignments = fetch_class_assignments(class_id)
|
||||
students = fetch_class_students(class_id)
|
||||
return render_template('class_detail.html', assignments=assignments, students=students, class_id=class_id)
|
||||
|
||||
@app.route('/teacher/class/<int:class_id>/add_assignment', methods=['POST'])
|
||||
@teacher_required
|
||||
def teacher_add_assignment(class_id):
|
||||
# Adding a new assignment to the class
|
||||
value = request.form['value']
|
||||
name = request.form['name']
|
||||
deadline = request.form['deadline']
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO assignments (value, name, deadline, class_id) VALUES (%s, %s, %s, %s)', (value, name, deadline, class_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
flash('Assignment added successfully', 'success')
|
||||
return redirect(url_for('view_class', class_id=class_id))
|
||||
|
||||
@app.route('/teacher/edit_assignment/<int:assignment_id>', methods=['POST'])
|
||||
def edit_assignment(assignment_id):
|
||||
new_deadline = request.json.get('deadline')
|
||||
if new_deadline:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE assignments SET deadline = %s WHERE id = %s', (new_deadline, assignment_id))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return jsonify({'status': 'success'})
|
||||
return jsonify({'error': 'Invalid deadline'}), 400
|
||||
|
||||
@app.route('/teacher/delete_assignment/<int:assignment_id>', methods=['DELETE'])
|
||||
def delete_assignment(assignment_id):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM assignments WHERE id = %s', (assignment_id,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return jsonify({'status': 'success'})
|
||||
|
69
ccc.py
Normal file
69
ccc.py
Normal file
@ -0,0 +1,69 @@
|
||||
import os
|
||||
from docx import Document
|
||||
|
||||
|
||||
def find_files(start_path, extensions):
|
||||
"""
|
||||
遍历目录并找到指定扩展名的文件。
|
||||
|
||||
:param start_path: 起始路径
|
||||
:param extensions: 需要查找的文件扩展名列表
|
||||
:return: 文件路径列表
|
||||
"""
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(start_path):
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
file_list.append(os.path.join(root, file))
|
||||
return file_list
|
||||
|
||||
|
||||
def read_files(file_list):
|
||||
"""
|
||||
读取文件内容。
|
||||
|
||||
:param file_list: 文件路径列表
|
||||
:return: 文件内容字典
|
||||
"""
|
||||
content_dict = {}
|
||||
for file_path in file_list:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content_dict[file_path] = file.read()
|
||||
return content_dict
|
||||
|
||||
|
||||
def save_to_docx(content_dict, output_file):
|
||||
"""
|
||||
将文件内容字典保存到 DOCX 文件。
|
||||
|
||||
:param content_dict: 文件内容字典
|
||||
:param output_file: 输出的 DOCX 文件名
|
||||
"""
|
||||
doc = Document()
|
||||
for file_path, content in content_dict.items():
|
||||
doc.add_heading(file_path, level=1)
|
||||
doc.add_paragraph(content)
|
||||
doc.add_page_break() # 添加分页符
|
||||
doc.save(output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 需要遍历的目录
|
||||
directory = '/Users/lishunqin/Desktop/study/pychram project/AWS-sure'
|
||||
|
||||
# 需要查找的文件扩展名
|
||||
extensions = ['.py', '.html', '.env', '.css', 'js']
|
||||
|
||||
# 查找文件
|
||||
files = find_files(directory, extensions)
|
||||
|
||||
# 读取文件内容
|
||||
content_dict = read_files(files)
|
||||
|
||||
# 输出 DOCX 文件名
|
||||
output_docx = 'output_files_content.docx'
|
||||
|
||||
# 保存到 DOCX 文件
|
||||
save_to_docx(content_dict, output_docx)
|
||||
|
||||
print(f"找到 {len(files)} 个文件,并保存了内容到 {output_docx}")
|
280
database.py
Normal file
280
database.py
Normal file
@ -0,0 +1,280 @@
|
||||
import os
|
||||
import mysql.connector
|
||||
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
|
||||
from flask_bcrypt import Bcrypt
|
||||
from some_module_to_verify_code import verify_code # 假设你有相应的模块
|
||||
import datetime
|
||||
import random
|
||||
import logging
|
||||
from flask_mail import Mail, Message
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
import csv
|
||||
|
||||
# 初始化 Flask 和 Bcrypt
|
||||
app = Flask(__name__)
|
||||
bcrypt = Bcrypt(app)
|
||||
mail = Mail(app)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# 配置 AWS S3
|
||||
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
region_name = os.getenv('AWS_REGION')
|
||||
bucket_name = os.getenv('S3_BUCKET_NAME')
|
||||
|
||||
# 初始化S3客户端
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name,
|
||||
config=Config(signature_version='s3v4')
|
||||
)
|
||||
|
||||
# 初始化 CSV 文件
|
||||
submissions_file = 'submissions.csv'
|
||||
if not os.path.exists(submissions_file):
|
||||
with open(submissions_file, 'w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件'])
|
||||
|
||||
|
||||
# 数据库连接函数
|
||||
def get_db_connection():
|
||||
return mysql.connector.connect(
|
||||
host='8.218.165.242',
|
||||
user='sure_001',
|
||||
password='EKKWLMmrGmG7sdPf',
|
||||
database='sure_001'
|
||||
)
|
||||
|
||||
|
||||
# 验证学生身份
|
||||
def validate_student(student_id, password):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM students WHERE id = %s', (student_id,))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
if student and bcrypt.check_password_hash(student['password'], password):
|
||||
return student
|
||||
return None
|
||||
|
||||
|
||||
# 检查作业提交
|
||||
def check_submission(student_id, assignment_id):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM submissions WHERE student_id = %s AND assignment_id = %s',
|
||||
(student_id, assignment_id))
|
||||
submission = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return submission
|
||||
|
||||
|
||||
# 添加或更新作业提交
|
||||
def add_or_update_submission(student_id, assignment_id, filename, code_verified=False):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
if code_verified:
|
||||
cursor.execute(
|
||||
'UPDATE submissions SET submit_date = NOW(), filename = %s WHERE student_id = %s AND assignment_id = %s',
|
||||
(filename, student_id, assignment_id))
|
||||
else:
|
||||
cursor.execute(
|
||||
'INSERT INTO submissions (student_id, assignment_id, filename, submit_date) VALUES (%s, %s, %s, NOW())',
|
||||
(student_id, assignment_id, filename))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def fetch_all_departments():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM departments')
|
||||
departments = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return departments
|
||||
|
||||
def fetch_all_grades():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM grades')
|
||||
grades = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return grades
|
||||
|
||||
def fetch_all_classes():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM classes')
|
||||
classes = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return classes
|
||||
|
||||
def fetch_all_teachers():
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM teachers')
|
||||
teachers = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return teachers
|
||||
|
||||
|
||||
|
||||
# 生成预签名URL
|
||||
def generate_presigned_url(object_key, content_type):
|
||||
try:
|
||||
url = s3_client.generate_presigned_url(
|
||||
'put_object',
|
||||
Params={'Bucket': bucket_name, 'Key': object_key, 'ContentType': content_type},
|
||||
ExpiresIn=3600
|
||||
)
|
||||
return url
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to generate presigned URL: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# 提交作业路由
|
||||
@app.route('/submit-assignment', methods=['POST'])
|
||||
def submit_assignment():
|
||||
student_id = request.form.get('student_id')
|
||||
assignment_id = request.form.get('assignment_id')
|
||||
filename = request.form.get('filename')
|
||||
|
||||
submission = check_submission(student_id, assignment_id)
|
||||
|
||||
if submission:
|
||||
# 要求用户输入验证码
|
||||
email = request.form.get('email')
|
||||
code = request.form.get('code')
|
||||
if verify_code(email, code):
|
||||
add_or_update_submission(student_id, assignment_id, filename, code_verified=True)
|
||||
else:
|
||||
add_or_update_submission(student_id, assignment_id, filename, code_verified=False)
|
||||
|
||||
return 'Submission successful'
|
||||
|
||||
|
||||
# 登录路由
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
student_id = request.form.get('student_id')
|
||||
password = request.form.get('password')
|
||||
student = validate_student(student_id, password)
|
||||
if student:
|
||||
session['student_id'] = student['id']
|
||||
session['student_name'] = student['name']
|
||||
return redirect(url_for('serve_index'))
|
||||
else:
|
||||
return render_template('login.html', error='学号或密码错误')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
# 重置密码路由
|
||||
@app.route('/reset-password', methods=['GET', 'POST'])
|
||||
def reset_password():
|
||||
if request.method == 'POST':
|
||||
student_id = request.form.get('student_id')
|
||||
email = request.form.get('email')
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM students WHERE id = %s AND email = %s', (student_id, email))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if student:
|
||||
reset_code = ''.join(random.choices('0123456789', k=6))
|
||||
session['reset_code'] = reset_code
|
||||
session['reset_student_id'] = student_id
|
||||
try:
|
||||
msg = Message('重置密码验证码', recipients=[email])
|
||||
msg.body = f'您用于重置密码的验证码是: {reset_code}'
|
||||
mail.send(msg)
|
||||
return render_template('reset_password.html', success='验证码已发送到您的邮箱,请检查并输入验证码')
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending email: {str(e)}")
|
||||
return render_template('reset_password.html', error='发送验证码失败,请稍后再试')
|
||||
else:
|
||||
return render_template('reset_password.html', error='学号和邮箱不匹配')
|
||||
return render_template('reset_password.html')
|
||||
|
||||
|
||||
# 提交记录路由
|
||||
@app.route('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student_id = session.get('student_id')
|
||||
student_name = session.get('student_name')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student_id or not filename or not assignment:
|
||||
return jsonify({'error': 'Student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
# Check if the student has already submitted this assignment
|
||||
submission = check_submission(student_id, assignment)
|
||||
if submission:
|
||||
session['filename'] = filename
|
||||
return jsonify({'error': '作业已提交过,需要验证码'}), 401
|
||||
|
||||
# 生成 pre-signed URL 并记录提交
|
||||
new_filename = f'{student_id}_{student_name}_{assignment}'
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
url = generate_presigned_url(object_key, 'application/octet-stream')
|
||||
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
add_or_update_submission(student_id, assignment, filename)
|
||||
return jsonify({'status': 'success', 'upload_url': url})
|
||||
|
||||
|
||||
# 生成验证码路由
|
||||
@app.route('/generate-code', methods=['POST'])
|
||||
def generate_code():
|
||||
student_id = session.get('student_id')
|
||||
assignment = request.json.get('assignment')
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT email FROM students WHERE id = %s', (student_id,))
|
||||
student = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if not student:
|
||||
return jsonify({'error': '学生信息未找到'}), 404
|
||||
|
||||
email = student['email']
|
||||
reset_code = ''.join(random.choices('0123456789', k=6))
|
||||
session['submission_code'] = reset_code
|
||||
session['submission_student_id'] = student_id
|
||||
session['submission_assignment'] = assignment
|
||||
|
||||
try:
|
||||
msg = Message('提交验证码', recipients=[email])
|
||||
msg.body = f'您用于提交作业的验证码是: {reset_code}'
|
||||
mail.send(msg)
|
||||
return jsonify({'status': '验证码已发送到您的邮箱'})
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending email: {str(e)}")
|
||||
return jsonify({'error': '发送验证码失败,请稍后再试'}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
46
edit_admin_info.py
Normal file
46
edit_admin_info.py
Normal file
@ -0,0 +1,46 @@
|
||||
from flask_bcrypt import Bcrypt
|
||||
import mysql.connector
|
||||
|
||||
# Initialize Bcrypt
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
# Database connection details
|
||||
MYSQL_HOST = '8.218.165.242'
|
||||
MYSQL_USER = 'sure_001'
|
||||
MYSQL_PASSWORD = 'EKKWLMmrGmG7sdPf'
|
||||
MYSQL_DB = 'sure_001'
|
||||
|
||||
# Function to get database connection
|
||||
def get_db_connection():
|
||||
return mysql.connector.connect(
|
||||
host=MYSQL_HOST,
|
||||
user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD,
|
||||
database=MYSQL_DB
|
||||
)
|
||||
|
||||
# Function to update admin password
|
||||
def update_teacher_password(teacher_id, new_plain_password):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Generate new hashed password
|
||||
hashed_password = bcrypt.generate_password_hash(new_plain_password).decode('utf-8')
|
||||
|
||||
# Update password in the database
|
||||
cursor.execute(
|
||||
'UPDATE teachers SET password = %s WHERE id = %s',
|
||||
(hashed_password, teacher_id)
|
||||
)
|
||||
|
||||
# Commit changes and close the connection
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# Example: Updating the password for admin with ID 1
|
||||
if __name__ == "__main__":
|
||||
tid = 1 # The ID of the admin to update
|
||||
new_password = 'xiaoyan99817' # The new plain password you want to set
|
||||
update_teacher_password(tid, new_password)
|
||||
print(f"Password for admin with ID {tid} has been updated.")
|
BIN
flask_session/0f9028b75157442ffeb13d3d89859e37
Normal file
BIN
flask_session/0f9028b75157442ffeb13d3d89859e37
Normal file
Binary file not shown.
BIN
flask_session/121e2e6237f93addcf2c8d3d0f754de5
Normal file
BIN
flask_session/121e2e6237f93addcf2c8d3d0f754de5
Normal file
Binary file not shown.
BIN
flask_session/1bcc7dc1ae94d369f266fc6c335f304f
Normal file
BIN
flask_session/1bcc7dc1ae94d369f266fc6c335f304f
Normal file
Binary file not shown.
BIN
flask_session/1dd18bd3439eb0ac9a5742e2231f6172
Normal file
BIN
flask_session/1dd18bd3439eb0ac9a5742e2231f6172
Normal file
Binary file not shown.
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
Binary file not shown.
BIN
flask_session/230a5611f6ae17aede03b96deb47fff3
Normal file
BIN
flask_session/230a5611f6ae17aede03b96deb47fff3
Normal file
Binary file not shown.
BIN
flask_session/32dcb849af4ca45626677d9fb3bd4768
Normal file
BIN
flask_session/32dcb849af4ca45626677d9fb3bd4768
Normal file
Binary file not shown.
BIN
flask_session/3378eb0eec6e11aefe392c5e676e8238
Normal file
BIN
flask_session/3378eb0eec6e11aefe392c5e676e8238
Normal file
Binary file not shown.
BIN
flask_session/35f86e813ab1fdd9f718aef70f2447b1
Normal file
BIN
flask_session/35f86e813ab1fdd9f718aef70f2447b1
Normal file
Binary file not shown.
BIN
flask_session/37ddbf9efe72bdf64d50c9a4ca92bc2e
Normal file
BIN
flask_session/37ddbf9efe72bdf64d50c9a4ca92bc2e
Normal file
Binary file not shown.
BIN
flask_session/3ba267ea541c85852c864ec0a198c77b
Normal file
BIN
flask_session/3ba267ea541c85852c864ec0a198c77b
Normal file
Binary file not shown.
BIN
flask_session/3d315f20ae346ffe9e4a7b40ce7fef37
Normal file
BIN
flask_session/3d315f20ae346ffe9e4a7b40ce7fef37
Normal file
Binary file not shown.
BIN
flask_session/3eafc0e8b084dcd91b81fbed0438bade
Normal file
BIN
flask_session/3eafc0e8b084dcd91b81fbed0438bade
Normal file
Binary file not shown.
BIN
flask_session/4661f790eca664fbdf71fa25c7c45a1e
Normal file
BIN
flask_session/4661f790eca664fbdf71fa25c7c45a1e
Normal file
Binary file not shown.
BIN
flask_session/4c88942d540c4b0697beb6fc71d0e515
Normal file
BIN
flask_session/4c88942d540c4b0697beb6fc71d0e515
Normal file
Binary file not shown.
BIN
flask_session/4feab7ace0e99d4bd4371560894aaa18
Normal file
BIN
flask_session/4feab7ace0e99d4bd4371560894aaa18
Normal file
Binary file not shown.
BIN
flask_session/543c69174ee1c6787e710d1656d1672a
Normal file
BIN
flask_session/543c69174ee1c6787e710d1656d1672a
Normal file
Binary file not shown.
BIN
flask_session/5ef0a832f26e148c61d14e95f03b6a33
Normal file
BIN
flask_session/5ef0a832f26e148c61d14e95f03b6a33
Normal file
Binary file not shown.
BIN
flask_session/66d1db5c2c79975bb6a80d14fa67dbfe
Normal file
BIN
flask_session/66d1db5c2c79975bb6a80d14fa67dbfe
Normal file
Binary file not shown.
BIN
flask_session/6c904875ff094c84e40eb4cb5ba58586
Normal file
BIN
flask_session/6c904875ff094c84e40eb4cb5ba58586
Normal file
Binary file not shown.
BIN
flask_session/6cafcac3319918665aaf1772f04b8432
Normal file
BIN
flask_session/6cafcac3319918665aaf1772f04b8432
Normal file
Binary file not shown.
BIN
flask_session/6f977085e9c72d687e7a132ccfbf6f88
Normal file
BIN
flask_session/6f977085e9c72d687e7a132ccfbf6f88
Normal file
Binary file not shown.
BIN
flask_session/6fecf9b935f7299d56f4a6324bc1b56c
Normal file
BIN
flask_session/6fecf9b935f7299d56f4a6324bc1b56c
Normal file
Binary file not shown.
BIN
flask_session/70195a82b9eb0295dc9eb356241021e7
Normal file
BIN
flask_session/70195a82b9eb0295dc9eb356241021e7
Normal file
Binary file not shown.
BIN
flask_session/777d3de3359fadd58752eff37b828bd3
Normal file
BIN
flask_session/777d3de3359fadd58752eff37b828bd3
Normal file
Binary file not shown.
BIN
flask_session/7e37cff998297dd71a9e4b115e258f12
Normal file
BIN
flask_session/7e37cff998297dd71a9e4b115e258f12
Normal file
Binary file not shown.
BIN
flask_session/870fe179fe41bf8bd2828724b80ca924
Normal file
BIN
flask_session/870fe179fe41bf8bd2828724b80ca924
Normal file
Binary file not shown.
BIN
flask_session/8f3bbee347151cfa1f698678c74250be
Normal file
BIN
flask_session/8f3bbee347151cfa1f698678c74250be
Normal file
Binary file not shown.
BIN
flask_session/90f85e48b946465ec01b5b9f7883ebc9
Normal file
BIN
flask_session/90f85e48b946465ec01b5b9f7883ebc9
Normal file
Binary file not shown.
BIN
flask_session/95231a9ec2e3d104ce75bd0dd780d736
Normal file
BIN
flask_session/95231a9ec2e3d104ce75bd0dd780d736
Normal file
Binary file not shown.
BIN
flask_session/95bb16dcb3c2e0f7a547ef0127e36165
Normal file
BIN
flask_session/95bb16dcb3c2e0f7a547ef0127e36165
Normal file
Binary file not shown.
BIN
flask_session/9b87431787e796d1ee5b357cca183fff
Normal file
BIN
flask_session/9b87431787e796d1ee5b357cca183fff
Normal file
Binary file not shown.
BIN
flask_session/a7a737b402857b6db6a1d193fa8e823d
Normal file
BIN
flask_session/a7a737b402857b6db6a1d193fa8e823d
Normal file
Binary file not shown.
BIN
flask_session/b8202bf4ccda274d60e685c1d1f84ad2
Normal file
BIN
flask_session/b8202bf4ccda274d60e685c1d1f84ad2
Normal file
Binary file not shown.
BIN
flask_session/be31830b82505a6516b3904fdc7aeddf
Normal file
BIN
flask_session/be31830b82505a6516b3904fdc7aeddf
Normal file
Binary file not shown.
BIN
flask_session/c1026cac75c91141eaf02c773ef7e6a4
Normal file
BIN
flask_session/c1026cac75c91141eaf02c773ef7e6a4
Normal file
Binary file not shown.
BIN
flask_session/cbfce63dc9c13b155891bad9f6d33232
Normal file
BIN
flask_session/cbfce63dc9c13b155891bad9f6d33232
Normal file
Binary file not shown.
BIN
flask_session/ceaa5b464cc4cd0d247143a82f99a170
Normal file
BIN
flask_session/ceaa5b464cc4cd0d247143a82f99a170
Normal file
Binary file not shown.
BIN
flask_session/cee8af23a1270f20c6b07ab921837c69
Normal file
BIN
flask_session/cee8af23a1270f20c6b07ab921837c69
Normal file
Binary file not shown.
BIN
flask_session/d1ab18f8f028a3781bf8a8cffc4d4f24
Normal file
BIN
flask_session/d1ab18f8f028a3781bf8a8cffc4d4f24
Normal file
Binary file not shown.
BIN
flask_session/dfb04ede4cc875e045b57e7e1d40d602
Normal file
BIN
flask_session/dfb04ede4cc875e045b57e7e1d40d602
Normal file
Binary file not shown.
BIN
flask_session/e1f855cdb1992a356fe2d07262ff56a1
Normal file
BIN
flask_session/e1f855cdb1992a356fe2d07262ff56a1
Normal file
Binary file not shown.
BIN
flask_session/e58189118050886d22b23b51e8651460
Normal file
BIN
flask_session/e58189118050886d22b23b51e8651460
Normal file
Binary file not shown.
BIN
flask_session/e6d467bfcddb3a16f2ad5a28d8bd7d36
Normal file
BIN
flask_session/e6d467bfcddb3a16f2ad5a28d8bd7d36
Normal file
Binary file not shown.
BIN
flask_session/e7f114dcf7f3868fb06d972494494b13
Normal file
BIN
flask_session/e7f114dcf7f3868fb06d972494494b13
Normal file
Binary file not shown.
BIN
flask_session/ee13c570a79dffb0127559fd9e0dfca7
Normal file
BIN
flask_session/ee13c570a79dffb0127559fd9e0dfca7
Normal file
Binary file not shown.
BIN
flask_session/f93924e17b9f01a5729553789b9857a5
Normal file
BIN
flask_session/f93924e17b9f01a5729553789b9857a5
Normal file
Binary file not shown.
BIN
flask_session/fba9a66c8591f2a7d6baee089322217e
Normal file
BIN
flask_session/fba9a66c8591f2a7d6baee089322217e
Normal file
Binary file not shown.
BIN
flask_session/feca8e10607ef23e8d1acfd652b20ac2
Normal file
BIN
flask_session/feca8e10607ef23e8d1acfd652b20ac2
Normal file
Binary file not shown.
@ -1,31 +0,0 @@
|
||||
import boto3
|
||||
from botocore.exceptions import NoCredentialsError, ClientError
|
||||
|
||||
# 使用获取的访问密钥ID和秘密访问密钥来初始化S3客户端
|
||||
aws_access_key_id = 'AKIAZQ3DT3KLN5WGXZOR' # 替换为你的访问密钥ID
|
||||
aws_secret_access_key = '5UZb8SovTrbroT7yU1pBzaR5myLn+NMA+c87RvLH' # 替换为你的秘密访问密钥
|
||||
region_name = 'ap-northeast-1' # 替换为你的S3存储桶所在区域
|
||||
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
region_name=region_name
|
||||
)
|
||||
|
||||
# 创建一个模拟“文件夹”的空对象
|
||||
def create_s3_folder(bucket_name, folder_name):
|
||||
try:
|
||||
s3_client.put_object(Bucket=bucket_name, Key=(folder_name + '/'))
|
||||
print(f"Folder {folder_name} created in bucket {bucket_name}")
|
||||
except NoCredentialsError:
|
||||
print("Credentials not available")
|
||||
except ClientError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Bucket name and folder name
|
||||
bucket_name = 'sure-ae-upload'
|
||||
folder_name = 'sure_homework_define_by_qin'
|
||||
|
||||
# Create folder
|
||||
create_s3_folder(bucket_name, folder_name)
|
2
generate_secret_key.py
Normal file
2
generate_secret_key.py
Normal file
@ -0,0 +1,2 @@
|
||||
import secrets
|
||||
print(secrets.token_hex(16)) # 生成一个32字符长的随机字符串
|
342
index.html
342
index.html
@ -1,342 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>秀儿文件提交系统</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.image-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.form-field {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="text"], select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.file-input-button {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
.file-input-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.file-input-button input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
#file-name {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
button[type="submit"] {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button[type="submit"]:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
.progress-container {
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
.progress {
|
||||
height: 10px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
width: 0;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
#progress-percentage {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#status {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
#download-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
text-decoration: none;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.upload-stats {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.upload-stats span {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<img src="image.jpg" alt="描述性文本">
|
||||
</div>
|
||||
<h1>秀儿文件提交系统</h1>
|
||||
<form id="upload-form">
|
||||
<div class="form-field">
|
||||
<label for="student">学生姓名:</label>
|
||||
<input type="text" id="student" name="student" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="student-id">学号:</label>
|
||||
<input type="text" id="student-id" name="student-id" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="assignment">选择作业:</label>
|
||||
<select id="assignment" name="assignment" required>
|
||||
<option value="">请选择作业</option>
|
||||
<option value="homework1">平时作业1</option>
|
||||
<option value="homework2">平时作业2</option>
|
||||
<option value="homework3">平时作业3</option>
|
||||
<option value="final_homework">结课大作业</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="file">选择文件:</label>
|
||||
<div class="file-input-wrapper">
|
||||
<button type="button" class="file-input-button">选择文件 <input type="file" id="file" name="file" required></button>
|
||||
</div>
|
||||
<div id="file-name">未选择文件</div>
|
||||
</div>
|
||||
<button type="submit">提交 (截止日期: 2024.07.03)</button>
|
||||
</form>
|
||||
<div id="status"></div>
|
||||
<div class="progress-container" id="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-percentage">0%</div>
|
||||
<div class="upload-stats">
|
||||
<span id="upload-speed">速度: 0 KB/s</span>
|
||||
<span id="upload-size">0 KB / 0 KB</span>
|
||||
<span id="upload-time">剩余时间: 计算中</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/download-submissions" id="download-link">下载统计表格</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('.file-input-button').addEventListener('click', function() {
|
||||
document.getElementById('file').click();
|
||||
});
|
||||
|
||||
document.getElementById('file').addEventListener('change', function(e) {
|
||||
const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件';
|
||||
document.getElementById('file-name').textContent = fileName;
|
||||
});
|
||||
|
||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const student = document.getElementById('student').value;
|
||||
const studentId = document.getElementById('student-id').value;
|
||||
const assignment = document.getElementById('assignment').value;
|
||||
const file = document.getElementById('file').files[0];
|
||||
const statusDiv = document.getElementById('status');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressPercentage = document.getElementById('progress-percentage');
|
||||
const uploadSpeed = document.getElementById('upload-speed');
|
||||
const uploadSize = document.getElementById('upload-size');
|
||||
const uploadTime = document.getElementById('upload-time');
|
||||
|
||||
if (!student || !studentId || !assignment || !file) {
|
||||
alert('请提供学生姓名、学号、选择作业,并选择一个文件。');
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.textContent = '准备上传...';
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&assignment=${encodeURIComponent(assignment)}&filename=${encodeURIComponent(file.name)}&content_type=${encodeURIComponent(file.type)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`生成预签名URL失败: ${errorData.error}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.url) {
|
||||
throw new Error('服务器未返回URL');
|
||||
}
|
||||
|
||||
statusDiv.textContent = '正在上传文件...';
|
||||
|
||||
const totalSize = file.size;
|
||||
let uploadedSize = 0;
|
||||
let startTime = Date.now();
|
||||
|
||||
const updateProgress = (additionalProgress = 0) => {
|
||||
uploadedSize += additionalProgress;
|
||||
const percentComplete = (uploadedSize / totalSize) * 100;
|
||||
const elapsedTime = (Date.now() - startTime) / 1000; // 秒
|
||||
const speed = uploadedSize / elapsedTime;
|
||||
const remainingSize = totalSize - uploadedSize;
|
||||
const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0;
|
||||
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
progressPercentage.textContent = percentComplete.toFixed(2) + '%';
|
||||
uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`;
|
||||
uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
|
||||
uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`;
|
||||
};
|
||||
|
||||
// 模拟进度更新
|
||||
const progressInterval = setInterval(() => {
|
||||
if (uploadedSize < totalSize) {
|
||||
updateProgress(totalSize / 100); // 每次更新1%的进度
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// 实际上传
|
||||
const uploadResponse = await fetch(data.url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': data.content_type
|
||||
}
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
||||
}
|
||||
|
||||
// 确保进度达到100%
|
||||
updateProgress(totalSize - uploadedSize);
|
||||
|
||||
statusDiv.textContent = '文件上传成功';
|
||||
|
||||
// 上传成功后,记录提交信息
|
||||
const recordResponse = await fetch('/record-submission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student,
|
||||
student_id: studentId,
|
||||
assignment,
|
||||
filename: file.name
|
||||
})
|
||||
});
|
||||
|
||||
if (!recordResponse.ok) {
|
||||
const errorRecordData = await recordResponse.json();
|
||||
throw new Error(`Failed to record submission: ${errorRecordData.error}`);
|
||||
}
|
||||
|
||||
alert('文件上传并记录成功');
|
||||
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
statusDiv.textContent = `错误: ${error.message}`;
|
||||
alert('发生错误: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) {
|
||||
return '计算中';
|
||||
}
|
||||
if (seconds < 60) return Math.round(seconds) + ' 秒';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
return `${minutes} 分 ${remainingSeconds} 秒`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
342
index.txt
342
index.txt
@ -1,342 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>秀儿文件提交系统</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.image-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.form-field {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="text"], select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.file-input-button {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
.file-input-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.file-input-button input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
#file-name {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
button[type="submit"] {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button[type="submit"]:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
.progress-container {
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
.progress {
|
||||
height: 10px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
width: 0;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
#progress-percentage {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#status {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
#download-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
text-decoration: none;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.upload-stats {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.upload-stats span {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<img src="image.jpg" alt="描述性文本">
|
||||
</div>
|
||||
<h1>秀儿文件提交系统</h1>
|
||||
<form id="upload-form">
|
||||
<div class="form-field">
|
||||
<label for="student">学生姓名:</label>
|
||||
<input type="text" id="student" name="student" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="student-id">学号:</label>
|
||||
<input type="text" id="student-id" name="student-id" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="assignment">选择作业:</label>
|
||||
<select id="assignment" name="assignment" required>
|
||||
<option value="">请选择作业</option>
|
||||
<option value="homework1">平时作业1</option>
|
||||
<option value="homework2">平时作业2</option>
|
||||
<option value="homework3">平时作业3</option>
|
||||
<option value="final_homework">结课大作业</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="file">选择文件:</label>
|
||||
<div class="file-input-wrapper">
|
||||
<button type="button" class="file-input-button">选择文件 <input type="file" id="file" name="file" required></button>
|
||||
</div>
|
||||
<div id="file-name">未选择文件</div>
|
||||
</div>
|
||||
<button type="submit">提交 (截止日期: 2024.07.03)</button>
|
||||
</form>
|
||||
<div id="status"></div>
|
||||
<div class="progress-container" id="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-percentage">0%</div>
|
||||
<div class="upload-stats">
|
||||
<span id="upload-speed">速度: 0 KB/s</span>
|
||||
<span id="upload-size">0 KB / 0 KB</span>
|
||||
<span id="upload-time">剩余时间: 计算中</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/download-submissions" id="download-link">下载统计表格</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('.file-input-button').addEventListener('click', function() {
|
||||
document.getElementById('file').click();
|
||||
});
|
||||
|
||||
document.getElementById('file').addEventListener('change', function(e) {
|
||||
const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件';
|
||||
document.getElementById('file-name').textContent = fileName;
|
||||
});
|
||||
|
||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const student = document.getElementById('student').value;
|
||||
const studentId = document.getElementById('student-id').value;
|
||||
const assignment = document.getElementById('assignment').value;
|
||||
const file = document.getElementById('file').files[0];
|
||||
const statusDiv = document.getElementById('status');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressPercentage = document.getElementById('progress-percentage');
|
||||
const uploadSpeed = document.getElementById('upload-speed');
|
||||
const uploadSize = document.getElementById('upload-size');
|
||||
const uploadTime = document.getElementById('upload-time');
|
||||
|
||||
if (!student || !studentId || !assignment || !file) {
|
||||
alert('请提供学生姓名、学号、选择作业,并选择一个文件。');
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.textContent = '准备上传...';
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&assignment=${encodeURIComponent(assignment)}&filename=${encodeURIComponent(file.name)}&content_type=${encodeURIComponent(file.type)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`生成预签名URL失败: ${errorData.error}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.url) {
|
||||
throw new Error('服务器未返回URL');
|
||||
}
|
||||
|
||||
statusDiv.textContent = '正在上传文件...';
|
||||
|
||||
const totalSize = file.size;
|
||||
let uploadedSize = 0;
|
||||
let startTime = Date.now();
|
||||
|
||||
const updateProgress = (additionalProgress = 0) => {
|
||||
uploadedSize += additionalProgress;
|
||||
const percentComplete = (uploadedSize / totalSize) * 100;
|
||||
const elapsedTime = (Date.now() - startTime) / 1000; // 秒
|
||||
const speed = uploadedSize / elapsedTime;
|
||||
const remainingSize = totalSize - uploadedSize;
|
||||
const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0;
|
||||
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
progressPercentage.textContent = percentComplete.toFixed(2) + '%';
|
||||
uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`;
|
||||
uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
|
||||
uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`;
|
||||
};
|
||||
|
||||
// 模拟进度更新
|
||||
const progressInterval = setInterval(() => {
|
||||
if (uploadedSize < totalSize) {
|
||||
updateProgress(totalSize / 100); // 每次更新1%的进度
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// 实际上传
|
||||
const uploadResponse = await fetch(data.url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': data.content_type
|
||||
}
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
||||
}
|
||||
|
||||
// 确保进度达到100%
|
||||
updateProgress(totalSize - uploadedSize);
|
||||
|
||||
statusDiv.textContent = '文件上传成功';
|
||||
|
||||
// 上传成功后,记录提交信息
|
||||
const recordResponse = await fetch('/record-submission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student,
|
||||
student_id: studentId,
|
||||
assignment,
|
||||
filename: file.name
|
||||
})
|
||||
});
|
||||
|
||||
if (!recordResponse.ok) {
|
||||
const errorRecordData = await recordResponse.json();
|
||||
throw new Error(`Failed to record submission: ${errorRecordData.error}`);
|
||||
}
|
||||
|
||||
alert('文件上传并记录成功');
|
||||
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
statusDiv.textContent = `错误: ${error.message}`;
|
||||
alert('发生错误: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) {
|
||||
return '计算中';
|
||||
}
|
||||
if (seconds < 60) return Math.round(seconds) + ' 秒';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
return `${minutes} 分 ${remainingSeconds} 秒`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
39
insert_data_to_database.py
Normal file
39
insert_data_to_database.py
Normal file
@ -0,0 +1,39 @@
|
||||
import json
|
||||
import mysql.connector
|
||||
from flask_bcrypt import Bcrypt
|
||||
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
# 读取 JSON 数据
|
||||
with open('students_config.json', 'r', encoding='utf-8') as f:
|
||||
students_config = json.load(f)
|
||||
|
||||
# 连接到 MySQL 数据库
|
||||
conn = mysql.connector.connect(
|
||||
host='8.218.165.242',
|
||||
user='sure_001',
|
||||
password='EKKWLMmrGmG7sdPf',
|
||||
database='sure_001'
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建 students 表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# 插入数据并哈希默认密码
|
||||
default_password_hash = bcrypt.generate_password_hash('skd123456').decode('utf-8')
|
||||
for student in students_config['students']:
|
||||
cursor.execute('INSERT INTO students (id, name, email, password) VALUES (%s, %s, %s, %s)',
|
||||
(student['id'], student['name'], student['email'], default_password_hash))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
16
main.py
16
main.py
@ -1,16 +0,0 @@
|
||||
# 这是一个示例 Python 脚本。
|
||||
|
||||
# 按 ⌃R 执行或将其替换为您的代码。
|
||||
# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
|
||||
|
||||
|
||||
def print_hi(name):
|
||||
# 在下面的代码行中使用断点来调试脚本。
|
||||
print(f'Hi, {name}') # 按 ⌘F8 切换断点。
|
||||
|
||||
|
||||
# 按间距中的绿色按钮以运行脚本。
|
||||
if __name__ == '__main__':
|
||||
print_hi('PyCharm')
|
||||
|
||||
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
|
BIN
output_files_content.docx
Normal file
BIN
output_files_content.docx
Normal file
Binary file not shown.
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Flask
|
||||
flask-cors
|
||||
flask-mail
|
||||
Flask-Bcrypt
|
||||
Flask-Session
|
||||
mysql-connector-python
|
||||
boto3
|
||||
botocore
|
||||
python-dotenv
|
||||
pytz
|
||||
pandas
|
||||
docx~=0.2.4
|
1
scp_to_remote_host.txt
Normal file
1
scp_to_remote_host.txt
Normal file
@ -0,0 +1 @@
|
||||
scp -r /Users/lishunqin/Desktop/study/pychram\ project/AWS-sure root@8.218.165.242:/root
|
99
static/admin.css
Normal file
99
static/admin.css
Normal file
@ -0,0 +1,99 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="date"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
float: right;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
119
static/base.css
Normal file
119
static/base.css
Normal file
@ -0,0 +1,119 @@
|
||||
/* base.css */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: #111;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
input[type="date"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #5cb85c;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #4cae4c;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
float: right;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background: url('data:image/svg+xml;utf8,<svg x...') no-repeat right .75rem center / 14px 12px;
|
||||
}
|
38
static/class_detail.js
Normal file
38
static/class_detail.js
Normal file
@ -0,0 +1,38 @@
|
||||
function editAssignment(assignmentId, currentDeadline) {
|
||||
const newDeadline = prompt("请输入新的截止日期(格式:YYYY-MM-DD):", currentDeadline);
|
||||
if (newDeadline) {
|
||||
fetch(`/teacher/edit_assignment/${assignmentId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ deadline: newDeadline })
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
alert('截止日期已更新');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('更新失败');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAssignment(assignmentId) {
|
||||
if (confirm("您确定要删除这个作业吗?")) {
|
||||
fetch(`/teacher/delete_assignment/${assignmentId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
alert('作业已删除');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('删除失败');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
}
|
BIN
static/dogking.jpg
Normal file
BIN
static/dogking.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 199 KiB |
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
312
static/index.css
Normal file
312
static/index.css
Normal file
@ -0,0 +1,312 @@
|
||||
:root {
|
||||
--primary-color: #1890ff;
|
||||
--secondary-color: #40a9ff;
|
||||
--background-color: #f0f2f5;
|
||||
--text-color: #333;
|
||||
--border-color: #ddd;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.image-container {
|
||||
margin-bottom: 30px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-container:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-color);
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input-button {
|
||||
background-color: #f0f0f0;
|
||||
border: 2px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
padding: 12px 15px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-input-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-input-button input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#file-name {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button[type="submit"], button[id="verify-submit-button"] {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
margin-top: 25px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover, button[id="verify-submit-button"]:hover {
|
||||
background-color: var(--secondary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button[type="submit"]:disabled, button[id="verify-submit-button"]:disabled {
|
||||
background-color: #d3d3d3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 25px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 12px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
width: 0;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#progress-percentage {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-stats {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.upload-stats span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.verification-container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.verification-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.verification-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.download-link-button, .preview-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-size: 200% 200%;
|
||||
animation: rainbow 5s ease infinite;
|
||||
}
|
||||
|
||||
.download-link-button {
|
||||
background-image: linear-gradient(45deg, #ff9a9e, #fad0c4, #ffecd2, #fcb69f);
|
||||
}
|
||||
|
||||
.preview-button {
|
||||
background-image: linear-gradient(45deg, #a1c4fd, #c2e9fb, #d4fc79, #96e6a1);
|
||||
}
|
||||
|
||||
.download-link-button:hover, .preview-button:hover {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background-color: var(--secondary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
margin-bottom: 25px; /* 适当的间距以与表单分隔开 */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% { background-position: 0% 50% }
|
||||
50% { background-position: 100% 50% }
|
||||
100% { background-position: 0% 50% }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 30px;
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.button-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
275
static/index.js
Normal file
275
static/index.js
Normal file
@ -0,0 +1,275 @@
|
||||
document.querySelector('.file-input-button').addEventListener('click', function() {
|
||||
document.getElementById('file').click();
|
||||
});
|
||||
|
||||
document.getElementById('file').addEventListener('change', function(e) {
|
||||
const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件';
|
||||
document.getElementById('file-name').textContent = fileName;
|
||||
});
|
||||
|
||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
handleSubmission();
|
||||
});
|
||||
|
||||
document.getElementById('verify-submit-button').addEventListener('click', async () => {
|
||||
await validateCodeAndSubmit();
|
||||
});
|
||||
|
||||
async function handleSubmission() {
|
||||
const assignment = document.getElementById('assignment').value;
|
||||
const file = document.getElementById('file').files[0];
|
||||
const statusDiv = document.getElementById('status');
|
||||
|
||||
if (!assignment || !file) {
|
||||
alert('请选择作业,并选择一个文件。');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingState(true);
|
||||
|
||||
try {
|
||||
const recordResponse = await fetch('/record-submission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ assignment, filename: file.name })
|
||||
});
|
||||
|
||||
if (recordResponse.status === 401) {
|
||||
setLoadingState(false);
|
||||
document.getElementById('verification-container').style.display = 'block';
|
||||
|
||||
const emailResponse = await fetch('/generate-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ assignment })
|
||||
});
|
||||
|
||||
if (emailResponse.ok) {
|
||||
alert('作业已提交过,请输入验证码提交');
|
||||
} else {
|
||||
const errorData = await emailResponse.json();
|
||||
throw new Error(`发送验证码失败: ${errorData.error}`);
|
||||
}
|
||||
} else if (!recordResponse.ok) {
|
||||
const errorRecordData = await recordResponse.json();
|
||||
throw new Error(`记录提交信息失败: ${errorRecordData.error}`);
|
||||
} else {
|
||||
const data = await recordResponse.json();
|
||||
await uploadFile(data.upload_url, file, statusDiv);
|
||||
alert('文件上传并记录成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
statusDiv.textContent = `错误: ${error.message}`;
|
||||
alert('发生错误: ' + error.message);
|
||||
} finally {
|
||||
setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateCodeAndSubmit() {
|
||||
const assignment = document.getElementById('assignment').value;
|
||||
const file = document.getElementById('file').files[0];
|
||||
const code = document.getElementById('verification-code').value;
|
||||
const statusDiv = document.getElementById('status');
|
||||
|
||||
setLoadingState(true, true);
|
||||
|
||||
try {
|
||||
const validateResponse = await fetch('/validate-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ assignment, code })
|
||||
});
|
||||
|
||||
if (validateResponse.ok) {
|
||||
const { upload_url } = await validateResponse.json();
|
||||
await uploadFile(upload_url, file, statusDiv);
|
||||
alert('文件上传并记录成功');
|
||||
document.getElementById('verification-container').style.display = 'none';
|
||||
} else {
|
||||
const errorData = await validateResponse.json();
|
||||
throw new Error(`验证码错误: ${errorData.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
statusDiv.textContent = `错误: ${error.message}`;
|
||||
alert('发生错误: ' + error.message);
|
||||
} finally {
|
||||
setLoadingState(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile(upload_url, file, statusDiv) {
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressPercentage = document.getElementById('progress-percentage');
|
||||
const uploadSpeed = document.getElementById('upload-speed');
|
||||
const uploadSize = document.getElementById('upload-size');
|
||||
const uploadTime = document.getElementById('upload-time');
|
||||
|
||||
statusDiv.textContent = '正在上传文件...';
|
||||
const totalSize = file.size;
|
||||
let uploadedSize = 0;
|
||||
let startTime = Date.now();
|
||||
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
const updateProgress = (additionalProgress = 0) => {
|
||||
uploadedSize += additionalProgress;
|
||||
const percentComplete = (uploadedSize / totalSize) * 100;
|
||||
const elapsedTime = (Date.now() - startTime) / 1000;
|
||||
const speed = uploadedSize / elapsedTime;
|
||||
const remainingSize = totalSize - uploadedSize;
|
||||
const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0;
|
||||
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
progressPercentage.textContent = percentComplete.toFixed(2) + '%';
|
||||
uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`;
|
||||
uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
|
||||
uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`;
|
||||
};
|
||||
|
||||
const progressInterval = setInterval(() => {
|
||||
if (uploadedSize < totalSize) {
|
||||
updateProgress(totalSize / 100);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const uploadResponse = await fetch(upload_url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`上传失败: ${uploadResponse.statusText}`);
|
||||
}
|
||||
|
||||
updateProgress(totalSize - uploadedSize);
|
||||
}
|
||||
|
||||
function setLoadingState(isLoading, isVerify = false) {
|
||||
const submitButton = document.querySelector('button[type="submit"]');
|
||||
const verifyButton = document.getElementById('verify-submit-button');
|
||||
const container = isVerify ? verifyButton.parentElement : submitButton.parentElement;
|
||||
|
||||
const loadingIndicator = container.querySelector('.loading-indicator') || document.createElement('div');
|
||||
loadingIndicator.className = 'loading-indicator';
|
||||
loadingIndicator.innerHTML = '<span>加载中...</span>';
|
||||
loadingIndicator.style.display = isLoading ? 'block' : 'none';
|
||||
|
||||
if (!loadingIndicator.parentElement) {
|
||||
container.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
if (isVerify) {
|
||||
verifyButton.disabled = isLoading;
|
||||
} else {
|
||||
submitButton.disabled = isLoading;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) {
|
||||
return '计算中';
|
||||
}
|
||||
if (seconds < 60) return Math.round(seconds) + ' 秒';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
return `${minutes} 分 ${remainingSeconds} 秒`;
|
||||
}
|
||||
|
||||
function previewTable(apiUrl, title) {
|
||||
fetch(apiUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const newWindow = window.open('', '_blank');
|
||||
newWindow.document.write('<html><head><title>' + title + '</title><style>');
|
||||
newWindow.document.write('table { width: 100%; border-collapse: collapse; margin-top: 10px; }');
|
||||
newWindow.document.write('th, td { border: 1px solid #ddd; padding: 8px; text-align: left; word-wrap: break-word; }');
|
||||
newWindow.document.write('th { background-color: #f2f2f2; }');
|
||||
newWindow.document.write('body { font-family: Arial, sans-serif; padding: 20px; }');
|
||||
newWindow.document.write('</style></head><body>');
|
||||
newWindow.document.write('<h2>' + title + '</h2>');
|
||||
|
||||
const table = newWindow.document.createElement('table');
|
||||
|
||||
if (data.length > 0) {
|
||||
let headers;
|
||||
if (title === '预览统计表格') {
|
||||
headers = ['时间', '提交的文件', '姓名', '学号', '作业'];
|
||||
} else {
|
||||
headers = ['学号', '姓名', '作业', '提交情况', '提交的文件'];
|
||||
}
|
||||
|
||||
const thead = newWindow.document.createElement('thead');
|
||||
const tr = newWindow.document.createElement('tr');
|
||||
headers.forEach((header, index) => {
|
||||
const th = newWindow.document.createElement('th');
|
||||
th.textContent = header;
|
||||
// 设置列宽
|
||||
if (title === '预览统计表格') {
|
||||
if (index === 0) th.style.width = '20%'; // 时间
|
||||
else if (index === 1) th.style.width = '30%'; // 提交的文件
|
||||
else if (index === 2) th.style.width = '15%'; // 姓名
|
||||
else if (index === 3) th.style.width = '15%'; // 学号
|
||||
else if (index === 4) th.style.width = '20%'; // 作业
|
||||
} else {
|
||||
if (index === 0) th.style.width = '15%'; // 学号
|
||||
else if (index === 1) th.style.width = '15%'; // 姓名
|
||||
else if (index === 2) th.style.width = '20%'; // 作业
|
||||
else if (index === 3) th.style.width = '15%'; // 提交情况
|
||||
else if (index === 4) th.style.width = '35%'; // 提交的文件
|
||||
}
|
||||
tr.appendChild(th);
|
||||
});
|
||||
thead.appendChild(tr);
|
||||
table.appendChild(thead);
|
||||
}
|
||||
|
||||
const tbody = newWindow.document.createElement('tbody');
|
||||
data.forEach(row => {
|
||||
const tr = newWindow.document.createElement('tr');
|
||||
if (title === '预览统计表格') {
|
||||
['时间', '提交的文件', '姓名', '学号', '作业'].forEach(key => {
|
||||
const td = newWindow.document.createElement('td');
|
||||
td.textContent = row[key] || '';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
} else {
|
||||
['学号', '姓名', '作业', '提交情况', '提交的文件'].forEach(key => {
|
||||
const td = newWindow.document.createElement('td');
|
||||
td.textContent = row[key] || '';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
newWindow.document.body.appendChild(table);
|
||||
newWindow.document.write('</body></html>');
|
||||
newWindow.document.close();
|
||||
})
|
||||
.catch(error => console.error('Error fetching data:', error));
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
ID,学生姓名,学号,提交的文件
|
||||
2024-07-02T15:38:06.844568,嗷呜呜汪汪汪钦,12345,fastgpt.txt
|
||||
2024-07-03T02:01:37.364487,王中王火腿肠,123,mao.png
|
||||
2024-07-03T02:02:02.087326,王中王火腿肠,123,《移动互联开发》软件设计考查材料.rar
|
||||
2024-07-03T02:31:23.019179,秀儿,000,SUS 304 Stainless Steel.png
|
||||
2024-07-03T02:31:55.184382,秀儿,000,阅读1.pdf
|
||||
2024-07-03T02:35:50.030507,嘿!,324,计算机网络_数据链路层.ppt
|
||||
2024-07-03T02:37:23.232007,CC,34534,"性学观止 (贺兰特·凯查杜里安, 胡颖翀) (Z-Library).pdf"
|
||||
2024-07-03T02:40:42.161975,4324,4234,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
|
||||
2024-07-03T02:42:47.614487,324,4234,男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf
|
||||
2024-07-03T02:43:01.033652,534534,543534,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
|
||||
2024-07-03T02:46:15.397526,CCC,AAA,"周易译注 (黄寿祺,张善文) (Z-Library).pdf"
|
||||
2024-07-03T02:48:26.189052,2423,423423,阅读1.pdf
|
||||
2024-07-03T03:06:53.527517,李顺,342,homework1,计算机网络_数据链路层.ppt
|
||||
2024-07-03T03:10:31.977965,Sure,324234,homework2,Sure_324234_彩虹易支付纯净源码.zip
|
||||
2024-07-03T03:15:33.951611,342,453534,homework2,342_453534_男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf
|
||||
2024-07-03T03:22:12.013752,李无天,983,homework3,李无天_983_真正全集:王阳明全集(以权威的隆庆初刻本《王文成公全书》为底本,增补2卷旧本未刊内容!王阳明的“心学哲理、文治武功、传奇人生”全在这... (Z-Library).pdf
|
||||
2024-07-03T03:24:24.223088,王刚,324,homework3,王刚_324_期末复习.pptx
|
||||
2024-07-06T19:16:46.811930,嗷呜呜0706,321,homework1,嗷呜呜0706_321_知行合一王阳明套装 (度阴山) (Z-Library).pdf
|
||||
2024-07-07T00:28:05.848994,test,432,homework1,期末复习.pptx
|
Can't render this file because it has a wrong number of fields in line 14.
|
BIN
submissions.xlsx
BIN
submissions.xlsx
Binary file not shown.
33
templates/admin_login.html
Normal file
33
templates/admin_login.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login</title>
|
||||
<link rel="stylesheet" href="/static/admin.css">
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Admin Login</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
160
templates/admin_panel.html
Normal file
160
templates/admin_panel.html
Normal file
@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理面板</title>
|
||||
<link rel="stylesheet" href="/static/admin.css">
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>管理面板</h1>
|
||||
<a href="{{ url_for('admin_logout') }}" class="logout-btn">登出</a>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<h2>添加新专业</h2>
|
||||
<form action="{{ url_for('add_department') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="department_name">专业名称:</label>
|
||||
<input type="text" id="department_name" name="name" required>
|
||||
</div>
|
||||
<button type="submit">添加专业</button>
|
||||
</form>
|
||||
|
||||
<h2>添加新年级</h2>
|
||||
<form action="{{ url_for('add_grade') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="year">年份:</label>
|
||||
<input type="number" id="year" name="year" required min="2000" max="2100">
|
||||
</div>
|
||||
<button type="submit">添加年级</button>
|
||||
</form>
|
||||
|
||||
<h2>添加新班级</h2>
|
||||
<form action="{{ url_for('add_class') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="class_name">班级名称:</label>
|
||||
<input type="text" id="class_name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="department_id">专业:</label>
|
||||
<select id="department_id" name="department_id" required>
|
||||
{% for department in departments %}
|
||||
<option value="{{ department.id }}">{{ department.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="grade_id">年级:</label>
|
||||
<select id="grade_id" name="grade_id" required>
|
||||
{% for grade in grades %}
|
||||
<option value="{{ grade.id }}">{{ grade.year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit">添加班级</button>
|
||||
</form>
|
||||
|
||||
<h2>分配教师到班级</h2>
|
||||
<form action="{{ url_for('assign_teacher') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="class_id">班级:</label>
|
||||
<select id="class_id" name="class_id" required>
|
||||
{% for class in classes %}
|
||||
<option value="{{ class.id }}">{{ class.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="teacher_id">教师:</label>
|
||||
<select id="teacher_id" name="teacher_id" required>
|
||||
{% for teacher in teachers %}
|
||||
<option value="{{ teacher.id }}">{{ teacher.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit">分配教师</button>
|
||||
</form>
|
||||
|
||||
<h2>添加新教师</h2>
|
||||
<form action="{{ url_for('add_teacher') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="teacher_name">教师姓名:</label>
|
||||
<input type="text" id="teacher_name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="teacher_email">邮箱:</label>
|
||||
<input type="email" id="teacher_email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="teacher_password">密码:</label>
|
||||
<input type="password" id="teacher_password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">添加教师</button>
|
||||
</form>
|
||||
|
||||
<h2>添加新管理员</h2>
|
||||
<form action="{{ url_for('add_administrator') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="admin_username">用户名:</label>
|
||||
<input type="text" id="admin_username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="admin_password">密码:</label>
|
||||
<input type="password" id="admin_password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="admin_teacher_id">教师:</label>
|
||||
<select id="admin_teacher_id" name="teacher_id" required>
|
||||
{% for teacher in teachers %}
|
||||
<option value="{{ teacher.id }}">{{ teacher.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit">添加管理员</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="editFormContainer" style="display: none;">
|
||||
<form id="editForm" method="POST">
|
||||
<input type="hidden" id="editId" name="id">
|
||||
<div class="form-group">
|
||||
<label for="editValue">值:</label>
|
||||
<input type="text" id="editValue" name="value" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editName">名称:</label>
|
||||
<input type="text" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editDeadline">截止日期:</label>
|
||||
<input type="date" id="editDeadline" name="deadline" required>
|
||||
</div>
|
||||
<button type="submit">更新任务</button>
|
||||
<button type="button" onclick="hideEditForm()">取消</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showEditForm(id, value, name, deadline) {
|
||||
document.getElementById('editId').value = id;
|
||||
document.getElementById('editValue').value = value;
|
||||
document.getElementById('editName').value = name;
|
||||
document.getElementById('editDeadline').value = deadline;
|
||||
document.getElementById('editForm').action = `/admin/edit_assignment/${id}`;
|
||||
document.getElementById('editFormContainer').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideEditForm() {
|
||||
document.getElementById('editFormContainer').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
79
templates/class_detail.html
Normal file
79
templates/class_detail.html
Normal file
@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>班级详情</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>班级详情</h1>
|
||||
<a href="{{ url_for('teacher_panel') }}">返回教师面板</a>
|
||||
|
||||
<h2>作业</h2>
|
||||
<form action="{{ url_for('teacher_add_assignment', class_id=class_id) }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="value">作业分值:</label>
|
||||
<input type="text" id="value" name="value" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">作业名称:</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="deadline">截止日期:</label>
|
||||
<input type="date" id="deadline" name="deadline" required>
|
||||
</div>
|
||||
<button type="submit">添加作业</button>
|
||||
</form>
|
||||
|
||||
<h2>现有作业</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分值</th>
|
||||
<th>名称</th>
|
||||
<th>截止日期</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for assignment in assignments %}
|
||||
<tr>
|
||||
<td>{{ assignment.value }}</td>
|
||||
<td>{{ assignment.name }}</td>
|
||||
<td>{{ assignment.deadline }}</td>
|
||||
<td>
|
||||
<button onclick="editAssignment({{ assignment.id }}, '{{ assignment.deadline }}')">修改</button>
|
||||
<button onclick="deleteAssignment({{ assignment.id }})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>学生</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>学生 ID</th>
|
||||
<th>学生姓名</th>
|
||||
<th>邮箱</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for student in students %}
|
||||
<tr>
|
||||
<td>{{ student.id }}</td>
|
||||
<td>{{ student.name }}</td>
|
||||
<td>{{ student.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script src="/static/class_detail.js"></script>
|
||||
</body>
|
||||
</html>
|
86
templates/index.html
Normal file
86
templates/index.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>秀儿文件提交系统</title>
|
||||
<link rel="stylesheet" href="/static/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<img src="/static/image.jpg" alt="描述性文本">
|
||||
</div>
|
||||
<!-- 增加退出按钮 -->
|
||||
<button class="logout-button" onclick="window.location.href='/logout'">退出账号</button>
|
||||
<h1>秀儿文件提交系统</h1>
|
||||
<form id="upload-form">
|
||||
<div class="form-field">
|
||||
<label for="assignment">选择作业:</label>
|
||||
<select id="assignment" name="assignment" required>
|
||||
<option value="">请选择作业</option>
|
||||
{% for assignment in assignments %}
|
||||
<option value="{{ assignment.value }}">{{ assignment.name }} (截止日期: {{ assignment.deadline }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="file">选择文件:</label>
|
||||
<div class="file-input-wrapper">
|
||||
<button type="button" class="file-input-button">选择文件 <input type="file" id="file" name="file" required></button>
|
||||
</div>
|
||||
<div id="file-name">未选择文件</div>
|
||||
</div>
|
||||
<button type="submit">提交 (截止日期: 2024.07.03)</button>
|
||||
</form>
|
||||
<div id="status"></div>
|
||||
<div class="progress-container" id="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-percentage">0%</div>
|
||||
<div class="upload-stats">
|
||||
<span id="upload-speed">速度: 0 KB/s</span>
|
||||
<span id="upload-size">0 KB / 0 KB</span>
|
||||
<span id="upload-time">剩余时间: 计算中</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 验证码输入框和按钮 -->
|
||||
<div id="verification-container" class="verification-container" style="display: none;">
|
||||
<div class="form-field">
|
||||
<label for="verification-code">输入验证码:</label>
|
||||
<input type="text" id="verification-code" class="verification-input" placeholder="请输入验证码">
|
||||
</div>
|
||||
<button id="verify-submit-button">提交验证码</button>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="action-card">
|
||||
<button class="download-link-button" onclick="window.location.href='/download-submissions'">
|
||||
<span class="button-icon">📊</span>
|
||||
下载统计表格
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<button class="download-link-button" onclick="window.location.href='/download-assignment-status'">
|
||||
<span class="button-icon">📥</span>
|
||||
下载作业提交情况
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<button class="preview-button" onclick="previewTable('/api/submissions', '预览统计表格')">
|
||||
<span class="button-icon">👁️</span>
|
||||
预览统计表格
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<button class="preview-button" onclick="previewTable('/api/assignment-status', '预览作业提交情况')">
|
||||
<span class="button-icon">🔍</span>
|
||||
预览作业提交情况
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/index.js"></script>
|
||||
</body>
|
||||
</html>
|
121
templates/login.html
Normal file
121
templates/login.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录页面</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.container:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.image-container {
|
||||
margin-bottom: 30px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.image-container:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form-field {
|
||||
margin-bottom: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
input[type="text"]:focus, input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
button[type="submit"], button[type="button"] {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
margin-top: 25px;
|
||||
font-weight: 500;
|
||||
}
|
||||
button[type="submit"]:hover, button[type="button"]:hover {
|
||||
background-color: #40a9ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<img src="../static/dogking.jpg" alt="Dog King">
|
||||
</div>
|
||||
<h1>登录</h1>
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('login') }}" method="post">
|
||||
<div class="form-field">
|
||||
<label for="student_id">学号:</label>
|
||||
<input type="text" id="student_id" name="student_id" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="password">密码:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
<button type="button" onclick="location.href='{{ url_for('reset_password') }}'">重置密码</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
131
templates/reset_password.html
Normal file
131
templates/reset_password.html
Normal file
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>重置密码</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="email"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.error, .success {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
button[type="submit"], button[type="button"] {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
margin-top: 25px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover, button[type="button"]:hover {
|
||||
background-color: #40a9ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>重置密码</h1>
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="success">{{ success }}</div>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('reset_password') }}" method="post">
|
||||
<div class="form-field">
|
||||
<label for="student_id">学号:</label>
|
||||
<input type="text" id="student_id" name="student_id" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="email">邮箱:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="code">验证码:</label>
|
||||
<input type="text" id="code" name="code">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="new_password">新密码:</label>
|
||||
<input type="password" id="new_password" name="new_password">
|
||||
</div>
|
||||
<button type="submit" name="reset_password">验证并修改密码</button>
|
||||
<button type="submit" name="send_code">发送验证码</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
34
templates/teacher_login.html
Normal file
34
templates/teacher_login.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Teacher Login</title>
|
||||
<link rel="stylesheet" href="/static/style.css"> <!-- 您可以在此处链接到您的全局样式表 -->
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Teacher Login</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form action="{{ url_for('teacher_login') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
40
templates/teacher_panel.html
Normal file
40
templates/teacher_panel.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>教师面板</title>
|
||||
<link rel="stylesheet" href="/static/teacher.css">
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>教师面板</h1>
|
||||
<a href="{{ url_for('teacher_logout') }}" class="logout-btn">登出</a>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<h2>选择班级</h2>
|
||||
<select id="class_select" onchange="navigateToClass(this.value)">
|
||||
<option value="">选择一个班级</option>
|
||||
{% for class in classes %}
|
||||
<option value="{{ class.id }}">{{ class.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- 额外内容如班级作业和学生提交将在这里显示。 -->
|
||||
</div>
|
||||
<script>
|
||||
function navigateToClass(classId) {
|
||||
if (classId) {
|
||||
window.location.href = `/teacher/class/${classId}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
~$tput_files_content.docx
Normal file
BIN
~$tput_files_content.docx
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user