diff --git a/README.md b/README.md index 1408c03..1e5ec97 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ # Gestion Scolaire -Application web pour gérer les élèves, évaluations et notes. +Application web Flask pour gérer les élèves, évaluations et notes dans un contexte scolaire. + +## Fonctionnalités + +### ✅ Gestion complète des évaluations +- **Groupes classes** : Organisation des élèves par classe +- **Évaluations** : Création et gestion des contrôles/devoirs +- **Exercices** : Structure hiérarchique des évaluations +- **Éléments de notation** : Configuration fine des critères +- **Saisie des notes** : Interface intuitive pour la notation + +### 🎯 Types de notation supportés +- **Points** : Notation classique (ex: 2.5/4 points) +- **Score** : Évaluation par compétences (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué) ## Installation et lancement @@ -20,19 +33,31 @@ uv run flask --app app run --debug uv run flask --app app run ``` -## Fonctionnalités +## Architecture technique -- Gestion des groupes classes -- Gestion des élèves -- Création d'évaluations avec exercices -- Éléments de notation configurables (score ou points) -- Saisie et suivi des notes - -## Architecture +### Structure du code +``` +├── app.py # Application Flask principale +├── models.py # Modèles SQLAlchemy +├── forms.py # Formulaires WTForms +├── commands.py # Commandes CLI Flask +├── routes/ +│ ├── assessments.py # Routes pour les évaluations +│ ├── exercises.py # Routes pour les exercices +│ └── grading.py # Routes pour la saisie des notes +└── templates/ # Templates Jinja2 avec TailwindCSS +``` +### Modèles de données - **ClassGroup** : Groupes de classes (ex: 6ème A, 5ème B) - **Student** : Élèves assignés à un groupe classe - **Assessment** : Évaluations liées à un groupe classe - **Exercise** : Exercices composant une évaluation -- **GradingElement** : Éléments de notation avec barème -- **Grade** : Notes attribuées aux éléments de notation \ No newline at end of file +- **GradingElement** : Éléments de notation avec barème et type +- **Grade** : Notes attribuées aux éléments de notation + +### Technologies utilisées +- **Backend** : Flask, SQLAlchemy, WTForms +- **Frontend** : TailwindCSS, Jinja2 +- **Base de données** : SQLite +- **Gestionnaire de paquets** : uv \ No newline at end of file diff --git a/app.py b/app.py index b5f131a..42303b4 100644 --- a/app.py +++ b/app.py @@ -1,466 +1,56 @@ -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 +from flask import Flask, render_template +from models import db, Assessment, Student, ClassGroup +from commands import init_db -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 +# Import blueprints +from routes.assessments import bp as assessments_bp +from routes.exercises import bp as exercises_bp +from routes.grading import bp as grading_bp -db = SQLAlchemy(app) +def create_app(): + 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 -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) + # Initialize extensions + db.init_app(app) - def __repr__(self): - return f'' + # Register blueprints + app.register_blueprint(assessments_bp) + app.register_blueprint(exercises_bp) + app.register_blueprint(grading_bp) -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) + # Register CLI commands + app.cli.add_command(init_db) - def __repr__(self): - return f'' + # Main routes + @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) -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') + @app.route('/classes') + def classes(): + classes = ClassGroup.query.order_by(ClassGroup.year, ClassGroup.name).all() + return render_template('classes.html', classes=classes) - def __repr__(self): - return f'' + @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) -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!") + return app if __name__ == '__main__': + app = create_app() with app.app_context(): db.create_all() app.run(debug=True) \ No newline at end of file diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..67248bc --- /dev/null +++ b/commands.py @@ -0,0 +1,83 @@ +import click +from flask.cli import with_appcontext +from models import db, ClassGroup, Student, Assessment, Exercise, GradingElement + +@click.command() +@with_appcontext +def init_db(): + """Initialize the database with sample data.""" + db.create_all() + + # Check if data already exists + if ClassGroup.query.first(): + click.echo("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() + click.echo("Database initialized with sample data!") \ No newline at end of file diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..5085c89 --- /dev/null +++ b/forms.py @@ -0,0 +1,50 @@ +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 date +from models import ClassGroup + +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') \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..7a029c5 --- /dev/null +++ b/models.py @@ -0,0 +1,72 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +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'' \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..7e1d20f --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1 @@ +# Routes package \ No newline at end of file diff --git a/routes/assessments.py b/routes/assessments.py new file mode 100644 index 0000000..82e1105 --- /dev/null +++ b/routes/assessments.py @@ -0,0 +1,55 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from models import db, Assessment, ClassGroup +from forms import AssessmentForm + +bp = Blueprint('assessments', __name__, url_prefix='/assessments') + +@bp.route('/') +def list(): + assessments = Assessment.query.join(ClassGroup).order_by(Assessment.date.desc()).all() + return render_template('assessments.html', assessments=assessments) + +@bp.route('/new', methods=['GET', 'POST']) +def new(): + 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('assessments.detail', id=assessment.id)) + return render_template('assessment_form.html', form=form, title='Nouvelle évaluation') + +@bp.route('/') +def detail(id): + assessment = Assessment.query.get_or_404(id) + return render_template('assessment_detail.html', assessment=assessment) + +@bp.route('//edit', methods=['GET', 'POST']) +def edit(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('assessments.detail', id=assessment.id)) + return render_template('assessment_form.html', form=form, title='Modifier l\'évaluation', assessment=assessment) + +@bp.route('//delete', methods=['POST']) +def delete(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.list')) \ No newline at end of file diff --git a/routes/exercises.py b/routes/exercises.py new file mode 100644 index 0000000..c1b9cde --- /dev/null +++ b/routes/exercises.py @@ -0,0 +1,113 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from models import db, Assessment, Exercise, GradingElement +from forms import ExerciseForm, GradingElementForm + +bp = Blueprint('exercises', __name__) + +# Exercise routes +@bp.route('/assessments//exercises/new', methods=['GET', 'POST']) +def new(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('exercises.detail', assessment_id=assessment_id, id=exercise.id)) + + return render_template('exercise_form.html', form=form, assessment=assessment, title='Nouvel exercice') + +@bp.route('/assessments//exercises/') +def 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) + +@bp.route('/assessments//exercises//edit', methods=['GET', 'POST']) +def edit(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('exercises.detail', assessment_id=assessment_id, id=exercise.id)) + + return render_template('exercise_form.html', form=form, assessment=assessment, exercise=exercise, title='Modifier l\'exercice') + +@bp.route('/assessments//exercises//delete', methods=['POST']) +def delete(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('assessments.detail', id=assessment_id)) + +# GradingElement routes +@bp.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('exercises.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') + +@bp.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('exercises.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') + +@bp.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('exercises.detail', assessment_id=assessment_id, id=exercise_id)) \ No newline at end of file diff --git a/routes/grading.py b/routes/grading.py new file mode 100644 index 0000000..0551fe1 --- /dev/null +++ b/routes/grading.py @@ -0,0 +1,78 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from models import db, Assessment, Student, Grade, GradingElement, Exercise + +bp = Blueprint('grading', __name__) + +@bp.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) + +@bp.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('grading.assessment_grading', assessment_id=assessment_id)) \ No newline at end of file diff --git a/templates/assessment_detail.html b/templates/assessment_detail.html index afc8f41..9a5b7cb 100644 --- a/templates/assessment_detail.html +++ b/templates/assessment_detail.html @@ -6,14 +6,14 @@
- + ← Retour aux évaluations

