diff --git a/app.py b/app.py index be3921a..b5f131a 100644 --- a/app.py +++ b/app.py @@ -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'' +# 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/') +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.""" diff --git a/templates/assessment_detail.html b/templates/assessment_detail.html new file mode 100644 index 0000000..afc8f41 --- /dev/null +++ b/templates/assessment_detail.html @@ -0,0 +1,142 @@ +{% extends "base.html" %} + +{% block title %}{{ assessment.title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+
+
+ + ← Retour aux évaluations + +

{{ assessment.title }}

+

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

+
+
+ + Modifier + + +
+
+ + + + +
+
+

Informations générales

+
+
+
+
+
Classe
+
{{ assessment.class_group.name }}
+
+
+
Date
+
{{ assessment.date.strftime('%d/%m/%Y') }}
+
+
+
Coefficient
+
{{ assessment.coefficient }}
+
+
+
Nombre d'exercices
+
{{ assessment.exercises|length }}
+
+ {% if assessment.description %} +
+
Description
+
{{ assessment.description }}
+
+ {% endif %} +
+
+
+ + +
+ +
+ {% if assessment.exercises %} +
+ {% for exercise in assessment.exercises|sort(attribute='order') %} +
+
+
+

{{ exercise.title }}

+ {% if exercise.description %} +

{{ exercise.description }}

+ {% endif %} +
+ {{ exercise.grading_elements|length }} élément(s) de notation +
+
+ +
+
+ {% endfor %} +
+ {% else %} +
+ + + +

Aucun exercice

+

Commencez par ajouter le premier exercice de cette évaluation.

+ +
+ {% endif %} +
+
+ + +
+
+

Actions

