466 lines
20 KiB
Python
466 lines
20 KiB
Python
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, 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'
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
|
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
|
|
db = SQLAlchemy(app)
|
|
|
|
class ClassGroup(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(100), nullable=False, unique=True)
|
|
description = db.Column(db.Text)
|
|
year = db.Column(db.String(20), nullable=False)
|
|
students = db.relationship('Student', backref='class_group', lazy=True)
|
|
assessments = db.relationship('Assessment', backref='class_group', lazy=True)
|
|
|
|
def __repr__(self):
|
|
return f'<ClassGroup {self.name}>'
|
|
|
|
class Student(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
last_name = db.Column(db.String(100), nullable=False)
|
|
first_name = db.Column(db.String(100), nullable=False)
|
|
email = db.Column(db.String(120), unique=True)
|
|
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
|
grades = db.relationship('Grade', backref='student', lazy=True)
|
|
|
|
def __repr__(self):
|
|
return f'<Student {self.first_name} {self.last_name}>'
|
|
|
|
class Assessment(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
|
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
|
coefficient = db.Column(db.Float, default=1.0)
|
|
exercises = db.relationship('Exercise', backref='assessment', lazy=True, cascade='all, delete-orphan')
|
|
|
|
def __repr__(self):
|
|
return f'<Assessment {self.title}>'
|
|
|
|
class Exercise(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id'), nullable=False)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
order = db.Column(db.Integer, default=1)
|
|
grading_elements = db.relationship('GradingElement', backref='exercise', lazy=True, cascade='all, delete-orphan')
|
|
|
|
def __repr__(self):
|
|
return f'<Exercise {self.title}>'
|
|
|
|
class GradingElement(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
exercise_id = db.Column(db.Integer, db.ForeignKey('exercise.id'), nullable=False)
|
|
label = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
skill = db.Column(db.String(200))
|
|
max_points = db.Column(db.Float, nullable=False)
|
|
grading_type = db.Column(db.String(10), nullable=False, default='points') # 'score' or 'points'
|
|
grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan')
|
|
|
|
def __repr__(self):
|
|
return f'<GradingElement {self.label}>'
|
|
|
|
class Grade(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False)
|
|
grading_element_id = db.Column(db.Integer, db.ForeignKey('grading_element.id'), nullable=False)
|
|
value = db.Column(db.String(10)) # String to handle scores (0,1,2,3,.) and points
|
|
comment = db.Column(db.Text)
|
|
|
|
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()
|
|
total_students = Student.query.count()
|
|
total_assessments = Assessment.query.count()
|
|
total_classes = ClassGroup.query.count()
|
|
return render_template('index.html',
|
|
recent_assessments=recent_assessments,
|
|
total_students=total_students,
|
|
total_assessments=total_assessments,
|
|
total_classes=total_classes)
|
|
|
|
@app.route('/classes')
|
|
def classes():
|
|
classes = ClassGroup.query.order_by(ClassGroup.year, ClassGroup.name).all()
|
|
return render_template('classes.html', classes=classes)
|
|
|
|
@app.route('/students')
|
|
def students():
|
|
students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all()
|
|
return render_template('students.html', students=students)
|
|
|
|
@app.route('/assessments')
|
|
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."""
|
|
db.create_all()
|
|
|
|
# Check if data already exists
|
|
if ClassGroup.query.first():
|
|
print("Database already initialized!")
|
|
return
|
|
|
|
# Create sample class groups
|
|
classe_6a = ClassGroup(name="6ème A", description="Classe de 6ème A", year="2024-2025")
|
|
classe_5b = ClassGroup(name="5ème B", description="Classe de 5ème B", year="2024-2025")
|
|
|
|
db.session.add(classe_6a)
|
|
db.session.add(classe_5b)
|
|
db.session.commit()
|
|
|
|
# Create sample students
|
|
students_data = [
|
|
("Dupont", "Marie", "marie.dupont@email.com", classe_6a.id),
|
|
("Martin", "Pierre", "pierre.martin@email.com", classe_6a.id),
|
|
("Durand", "Sophie", "sophie.durand@email.com", classe_6a.id),
|
|
("Moreau", "Lucas", "lucas.moreau@email.com", classe_5b.id),
|
|
("Bernard", "Emma", "emma.bernard@email.com", classe_5b.id),
|
|
]
|
|
|
|
for last_name, first_name, email, class_group_id in students_data:
|
|
student = Student(
|
|
last_name=last_name,
|
|
first_name=first_name,
|
|
email=email,
|
|
class_group_id=class_group_id
|
|
)
|
|
db.session.add(student)
|
|
|
|
db.session.commit()
|
|
|
|
# Create sample assessment
|
|
assessment = Assessment(
|
|
title="Évaluation de mathématiques",
|
|
description="Évaluation sur les fractions et les décimaux",
|
|
class_group_id=classe_6a.id,
|
|
coefficient=2.0
|
|
)
|
|
db.session.add(assessment)
|
|
db.session.commit()
|
|
|
|
# Create sample exercise
|
|
exercise = Exercise(
|
|
assessment_id=assessment.id,
|
|
title="Exercice 1 - Fractions",
|
|
description="Calculs avec les fractions",
|
|
order=1
|
|
)
|
|
db.session.add(exercise)
|
|
db.session.commit()
|
|
|
|
# Create sample grading elements
|
|
elements_data = [
|
|
("Calcul de base", "Addition et soustraction de fractions", "Calculer", 4.0, "points"),
|
|
("Méthode", "Justification de la méthode utilisée", "Raisonner", 2.0, "score"),
|
|
("Présentation", "Clarté de la présentation", "Communiquer", 2.0, "score"),
|
|
]
|
|
|
|
for label, description, skill, max_points, grading_type in elements_data:
|
|
element = GradingElement(
|
|
exercise_id=exercise.id,
|
|
label=label,
|
|
description=description,
|
|
skill=skill,
|
|
max_points=max_points,
|
|
grading_type=grading_type
|
|
)
|
|
db.session.add(element)
|
|
|
|
db.session.commit()
|
|
print("Database initialized with sample data!")
|
|
|
|
if __name__ == '__main__':
|
|
with app.app_context():
|
|
db.create_all()
|
|
app.run(debug=True) |