feat(interventions): aligner les permissions des notes par statut et historiser le mobile
- Autoriser les modifications/suppressions de notes jusqu'au statut 'traité' pour les externes - Autoriser les modifications/suppressions de notes jusqu'au statut 'validé' pour les internes - Supprimer la contrainte horaire de 1h sur la modification des notes - Brancher l'historisation de la chronologie sur la création et modification de notes mobile - Tracer l'upload des photos de formulaires mobiles dans la chronologie - Ajouter les tests unitaires et d'intégration correspondants
This commit is contained in:
parent
7911369740
commit
205a05cb5b
6 changed files with 282 additions and 38 deletions
|
|
@ -1471,10 +1471,14 @@ def get_allowed_transitions(request, intervention):
|
||||||
|
|
||||||
def can_edit_note(user, user_config, note):
|
def can_edit_note(user, user_config, note):
|
||||||
"""
|
"""
|
||||||
Règle:
|
Règles de modification et suppression d'une note :
|
||||||
- Admin: peut tout modifier.
|
- Admin : peut tout modifier et supprimer sans restriction.
|
||||||
- Thématique 'structures': l'auteur OU rôle manager/external_manager (pas de contrainte horaire).
|
- Externes (non is_intern) :
|
||||||
- Sinon: uniquement l'auteur ET la note a été créée il y a < 1h.
|
* Autorisés jusqu'au statut 'traité' (status_order <= STATUS_ORDERS['processed'], soit <= 31).
|
||||||
|
* Auteur de la note OU rôle 'external_manager' (hors notes rédigées par des internes).
|
||||||
|
- Internes (is_intern) :
|
||||||
|
* Autorisés jusqu'au statut 'validé' (status_order <= STATUS_ORDERS['validated'], soit <= 51).
|
||||||
|
* Auteur de la note OU rôle 'manager'.
|
||||||
"""
|
"""
|
||||||
if not user.is_authenticated or user_config is None:
|
if not user.is_authenticated or user_config is None:
|
||||||
return False
|
return False
|
||||||
|
|
@ -1483,29 +1487,42 @@ def can_edit_note(user, user_config, note):
|
||||||
if user_config.roles.filter(name='admin').exists():
|
if user_config.roles.filter(name='admin').exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 2) Thématique 'structures'
|
|
||||||
intervention = getattr(note, 'intervention', None)
|
intervention = getattr(note, 'intervention', None)
|
||||||
thematic_code = getattr(getattr(intervention, 'thematic', None), 'code', None)
|
if intervention is None:
|
||||||
if thematic_code == 'structures':
|
return False
|
||||||
# Auteur OU rôle manager/external_manager
|
|
||||||
|
status_order = getattr(intervention, 'status_order', None)
|
||||||
|
if status_order is None:
|
||||||
|
status_order = STATUS_ORDERS.get(intervention.status, 0)
|
||||||
|
|
||||||
|
# 2) Utilisateurs internes (commune / régie / gestionnaires)
|
||||||
|
if user_config.is_intern:
|
||||||
|
# Autorisé jusqu'au statut 'validé' (inclus)
|
||||||
|
if status_order > STATUS_ORDERS.get('validated', 51):
|
||||||
|
return False
|
||||||
|
# Auteur OU rôle manager
|
||||||
if note.note_author_id == user.id:
|
if note.note_author_id == user.id:
|
||||||
return True
|
return True
|
||||||
if user_config.roles.filter(name__in=['manager', 'external_manager']).exists():
|
if user_config.roles.filter(name='manager').exists():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 3) Auteur + moins d'une heure (autres thématiques)
|
# 3) Utilisateurs externes (prestataires / soumissionnaires)
|
||||||
if note.note_author_id != user.id:
|
# Autorisé jusqu'au statut 'traité' (inclus)
|
||||||
|
if status_order > STATUS_ORDERS.get('processed', 31):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
created_at = getattr(note, 'note_time', None) or getattr(note, 'created_at', None)
|
# Auteur
|
||||||
if created_at is None:
|
if note.note_author_id == user.id:
|
||||||
return False
|
return True
|
||||||
|
|
||||||
if timezone.is_naive(created_at):
|
# Rôle external_manager (hors notes rédigées par des agents internes)
|
||||||
created_at = timezone.make_aware(created_at, timezone.get_current_timezone())
|
if user_config.roles.filter(name='external_manager').exists():
|
||||||
|
if note.note_author and hasattr(note.note_author, 'config') and getattr(note.note_author.config, 'is_intern', False):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
return (timezone.now() - created_at) <= timedelta(hours=1)
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
{% for note in notes %}
|
{% for note in notes %}
|
||||||
<div id="note-container-{{ note.id }}"
|
<div id="note-container-{{ note.id }}"
|
||||||
class="note-item py-3 {% if not forloop.last %}border-bottom{% endif %} {% if note.id in editable_note_ids and can_edit_notes %}editable-note{% endif %}"
|
class="note-item py-3 {% if not forloop.last %}border-bottom{% endif %} {% if note.id in editable_note_ids %}editable-note{% endif %}"
|
||||||
data-note-id="{{ note.id }}"
|
data-note-id="{{ note.id }}"
|
||||||
data-note-content="{{ note.content|urlencode }}"
|
data-note-content="{{ note.content|urlencode }}"
|
||||||
data-note-author-id="{{ note.note_author_id }}"
|
data-note-author-id="{{ note.note_author_id }}"
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-0 note-text">{{ note.content|safe|linebreaksbr }}</p>
|
<p class="mb-0 note-text">{{ note.content|safe|linebreaksbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if note.id in editable_note_ids and can_edit_notes %}
|
{% if note.id in editable_note_ids %}
|
||||||
<div class="note-actions ms-auto">
|
<div class="note-actions ms-auto">
|
||||||
<button type="button" class="btn btn-link text-danger p-0 delete-note-btn" data-note-id="{{ note.id }}" aria-label="{% translate "Supprimer la note" %}" style="display: none;">
|
<button type="button" class="btn btn-link text-danger p-0 delete-note-btn" data-note-id="{{ note.id }}" aria-label="{% translate "Supprimer la note" %}" style="display: none;">
|
||||||
<i class="bi bi-trash fs-6"></i>
|
<i class="bi bi-trash fs-6"></i>
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit mode (shown only in CMS edit mode for editable notes) -->
|
<!-- Edit mode (shown only in CMS edit mode for editable notes) -->
|
||||||
{% if note.id in editable_note_ids and can_edit_notes %}
|
{% if note.id in editable_note_ids %}
|
||||||
<div class="note-edit" style="display: none;">
|
<div class="note-edit" style="display: none;">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small fw-bold">{% translate "Type de note" %}</label>
|
<label class="form-label small fw-bold">{% translate "Type de note" %}</label>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
<div class="notes-list {% if block.documents %}border-top pt-3{% endif %}">
|
<div class="notes-list {% if block.documents %}border-top pt-3{% endif %}">
|
||||||
{% for note in block.notes %}
|
{% for note in block.notes %}
|
||||||
<div id="note-container-{{ note.id }}"
|
<div id="note-container-{{ note.id }}"
|
||||||
class="note-item py-2 {% if not forloop.last %}border-bottom{% endif %} {% if note.id in editable_note_ids and can_edit_notes %}editable-note{% endif %}"
|
class="note-item py-2 {% if not forloop.last %}border-bottom{% endif %} {% if note.id in editable_note_ids %}editable-note{% endif %}"
|
||||||
data-note-id="{{ note.id }}"
|
data-note-id="{{ note.id }}"
|
||||||
data-note-content="{{ note.content|urlencode }}"
|
data-note-content="{{ note.content|urlencode }}"
|
||||||
data-note-author-id="{{ note.note_author_id }}"
|
data-note-author-id="{{ note.note_author_id }}"
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-0">{{ note.content|linebreaksbr }}</p>
|
<p class="mb-0">{{ note.content|linebreaksbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if note.id in editable_note_ids and can_edit_notes %}
|
{% if note.id in editable_note_ids %}
|
||||||
<div class="note-actions cms-note-actions" style="display: none;">
|
<div class="note-actions cms-note-actions" style="display: none;">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary btn-edit-note me-1" title="{% translate 'Modifier la note' %}">
|
<button type="button" class="btn btn-sm btn-outline-primary btn-edit-note me-1" title="{% translate 'Modifier la note' %}">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit mode (shown only in CMS edit mode for editable notes) -->
|
<!-- Edit mode (shown only in CMS edit mode for editable notes) -->
|
||||||
{% if note.id in editable_note_ids and can_edit_notes %}
|
{% if note.id in editable_note_ids %}
|
||||||
<div class="note-edit" style="display: none;">
|
<div class="note-edit" style="display: none;">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small fw-bold">{% translate "Type de note" %}</label>
|
<label class="form-label small fw-bold">{% translate "Type de note" %}</label>
|
||||||
|
|
|
||||||
|
|
@ -727,7 +727,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-0 small">{{ note.content|safe|linebreaksbr }}</p>
|
<p class="mb-0 small">{{ note.content|safe|linebreaksbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if note.id in editable_note_ids and can_edit_notes %}
|
{% if note.id in editable_note_ids %}
|
||||||
<div class="note-actions d-flex flex-column align-items-center gap-2 ms-auto">
|
<div class="note-actions d-flex flex-column align-items-center gap-2 ms-auto">
|
||||||
<button type="button" class="btn btn-link text-primary p-0 edit-note-btn"
|
<button type="button" class="btn btn-link text-primary p-0 edit-note-btn"
|
||||||
data-note-id="{{ note.id }}" aria-label="{% translate "Modifier la note" %}">
|
data-note-id="{{ note.id }}" aria-label="{% translate "Modifier la note" %}">
|
||||||
|
|
|
||||||
|
|
@ -519,5 +519,192 @@ class TimelineHistoryTests(TestCase):
|
||||||
self.assertContains(response, 'history-status-circle')
|
self.assertContains(response, 'history-status-circle')
|
||||||
self.assertContains(response, 'history-status-transition')
|
self.assertContains(response, 'history-status-transition')
|
||||||
|
|
||||||
|
def test_can_edit_note_status_flexibility_for_internals_and_externals(self):
|
||||||
|
"""Test note edit/delete flexibility:
|
||||||
|
- Externals can edit/delete up to 'processed' (status_order <= 31)
|
||||||
|
- Internals can edit/delete up to 'validated' (status_order <= 51)
|
||||||
|
- Externals cannot edit internal notes
|
||||||
|
- Admin can always edit
|
||||||
|
"""
|
||||||
|
from interventions.permissions import can_edit_note
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
ext_user = User.objects.create_user(username='ext_author', password='pwd')
|
||||||
|
ext_cfg = UserConfig.objects.create(user=ext_user, is_intern=False)
|
||||||
|
role_tech, _ = Role.objects.get_or_create(name='technician')
|
||||||
|
ext_cfg.roles.add(role_tech)
|
||||||
|
|
||||||
|
int_user = self.user # is_intern=True, admin
|
||||||
|
int_agent = User.objects.create_user(username='int_agent', password='pwd')
|
||||||
|
int_cfg = UserConfig.objects.create(user=int_agent, is_intern=True)
|
||||||
|
|
||||||
|
int_manager = User.objects.create_user(username='int_manager', password='pwd')
|
||||||
|
mgr_cfg = UserConfig.objects.create(user=int_manager, is_intern=True)
|
||||||
|
role_mgr, _ = Role.objects.get_or_create(name='manager')
|
||||||
|
mgr_cfg.roles.add(role_mgr)
|
||||||
|
|
||||||
|
# Note créée par l'externe
|
||||||
|
ext_note = InterventionNote.objects.create(
|
||||||
|
intervention=self.intervention,
|
||||||
|
content='Note de l\'externe',
|
||||||
|
note_type='comment',
|
||||||
|
note_author=ext_user,
|
||||||
|
note_time=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note créée par l'interne
|
||||||
|
int_note = InterventionNote.objects.create(
|
||||||
|
intervention=self.intervention,
|
||||||
|
content='Note de l\'interne',
|
||||||
|
note_type='comment',
|
||||||
|
note_author=int_agent,
|
||||||
|
note_time=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Statut initial (in_preparation, order=1 <= 31)
|
||||||
|
self.intervention.status = 'in_preparation'
|
||||||
|
self.intervention.status_order = 1
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# L'externe peut modifier sa propre note
|
||||||
|
self.assertTrue(can_edit_note(ext_user, ext_cfg, ext_note))
|
||||||
|
# L'externe NE PEUT PAS modifier la note de l'agent interne
|
||||||
|
self.assertFalse(can_edit_note(ext_user, ext_cfg, int_note))
|
||||||
|
# L'agent interne peut modifier sa propre note
|
||||||
|
self.assertTrue(can_edit_note(int_agent, int_cfg, int_note))
|
||||||
|
# Le manager interne peut modifier les notes
|
||||||
|
self.assertTrue(can_edit_note(int_manager, mgr_cfg, ext_note))
|
||||||
|
|
||||||
|
# 2. Statut 'processed' (Traité, order=31)
|
||||||
|
self.intervention.status = 'processed'
|
||||||
|
self.intervention.status_order = 31
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# Jusqu'à 'traité', l'externe peut toujours modifier sa note
|
||||||
|
self.assertTrue(can_edit_note(ext_user, ext_cfg, ext_note))
|
||||||
|
# L'interne peut modifier sa note
|
||||||
|
self.assertTrue(can_edit_note(int_agent, int_cfg, int_note))
|
||||||
|
|
||||||
|
# 3. Statut 'to_be_corrected' (A corriger, order=41 > 31)
|
||||||
|
self.intervention.status = 'to_be_corrected'
|
||||||
|
self.intervention.status_order = 41
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# Après 'traité', l'externe NE PEUT PLUS modifier sa note
|
||||||
|
self.assertFalse(can_edit_note(ext_user, ext_cfg, ext_note))
|
||||||
|
# Mais l'interne PEUT TOUJOURS modifier sa note (car <= 51)
|
||||||
|
self.assertTrue(can_edit_note(int_agent, int_cfg, int_note))
|
||||||
|
|
||||||
|
# 4. Statut 'validated' (Validé, order=51)
|
||||||
|
self.intervention.status = 'validated'
|
||||||
|
self.intervention.status_order = 51
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# L'externe ne peut pas
|
||||||
|
self.assertFalse(can_edit_note(ext_user, ext_cfg, ext_note))
|
||||||
|
# Jusqu'à 'validé', l'interne peut toujours modifier
|
||||||
|
self.assertTrue(can_edit_note(int_agent, int_cfg, int_note))
|
||||||
|
self.assertTrue(can_edit_note(int_manager, mgr_cfg, int_note))
|
||||||
|
|
||||||
|
# 5. Statut 'closed' (Finalisé, order=71 > 51)
|
||||||
|
self.intervention.status = 'closed'
|
||||||
|
self.intervention.status_order = 71
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# Ni l'externe ni l'interne ne peuvent modifier
|
||||||
|
self.assertFalse(can_edit_note(ext_user, ext_cfg, ext_note))
|
||||||
|
self.assertFalse(can_edit_note(int_agent, int_cfg, int_note))
|
||||||
|
self.assertFalse(can_edit_note(int_manager, mgr_cfg, int_note))
|
||||||
|
# Sauf l'admin
|
||||||
|
self.assertTrue(can_edit_note(int_user, self.user_config, int_note))
|
||||||
|
|
||||||
|
# 6. Test direct des endpoints AJAX de modification et suppression
|
||||||
|
# Au statut 'processed' (31) : l'externe peut supprimer sa note
|
||||||
|
self.intervention.status = 'processed'
|
||||||
|
self.intervention.status_order = 31
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
self.client.login(username='ext_author', password='pwd')
|
||||||
|
del_url = reverse('interventions:intervention_delete_note', args=[self.intervention.id, ext_note.id])
|
||||||
|
res = self.client.post(del_url)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertFalse(InterventionNote.objects.filter(id=ext_note.id).exists())
|
||||||
|
|
||||||
|
# Création d'une nouvelle note externe
|
||||||
|
new_ext_note = InterventionNote.objects.create(
|
||||||
|
intervention=self.intervention,
|
||||||
|
content='Nouvelle note externe',
|
||||||
|
note_type='comment',
|
||||||
|
note_author=ext_user,
|
||||||
|
note_time=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Au statut 'to_be_corrected' (41 > 31) : l'externe ne peut plus modifier ni supprimer
|
||||||
|
self.intervention.status = 'to_be_corrected'
|
||||||
|
self.intervention.status_order = 41
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
upd_url = reverse('interventions:intervention_update_note', args=[self.intervention.id, new_ext_note.id])
|
||||||
|
res = self.client.post(upd_url, {'content': 'Modification interdite', 'note_type': 'comment'})
|
||||||
|
self.assertEqual(res.status_code, 403)
|
||||||
|
|
||||||
|
del_url = reverse('interventions:intervention_delete_note', args=[self.intervention.id, new_ext_note.id])
|
||||||
|
res = self.client.post(del_url)
|
||||||
|
self.assertEqual(res.status_code, 403)
|
||||||
|
self.assertTrue(InterventionNote.objects.filter(id=new_ext_note.id).exists())
|
||||||
|
|
||||||
|
# Au statut 'validated' (51) : l'interne peut toujours modifier sa note
|
||||||
|
self.intervention.status = 'validated'
|
||||||
|
self.intervention.status_order = 51
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
self.client.login(username='int_agent', password='pwd')
|
||||||
|
upd_url = reverse('interventions:intervention_update_note', args=[self.intervention.id, int_note.id])
|
||||||
|
res = self.client.post(upd_url, {'content': 'Modification autorisée interne', 'note_type': 'comment'})
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
int_note.refresh_from_db()
|
||||||
|
self.assertEqual(int_note.content, 'Modification autorisée interne')
|
||||||
|
|
||||||
|
def test_mobile_note_events_recorded_in_timeline(self):
|
||||||
|
"""Test that notes saved or updated via mobile endpoints create timeline events."""
|
||||||
|
self.client.login(username='agent_test', password='secret_password')
|
||||||
|
|
||||||
|
# 1. Création d'une note via save_intervention_mobile_ajax
|
||||||
|
url = reverse('interventions:save_operations_ajax', args=[self.intervention.id])
|
||||||
|
# Note: on utilise la vue mobile AJAX save_intervention_mobile_ajax
|
||||||
|
# Vérifions son url
|
||||||
|
mobile_ajax_url = f"/interventions/{self.intervention.id}/mobile/"
|
||||||
|
post_data = {
|
||||||
|
'content': 'Note créée depuis mobile',
|
||||||
|
'note_type': 'comment',
|
||||||
|
'note_id': '',
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/interventions/{self.intervention.id}/mobile/", post_data)
|
||||||
|
# Vérifier qu'un événement note a été consigné
|
||||||
|
note_event = self.intervention.events.filter(event_type='note').last()
|
||||||
|
self.assertIsNotNone(note_event)
|
||||||
|
self.assertTrue('Note ajoutée' in note_event.event_description or 'Note added' in note_event.event_description)
|
||||||
|
self.assertEqual(note_event.field_changes.get('content', {}).get('new'), 'Note créée depuis mobile')
|
||||||
|
|
||||||
|
# 2. Modification de la note créée via save_intervention_mobile_ajax
|
||||||
|
created_note = InterventionNote.objects.filter(content='Note créée depuis mobile').first()
|
||||||
|
self.assertIsNotNone(created_note)
|
||||||
|
update_data = {
|
||||||
|
'content': 'Note mise à jour depuis mobile',
|
||||||
|
'note_type': 'comment',
|
||||||
|
'note_id': str(created_note.id),
|
||||||
|
}
|
||||||
|
self.client.post(f"/interventions/{self.intervention.id}/mobile/", update_data)
|
||||||
|
update_event = self.intervention.events.filter(event_type='note').last()
|
||||||
|
self.assertIsNotNone(update_event)
|
||||||
|
self.assertTrue('Note modifiée' in update_event.event_description or 'Note updated' in update_event.event_description)
|
||||||
|
self.assertEqual(update_event.field_changes.get('content', {}).get('old'), 'Note créée depuis mobile')
|
||||||
|
self.assertEqual(update_event.field_changes.get('content', {}).get('new'), 'Note mise à jour depuis mobile')
|
||||||
|
|
||||||
|
# 3. Upload photo depuis intervention_detail_mobile POST
|
||||||
|
photo_file = SimpleUploadedFile("test_mobile_photo.jpg", b"image_data", content_type="image/jpeg")
|
||||||
|
detail_url = reverse('interventions:intervention_detail_mobile', args=[self.intervention.id])
|
||||||
|
self.client.post(detail_url, {'photos': [photo_file]})
|
||||||
|
doc_event = self.intervention.events.filter(event_type='document').last()
|
||||||
|
self.assertIsNotNone(doc_event)
|
||||||
|
self.assertTrue('Document ajouté' in doc_event.event_description or 'Document added' in doc_event.event_description)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ from dateutil.relativedelta import relativedelta
|
||||||
from common.private_files.helpers import build_private_url
|
from common.private_files.helpers import build_private_url
|
||||||
from common.utils import get_short_name
|
from common.utils import get_short_name
|
||||||
|
|
||||||
from interventions.services.timeline_service import record_document_event
|
from interventions.services.timeline_service import record_document_event, record_note_event
|
||||||
from interventions import views
|
from interventions import views
|
||||||
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
||||||
InterventionLink,
|
InterventionLink,
|
||||||
|
|
@ -491,12 +491,23 @@ def intervention_detail_mobile(request, intervention_id):
|
||||||
if note_id:
|
if note_id:
|
||||||
try:
|
try:
|
||||||
note = InterventionNote.objects.get(pk=note_id, intervention=intervention)
|
note = InterventionNote.objects.get(pk=note_id, intervention=intervention)
|
||||||
# Only allow updating if user is the author
|
user_cfg = getattr(request.user, 'config', None)
|
||||||
if note.note_author == request.user:
|
if can_edit_note(request.user, user_cfg, note):
|
||||||
|
old_note_type = note.note_type
|
||||||
|
old_content = note.content
|
||||||
note.content = cleaned_data['content']
|
note.content = cleaned_data['content']
|
||||||
note.note_type = cleaned_data['note_type']
|
note.note_type = cleaned_data['note_type']
|
||||||
note.updated_by = request.user
|
note.updated_by = request.user
|
||||||
note.save()
|
note.save()
|
||||||
|
record_note_event(
|
||||||
|
intervention=intervention,
|
||||||
|
user=request.user,
|
||||||
|
action='update',
|
||||||
|
note_type=note.note_type,
|
||||||
|
content=note.content,
|
||||||
|
old_note_type=old_note_type,
|
||||||
|
old_content=old_content,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
messages.error(request, _("Vous n'êtes pas autorisé à modifier cette note"))
|
messages.error(request, _("Vous n'êtes pas autorisé à modifier cette note"))
|
||||||
note = None
|
note = None
|
||||||
|
|
@ -506,23 +517,40 @@ def intervention_detail_mobile(request, intervention_id):
|
||||||
|
|
||||||
# Create new note if no existing note was found/updated
|
# Create new note if no existing note was found/updated
|
||||||
if not note:
|
if not note:
|
||||||
InterventionNote.objects.create(
|
new_note = InterventionNote.objects.create(
|
||||||
intervention=intervention,
|
intervention=intervention,
|
||||||
content=cleaned_data['content'],
|
content=cleaned_data['content'],
|
||||||
note_type=cleaned_data['note_type'],
|
note_type=cleaned_data['note_type'],
|
||||||
note_author=request.user,
|
note_author=request.user,
|
||||||
note_time=now()
|
note_time=now()
|
||||||
)
|
)
|
||||||
|
record_note_event(
|
||||||
|
intervention=intervention,
|
||||||
|
user=request.user,
|
||||||
|
action='create',
|
||||||
|
note_type=new_note.note_type,
|
||||||
|
content=new_note.content,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
messages.error(request, _("Erreur lors de l'enregistrement de la note."))
|
messages.error(request, _("Erreur lors de l'enregistrement de la note."))
|
||||||
|
|
||||||
# 2. Upload de documents (photos)
|
# 2. Upload de documents (photos)
|
||||||
|
uploaded_photos = []
|
||||||
for file in request.FILES.getlist("photos"):
|
for file in request.FILES.getlist("photos"):
|
||||||
InterventionDocument.objects.create(
|
doc = InterventionDocument.objects.create(
|
||||||
intervention=intervention,
|
intervention=intervention,
|
||||||
file=file,
|
file=file,
|
||||||
uploaded_by=request.user
|
uploaded_by=request.user
|
||||||
)
|
)
|
||||||
|
filename = getattr(file, 'name', '') or getattr(doc, 'filename', None) or str(doc.id)
|
||||||
|
uploaded_photos.append(filename)
|
||||||
|
if uploaded_photos:
|
||||||
|
record_document_event(
|
||||||
|
intervention=intervention,
|
||||||
|
user=request.user,
|
||||||
|
action='upload',
|
||||||
|
filenames=uploaded_photos,
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Traitement des opérations
|
# 3. Traitement des opérations
|
||||||
for operation in intervention.operations.all():
|
for operation in intervention.operations.all():
|
||||||
|
|
@ -783,13 +811,7 @@ def intervention_detail_mobile(request, intervention_id):
|
||||||
.annotate(type_priority_order=note_priority_case)
|
.annotate(type_priority_order=note_priority_case)
|
||||||
.order_by('type_priority_order', 'note_time')
|
.order_by('type_priority_order', 'note_time')
|
||||||
)
|
)
|
||||||
if is_admin:
|
editable_note_ids = {n.id for n in notes if can_edit_note(request.user, user_config, n)}
|
||||||
editable_note_ids = set(notes.values_list('id', flat=True))
|
|
||||||
else:
|
|
||||||
editable_note_ids = set(
|
|
||||||
notes.filter(note_author=request.user, note_time__gte=one_hour_ago)
|
|
||||||
.values_list('id', flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
documents_qs = intervention.documents.all()
|
documents_qs = intervention.documents.all()
|
||||||
|
|
@ -1948,12 +1970,23 @@ def save_intervention_mobile_ajax(request, intervention_id):
|
||||||
if note_id:
|
if note_id:
|
||||||
try:
|
try:
|
||||||
note = InterventionNote.objects.get(pk=note_id, intervention=intervention)
|
note = InterventionNote.objects.get(pk=note_id, intervention=intervention)
|
||||||
# Only allow updating if user is the author
|
user_cfg = getattr(request.user, 'config', None)
|
||||||
if note.note_author == request.user:
|
if can_edit_note(request.user, user_cfg, note):
|
||||||
|
old_note_type = note.note_type
|
||||||
|
old_content = note.content
|
||||||
note.content = note_content
|
note.content = note_content
|
||||||
note.note_type = note_type or note.note_type
|
note.note_type = note_type or note.note_type
|
||||||
note.updated_by = request.user
|
note.updated_by = request.user
|
||||||
note.save()
|
note.save()
|
||||||
|
record_note_event(
|
||||||
|
intervention=intervention,
|
||||||
|
user=request.user,
|
||||||
|
action='update',
|
||||||
|
note_type=note.note_type,
|
||||||
|
content=note.content,
|
||||||
|
old_note_type=old_note_type,
|
||||||
|
old_content=old_content,
|
||||||
|
)
|
||||||
result['note_saved'] = True
|
result['note_saved'] = True
|
||||||
result['note_updated'] = True
|
result['note_updated'] = True
|
||||||
else:
|
else:
|
||||||
|
|
@ -1971,6 +2004,13 @@ def save_intervention_mobile_ajax(request, intervention_id):
|
||||||
note_author=request.user,
|
note_author=request.user,
|
||||||
note_time=now()
|
note_time=now()
|
||||||
)
|
)
|
||||||
|
record_note_event(
|
||||||
|
intervention=intervention,
|
||||||
|
user=request.user,
|
||||||
|
action='create',
|
||||||
|
note_type=note.note_type,
|
||||||
|
content=note.content,
|
||||||
|
)
|
||||||
result['note_saved'] = True
|
result['note_saved'] = True
|
||||||
result['note_updated'] = False
|
result['note_updated'] = False
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue