Add app_function.py
This commit is contained in:
		
							parent
							
								
									1760f36f7b
								
							
						
					
					
						commit
						32cbad1698
					
				
							
								
								
									
										924
									
								
								app_function.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										924
									
								
								app_function.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,924 @@ | ||||
| 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 | ||||
| import tempfile | ||||
| import shutil | ||||
| import zipfile | ||||
| import time | ||||
| from urllib.parse import quote | ||||
| 
 | ||||
| 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 download_and_zip_files(bucket_name, folder_key): | ||||
|     """下载文件并进行支持 ZIP64 的压缩打包""" | ||||
|     temp_dir = tempfile.mkdtemp()  # 创建一个临时目录 | ||||
|     zip_file_path = os.path.join(temp_dir, 'homework.zip') | ||||
| 
 | ||||
|     try: | ||||
|         # 列出文件 | ||||
|         file_keys = list_files_in_folder(bucket_name, folder_key) | ||||
| 
 | ||||
|         if not file_keys: | ||||
|             logging.error(f"No files found in folder: {folder_key}") | ||||
|             return None | ||||
| 
 | ||||
|         # 下载文件 | ||||
|         for file_key in file_keys: | ||||
|             file_name = os.path.basename(file_key) | ||||
|             file_download_path = os.path.join(temp_dir, file_name) | ||||
| 
 | ||||
|             # 下载文件 | ||||
|             logging.info(f"Downloading {file_key} to {file_download_path}") | ||||
|             s3_client.download_file(bucket_name, file_key, file_download_path) | ||||
|          | ||||
|         # ZIP 压缩过程 | ||||
|         with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zipf: | ||||
|             for root, _, files in os.walk(temp_dir): | ||||
|                 for file in files: | ||||
|                     file_path = os.path.join(root, file) | ||||
| 
 | ||||
|                     # 排除自身的 ZIP 文件,防止自我压缩 | ||||
|                     if file == os.path.basename(zip_file_path): | ||||
|                         logging.info(f"Skipping self zip file: {file}") | ||||
|                         continue | ||||
|                      | ||||
|                     # 打包其它文件 | ||||
|                     arcname = os.path.relpath(file_path, start=temp_dir) | ||||
|                     logging.info(f"Adding {file} to archive as {arcname}") | ||||
|                     zipf.write(file_path, arcname=arcname) | ||||
| 
 | ||||
|         logging.info(f"Zip file created at {zip_file_path}") | ||||
|         return zip_file_path  # 返回 ZIP 文件路径 | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logging.error(f"Error during download or zip operation: {e}") | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| def upload_zip_to_s3(zip_file_path, bucket_name, object_key): | ||||
|     """上传压缩包文件到S3""" | ||||
|     try: | ||||
|         s3_client.upload_file(zip_file_path, bucket_name, object_key) | ||||
|     except ClientError as e: | ||||
|         logging.error(f"Error uploading zip to S3: {e}") | ||||
|         return None | ||||
| 
 | ||||
|     return generate_presigned_download_url(bucket_name, object_key) | ||||
| 
 | ||||
| # 列出文件 | ||||
| def list_files_in_folder(bucket_name, folder_key): | ||||
|     try: | ||||
|         response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=folder_key) | ||||
|         if 'Contents' not in response: | ||||
|             logging.error(f"No contents found in S3 folder {folder_key}") | ||||
|             return [] | ||||
|          | ||||
|         files = [obj['Key'] for obj in response['Contents'] if obj['Key'] != folder_key] | ||||
|         logging.info(f"Files in folder {folder_key}: {files}") | ||||
|         return files | ||||
|      | ||||
|     except ClientError as e: | ||||
|         logging.error(f"Error listing files in folder {folder_key}: {e}") | ||||
|         return [] | ||||
| 
 | ||||
| # 数据库连接函数 | ||||
| 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': '学号、作业和文件名是必要参数'}), 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  # 保留新文件名以备`validate-code`使用 | ||||
|             return jsonify({'error': '作业已提交过,请输入验证码继续'}), 401  # 提示用户输入验证码以覆盖提交 | ||||
| 
 | ||||
|         # 提取当前上传文件的扩展名 | ||||
|         _, file_extension = os.path.splitext(filename)  # 通过前端传递的文件提取扩展名 | ||||
| 
 | ||||
|         # 生成新的文件名,并确保文件扩展名保持正确 | ||||
|         new_filename = f'{student_id}_{student_name}_{assignment}{file_extension}' | ||||
|         folder_name = f'sure_homework_define_by_qin/{assignment}' | ||||
|         object_key = f'{folder_name}/{new_filename}' | ||||
| 
 | ||||
