feat: improve eval presentation
This commit is contained in:
69
models.py
69
models.py
@@ -358,6 +358,75 @@ class Assessment(db.Model):
|
||||
total += element.max_points
|
||||
return total
|
||||
|
||||
def get_domains_distribution(self):
|
||||
"""
|
||||
Retourne la distribution des points par domaine pour cette évaluation.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Liste des domaines avec leurs points et couleurs
|
||||
[{'name': str, 'points': float, 'color': str}, ...]
|
||||
"""
|
||||
domains_points = {}
|
||||
|
||||
for exercise in self.exercises:
|
||||
for element in exercise.grading_elements:
|
||||
if element.domain:
|
||||
domain_name = element.domain.name
|
||||
domain_color = element.domain.color
|
||||
if domain_name not in domains_points:
|
||||
domains_points[domain_name] = {'points': 0, 'color': domain_color}
|
||||
domains_points[domain_name]['points'] += element.max_points
|
||||
|
||||
# Convertir en liste triée par nombre de points (descendant)
|
||||
domains_list = []
|
||||
for domain_name, data in domains_points.items():
|
||||
domains_list.append({
|
||||
'name': domain_name,
|
||||
'points': data['points'],
|
||||
'color': data['color']
|
||||
})
|
||||
|
||||
# Trier par nombre de points décroissant
|
||||
domains_list.sort(key=lambda x: x['points'], reverse=True)
|
||||
return domains_list
|
||||
|
||||
def get_skills_distribution(self):
|
||||
"""
|
||||
Retourne la distribution des points par compétence pour cette évaluation.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Liste des compétences avec leurs points et couleurs
|
||||
[{'name': str, 'points': float, 'color': str}, ...]
|
||||
"""
|
||||
skills_points = {}
|
||||
|
||||
for exercise in self.exercises:
|
||||
for element in exercise.grading_elements:
|
||||
if element.skill:
|
||||
skill_name = element.skill
|
||||
if skill_name not in skills_points:
|
||||
skills_points[skill_name] = 0
|
||||
skills_points[skill_name] += element.max_points
|
||||
|
||||
# Récupérer les couleurs des compétences depuis la base de données
|
||||
competences_colors = {}
|
||||
competences = Competence.query.all()
|
||||
for comp in competences:
|
||||
competences_colors[comp.name] = comp.color
|
||||
|
||||
# Convertir en liste triée par nombre de points (descendant)
|
||||
skills_list = []
|
||||
for skill_name, points in skills_points.items():
|
||||
skills_list.append({
|
||||
'name': skill_name,
|
||||
'points': points,
|
||||
'color': competences_colors.get(skill_name, '#6B7280') # Couleur par défaut si non trouvée
|
||||
})
|
||||
|
||||
# Trier par nombre de points décroissant
|
||||
skills_list.sort(key=lambda x: x['points'], reverse=True)
|
||||
return skills_list
|
||||
|
||||
class Exercise(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id'), nullable=False)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy import and_
|
||||
from models import Assessment, ClassGroup, Exercise
|
||||
from models import Assessment, ClassGroup, Exercise, GradingElement
|
||||
from .base_repository import BaseRepository
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class AssessmentRepository(BaseRepository[Assessment]):
|
||||
"""Trouve une évaluation avec tous ses détails."""
|
||||
return Assessment.query.options(
|
||||
joinedload(Assessment.class_group),
|
||||
joinedload(Assessment.exercises).joinedload(Exercise.grading_elements)
|
||||
joinedload(Assessment.exercises).joinedload(Exercise.grading_elements).joinedload(GradingElement.domain)
|
||||
).filter_by(id=id).first()
|
||||
|
||||
def get_or_404(self, id: int) -> Assessment:
|
||||
|
@@ -216,7 +216,46 @@
|
||||
<span class="text-sm font-medium text-gray-500">Exercices</span>
|
||||
<span class="text-sm font-bold text-gray-900">{{ assessment.exercises|length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-500">Total points</span>
|
||||
<span class="text-sm font-bold text-purple-600">{{ "%.1f"|format(assessment.get_total_max_points()) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Domaines -->
|
||||
{% set domains = assessment.get_domains_distribution() %}
|
||||
{% if domains %}
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<span class="text-sm font-medium text-gray-500 mb-2 block">Domaines</span>
|
||||
<div class="space-y-1">
|
||||
{% for domain in domains %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full mr-2" style="background-color: {{ domain.color }};"></div>
|
||||
<span class="text-xs text-gray-700">{{ domain.name }}</span>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-gray-900">{{ "%.1f"|format(domain.points) }} pts</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Compétences -->
|
||||
{% set skills = assessment.get_skills_distribution() %}
|
||||
{% if skills %}
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<span class="text-sm font-medium text-gray-500 mb-2 block">Compétences</span>
|
||||
<div class="space-y-1">
|
||||
{% for skill in skills %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-700">{{ skill.name }}</span>
|
||||
<span class="text-xs font-bold text-gray-900">{{ "%.1f"|format(skill.points) }} pts</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if assessment.description %}
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<span class="text-sm font-medium text-gray-500">Description</span>
|
||||
@@ -334,7 +373,57 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Diagrammes de répartition -->
|
||||
{% set domains = assessment.get_domains_distribution() %}
|
||||
{% set skills = assessment.get_skills_distribution() %}
|
||||
{% if domains or skills %}
|
||||
<div class="bg-white shadow rounded-xl">
|
||||
<div class="px-6 py-5 border-b border-gray-200">
|
||||
<h2 class="text-xl font-bold text-gray-900">Répartition des points</h2>
|
||||
</div>
|
||||
<div class="px-6 py-6">
|
||||
<div class="grid grid-cols-1 {% if domains and skills %}md:grid-cols-2{% endif %} gap-8">
|
||||
<!-- Diagramme des domaines -->
|
||||
{% if domains %}
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Par domaines</h3>
|
||||
<div class="relative">
|
||||
<canvas id="domainsChart" width="300" height="300" class="mx-auto"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 space-y-2">
|
||||
{% for domain in domains %}
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-3 h-3 rounded-full mr-2" style="background-color: {{ domain.color }};"></div>
|
||||
<span class="text-sm text-gray-700">{{ domain.name }} ({{ "%.1f"|format(domain.points) }} pts)</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Diagramme des compétences -->
|
||||
{% if skills %}
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Par compétences</h3>
|
||||
<div class="relative">
|
||||
<canvas id="skillsChart" width="300" height="300" class="mx-auto"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 space-y-2 max-h-48 overflow-y-auto">
|
||||
{% for skill in skills %}
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-3 h-3 rounded-full mr-2" style="background-color: {{ skill.color }};"></div>
|
||||
<span class="text-sm text-gray-700">{{ skill.name }} ({{ "%.1f"|format(skill.points) }} pts)</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Actions secondaires -->
|
||||
<div class="bg-white shadow rounded-xl">
|
||||
<div class="px-6 py-5 border-b border-gray-200">
|
||||
@@ -369,4 +458,84 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js pour les diagrammes -->
|
||||
{% if domains or skills %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Configuration commune pour les graphiques
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false // Légende externe
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
const percentage = ((value / context.dataset.data.reduce((a, b) => a + b, 0)) * 100).toFixed(1);
|
||||
return `${label}: ${value} pts (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Diagramme des domaines
|
||||
{% if domains %}
|
||||
const domainsData = {
|
||||
labels: [{% for domain in domains %}'{{ domain.name }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
datasets: [{
|
||||
data: [{% for domain in domains %}{{ domain.points }}{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
backgroundColor: [{% for domain in domains %}'{{ domain.color }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff'
|
||||
}]
|
||||
};
|
||||
|
||||
const domainsCtx = document.getElementById('domainsChart');
|
||||
if (domainsCtx) {
|
||||
new Chart(domainsCtx, {
|
||||
type: 'doughnut',
|
||||
data: domainsData,
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '60%'
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Diagramme des compétences
|
||||
{% if skills %}
|
||||
const skillsData = {
|
||||
labels: [{% for skill in skills %}'{{ skill.name }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
datasets: [{
|
||||
data: [{% for skill in skills %}{{ skill.points }}{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
backgroundColor: [{% for skill in skills %}'{{ skill.color }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff'
|
||||
}]
|
||||
};
|
||||
|
||||
const skillsCtx = document.getElementById('skillsChart');
|
||||
if (skillsCtx) {
|
||||
new Chart(skillsCtx, {
|
||||
type: 'doughnut',
|
||||
data: skillsData,
|
||||
options: {
|
||||
...chartOptions,
|
||||
cutout: '60%'
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
Reference in New Issue
Block a user