feat: add exercises and scoring

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

280
app.py
View File

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

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}{{ assessment.title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<a href="{{ url_for('assessments') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour aux évaluations
</a>
<h1 class="text-2xl font-bold text-gray-900">{{ assessment.title }}</h1>
<p class="text-gray-600">{{ assessment.class_group.name }} - {{ assessment.date.strftime('%d/%m/%Y') }}</p>
</div>
<div class="flex space-x-3">
<a href="{{ url_for('edit_assessment', id=assessment.id) }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cette évaluation ?')) { document.getElementById('delete-form').submit(); }"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Supprimer
</button>
</div>
</div>
<form id="delete-form" method="POST" action="{{ url_for('delete_assessment', id=assessment.id) }}" style="display: none;"></form>
<!-- Informations de l'évaluation -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Informations générales</h2>
</div>
<div class="px-6 py-4">
<dl class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<dt class="text-sm font-medium text-gray-500">Classe</dt>
<dd class="mt-1 text-sm text-gray-900">{{ assessment.class_group.name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Date</dt>
<dd class="mt-1 text-sm text-gray-900">{{ assessment.date.strftime('%d/%m/%Y') }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Coefficient</dt>
<dd class="mt-1 text-sm text-gray-900">{{ assessment.coefficient }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Nombre d'exercices</dt>
<dd class="mt-1 text-sm text-gray-900">{{ assessment.exercises|length }}</dd>
</div>
{% if assessment.description %}
<div class="md:col-span-2">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{ assessment.description }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
<!-- Exercices -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">Exercices</h2>
<a href="{{ url_for('new_exercise', assessment_id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un exercice
</a>
</div>
<div class="px-6 py-4">
{% if assessment.exercises %}
<div class="space-y-4">
{% for exercise in assessment.exercises|sort(attribute='order') %}
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-sm font-medium text-gray-900">{{ exercise.title }}</h3>
{% if exercise.description %}
<p class="text-sm text-gray-600 mt-1">{{ exercise.description }}</p>
{% endif %}
<div class="text-xs text-gray-500 mt-2">
{{ exercise.grading_elements|length }} élément(s) de notation
</div>
</div>
<div class="flex space-x-2 ml-4">
<a href="{{ url_for('exercise_detail', assessment_id=assessment.id, id=exercise.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Voir détails
</a>
<a href="{{ url_for('edit_exercise', assessment_id=assessment.id, id=exercise.id) }}" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
Modifier
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8m9-16v20m5-14H9m1 4h8m1 2h8m-8 6h8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun exercice</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par ajouter le premier exercice de cette évaluation.</p>
<div class="mt-6">
<a href="{{ url_for('new_exercise', assessment_id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un exercice
</a>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Actions rapides -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Actions</h2>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="{{ url_for('assessment_grading', assessment_id=assessment.id) }}" class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8z" clip-rule="evenodd"/>
</svg>
Saisir les notes
</a>
<button class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1v-1zM3 4a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1V4z" clip-rule="evenodd"/>
</svg>
Voir les résultats
</button>
<button class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1v-1zM3 4a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1V4z" clip-rule="evenodd"/>
</svg>
Exporter
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('assessments') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour aux évaluations
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.title.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.title.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="{{ form.date.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.date.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.date.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.coefficient.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.coefficient.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.coefficient.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div>
<label for="{{ form.class_group_id.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.class_group_id.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.class_group_id.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('assessments') }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block title %}Saisie des notes - {{ assessment.title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour à l'évaluation
</a>
<h1 class="text-2xl font-bold text-gray-900">Saisie des notes</h1>
<p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p>
</div>
</div>
{% if not grading_elements %}
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Aucun élément de notation</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>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.</p>
</div>
<div class="mt-4">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-sm font-medium text-yellow-800 underline hover:text-yellow-900">
Configurer l'évaluation →
</a>
</div>
</div>
</div>
</div>
{% else %}
<form method="POST" action="{{ url_for('save_grades', assessment_id=assessment.id) }}" class="space-y-6">
<!-- Informations sur les types de notation -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<h3 class="text-sm font-medium text-blue-900 mb-2">Guide de saisie</h3>
<div class="text-xs text-blue-800 space-y-1">
<p><strong>Points :</strong> Saisissez une valeur numérique (ex: 2.5, 3, 0)</p>
<p><strong>Score :</strong> 0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué</p>
</div>
</div>
<!-- Tableau de saisie -->
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Grille de notation</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50">
Élève
</th>
{% for element in grading_elements %}
<th scope="col" class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-32">
<div>{{ element.label }}</div>
<div class="font-normal text-xs mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
{% if element.grading_type == 'score' %}bg-purple-100 text-purple-800{% else %}bg-green-100 text-green-800{% endif %}">
{% if element.grading_type == 'score' %}Score/{{ element.max_points|int }}{% else %}/{{ element.max_points }}{% endif %}
</span>
</div>
{% if element.skill %}
<div class="text-xs text-gray-400 mt-1">{{ element.skill }}</div>
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for student in students %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 sticky left-0 bg-white">
{{ student.first_name }} {{ student.last_name }}
</td>
{% for element in grading_elements %}
{% set grade_key = student.id ~ '_' ~ element.id %}
{% set existing_grade = existing_grades.get(grade_key) %}
<td class="px-3 py-4 whitespace-nowrap text-center">
<div class="space-y-2">
{% if element.grading_type == 'score' %}
<select name="grade_{{ student.id }}_{{ element.id }}" class="block w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
<option value="">-</option>
<option value="." {% if existing_grade and existing_grade.value == '.' %}selected{% endif %}>. (non évalué)</option>
<option value="0" {% if existing_grade and existing_grade.value == '0' %}selected{% endif %}>0 (non acquis)</option>
<option value="1" {% if existing_grade and existing_grade.value == '1' %}selected{% endif %}>1 (en cours)</option>
<option value="2" {% if existing_grade and existing_grade.value == '2' %}selected{% endif %}>2 (acquis)</option>
<option value="3" {% if existing_grade and existing_grade.value == '3' %}selected{% endif %}>3 (expert)</option>
</select>
{% else %}
<input type="number" step="0.1" min="0" max="{{ element.max_points }}"
name="grade_{{ student.id }}_{{ element.id }}"
value="{% if existing_grade %}{{ existing_grade.value }}{% endif %}"
class="block w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 text-center"
placeholder="0">
{% endif %}
<input type="text"
name="comment_{{ student.id }}_{{ element.id }}"
value="{% if existing_grade and existing_grade.comment %}{{ existing_grade.comment }}{% endif %}"
class="block w-full text-xs border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
placeholder="Commentaire (optionnel)">
</div>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end space-x-3">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
Sauvegarder les notes
</button>
</div>
</div>
</form>
<!-- Légende -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Légende des exercices</h3>
</div>
<div class="px-6 py-4">
<div class="space-y-4">
{% for exercise in assessment.exercises|sort(attribute='order') %}
<div class="border-l-4 border-blue-500 pl-4">
<h4 class="font-medium text-gray-900">{{ exercise.title }}</h4>
{% if exercise.description %}
<p class="text-sm text-gray-600 mt-1">{{ exercise.description }}</p>
{% endif %}
<div class="mt-2 space-y-1">
{% for element in exercise.grading_elements %}
<div class="text-sm text-gray-700">
<span class="font-medium">{{ element.label }}</span>
{% if element.skill %} - {{ element.skill }}{% endif %}
{% if element.description %} : {{ element.description }}{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -6,9 +6,9 @@
<div class="space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900">Gestion des évaluations</h1>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
<a href="{{ url_for('new_assessment') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation
</button>
</a>
</div>
{% if assessments %}
@@ -42,15 +42,15 @@
</div>
</div>
<div class="flex space-x-2">
<button class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Voir exercices
</button>
<button class="text-green-600 hover:text-green-900 text-sm font-medium">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Voir détails
</a>
<a href="{{ url_for('assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium">
Saisir notes
</button>
<button class="text-gray-600 hover:text-gray-900 text-sm font-medium">
</a>
<a href="{{ url_for('edit_assessment', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium">
Modifier
</button>
</a>
</div>
</div>
</div>
@@ -66,9 +66,9 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucune évaluation</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par créer votre première évaluation.</p>
<div class="mt-6">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
<a href="{{ url_for('new_assessment') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation
</button>
</a>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block title %}{{ exercise.title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour à l'évaluation "{{ assessment.title }}"
</a>
<h1 class="text-2xl font-bold text-gray-900">{{ exercise.title }}</h1>
<p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p>
</div>
<div class="flex space-x-3">
<a href="{{ url_for('edit_exercise', assessment_id=assessment.id, id=exercise.id) }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet exercice ?')) { document.getElementById('delete-form').submit(); }"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Supprimer
</button>
</div>
</div>
<form id="delete-form" method="POST" action="{{ url_for('delete_exercise', assessment_id=assessment.id, id=exercise.id) }}" style="display: none;"></form>
<!-- Informations de l'exercice -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Informations de l'exercice</h2>
</div>
<div class="px-6 py-4">
<dl class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<dt class="text-sm font-medium text-gray-500">Ordre</dt>
<dd class="mt-1 text-sm text-gray-900">{{ exercise.order }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Nombre d'éléments de notation</dt>
<dd class="mt-1 text-sm text-gray-900">{{ exercise.grading_elements|length }}</dd>
</div>
{% if exercise.description %}
<div class="md:col-span-2">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{ exercise.description }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
<!-- Éléments de notation -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">Éléments de notation</h2>
<a href="{{ url_for('new_grading_element', assessment_id=assessment.id, exercise_id=exercise.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un élément
</a>
</div>
<div class="px-6 py-4">
{% if exercise.grading_elements %}
<div class="space-y-4">
{% for element in exercise.grading_elements %}
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3">
<h3 class="text-sm font-medium text-gray-900">{{ element.label }}</h3>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{% if element.grading_type == 'score' %}bg-purple-100 text-purple-800{% else %}bg-green-100 text-green-800{% endif %}">
{% if element.grading_type == 'score' %}Score (0-3){% else %}Points{% endif %}
</span>
</div>
{% if element.description %}
<p class="text-sm text-gray-600 mt-1">{{ element.description }}</p>
{% endif %}
<div class="flex items-center space-x-4 text-xs text-gray-500 mt-2">
{% if element.skill %}
<span><strong>Compétence:</strong> {{ element.skill }}</span>
{% endif %}
<span><strong>Barème:</strong> {{ element.max_points }} {% if element.grading_type == 'points' %}point(s){% else %}max{% endif %}</span>
</div>
</div>
<div class="flex space-x-2 ml-4">
<a href="{{ url_for('edit_grading_element', assessment_id=assessment.id, exercise_id=exercise.id, id=element.id) }}" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
Modifier
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet élément ?')) { document.getElementById('delete-element-{{ element.id }}').submit(); }" class="text-red-600 hover:text-red-800 text-sm font-medium">
Supprimer
</button>
<form id="delete-element-{{ element.id }}" method="POST" action="{{ url_for('delete_grading_element', assessment_id=assessment.id, exercise_id=exercise.id, id=element.id) }}" style="display: none;"></form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8m9-16v20m5-14H9m1 4h8m1 2h8m-8 6h8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun élément de notation</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par ajouter le premier élément de notation pour cet exercice.</p>
<div class="mt-6">
<a href="{{ url_for('new_grading_element', assessment_id=assessment.id, exercise_id=exercise.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un élément
</a>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Actions rapides -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Actions</h2>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<a href="{{ url_for('assessment_grading', assessment_id=assessment.id) }}" class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8z" clip-rule="evenodd"/>
</svg>
Saisir les notes
</a>
<button class="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1v-1zM3 4a1 1 0 011-1h12a1 1 0 011 1v1a1 1 0 01-1 1H4a1 1 0 01-1-1V4z" clip-rule="evenodd"/>
</svg>
Voir les résultats
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'évaluation "{{ assessment.title }}"
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
<p class="text-sm text-gray-600 mt-1">Évaluation : {{ assessment.title }} ({{ assessment.class_group.name }})</p>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.title.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.title.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.order.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.order.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.order.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">L'ordre détermine l'affichage des exercices dans l'évaluation</p>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('exercise_detail', assessment_id=assessment.id, id=exercise.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'exercice "{{ exercise.title }}"
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
<p class="text-sm text-gray-600 mt-1">{{ assessment.title }} > {{ exercise.title }}</p>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.label.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.label.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.label.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">Ex: "Calcul de base", "Méthode", "Présentation"</p>
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.skill.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.skill.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.skill.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">Ex: "Calculer", "Raisonner", "Communiquer"</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="{{ form.max_points.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.max_points.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.max_points.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.grading_type.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.grading_type.label.text }}
</label>
{{ 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 %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.grading_type.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<!-- Aide sur les types de notation -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<h4 class="text-sm font-medium text-blue-900 mb-2">Types de notation</h4>
<div class="text-xs text-blue-800 space-y-1">
<p><strong>Points :</strong> Notation classique (ex: 2.5/4 points)</p>
<p><strong>Score :</strong> Évaluation par niveaux (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)</p>
</div>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('exercise_detail', assessment_id=assessment.id, id=exercise.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}