feat: add exercises and scoring

This commit is contained in:
2025-08-03 20:39:00 +02:00
parent 0ee7abbd48
commit 7afe54d877
8 changed files with 1017 additions and 14 deletions

280
app.py
View File

@@ -1,9 +1,9 @@
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField
from wtforms.validators import DataRequired, Email, NumberRange, Optional
from datetime import datetime
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Email, NumberRange, Optional, Length
from datetime import datetime, date
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
@@ -80,6 +80,52 @@ class Grade(db.Model):
def __repr__(self):
return f'<Grade {self.value} for {self.student.first_name}>'
# Forms
class AssessmentForm(FlaskForm):
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
date = DateField('Date', validators=[DataRequired()], default=date.today)
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
coefficient = FloatField('Coefficient', validators=[DataRequired(), NumberRange(min=0.1, max=10)], default=1.0)
submit = SubmitField('Enregistrer')
def __init__(self, *args, **kwargs):
super(AssessmentForm, self).__init__(*args, **kwargs)
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
class ClassGroupForm(FlaskForm):
name = StringField('Nom de la classe', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description', validators=[Optional()])
year = StringField('Année scolaire', validators=[DataRequired(), Length(max=20)], default="2024-2025")
submit = SubmitField('Enregistrer')
class StudentForm(FlaskForm):
first_name = StringField('Prénom', validators=[DataRequired(), Length(max=100)])
last_name = StringField('Nom', validators=[DataRequired(), Length(max=100)])
email = StringField('Email', validators=[Optional(), Email(), Length(max=120)])
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
submit = SubmitField('Enregistrer')
def __init__(self, *args, **kwargs):
super(StudentForm, self).__init__(*args, **kwargs)
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
class ExerciseForm(FlaskForm):
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
order = IntegerField('Ordre', validators=[DataRequired(), NumberRange(min=1)], default=1)
submit = SubmitField('Enregistrer')
class GradingElementForm(FlaskForm):
label = StringField('Libellé', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
skill = StringField('Compétence', validators=[Optional(), Length(max=200)])
max_points = FloatField('Barème (points max)', validators=[DataRequired(), NumberRange(min=0.1)], default=1.0)
grading_type = SelectField('Type de notation', validators=[DataRequired()],
choices=[('points', 'Points (ex: 2.5/4)'), ('score', 'Score (0, 1, 2, 3, .)')],
default='points')
submit = SubmitField('Enregistrer')
@app.route('/')
def index():
recent_assessments = Assessment.query.order_by(Assessment.date.desc()).limit(5).all()
@@ -107,6 +153,234 @@ def assessments():
assessments = Assessment.query.join(ClassGroup).order_by(Assessment.date.desc()).all()
return render_template('assessments.html', assessments=assessments)
@app.route('/assessments/new', methods=['GET', 'POST'])
def new_assessment():
form = AssessmentForm()
if form.validate_on_submit():
assessment = Assessment(
title=form.title.data,
description=form.description.data,
date=form.date.data,
class_group_id=form.class_group_id.data,
coefficient=form.coefficient.data
)
db.session.add(assessment)
db.session.commit()
flash('Évaluation créée avec succès !', 'success')
return redirect(url_for('assessment_detail', id=assessment.id))
return render_template('assessment_form.html', form=form, title='Nouvelle évaluation')
@app.route('/assessments/<int:id>')
def assessment_detail(id):
assessment = Assessment.query.get_or_404(id)
return render_template('assessment_detail.html', assessment=assessment)
@app.route('/assessments/<int:id>/edit', methods=['GET', 'POST'])
def edit_assessment(id):
assessment = Assessment.query.get_or_404(id)
form = AssessmentForm(obj=assessment)
if form.validate_on_submit():
assessment.title = form.title.data
assessment.description = form.description.data
assessment.date = form.date.data
assessment.class_group_id = form.class_group_id.data
assessment.coefficient = form.coefficient.data
db.session.commit()
flash('Évaluation modifiée avec succès !', 'success')
return redirect(url_for('assessment_detail', id=assessment.id))
return render_template('assessment_form.html', form=form, title='Modifier l\'évaluation', assessment=assessment)
@app.route('/assessments/<int:id>/delete', methods=['POST'])
def delete_assessment(id):
assessment = Assessment.query.get_or_404(id)
db.session.delete(assessment)
db.session.commit()
flash('Évaluation supprimée avec succès !', 'success')
return redirect(url_for('assessments'))
# Exercise routes
@app.route('/assessments/<int:assessment_id>/exercises/new', methods=['GET', 'POST'])
def new_exercise(assessment_id):
assessment = Assessment.query.get_or_404(assessment_id)
form = ExerciseForm()
# Set default order to next available
if form.order.data == 1: # Only if it's the default value
max_order = db.session.query(db.func.max(Exercise.order)).filter_by(assessment_id=assessment_id).scalar()
form.order.data = (max_order or 0) + 1
if form.validate_on_submit():
exercise = Exercise(
assessment_id=assessment_id,
title=form.title.data,
description=form.description.data,
order=form.order.data
)
db.session.add(exercise)
db.session.commit()
flash('Exercice créé avec succès !', 'success')
return redirect(url_for('exercise_detail', assessment_id=assessment_id, id=exercise.id))
return render_template('exercise_form.html', form=form, assessment=assessment, title='Nouvel exercice')
@app.route('/assessments/<int:assessment_id>/exercises/<int:id>')
def exercise_detail(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
return render_template('exercise_detail.html', assessment=assessment, exercise=exercise)
@app.route('/assessments/<int:assessment_id>/exercises/<int:id>/edit', methods=['GET', 'POST'])
def edit_exercise(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
form = ExerciseForm(obj=exercise)
if form.validate_on_submit():
exercise.title = form.title.data
exercise.description = form.description.data
exercise.order = form.order.data
db.session.commit()
flash('Exercice modifié avec succès !', 'success')
return redirect(url_for('exercise_detail', assessment_id=assessment_id, id=exercise.id))
return render_template('exercise_form.html', form=form, assessment=assessment, exercise=exercise, title='Modifier l\'exercice')
@app.route('/assessments/<int:assessment_id>/exercises/<int:id>/delete', methods=['POST'])
def delete_exercise(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
db.session.delete(exercise)
db.session.commit()
flash('Exercice supprimé avec succès !', 'success')
return redirect(url_for('assessment_detail', id=assessment_id))
# GradingElement routes
@app.route('/assessments/<int:assessment_id>/exercises/<int:exercise_id>/elements/new', methods=['GET', 'POST'])
def new_grading_element(assessment_id, exercise_id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=exercise_id, assessment_id=assessment_id).first_or_404()
form = GradingElementForm()
if form.validate_on_submit():
element = GradingElement(
exercise_id=exercise_id,
label=form.label.data,
description=form.description.data,
skill=form.skill.data,
max_points=form.max_points.data,
grading_type=form.grading_type.data
)
db.session.add(element)
db.session.commit()
flash('Élément de notation créé avec succès !', 'success')
return redirect(url_for('exercise_detail', assessment_id=assessment_id, id=exercise_id))
return render_template('grading_element_form.html', form=form, assessment=assessment, exercise=exercise, title='Nouvel élément de notation')
@app.route('/assessments/<int:assessment_id>/exercises/<int:exercise_id>/elements/<int:id>/edit', methods=['GET', 'POST'])
def edit_grading_element(assessment_id, exercise_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=exercise_id, assessment_id=assessment_id).first_or_404()
element = GradingElement.query.filter_by(id=id, exercise_id=exercise_id).first_or_404()
form = GradingElementForm(obj=element)
if form.validate_on_submit():
element.label = form.label.data
element.description = form.description.data
element.skill = form.skill.data
element.max_points = form.max_points.data
element.grading_type = form.grading_type.data
db.session.commit()
flash('Élément de notation modifié avec succès !', 'success')
return redirect(url_for('exercise_detail', assessment_id=assessment_id, id=exercise_id))
return render_template('grading_element_form.html', form=form, assessment=assessment, exercise=exercise, element=element, title='Modifier l\'élément de notation')
@app.route('/assessments/<int:assessment_id>/exercises/<int:exercise_id>/elements/<int:id>/delete', methods=['POST'])
def delete_grading_element(assessment_id, exercise_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=exercise_id, assessment_id=assessment_id).first_or_404()
element = GradingElement.query.filter_by(id=id, exercise_id=exercise_id).first_or_404()
db.session.delete(element)
db.session.commit()
flash('Élément de notation supprimé avec succès !', 'success')
return redirect(url_for('exercise_detail', assessment_id=assessment_id, id=exercise_id))
# Grading routes
@app.route('/assessments/<int:assessment_id>/grading')
def assessment_grading(assessment_id):
assessment = Assessment.query.get_or_404(assessment_id)
students = Student.query.filter_by(class_group_id=assessment.class_group_id).order_by(Student.last_name, Student.first_name).all()
# Get all grading elements for this assessment
grading_elements = []
for exercise in assessment.exercises:
for element in exercise.grading_elements:
grading_elements.append(element)
# Get existing grades
existing_grades = {}
for grade in Grade.query.join(GradingElement).join(Exercise).filter_by(assessment_id=assessment_id).all():
key = f"{grade.student_id}_{grade.grading_element_id}"
existing_grades[key] = grade
return render_template('assessment_grading.html',
assessment=assessment,
students=students,
grading_elements=grading_elements,
existing_grades=existing_grades)
@app.route('/assessments/<int:assessment_id>/grading/save', methods=['POST'])
def save_grades(assessment_id):
assessment = Assessment.query.get_or_404(assessment_id)
for key, value in request.form.items():
if key.startswith('grade_'):
# Parse key: grade_<student_id>_<element_id>
parts = key.split('_')
if len(parts) == 3:
student_id = int(parts[1])
element_id = int(parts[2])
# Find or create grade
grade = Grade.query.filter_by(
student_id=student_id,
grading_element_id=element_id
).first()
if value.strip(): # If value is not empty
if not grade:
grade = Grade(
student_id=student_id,
grading_element_id=element_id,
value=value
)
db.session.add(grade)
else:
grade.value = value
elif grade: # If value is empty but grade exists, delete it
db.session.delete(grade)
# Handle comments
for key, value in request.form.items():
if key.startswith('comment_'):
parts = key.split('_')
if len(parts) == 3:
student_id = int(parts[1])
element_id = int(parts[2])
grade = Grade.query.filter_by(
student_id=student_id,
grading_element_id=element_id
).first()
if grade:
grade.comment = value.strip() if value.strip() else None
db.session.commit()
flash('Notes sauvegardées avec succès !', 'success')
return redirect(url_for('assessment_grading', assessment_id=assessment_id))
@app.cli.command()
def init_db():
"""Initialize the database with sample data."""