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:
45
README.md
45
README.md
@@ -1,6 +1,19 @@
|
||||
# Gestion Scolaire
|
||||
|
||||
Application web pour gérer les élèves, évaluations et notes.
|
||||
Application web Flask pour gérer les élèves, évaluations et notes dans un contexte scolaire.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### ✅ Gestion complète des évaluations
|
||||
- **Groupes classes** : Organisation des élèves par classe
|
||||
- **Évaluations** : Création et gestion des contrôles/devoirs
|
||||
- **Exercices** : Structure hiérarchique des évaluations
|
||||
- **Éléments de notation** : Configuration fine des critères
|
||||
- **Saisie des notes** : Interface intuitive pour la notation
|
||||
|
||||
### 🎯 Types de notation supportés
|
||||
- **Points** : Notation classique (ex: 2.5/4 points)
|
||||
- **Score** : Évaluation par compétences (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)
|
||||
|
||||
## Installation et lancement
|
||||
|
||||
@@ -20,19 +33,31 @@ uv run flask --app app run --debug
|
||||
uv run flask --app app run
|
||||
```
|
||||
|
||||
## Fonctionnalités
|
||||
## Architecture technique
|
||||
|
||||
- Gestion des groupes classes
|
||||
- Gestion des élèves
|
||||
- Création d'évaluations avec exercices
|
||||
- Éléments de notation configurables (score ou points)
|
||||
- Saisie et suivi des notes
|
||||
|
||||
## Architecture
|
||||
### Structure du code
|
||||
```
|
||||
├── app.py # Application Flask principale
|
||||
├── models.py # Modèles SQLAlchemy
|
||||
├── forms.py # Formulaires WTForms
|
||||
├── commands.py # Commandes CLI Flask
|
||||
├── routes/
|
||||
│ ├── assessments.py # Routes pour les évaluations
|
||||
│ ├── exercises.py # Routes pour les exercices
|
||||
│ └── grading.py # Routes pour la saisie des notes
|
||||
└── templates/ # Templates Jinja2 avec TailwindCSS
|
||||
```
|
||||
|
||||
### Modèles de données
|
||||
- **ClassGroup** : Groupes de classes (ex: 6ème A, 5ème B)
|
||||
- **Student** : Élèves assignés à un groupe classe
|
||||
- **Assessment** : Évaluations liées à un groupe classe
|
||||
- **Exercise** : Exercices composant une évaluation
|
||||
- **GradingElement** : Éléments de notation avec barème
|
||||
- **GradingElement** : Éléments de notation avec barème et type
|
||||
- **Grade** : Notes attribuées aux éléments de notation
|
||||
|
||||
### Technologies utilisées
|
||||
- **Backend** : Flask, SQLAlchemy, WTForms
|
||||
- **Frontend** : TailwindCSS, Jinja2
|
||||
- **Base de données** : SQLite
|
||||
- **Gestionnaire de paquets** : uv
|
||||
512
app.py
512
app.py
@@ -1,466 +1,56 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, TextAreaField, FloatField, SelectField, DateField, IntegerField, SubmitField
|
||||
from wtforms.validators import DataRequired, Email, NumberRange, Optional, Length
|
||||
from datetime import datetime, date
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
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}>'
|
||||
|
||||
# Forms
|
||||
class AssessmentForm(FlaskForm):
|
||||
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
date = DateField('Date', validators=[DataRequired()], default=date.today)
|
||||
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
|
||||
coefficient = FloatField('Coefficient', validators=[DataRequired(), NumberRange(min=0.1, max=10)], default=1.0)
|
||||
submit = SubmitField('Enregistrer')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AssessmentForm, self).__init__(*args, **kwargs)
|
||||
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
|
||||
|
||||
class ClassGroupForm(FlaskForm):
|
||||
name = StringField('Nom de la classe', validators=[DataRequired(), Length(max=100)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
year = StringField('Année scolaire', validators=[DataRequired(), Length(max=20)], default="2024-2025")
|
||||
submit = SubmitField('Enregistrer')
|
||||
|
||||
class StudentForm(FlaskForm):
|
||||
first_name = StringField('Prénom', validators=[DataRequired(), Length(max=100)])
|
||||
last_name = StringField('Nom', validators=[DataRequired(), Length(max=100)])
|
||||
email = StringField('Email', validators=[Optional(), Email(), Length(max=120)])
|
||||
class_group_id = SelectField('Classe', validators=[DataRequired()], coerce=int)
|
||||
submit = SubmitField('Enregistrer')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StudentForm, self).__init__(*args, **kwargs)
|
||||
self.class_group_id.choices = [(cg.id, cg.name) for cg in ClassGroup.query.order_by(ClassGroup.name).all()]
|
||||
|
||||
class ExerciseForm(FlaskForm):
|
||||
title = StringField('Titre', validators=[DataRequired(), Length(max=200)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
order = IntegerField('Ordre', validators=[DataRequired(), NumberRange(min=1)], default=1)
|
||||
submit = SubmitField('Enregistrer')
|
||||
|
||||
class GradingElementForm(FlaskForm):
|
||||
label = StringField('Libellé', validators=[DataRequired(), Length(max=200)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
skill = StringField('Compétence', validators=[Optional(), Length(max=200)])
|
||||
max_points = FloatField('Barème (points max)', validators=[DataRequired(), NumberRange(min=0.1)], default=1.0)
|
||||
grading_type = SelectField('Type de notation', validators=[DataRequired()],
|
||||
choices=[('points', 'Points (ex: 2.5/4)'), ('score', 'Score (0, 1, 2, 3, .)')],
|
||||
default='points')
|
||||
submit = SubmitField('Enregistrer')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
recent_assessments = Assessment.query.order_by(Assessment.date.desc()).limit(5).all()
|
||||
total_students = Student.query.count()
|
||||
total_assessments = Assessment.query.count()
|
||||
total_classes = ClassGroup.query.count()
|
||||
return render_template('index.html',
|
||||
recent_assessments=recent_assessments,
|
||||
total_students=total_students,
|
||||
total_assessments=total_assessments,
|
||||
total_classes=total_classes)
|
||||
|
||||
@app.route('/classes')
|
||||
def classes():
|
||||
classes = ClassGroup.query.order_by(ClassGroup.year, ClassGroup.name).all()
|
||||
return render_template('classes.html', classes=classes)
|
||||
|
||||
@app.route('/students')
|
||||
def students():
|
||||
students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all()
|
||||
return render_template('students.html', students=students)
|
||||
|
||||
@app.route('/assessments')
|
||||
def assessments():
|
||||
assessments = Assessment.query.join(ClassGroup).order_by(Assessment.date.desc()).all()
|
||||
return render_template('assessments.html', assessments=assessments)
|
||||
|
||||
@app.route('/assessments/new', methods=['GET', 'POST'])
|
||||
def new_assessment():
|
||||
form = AssessmentForm()
|
||||
if form.validate_on_submit():
|
||||
assessment = Assessment(
|
||||
title=form.title.data,
|
||||
description=form.description.data,
|
||||
date=form.date.data,
|
||||
class_group_id=form.class_group_id.data,
|
||||
coefficient=form.coefficient.data
|
||||
)
|
||||
db.session.add(assessment)
|
||||
db.session.commit()
|
||||
flash('Évaluation créée avec succès !', 'success')
|
||||
return redirect(url_for('assessment_detail', id=assessment.id))
|
||||
return render_template('assessment_form.html', form=form, title='Nouvelle évaluation')
|
||||
|
||||
@app.route('/assessments/<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!")
|
||||
from flask import Flask, render_template
|
||||
from models import db, Assessment, Student, ClassGroup
|
||||
from commands import init_db
|
||||
|
||||
# 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.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(assessments_bp)
|
||||
app.register_blueprint(exercises_bp)
|
||||
app.register_blueprint(grading_bp)
|
||||
|
||||
# Register CLI commands
|
||||
app.cli.add_command(init_db)
|
||||
|
||||
# Main routes
|
||||
@app.route('/')
|
||||
def index():
|
||||
recent_assessments = Assessment.query.order_by(Assessment.date.desc()).limit(5).all()
|
||||
total_students = Student.query.count()
|
||||
total_assessments = Assessment.query.count()
|
||||
total_classes = ClassGroup.query.count()
|
||||
return render_template('index.html',
|
||||
recent_assessments=recent_assessments,
|
||||
total_students=total_students,
|
||||
total_assessments=total_assessments,
|
||||
total_classes=total_classes)
|
||||
|
||||
@app.route('/classes')
|
||||
def classes():
|
||||
classes = ClassGroup.query.order_by(ClassGroup.year, ClassGroup.name).all()
|
||||
return render_template('classes.html', classes=classes)
|
||||
|
||||
@app.route('/students')
|
||||
def students():
|
||||
students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all()
|
||||
return render_template('students.html', students=students)
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
app.run(debug=True)
|
||||
83
commands.py
Normal file
83
commands.py
Normal 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
50
forms.py
Normal 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
72
models.py
Normal 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
1
routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routes package
|
||||
55
routes/assessments.py
Normal file
55
routes/assessments.py
Normal 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
113
routes/exercises.py
Normal 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
78
routes/grading.py
Normal 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))
|
||||
@@ -6,14 +6,14 @@
|
||||
<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">
|
||||
<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
|
||||
</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">
|
||||
<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
|
||||
</a>
|
||||
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cette évaluation ?')) { document.getElementById('delete-form').submit(); }"
|
||||
@@ -23,7 +23,7 @@
|
||||
</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 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
@@ -62,7 +62,7 @@
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -82,10 +82,10 @@
|
||||
</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">
|
||||
<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
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@
|
||||
</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">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% 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">
|
||||
<a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||
← Retour aux évaluations
|
||||
</a>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
</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">
|
||||
<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
|
||||
</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") }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<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>
|
||||
</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">
|
||||
<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 →
|
||||
</a>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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 -->
|
||||
<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>
|
||||
@@ -117,7 +117,7 @@
|
||||
</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">
|
||||
<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
|
||||
</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">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gestion des évaluations</h1>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -42,13 +42,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucune évaluation</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Commencez par créer votre première évaluation.</p>
|
||||
<div class="mt-6">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<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('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>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<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">
|
||||
<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 }}"
|
||||
</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">
|
||||
<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
|
||||
</a>
|
||||
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet exercice ?')) { document.getElementById('delete-form').submit(); }"
|
||||
@@ -23,7 +23,7 @@
|
||||
</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 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
@@ -54,7 +54,7 @@
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -83,13 +83,13 @@
|
||||
</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">
|
||||
<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
|
||||
</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>
|
||||
<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>
|
||||
@@ -119,7 +119,7 @@
|
||||
</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">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% 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">
|
||||
<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 }}"
|
||||
</a>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
</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">
|
||||
<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
|
||||
</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") }}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% 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">
|
||||
<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 }}"
|
||||
</a>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
</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">
|
||||
<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
|
||||
</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") }}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="text-sm font-medium text-gray-900">Gérer les élèves</div>
|
||||
</div>
|
||||
</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="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">
|
||||
|
||||
Reference in New Issue
Block a user