添加chapter和lesson

This commit is contained in:
CakeCN
2025-08-29 21:41:20 +08:00
parent 5c0187bc4b
commit f9d8b3d092
19 changed files with 736 additions and 53 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
Html/runs/*
Html/config.ini
Html/runs/*
**.pyc
# Python bytecode files

View File

@@ -52,15 +52,15 @@ def require_role(
def wrapper(*args, **kwargs):
uuid2username, users_list, injected_checker = _get_deps()
user_id = session.get("user_id")
if not user_id or uuid2username is None or user_id not in uuid2username:
user_uuid = session.get("user_uuid")
if not user_uuid or uuid2username is None or user_uuid not in uuid2username:
return redirect(url_for(login_endpoint))
# 仅需登录
if not roles:
return func(*args, **kwargs)
username = uuid2username[user_id]
username = uuid2username[user_uuid]
# 选择校验器优先级:参数 checker > app 注入的 role_checker > 内置 users_list 适配
effective_checker = checker or injected_checker

View File

@@ -35,3 +35,13 @@ class Config:
"windows_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "windows_path", fallback=""),
"wsl_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "wsl_path", fallback=""),
}
MONGO_URI = GLOBAL_CONFIG['MONGO']['uri']
TENCENT_COS = {
"secret_id": GLOBAL_CONFIG['TENCENT_COS']['secret_id'],
"secret_key": GLOBAL_CONFIG['TENCENT_COS']['secret_key'],
"bucket": GLOBAL_CONFIG['TENCENT_COS']['bucket'],
"region": GLOBAL_CONFIG['TENCENT_COS']['region'],
}

View File

@@ -5,8 +5,13 @@ from flask_cors import CORS
from db.user_list import UserList
from db.course_list import CourseList
from .services.my_function import MyFunction
from flask_pymongo import PyMongo
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
mongo = PyMongo()
db = SQLAlchemy()
socketio = SocketIO(cors_allowed_origins="*") # 不直接传 app
cors = CORS()
@@ -26,7 +31,6 @@ user_id2UserClass = {}
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
agent_manager = AgentManager()
def init_extensions(app):
db.init_app(app)
# 第三方
socketio.init_app(
app,
@@ -51,6 +55,18 @@ def init_extensions(app):
app.extensions["user_id2UserClass"] = user_id2UserClass
app.extensions["agent_manager"] = agent_manager
app.extensions["my_function"] = MyFunction()
mongo.init_app(app,uri=app.config["MONGO_URI"])
app.extensions["mongo"] = mongo
cos_config = CosConfig(
Region=app.config["TENCENT_COS"]["region"],
SecretId=app.config["TENCENT_COS"]["secret_id"],
SecretKey=app.config["TENCENT_COS"]["secret_key"],
)
cos_client = CosS3Client(cos_config)
app.extensions["cos_client"] = cos_client
def register_namespaces(app):
"""把所有 Socket.IO namespaces 注册到 socketio"""
# 延迟导入以避免循环

View File

@@ -0,0 +1,24 @@
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from bson import ObjectId
class Lesson(BaseModel):
lesson_name: str
markdown_lesson_file_link: str
markdown_prompt_file_name: str
markdown_score_prompt_file_link: str
class Chapter(BaseModel):
chapter_name: str
lessons: List[Lesson]
class Material(BaseModel):
_id: ObjectId
name: str
description: Optional[str] = None
teacher_id: str
created_at: datetime
updated_at: datetime
image_url: Optional[str] = None # 封面图片的url
chapters: List[Chapter]

View File

@@ -33,7 +33,7 @@ def login_user(username: str, password: str, *, require_teacher: bool=False):
# 设置会话 + 关联映射
user_uuid = "user_" + str(uuid.uuid4())
session["user_id"] = user_uuid
session["user_uuid"] = user_uuid
username2uuid = current_app.extensions["username2uuid"]
uuid2username = current_app.extensions["uuid2username"]
@@ -43,4 +43,4 @@ def login_user(username: str, password: str, *, require_teacher: bool=False):
return True, "登录成功"
def logout_user():
session.pop("user_id", None)
session.pop("user_uuid", None)

View File

@@ -0,0 +1,10 @@
from flask import current_app
def upload_file(rb_file, file_name):
cos_client = current_app.extensions["cos_client"]
cos_client.put_object(
Bucket=current_app.config["TENCENT_COS"]["bucket"],
Body=rb_file,
Key=file_name,
)
return f"https://{current_app.config['TENCENT_COS']['bucket']}.cos.{current_app.config['TENCENT_COS']['region']}.myqcloud.com/{file_name}"

View File

@@ -1,5 +1,10 @@
# myapp/services/course_service.py
from typing import Dict, List
from flask import current_app
from datetime import datetime
from bson import ObjectId
from ..models.material import Material
# 假设你已有这两个 loader
from db.course import load_course_from_json
@@ -18,3 +23,82 @@ def user_selected_course_briefs(user_obj):
brief = course_list.get_course_brief_info(cid, course_obj)
briefs.append(brief)
return briefs
def create_material(teacher_id, material_name, description, chapters, image_url):
material = Material(
name=material_name,
description=description,
teacher_id=teacher_id,
created_at=datetime.now(),
updated_at=datetime.now(),
chapters=chapters,
image_url=image_url
)
mongo = current_app.extensions["mongo"]
material_id = mongo.db.materials.insert_one(material.model_dump()).inserted_id
return str(material_id)
def load_material(material_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({"_id": ObjectId(material_id)})
return Material(**material)
def update_material(material_id, chapters):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters': chapters, 'updated_at': datetime.now()}}
)
return True
return False
def add_material_chapter(material_id, chapter_name, teacher_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material['teacher_id'] != teacher_id:
return False
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}})
return True
def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
mongo = current_app.extensions["mongo"]
lesson_doc = {
"lesson_name": lesson_name or "",
"markdown_lesson_file_link": "",
"markdown_prompt_file_name": "",
"markdown_score_prompt_file_link": "",
}
res = mongo.db.materials.update_one(
{
'_id': ObjectId(material_id),
'teacher_id': teacher_id,
},
{
'$push': {'chapters.$[c].lessons': lesson_doc},
'$set': {'updated_at': datetime.now()},
},
array_filters=[{'c.chapter_name': chapter_name}] # 命名占位符 c 指向命中的 chapter
)
return res.modified_count == 1
def get_materials_by_teacher(teacher_id)->List[Material]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
return [Material(**material) for material in materials]
def get_materials_by_teacher_dict(teacher_id)->List[Dict]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
result = []
for material in materials:
material['_id'] = str(material['_id'])
result.append(material)
return result

View File

@@ -1,4 +1,8 @@
/* Teacherboard 样式 */
/* 按钮样式 */
.teacherboard {
padding: 20px;
font-family: Arial, sans-serif;
@@ -62,6 +66,206 @@
z-index: 9999;
border-radius: 15px; /* 添加圆角 */
}
/* 基本的课程卡片样式 */
.course-card {
width: 150px;
height: 200px;
margin: 10px;
border: 1px solid #ddd;
border-radius: 8px;
position: relative;
overflow: hidden;
cursor: pointer;
}
.course-image {
width: 100%;
height: 100px;
object-fit: cover;
}
.course-info {
padding: 10px;
}
.course-name {
font-size: 16px;
font-weight: bold;
}
.course-description {
font-size: 14px;
color: #777;
}
/* 空的添加课程按钮卡片 */
.add-course-card {
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
border: 1px dashed #bbb;
}
/* 模态框背景 */
.modal {
display: none; /* 初始时隐藏 */
position: fixed;
z-index: 1; /* 保证浮窗在顶部 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* 半透明的黑色背景 */
}
/* 浮窗内容 */
.modal-content {
background-color: #fff;
margin: 15% auto;
padding: 20px;
border-radius: 10px;
width: 40%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 按钮样式 */
.modal-buttons {
display: flex;
justify-content: space-between;
}
/* 基本样式,保证输入框和下拉框占满整行 */
input[type="text"], select {
width: 100%; /* 占满一行 */
padding: 12px; /* 增加内边距 */
font-size: 16px; /* 调整字体大小 */
margin: 10px 0; /* 上下留间距 */
border: 2px solid #ccc; /* 边框 */
border-radius: 8px; /* 圆角边框 */
background-color: #f9f9f9; /* 背景色 */
transition: border 0.3s ease, box-shadow 0.3s ease; /* 添加过渡效果 */
}
/* 鼠标悬停时输入框的效果 */
input[type="text"]:focus, select:focus {
border-color: #4CAF50; /* 聚焦时的边框颜色 */
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5); /* 聚焦时的阴影效果 */
outline: none; /* 去掉默认的聚焦框 */
}
/* 给表单标签添加样式 */
label {
font-size: 18px; /* 增加字体大小 */
font-weight: bold; /* 加粗字体 */
margin-bottom: 8px; /* 下方间距 */
display: block; /* 使标签占一行 */
color: #333; /* 标签字体颜色 */
}
/* 按钮部分 */
.modal-buttons {
display: flex;
justify-content: space-between;
margin-top: 20px; /* 上方间距 */
}
/* 添加课程按钮样式 */
.add-course-button {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4CAF50;
color: white;
font-size: 30px;
font-weight: bold;
cursor: pointer;
}
.add-course-button:hover {
background-color: #45a049;
}
.cover-image-container {
display: flex;
flex-direction: column;
align-items: center;
}
.cover-image-placeholder {
width: 150px;
height: 150px;
border: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.modal-buttons button {
width: 48%;
padding: 12px 0;
font-size: 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.modal-buttons button:hover {
background-color: #45a049;
}
.modal-buttons button[type="button"] {
background-color: #f44336;
}
.modal-buttons button[type="button"]:hover {
background-color: #e53935;
}
.plus-sign {
font-size: 30px;
color: #888;
}
#cover-preview {
width: 150px;
height: 150px;
margin-top: 10px;
}
/* 课程描述文本框样式 */
#course-description {
width: 100%; /* 让 textarea 占据整个父容器的宽度 */
height: 150px; /* 设置适当的高度 */
padding: 10px; /* 内边距,让文本与边框之间有一定的间距 */
font-size: 16px; /* 设置字体大小 */
font-family: Arial, sans-serif; /* 字体样式 */
border: 1px solid #ddd; /* 边框颜色 */
border-radius: 8px; /* 边框圆角,使其更加圆润 */
resize: vertical; /* 允许用户垂直调整大小 */
box-sizing: border-box; /* 包含内边距和边框在内的总宽高 */
transition: border-color 0.3s, box-shadow 0.3s; /* 鼠标悬浮和聚焦时的动画效果 */
}
/* 课程描述文本框的聚焦样式 */
#course-description:focus {
border-color: #4CAF50; /* 聚焦时改变边框颜色 */
box-shadow: 0 0 8px rgba(76, 175, 80, 0.4); /* 聚焦时加上绿色的阴影 */
outline: none; /* 去掉聚焦时的默认外轮廓 */
}
/* 课程描述标签样式 */
label[for="course-description"] {
font-size: 18px; /* 标签字体大小 */
font-weight: bold; /* 加粗字体 */
margin-bottom: 8px; /* 标签与文本框之间的间距 */
display: block; /* 将标签设置为块级元素,保证换行 */
color: #333; /* 标签颜色 */
}
.course-details.open {
right: 0; /* 打开时显示 */
@@ -71,7 +275,8 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
font-size: 18px;
font-weight: bold;
}
.details-header button {
@@ -81,6 +286,39 @@
cursor: pointer;
}
.close-btn {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
}
.add-chapter-btn {
margin-top: 20px;
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.add-chapter-btn:hover {
background-color: #45a049;
}
.course-card {
border: 1px solid #ddd;
padding: 15px;
cursor: pointer;
}
.course-card:hover {
background-color: #f1f1f1;
}
#courseDetails.open {
right: 0; /* 打开侧边栏 */
}
.details-content {
padding-right: 20px;
}

