feat: add cancel/restore functionality for controls with granular permissions and API support
This commit is contained in:
parent
10942c81af
commit
27056b61fa
10 changed files with 291 additions and 23 deletions
|
|
@ -7,6 +7,20 @@ from .models import (
|
||||||
)
|
)
|
||||||
from common.private_files.helpers import build_private_url
|
from common.private_files.helpers import build_private_url
|
||||||
|
|
||||||
|
@admin.action(description="Marquer comme annulé(s)")
|
||||||
|
def make_canceled(modeladmin, request, queryset):
|
||||||
|
for obj in queryset:
|
||||||
|
obj.status = 'canceled'
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Rétablir comme en cours")
|
||||||
|
def make_in_progress(modeladmin, request, queryset):
|
||||||
|
for obj in queryset:
|
||||||
|
obj.status = 'in_progress'
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Control)
|
@admin.register(Control)
|
||||||
class ControlAdmin(admin.ModelAdmin):
|
class ControlAdmin(admin.ModelAdmin):
|
||||||
list_display = ("control_code", "control_type", "status", "project", "phase", "control_author", "updated_at")
|
list_display = ("control_code", "control_type", "status", "project", "phase", "control_author", "updated_at")
|
||||||
|
|
@ -14,6 +28,7 @@ class ControlAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("control_code", "description", "location", "control_author__username")
|
search_fields = ("control_code", "description", "location", "control_author__username")
|
||||||
ordering = ("-updated_at",)
|
ordering = ("-updated_at",)
|
||||||
autocomplete_fields = ["project", "phase", "interventions", "control_author"]
|
autocomplete_fields = ["project", "phase", "interventions", "control_author"]
|
||||||
|
actions = [make_canceled, make_in_progress]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,18 @@ class Control(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.control_code
|
return self.control_code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_cancelled(self):
|
||||||
|
return self.status == 'canceled'
|
||||||
|
|
||||||
|
def can_cancel(self, user):
|
||||||
|
from .permissions import user_can_cancel_control
|
||||||
|
return user_can_cancel_control(user, self)
|
||||||
|
|
||||||
|
def can_edit(self, user):
|
||||||
|
from .permissions import user_can_change_control
|
||||||
|
return user_can_change_control(user, self)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""
|
"""
|
||||||
Returns the URL to access a specific project instance.
|
Returns the URL to access a specific project instance.
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,44 @@ def control_change_required(view_func):
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
|
|
||||||
|
|
||||||
|
def user_can_cancel_control(user, control):
|
||||||
|
"""
|
||||||
|
Vérifie si un utilisateur a le droit d'annuler ou rétablir un contrôle.
|
||||||
|
Seuls l'auteur du contrôle et les administrateurs ont ce droit.
|
||||||
|
"""
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Administrateur Django superuser
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Administrateur au sens configuration de rôle (admin)
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
if user_config and hasattr(user_config, 'has_role'):
|
||||||
|
if user_config.has_role('admin'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Auteur du contrôle
|
||||||
|
if control and control.control_author_id == user.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def control_cancel_required(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def _wrapped_view(request, control_id, *args, **kwargs):
|
||||||
|
control = get_object_or_404(Control, id=control_id)
|
||||||
|
|
||||||
|
if user_can_cancel_control(request.user, control):
|
||||||
|
return view_func(request, control_id, *args, **kwargs)
|
||||||
|
|
||||||
|
return HttpResponseForbidden(_("Vous n'êtes pas autorisé à annuler ou rétablir ce contrôle."))
|
||||||
|
|
||||||
|
return _wrapped_view
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Guards pour les fichiers privés (private_media)
|
# Guards pour les fichiers privés (private_media)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -2222,3 +2222,49 @@ function showImageModal(imageUrl) {
|
||||||
}];
|
}];
|
||||||
showRemarkCarouselModal(0);
|
showRemarkCarouselModal(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.handleToggleCancelControl = function(controlId, action) {
|
||||||
|
const defaultCancelMsg = "Êtes-vous sûr de vouloir annuler ce contrôle ?";
|
||||||
|
const defaultRestoreMsg = "Êtes-vous sûr de vouloir rétablir ce contrôle ?";
|
||||||
|
const confirmMsg = action === 'cancel'
|
||||||
|
? (typeof gettext === 'function' ? gettext(defaultCancelMsg) : defaultCancelMsg)
|
||||||
|
: (typeof gettext === 'function' ? gettext(defaultRestoreMsg) : defaultRestoreMsg);
|
||||||
|
if (!confirm(confirmMsg)) return;
|
||||||
|
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, 10) === 'csrftoken=') {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(10));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', action);
|
||||||
|
|
||||||
|
fetch(`/controls/api/${controlId}/toggle-cancel/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': cookieValue || (typeof csrftoken !== 'undefined' ? csrftoken : '')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const defaultErrMsg = "Une erreur est survenue.";
|
||||||
|
alert(data.error || (typeof gettext === 'function' ? gettext(defaultErrMsg) : defaultErrMsg));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
const defaultNetworkErrMsg = "Erreur de communication avec le serveur.";
|
||||||
|
alert(typeof gettext === 'function' ? gettext(defaultNetworkErrMsg) : defaultNetworkErrMsg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -15,23 +15,45 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if control.is_cancelled %}
|
||||||
|
<div class="alert alert-secondary py-2 px-3 small d-flex align-items-center mb-3">
|
||||||
|
<i class="bi bi-slash-circle me-2 fs-6"></i>
|
||||||
|
<div>
|
||||||
|
<strong>{% translate "Contrôle annulé" %}</strong> — {% translate "Ce contrôle a été annulé." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Informations principales -->
|
<!-- Informations principales -->
|
||||||
<!-- <div class="card shadow-sm"> -->
|
<!-- <div class="card shadow-sm"> -->
|
||||||
<div class="card-body position-relative">
|
<div class="card-body position-relative">
|
||||||
<!-- <h5 class="card-title ">{% translate 'Contrôle' %} {{ control.control_code }}</h5> -->
|
<!-- <h5 class="card-title ">{% translate 'Contrôle' %} {{ control.control_code }}</h5> -->
|
||||||
{% if can_change_control == True %}
|
<div class="position-absolute top-0 end-0 m-2 d-flex align-items-center gap-1">
|
||||||
<a id="control-edit-btn"
|
{% if can_cancel_control %}
|
||||||
href="{% url 'controls:change_control' control.id %}?v={{ request.GET.v }}"
|
{% if control.is_cancelled %}
|
||||||
class="btn btn-sm btn-light border position-absolute top-0 end-0 m-2"
|
<button type="button" class="btn btn-sm btn-outline-success fw-semibold" onclick="handleToggleCancelControl({{ control.id }}, 'restore')" title="{% translate 'Rétablir le contrôle' %}">
|
||||||
title="{% translate 'Modifier le template' %}"
|
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir" %}
|
||||||
onclick="showControlEditSpinner(event)">
|
</button>
|
||||||
<i id="control-edit-icon" class="bi bi-pencil"></i>
|
{% else %}
|
||||||
<div id="control-edit-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></div>
|
<button type="button" class="btn btn-sm btn-outline-danger fw-semibold" onclick="handleToggleCancelControl({{ control.id }}, 'cancel')" title="{% translate 'Annuler le contrôle' %}">
|
||||||
</a>
|
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler" %}
|
||||||
{% endif %}
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if can_change_control == True %}
|
||||||
|
<a id="control-edit-btn"
|
||||||
|
href="{% url 'controls:change_control' control.id %}?v={{ request.GET.v }}"
|
||||||
|
class="btn btn-sm btn-light border"
|
||||||
|
title="{% translate 'Modifier le contrôle' %}"
|
||||||
|
onclick="showControlEditSpinner(event)">
|
||||||
|
<i id="control-edit-icon" class="bi bi-pencil"></i>
|
||||||
|
<div id="control-edit-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="mb-1"><strong>{% translate 'Statut' %} :</strong>
|
<p class="mb-1"><strong>{% translate 'Statut' %} :</strong>
|
||||||
<span class="badge bg-info">{{ control.get_status_display }}</span>
|
<span class="badge {{ control.status|status_badge }}"><i class="bi {{ control.status|status_icon }} me-1"></i>{{ control.get_status_display }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mb-1"><strong>{% translate 'Type' %} :</strong>
|
<p class="mb-1"><strong>{% translate 'Type' %} :</strong>
|
||||||
|
|
|
||||||
|
|
@ -70,19 +70,43 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if control.is_cancelled %}
|
||||||
|
<div class="alert alert-secondary py-2 px-3 small d-flex align-items-center mb-3">
|
||||||
|
<i class="bi bi-slash-circle me-2 fs-6"></i>
|
||||||
|
<div>
|
||||||
|
<strong>{% translate "Contrôle annulé" %}</strong> — {% translate "Ce contrôle a été annulé." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Informations principales -->
|
<!-- Informations principales -->
|
||||||
<div class="card-body position-relative">
|
<div class="card-body position-relative">
|
||||||
<a id="control-edit-btn"
|
<div class="position-absolute top-0 end-0 m-2 d-flex align-items-center gap-1">
|
||||||
href="{% url 'controls:change_control' control.id %}?v=mobile"
|
{% if can_cancel_control %}
|
||||||
class="btn btn-sm btn-light border position-absolute top-0 end-0 m-2"
|
{% if control.is_cancelled %}
|
||||||
title="{% translate 'Modifier le template' %}"
|
<button type="button" class="btn btn-sm btn-outline-success fw-semibold" onclick="handleToggleCancelControl({{ control.id }}, 'restore')" title="{% translate 'Rétablir le contrôle' %}">
|
||||||
onclick="showControlEditSpinner(event)">
|
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir" %}
|
||||||
<i id="control-edit-icon" class="bi bi-pencil"></i>
|
</button>
|
||||||
<div id="control-edit-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></div>
|
{% else %}
|
||||||
</a>
|
<button type="button" class="btn btn-sm btn-outline-danger fw-semibold" onclick="handleToggleCancelControl({{ control.id }}, 'cancel')" title="{% translate 'Annuler le contrôle' %}">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if can_change_control == True %}
|
||||||
|
<a id="control-edit-btn"
|
||||||
|
href="{% url 'controls:change_control' control.id %}?v=mobile"
|
||||||
|
class="btn btn-sm btn-light border"
|
||||||
|
title="{% translate 'Modifier le contrôle' %}"
|
||||||
|
onclick="showControlEditSpinner(event)">
|
||||||
|
<i id="control-edit-icon" class="bi bi-pencil"></i>
|
||||||
|
<div id="control-edit-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="mb-1"><strong>{% translate 'Statut' %} :</strong>
|
<p class="mb-1"><strong>{% translate 'Statut' %} :</strong>
|
||||||
<span class="badge bg-info">{{ control.get_status_display }}</span>
|
<span class="badge {{ control.status|status_badge }}"><i class="bi {{ control.status|status_icon }} me-1"></i>{{ control.get_status_display }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mb-1"><strong>{% translate 'Type' %} :</strong>
|
<p class="mb-1"><strong>{% translate 'Type' %} :</strong>
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,12 @@ def private_file_url(instance, field_name="file"):
|
||||||
instance.pk,
|
instance.pk,
|
||||||
field_name,
|
field_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def can_cancel_control(control, user):
|
||||||
|
"""
|
||||||
|
Vérifie si un utilisateur a le droit d'annuler/rétablir un contrôle.
|
||||||
|
"""
|
||||||
|
from controls.permissions import user_can_cancel_control
|
||||||
|
return user_can_cancel_control(user, control)
|
||||||
|
|
@ -325,3 +325,61 @@ class ControlsIndexTests(TestCase):
|
||||||
res_json = response.json()
|
res_json = response.json()
|
||||||
self.assertEqual(res_json["status"], "error")
|
self.assertEqual(res_json["status"], "error")
|
||||||
self.assertIn("remark_form", res_json["errors"])
|
self.assertIn("remark_form", res_json["errors"])
|
||||||
|
|
||||||
|
def test_toggle_cancel_control_api(self):
|
||||||
|
self.client.login(username="internal_user", password="password123")
|
||||||
|
self.assertFalse(self.ctrl1.is_cancelled)
|
||||||
|
|
||||||
|
url = reverse("controls:toggle_cancel_control_api", kwargs={"control_id": self.ctrl1.id})
|
||||||
|
|
||||||
|
# Annuler le contrôle
|
||||||
|
resp = self.client.post(url, {"action": "cancel"})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
self.assertTrue(data["is_cancelled"])
|
||||||
|
self.assertEqual(data["status"], "canceled")
|
||||||
|
|
||||||
|
self.ctrl1.refresh_from_db()
|
||||||
|
self.assertTrue(self.ctrl1.is_cancelled)
|
||||||
|
self.assertEqual(self.ctrl1.status, "canceled")
|
||||||
|
|
||||||
|
# Rétablir le contrôle
|
||||||
|
resp_restore = self.client.post(url, {"action": "restore"})
|
||||||
|
self.assertEqual(resp_restore.status_code, 200)
|
||||||
|
data_restore = resp_restore.json()
|
||||||
|
self.assertTrue(data_restore["success"])
|
||||||
|
self.assertFalse(data_restore["is_cancelled"])
|
||||||
|
self.assertEqual(data_restore["status"], "in_progress")
|
||||||
|
|
||||||
|
self.ctrl1.refresh_from_db()
|
||||||
|
self.assertFalse(self.ctrl1.is_cancelled)
|
||||||
|
self.assertEqual(self.ctrl1.status, "in_progress")
|
||||||
|
|
||||||
|
def test_toggle_cancel_control_permissions(self):
|
||||||
|
url = reverse("controls:toggle_cancel_control_api", kwargs={"control_id": self.ctrl1.id})
|
||||||
|
|
||||||
|
# Un utilisateur non-auteur et non-admin ne peut pas annuler
|
||||||
|
self.client.login(username="other_user", password="password123")
|
||||||
|
resp = self.client.post(url, {"action": "cancel"})
|
||||||
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
self.ctrl1.refresh_from_db()
|
||||||
|
self.assertFalse(self.ctrl1.is_cancelled)
|
||||||
|
|
||||||
|
# Un administrateur peut annuler
|
||||||
|
self.client.login(username="admin_user", password="password123")
|
||||||
|
resp_admin = self.client.post(url, {"action": "cancel"})
|
||||||
|
self.assertEqual(resp_admin.status_code, 200)
|
||||||
|
self.ctrl1.refresh_from_db()
|
||||||
|
self.assertTrue(self.ctrl1.is_cancelled)
|
||||||
|
self.assertEqual(self.ctrl1.status, "canceled")
|
||||||
|
|
||||||
|
def test_control_model_cancel_properties(self):
|
||||||
|
self.assertFalse(self.ctrl1.is_cancelled)
|
||||||
|
self.assertTrue(self.ctrl1.can_cancel(self.internal_user))
|
||||||
|
self.assertTrue(self.ctrl1.can_cancel(self.admin_user))
|
||||||
|
self.assertFalse(self.ctrl1.can_cancel(self.other_user))
|
||||||
|
|
||||||
|
self.ctrl1.status = "canceled"
|
||||||
|
self.assertTrue(self.ctrl1.is_cancelled)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
||||||
path("offline/", views.controls_offline, name="controls_offline"),
|
path("offline/", views.controls_offline, name="controls_offline"),
|
||||||
path('<int:control_id>/', views.controls_detail, name='controls_detail'),
|
path('<int:control_id>/', views.controls_detail, name='controls_detail'),
|
||||||
path('<int:control_id>/change/', views.change_control, name='change_control'),
|
path('<int:control_id>/change/', views.change_control, name='change_control'),
|
||||||
|
path('api/<int:control_id>/toggle-cancel/', views.toggle_cancel_control_api, name='toggle_cancel_control_api'),
|
||||||
path('add/', views.controls_add, name='controls_add'),
|
path('add/', views.controls_add, name='controls_add'),
|
||||||
path('quick-create/', views.control_quick_create_from_intervention, name='control_quick_create_from_intervention'),
|
path('quick-create/', views.control_quick_create_from_intervention, name='control_quick_create_from_intervention'),
|
||||||
path("remark/<int:remark_id>/change/", views.change_remark, name="change_remark"),
|
path("remark/<int:remark_id>/change/", views.change_remark, name="change_remark"),
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,11 @@ from .models import (
|
||||||
CONTROL_STATUS_CHOICES, CONTROL_TYPE_CHOICES
|
CONTROL_STATUS_CHOICES, CONTROL_TYPE_CHOICES
|
||||||
)
|
)
|
||||||
from .forms import ControlForm, RemarkForm, ControlReferencePlanForm, ControlReportTemplateForm, ControlQuantityForm, QuantityFormSet
|
from .forms import ControlForm, RemarkForm, ControlReferencePlanForm, ControlReportTemplateForm, ControlQuantityForm, QuantityFormSet
|
||||||
from .permissions import control_view_required, control_add_required, user_can_add_control, control_change_required, user_can_change_control
|
from .permissions import (
|
||||||
|
control_view_required, control_add_required, user_can_add_control,
|
||||||
|
control_change_required, user_can_change_control,
|
||||||
|
control_cancel_required, user_can_cancel_control
|
||||||
|
)
|
||||||
|
|
||||||
from projects.models import Project, ProjectUserAccess, ProjectPhase
|
from projects.models import Project, ProjectUserAccess, ProjectPhase
|
||||||
from projects.permissions import filter_viewable_projects_for_user
|
from projects.permissions import filter_viewable_projects_for_user
|
||||||
|
|
@ -1203,6 +1207,7 @@ def controls_detail(request, control_id):
|
||||||
upload_plan_form = ControlReferencePlanForm()
|
upload_plan_form = ControlReferencePlanForm()
|
||||||
report_template = control.templates.first()
|
report_template = control.templates.first()
|
||||||
can_change_control = user_can_change_control(request.user, control)
|
can_change_control = user_can_change_control(request.user, control)
|
||||||
|
can_cancel_control = user_can_cancel_control(request.user, control)
|
||||||
|
|
||||||
# Préparer les données des plans pour JavaScript
|
# Préparer les données des plans pour JavaScript
|
||||||
plans_data = [
|
plans_data = [
|
||||||
|
|
@ -1259,6 +1264,7 @@ def controls_detail(request, control_id):
|
||||||
"remarks_data": remarks_data,
|
"remarks_data": remarks_data,
|
||||||
"report_template": report_template,
|
"report_template": report_template,
|
||||||
"can_change_control": can_change_control,
|
"can_change_control": can_change_control,
|
||||||
|
"can_cancel_control": can_cancel_control,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, template_name, context)
|
return render(request, template_name, context)
|
||||||
|
|
@ -1597,6 +1603,43 @@ def change_template(request, template_id):
|
||||||
return render(request, template_name, {'form': form, 'template': template})
|
return render(request, template_name, {'form': form, 'template': template})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def toggle_cancel_control_api(request, control_id):
|
||||||
|
"""
|
||||||
|
API endpoint pour annuler ou rétablir un contrôle.
|
||||||
|
Seuls l'auteur et les administrateurs ont le droit d'annuler/rétablir un contrôle.
|
||||||
|
"""
|
||||||
|
control = get_object_or_404(Control, pk=control_id)
|
||||||
|
|
||||||
|
if not user_can_cancel_control(request.user, control):
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(_("Permission refusée. Seuls l'auteur du contrôle et les administrateurs peuvent l'annuler ou le rétablir."))
|
||||||
|
}, status=403)
|
||||||
|
|
||||||
|
action = request.POST.get('action') # 'cancel', 'restore', ou toggle automatique si non spécifié
|
||||||
|
if action == 'cancel' or (action is None and not control.is_cancelled):
|
||||||
|
control.status = 'canceled'
|
||||||
|
elif action == 'restore' or (action is None and control.is_cancelled):
|
||||||
|
target_status = request.POST.get('status', 'in_progress')
|
||||||
|
control.status = target_status if target_status != 'canceled' else 'in_progress'
|
||||||
|
|
||||||
|
control.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'control_id': control.id,
|
||||||
|
'is_cancelled': control.is_cancelled,
|
||||||
|
'status': control.status,
|
||||||
|
'status_display': str(control.get_status_display()),
|
||||||
|
'status_badge': _CONTROL_STATUS_BADGE_MAP.get(control.status, 'secondary'),
|
||||||
|
'status_color': _CONTROL_STATUS_COLOR_MAP.get(control.status, '#6c757d'),
|
||||||
|
'message': str(_("Le contrôle a été annulé avec succès.")) if control.is_cancelled else str(_("Le contrôle a été rétabli avec succès."))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue