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'' 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'' 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'' 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'' 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'' 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'' # 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/') def assessment_detail(id): assessment = Assessment.query.get_or_404(id) return render_template('assessment_detail.html', assessment=assessment) @app.route('/assessments//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//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//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//exercises/') 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//exercises//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//exercises//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//exercises//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//exercises//elements//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//exercises//elements//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//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//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__ 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)