View File

@@ -1,12 +1,14 @@
function lessonTemplate(lesson) {
let material_id = null;
function lessonTemplate(lesson_name) {
return `
<li class="lesson-item">
<span class="lesson-text" onclick="editLesson('${lesson}')">${lesson}
<button class="icon-btn edit-btn" onclick="editLesson('${lesson}')">
<span class="lesson-text" onclick="editLesson('${lesson_name}')">${lesson_name}
<button class="icon-btn edit-btn" onclick="editLesson('${lesson_name}')">
<i class="fas fa-pencil-alt"></i>
</button>
</span>
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson}')">
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson_name}')">
<i class="fas fa-trash-alt"></i>
</button>
</li>
@@ -77,13 +79,26 @@ function checkAndAddChapter(input) {
<ul>
</ul>
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapterName}', this)">新增课时</button>
`;
chapterList.appendChild(chapterItem);
// 恢复新增按钮的显示
document.querySelector('.add-chapter-btn').style.display = 'block';
input.remove();
isAddingChapter = false;
fetch(`/materials/addchapter/${material_id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({chapter_name: chapterName})
})
.then(response => response.json())
.then(data => {
alert(data.message);
})
.catch(error => {
console.error('Error adding chapter:', error);
});
}
function openCourseDetails(courseId) {
// 打开课程目录
@@ -104,44 +119,61 @@ function deleteChapter(deleteButton) {
function renderChapters(courseId) {
const chapterList = document.getElementById('chapter-list');
chapterList.innerHTML = ""; // 清空章节列表
material_id = courseId;
fetch(`/materials/${courseId}`)
.then(response => response.json())
.then(data => {
document.getElementById('course-title').innerText = data.name;
const detailsContent = document.getElementById('details-content');
// 填充课程描述
const description = data.description || '暂无描述';
detailsContent.innerHTML = `<h3>课程描述:</h3><p>${description}</p>`;
// 假设从服务器获取课程章节数据
const chapters = [
{ title: "第一章:算法基础", lessons: ["算法简介", "算法设计方法"] },
{ title: "第二章:数据结构", lessons: ["数组", "链表"] }
];
// 填充章节列表
const chapterList = document.getElementById('chapter-list');
chapterList.innerHTML = ''; // 清空之前的章节列表
chapters.forEach(chapter => {
const chapterItem = document.createElement('li');
data.chapters.forEach(chapter => {
const chapterItem = document.createElement('li');
chapterItem.innerHTML = `<strong>${chapter.chapter_name}</strong>`;
// 渲染章节标题
let lessonsHtml = chapter.lessons.map(lesson => lessonTemplate(lesson)).join('');
const lessonList = document.createElement('ul');
chapter.lessons.forEach(lesson => {
const lessonItem = document.createElement('li');
lessonItem.innerHTML = lessonTemplate(lesson.lesson_name);
lessonList.appendChild(lessonItem);
});
chapterItem.appendChild(lessonList);
const addBtn = document.createElement('button');
addBtn.className = 'add-lesson-btn';
addBtn.onclick = () => showInputForNewLesson(chapter.chapter_name, addBtn);
addBtn.textContent = '新增课时';
chapterItem.appendChild(addBtn);
chapterItem.innerHTML = `
<strong>${chapter.title}</strong>
<ul>
${lessonsHtml}
</ul>
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapter.title}', this)">新增课时</button>
`;
chapterList.appendChild(chapterItem);
});
chapterList.appendChild(chapterItem);
// 使侧边栏滑出
document.getElementById('courseDetails').classList.add('open');
})
.catch(error => {
const detailsContent = document.getElementById('details-content');
detailsContent.innerHTML = '<p>加载课程详情失败,请稍后再试。</p>';
console.error('Error loading course details:', error);
});
}
function showInputForNewLesson(chapterTitle, button) {
// 防止重复点击
const addButton = button;
addButton.style.display = 'none'; // 隐藏按钮
// 创建一个输入框
const input = document.createElement('input');
input.type = 'text';
input.placeholder = '请输入课时名称...';
input.className = 'new-lesson-input';
input.onblur = () => checkAndAddLesson(input, chapterTitle, addButton); // 失去焦点时检查输入
// 将输入框插入到章节列表中
const chapterItem = addButton.closest('li'); // 获取到点击按钮的父元素
chapterItem.appendChild(input);
@@ -168,6 +200,13 @@ function checkAndAddLesson(input, chapterTitle, addButton) {
lessonsList.appendChild(lessonItem); // 将新课时添加到章节下
input.remove(); // 删除输入框
addButton.style.display = 'block'; // 重新显示新增课时按钮
fetch(`/materials/addlesson/${material_id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({chapter_name: chapterTitle, lesson_name: lessonName})
})
}
function deleteLesson(lesson) {
const lessonItems = document.querySelectorAll('.lesson-item');
@@ -186,4 +225,93 @@ function editLesson(lesson) {
function closeCourseDetails() {
document.getElementById('courseDetails').classList.remove('open');
material_id = null;
}
// 打开新建课程浮窗
function openAddCourseModal() {
document.getElementById('add-course-modal').style.display = 'block';
}
// 关闭新建课程浮窗
function closeAddCourseModal() {
document.getElementById('add-course-modal').style.display = 'none';
}
// 提交表单处理
document.getElementById('add-course-form').addEventListener('submit', function(event) {
event.preventDefault();
const courseName = document.getElementById('course-name').value;
const courseSelection = document.getElementById('course-selection').value;
const coverImage = document.getElementById('cover-preview').src;
const courseDescription = document.getElementById('course-description').value;
// 构建发送的数据
const data = {
material_name: courseName,
description: courseDescription,
chapters: [], // 这里可以根据需要添加章节信息,如果有的话
image_url: coverImage
};
// 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改
if (courseSelection !== 'new') {
// 如果是修改已有课程,设置相关的章节信息或其他数据
data.chapters = ["Chapter 1", "Chapter 2"]; // 示例章节信息
}
// 发送 POST 请求到 /create_material
fetch('/create_material', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data) // 将数据转为 JSON 格式
})
.then(response => response.json())
.then(data => {
console.log("返回消息:", data.message); // 打印返回消息
alert(data.message); // 显示一个警告框来显示返回的消息
courseName.value = "";
closeAddCourseModal(); // 关闭浮窗
})
.catch(error => {
console.error("请求失败:", error); // 如果请求失败,打印错误
alert("创建课程失败,请稍后再试!");
});
});
document.getElementById("cover-image").addEventListener("click", function() {
document.getElementById("cover-image-input").click();
});
document.getElementById("cover-image-input").addEventListener("change", function(event) {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append("file", file);
// 上传图片到服务器
fetch('/teacher/upload_image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.image_url) {
// 显示上传后的图片预览
document.getElementById("cover-preview").src = data.image_url;
document.getElementById("cover-preview").style.display = 'block';
document.getElementById("cover-image").style.display = 'none'; // 隐藏占位符
} else {
alert('图片上传失败');
}
})
.catch(error => {
alert('上传出错');
console.error(error);
});
}
});

