feat: add exercises and scoring
This commit is contained in:
280
app.py
280
app.py
@@ -1,9 +1,9 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField
|
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Email, NumberRange, Optional
|
from wtforms.validators import DataRequired, Email, NumberRange, Optional, Length
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||||
@@ -80,6 +80,52 @@ class Grade(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Grade {self.value} for {self.student.first_name}>'
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
recent_assessments = Assessment.query.order_by(Assessment.date.desc()).limit(5).all()
|
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()
|
assessments = Assessment.query.join(ClassGroup).order_by(Assessment.date.desc()).all()
|
||||||
return render_template('assessments.html', assessments=assessments)
|
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()
|
@app.cli.command()
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Initialize the database with sample data."""
|
"""Initialize the database with sample data."""
|
||||||
|
|||||||
142
templates/assessment_detail.html
Normal file
142
templates/assessment_detail.html
Normal 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 %}
|
||||||
102
templates/assessment_form.html
Normal file
102
templates/assessment_form.html
Normal 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 %}
|
||||||
159
templates/assessment_grading.html
Normal file
159
templates/assessment_grading.html
Normal 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 %}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Gestion des évaluations</h1>
|
<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
|
Nouvelle évaluation
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if assessments %}
|
{% if assessments %}
|
||||||
@@ -42,15 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button class="text-indigo-600 hover:text-indigo-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 exercices
|
Voir détails
|
||||||
</button>
|
</a>
|
||||||
<button class="text-green-600 hover:text-green-900 text-sm font-medium">
|
<a href="{{ url_for('assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium">
|
||||||
Saisir notes
|
Saisir notes
|
||||||
</button>
|
</a>
|
||||||
<button class="text-gray-600 hover:text-gray-900 text-sm font-medium">
|
<a href="{{ url_for('edit_assessment', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium">
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,9 +66,9 @@
|
|||||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucune évaluation</h3>
|
<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>
|
<p class="mt-1 text-sm text-gray-500">Commencez par créer votre première évaluation.</p>
|
||||||
<div class="mt-6">
|
<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
|
Nouvelle évaluation
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
138
templates/exercise_detail.html
Normal file
138
templates/exercise_detail.html
Normal 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 %}
|
||||||
74
templates/exercise_form.html
Normal file
74
templates/exercise_form.html
Normal 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 %}
|
||||||
114
templates/grading_element_form.html
Normal file
114
templates/grading_element_form.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user