feat: add mailing and bilan to send
This commit is contained in:
@@ -52,6 +52,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton d'envoi des bilans -->
|
||||
<div class="flex justify-end mb-6">
|
||||
<button id="sendReportsBtn" onclick="openSendReportsModal()"
|
||||
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
📧 Envoyer les bilans par email
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Histogramme des notes -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
@@ -764,5 +775,284 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
createGradingElementsHeatmap('gradingElementsHeatmap', gradingElementsData);
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
// === FONCTIONNALITÉ D'ENVOI DE BILANS ===
|
||||
|
||||
let eligibleStudents = [];
|
||||
let selectedStudents = [];
|
||||
|
||||
// Charger les élèves éligibles au chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadEligibleStudents();
|
||||
});
|
||||
|
||||
function loadEligibleStudents() {
|
||||
fetch(`/assessments/{{ assessment.id }}/eligible-students`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
eligibleStudents = data.students;
|
||||
updateSendReportsButton(data.with_email_count, data.total_count);
|
||||
} else {
|
||||
console.error('Erreur lors du chargement des élèves:', data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur réseau:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSendReportsButton(withEmailCount, totalCount) {
|
||||
const btn = document.getElementById('sendReportsBtn');
|
||||
if (withEmailCount === 0) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
btn.classList.remove('hover:bg-blue-700');
|
||||
btn.innerHTML = `
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Aucune adresse email configurée
|
||||
`;
|
||||
} else {
|
||||
btn.innerHTML = `
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
📧 Envoyer les bilans (${withEmailCount}/${totalCount} avec email)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function openSendReportsModal() {
|
||||
if (eligibleStudents.length === 0) {
|
||||
alert('Aucun élève éligible trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('sendReportsModal').classList.remove('hidden');
|
||||
populateStudentsList();
|
||||
}
|
||||
|
||||
function closeSendReportsModal() {
|
||||
document.getElementById('sendReportsModal').classList.add('hidden');
|
||||
selectedStudents = [];
|
||||
}
|
||||
|
||||
function populateStudentsList() {
|
||||
const container = document.getElementById('studentsList');
|
||||
const studentsWithEmail = eligibleStudents.filter(s => s.has_email);
|
||||
|
||||
if (studentsWithEmail.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-4">Aucun élève avec adresse email configurée</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = studentsWithEmail.map(student => `
|
||||
<label class="flex items-center p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
|
||||
<input type="checkbox"
|
||||
value="${student.id}"
|
||||
onchange="updateSelectedStudents()"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-900">${student.full_name}</div>
|
||||
<div class="text-sm text-gray-500">${student.email}</div>
|
||||
</div>
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
// Sélectionner tous par défaut
|
||||
selectAllStudents();
|
||||
}
|
||||
|
||||
function selectAllStudents() {
|
||||
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => cb.checked = true);
|
||||
updateSelectedStudents();
|
||||
}
|
||||
|
||||
function unselectAllStudents() {
|
||||
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
updateSelectedStudents();
|
||||
}
|
||||
|
||||
function updateSelectedStudents() {
|
||||
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]:checked');
|
||||
selectedStudents = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
|
||||
const count = selectedStudents.length;
|
||||
document.getElementById('selectedCount').textContent = count;
|
||||
document.getElementById('sendReportsSubmitBtn').disabled = count === 0;
|
||||
}
|
||||
|
||||
function sendReports() {
|
||||
if (selectedStudents.length === 0) {
|
||||
alert('Veuillez sélectionner au moins un élève');
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = document.getElementById('customMessage').value.trim();
|
||||
const submitBtn = document.getElementById('sendReportsSubmitBtn');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
|
||||
// Désactiver le bouton et afficher le loading
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Envoi en cours...
|
||||
`;
|
||||
|
||||
// Envoyer la requête
|
||||
fetch(`/assessments/{{ assessment.id }}/send-reports`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student_ids: selectedStudents,
|
||||
custom_message: customMessage
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Restaurer le bouton
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
|
||||
if (data.success) {
|
||||
// Afficher le résultat
|
||||
showSendResult(data);
|
||||
// Fermer la modal après un délai
|
||||
setTimeout(() => {
|
||||
closeSendReportsModal();
|
||||
}, 3000);
|
||||
} else {
|
||||
alert(`Erreur lors de l'envoi: ${data.error}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Restaurer le bouton en cas d'erreur
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
console.error('Erreur:', error);
|
||||
alert('Erreur réseau lors de l\'envoi des bilans');
|
||||
});
|
||||
}
|
||||
|
||||
function showSendResult(result) {
|
||||
const resultDiv = document.getElementById('sendResult');
|
||||
|
||||
let content = `<div class="mb-4">
|
||||
<h3 class="text-lg font-medium ${result.success ? 'text-green-600' : 'text-red-600'} mb-2">
|
||||
${result.message}
|
||||
</h3>
|
||||
</div>`;
|
||||
|
||||
if (result.sent_count > 0) {
|
||||
content += `<div class="mb-3">
|
||||
<p class="text-green-600">✅ ${result.sent_count} bilan(s) envoyé(s) avec succès</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (result.error_count > 0) {
|
||||
content += `<div class="mb-3">
|
||||
<p class="text-red-600 font-medium">❌ ${result.error_count} erreur(s):</p>
|
||||
<ul class="text-sm text-red-600 mt-2 max-h-32 overflow-y-auto">`;
|
||||
|
||||
result.errors.forEach(error => {
|
||||
content += `<li class="py-1">• ${error}</li>`;
|
||||
});
|
||||
|
||||
content += `</ul></div>`;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = content;
|
||||
resultDiv.classList.remove('hidden');
|
||||
|
||||
// Cacher les autres sections
|
||||
document.getElementById('studentsSelection').classList.add('hidden');
|
||||
document.getElementById('modalActions').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Fermer la modal en cliquant à côté
|
||||
document.addEventListener('click', function(e) {
|
||||
const modal = document.getElementById('sendReportsModal');
|
||||
if (e.target === modal) {
|
||||
closeSendReportsModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Modal d'envoi des bilans -->
|
||||
<div id="sendReportsModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-2xl shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<!-- En-tête -->
|
||||
<div class="flex justify-between items-center pb-4 border-b">
|
||||
<h3 class="text-lg font-medium text-gray-900">📧 Envoyer les bilans par email</h3>
|
||||
<button onclick="closeSendReportsModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sélection des élèves -->
|
||||
<div id="studentsSelection" class="py-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="font-medium text-gray-900">Sélectionner les élèves</h4>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="selectAllStudents()"
|
||||
class="text-sm text-blue-600 hover:text-blue-800">Tout sélectionner</button>
|
||||
<span class="text-gray-300">|</span>
|
||||
<button onclick="unselectAllStudents()"
|
||||
class="text-sm text-blue-600 hover:text-blue-800">Tout désélectionner</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="studentsList" class="max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-2 space-y-1">
|
||||
<!-- Les élèves seront ajoutés ici par JavaScript -->
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mt-2">
|
||||
<span id="selectedCount">0</span> élève(s) sélectionné(s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Message personnalisé -->
|
||||
<div class="py-4 border-t">
|
||||
<label for="customMessage" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Message personnalisé (optionnel)
|
||||
</label>
|
||||
<textarea id="customMessage"
|
||||
rows="3"
|
||||
placeholder="Message du professeur à ajouter dans les bilans..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Résultat de l'envoi -->
|
||||
<div id="sendResult" class="hidden py-4 border-t">
|
||||
<!-- Le résultat sera affiché ici -->
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div id="modalActions" class="flex justify-end space-x-3 pt-4 border-t">
|
||||
<button onclick="closeSendReportsModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Annuler
|
||||
</button>
|
||||
<button id="sendReportsSubmitBtn" onclick="sendReports()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
disabled>
|
||||
📧 Envoyer les bilans
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
258
templates/config/email.html
Normal file
258
templates/config/email.html
Normal file
@@ -0,0 +1,258 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuration Email - Gestion Scolaire{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<a href="{{ url_for('config.index') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
|
||||
← Retour à la configuration
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Configuration Email</h1>
|
||||
<p class="text-gray-600">Configurez les paramètres SMTP pour l'envoi des bilans d'évaluation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de configuration email -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">Paramètres SMTP</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
Configurez votre serveur SMTP pour envoyer les bilans d'évaluation par email
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('config.update_email') }}" class="px-6 py-4 space-y-4">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Serveur SMTP -->
|
||||
<div>
|
||||
<label for="smtp_host" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Serveur SMTP
|
||||
</label>
|
||||
<input type="text"
|
||||
id="smtp_host"
|
||||
name="smtp_host"
|
||||
value="{{ email_config.smtp_host }}"
|
||||
placeholder="smtp.gmail.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Ex: smtp.gmail.com, smtp.outlook.com</p>
|
||||
</div>
|
||||
|
||||
<!-- Port SMTP -->
|
||||
<div>
|
||||
<label for="smtp_port" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Port SMTP
|
||||
</label>
|
||||
<input type="text"
|
||||
id="smtp_port"
|
||||
name="smtp_port"
|
||||
value="{{ email_config.smtp_port }}"
|
||||
placeholder="587"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">587 (TLS) ou 465 (SSL) généralement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Nom d'utilisateur -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value="{{ email_config.username }}"
|
||||
placeholder="votre.email@domain.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Votre adresse email de connexion SMTP</p>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value="{{ email_config.password }}"
|
||||
placeholder="••••••••"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Mot de passe ou mot de passe d'application</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options de sécurité -->
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
id="use_tls"
|
||||
name="use_tls"
|
||||
{% if email_config.use_tls %}checked{% endif %}
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="use_tls" class="ml-2 text-sm text-gray-700">
|
||||
Utiliser TLS (recommandé)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Nom d'expéditeur -->
|
||||
<div>
|
||||
<label for="from_name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nom d'expéditeur
|
||||
</label>
|
||||
<input type="text"
|
||||
id="from_name"
|
||||
name="from_name"
|
||||
value="{{ email_config.from_name }}"
|
||||
placeholder="Notytex"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Nom qui apparaîtra comme expéditeur</p>
|
||||
</div>
|
||||
|
||||
<!-- Adresse d'expéditeur -->
|
||||
<div>
|
||||
<label for="from_address" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Adresse d'expéditeur
|
||||
</label>
|
||||
<input type="email"
|
||||
id="from_address"
|
||||
name="from_address"
|
||||
value="{{ email_config.from_address }}"
|
||||
placeholder="professeur@etablissement.fr"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Adresse qui apparaîtra comme expéditeur</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<a href="{{ url_for('config.index') }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Annuler
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Test de configuration -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">Test de configuration</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
Testez votre configuration en envoyant un email de test
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('config.test_email') }}" class="px-6 py-4">
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="test_email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Adresse email de test
|
||||
</label>
|
||||
<input type="email"
|
||||
id="test_email"
|
||||
name="test_email"
|
||||
placeholder="test@example.com"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Un email de test sera envoyé à cette adresse</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
📧 Envoyer un test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Aide configuration -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-blue-900 mb-3">💡 Aide à la configuration</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 class="font-medium text-blue-800 mb-2">Gmail</h4>
|
||||
<ul class="text-sm text-blue-700 space-y-1">
|
||||
<li><strong>Serveur:</strong> smtp.gmail.com</li>
|
||||
<li><strong>Port:</strong> 587 (TLS)</li>
|
||||
<li><strong>Sécurité:</strong> Utiliser un mot de passe d'application</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-blue-800 mb-2">Outlook/Hotmail</h4>
|
||||
<ul class="text-sm text-blue-700 space-y-1">
|
||||
<li><strong>Serveur:</strong> smtp-mail.outlook.com</li>
|
||||
<li><strong>Port:</strong> 587 (TLS)</li>
|
||||
<li><strong>Sécurité:</strong> Authentification moderne requise</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<strong>⚠️ Sécurité:</strong> Pour Gmail, créez un mot de passe d'application dans votre compte Google.
|
||||
N'utilisez pas votre mot de passe principal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Serveur SMTP de test local -->
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-green-900 mb-3">🧪 Tests en local - Serveur SMTP factice</h3>
|
||||
|
||||
<p class="text-sm text-green-700 mb-3">
|
||||
Pour tester l'envoi d'emails sans configuration réelle, utilisez le serveur SMTP de débogage Python :
|
||||
</p>
|
||||
|
||||
<div class="bg-green-100 p-4 rounded-md mb-4">
|
||||
<h4 class="font-medium text-green-800 mb-2">1. Lancer le serveur de débogage</h4>
|
||||
<p class="text-sm text-green-700 mb-2"><strong>Option A:</strong> Script inclus dans Notytex (recommandé)</p>
|
||||
<code class="block text-sm bg-gray-900 text-green-400 p-2 rounded mb-3">
|
||||
python debug_smtp_server.py
|
||||
</code>
|
||||
|
||||
<p class="text-sm text-green-700 mb-2"><strong>Option B:</strong> Avec aiosmtpd</p>
|
||||
<code class="block text-sm bg-gray-900 text-blue-400 p-2 rounded mb-1">
|
||||
pip install aiosmtpd
|
||||
</code>
|
||||
<code class="block text-sm bg-gray-900 text-blue-400 p-2 rounded mb-3">
|
||||
python -m aiosmtpd -n -l localhost:1025
|
||||
</code>
|
||||
|
||||
<p class="text-xs text-green-600">💡 Le script inclus affiche les emails avec un formatage amélioré</p>
|
||||
<p class="text-xs text-green-600">⚠️ Laissez ce terminal ouvert pendant les tests</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-100 p-4 rounded-md mb-4">
|
||||
<h4 class="font-medium text-green-800 mb-2">2. Configuration pour les tests</h4>
|
||||
<ul class="text-sm text-green-700 space-y-1">
|
||||
<li><strong>Serveur SMTP:</strong> localhost</li>
|
||||
<li><strong>Port:</strong> 1025</li>
|
||||
<li><strong>Utilisateur/Mot de passe:</strong> Laisser vides</li>
|
||||
<li><strong>TLS:</strong> ❌ Décocher</li>
|
||||
<li><strong>Adresse expéditeur:</strong> test@notytex.local</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-100 p-4 rounded-md">
|
||||
<h4 class="font-medium text-green-800 mb-2">3. Résultat</h4>
|
||||
<p class="text-sm text-green-700">
|
||||
✅ Tous les emails s'afficheront dans le terminal (contenu HTML complet)<br>
|
||||
✅ Aucun email réellement envoyé<br>
|
||||
✅ Parfait pour tester les bilans d'évaluation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -103,6 +103,43 @@
|
||||
Configurer l'échelle
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Configuration email -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Configuration Email</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">Paramètres SMTP pour l'envoi des bilans d'évaluation</p>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Serveur SMTP :</span>
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
{% set smtp_host = app_config.get('email.smtp_host', '') %}
|
||||
{{ smtp_host if smtp_host else 'Non configuré' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Statut :</span>
|
||||
<span class="text-sm font-medium">
|
||||
{% set smtp_host = app_config.get('email.smtp_host', '') %}
|
||||
{% set from_address = app_config.get('email.from_address', '') %}
|
||||
{% if smtp_host and from_address %}
|
||||
<span class="text-green-600">✅ Configuré</span>
|
||||
{% else %}
|
||||
<span class="text-red-600">⚠️ À configurer</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('config.email') }}" class="mt-4 block w-full bg-orange-600 text-white py-2 px-4 rounded-md hover:bg-orange-700 transition-colors text-center">
|
||||
Configurer Email
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions globales -->
|
||||
|
||||
269
templates/email/base_email.html
Normal file
269
templates/email/base_email.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Notytex - Bilan d'évaluation{% endblock %}</title>
|
||||
<style>
|
||||
/* Reset styles pour compatibilité email */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
background-color: #f9fafb;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #1f2937;
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
color: #374151;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f3f4f6;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.competence-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
margin: 8px 0;
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.competence-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.competence-score {
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f3f4f6;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #fef3c7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Responsive pour mobile */
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<h1>{% block header_title %}🎓 Notytex{% endblock %}</h1>
|
||||
<p>{% block header_subtitle %}Système de gestion scolaire{% endblock %}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{% block content %}
|
||||
<!-- Contenu principal de l'email -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>{% block school_name %}Établissement scolaire{% endblock %}</strong></p>
|
||||
<p>Email généré automatiquement par Notytex</p>
|
||||
<p>Pour toute question, contactez votre professeur.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
239
templates/email/student_report.html
Normal file
239
templates/email/student_report.html
Normal file
@@ -0,0 +1,239 @@
|
||||
{% extends "email/base_email.html" %}
|
||||
|
||||
{% block title %}{{ report.assessment.title }} - {{ report.student.full_name }}{% endblock %}
|
||||
|
||||
{% block header_title %}📊 Bilan d'Évaluation{% endblock %}
|
||||
{% block header_subtitle %}{{ report.assessment.title }} - {{ report.student.full_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Informations sur l'évaluation -->
|
||||
<div class="section">
|
||||
<h2>📋 Informations sur l'évaluation</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ report.assessment.class_name }}</div>
|
||||
<div class="stat-label">Classe</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ report.assessment.date.strftime('%d/%m/%Y') }}</div>
|
||||
<div class="stat-label">Date</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">T{{ report.assessment.trimester }}</div>
|
||||
<div class="stat-label">Trimestre</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">×{{ report.assessment.coefficient }}</div>
|
||||
<div class="stat-label">Coefficient</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if report.assessment.description %}
|
||||
<p style="margin-top: 15px; padding: 15px; background-color: #e0f2fe; border-radius: 6px; border-left: 3px solid #0288d1;">
|
||||
<strong>Description :</strong> {{ report.assessment.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Note globale -->
|
||||
<div class="section" style="border-left-color: #10b981;">
|
||||
<h2>🎯 Note globale</h2>
|
||||
<div style="text-align: center; padding: 30px;">
|
||||
<div style="font-size: 48px; font-weight: bold; color: #10b981; margin-bottom: 10px;">
|
||||
{{ "%.1f"|format(report.results.total_score) }}/{{ "%.1f"|format(report.results.total_max_points) }}
|
||||
</div>
|
||||
<div style="font-size: 16px; color: #6b7280; margin-bottom: 20px;">
|
||||
Note obtenue sur cette évaluation
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {{ report.results.percentage }}%;
|
||||
background: linear-gradient(90deg,
|
||||
{% if report.results.percentage < 50 %}#ef4444{% elif report.results.percentage < 75 %}#f59e0b{% else %}#10b981{% endif %},
|
||||
{% if report.results.percentage < 50 %}#dc2626{% elif report.results.percentage < 75 %}#d97706{% else %}#059669{% endif %});"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Résultats par exercice -->
|
||||
{% if report.exercises %}
|
||||
<div class="section" style="border-left-color: #8b5cf6;">
|
||||
<h2>📝 Résultats par exercice</h2>
|
||||
|
||||
{% for exercise in report.exercises %}
|
||||
<div style="margin: 20px 0; padding: 15px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb;">
|
||||
<h3 style="color: #8b5cf6;">{{ exercise.title }}</h3>
|
||||
{% if exercise.description %}
|
||||
<p style="font-size: 14px; color: #6b7280; margin-bottom: 10px;">{{ exercise.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin: 10px 0;">
|
||||
<span style="font-weight: 600;">Score :</span>
|
||||
<span style="font-size: 18px; font-weight: 700; color: #8b5cf6;">
|
||||
{{ "%.1f"|format(exercise.score) }}/{{ "%.1f"|format(exercise.max_points) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {{ exercise.percentage }}%; background-color: #8b5cf6;"></div>
|
||||
</div>
|
||||
|
||||
{% if exercise.elements %}
|
||||
<div style="margin-top: 15px;">
|
||||
<h4 style="font-size: 14px; color: #374151; margin-bottom: 8px;">Détail des questions :</h4>
|
||||
<table class="table" style="font-size: 14px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Question</th>
|
||||
<th>Compétence</th>
|
||||
<th>Domaine</th>
|
||||
<th>Résultat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for element in exercise.elements %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ element.label }}</strong>
|
||||
{% if element.description %}
|
||||
<br><small style="color: #6b7280;">{{ element.description }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if element.skill %}
|
||||
<span class="badge badge-info">{{ element.skill }}</span>
|
||||
{% else %}
|
||||
<span style="color: #9ca3af;">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if element.domain %}
|
||||
<small style="color: #6b7280;">{{ element.domain }}</small>
|
||||
{% else %}
|
||||
<span style="color: #9ca3af;">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{% if element.raw_value == '.' %}
|
||||
<span style="color: #9ca3af; font-size: 20px;">❓</span>
|
||||
<br><small style="color: #9ca3af;">Pas de réponse</small>
|
||||
{% elif element.grading_type == 'score' and element.raw_value %}
|
||||
{% set score_value = element.raw_value|int %}
|
||||
<div style="font-size: 18px;">
|
||||
{% for i in range(3) %}
|
||||
{% if i < score_value %}⭐{% else %}☆{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small style="color: #6b7280;">
|
||||
{{ element.score_label if element.score_label else 'Score ' + score_value|string }}
|
||||
</small>
|
||||
{% elif element.raw_value %}
|
||||
<strong style="font-size: 16px;">{{ element.raw_value }}/{{ element.max_points }}</strong>
|
||||
{% else %}
|
||||
<span style="color: #9ca3af; font-size: 20px;">❓</span>
|
||||
<br><small style="color: #9ca3af;">Non noté</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Performances par compétence -->
|
||||
{% if report.competences %}
|
||||
<div class="section" style="border-left-color: #f59e0b;">
|
||||
<h2>⭐ Performances par compétence</h2>
|
||||
<p style="font-size: 14px; color: #6b7280; margin-bottom: 15px;">
|
||||
Analyse de vos performances sur chaque compétence évaluée
|
||||
</p>
|
||||
|
||||
{% for competence in report.competences %}
|
||||
<div class="competence-item">
|
||||
<div>
|
||||
<div class="competence-name">{{ competence.name }}</div>
|
||||
<div style="font-size: 12px; color: #6b7280;">
|
||||
{{ competence.elements_count }} élément{{ 's' if competence.elements_count > 1 else '' }} évalué{{ 's' if competence.elements_count > 1 else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
{% set competence_percentage = (competence.score / competence.max_points * 100) if competence.max_points > 0 else 0 %}
|
||||
{% if competence_percentage < 20 %}
|
||||
{% set star_count = 0 %}
|
||||
{% elif competence_percentage < 50 %}
|
||||
{% set star_count = 1 %}
|
||||
{% elif competence_percentage < 80 %}
|
||||
{% set star_count = 2 %}
|
||||
{% else %}
|
||||
{% set star_count = 3 %}
|
||||
{% endif %}
|
||||
<div style="font-size: 18px; margin-bottom: 4px;">
|
||||
{% for i in range(3) %}
|
||||
{% if i < star_count %}⭐{% else %}☆{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #6b7280;">
|
||||
{{ "%.1f"|format(competence.score) }}/{{ "%.1f"|format(competence.max_points) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Performances par domaine -->
|
||||
{% if report.domains %}
|
||||
<div class="section" style="border-left-color: #06b6d4;">
|
||||
<h2>🏷️ Performances par domaine</h2>
|
||||
<p style="font-size: 14px; color: #6b7280; margin-bottom: 15px;">
|
||||
Analyse de vos performances par thème/domaine
|
||||
</p>
|
||||
|
||||
{% for domain in report.domains %}
|
||||
<div class="competence-item">
|
||||
<div>
|
||||
<div class="competence-name">{{ domain.name }}</div>
|
||||
<div style="font-size: 12px; color: #6b7280;">
|
||||
{{ domain.elements_count }} élément{{ 's' if domain.elements_count > 1 else '' }} évalué{{ 's' if domain.elements_count > 1 else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
{% set domain_percentage = (domain.score / domain.max_points * 100) if domain.max_points > 0 else 0 %}
|
||||
{% if domain_percentage < 20 %}
|
||||
{% set star_count = 0 %}
|
||||
{% elif domain_percentage < 50 %}
|
||||
{% set star_count = 1 %}
|
||||
{% elif domain_percentage < 80 %}
|
||||
{% set star_count = 2 %}
|
||||
{% else %}
|
||||
{% set star_count = 3 %}
|
||||
{% endif %}
|
||||
<div style="font-size: 18px; margin-bottom: 4px;">
|
||||
{% for i in range(3) %}
|
||||
{% if i < star_count %}⭐{% else %}☆{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #6b7280;">
|
||||
{{ "%.1f"|format(domain.score) }}/{{ "%.1f"|format(domain.max_points) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Message personnalisé si fourni -->
|
||||
{% if custom_message %}
|
||||
<div class="section" style="border-left-color: #10b981; background-color: #f0fdf4;">
|
||||
<h2>💬 Message du professeur</h2>
|
||||
<p style="font-style: italic; color: #166534;">{{ custom_message }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user