View File

@@ -84,11 +84,18 @@
<div class="logo">
<h1>华实君:教学练评一体的虚拟助教</h1>
</div>
<nav class="navbar-links">
{% if role == 'teacher' %}
<!-- 如果是教师角色 -->
<a href="/teacherindex">首页</a>
<a href="/teacherdashboard">课程</a>
<a href="/teachergrades">成绩</a>
{% else %}
<!-- 如果是学生角色 -->
<a href="/">首页</a>
<a href="/dashboard">课程</a>
<a href="/">成绩</a>
</nav>
<a href="/grades">成绩</a>
{% endif %}
<div class="avatar dropdown">
<img src="/static/image/algorithm/book_cover.png" alt="个人头像" class="avatar-img">
<div class="dropdown-content">

View File

@@ -33,7 +33,25 @@
<p class="course-description">深入学习数据结构和算法设计。</p>
</div>
</div>
<!-- 可以继续添加课程卡片 -->
<!-- 动态加载课程卡片 -->
{% for course in materials %}
<div class="course-card" onclick="openCourseDetails('{{ course._id }}')">
<img src="{{ course.image_url }}" alt="课程封面" class="course-image">
<div class="course-info">
<h3 class="course-name">{{ course.name }}</h3>
<p class="course-description">{{ course.description }}</p>
</div>
<div class="course-date">
<p class="course-date-text">创建时间:{{ course.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
<p class="course-date-text">更新时间:{{ course.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
</div>
</div>
{% endfor %}
<div class="course-card add-course-card" onclick="openAddCourseModal()">
<div class="add-course-button">
<span>+</span>
</div>
</div>
</div>
</div>
@@ -43,13 +61,15 @@
<span id="course-title"></span>
<button class="close-btn" onclick="closeCourseDetails()">×</button>
</div>
<div class="details-content">
<h3>课程章节列表:</h3>
<ul id="chapter-list">
<!-- 章节列表会在点击课程时动态生成 -->
</ul>
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
</div>
<div class="details-content" id="details-content"></div>
<h3>课程章节列表:</h3>
<ul id="chapter-list">
<div id="loading-message">
<p>加载中...</p> <!-- 加载中效果 -->
</div>
</ul>
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
</div>
<!-- 编辑章节或课时弹窗 -->
@@ -67,6 +87,42 @@
</div>
</div>
<!-- 新建课程的浮窗 -->
<div id="add-course-modal" class="modal">
<div class="modal-content">
<h2>新建课程</h2>
<div class="cover-image-container">
<div id="cover-image" class="cover-image-placeholder">
<span class="plus-sign">+</span>
<input type="file" id="cover-image-input" style="display: none;" accept="image/*" />
</div>
<img id="cover-preview" src="" alt="封面预览" style="display: none;" />
</div>
<form id="add-course-form">
<!-- 选择课程或新建课程的下拉框 -->
<label for="course-selection">从课程创建或新建课程:</label>
<select id="course-selection">
<option value="new">新建课程</option>
{% for course in courses %}
<option value="{{ course.id }}">{{ course.name }}</option>
{% endfor %}
</select>
<br><br>
<!-- 课程名称输入框 -->
<label for="course-name">课程名称:</label>
<input type="text" id="course-name" name="course-name" required><br><br>
<label for="course-description">课程描述:</label>
<textarea id="course-description" name="course-description"></textarea><br><br>
<div class="modal-buttons">
<button type="button" onclick="closeAddCourseModal()">取消</button>
<button type="submit">确认</button>
</div>
</form>
</div>
</div>
<script src="/static/js/teacherboard.js"></script>
<script>
window.appData = {

View File

@@ -3,8 +3,10 @@ from .markdown import bp as markdown_bp
from .vscode import bp as vscode_bp
from .auth import bp as auth_bp
from .dashboard import bp as main_bp
from .material import bp as material_bp
def register_blueprints(app: Flask):
app.register_blueprint(markdown_bp) # 默认就是挂在根路径
app.register_blueprint(vscode_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(material_bp)

View File

@@ -42,10 +42,6 @@ def login_teacher_post():
ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=True)
return jsonify({"success": ok, "message": msg})
@bp.get("/teacherboard")
@require_role(roles="teacher")
def teacherboard():
return render_template("teacherboard.html")
@bp.get("/logout")
def logout():

View File

@@ -33,3 +33,5 @@ def convert_md(course_id, filename):
@bp.route("/static/<path:filename>")
def serve_static(filename):
return send_from_directory(current_app.config["STATIC_DIR"], filename)

100
Html/apps/views/material.py Normal file
View File

@@ -0,0 +1,100 @@
from flask import Blueprint, request, jsonify, current_app, session, render_template, session
from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson
from ..auth.decorators import require_role
from ..services.cos_service import upload_file
bp = Blueprint('material', __name__)
@bp.get("/teacherboard")
@require_role(roles="teacher")
def teacherboard():
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
return render_template("teacherboard.html",role="teacher", materials=get_materials_by_teacher_dict(teacher_id))
@bp.route('/teacher/upload_image', methods=['POST'])
@require_role(roles="teacher")
def upload_image():
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
file = request.files.get('file')
if not file:
return jsonify({"message": "未上传文件"}), 400
image_url = upload_file(file, teacher_id + '_' + file.filename)
return jsonify({"message": "图片上传成功", "image_url": image_url}), 200
@bp.route('/create_material', methods=['POST'])
@require_role(roles="teacher")
def create_new_material():
data = request.get_json()
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
image_url = data.get('image_url',None)
material_name = data.get('material_name',None)
description = data.get('description','')
chapters = data.get('chapters',[]) # 如果从其他课程为例进一步修改,则非空
if material_name is None:
return jsonify({"message": "教材名称不能为空"}), 400
material_id = create_material(teacher_id, material_name, description, chapters, image_url)
return jsonify({"message": "教材创建成功", "material_id": material_id}), 201
@bp.route('/materials/<material_id>', methods=['GET'])
@require_role(roles="teacher")
def get_material(material_id):
if material_id is None:
return jsonify({"message": "教材名称不能为空"}), 400
material = load_material(material_id)
print(material)
return jsonify( material.model_dump()), 200
@bp.route('/materials/<material_id>', methods=['PUT'])
@require_role(roles="teacher")
def update_existing_material(material_id):
data = request.get_json()
chapters = data.get('chapters')
if update_material(material_id, chapters):
return jsonify({"message": "教材更新成功"}), 200
return jsonify({"message": "教材未找到"}), 404
@bp.route('/materials/addchapter/<material_id>', methods=['POST'])
@require_role(roles="teacher")
def add_chapter(material_id):
data = request.get_json()
chapter_name = data.get('chapter_name')
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
if add_material_chapter(material_id, chapter_name, teacher_id):
return jsonify({"message": "章节添加成功"}), 200
return jsonify({"message": "章节添加失败"}), 404
@bp.route('/materials/addlesson/<material_id>', methods=['POST'])
@require_role(roles="teacher")
def add_lesson(material_id):
data = request.get_json()
chapter_name = data.get('chapter_name')
lesson_name = data.get('lesson_name')
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
if add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
return jsonify({"message": "课时添加成功"}), 200
return jsonify({"message": "课时添加失败"}), 404
@bp.route('/teacher/materials', methods=['GET'])
@require_role(roles="teacher")
def get_materials_by_teacher_view():
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
materials = get_materials_by_teacher(teacher_id)
result = []
for material in materials:
result.append({
'_id': str(material._id),
'name': material.name,
'description': material.description,
'created_at': material.created_at,
'updated_at': material.updated_at,
#'chapters': material.chapters,
'image_url': material.image_url
})
return jsonify(result), 200

View File

@@ -15,3 +15,10 @@
![1746850374313](image/README/1746850374313.png)
12. 大部分操作应该要登陆直接检查db文件夹下的内容即可cake123123
13. Vscode服务器需要安装插件,Vscode-AgentHands-0.0.1.vsix通过Web安装vsix即可可以百度一下
# 启动
pip install "gunicorn>=20" eventlet
`gunicorn -k eventlet -w 1 -b 0.0.0.0:5551 wsgi:app`

View File

@@ -1 +1,3 @@
markdown
flask-pymongo
cos-python-sdk-v5

View File

@@ -0,0 +1 @@
{"user_id": "TCake", "course_id": "algorithm", "chapter_id": "binary_search", "path": "/mnt/f/GIThubRepository/CodeAgent/code-agent/study/TCake/algorithm/binary_search"}