feat: implement control management views, offline support, and associated unit tests
This commit is contained in:
parent
6b8978254b
commit
4a2aaf9fd0
6 changed files with 305 additions and 64 deletions
|
|
@ -391,9 +391,12 @@ document.querySelector("#remark-form").addEventListener("submit", async (e) => {
|
||||||
// Vérifier si on est en mode édition d'une remarque offline
|
// Vérifier si on est en mode édition d'une remarque offline
|
||||||
const editingRemarkId = form.dataset.editingRemarkId;
|
const editingRemarkId = form.dataset.editingRemarkId;
|
||||||
|
|
||||||
// Récupérer l'ID du contrôle depuis l'URL
|
// Récupérer l'ID du contrôle depuis le champ caché ou l'URL
|
||||||
|
const hiddenControlId = document.getElementById('control_id')?.value;
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
const controlId = pathParts[pathParts.indexOf('controls') + 1];
|
const controlsIndex = pathParts.indexOf('controls');
|
||||||
|
const urlControlId = controlsIndex !== -1 ? pathParts[controlsIndex + 1] : null;
|
||||||
|
const controlId = hiddenControlId || urlControlId;
|
||||||
|
|
||||||
// Collecter les données du formulaire
|
// Collecter les données du formulaire
|
||||||
const formData = {
|
const formData = {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ class OfflineManager {
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
|
|
||||||
// Seuils de qualité réseau en-dessous desquels le mode hors ligne est activé.
|
// Seuils de qualité réseau en-dessous desquels le mode hors ligne est activé.
|
||||||
// Équivalent 4G faible : effectiveType != '4g', downlink < 0.5 Mbps ou rtt > 500 ms.
|
// Seuls 'slow-2g' déclenchent le mode dégradé, laissant 2G, 3G et 4G fonctionner.
|
||||||
this.connectionThreshold = {
|
this.connectionThreshold = {
|
||||||
minEffectiveType: '4g', // '3g' et inférieurs déclenchent le mode offline
|
minEffectiveType: '2g', // 'slow-2g' déclenche le mode offline
|
||||||
minDownlink: 0.5, // Mbps
|
minDownlink: 0.1, // Mbps (100 kbps)
|
||||||
maxRtt: 500 // ms
|
maxRtt: 2000 // ms
|
||||||
};
|
};
|
||||||
|
this.isSyncing = false;
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this._evaluateConnectionQuality();
|
this._evaluateConnectionQuality();
|
||||||
|
|
@ -107,6 +108,20 @@ class OfflineManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retry périodique toutes les 30s si des remarques sont en attente et que navigator.onLine est vrai
|
||||||
|
setInterval(async () => {
|
||||||
|
if (navigator.onLine && this.db && this.isReady && !this.isSyncing) {
|
||||||
|
try {
|
||||||
|
const count = await this.getPendingCount();
|
||||||
|
if (count > 0) {
|
||||||
|
this.syncPendingData();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorer les erreurs silencieuses du timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -117,7 +132,7 @@ class OfflineManager {
|
||||||
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||||
if (!conn) return false; // API non disponible : conserver le comportement par défaut
|
if (!conn) return false; // API non disponible : conserver le comportement par défaut
|
||||||
|
|
||||||
const poorTypes = ['slow-2g', '2g', '3g'];
|
const poorTypes = ['slow-2g'];
|
||||||
if (poorTypes.includes(conn.effectiveType)) return true;
|
if (poorTypes.includes(conn.effectiveType)) return true;
|
||||||
if (conn.rtt > 0 && conn.rtt > this.connectionThreshold.maxRtt) return true;
|
if (conn.rtt > 0 && conn.rtt > this.connectionThreshold.maxRtt) return true;
|
||||||
if (conn.downlink > 0 && conn.downlink < this.connectionThreshold.minDownlink) return true;
|
if (conn.downlink > 0 && conn.downlink < this.connectionThreshold.minDownlink) return true;
|
||||||
|
|
@ -246,55 +261,81 @@ class OfflineManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchroniser les données en attente
|
// Synchroniser les données en attente
|
||||||
async syncPendingData() {
|
async syncPendingData(force = false) {
|
||||||
if (!this.isOnline) {
|
if (!force && !this.isOnline) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly');
|
if (this.isSyncing) {
|
||||||
const remarksStore = transaction.objectStore('pendingRemarks');
|
|
||||||
|
|
||||||
const remarks = await this.getAllRecords(remarksStore);
|
|
||||||
|
|
||||||
if (remarks.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.isSyncing = true;
|
||||||
|
|
||||||
this.showNotification(
|
try {
|
||||||
`Synchronisation de ${remarks.length} remarque(s) en cours...`,
|
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly');
|
||||||
'info'
|
const remarksStore = transaction.objectStore('pendingRemarks');
|
||||||
);
|
|
||||||
|
|
||||||
let syncedCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
for (const remark of remarks) {
|
|
||||||
try {
|
|
||||||
await this.uploadRemark(remark);
|
|
||||||
await this.deleteRemark(remark.id);
|
|
||||||
syncedCount++;
|
|
||||||
} catch (error) {
|
|
||||||
failedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncedCount > 0) {
|
|
||||||
this.showNotification(
|
|
||||||
`${syncedCount} remarque(s) synchronisée(s) avec succès`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Recharger la page pour afficher les nouvelles remarques
|
const remarks = await this.getAllRecords(remarksStore);
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
if (remarks.length === 0) {
|
||||||
}, 1500);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedCount > 0) {
|
|
||||||
this.showNotification(
|
this.showNotification(
|
||||||
`${failedCount} remarque(s) n'ont pas pu être synchronisées`,
|
`Synchronisation de ${remarks.length} remarque(s) en cours...`,
|
||||||
'error'
|
'info'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let syncedCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
const syncedControlIds = new Set();
|
||||||
|
|
||||||
|
for (const remark of remarks) {
|
||||||
|
try {
|
||||||
|
await this.uploadRemark(remark);
|
||||||
|
await this.deleteRemark(remark.id);
|
||||||
|
syncedCount++;
|
||||||
|
syncedControlIds.add(String(remark.controlId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la synchronisation de la remarque:', error);
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncedCount > 0) {
|
||||||
|
this.showNotification(
|
||||||
|
`${syncedCount} remarque(s) synchronisée(s) avec succès`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notifier les écouteurs de l'événement de synchronisation
|
||||||
|
window.dispatchEvent(new CustomEvent('pendingRemarksSynced', {
|
||||||
|
detail: {
|
||||||
|
syncedCount,
|
||||||
|
syncedControlIds: Array.from(syncedControlIds)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Vérifier si le contrôle actuel affiché fait partie de ceux qui ont été synchronisés
|
||||||
|
const currentPathParts = window.location.pathname.split('/');
|
||||||
|
const controlsIndex = currentPathParts.indexOf('controls');
|
||||||
|
const currentControlId = controlsIndex !== -1 ? currentPathParts[controlsIndex + 1] : null;
|
||||||
|
|
||||||
|
if (currentControlId && syncedControlIds.has(String(currentControlId))) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedCount > 0) {
|
||||||
|
this.showNotification(
|
||||||
|
`${failedCount} remarque(s) n'ont pas pu être synchronisées`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isSyncing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,20 +389,53 @@ class OfflineManager {
|
||||||
formData.append('images', photo);
|
formData.append('images', photo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Déterminer l'URL cible selon le contexte (mobile ou desktop) et l'ID du contrôle de la remarque
|
||||||
|
const isMobile = window.location.pathname.startsWith('/mobile/') || window.location.pathname.includes('/mobile');
|
||||||
|
const targetUrl = isMobile
|
||||||
|
? `/mobile/controls/${encodeURIComponent(remark.controlId)}/`
|
||||||
|
: `/controls/${encodeURIComponent(remark.controlId)}/`;
|
||||||
|
|
||||||
// Envoyer au serveur
|
// Envoyer au serveur
|
||||||
const csrftoken = this.getCookie('csrftoken');
|
const csrftoken = this.getCookie('csrftoken') || document.querySelector('[name=csrfmiddlewaretoken]')?.value;
|
||||||
const currentUrl = window.location.pathname;
|
|
||||||
|
|
||||||
const response = await fetch(currentUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrftoken,
|
'X-CSRFToken': csrftoken,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json, text/javascript, */*; q=0.01'
|
||||||
},
|
},
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
let errorDetail = `Erreur HTTP: ${response.status} - ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const errorJson = await response.json();
|
||||||
|
if (errorJson && errorJson.message) {
|
||||||
|
errorDetail += ` (${errorJson.message})`;
|
||||||
|
} else if (errorJson && errorJson.error) {
|
||||||
|
errorDetail += ` (${errorJson.error})`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Pas du JSON, conserver le message d'erreur HTTP standard
|
||||||
|
}
|
||||||
|
throw new Error(errorDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le serveur a répondu en JSON, vérifier le statut
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
try {
|
||||||
|
const data = await response.clone().json();
|
||||||
|
if (data.status === 'error') {
|
||||||
|
throw new Error(data.message || 'Erreur lors de la sauvegarde côté serveur');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message && e.message.includes('sauvegarde')) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supprimer les photos associées
|
// Supprimer les photos associées
|
||||||
|
|
@ -452,7 +526,7 @@ class OfflineManager {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtenir le nombre de remarques en attente
|
// Obtenir le nombre de remarques en attente (total)
|
||||||
async getPendingCount() {
|
async getPendingCount() {
|
||||||
if (!this.db) return 0;
|
if (!this.db) return 0;
|
||||||
const transaction = this.db.transaction('pendingRemarks', 'readonly');
|
const transaction = this.db.transaction('pendingRemarks', 'readonly');
|
||||||
|
|
@ -464,6 +538,15 @@ class OfflineManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtenir le nombre de remarques en attente pour un contrôle spécifique
|
||||||
|
async getPendingCountForControl(controlId) {
|
||||||
|
if (!this.db) return 0;
|
||||||
|
const transaction = this.db.transaction('pendingRemarks', 'readonly');
|
||||||
|
const remarksStore = transaction.objectStore('pendingRemarks');
|
||||||
|
const allRemarks = await this.getAllRecords(remarksStore);
|
||||||
|
return allRemarks.filter(r => String(r.controlId) === String(controlId)).length;
|
||||||
|
}
|
||||||
|
|
||||||
// Obtenir toutes les remarques en attente pour un contrôle
|
// Obtenir toutes les remarques en attente pour un contrôle
|
||||||
async getPendingRemarks(controlId) {
|
async getPendingRemarks(controlId) {
|
||||||
if (!this.db) return [];
|
if (!this.db) return [];
|
||||||
|
|
|
||||||
|
|
@ -862,18 +862,32 @@ async function updatePendingBadge() {
|
||||||
if (typeof offlineManager === 'undefined' || !offlineManager) return;
|
if (typeof offlineManager === 'undefined' || !offlineManager) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const count = await offlineManager.getPendingCount();
|
const totalCount = await offlineManager.getPendingCount();
|
||||||
|
const controlId = document.getElementById('control_id')?.value;
|
||||||
|
const currentCount = controlId ? await offlineManager.getPendingCountForControl(controlId) : totalCount;
|
||||||
|
|
||||||
const pendingBadge = document.getElementById('pending-count');
|
const pendingBadge = document.getElementById('pending-count');
|
||||||
const pendingNumber = document.getElementById('pending-number');
|
const pendingNumber = document.getElementById('pending-number');
|
||||||
const forceSyncBtn = document.getElementById('force-sync-btn');
|
const forceSyncBtn = document.getElementById('force-sync-btn');
|
||||||
|
const syncStatus = document.getElementById('sync-status');
|
||||||
|
|
||||||
if (count > 0) {
|
if (totalCount > 0) {
|
||||||
pendingBadge.classList.remove('d-none');
|
pendingBadge.classList.remove('d-none');
|
||||||
pendingNumber.textContent = count;
|
if (currentCount === totalCount) {
|
||||||
|
pendingNumber.textContent = totalCount;
|
||||||
|
pendingBadge.title = '';
|
||||||
|
} else {
|
||||||
|
pendingNumber.textContent = `${currentCount} (${totalCount} total)`;
|
||||||
|
pendingBadge.title = '{% translate "Remarques en attente sur ce contrôle et d’autres contrôles" %}';
|
||||||
|
}
|
||||||
forceSyncBtn.classList.remove('d-none');
|
forceSyncBtn.classList.remove('d-none');
|
||||||
|
if (syncStatus) syncStatus.classList.remove('d-none');
|
||||||
} else {
|
} else {
|
||||||
pendingBadge.classList.add('d-none');
|
pendingBadge.classList.add('d-none');
|
||||||
forceSyncBtn.classList.add('d-none');
|
forceSyncBtn.classList.add('d-none');
|
||||||
|
if (navigator.onLine && syncStatus) {
|
||||||
|
syncStatus.classList.add('d-none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour du badge:', error);
|
console.error('Erreur lors de la mise à jour du badge:', error);
|
||||||
|
|
@ -934,8 +948,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Bouton de synchronisation forcée
|
// Bouton de synchronisation forcée
|
||||||
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
|
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
|
||||||
if (offlineManager) {
|
if (offlineManager) {
|
||||||
await offlineManager.syncPendingData();
|
const syncBtn = document.getElementById('force-sync-btn');
|
||||||
await updatePendingBadge();
|
const originalHtml = syncBtn ? syncBtn.innerHTML : '';
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.disabled = true;
|
||||||
|
syncBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await offlineManager.syncPendingData(true);
|
||||||
|
} finally {
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.disabled = false;
|
||||||
|
syncBtn.innerHTML = originalHtml;
|
||||||
|
}
|
||||||
|
await updatePendingBadge();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Écouter les synchronisations réussies pour rafraîchir l'affichage
|
||||||
|
window.addEventListener('pendingRemarksSynced', async () => {
|
||||||
|
await updatePendingBadge();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -887,15 +887,24 @@ async function updatePendingBadge() {
|
||||||
if (!offlineManager) return;
|
if (!offlineManager) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const count = await offlineManager.getPendingCount();
|
const totalCount = await offlineManager.getPendingCount();
|
||||||
|
const controlId = document.getElementById('control_id')?.value;
|
||||||
|
const currentCount = controlId ? await offlineManager.getPendingCountForControl(controlId) : totalCount;
|
||||||
|
|
||||||
const pendingBadge = document.getElementById('pending-count');
|
const pendingBadge = document.getElementById('pending-count');
|
||||||
const pendingNumber = document.getElementById('pending-number');
|
const pendingNumber = document.getElementById('pending-number');
|
||||||
const forceSyncBtn = document.getElementById('force-sync-btn');
|
const forceSyncBtn = document.getElementById('force-sync-btn');
|
||||||
const syncStatus = document.getElementById('sync-status');
|
const syncStatus = document.getElementById('sync-status');
|
||||||
|
|
||||||
if (count > 0) {
|
if (totalCount > 0) {
|
||||||
pendingBadge.classList.remove('d-none');
|
pendingBadge.classList.remove('d-none');
|
||||||
pendingNumber.textContent = count;
|
if (currentCount === totalCount) {
|
||||||
|
pendingNumber.textContent = totalCount;
|
||||||
|
pendingBadge.title = '';
|
||||||
|
} else {
|
||||||
|
pendingNumber.textContent = `${currentCount} (${totalCount} total)`;
|
||||||
|
pendingBadge.title = '{% translate "Remarques en attente sur ce contrôle et d’autres contrôles" %}';
|
||||||
|
}
|
||||||
forceSyncBtn.classList.remove('d-none');
|
forceSyncBtn.classList.remove('d-none');
|
||||||
// Afficher le badge s'il y a des remarques en attente
|
// Afficher le badge s'il y a des remarques en attente
|
||||||
syncStatus.classList.remove('d-none');
|
syncStatus.classList.remove('d-none');
|
||||||
|
|
@ -1180,11 +1189,30 @@ window.addEventListener('offline', () => {
|
||||||
// Bouton de synchronisation forcée
|
// Bouton de synchronisation forcée
|
||||||
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
|
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
|
||||||
if (offlineManager) {
|
if (offlineManager) {
|
||||||
await offlineManager.syncPendingData();
|
const syncBtn = document.getElementById('force-sync-btn');
|
||||||
await updatePendingBadge();
|
const originalHtml = syncBtn ? syncBtn.innerHTML : '';
|
||||||
await displayPendingRemarks();
|
if (syncBtn) {
|
||||||
|
syncBtn.disabled = true;
|
||||||
|
syncBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await offlineManager.syncPendingData(true);
|
||||||
|
} finally {
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.disabled = false;
|
||||||
|
syncBtn.innerHTML = originalHtml;
|
||||||
|
}
|
||||||
|
await updatePendingBadge();
|
||||||
|
await displayPendingRemarks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Écouter les synchronisations réussies pour rafraîchir l'affichage
|
||||||
|
window.addEventListener('pendingRemarksSynced', async () => {
|
||||||
|
await updatePendingBadge();
|
||||||
|
await displayPendingRemarks();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Modal Carrousel pour les photos d'un contrôle ou d'une remarque -->
|
<!-- Modal Carrousel pour les photos d'un contrôle ou d'une remarque -->
|
||||||
|
|
|
||||||
|
|
@ -261,4 +261,67 @@ class ControlsIndexTests(TestCase):
|
||||||
self.assertEqual(len(data["markers"]), 1)
|
self.assertEqual(len(data["markers"]), 1)
|
||||||
self.assertEqual(data["markers"][0]["code"], "CTRL-0001")
|
self.assertEqual(data["markers"][0]["code"], "CTRL-0001")
|
||||||
|
|
||||||
|
def test_submit_remark_ajax_success(self):
|
||||||
|
self.client.login(username="admin_user", password="password123")
|
||||||
|
url = reverse("controls:controls_detail", kwargs={"control_id": self.ctrl1.id})
|
||||||
|
data = {
|
||||||
|
"submit_remark": "true",
|
||||||
|
"control": str(self.ctrl1.id),
|
||||||
|
"description": "Nouvelle remarque test AJAX",
|
||||||
|
"status": "to_be_processed",
|
||||||
|
"reference_plan": "map",
|
||||||
|
"categories": "",
|
||||||
|
"qty-TOTAL_FORMS": "0",
|
||||||
|
"qty-INITIAL_FORMS": "0",
|
||||||
|
"qty-MIN_NUM_FORMS": "0",
|
||||||
|
"qty-MAX_NUM_FORMS": "1000",
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_json = response.json()
|
||||||
|
self.assertEqual(res_json["status"], "success")
|
||||||
|
self.assertEqual(res_json["control_id"], self.ctrl1.id)
|
||||||
|
self.assertTrue(Remark.objects.filter(control=self.ctrl1, description="Nouvelle remarque test AJAX").exists())
|
||||||
|
|
||||||
|
def test_submit_remark_ajax_control_id_mismatch(self):
|
||||||
|
self.client.login(username="admin_user", password="password123")
|
||||||
|
# Envoi d'une remarque destinée au contrôle 2 sur l'URL du contrôle 1
|
||||||
|
url = reverse("controls:controls_detail", kwargs={"control_id": self.ctrl1.id})
|
||||||
|
data = {
|
||||||
|
"submit_remark": "true",
|
||||||
|
"control": str(self.ctrl2.id),
|
||||||
|
"description": "Remarque avec mauvais control_id",
|
||||||
|
"status": "to_be_processed",
|
||||||
|
"reference_plan": "map",
|
||||||
|
"categories": "",
|
||||||
|
"qty-TOTAL_FORMS": "0",
|
||||||
|
"qty-INITIAL_FORMS": "0",
|
||||||
|
"qty-MIN_NUM_FORMS": "0",
|
||||||
|
"qty-MAX_NUM_FORMS": "1000",
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
res_json = response.json()
|
||||||
|
self.assertEqual(res_json["status"], "error")
|
||||||
|
self.assertIn("Incohérence", res_json["message"])
|
||||||
|
self.assertFalse(Remark.objects.filter(description="Remarque avec mauvais control_id").exists())
|
||||||
|
|
||||||
|
def test_submit_remark_ajax_validation_error(self):
|
||||||
|
self.client.login(username="admin_user", password="password123")
|
||||||
|
url = reverse("controls:controls_detail", kwargs={"control_id": self.ctrl1.id})
|
||||||
|
# Description vide / statut manquant
|
||||||
|
data = {
|
||||||
|
"submit_remark": "true",
|
||||||
|
"control": str(self.ctrl1.id),
|
||||||
|
"description": "",
|
||||||
|
"status": "",
|
||||||
|
"qty-TOTAL_FORMS": "0",
|
||||||
|
"qty-INITIAL_FORMS": "0",
|
||||||
|
"qty-MIN_NUM_FORMS": "0",
|
||||||
|
"qty-MAX_NUM_FORMS": "1000",
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
res_json = response.json()
|
||||||
|
self.assertEqual(res_json["status"], "error")
|
||||||
|
self.assertIn("remark_form", res_json["errors"])
|
||||||
|
|
|
||||||
|
|
@ -1036,7 +1036,19 @@ def controls_detail(request, control_id):
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if 'submit_remark' in request.POST:
|
if 'submit_remark' in request.POST:
|
||||||
if remark_form.is_valid() and qty_formset.is_valid():
|
is_ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' or 'application/json' in request.headers.get('Accept', '')
|
||||||
|
|
||||||
|
# Vérifier la cohérence de l'ID du contrôle si fourni dans le formulaire
|
||||||
|
posted_control_id = request.POST.get('control')
|
||||||
|
if posted_control_id and str(posted_control_id) != str(control.id):
|
||||||
|
if is_ajax:
|
||||||
|
return JsonResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Incohérence d'identifiant de contrôle (reçu {posted_control_id}, attendu {control.id})"
|
||||||
|
}, status=400)
|
||||||
|
messages.error(request, _("Incohérence lors de l'enregistrement de la remarque."))
|
||||||
|
|
||||||
|
elif remark_form.is_valid() and qty_formset.is_valid():
|
||||||
remark = remark_form.save(commit=False)
|
remark = remark_form.save(commit=False)
|
||||||
remark.control = control
|
remark.control = control
|
||||||
remark.updated_by = request.user
|
remark.updated_by = request.user
|
||||||
|
|
@ -1096,11 +1108,31 @@ def controls_detail(request, control_id):
|
||||||
|
|
||||||
qty_formset.save()
|
qty_formset.save()
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
return JsonResponse({
|
||||||
|
"status": "success",
|
||||||
|
"remark_id": remark.id,
|
||||||
|
"control_id": control.id,
|
||||||
|
"message": str(_("Remarque sauvegardée avec succès !"))
|
||||||
|
})
|
||||||
|
|
||||||
messages.success(request, _("Remarque sauvegardée avec succès !"))
|
messages.success(request, _("Remarque sauvegardée avec succès !"))
|
||||||
|
|
||||||
#return redirect("controls:control_detail", control_id=control.id)
|
#return redirect("controls:control_detail", control_id=control.id)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
if is_ajax:
|
||||||
|
errors = {}
|
||||||
|
if not remark_form.is_valid():
|
||||||
|
errors['remark_form'] = remark_form.errors.get_json_data()
|
||||||
|
if not qty_formset.is_valid():
|
||||||
|
errors['qty_formset'] = qty_formset.errors
|
||||||
|
return JsonResponse({
|
||||||
|
"status": "error",
|
||||||
|
"message": str(_("Erreur lors de la sauvegarde de la remarque.")),
|
||||||
|
"errors": errors
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
messages.error(request, _("Erreur lors de la sauvegarde de la remarque."))
|
messages.error(request, _("Erreur lors de la sauvegarde de la remarque."))
|
||||||
|
|
||||||
elif 'submit_plan' in request.POST:
|
elif 'submit_plan' in request.POST:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue