diff --git a/app.py b/app.py index 56b4a6d..ffc487a 100644 --- a/app.py +++ b/app.py @@ -151,10 +151,15 @@ def decrypt_questions(encrypted_str): return decrypted.decode('utf-8') def get_exam_questions(exam): - """统一获取考试题目(自动处理加密)""" + """统一获取考试题目(自动处理加密,确保每题有id)""" if exam.is_encrypted and exam.encrypted_questions: - return json.loads(decrypt_questions(exam.encrypted_questions)) - return exam.get_questions() + questions = json.loads(decrypt_questions(exam.encrypted_questions)) + else: + questions = exam.get_questions() + for i, q in enumerate(questions): + if 'id' not in q: + q['id'] = i + 1 + return questions def teacher_required(f): @wraps(f) @@ -580,9 +585,11 @@ def exam_list(): Exam.creator_id == user.get('id') ) # 杯赛负责人和杯赛老师可见其杯赛下的所有考试(用于批改) - cup_member_contest_ids = [m.contest_id for m in ContestMembership.query.filter_by( + user_contest_memberships = ContestMembership.query.filter_by( user_id=user.get('id')).filter( - ContestMembership.role.in_(['owner', 'teacher'])).all()] + ContestMembership.role.in_(['owner', 'teacher'])).all() + cup_member_contest_ids = [m.contest_id for m in user_contest_memberships] + cup_owner_contest_ids = [m.contest_id for m in user_contest_memberships if m.role == 'owner'] if cup_member_contest_ids: base_filter = or_(base_filter, Exam.contest_id.in_(cup_member_contest_ids)) query = query.filter(base_filter) @@ -595,6 +602,15 @@ def exam_list(): ) all_exams = query.all() + now = datetime.now() + changed = False + for exam in all_exams: + if exam.status == 'available' and exam.scheduled_end and now > exam.scheduled_end: + exam.status = 'closed' + changed = True + if changed: + db.session.commit() + all_subjects = db.session.query(Exam.subject).distinct().all() all_subjects = [s[0] for s in all_subjects if s[0]] @@ -612,7 +628,9 @@ def exam_list(): user_submissions=user_submissions, search_query=search_query, subject_filter=subject_filter, - all_subjects=all_subjects + all_subjects=all_subjects, + now=now, + cup_owner_contest_ids=cup_owner_contest_ids ) @app.route('/exams/create') @@ -655,12 +673,15 @@ def exam_detail(exam_id): draft = Draft.query.filter_by(exam_id=exam_id, user_id=user.get('id')).first() questions = get_exam_questions(exam) # 检查预定时间 - now = datetime.utcnow() + now = datetime.now() schedule_status = 'available' # available, not_started, ended if exam.scheduled_start and now < exam.scheduled_start: schedule_status = 'not_started' if exam.scheduled_end and now > exam.scheduled_end: schedule_status = 'ended' + if exam.status == 'available': + exam.status = 'closed' + db.session.commit() return render_template('exam_detail.html', exam=exam, questions=questions, existing_submission=existing, draft=draft, schedule_status=schedule_status, need_password=False) @@ -677,7 +698,7 @@ def exam_result(exam_id): return redirect(url_for('exam_detail', exam_id=exam_id)) # 检查成绩公布时间 score_hidden = False - if exam.score_release_time and datetime.utcnow() < exam.score_release_time: + if exam.score_release_time and datetime.now() < exam.score_release_time: if user.get('role') != 'teacher' and user.get('role') != 'admin': score_hidden = True questions = get_exam_questions(exam) @@ -1315,6 +1336,11 @@ def api_notifications(): item['contest_name'] = contest.name if contest else '' applicant = User.query.get(ta.user_id) item['applicant_name'] = applicant.name if applicant else '' + if n.type == 'exam_submission' and n.post_id: + sub = Submission.query.get(n.post_id) + if sub: + item['exam_id'] = sub.exam_id + item['submission_id'] = sub.id if n.type == 'friend_request' and n.post_id: fr = Friend.query.get(n.post_id) if fr: @@ -1362,7 +1388,7 @@ def api_delete_notification(nid): @app.route('/notifications') @login_required def notifications_page(): - return redirect(url_for('chat')) + return redirect(url_for('chat_page')) # ========== 系统公告管理 API ========== @app.route('/api/system-notifications') @@ -2753,7 +2779,7 @@ def api_submit_exam(exam_id): if exam.status == 'closed': return jsonify({'success': False, 'message': '该考试已关闭'}), 400 # 检查预定时间 - now = datetime.utcnow() + now = datetime.now() if exam.scheduled_start and now < exam.scheduled_start: return jsonify({'success': False, 'message': '考试尚未开始'}), 400 if exam.scheduled_end and now > exam.scheduled_end: @@ -2761,73 +2787,78 @@ def api_submit_exam(exam_id): if Submission.query.filter_by(exam_id=exam_id, user_id=user.get('id')).first(): return jsonify({'success': False, 'message': '您已提交过该试卷'}), 400 data = request.get_json(force=True, silent=True) + if not data: + return jsonify({'success': False, 'message': '无效的请求数据'}), 400 answers = data.get('answers', {}) - score = 0 - auto_graded = True - question_scores = {} - questions = get_exam_questions(exam) - - # AI批改客观题 - for q in questions: - qid = str(q['id']) - if q['type'] == 'choice': - # 选择题:精确匹配 - if answers.get(qid) == q.get('answer'): - score += q.get('score', 0) - question_scores[qid] = q.get('score', 0) + try: + score = 0 + auto_graded = True + question_scores = {} + questions = get_exam_questions(exam) + + for q in questions: + qid = str(q.get('id', '')) + q_score = q.get('score', 0) or 0 + if q.get('type') == 'choice': + if answers.get(qid) == q.get('answer'): + score += q_score + question_scores[qid] = q_score + else: + question_scores[qid] = 0 + elif q.get('type') == 'fill': + student_answer = (answers.get(qid) or '').strip() + correct_answer = (q.get('answer') or '').strip() + + correct_answers = [a.strip() for a in correct_answer.split('|')] if correct_answer else [] + if correct_answers and student_answer in correct_answers: + score += q_score + question_scores[qid] = q_score + elif correct_answers and student_answer.lower() in [a.lower() for a in correct_answers]: + score += q_score + question_scores[qid] = q_score + else: + auto_graded = False + question_scores[qid] = 0 + elif q.get('type') == 'judge': + if answers.get(qid) == q.get('answer'): + score += q_score + question_scores[qid] = q_score + else: + question_scores[qid] = 0 else: + auto_graded = False question_scores[qid] = 0 - elif q['type'] == 'fill': - # 填空题:使用AI智能判断 - student_answer = answers.get(qid, '').strip() - correct_answer = q.get('answer', '').strip() - - # 先尝试精确匹配(支持多个正确答案,用|分隔) - correct_answers = [a.strip() for a in correct_answer.split('|')] - if student_answer in correct_answers: - score += q.get('score', 0) - question_scores[qid] = q.get('score', 0) - else: - # 使用AI判断答案是否正确 - ai_score = ai_grade_fill_question(q.get('content', ''), correct_answer, student_answer, q.get('score', 0)) - score += ai_score - question_scores[qid] = ai_score - elif q['type'] == 'judge': - # 判断题:精确匹配 - if answers.get(qid) == q.get('answer'): - score += q.get('score', 0) - question_scores[qid] = q.get('score', 0) - else: - question_scores[qid] = 0 - else: - # 主观题需要人工批改 - auto_graded = False - question_scores[qid] = 0 - - sub = Submission( - exam_id=exam_id, - user_id=user.get('id'), - score=score, - graded=auto_graded, - graded_by='AI自动批改' if auto_graded else '' - ) - sub.set_answers(answers) - sub.set_question_scores(question_scores) - db.session.add(sub) - Draft.query.filter_by(exam_id=exam_id, user_id=user.get('id')).delete() - db.session.commit() + + sub = Submission( + exam_id=exam_id, + user_id=user.get('id'), + score=score, + graded=auto_graded, + graded_by='AI自动批改' if auto_graded else '' + ) + sub.set_answers(answers) + sub.set_question_scores(question_scores) + db.session.add(sub) + Draft.query.filter_by(exam_id=exam_id, user_id=user.get('id')).delete() + db.session.commit() + except Exception as e: + db.session.rollback() + app.logger.error(f'考试提交失败: exam_id={exam_id}, user_id={user.get("id")}, error={str(e)}') + return jsonify({'success': False, 'message': '提交处理失败,请重试'}), 500 # 如果有主观题,通知老师批改 - if not auto_graded and exam.contest_id: - contest = Contest.query.get(exam.contest_id) - if contest: - # 通知杯赛负责人和老师 - memberships = ContestMembership.query.filter_by(contest_id=exam.contest_id).all() - for m in memberships: - if m.role in ('owner', 'teacher'): - add_notification(m.user_id, 'exam_submission', - f'考生 {user.get("name")} 提交了考试「{exam.title}」,请批改主观题。', - from_user=user.get('name', ''), post_id=sub.id) + try: + if not auto_graded and exam.contest_id: + contest = Contest.query.get(exam.contest_id) + if contest: + memberships = ContestMembership.query.filter_by(contest_id=exam.contest_id).all() + for m in memberships: + if m.role in ('owner', 'teacher'): + add_notification(m.user_id, 'exam_submission', + f'考生 {user.get("name")} 提交了考试「{exam.title}」,请批改主观题。', + from_user=user.get('name', ''), post_id=sub.id) + except Exception: + pass return jsonify({'success': True, 'message': '提交成功', 'submission_id': sub.id}) @@ -2861,13 +2892,23 @@ def api_grade_submission(exam_id, sub_id): @app.route('/api/exams//status', methods=['POST']) def api_update_exam_status(exam_id): user = session.get('user') - if not user or user.get('role') not in ('teacher', 'admin'): + if not user: return jsonify({'success': False, 'message': '无权限'}), 403 exam = Exam.query.get(exam_id) if not exam: return jsonify({'success': False, 'message': '试卷不存在'}), 404 - if exam.creator_id != user.get('id'): - return jsonify({'success': False, 'message': '只能修改自己创建的试卷'}), 403 + has_permission = False + if user.get('role') == 'admin': + has_permission = True + elif user.get('role') == 'teacher' and exam.creator_id == user.get('id'): + has_permission = True + elif exam.contest_id: + membership = ContestMembership.query.filter_by( + user_id=user['id'], contest_id=exam.contest_id).first() + if membership and membership.role == 'owner': + has_permission = True + if not has_permission: + return jsonify({'success': False, 'message': '无权限修改该试卷状态'}), 403 data = request.get_json(force=True, silent=True) new_status = data.get('status', '') if new_status not in ('available', 'closed'): @@ -2915,13 +2956,23 @@ def api_update_exam_visibility(exam_id): @app.route('/api/exams/', methods=['DELETE']) def api_delete_exam(exam_id): user = session.get('user') - if not user or user.get('role') not in ('teacher', 'admin'): + if not user: return jsonify({'success': False, 'message': '无权限'}), 403 exam = Exam.query.get(exam_id) if not exam: return jsonify({'success': False, 'message': '试卷不存在'}), 404 - if exam.creator_id != user.get('id'): - return jsonify({'success': False, 'message': '只能删除自己创建的试卷'}), 403 + has_permission = False + if user.get('role') == 'admin': + has_permission = True + elif user.get('role') == 'teacher' and exam.creator_id == user.get('id'): + has_permission = True + elif exam.contest_id: + membership = ContestMembership.query.filter_by( + user_id=user['id'], contest_id=exam.contest_id).first() + if membership and membership.role == 'owner': + has_permission = True + if not has_permission: + return jsonify({'success': False, 'message': '无权限删除该试卷'}), 403 Submission.query.filter_by(exam_id=exam_id).delete() Draft.query.filter_by(exam_id=exam_id).delete() db.session.delete(exam) @@ -2993,6 +3044,14 @@ def api_contest_exams(contest_id): if user: membership = ContestMembership.query.filter_by(user_id=user['id'], contest_id=contest_id).first() is_owner = (membership and membership.role == 'owner') or user.get('role') == 'admin' + now = datetime.now() + changed = False + for e in exams: + if e.status == 'available' and e.scheduled_end and now > e.scheduled_end: + e.status = 'closed' + changed = True + if changed: + db.session.commit() data = [] for e in exams: item = { @@ -3234,11 +3293,14 @@ def api_create_exam_from_bank(contest_id): return jsonify({'success': False, 'message': '请填写标题并选择题目'}), 400 # 从题库获取题目并转换为考试题目格式 questions = [] + q_idx = 0 for qid in question_ids: item = QuestionBankItem.query.get(qid) if not item or item.contest_id != contest_id: continue + q_idx += 1 q = { + 'id': q_idx, 'type': item.type, 'content': item.content, 'score': item.score, diff --git a/templates/chat.html b/templates/chat.html index 65b94d6..f410423 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -80,7 +80,7 @@ -