+
+
+
+ + + + + Saisir les notes + + + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/assessment_form.html b/templates/assessment_form.html new file mode 100644 index 0000000..39ed2c6 --- /dev/null +++ b/templates/assessment_form.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} + +{% block title %}{{ title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+ + +
+
+

{{ title }}

+
+ +
+ {{ form.hidden_tag() }} + +
+ + {{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.title.errors %} +
+ {% for error in form.title.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+
+ + {{ form.date(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.date.errors %} +
+ {% for error in form.date.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.coefficient(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", step="0.1") }} + {% if form.coefficient.errors %} +
+ {% for error in form.coefficient.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+
+ +
+ + {{ form.class_group_id(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.class_group_id.errors %} +
+ {% for error in form.class_group_id.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + 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") }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/assessment_grading.html b/templates/assessment_grading.html new file mode 100644 index 0000000..a26a736 --- /dev/null +++ b/templates/assessment_grading.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} + +{% block title %}Saisie des notes - {{ assessment.title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+
+
+ + ← Retour à l'évaluation + +

Saisie des notes

+

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

+
+
+ + {% if not grading_elements %} +
+
+
+ + + +
+
+

Aucun élément de notation

+
+

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.

+
+ +
+
+
+ {% else %} +
+ +
+

Guide de saisie

+
+

Points : Saisissez une valeur numérique (ex: 2.5, 3, 0)

+

Score : 0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué

+
+
+ + +
+
+

Grille de notation

+
+ +
+ + + + + {% for element in grading_elements %} + + {% endfor %} + + + + {% for student in students %} + + + {% for element in grading_elements %} + {% set grade_key = student.id ~ '_' ~ element.id %} + {% set existing_grade = existing_grades.get(grade_key) %} + + {% endfor %} + + {% endfor %} + +
+ Élève + +
{{ element.label }}
+
+ + {% if element.grading_type == 'score' %}Score/{{ element.max_points|int }}{% else %}/{{ element.max_points }}{% endif %} + +
+ {% if element.skill %} +
{{ element.skill }}
+ {% endif %} +
+ {{ student.first_name }} {{ student.last_name }} + +
+ {% if element.grading_type == 'score' %} + + {% else %} + + {% endif %} + +
+
+
+ +
+ + Annuler + + +
+
+
+ + +
+
+

Légende des exercices

+
+
+
+ {% for exercise in assessment.exercises|sort(attribute='order') %} +
+

{{ exercise.title }}

+ {% if exercise.description %} +

{{ exercise.description }}

+ {% endif %} +
+ {% for element in exercise.grading_elements %} +
+ {{ element.label }} + {% if element.skill %} - {{ element.skill }}{% endif %} + {% if element.description %} : {{ element.description }}{% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/assessments.html b/templates/assessments.html index a3b1b22..57e5d21 100644 --- a/templates/assessments.html +++ b/templates/assessments.html @@ -6,9 +6,9 @@

Gestion des évaluations

- +
{% if assessments %} @@ -42,15 +42,15 @@
@@ -66,9 +66,9 @@

Aucune évaluation

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

{% endif %} diff --git a/templates/exercise_detail.html b/templates/exercise_detail.html new file mode 100644 index 0000000..d4c4b82 --- /dev/null +++ b/templates/exercise_detail.html @@ -0,0 +1,138 @@ +{% extends "base.html" %} + +{% block title %}{{ exercise.title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+
+
+ + ← Retour à l'évaluation "{{ assessment.title }}" + +

{{ exercise.title }}

+

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

+
+
+ + Modifier + + +
+
+ + + + +
+
+

Informations de l'exercice

+
+
+
+
+
Ordre
+
{{ exercise.order }}
+
+
+
Nombre d'éléments de notation
+
{{ exercise.grading_elements|length }}
+
+ {% if exercise.description %} +
+
Description
+
{{ exercise.description }}
+
+ {% endif %} +
+
+
+ + +
+
+

Éléments de notation

+ + Ajouter un élément + +
+
+ {% if exercise.grading_elements %} +
+ {% for element in exercise.grading_elements %} +
+
+
+
+

{{ element.label }}

+ + {% if element.grading_type == 'score' %}Score (0-3){% else %}Points{% endif %} + +
+ {% if element.description %} +

{{ element.description }}

+ {% endif %} +
+ {% if element.skill %} + Compétence: {{ element.skill }} + {% endif %} + Barème: {{ element.max_points }} {% if element.grading_type == 'points' %}point(s){% else %}max{% endif %} +
+
+
+ + Modifier + + + +
+
+
+ {% endfor %} +
+ {% else %} +
+ + + +

Aucun élément de notation

+

Commencez par ajouter le premier élément de notation pour cet exercice.

+ +
+ {% endif %} +
+
+ + +
+
+

Actions

+
+
+
+ + + + + Saisir les notes + + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/exercise_form.html b/templates/exercise_form.html new file mode 100644 index 0000000..0259db7 --- /dev/null +++ b/templates/exercise_form.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block title %}{{ title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+ + +
+
+

{{ title }}

+

Évaluation : {{ assessment.title }} ({{ assessment.class_group.name }})

+
+ +
+ {{ form.hidden_tag() }} + +
+ + {{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.title.errors %} +
+ {% for error in form.title.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.order(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.order.errors %} +
+ {% for error in form.order.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +

L'ordre détermine l'affichage des exercices dans l'évaluation

+
+ +
+ + 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") }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/grading_element_form.html b/templates/grading_element_form.html new file mode 100644 index 0000000..0bba247 --- /dev/null +++ b/templates/grading_element_form.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} + +{% block title %}{{ title }} - Gestion Scolaire{% endblock %} + +{% block content %} +
+ + +
+
+

{{ title }}

+

{{ assessment.title }} > {{ exercise.title }}

+
+ +
+ {{ form.hidden_tag() }} + +
+ + {{ form.label(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.label.errors %} +
+ {% for error in form.label.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +

Ex: "Calcul de base", "Méthode", "Présentation"

+
+ +
+ + {{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.skill(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.skill.errors %} +
+ {% for error in form.skill.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +

Ex: "Calculer", "Raisonner", "Communiquer"

+
+ +
+
+ + {{ form.max_points(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", step="0.1") }} + {% if form.max_points.errors %} +
+ {% for error in form.max_points.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.grading_type(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }} + {% if form.grading_type.errors %} +
+ {% for error in form.grading_type.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+
+ + +
+

Types de notation

+
+

Points : Notation classique (ex: 2.5/4 points)

+

Score : Évaluation par niveaux (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)

+
+
+ +
+ + 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") }} +
+
+
+
+{% endblock %} \ No newline at end of file