|         # 生成预签名 URL | ||||
|         url = generate_presigned_url(object_key, 'application/octet-stream') | ||||
| 
 | ||||
|         if not url: | ||||
|             logging.error("Failed to generate presigned URL") | ||||
|             return jsonify({'error': '无法生成预签名 URL'}), 500 | ||||
| 
 | ||||
|         # 保存提交记录 | ||||
|         add_or_update_submission(student_id, assignment, new_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') | ||||
| 
 | ||||
|         # 验证提交的验证码与 session 中的值是否匹配 | ||||
|         if (code == session.get('submission_code') and | ||||
|                 student_id == session.get('submission_student_id') and | ||||
|                 assignment == session.get('submission_assignment')): | ||||
| 
 | ||||
|             # 从 session 中获取新文件名 | ||||
|             filename = session.pop('filename', None) | ||||
| 
 | ||||
|             if not filename: | ||||
|                 return jsonify({'error': 'No file was found in session'}), 400 | ||||
| 
 | ||||
|             # 提取文件扩展名 | ||||
|             _, file_extension = os.path.splitext(filename)  # 获取文件的后缀名 | ||||
| 
 | ||||
|             # 使用学号、学生名、作业名生成文件名(不含后缀) | ||||
|             new_filename = f'{student_id}_{session.get("student_name")}_{assignment}{file_extension}' | ||||
|             folder_name = f'sure_homework_define_by_qin/{assignment}' | ||||
| 
 | ||||
|             # 生成对象键前缀(不含扩展名),用于查找已存在的文件 | ||||
|             object_key_prefix = f'{folder_name}/{student_id}_{session.get("student_name")}_{assignment}' | ||||
| 
 | ||||
|             # 删除已存在的文件(任何扩展名) | ||||
|             delete_old_files_in_s3(object_key_prefix) | ||||
| 
 | ||||
|             # 再生成新的对象键(含有扩展名) | ||||
|             object_key = f'{object_key_prefix}{file_extension}' | ||||
| 
 | ||||
|             logging.info(f"Generated object_key: {object_key}") | ||||
| 
 | ||||
|             # 生成预签名 URL | ||||
|             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, new_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 | ||||
| 
 | ||||
|     # 辅助函数:删除 S3 存储桶中具有指定前缀的旧文件(无论扩展名是何) | ||||
|     def delete_old_files_in_s3(object_key_prefix): | ||||
|         # 查找存储桶中是否已经存在具有相同前缀(不同后缀)的文件 | ||||
|         response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=object_key_prefix) | ||||
| 
 | ||||
|         if 'Contents' in response: | ||||
|             for obj in response['Contents']: | ||||
|                 s3_client.delete_object(Bucket=bucket_name, Key=obj['Key']) | ||||
|                 logging.info(f"Deleted old file from S3: {obj['Key']}") | ||||
| 
 | ||||
|     @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/download-assignment/<assignment_value>', methods=['GET']) | ||||
|     def download_assignment(assignment_value): | ||||
|         """生成 ZIP 并直接发送给用户""" | ||||
|         teacher_id = session.get('teacher_id') | ||||
|         if not teacher_id: | ||||
|             return redirect(url_for('teacher_login')) | ||||
| 
 | ||||
|         # 构造 S3 文件夹路径 | ||||
|         folder_key = f"sure_homework_define_by_qin/{assignment_value}/" | ||||
| 
 | ||||
|         # 下载并压缩作业文件夹 | ||||
|         zip_file_path = download_and_zip_files(os.getenv('S3_BUCKET_NAME'), folder_key) | ||||
| 
 | ||||
|         if not zip_file_path or not os.path.exists(zip_file_path): | ||||
|             logging.error(f"Failed to create ZIP file at {zip_file_path}") | ||||
|             return jsonify({"error": "无法创建 zip 文件"}), 500 | ||||
| 
 | ||||
|         try: | ||||
|             # 将来自 assignment_value 的文件名进行 URL 编码,以支持中文和特殊字符 | ||||
|             filename = quote(f"{assignment_value}.zip") | ||||
| 
 | ||||
|             logging.info(f"Sending file: {zip_file_path} as {filename}") | ||||
|             return send_file(zip_file_path, as_attachment=True, download_name=filename, mimetype='application/zip') | ||||
|         except Exception as ex: | ||||
|             logging.error(f"Error sending file: {str(ex)}") | ||||
|             return jsonify({"error": "Error sending file", "details": str(ex)}), 500 | ||||
|          | ||||
|     @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'}) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 superlishunqin
						superlishunqin