refactor: restructure codebase into modular architecture

- Split monolithic app.py (400+ lines) into organized modules
- Extract models, forms, and commands into separate files
- Implement Flask blueprints for route organization
- Maintain full functionality with cleaner architecture
- Update all templates to use new blueprint URLs
- Enhance README with technical documentation

Structure:
├── app.py (50 lines) - Flask app factory
├── models.py (62 lines) - SQLAlchemy models
├── forms.py (43 lines) - WTForms definitions
├── commands.py (74 lines) - CLI commands
└── routes/ - Blueprint modules for each feature

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-03 20:59:10 +02:00
parent 7afe54d877
commit 3e49bd467c
18 changed files with 562 additions and 495 deletions

View File

@@ -1,6 +1,19 @@
# Gestion Scolaire # Gestion Scolaire
Application web pour gérer les élèves, évaluations et notes. Application web Flask pour gérer les élèves, évaluations et notes dans un contexte scolaire.
## Fonctionnalités
### ✅ Gestion complète des évaluations
- **Groupes classes** : Organisation des élèves par classe
- **Évaluations** : Création et gestion des contrôles/devoirs
- **Exercices** : Structure hiérarchique des évaluations
- **Éléments de notation** : Configuration fine des critères
- **Saisie des notes** : Interface intuitive pour la notation
### 🎯 Types de notation supportés
- **Points** : Notation classique (ex: 2.5/4 points)
- **Score** : Évaluation par compétences (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)
## Installation et lancement ## Installation et lancement
@@ -20,19 +33,31 @@ uv run flask --app app run --debug
uv run flask --app app run uv run flask --app app run
``` ```
## Fonctionnalités ## Architecture technique
- Gestion des groupes classes ### Structure du code
- Gestion des élèves ```
- Création d'évaluations avec exercices ├── app.py # Application Flask principale
- Éléments de notation configurables (score ou points) ├── models.py # Modèles SQLAlchemy
- Saisie et suivi des notes ├── forms.py # Formulaires WTForms
├── commands.py # Commandes CLI Flask
## Architecture ├── routes/
│ ├── assessments.py # Routes pour les évaluations
│ ├── exercises.py # Routes pour les exercices
│ └── grading.py # Routes pour la saisie des notes
└── templates/ # Templates Jinja2 avec TailwindCSS
```
### Modèles de données
- **ClassGroup** : Groupes de classes (ex: 6ème A, 5ème B) - **ClassGroup** : Groupes de classes (ex: 6ème A, 5ème B)
- **Student** : Élèves assignés à un groupe classe - **Student** : Élèves assignés à un groupe classe
- **Assessment** : Évaluations liées à un groupe classe - **Assessment** : Évaluations liées à un groupe classe
- **Exercise** : Exercices composant une évaluation - **Exercise** : Exercices composant une évaluation
- **GradingElement** : Éléments de notation avec barème - **GradingElement** : Éléments de notation avec barème et type
- **Grade** : Notes attribuées aux éléments de notation - **Grade** : Notes attribuées aux éléments de notation
### Technologies utilisées
- **Backend** : Flask, SQLAlchemy, WTForms
- **Frontend** : TailwindCSS, Jinja2
- **Base de données** : SQLite
- **Gestionnaire de paquets** : uv

450
app.py
View File

