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
|
# 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
|
||||||
512
app.py
512
app.py
@@ -1,466 +1,56 @@
|
|||||||
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
|
# Import blueprints
|
||||||
from datetime import datetime, date
|
from routes.assessments import bp as assessments_bp
|
||||||
|
from routes.exercises import bp as exercises_bp
|
||||||
app = Flask(__name__)
|
from routes.grading import bp as grading_bp
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
|
def create_app():
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||||
db = SQLAlchemy(app)
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///school_management.db'
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
class ClassGroup(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
# Initialize extensions
|
||||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
db.init_app(app)
|
||||||
description = db.Column(db.Text)
|
|
||||||
year = db.Column(db.String(20), nullable=False)
|
# Register blueprints
|
||||||
students = db.relationship('Student', backref='class_group', lazy=True)
|
app.register_blueprint(assessments_bp)
|
||||||
assessments = db.relationship('Assessment', backref='class_group', lazy=True)
|
app.register_blueprint(exercises_bp)
|
||||||
|
app.register_blueprint(grading_bp)
|
||||||
def __repr__(self):
|
|
||||||
return f'<ClassGroup {self.name}>'
|
# Register CLI commands
|
||||||
|
app.cli.add_command(init_db)
|
||||||
class Student(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
# Main routes
|
||||||
last_name = db.Column(db.String(100), nullable=False)
|
@app.route('/')
|
||||||
first_name = db.Column(db.String(100), nullable=False)
|
def index():
|
||||||
email = db.Column(db.String(120), unique=True)
|
recent_assessments = Assessment.query.order_by(Assessment.date.desc()).limit(5).all()
|
||||||
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
total_students = Student.query.count()
|
||||||
grades = db.relationship('Grade', backref='student', lazy=True)
|
total_assessments = Assessment.query.count()
|
||||||
|
total_classes = ClassGroup.query.count()
|
||||||
def __repr__(self):
|
return render_template('index.html',
|
||||||
return f'<Student {self.first_name} {self.last_name}>'
|
recent_assessments=recent_assessments,
|
||||||
|
total_students=total_students,
|
||||||
class Assessment(db.Model):
|
total_assessments=total_assessments,
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
total_classes=total_classes)
|
||||||
title = db.Column(db.String(200), nullable=False)
|
|
||||||
description = db.Column(db.Text)
|
@app.route('/classes')
|
||||||
date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
def classes():
|
||||||
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
classes = ClassGroup.query.order_by(ClassGroup.year, ClassGroup.name).all()
|
||||||
coefficient = db.Column(db.Float, default=1.0)
|
return render_template('classes.html', classes=classes)
|
||||||
exercises = db.relationship('Exercise', backref='assessment', lazy=True, cascade='all, delete-orphan')
|
|
||||||
|
@app.route('/students')
|
||||||
def __repr__(self):
|
def students():
|
||||||
return f'<Assessment {self.title}>'
|
students = Student.query.join(ClassGroup).order_by(ClassGroup.name, Student.last_name, Student.first_name).all()
|
||||||
|
return render_template('students.html', students=students)
|
||||||
class Exercise(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
return app
|
||||||
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!")
|
|
||||||
|
|
||||||
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
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="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>
|
||||||
|
|||||||
@@ -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") }}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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") }}
|
||||||
|
|||||||
@@ -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") }}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user