commit
a880eb667b
@ -0,0 +1,2 @@
|
||||
SECRET_KEY=change-me
|
||||
DATABASE_URL=mysql+pymysql://user:password@host:3306/dbname
|
||||
@ -0,0 +1,73 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# Environment variables and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS-generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.direnv
|
||||
@ -0,0 +1,48 @@
|
||||
# 课堂点名系统(最小可运行版本)
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
cd class-random-caller
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Windows 使用 venv\\Scripts\\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置数据库
|
||||
|
||||
在项目根目录创建 `.env` 文件,内容示例:
|
||||
|
||||
```env
|
||||
SECRET_KEY=your-secret-key
|
||||
DATABASE_URL=mysql+pymysql://user:password@host:3306/dbname
|
||||
```
|
||||
|
||||
请将 `user/password/host/dbname` 替换为你在免费 MySQL 服务上的实际信息。
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
在 Python 交互环境或单独脚本中执行:
|
||||
|
||||
```python
|
||||
from run import app
|
||||
from app import db
|
||||
from app.models import User
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
# 创建一个管理员账号(只需执行一次)
|
||||
if not User.query.filter_by(username="admin").first():
|
||||
u = User(username="admin", password_hash=generate_password_hash("admin123"))
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
```
|
||||
|
||||
## 运行项目
|
||||
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
|
||||
浏览器打开 `http://127.0.0.1:5000/auth/login`,使用 `admin / admin123` 登录。
|
||||
@ -0,0 +1,25 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from .config import Config
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = "auth.login"
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
from .auth.routes import auth_bp
|
||||
from .main.routes import main_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
return app
|
||||
@ -0,0 +1,28 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, flash
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from app.models import User
|
||||
|
||||
|
||||
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and check_password_hash(user.password_hash, password):
|
||||
login_user(user)
|
||||
return redirect(url_for("main.rollcall"))
|
||||
flash("用户名或密码错误")
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@auth_bp.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("auth.login"))
|
||||
@ -0,0 +1,13 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"mysql+pymysql://user:password@host:3306/dbname",
|
||||
)
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
@ -0,0 +1,29 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
main_bp = Blueprint("main", __name__)
|
||||
|
||||
|
||||
@main_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
return render_template("rollcall.html")
|
||||
|
||||
|
||||
@main_bp.route("/rollcall")
|
||||
@login_required
|
||||
def rollcall():
|
||||
return render_template("rollcall.html")
|
||||
|
||||
|
||||
@main_bp.route("/stats")
|
||||
@login_required
|
||||
def stats():
|
||||
return render_template("stats.html")
|
||||
|
||||
|
||||
@main_bp.route("/manage")
|
||||
@login_required
|
||||
def manage():
|
||||
return render_template("manage.html")
|
||||
@ -0,0 +1,41 @@
|
||||
from datetime import datetime
|
||||
from flask_login import UserMixin
|
||||
from . import db, login_manager
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
|
||||
|
||||
class Student(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
student_no = db.Column(db.String(32), unique=True, nullable=False)
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
major = db.Column(db.String(128))
|
||||
total_score = db.Column(db.Float, default=0.0)
|
||||
attendance_count = db.Column(db.Integer, default=0)
|
||||
random_called_count = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
class RollCall(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
student_id = db.Column(db.Integer, db.ForeignKey("student.id"), nullable=False)
|
||||
call_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
mode = db.Column(db.String(16), nullable=False) # random / sequence
|
||||
status = db.Column(
|
||||
db.String(16), nullable=False
|
||||
) # absent / distracted / attended
|
||||
score_change = db.Column(db.Float, nullable=False, default=0.0)
|
||||
|
||||
student = db.relationship("Student", backref="roll_calls")
|
||||
|
||||
# user 关系可按需在查询中使用,不强制 backref
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}课堂点名系统{% endblock %}</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('main.rollcall') }}">课堂点名</a>
|
||||
<div>
|
||||
<a class="btn btn-outline-light me-2" href="{{ url_for('main.rollcall') }}">点名</a>
|
||||
<a class="btn btn-outline-light me-2" href="{{ url_for('main.stats') }}">统计</a>
|
||||
<a class="btn btn-outline-light me-2" href="{{ url_for('main.manage') }}">管理</a>
|
||||
<a class="btn btn-warning" href="{{ url_for('auth.logout') }}">退出</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for m in messages %} {{ m }}<br /> {% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,50 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>登录 - 课堂点名系统</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">管理员登录</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">用户名</label>
|
||||
<input class="form-control" name="username" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" type="submit">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning mt-3">
|
||||
{% for m in messages %} {{ m }}<br /> {% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}管理 - 课堂点名系统{% endblock %}
|
||||
{% block content %}
|
||||
<h4>管理页面占位</h4>
|
||||
<p>学生总表,以及 Excel 导入功能,将在这里实现。</p>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}点名 - 课堂点名系统{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4>点名区(稍后实现逻辑)</h4>
|
||||
<p>这里将显示大字姓名和学号,以及随机/顺序选择。</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>打分区(稍后实现逻辑)</h4>
|
||||
<p>缺勤 / 走神 / 加分 按钮区域。</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}统计 - 课堂点名系统{% endblock %}
|
||||
{% block content %}
|
||||
<h4>统计页面占位</h4>
|
||||
<p>上半部分:积分随时间走势图;下半部分:排行榜;右侧:点名记录。</p>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,7 @@
|
||||
Flask==3.0.3
|
||||
Flask-Login==0.6.3
|
||||
Flask_SQLAlchemy==3.1.1
|
||||
pymysql==1.1.1
|
||||
pandas==2.2.3
|
||||
openpyxl==3.1.5
|
||||
python-dotenv==1.0.1
|
||||
@ -0,0 +1,8 @@
|
||||
from app import create_app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
Loading…
Reference in new issue