@@ -1,131 +1,30 @@
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy from models import db, Assessment, Student, ClassGroup
from flask_wtf import FlaskForm from commands import init_db
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Email, NumberRange, Optional, Length
from datetime import datetime, date
# Import blueprints
from routes.assessments import bp as assessments_bp
from routes.exercises import bp as exercises_bp
from routes.grading import bp as grading_bp
def create_app():
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here' app.config['SECRET_KEY'] = 'your-secret-key-here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app) # Initialize extensions
db.init_app(app)
class ClassGroup(db.Model): # Register blueprints
id = db.Column(db.Integer, primary_key=True) app.register_blueprint(assessments_bp)
name = db.Column(db.String(100), nullable=False, unique=True) app.register_blueprint(exercises_bp)
description = db.Column(db.Text) app.register_blueprint(grading_bp)
year = db.Column(db.String(20), nullable=False)
students = db.relationship('Student', backref='class_group', lazy=True)
assessments = db.relationship('Assessment', backref='class_group', lazy=True)
def __repr__(self): # Register CLI commands
return f'<ClassGroup {self.name}>' app.cli.add_command(init_db)
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
last_name = db.Column(db.String(100), nullable=False)
first_name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True)
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
grades = db.relationship('Grade', backref='student', lazy=True)
def __repr__(self):
return f'<Student {self.first_name} {self.last_name}>'
class Assessment(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
coefficient = db.Column(db.Float, default=1.0)
exercises = db.relationship('Exercise', backref='assessment', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Assessment {self.title}>'
class Exercise(db.Model):
id = db.Column(db.Integer, primary_key=True)
assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
order = db.Column(db.Integer, default=1)
grading_elements = db.relationship('GradingElement', backref='exercise', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Exercise {self.title}>'
class GradingElement(db.Model):
id = db.Column(db.Integer, primary_key=True)
exercise_id = db.Column(db.Integer, db.ForeignKey('exercise.id'), nullable=False)
label = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
skill = db.Column(db.String(200))
max_points = db.Column(db.Float, nullable=False)
grading_type = db.Column(db.String(10), nullable=False, default='points') # 'score' or 'points'
grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<GradingElement {self.label}>'
class Grade(db.Model):
id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False)
grading_element_id = db.Column(db.Integer, db.ForeignKey('grading_element.id'), nullable=False)
value = db.Column(db.String(10)) # String to handle scores (0,1,2,3,.) and points
comment = db.Column(db.Text)
def __repr__(self):
return f'<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')
# Main routes
@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()
@@ -148,319 +47,10 @@ def students():
students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all() students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all()
return render_template('students.html', students=students) return render_template('students.html', students=students)
@app.route('/assessments') return app
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."""
db.create_all()
# Check if data already exists
if ClassGroup.query.first():
print("Database already initialized!")
return
# Create sample class groups
classe_6a = ClassGroup(name="6ème A", description="Classe de 6ème A", year="2024-2025")
classe_5b = ClassGroup(name="5ème B", description="Classe de 5ème B", year="2024-2025")
db.session.add(classe_6a)
db.session.add(classe_5b)
db.session.commit()
# Create sample students
students_data = [
("Dupont", "Marie", "marie.dupont@email.com", classe_6a.id),
("Martin", "Pierre", "pierre.martin@email.com", classe_6a.id),
("Durand", "Sophie", "sophie.durand@email.com", classe_6a.id),
("Moreau", "Lucas", "lucas.moreau@email.com", classe_5b.id),
("Bernard", "Emma", "emma.bernard@email.com", classe_5b.id),
]
for last_name, first_name, email, class_group_id in students_data:
student = Student(
last_name=last_name,
first_name=first_name,
email=email,
class_group_id=class_group_id
)
db.session.add(student)
db.session.commit()
# Create sample assessment
assessment = Assessment(
title="Évaluation de mathématiques",
description="Évaluation sur les fractions et les décimaux",
class_group_id=classe_6a.id,
coefficient=2.0
)
db.session.add(assessment)
db.session.commit()
# Create sample exercise
exercise = Exercise(
assessment_id=assessment.id,
title="Exercice 1 - Fractions",
description="Calculs avec les fractions",
order=1
)
db.session.add(exercise)
db.session.commit()
# Create sample grading elements
elements_data = [
("Calcul de base", "Addition et soustraction de fractions", "Calculer", 4.0, "points"),
("Méthode", "Justification de la méthode utilisée", "Raisonner", 2.0, "score"),
("Présentation", "Clarté de la présentation", "Communiquer", 2.0, "score"),
]
for label, description, skill, max_points, grading_type in elements_data:
element = GradingElement(
exercise_id=exercise.id,
label=label,
description=description,
skill=skill,
max_points=max_points,
grading_type=grading_type
)
db.session.add(element)
db.session.commit()
print("Database initialized with sample data!")
if __name__ == '__main__': if __name__ == '__main__':
app = create_app()
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
app.run(debug=True) app.run(debug=True)

83
commands.py Normal file
View File

@@ -0,0 +1,83 @@
import click
from flask.cli import with_appcontext
from models import db, ClassGroup, Student, Assessment, Exercise, GradingElement
@click.command()
@with_appcontext
def init_db():
"""Initialize the database with sample data."""
db.create_all()
# Check if data already exists
if ClassGroup.query.first():
click.echo("Database already initialized!")
return
# Create sample class groups
classe_6a = ClassGroup(name="6ème A", description="Classe de 6ème A", year="2024-2025")
classe_5b = ClassGroup(name="5ème B", description="Classe de 5ème B", year="2024-2025")
db.session.add(classe_6a)
db.session.add(classe_5b)
db.session.commit()
# Create sample students
students_data = [
("Dupont", "Marie", "marie.dupont@email.com", classe_6a.id),
("Martin", "Pierre", "pierre.martin@email.com", classe_6a.id),
("Durand", "Sophie", "sophie.durand@email.com", classe_6a.id),
("Moreau", "Lucas", "lucas.moreau@email.com", classe_5b.id),
("Bernard", "Emma", "emma.bernard@email.com", classe_5b.id),
]
for last_name, first_name, email, class_group_id in students_data:
student = Student(
last_name=last_name,
first_name=first_name,
email=email,
class_group_id=class_group_id
)
db.session.add(student)
db.session.commit()
# Create sample assessment
assessment = Assessment(
title="Évaluation de mathématiques",
description="Évaluation sur les fractions et les décimaux",
class_group_id=classe_6a.id,
coefficient=2.0
)
db.session.add(assessment)
db.session.commit()
# Create sample exercise
exercise = Exercise(
assessment_id=assessment.id,
title="Exercice 1 - Fractions",
description="Calculs avec les fractions",
order=1
)
db.session.add(exercise)
db.session.commit()
# Create sample grading elements
elements_data = [
("Calcul de base", "Addition et soustraction de fractions", "Calculer", 4.0, "points"),
("Méthode", "Justification de la méthode utilisée", "Raisonner", 2.0, "score"),
("Présentation", "Clarté de la présentation", "Communiquer", 2.0, "score"),
]
for label, description, skill, max_points, grading_type in elements_data:
element = GradingElement(
exercise_id=exercise.id,
label=label,
description=description,
skill=skill,
max_points=max_points,
grading_type=grading_type
)
db.session.add(element)
db.session.commit()
click.echo("Database initialized with sample data!")

50
forms.py Normal file
View File

@@ -0,0 +1,50 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Email, NumberRange, Optional, Length
from datetime import date
from models import ClassGroup
class AssessmentForm(FlaskForm):
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
date = DateField('Date', validators=[DataRequired()], default=date.today)
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
coefficient = FloatField('Coefficient', validators=[DataRequired(), NumberRange(min=0.1, max=10)], default=1.0)
submit = SubmitField('Enregistrer')
def __init__(self, *args, **kwargs):
super(AssessmentForm, self).__init__(*args, **kwargs)
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
class ClassGroupForm(FlaskForm):
name = StringField('Nom de la classe', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description', validators=[Optional()])
year = StringField('Année scolaire', validators=[DataRequired(), Length(max=20)], default="2024-2025")
submit = SubmitField('Enregistrer')
class StudentForm(FlaskForm):
first_name = StringField('Prénom', validators=[DataRequired(), Length(max=100)])
last_name = StringField('Nom', validators=[DataRequired(), Length(max=100)])
email = StringField('Email', validators=[Optional(), Email(), Length(max=120)])
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
submit = SubmitField('Enregistrer')
def __init__(self, *args, **kwargs):
super(StudentForm, self).__init__(*args, **kwargs)
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
class ExerciseForm(FlaskForm):
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
order = IntegerField('Ordre', validators=[DataRequired(), NumberRange(min=1)], default=1)
submit = SubmitField('Enregistrer')
class GradingElementForm(FlaskForm):
label = StringField('Libellé', validators=[DataRequired(), Length(max=200)])
description = TextAreaField('Description', validators=[Optional()])
skill = StringField('Compétence', validators=[Optional(), Length(max=200)])
max_points = FloatField('Barème (points max)', validators=[DataRequired(), NumberRange(min=0.1)], default=1.0)
grading_type = SelectField('Type de notation', validators=[DataRequired()],
choices=[('points', 'Points (ex: 2.5/4)'), ('score', 'Score (0, 1, 2, 3, .)')],
default='points')
submit = SubmitField('Enregistrer')

72
models.py Normal file
View File

@@ -0,0 +1,72 @@
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class ClassGroup(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
description = db.Column(db.Text)
year = db.Column(db.String(20), nullable=False)
students = db.relationship('Student', backref='class_group', lazy=True)
assessments = db.relationship('Assessment', backref='class_group', lazy=True)
def __repr__(self):
return f'<ClassGroup {self.name}>'
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
last_name = db.Column(db.String(100), nullable=False)
first_name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True)
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
grades = db.relationship('Grade', backref='student', lazy=True)
def __repr__(self):
return f'<Student {self.first_name} {self.last_name}>'
class Assessment(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
coefficient = db.Column(db.Float, default=1.0)
exercises = db.relationship('Exercise', backref='assessment', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Assessment {self.title}>'
class Exercise(db.Model):
id = db.Column(db.Integer, primary_key=True)
assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
order = db.Column(db.Integer, default=1)
grading_elements = db.relationship('GradingElement', backref='exercise', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Exercise {self.title}>'
class GradingElement(db.Model):
id = db.Column(db.Integer, primary_key=True)
exercise_id = db.Column(db.Integer, db.ForeignKey('exercise.id'), nullable=False)
label = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
skill = db.Column(db.String(200))
max_points = db.Column(db.Float, nullable=False)
grading_type = db.Column(db.String(10), nullable=False, default='points') # 'score' or 'points'
grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<GradingElement {self.label}>'
class Grade(db.Model):
id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False)
grading_element_id = db.Column(db.Integer, db.ForeignKey('grading_element.id'), nullable=False)
value = db.Column(db.String(10)) # String to handle scores (0,1,2,3,.) and points
comment = db.Column(db.Text)
def __repr__(self):
return f'<Grade {self.value} for {self.student.first_name}>'

1
routes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Routes package

55
routes/assessments.py Normal file
View File

@@ -0,0 +1,55 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from models import db, Assessment, ClassGroup
from forms import AssessmentForm
bp = Blueprint('assessments', __name__, url_prefix='/assessments')
@bp.route('/')
def list():
assessments = Assessment.query.join(ClassGroup).order_by(Assessment.date.desc()).all()
return render_template('assessments.html', assessments=assessments)
@bp.route('/new', methods=['GET', 'POST'])
def new():
form = AssessmentForm()
if form.validate_on_submit():
assessment = Assessment(
title=form.title.data,
description=form.description.data,
date=form.date.data,
class_group_id=form.class_group_id.data,
coefficient=form.coefficient.data
)
db.session.add(assessment)
db.session.commit()
flash('Évaluation créée avec succès !', 'success')
return redirect(url_for('assessments.detail', id=assessment.id))
return render_template('assessment_form.html', form=form, title='Nouvelle évaluation')
@bp.route('/<int:id>')
def detail(id):
assessment = Assessment.query.get_or_404(id)
return render_template('assessment_detail.html', assessment=assessment)
@bp.route('/<int:id>/edit', methods=['GET', 'POST'])
def edit(id):
assessment = Assessment.query.get_or_404(id)
form = AssessmentForm(obj=assessment)
if form.validate_on_submit():
assessment.title = form.title.data
assessment.description = form.description.data
assessment.date = form.date.data
assessment.class_group_id = form.class_group_id.data
assessment.coefficient = form.coefficient.data
db.session.commit()
flash('Évaluation modifiée avec succès !', 'success')
return redirect(url_for('assessments.detail', id=assessment.id))
return render_template('assessment_form.html', form=form, title='Modifier l\'évaluation', assessment=assessment)
@bp.route('/<int:id>/delete', methods=['POST'])
def delete(id):
assessment = Assessment.query.get_or_404(id)
db.session.delete(assessment)
db.session.commit()
flash('Évaluation supprimée avec succès !', 'success')
return redirect(url_for('assessments.list'))

113
routes/exercises.py Normal file
View File

@@ -0,0 +1,113 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from models import db, Assessment, Exercise, GradingElement
from forms import ExerciseForm, GradingElementForm
bp = Blueprint('exercises', __name__)
# Exercise routes
@bp.route('/assessments/<int:assessment_id>/exercises/new', methods=['GET', 'POST'])
def new(assessment_id):
assessment = Assessment.query.get_or_404(assessment_id)
form = ExerciseForm()
# Set default order to next available
if form.order.data == 1: # Only if it's the default value
max_order = db.session.query(db.func.max(Exercise.order)).filter_by(assessment_id=assessment_id).scalar()
form.order.data = (max_order or 0) + 1
if form.validate_on_submit():
exercise = Exercise(
assessment_id=assessment_id,
title=form.title.data,
description=form.description.data,
order=form.order.data
)
db.session.add(exercise)
db.session.commit()
flash('Exercice créé avec succès !', 'success')
return redirect(url_for('exercises.detail', assessment_id=assessment_id, id=exercise.id))
return render_template('exercise_form.html', form=form, assessment=assessment, title='Nouvel exercice')
@bp.route('/assessments/<int:assessment_id>/exercises/<int:id>')
def detail(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
return render_template('exercise_detail.html', assessment=assessment, exercise=exercise)
@bp.route('/assessments/<int:assessment_id>/exercises/<int:id>/edit', methods=['GET', 'POST'])
def edit(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
form = ExerciseForm(obj=exercise)
if form.validate_on_submit():
exercise.title = form.title.data
exercise.description = form.description.data
exercise.order = form.order.data
db.session.commit()
flash('Exercice modifié avec succès !', 'success')
return redirect(url_for('exercises.detail', assessment_id=assessment_id, id=exercise.id))
return render_template('exercise_form.html', form=form, assessment=assessment, exercise=exercise, title='Modifier l\'exercice')
@bp.route('/assessments/<int:assessment_id>/exercises/<int:id>/delete', methods=['POST'])
def delete(assessment_id, id):
assessment = Assessment.query.get_or_404(assessment_id)
exercise = Exercise.query.filter_by(id=id, assessment_id=assessment_id).first_or_404()
db.session.delete(exercise)
db.session.commit()
flash('Exercice supprimé avec succès !', 'success')
return redirect(url_for('assessments.detail', id=assessment_id))
# GradingElement routes
@bp.route('/assessments/<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('exercises.detail', assessment_id=assessment_id, id=exercise_id))
return render_template('grading_element_form.html', form=form, assessment=assessment, exercise=exercise, title='Nouvel élément de notation')
@bp.route('/assessments/<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('exercises.detail', assessment_id=assessment_id, id=exercise_id))
return render_template('grading_element_form.html', form=form, assessment=assessment, exercise=exercise, element=element, title='Modifier l\'élément de notation')
@bp.route('/assessments/<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('exercises.detail', assessment_id=assessment_id, id=exercise_id))

78
routes/grading.py Normal file
View File

@@ -0,0 +1,78 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from models import db, Assessment, Student, Grade, GradingElement, Exercise
bp = Blueprint('grading', __name__)
@bp.route('/assessments/<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)
@bp.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('grading.assessment_grading', assessment_id=assessment_id))

View File

@@ -6,14 +6,14 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<a href="{{ url_for('assessments') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block"> <a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour aux évaluations ← Retour aux évaluations
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ assessment.title }}</h1> <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> <p class="text-gray-600">{{ assessment.class_group.name }} - {{ assessment.date.strftime('%d/%m/%Y') }}</p>
</div> </div>
<div class="flex space-x-3"> <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"> <a href="{{ url_for('assessments.edit', 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 Modifier
</a> </a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cette évaluation ?')) { document.getElementById('delete-form').submit(); }" <button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cette évaluation ?')) { document.getElementById('delete-form').submit(); }"
@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<form id="delete-form" method="POST" action="{{ url_for('delete_assessment', id=assessment.id) }}" style="display: none;"></form> <form id="delete-form" method="POST" action="{{ url_for('assessments.delete', id=assessment.id) }}" style="display: none;"></form>
<!-- Informations de l'évaluation --> <!-- Informations de l'évaluation -->
<div class="bg-white shadow rounded-lg"> <div class="bg-white shadow rounded-lg">
@@ -62,7 +62,7 @@
<div class="bg-white shadow rounded-lg"> <div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> <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> <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"> <a href="{{ url_for('exercises.new', 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 Ajouter un exercice
</a> </a>
</div> </div>
@@ -82,10 +82,10 @@
</div> </div>
</div> </div>
<div class="flex space-x-2 ml-4"> <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"> <a href="{{ url_for('exercises.detail', assessment_id=assessment.id, id=exercise.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Voir détails Voir détails
</a> </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"> <a href="{{ url_for('exercises.edit', assessment_id=assessment.id, id=exercise.id) }}" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
Modifier Modifier
</a> </a>
</div> </div>
@@ -101,7 +101,7 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun exercice</h3> <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> <p class="mt-1 text-sm text-gray-500">Commencez par ajouter le premier exercice de cette évaluation.</p>
<div class="mt-6"> <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"> <a href="{{ url_for('exercises.new', 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 Ajouter un exercice
</a> </a>
</div> </div>
@@ -117,7 +117,7 @@
</div> </div>
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-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"> <a href="{{ url_for('grading.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"> <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"/> <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> </svg>

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="mb-6"> <div class="mb-6">
<a href="{{ url_for('assessments') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium"> <a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour aux évaluations ← Retour aux évaluations
</a> </a>
</div> </div>
@@ -91,7 +91,7 @@
</div> </div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200"> <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"> <a href="{{ url_for('assessments.list') }}" 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 Annuler
</a> </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") }} {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}

View File

@@ -6,7 +6,7 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <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"> <a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
← Retour à l'évaluation ← Retour à l'évaluation
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">Saisie des notes</h1> <h1 class="text-2xl font-bold text-gray-900">Saisie des notes</h1>
@@ -28,7 +28,7 @@
<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> <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>
<div class="mt-4"> <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"> <a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-sm font-medium text-yellow-800 underline hover:text-yellow-900">
Configurer l'évaluation → Configurer l'évaluation →
</a> </a>
</div> </div>
@@ -36,7 +36,7 @@
</div> </div>
</div> </div>
{% else %} {% else %}
<form method="POST" action="{{ url_for('save_grades', assessment_id=assessment.id) }}" class="space-y-6"> <form method="POST" action="{{ url_for('grading.save_grades', assessment_id=assessment.id) }}" class="space-y-6">
<!-- Informations sur les types de notation --> <!-- Informations sur les types de notation -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-4"> <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> <h3 class="text-sm font-medium text-blue-900 mb-2">Guide de saisie</h3>
@@ -117,7 +117,7 @@
</div> </div>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end space-x-3"> <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"> <a href="{{ url_for('assessments.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 Annuler
</a> </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"> <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">

View File

@@ -6,7 +6,7 @@
<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>
<a href="{{ url_for('new_assessment') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors"> <a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation Nouvelle évaluation
</a> </a>
</div> </div>
@@ -42,13 +42,13 @@
</div> </div>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<a href="{{ url_for('assessment_detail', id=assessment.id) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium"> <a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Voir détails Voir détails
</a> </a>
<a href="{{ url_for('assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium"> <a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium">
Saisir notes Saisir notes
</a> </a>
<a href="{{ url_for('edit_assessment', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium"> <a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium">
Modifier Modifier
</a> </a>
</div> </div>
@@ -66,7 +66,7 @@
<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">
<a href="{{ url_for('new_assessment') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors"> <a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation Nouvelle évaluation
</a> </a>
</div> </div>

View File

@@ -19,7 +19,7 @@
<a href="{{ url_for('index') }}" class="hover:text-blue-200 transition-colors">Accueil</a> <a href="{{ url_for('index') }}" class="hover:text-blue-200 transition-colors">Accueil</a>
<a href="{{ url_for('classes') }}" class="hover:text-blue-200 transition-colors">Classes</a> <a href="{{ url_for('classes') }}" class="hover:text-blue-200 transition-colors">Classes</a>
<a href="{{ url_for('students') }}" class="hover:text-blue-200 transition-colors">Élèves</a> <a href="{{ url_for('students') }}" class="hover:text-blue-200 transition-colors">Élèves</a>
<a href="{{ url_for('assessments') }}" class="hover:text-blue-200 transition-colors">Évaluations</a> <a href="{{ url_for('assessments.list') }}" class="hover:text-blue-200 transition-colors">Évaluations</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,14 +6,14 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <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"> <a href="{{ url_for('assessments.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 }}" ← Retour à l'évaluation "{{ assessment.title }}"
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ exercise.title }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ exercise.title }}</h1>
<p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p> <p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p>
</div> </div>
<div class="flex space-x-3"> <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"> <a href="{{ url_for('exercises.edit', 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 Modifier
</a> </a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet exercice ?')) { document.getElementById('delete-form').submit(); }" <button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet exercice ?')) { document.getElementById('delete-form').submit(); }"
@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<form id="delete-form" method="POST" action="{{ url_for('delete_exercise', assessment_id=assessment.id, id=exercise.id) }}" style="display: none;"></form> <form id="delete-form" method="POST" action="{{ url_for('exercises.delete', assessment_id=assessment.id, id=exercise.id) }}" style="display: none;"></form>
<!-- Informations de l'exercice --> <!-- Informations de l'exercice -->
<div class="bg-white shadow rounded-lg"> <div class="bg-white shadow rounded-lg">
@@ -54,7 +54,7 @@
<div class="bg-white shadow rounded-lg"> <div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> <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> <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"> <a href="{{ url_for('exercises.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 Ajouter un élément
</a> </a>
</div> </div>
@@ -83,13 +83,13 @@
</div> </div>
</div> </div>
<div class="flex space-x-2 ml-4"> <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"> <a href="{{ url_for('exercises.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 Modifier
</a> </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"> <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 Supprimer
</button> </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> <form id="delete-element-{{ element.id }}" method="POST" action="{{ url_for('exercises.delete_grading_element', assessment_id=assessment.id, exercise_id=exercise.id, id=element.id) }}" style="display: none;"></form>
</div> </div>
</div> </div>
</div> </div>
@@ -119,7 +119,7 @@
</div> </div>
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-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"> <a href="{{ url_for('grading.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"> <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"/> <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> </svg>

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="mb-6"> <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"> <a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'évaluation "{{ assessment.title }}" ← Retour à l'évaluation "{{ assessment.title }}"
</a> </a>
</div> </div>
@@ -63,7 +63,7 @@
</div> </div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200"> <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"> <a href="{{ url_for('assessments.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 Annuler
</a> </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") }} {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="mb-6"> <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"> <a href="{{ url_for('exercises.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 }}" ← Retour à l'exercice "{{ exercise.title }}"
</a> </a>
</div> </div>
@@ -103,7 +103,7 @@
</div> </div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200"> <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"> <a href="{{ url_for('exercises.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 Annuler
</a> </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") }} {{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}

View File

@@ -114,7 +114,7 @@
<div class="text-sm font-medium text-gray-900">Gérer les élèves</div> <div class="text-sm font-medium text-gray-900">Gérer les élèves</div>
</div> </div>
</a> </a>
<a href="{{ url_for('assessments') }}" class="block p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"> <a href="{{ url_for('assessments.list') }}" class="block p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<div class="text-center"> <div class="text-center">
<div class="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-2"> <div class="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-2">
<svg class="w-4 h-4 text-purple-600" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-4 h-4 text-purple-600" fill="currentColor" viewBox="0 0 20 20">