{{ assessment.title }}

{{ assessment.class_group.name }} - {{ assessment.date.strftime('%d/%m/%Y') }}

- +
@@ -62,7 +62,7 @@
@@ -82,10 +82,10 @@
@@ -101,7 +101,7 @@

Aucun exercice

Commencez par ajouter le premier exercice de cette évaluation.

@@ -117,7 +117,7 @@
- + diff --git a/templates/assessment_form.html b/templates/assessment_form.html index 39ed2c6..00117cf 100644 --- a/templates/assessment_form.html +++ b/templates/assessment_form.html @@ -5,7 +5,7 @@ {% block content %}
- + Annuler {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }} diff --git a/templates/assessment_grading.html b/templates/assessment_grading.html index a26a736..ac2ee30 100644 --- a/templates/assessment_grading.html +++ b/templates/assessment_grading.html @@ -6,7 +6,7 @@
- + ← Retour à l'évaluation

Saisie des notes

@@ -28,7 +28,7 @@

Cette évaluation n'a pas encore d'éléments de notation configurés. Vous devez d'abord créer des exercices et leurs éléments de notation.

@@ -36,7 +36,7 @@
{% else %} -
+

Guide de saisie

@@ -117,7 +117,7 @@
- + Annuler
@@ -66,7 +66,7 @@

Aucune évaluation

Commencez par créer votre première évaluation.

diff --git a/templates/base.html b/templates/base.html index e8190c4..b59d89a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -19,7 +19,7 @@ Accueil Classes Élèves - Évaluations + Évaluations
diff --git a/templates/exercise_detail.html b/templates/exercise_detail.html index d4c4b82..f32139c 100644 --- a/templates/exercise_detail.html +++ b/templates/exercise_detail.html @@ -6,14 +6,14 @@
- + ← Retour à l'évaluation "{{ assessment.title }}"

{{ exercise.title }}

{{ assessment.title }} - {{ assessment.class_group.name }}

- +
@@ -54,7 +54,7 @@

Éléments de notation

- + Ajouter un élément
@@ -83,13 +83,13 @@
- + Modifier - +
@@ -119,7 +119,7 @@
- + diff --git a/templates/exercise_form.html b/templates/exercise_form.html index 0259db7..3d80c34 100644 --- a/templates/exercise_form.html +++ b/templates/exercise_form.html @@ -5,7 +5,7 @@ {% block content %}
- + Annuler {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }} diff --git a/templates/grading_element_form.html b/templates/grading_element_form.html index 0bba247..b42819e 100644 --- a/templates/grading_element_form.html +++ b/templates/grading_element_form.html @@ -5,7 +5,7 @@ {% block content %}
- + Annuler {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }} diff --git a/templates/index.html b/templates/index.html index 4157286..79d64f9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -114,7 +114,7 @@
Gérer les élèves
- +