Compare commits
No commits in common. "496b6d9c8bbe26b260bd4ea29906ea95ac26e1fe" and "384a2b29c37ed658141bd49b762c05c0e1a80978" have entirely different histories.
496b6d9c8b
...
384a2b29c3
28 changed files with 20 additions and 554 deletions
|
|
@ -1,43 +0,0 @@
|
|||
import os
|
||||
from django.db import migrations
|
||||
from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon
|
||||
|
||||
|
||||
def create_rbc_territory(apps, schema_editor):
|
||||
Territory = apps.get_model('common', 'Territory')
|
||||
if Territory.objects.filter(code='RBC').exists():
|
||||
return
|
||||
|
||||
from django.conf import settings
|
||||
fixture_path = os.path.join(settings.BASE_DIR, 'common', 'fixtures', 'rbc_territory_wkt.txt')
|
||||
if os.path.exists(fixture_path):
|
||||
with open(fixture_path, 'r', encoding='utf-8') as f:
|
||||
wkt = f.read().strip()
|
||||
geom = GEOSGeometry(wkt, srid=3812)
|
||||
if isinstance(geom, Polygon):
|
||||
geom = MultiPolygon(geom, srid=3812)
|
||||
Territory.objects.create(
|
||||
name_fr="Région de Bruxelles-Capitale",
|
||||
name_nl="Brussels Hoofdstedelijk Gewest",
|
||||
code="RBC",
|
||||
description="Zone d'action régionale de Bruxelles Mobilité couvrant les 19 communes bruxelloises sans discontinuités territoriales.",
|
||||
geom=geom,
|
||||
is_default=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
def remove_rbc_territory(apps, schema_editor):
|
||||
Territory = apps.get_model('common', 'Territory')
|
||||
Territory.objects.filter(code='RBC').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0017_territory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_rbc_territory, reverse_code=remove_rbc_territory),
|
||||
]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-05 13:22
|
||||
# Generated by Django 6.0.7 on 2026-10-05 13:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0018_setup_rbc_default_territory'),
|
||||
('common', '0017_territory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
@ -1141,7 +1141,6 @@ def _get_recent_activity(user, allowed_contracts):
|
|||
for item in Observation.objects.filter(intervention__contract__in=allowed_contracts, created_at__gte=cutoff).select_related('thematic').order_by('-created_at')[:4]:
|
||||
status_classes = {
|
||||
'to_process': 'bg-warning-subtle text-warning-emphasis border border-warning-subtle',
|
||||
'needs_info': 'bg-warning-subtle text-warning-emphasis border border-warning-subtle',
|
||||
'in_progress': 'bg-primary-subtle text-primary border border-primary-subtle',
|
||||
'processed': 'bg-success-subtle text-success-emphasis border border-success-subtle',
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -400,12 +400,6 @@ msgstr "In preparation"
|
|||
msgid "À traiter"
|
||||
msgstr "To be processed"
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
msgid "Informations demandées"
|
||||
msgstr "Information requested"
|
||||
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
#: observations/templates/observations/observation_detail.html:876
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -24,11 +24,6 @@ msgstr "In preparation"
|
|||
msgid "À traiter"
|
||||
msgstr "To be processed"
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:16
|
||||
msgid "Informations demandées"
|
||||
msgstr "Information requested"
|
||||
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:17
|
||||
msgid "En cours"
|
||||
msgstr "In progress"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -405,12 +405,6 @@ msgstr "En préparation"
|
|||
msgid "À traiter"
|
||||
msgstr "À traiter"
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
msgid "Informations demandées"
|
||||
msgstr "Informations demandées"
|
||||
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
#: observations/templates/observations/observation_detail.html:876
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -24,11 +24,6 @@ msgstr "En préparation"
|
|||
msgid "À traiter"
|
||||
msgstr "À traiter"
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:16
|
||||
msgid "Informations demandées"
|
||||
msgstr "Informations demandées"
|
||||
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:17
|
||||
msgid "En cours"
|
||||
msgstr "En cours"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -404,12 +404,6 @@ msgstr "In voorbereiding"
|
|||
msgid "À traiter"
|
||||
msgstr "Te behandelen"
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
msgid "Informations demandées"
|
||||
msgstr "Informatie gevraagd"
|
||||
|
||||
|
||||
#: observations/models.py:428
|
||||
#: observations/templates/observations/_observations_list_mobile.html:71
|
||||
#: observations/templates/observations/observation_detail.html:876
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -24,11 +24,6 @@ msgstr "In voorbereiding"
|
|||
msgid "À traiter"
|
||||
msgstr "Te behandelen"
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:16
|
||||
msgid "Informations demandées"
|
||||
msgstr "Informatie gevraagd"
|
||||
|
||||
|
||||
#: loko/observations/static/observations/observations_index.js:17
|
||||
msgid "En cours"
|
||||
msgstr "In behandeling"
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-06 07:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('observations', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='observation',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_preparation', 'En préparation'), ('to_process', 'À traiter'), ('needs_info', 'Informations demandées'), ('in_progress', 'En cours'), ('processed', 'Traité'), ('not_relevant', 'Non relevant'), ('to_redirect', 'À rediriger'), ('duplicate', 'Doublon'), ('closed', 'Clôturé'), ('archived', 'Archivé')], default='in_preparation', max_length=20),
|
||||
),
|
||||
]
|
||||
|
|
@ -425,7 +425,6 @@ class Observation(models.Model):
|
|||
STATUS_CHOICES = [
|
||||
('in_preparation', _('En préparation')),
|
||||
('to_process', _('À traiter')),
|
||||
('needs_info', _('Informations demandées')),
|
||||
('in_progress', _('En cours')),
|
||||
('processed', _('Traité')),
|
||||
('not_relevant', _('Non relevant')),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ function truncateText(text, limit) {
|
|||
const STATUS_METADATA = {
|
||||
in_preparation: { label: gettext('En préparation'), badge: 'secondary', color: '#6c757d' },
|
||||
to_process: { label: gettext('À traiter'), badge: 'danger', color: '#dc3545' },
|
||||
needs_info: { label: gettext('Informations demandées'), badge: 'warning', color: '#fd7e14' },
|
||||
in_progress: { label: gettext('En cours'), badge: 'warning', color: '#ffc107' },
|
||||
processed: { label: gettext('Traité'), badge: 'success', color: '#198754' },
|
||||
not_relevant: { label: gettext('Non relevant'), badge: 'secondary', color: '#6c757d' },
|
||||
|
|
@ -60,7 +59,6 @@ const observations_layer_defs = [
|
|||
['get', 'status'],
|
||||
'in_preparation', STATUS_METADATA.in_preparation.color,
|
||||
'to_process', STATUS_METADATA.to_process.color,
|
||||
'needs_info', STATUS_METADATA.needs_info.color,
|
||||
'in_progress', STATUS_METADATA.in_progress.color,
|
||||
'processed', STATUS_METADATA.processed.color,
|
||||
'closed', STATUS_METADATA.closed.color,
|
||||
|
|
|
|||
|
|
@ -66,10 +66,6 @@
|
|||
<span class="badge bg-danger">
|
||||
<i class="bi bi-clock-history me-1"></i>{% translate "À traiter" %}
|
||||
</span>
|
||||
{% elif observation.status == 'needs_info' %}
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-question-circle me-1"></i>{% translate "Informations demandées" %}
|
||||
</span>
|
||||
{% elif observation.status == 'in_progress' %}
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-hourglass-split me-1"></i>{% translate "En cours" %}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
{% if can_edit or can_delete %}
|
||||
<div class="d-flex flex-wrap gap-2 pt-1">
|
||||
{% if can_edit %}
|
||||
{% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' or observation.status == 'needs_info' %}
|
||||
{% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' %}
|
||||
<a href="{% url 'observations:observation_edit' observation.id %}" class="btn btn-outline-secondary btn-sm" style="width:150px">
|
||||
<i class="bi bi-pencil"></i> {% translate "Modifier" %}
|
||||
</a>
|
||||
|
|
@ -161,8 +161,6 @@
|
|||
<span class="badge bg-secondary"><i class="bi bi-hourglass-split me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'to_process' %}
|
||||
<span class="badge bg-danger"><i class="bi bi-clock-history me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'needs_info' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-question-circle me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'in_progress' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-hourglass-split me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'processed' %}
|
||||
|
|
@ -181,34 +179,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if observation.status == 'needs_info' %}
|
||||
<div class="alert alert-warning d-flex align-items-start gap-2 mb-3" role="alert">
|
||||
<i class="bi bi-question-circle-fill fs-5 flex-shrink-0 mt-1"></i>
|
||||
<div class="w-100">
|
||||
<h6 class="alert-heading mb-1">{% translate "Informations complémentaires demandées" %}</h6>
|
||||
<p class="mb-2 small">
|
||||
{% if is_creator %}
|
||||
{% translate "Des informations complémentaires sont nécessaires pour traiter votre observation. Vous pouvez modifier la fiche pour corriger/compléter les données (photos, checklist, description) ou répondre directement ci-dessous dans la discussion." %}
|
||||
{% else %}
|
||||
{% translate "Cette observation est en attente d'informations complémentaires de la part de l'observateur." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if is_creator %}
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'observations:observation_edit' observation.id %}" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-pencil"></i> {% translate "Modifier la fiche" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="#discussion" class="btn btn-outline-dark btn-sm">
|
||||
<i class="bi bi-chat-dots"></i> {% translate "Répondre dans la discussion" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Description -->
|
||||
<div class="info-section">
|
||||
<h5>{% translate "Description" %}</h5>
|
||||
|
|
@ -645,13 +615,6 @@
|
|||
data-icon="bi-check2-circle" data-btn-class="btn-success">
|
||||
<i class="bi bi-check2-circle"></i> {% translate "Marquer comme traitée" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" style="width:200px"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="needs_info"
|
||||
data-label="{% translate 'Demander un complément' %}"
|
||||
data-icon="bi-question-circle" data-btn-class="btn-warning">
|
||||
<i class="bi bi-question-circle"></i> {% translate "Demander un complément" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" style="width:200px"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="duplicate"
|
||||
|
|
@ -677,30 +640,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Renvoyer / Compléments fournis (needs_info → to_process) -->
|
||||
{% if is_creator or can_take_action %}
|
||||
{% if observation.status == 'needs_info' %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="post" action="{% url 'observations:observation_change_status' observation.id %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="new_status" value="to_process">
|
||||
<button type="submit" class="btn btn-success btn-sm" style="width:200px">
|
||||
<i class="bi bi-send-check"></i> {% translate "Compléments fournis" %}
|
||||
</button>
|
||||
</form>
|
||||
{% if can_take_action %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" style="width:200px"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="not_relevant"
|
||||
data-label="{% translate 'Non relevant / Sans suite' %}"
|
||||
data-icon="bi-slash-circle" data-btn-class="btn-secondary">
|
||||
<i class="bi bi-slash-circle"></i> {% translate "Classer sans suite" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Envoyer (in_preparation → to_process) -->
|
||||
{% if is_creator or 'admin' in user_roles %}
|
||||
{% if observation.status == 'in_preparation' or not observation.status %}
|
||||
|
|
@ -718,7 +657,7 @@
|
|||
|
||||
<!-- Archiver -->
|
||||
{% if 'admin' in user_roles %}
|
||||
{% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' or observation.status == 'needs_info' %}
|
||||
{% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' %}
|
||||
<div>
|
||||
<form method="post" action="{% url 'observations:observation_change_status' observation.id %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
|
|
@ -1165,14 +1104,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (!isSymptom) {
|
||||
document.getElementById('actionStatusSymptomId').value = '';
|
||||
}
|
||||
} else if (status === 'needs_info') {
|
||||
const comment = document.getElementById('actionStatusComment').value.trim();
|
||||
if (!comment) {
|
||||
e.preventDefault();
|
||||
document.getElementById('actionStatusComment').classList.add('is-invalid');
|
||||
document.getElementById('actionStatusComment').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1194,22 +1125,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
document.getElementById('redirectSymptomDisplay').style.display = 'none';
|
||||
document.getElementById('redirectSymptomDisplay').textContent = '';
|
||||
document.getElementById('redirectSymptomSearch').classList.remove('is-invalid');
|
||||
document.getElementById('actionStatusComment').classList.remove('is-invalid');
|
||||
|
||||
// Adapter le label du commentaire si needs_info
|
||||
const isNeedsInfo = status === 'needs_info';
|
||||
const commentLabel = document.querySelector('label[for="actionStatusComment"]');
|
||||
if (commentLabel) {
|
||||
if (isNeedsInfo) {
|
||||
commentLabel.innerHTML = '{% translate "Précisez les informations demandées" %} <span class="text-danger">*</span>';
|
||||
} else {
|
||||
commentLabel.innerHTML = '{% translate "Commentaire" %} <span class="text-muted fst-italic">({% translate "optionnel" %})</span>';
|
||||
}
|
||||
}
|
||||
document.getElementById('actionStatusComment').placeholder = isNeedsInfo ?
|
||||
'{% translate "Indiquez ici les détails ou photos attendus..." %}' :
|
||||
'{% translate "Ajoutez un commentaire..." %}';
|
||||
|
||||
// Afficher/masquer la section choix selon le statut
|
||||
const isToRedirect = status === 'to_redirect';
|
||||
document.getElementById('redirectChoiceSection').style.display = isToRedirect ? '' : 'none';
|
||||
|
|
@ -1309,7 +1224,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const _nearbyStatusColors = {
|
||||
'in_preparation': '#6c757d',
|
||||
'to_process': '#dc3545',
|
||||
'needs_info': '#fd7e14',
|
||||
'in_progress': '#ffc107',
|
||||
'processed': '#28a745',
|
||||
'not_relevant': '#6c757d',
|
||||
|
|
@ -1342,7 +1256,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
'match', ['get', 'status'],
|
||||
'in_preparation', '#6c757d',
|
||||
'to_process', '#dc3545',
|
||||
'needs_info', '#fd7e14',
|
||||
'in_progress', '#ffc107',
|
||||
'processed', '#28a745',
|
||||
'not_relevant', '#6c757d',
|
||||
|
|
|
|||
|
|
@ -148,13 +148,6 @@
|
|||
data-icon="bi-files" data-btn-class="btn-info">
|
||||
<i class="bi bi-files"></i> {% translate "Doublon" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="needs_info"
|
||||
data-label="{% translate 'Demander un complément' %}"
|
||||
data-icon="bi-question-circle" data-btn-class="btn-warning">
|
||||
<i class="bi bi-question-circle"></i> {% translate "Demander un complément" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="to_redirect"
|
||||
|
|
@ -185,30 +178,8 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- Renvoyer / Compléments fournis (needs_info → to_process) -->
|
||||
{% if is_creator or can_take_action %}
|
||||
{% if observation.status == 'needs_info' %}
|
||||
<form method="post" action="{% url 'observations:observation_change_status' observation.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="new_status" value="to_process">
|
||||
<button type="submit" class="btn btn-success btn-sm" style="min-width:120px">
|
||||
<i class="bi bi-send-check"></i> {% translate "Compléments fournis" %}
|
||||
</button>
|
||||
</form>
|
||||
{% if can_take_action %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" style="min-width:120px"
|
||||
data-bs-toggle="modal" data-bs-target="#actionStatusModal"
|
||||
data-status="not_relevant"
|
||||
data-label="{% translate 'Non relevant / Sans suite' %}"
|
||||
data-icon="bi-slash-circle" data-btn-class="btn-secondary">
|
||||
<i class="bi bi-slash-circle"></i> {% translate "Classer sans suite" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Archiver -->
|
||||
{% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' or observation.status == 'needs_info' %}
|
||||
{% if observation.status == 'processed' or observation.status == 'not_relevant' or observation.status == 'to_redirect' or observation.status == 'duplicate' %}
|
||||
{% if 'admin' in user_roles or 'operator' in user_roles or 'manager' in user_roles or 'controller' in user_roles %}
|
||||
<form method="post" action="{% url 'observations:observation_change_status' observation.id %}">
|
||||
{% csrf_token %}
|
||||
|
|
@ -230,7 +201,7 @@
|
|||
{% if can_edit or can_delete %}
|
||||
<span class="ms-1">
|
||||
{% if can_edit %}
|
||||
{% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' or observation.status == 'needs_info' %}
|
||||
{% if observation.status == 'in_preparation' or observation.status == 'to_process' or observation.status == 'to_redirect' %}
|
||||
<a href="{% url 'mobile:observation_edit_mobile' observation.id %}" class="btn btn-outline-secondary btn-sm" style="font-size:small">
|
||||
<i class="bi bi-pencil"></i> {% translate "Modifier" %}
|
||||
</a>
|
||||
|
|
@ -254,8 +225,6 @@
|
|||
<span class="badge bg-secondary text-dark"><i class="bi bi-hourglass-split me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'to_process' %}
|
||||
<span class="badge bg-danger"><i class="bi bi-clock-history me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'needs_info' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-question-circle me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'in_progress' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-hourglass-split me-1"></i>{{ observation.get_status_display }}</span>
|
||||
{% elif observation.status == 'processed' %}
|
||||
|
|
@ -274,34 +243,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if observation.status == 'needs_info' %}
|
||||
<div class="alert alert-warning d-flex align-items-start gap-2 mb-3" role="alert">
|
||||
<i class="bi bi-question-circle-fill fs-5 flex-shrink-0 mt-1"></i>
|
||||
<div class="w-100">
|
||||
<h6 class="alert-heading mb-1">{% translate "Informations complémentaires demandées" %}</h6>
|
||||
<p class="mb-2 small">
|
||||
{% if is_creator %}
|
||||
{% translate "Des informations complémentaires sont nécessaires pour traiter votre observation. Vous pouvez modifier la fiche pour corriger/compléter les données (photos, checklist, description) ou répondre directement ci-dessous dans la discussion." %}
|
||||
{% else %}
|
||||
{% translate "Cette observation est en attente d'informations complémentaires de la part de l'observateur." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if is_creator %}
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'mobile:observation_edit_mobile' observation.id %}" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-pencil"></i> {% translate "Modifier la fiche" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="#discussion" class="btn btn-outline-dark btn-sm">
|
||||
<i class="bi bi-chat-dots"></i> {% translate "Discussion" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Thematic and Category -->
|
||||
<div class="info-card">
|
||||
<div class="row">
|
||||
|
|
@ -968,14 +909,6 @@ function lightboxNavMobile(dir) { _mobLightboxShow(_mobPhotoIdx + dir); }
|
|||
if (!isSymptom) {
|
||||
document.getElementById('actionStatusSymptomId').value = '';
|
||||
}
|
||||
} else if (status === 'needs_info') {
|
||||
const comment = document.getElementById('actionStatusComment').value.trim();
|
||||
if (!comment) {
|
||||
e.preventDefault();
|
||||
document.getElementById('actionStatusComment').classList.add('is-invalid');
|
||||
document.getElementById('actionStatusComment').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -997,22 +930,6 @@ function lightboxNavMobile(dir) { _mobLightboxShow(_mobPhotoIdx + dir); }
|
|||
document.getElementById('redirectSymptomDisplay').style.display = 'none';
|
||||
document.getElementById('redirectSymptomDisplay').textContent = '';
|
||||
document.getElementById('redirectSymptomSearch').classList.remove('is-invalid');
|
||||
document.getElementById('actionStatusComment').classList.remove('is-invalid');
|
||||
|
||||
// Adapter le label du commentaire si needs_info
|
||||
const isNeedsInfo = status === 'needs_info';
|
||||
const commentLabel = document.querySelector('label[for="actionStatusComment"]');
|
||||
if (commentLabel) {
|
||||
if (isNeedsInfo) {
|
||||
commentLabel.innerHTML = '{% translate "Précisez les informations demandées" %} <span class="text-danger">*</span>';
|
||||
} else {
|
||||
commentLabel.innerHTML = '{% translate "Commentaire" %} <span class="text-muted fst-italic">({% translate "optionnel" %})</span>';
|
||||
}
|
||||
}
|
||||
document.getElementById('actionStatusComment').placeholder = isNeedsInfo ?
|
||||
'{% translate "Indiquez ici les détails ou photos attendus..." %}' :
|
||||
'{% translate "Ajoutez un commentaire..." %}';
|
||||
|
||||
// Afficher/masquer la section choix selon le statut
|
||||
const isToRedirect = status === 'to_redirect';
|
||||
document.getElementById('redirectChoiceSection').style.display = isToRedirect ? '' : 'none';
|
||||
|
|
|
|||
|
|
@ -247,23 +247,6 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline-status-needs_info {
|
||||
--bs-btn-color: #fd7e14;
|
||||
--bs-btn-border-color: #fd7e14;
|
||||
--bs-btn-hover-color: #fff;
|
||||
--bs-btn-hover-bg: #fd7e14;
|
||||
--bs-btn-hover-border-color: #fd7e14;
|
||||
--bs-btn-active-color: #fff;
|
||||
--bs-btn-active-bg: #fd7e14;
|
||||
--bs-btn-active-border-color: #fd7e14;
|
||||
}
|
||||
|
||||
.btn-check:checked + .btn-outline-status-needs_info {
|
||||
background-color: #fd7e14;
|
||||
border-color: #fd7e14;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline-status-to_redirect {
|
||||
--bs-btn-color: #ffc107;
|
||||
--bs-btn-border-color: #ffc107;
|
||||
|
|
|
|||
|
|
@ -639,153 +639,3 @@ class ObservationViewerPermissionsTest(TestCase):
|
|||
self.assertFalse(access_context.can_process_observation(self.obs_green))
|
||||
|
||||
|
||||
class ObservationNeedsInfoTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
|
||||
self.thematic = Thematic.objects.create(
|
||||
code='THEME-OBS-INFO',
|
||||
name_fr='Thématique Obs Info',
|
||||
name_nl='Thema Obs Info',
|
||||
)
|
||||
self.category = AssetCategory.objects.create(
|
||||
thematic=self.thematic,
|
||||
code='CAT-OBS-INFO',
|
||||
name_fr='Catégorie Obs Info',
|
||||
name_nl='Categorie Obs Info',
|
||||
)
|
||||
|
||||
self.creator = User.objects.create_user('creator_user', password='pass')
|
||||
self.creator_config = UserConfig.objects.create(user=self.creator, is_intern=True)
|
||||
UserThematics.objects.create(
|
||||
user_config=self.creator_config,
|
||||
thematic=self.thematic,
|
||||
can_view_interventions=True,
|
||||
)
|
||||
|
||||
self.manager = User.objects.create_user('manager_user', password='pass')
|
||||
self.manager_config = UserConfig.objects.create(user=self.manager, is_intern=True)
|
||||
manager_role, _ = Role.objects.get_or_create(name='manager')
|
||||
self.manager_config.roles.add(manager_role)
|
||||
UserThematics.objects.create(
|
||||
user_config=self.manager_config,
|
||||
thematic=self.thematic,
|
||||
can_process_observations=True,
|
||||
can_view_interventions=True,
|
||||
)
|
||||
|
||||
self.unprivileged = User.objects.create_user('unprivileged_user', password='pass')
|
||||
self.unprivileged_config = UserConfig.objects.create(user=self.unprivileged, is_intern=False)
|
||||
UserThematics.objects.create(
|
||||
user_config=self.unprivileged_config,
|
||||
thematic=self.thematic,
|
||||
)
|
||||
|
||||
self.obs = Observation.objects.create(
|
||||
description='Observation to be clarified',
|
||||
latitude=50.85,
|
||||
longitude=4.35,
|
||||
created_by=self.creator,
|
||||
thematic=self.thematic,
|
||||
category=self.category,
|
||||
status='to_process',
|
||||
)
|
||||
|
||||
def test_manager_can_request_info_with_mandatory_comment(self):
|
||||
self.client.login(username='manager_user', password='pass')
|
||||
url = reverse('observations:observation_change_status', args=[self.obs.id])
|
||||
|
||||
# Without comment: must fail/stay in to_process
|
||||
resp = self.client.post(url, {'new_status': 'needs_info', 'comment': ''})
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'to_process')
|
||||
|
||||
# With comment: transitions to needs_info, creates comment and discussion message
|
||||
resp = self.client.post(url, {
|
||||
'new_status': 'needs_info',
|
||||
'comment': 'Pouvez-vous préciser la taille et joindre une photo ?',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'needs_info')
|
||||
|
||||
# Check ObservationComment created
|
||||
comment = self.obs.comments.last()
|
||||
self.assertIsNotNone(comment)
|
||||
self.assertEqual(comment.author, self.manager)
|
||||
self.assertIn('Pouvez-vous préciser', comment.comment)
|
||||
|
||||
# Check Discussion & Message created
|
||||
from notifications.services import get_or_create_observation_discussion
|
||||
discussion, _ = get_or_create_observation_discussion(self.obs)
|
||||
last_msg = discussion.messages.last()
|
||||
self.assertIsNotNone(last_msg)
|
||||
self.assertEqual(last_msg.author, self.manager)
|
||||
self.assertIn('Pouvez-vous préciser', last_msg.content)
|
||||
|
||||
def test_unauthorized_user_cannot_request_info(self):
|
||||
self.client.login(username='unprivileged_user', password='pass')
|
||||
url = reverse('observations:observation_change_status', args=[self.obs.id])
|
||||
resp = self.client.post(url, {
|
||||
'new_status': 'needs_info',
|
||||
'comment': 'Commentaire non autorisé',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'to_process')
|
||||
|
||||
def test_creator_answering_in_discussion_auto_resubmits_observation(self):
|
||||
self.obs.status = 'needs_info'
|
||||
self.obs.save(update_fields=['status'])
|
||||
|
||||
self.client.login(username='creator_user', password='pass')
|
||||
msg_url = reverse('observations:observation_add_message', args=[self.obs.id])
|
||||
|
||||
resp = self.client.post(msg_url, {
|
||||
'content': "Voici les informations : la fissure mesure 20 cm.",
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'to_process')
|
||||
|
||||
# Check auto-comment created
|
||||
comment = self.obs.comments.last()
|
||||
self.assertIsNotNone(comment)
|
||||
self.assertEqual(comment.author, self.creator)
|
||||
self.assertIn("Réponse apportée dans la discussion", comment.comment)
|
||||
|
||||
def test_creator_clicking_complements_provided_resubmits_observation(self):
|
||||
self.obs.status = 'needs_info'
|
||||
self.obs.save(update_fields=['status'])
|
||||
|
||||
self.client.login(username='creator_user', password='pass')
|
||||
url = reverse('observations:observation_change_status', args=[self.obs.id])
|
||||
|
||||
resp = self.client.post(url, {
|
||||
'new_status': 'to_process',
|
||||
'comment': 'Compléments apportés',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'to_process')
|
||||
|
||||
def test_manager_can_classify_not_relevant_from_needs_info(self):
|
||||
self.obs.status = 'needs_info'
|
||||
self.obs.save(update_fields=['status'])
|
||||
|
||||
self.client.login(username='manager_user', password='pass')
|
||||
url = reverse('observations:observation_change_status', args=[self.obs.id])
|
||||
|
||||
resp = self.client.post(url, {
|
||||
'new_status': 'not_relevant',
|
||||
'comment': 'Sans suite après absence de réponse',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
self.obs.refresh_from_db()
|
||||
self.assertEqual(self.obs.status, 'not_relevant')
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ def observations_index(request):
|
|||
_status_badge_map = {
|
||||
'in_preparation': 'secondary',
|
||||
'to_process': 'danger',
|
||||
'needs_info': 'warning',
|
||||
'in_progress': 'warning',
|
||||
'processed': 'success',
|
||||
'not_relevant': 'secondary',
|
||||
|
|
@ -206,7 +205,6 @@ def observations_index_mobile(request):
|
|||
_OBS_STATUS_COLOR = {
|
||||
'in_preparation': 'secondary',
|
||||
'to_process': 'danger',
|
||||
'needs_info': 'warning',
|
||||
'in_progress': 'warning',
|
||||
'processed': 'success',
|
||||
'not_relevant': 'secondary',
|
||||
|
|
@ -746,7 +744,7 @@ def observation_detail(request, observation_id):
|
|||
|
||||
# Vérifier si l'utilisateur peut supprimer l'observation
|
||||
can_delete = (
|
||||
observation.status in ('in_preparation', 'to_process', 'needs_info') and
|
||||
observation.status in ('in_preparation', 'to_process') and
|
||||
can_edit
|
||||
)
|
||||
|
||||
|
|
@ -896,30 +894,12 @@ def observation_detail(request, observation_id):
|
|||
def observation_change_status(request, observation_id):
|
||||
observation = request.observation_obj
|
||||
new_status = request.POST.get('new_status')
|
||||
allowed_statuses = ['to_process', 'processed', 'not_relevant', 'to_redirect', 'duplicate', 'needs_info', 'closed', 'archived']
|
||||
allowed_statuses = ['to_process', 'processed', 'not_relevant', 'to_redirect', 'duplicate', 'closed', 'archived']
|
||||
if new_status not in allowed_statuses:
|
||||
return HttpResponseForbidden("Statut non autorisé.")
|
||||
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
is_mobile = '/mobile/' in referer
|
||||
redirect_target = 'mobile:observation_detail_mobile' if is_mobile else 'observations:observation_detail'
|
||||
|
||||
# Règles de transition vers 'to_process'
|
||||
if new_status == 'to_process':
|
||||
if observation.status == 'in_preparation':
|
||||
pass
|
||||
elif observation.status == 'needs_info':
|
||||
# Autorisé pour le créateur ou les gestionnaires habilités
|
||||
is_creator = (observation.created_by == request.user)
|
||||
access_context = request.observation_access
|
||||
can_manage = access_context.can_process_observation(observation) or any(
|
||||
access_context.user_config.has_role(r) for r in ['admin', 'operator', 'manager', 'controller', 'top_manager']
|
||||
)
|
||||
if not (is_creator or can_manage):
|
||||
return HttpResponseForbidden("Vous n'avez pas la permission d'effectuer cette action.")
|
||||
else:
|
||||
return HttpResponseForbidden("Transition non autorisée.")
|
||||
|
||||
# Règles simples de transition
|
||||
if new_status == 'to_process' and observation.status != 'in_preparation':
|
||||
return HttpResponseForbidden("Transition non autorisée.")
|
||||
# Contrôle des rôles/permissions pour 'processed' (tous types)
|
||||
if new_status == 'processed':
|
||||
access_context = request.observation_access
|
||||
|
|
@ -928,28 +908,15 @@ def observation_change_status(request, observation_id):
|
|||
fallback_roles = ['manager', 'controller']
|
||||
if not any(access_context.user_config.has_role(role) for role in fallback_roles):
|
||||
return HttpResponseForbidden("Vous n'avez pas la permission de traiter cette observation.")
|
||||
|
||||
# Contrôle des rôles pour 'not_relevant', 'to_redirect', 'duplicate' et 'needs_info'
|
||||
if new_status in ('not_relevant', 'to_redirect', 'duplicate', 'needs_info'):
|
||||
if new_status == 'not_relevant':
|
||||
if observation.status not in ('to_process', 'needs_info'):
|
||||
return HttpResponseForbidden("Transition non autorisée.")
|
||||
elif observation.status != 'to_process':
|
||||
# Contrôle des rôles pour 'not_relevant', 'to_redirect' et 'duplicate'
|
||||
if new_status in ('not_relevant', 'to_redirect', 'duplicate'):
|
||||
if observation.status != 'to_process':
|
||||
return HttpResponseForbidden("Transition non autorisée.")
|
||||
|
||||
access_context = request.observation_access
|
||||
if not access_context.can_process_observation(observation):
|
||||
fallback_roles = ['manager', 'controller', 'operator', 'admin']
|
||||
fallback_roles = ['manager', 'controller']
|
||||
if not any(access_context.user_config.has_role(role) for role in fallback_roles):
|
||||
return HttpResponseForbidden("Vous n'avez pas la permission d'effectuer cette action.")
|
||||
|
||||
# Pour 'needs_info' : le motif / commentaire est obligatoire
|
||||
if new_status == 'needs_info':
|
||||
comment_text = (request.POST.get('comment') or '').strip()
|
||||
if not comment_text:
|
||||
messages.error(request, _("Veuillez préciser les informations complémentaires demandées."))
|
||||
return redirect(redirect_target, observation_id=observation.id)
|
||||
|
||||
# Pour "à rediriger" avec choix d'un nouveau symptôme : mettre à jour symptôme/catégorie/thématique
|
||||
# Dans ce cas, le statut n'est PAS modifié et un commentaire automatique est ajouté.
|
||||
symptom_changed = False
|
||||
|
|
@ -985,7 +952,6 @@ def observation_change_status(request, observation_id):
|
|||
new_status = observation.status # conserver le statut actuel
|
||||
except Symptom.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Enregistrer un commentaire optionnel (pour tous les autres changements de statut)
|
||||
if not symptom_changed:
|
||||
comment_text = (request.POST.get('comment') or '').strip()
|
||||
|
|
@ -995,41 +961,15 @@ def observation_change_status(request, observation_id):
|
|||
author=request.user if request.user.is_authenticated else None,
|
||||
comment=comment_text,
|
||||
)
|
||||
|
||||
# Si passage en needs_info : publier aussi dans la discussion pour alerter l'auteur
|
||||
if new_status == 'needs_info':
|
||||
from notifications.services import get_or_create_observation_discussion, post_message
|
||||
discussion, _ = get_or_create_observation_discussion(observation)
|
||||
post_message(
|
||||
discussion=discussion,
|
||||
author=request.user,
|
||||
content=_("Demande d'informations complémentaires :\n%(comment)s") % {'comment': comment_text}
|
||||
)
|
||||
messages.success(request, _("La demande d'informations a été envoyée à l'observateur."))
|
||||
|
||||
# Si retour à to_process depuis needs_info :
|
||||
elif observation.status == 'needs_info' and new_status == 'to_process':
|
||||
res_content = (
|
||||
_("Compléments d'information fournis :\n%(comment)s") % {'comment': comment_text}
|
||||
if comment_text else
|
||||
_("Compléments d'information fournis. L'observation est à nouveau à traiter.")
|
||||
)
|
||||
from notifications.services import get_or_create_observation_discussion, post_message
|
||||
discussion, _ = get_or_create_observation_discussion(observation)
|
||||
post_message(
|
||||
discussion=discussion,
|
||||
author=request.user,
|
||||
content=res_content
|
||||
)
|
||||
messages.success(request, _("L'observation a été renvoyée pour traitement."))
|
||||
|
||||
# Archiver seulement depuis 'processed', 'not_relevant', 'to_redirect', 'duplicate' ou 'needs_info'
|
||||
if new_status == 'archived' and observation.status not in ('processed', 'not_relevant', 'to_redirect', 'duplicate', 'needs_info'):
|
||||
return HttpResponseForbidden("Seules les observations traitées, non relevantes, à rediriger, en attente d'infos ou doublons peuvent être archivées.")
|
||||
# Archiver seulement depuis 'processed', 'not_relevant', 'to_redirect' ou 'duplicate'
|
||||
if new_status == 'archived' and observation.status not in ('processed', 'not_relevant', 'to_redirect', 'duplicate'):
|
||||
return HttpResponseForbidden("Seules les observations traitées, non relevantes, à rediriger ou doublons peuvent être archivées.")
|
||||
observation.status = new_status
|
||||
observation.save()
|
||||
|
||||
# Rediriger vers la vue mobile ou desktop selon l'origine de la requête
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
is_mobile = '/mobile/' in referer
|
||||
if is_mobile:
|
||||
return redirect('mobile:observation_detail_mobile', observation_id=observation.id)
|
||||
return redirect('observations:observation_detail', observation_id=observation.id)
|
||||
|
|
@ -1185,7 +1125,6 @@ def observation_add(request, observation_id=None):
|
|||
observation.created_by = request.user
|
||||
|
||||
# Gestion du statut selon le bouton utilisé
|
||||
previous_status = observation_to_edit.status if observation_to_edit else None
|
||||
if 'save' in request.POST:
|
||||
observation.status = 'in_preparation'
|
||||
elif 'send' in request.POST:
|
||||
|
|
@ -1274,20 +1213,6 @@ def observation_add(request, observation_id=None):
|
|||
except json.JSONDecodeError:
|
||||
pass # Ignore invalid JSON
|
||||
|
||||
if observation_to_edit and previous_status == 'needs_info' and observation.status == 'to_process':
|
||||
ObservationComment.objects.create(
|
||||
observation=observation,
|
||||
author=request.user,
|
||||
comment=_("Observation modifiée et renvoyée par l'auteur."),
|
||||
)
|
||||
from notifications.services import get_or_create_observation_discussion, post_message
|
||||
discussion, _ = get_or_create_observation_discussion(observation)
|
||||
post_message(
|
||||
discussion=discussion,
|
||||
author=request.user,
|
||||
content=_("L'observation a été modifiée et renvoyée par l'auteur.")
|
||||
)
|
||||
|
||||
# After creation/update, redirect to detail
|
||||
messages.success(request, "Observation enregistrée avec succès.")
|
||||
|
||||
|
|
@ -2008,23 +1933,6 @@ def observation_add_message(request, observation_id):
|
|||
files = request.FILES.getlist('attachments') or None
|
||||
post_message(discussion=discussion, author=request.user, content=content, files=files)
|
||||
|
||||
# Si l'observation était en attente d'informations et que l'auteur répond,
|
||||
# basculer automatiquement l'observation à 'to_process'.
|
||||
if observation.status == 'needs_info' and request.user == observation.created_by:
|
||||
observation.status = 'to_process'
|
||||
observation.save(update_fields=['status'])
|
||||
ObservationComment.objects.create(
|
||||
observation=observation,
|
||||
author=request.user,
|
||||
comment=_("Réponse apportée dans la discussion : l'observation repasse à l'état 'À traiter'."),
|
||||
)
|
||||
messages.success(request, _("Votre réponse a été envoyée et l'observation a été renvoyée pour traitement."))
|
||||
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
if '/mobile/' in referer:
|
||||
return redirect(
|
||||
reverse('mobile:observation_detail_mobile', args=[observation_id]) + '#discussion'
|
||||
)
|
||||
return redirect(
|
||||
reverse('observations:observation_detail', args=[observation_id]) + '#discussion'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -969,7 +969,6 @@
|
|||
fr: {
|
||||
'in_preparation': 'En préparation',
|
||||
'to_process': 'À traiter',
|
||||
'needs_info': 'Informations demandées',
|
||||
'in_progress': 'En cours',
|
||||
'processed': 'Traité',
|
||||
'not_relevant': 'Non relevant',
|
||||
|
|
@ -980,7 +979,6 @@
|
|||
nl: {
|
||||
'in_preparation': 'In voorbereiding',
|
||||
'to_process': 'Te behandelen',
|
||||
'needs_info': 'Informatie gevraagd',
|
||||
'in_progress': 'In behandeling',
|
||||
'processed': 'Verwerkt',
|
||||
'not_relevant': 'Niet relevant',
|
||||
|
|
@ -993,7 +991,6 @@
|
|||
const STATUS_BADGES = {
|
||||
'in_preparation': 'bg-secondary',
|
||||
'to_process': 'bg-danger',
|
||||
'needs_info': 'bg-warning text-dark',
|
||||
'in_progress': 'bg-primary',
|
||||
'processed': 'bg-success',
|
||||
'not_relevant': 'bg-secondary',
|
||||
|
|
|
|||
Loading…
Reference in a new issue