feat: add support for dynamic intervention subtype filtering and validation
This commit is contained in:
parent
396d26414c
commit
368a167b9c
9 changed files with 387 additions and 6 deletions
|
|
@ -471,7 +471,7 @@ def get_allowed_update_fields(user, intervention):
|
||||||
|
|
||||||
# Type d'intervention, Osiris, PMO, Assets
|
# Type d'intervention, Osiris, PMO, Assets
|
||||||
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
|
||||||
allowed_fields.update(['type', 'osiris', 'pmo_number'])
|
allowed_fields.update(['type', 'sub_type', 'osiris', 'pmo_number'])
|
||||||
|
|
||||||
if intervention.status_order < STATUS_ORDERS.get('validated'):
|
if intervention.status_order < STATUS_ORDERS.get('validated'):
|
||||||
allowed_fields.add('category')
|
allowed_fields.add('category')
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
let isEditMode = false;
|
let isEditMode = false;
|
||||||
let originalValues = {};
|
let originalValues = {};
|
||||||
let originalContractOptions = null; // Store original contract options for cancel
|
let originalContractOptions = null; // Store original contract options for cancel
|
||||||
|
let originalSubtypeOptions = null; // Store original subtype options for cancel
|
||||||
|
let originalSubtypeVisible = null; // Store original subtype visibility for cancel
|
||||||
let isSavingInProgress = false;
|
let isSavingInProgress = false;
|
||||||
let isAutoSavingAndNavigating = false;
|
let isAutoSavingAndNavigating = false;
|
||||||
let isSubmittingAutoSavedForm = false;
|
let isSubmittingAutoSavedForm = false;
|
||||||
|
|
@ -29,6 +31,67 @@
|
||||||
|
|
||||||
// Setup dynamic contract loading when provider changes
|
// Setup dynamic contract loading when provider changes
|
||||||
setupProviderContractLink();
|
setupProviderContractLink();
|
||||||
|
// Setup dynamic subtype loading/visibility when type changes
|
||||||
|
setupTypeSubtypeLink();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the dynamic link between type and subtype selects.
|
||||||
|
* When the type changes, filter the subtype options and show/hide the subtype field.
|
||||||
|
*/
|
||||||
|
function setupTypeSubtypeLink() {
|
||||||
|
const typeField = document.querySelector('.editable-field[data-field="type"]');
|
||||||
|
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
|
||||||
|
|
||||||
|
if (!typeField || !subtypeField) return;
|
||||||
|
|
||||||
|
const typeSelect = typeField.querySelector('.field-edit select');
|
||||||
|
const subtypeSelect = subtypeField.querySelector('.field-edit select');
|
||||||
|
|
||||||
|
if (!typeSelect || !subtypeSelect) return;
|
||||||
|
|
||||||
|
let subtypes = [];
|
||||||
|
try {
|
||||||
|
if (subtypeField.dataset.subtypes) {
|
||||||
|
subtypes = JSON.parse(subtypeField.dataset.subtypes);
|
||||||
|
} else if (typeof interventionSubtypesData !== 'undefined') {
|
||||||
|
subtypes = interventionSubtypesData;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing subtypes:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSubtype(selectedType, keepValue) {
|
||||||
|
const available = subtypes.filter(st => !st.types || st.types.length === 0 || st.types.includes(selectedType));
|
||||||
|
const hasSubtypes = available.length > 0;
|
||||||
|
|
||||||
|
if (!hasSubtypes) {
|
||||||
|
subtypeField.classList.add('d-none');
|
||||||
|
subtypeSelect.value = '';
|
||||||
|
} else {
|
||||||
|
subtypeField.classList.remove('d-none');
|
||||||
|
const currentVal = subtypeSelect.value;
|
||||||
|
subtypeSelect.innerHTML = '<option value="">-</option>';
|
||||||
|
let valueStillValid = false;
|
||||||
|
available.forEach(st => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = String(st.id);
|
||||||
|
opt.textContent = st.name;
|
||||||
|
if (keepValue && String(st.id) === currentVal) {
|
||||||
|
opt.selected = true;
|
||||||
|
valueStillValid = true;
|
||||||
|
}
|
||||||
|
subtypeSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (keepValue && !valueStillValid) {
|
||||||
|
subtypeSelect.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelect.addEventListener('change', function() {
|
||||||
|
updateSubtype(this.value, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the dynamic link between provider and contract selects.
|
* Setup the dynamic link between provider and contract selects.
|
||||||
|
|
@ -130,6 +193,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store original subtype options and visibility
|
||||||
|
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
|
||||||
|
if (subtypeField) {
|
||||||
|
const subtypeSelect = subtypeField.querySelector('.field-edit select');
|
||||||
|
if (subtypeSelect) {
|
||||||
|
originalSubtypeOptions = subtypeSelect.innerHTML;
|
||||||
|
}
|
||||||
|
originalSubtypeVisible = !subtypeField.classList.contains('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
// Store original values
|
// Store original values
|
||||||
originalValues = {};
|
originalValues = {};
|
||||||
editableFields.forEach(field => {
|
editableFields.forEach(field => {
|
||||||
|
|
@ -196,6 +269,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore original subtype options and visibility
|
||||||
|
if (originalSubtypeOptions !== null) {
|
||||||
|
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
|
||||||
|
if (subtypeField) {
|
||||||
|
const subtypeSelect = subtypeField.querySelector('.field-edit select');
|
||||||
|
if (subtypeSelect) {
|
||||||
|
subtypeSelect.innerHTML = originalSubtypeOptions;
|
||||||
|
}
|
||||||
|
if (originalSubtypeVisible) {
|
||||||
|
subtypeField.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
subtypeField.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore original values
|
// Restore original values
|
||||||
editableFields.forEach(field => {
|
editableFields.forEach(field => {
|
||||||
const canEdit = field.dataset.canEdit === 'true';
|
const canEdit = field.dataset.canEdit === 'true';
|
||||||
|
|
@ -234,6 +323,8 @@
|
||||||
|
|
||||||
originalValues = {};
|
originalValues = {};
|
||||||
originalContractOptions = null;
|
originalContractOptions = null;
|
||||||
|
originalSubtypeOptions = null;
|
||||||
|
originalSubtypeVisible = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveChanges(options = {}) {
|
async function saveChanges(options = {}) {
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,11 @@
|
||||||
{{ form.type }}
|
{{ form.type }}
|
||||||
{{ form.type.errors }}
|
{{ form.type.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 d-none" id="sub_type_field_wrapper">
|
||||||
|
<label for="id_sub_type" class="form-label">{% translate "Sous-type d'intervention" %}</label>
|
||||||
|
{{ form.sub_type }}
|
||||||
|
{{ form.sub_type.errors }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
const thematic = '{{ thematic.code }}';
|
const thematic = '{{ thematic.code }}';
|
||||||
const symptomsData = {{ symptoms_data|safe }};
|
const symptomsData = {{ symptoms_data|safe }};
|
||||||
const sourceCategoriesData = {{ source_categories_data|safe }};
|
const sourceCategoriesData = {{ source_categories_data|safe }};
|
||||||
|
const subtypesData = {{ subtypes_data|default:"[]"|safe }};
|
||||||
const autocomplete_street_url = "{% url 'common:autocomplete_street' %}";
|
const autocomplete_street_url = "{% url 'common:autocomplete_street' %}";
|
||||||
var autocomplete_locations_url = "{% url 'assets:locations_autocomplete' %}?thematic={{ thematic.code }}{% if inspection_config %}&prioritize={% if inspection_config.default_category %}{{ inspection_config.default_category.code|lower }}{% else %}giep{% endif %}{% endif %}";
|
var autocomplete_locations_url = "{% url 'assets:locations_autocomplete' %}?thematic={{ thematic.code }}{% if inspection_config %}&prioritize={% if inspection_config.default_category %}{{ inspection_config.default_category.code|lower }}{% else %}giep{% endif %}{% endif %}";
|
||||||
var regional_roads_url = '{% url "common:get_regional_roads_geojson" %}';
|
var regional_roads_url = '{% url "common:get_regional_roads_geojson" %}';
|
||||||
|
|
@ -54,6 +55,50 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Dynamic subtype filtering for intervention add form -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeSelect = document.getElementById('id_type');
|
||||||
|
const subTypeSelect = document.getElementById('id_sub_type');
|
||||||
|
const subTypeWrapper = document.getElementById('sub_type_field_wrapper');
|
||||||
|
|
||||||
|
if (!typeSelect || !subTypeSelect || !subTypeWrapper) return;
|
||||||
|
|
||||||
|
function updateSubTypeVisibility() {
|
||||||
|
const selectedType = typeSelect.value;
|
||||||
|
const available = (typeof subtypesData !== 'undefined' ? subtypesData : []).filter(st =>
|
||||||
|
!st.types || st.types.length === 0 || st.types.includes(selectedType)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (available.length === 0) {
|
||||||
|
subTypeWrapper.classList.add('d-none');
|
||||||
|
subTypeSelect.value = '';
|
||||||
|
} else {
|
||||||
|
subTypeWrapper.classList.remove('d-none');
|
||||||
|
const currentVal = subTypeSelect.value;
|
||||||
|
subTypeSelect.innerHTML = '<option value="">---------</option>';
|
||||||
|
let valueStillValid = false;
|
||||||
|
available.forEach(st => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = String(st.id);
|
||||||
|
opt.textContent = st.name;
|
||||||
|
if (String(st.id) === currentVal) {
|
||||||
|
opt.selected = true;
|
||||||
|
valueStillValid = true;
|
||||||
|
}
|
||||||
|
subTypeSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (!valueStillValid && currentVal) {
|
||||||
|
subTypeSelect.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelect.addEventListener('change', updateSubTypeVisibility);
|
||||||
|
updateSubTypeVisibility();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- intervention_add.js (split) -->
|
<!-- intervention_add.js (split) -->
|
||||||
<script src="{% static 'interventions/add/map.js' %}"></script>
|
<script src="{% static 'interventions/add/map.js' %}"></script>
|
||||||
<script src="{% static 'interventions/add/source.js' %}"></script>
|
<script src="{% static 'interventions/add/source.js' %}"></script>
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,31 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sous-type d'intervention -->
|
||||||
|
<div class="d-flex align-items-center editable-field {% if not has_subtypes_for_current_type %}d-none{% endif %}"
|
||||||
|
id="intervention-subtype-field"
|
||||||
|
data-field="sub_type"
|
||||||
|
data-subtypes="{{ subtypes_data_json|escape }}"
|
||||||
|
data-can-edit="{% if 'sub_type' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||||||
|
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tag"></i></button>
|
||||||
|
<span class="label mx-1">{% translate "Sous-type" %}:</span>
|
||||||
|
<div class="field-display">
|
||||||
|
<span class="field-value">{{ intervention.sub_type.get_name|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-edit d-none" style="flex: 1;">
|
||||||
|
<select class="form-select form-select-sm" id="intervention-subtype-select">
|
||||||
|
<option value="">-</option>
|
||||||
|
{% for st in sub_types %}
|
||||||
|
{% if not st.intervention_types or intervention.type in st.intervention_types %}
|
||||||
|
<option value="{{ st.id }}" {% if st.id == intervention.sub_type_id %}selected{% endif %}>
|
||||||
|
{{ st.get_name }}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Catégorie -->
|
<!-- Catégorie -->
|
||||||
<div class="d-flex align-items-center editable-field" data-field="category" data-can-edit="{% if 'category' in allowed_update_fields %}true{% else %}false{% endif %}">
|
<div class="d-flex align-items-center editable-field" data-field="category" data-can-edit="{% if 'category' in allowed_update_fields %}true{% else %}false{% endif %}">
|
||||||
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tags"></i></button>
|
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tags"></i></button>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
const recommendedNextStatus = '{{ recommended_next_status }}';
|
const recommendedNextStatus = '{{ recommended_next_status }}';
|
||||||
const interventionId = '{{ intervention.id }}';
|
const interventionId = '{{ intervention.id }}';
|
||||||
const interventionThematicId = {% if intervention.thematic %}{{ intervention.thematic.id }}{% else %}null{% endif %};
|
const interventionThematicId = {% if intervention.thematic %}{{ intervention.thematic.id }}{% else %}null{% endif %};
|
||||||
|
const interventionSubtypesData = {{ subtypes_data_json|default:"[]"|safe }};
|
||||||
const allowed_transitions = {{ allowed_transitions|safe }};
|
const allowed_transitions = {{ allowed_transitions|safe }};
|
||||||
const canViewPrices = {{ can_view_prices|yesno:"true,false" }};
|
const canViewPrices = {{ can_view_prices|yesno:"true,false" }};
|
||||||
var contractId = {% if contract_id %} {{ contract_id }} {% else %} null {% endif %};
|
var contractId = {% if contract_id %} {{ contract_id }} {% else %} null {% endif %};
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,165 @@ class InterventionSubTypeTests(TestCase):
|
||||||
display_univ = admin_instance.types_display(self.subtype_universal)
|
display_univ = admin_instance.types_display(self.subtype_universal)
|
||||||
self.assertEqual(str(display_univ), "Tous")
|
self.assertEqual(str(display_univ), "Tous")
|
||||||
|
|
||||||
|
def test_update_intervention_field_subtype(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
from interventions.views.helpers import _update_intervention_field
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Test SubType Update',
|
||||||
|
type='repair',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Setting compatible subtype succeeds
|
||||||
|
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_multi.id))
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertIsNone(err)
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_multi)
|
||||||
|
|
||||||
|
# 2. Setting universal subtype succeeds
|
||||||
|
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_universal.id))
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_universal)
|
||||||
|
|
||||||
|
# 3. Setting incompatible subtype fails
|
||||||
|
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_build_only.id))
|
||||||
|
self.assertFalse(success)
|
||||||
|
self.assertIn("non compatible", err)
|
||||||
|
# Old value unchanged
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_universal)
|
||||||
|
|
||||||
|
# 4. Setting empty string clears subtype
|
||||||
|
success, err = _update_intervention_field(intervention, 'sub_type', "")
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertIsNone(intervention.sub_type)
|
||||||
|
|
||||||
|
def test_type_change_resets_incompatible_subtype(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
from interventions.views.helpers import _update_intervention_field
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Test SubType Incompatible Reset',
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.subtype_multi,
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
)
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_multi)
|
||||||
|
|
||||||
|
# Change type to 'build' (incompatible with subtype_multi ['repair', 'fixing'])
|
||||||
|
success, err = _update_intervention_field(intervention, 'type', 'build')
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(intervention.type, 'build')
|
||||||
|
self.assertIsNone(intervention.sub_type)
|
||||||
|
|
||||||
|
def test_permissions_includes_sub_type(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
from interventions.permissions import get_allowed_update_fields
|
||||||
|
from common.models import UserConfig, Role
|
||||||
|
|
||||||
|
admin_role, _ = Role.objects.get_or_create(name='admin')
|
||||||
|
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
|
||||||
|
user_config.roles.add(admin_role)
|
||||||
|
user_config.is_intern = True
|
||||||
|
user_config.save()
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='Test Perms',
|
||||||
|
type='repair',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
)
|
||||||
|
allowed_fields = get_allowed_update_fields(self.user, intervention)
|
||||||
|
self.assertIn('type', allowed_fields)
|
||||||
|
self.assertIn('sub_type', allowed_fields)
|
||||||
|
|
||||||
|
def test_detail_view_subtypes_context(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
from common.models import UserConfig, Role
|
||||||
|
import json
|
||||||
|
|
||||||
|
admin_role, _ = Role.objects.get_or_create(name='admin')
|
||||||
|
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
|
||||||
|
user_config.roles.add(admin_role)
|
||||||
|
user_config.is_intern = True
|
||||||
|
user_config.save()
|
||||||
|
|
||||||
|
# 1. Intervention with type 'repair' (has matching subtypes: sub_multi, sub_univ)
|
||||||
|
interv_repair = Intervention.objects.create(
|
||||||
|
title='Detail View Subtype Test',
|
||||||
|
type='repair',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(reverse('interventions:intervention_detail', args=[interv_repair.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context['has_subtypes_for_current_type'])
|
||||||
|
subtypes_in_json = json.loads(response.context['subtypes_data_json'])
|
||||||
|
self.assertTrue(any(s['id'] == self.subtype_multi.id for s in subtypes_in_json))
|
||||||
|
|
||||||
|
# 2. Deactivate universal subtype to test type without subtypes
|
||||||
|
self.subtype_universal.is_active = False
|
||||||
|
self.subtype_universal.save()
|
||||||
|
|
||||||
|
interv_cleaning = Intervention.objects.create(
|
||||||
|
title='Detail View No Subtype Test',
|
||||||
|
type='cleaning',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
response_cleaning = self.client.get(reverse('interventions:intervention_detail', args=[interv_cleaning.id]))
|
||||||
|
self.assertEqual(response_cleaning.status_code, 200)
|
||||||
|
self.assertFalse(response_cleaning.context['has_subtypes_for_current_type'])
|
||||||
|
|
||||||
|
def test_update_intervention_endpoint_subtype(self):
|
||||||
|
from interventions.models import Intervention
|
||||||
|
from common.models import UserConfig, Role
|
||||||
|
import json
|
||||||
|
|
||||||
|
admin_role, _ = Role.objects.get_or_create(name='admin')
|
||||||
|
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
|
||||||
|
user_config.roles.add(admin_role)
|
||||||
|
user_config.is_intern = True
|
||||||
|
user_config.save()
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
title='API Update Subtype Test',
|
||||||
|
type='repair',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='in_preparation',
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
url = reverse('interventions:update_intervention', args=[intervention.id])
|
||||||
|
|
||||||
|
# Batch update sub_type
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps({'sub_type': str(self.subtype_multi.id)}),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
intervention.refresh_from_db()
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_multi)
|
||||||
|
|
||||||
|
# Batch update type and sub_type together: switch type to 'build' and sub_type to sub_build
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data=json.dumps({'type': 'build', 'sub_type': str(self.subtype_build_only.id)}),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
intervention.refresh_from_db()
|
||||||
|
self.assertEqual(intervention.type, 'build')
|
||||||
|
self.assertEqual(intervention.sub_type, self.subtype_build_only)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ 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.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
||||||
InterventionLink,
|
InterventionLink, InterventionSubType,
|
||||||
ContractPauseReasonAssignment,
|
ContractPauseReasonAssignment,
|
||||||
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
|
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
|
||||||
InterventionOccupation,
|
InterventionOccupation,
|
||||||
|
|
@ -632,6 +632,20 @@ def intervention_detail(request, intervention_id):
|
||||||
|
|
||||||
sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
|
sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
|
||||||
|
|
||||||
|
sub_types_qs = InterventionSubType.objects.filter(is_active=True, thematic=intervention.thematic).order_by('order_number', 'name_fr')
|
||||||
|
subtypes_data = [
|
||||||
|
{
|
||||||
|
'id': st.id,
|
||||||
|
'name': st.get_name(),
|
||||||
|
'types': st.intervention_types,
|
||||||
|
}
|
||||||
|
for st in sub_types_qs
|
||||||
|
]
|
||||||
|
has_subtypes_for_current_type = any(
|
||||||
|
not st.intervention_types or intervention.type in st.intervention_types
|
||||||
|
for st in sub_types_qs
|
||||||
|
)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"intervention": intervention,
|
"intervention": intervention,
|
||||||
"status_choices" : [{"key": key, "label": label} for key, label in STATUS_CHOICES.items()],
|
"status_choices" : [{"key": key, "label": label} for key, label in STATUS_CHOICES.items()],
|
||||||
|
|
@ -663,6 +677,9 @@ def intervention_detail(request, intervention_id):
|
||||||
'contracts': contracts,
|
'contracts': contracts,
|
||||||
'maintain_choices': MAINTAIN_CHOICES,
|
'maintain_choices': MAINTAIN_CHOICES,
|
||||||
'type_choices': get_type_choices(intervention.thematic),
|
'type_choices': get_type_choices(intervention.thematic),
|
||||||
|
'sub_types': sub_types_qs,
|
||||||
|
'subtypes_data_json': _json_mod.dumps(subtypes_data),
|
||||||
|
'has_subtypes_for_current_type': has_subtypes_for_current_type,
|
||||||
'origin_type_choices': ORIGIN_TYPE_CHOICES,
|
'origin_type_choices': ORIGIN_TYPE_CHOICES,
|
||||||
'priority_choices': Intervention._meta.get_field('priority').choices,
|
'priority_choices': Intervention._meta.get_field('priority').choices,
|
||||||
'symptoms': Symptom.objects.filter(is_active=True, thematic=intervention.thematic).order_by('name_fr'),
|
'symptoms': Symptom.objects.filter(is_active=True, thematic=intervention.thematic).order_by('name_fr'),
|
||||||
|
|
@ -1694,6 +1711,16 @@ def interventions_add(request, thematic_code):
|
||||||
for sc in SourceCategory.objects.filter(is_active=True)
|
for sc in SourceCategory.objects.filter(is_active=True)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
subtypes_qs = InterventionSubType.objects.filter(thematic=thematic, is_active=True).order_by('order_number', 'name_fr')
|
||||||
|
subtypes_data = [
|
||||||
|
{
|
||||||
|
"id": st.id,
|
||||||
|
"name": st.get_name(),
|
||||||
|
"types": st.intervention_types,
|
||||||
|
}
|
||||||
|
for st in subtypes_qs
|
||||||
|
]
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
maintain_type = request.POST.get('maintain_type', 'corrective')
|
maintain_type = request.POST.get('maintain_type', 'corrective')
|
||||||
form = InterventionForm(request.POST, request.FILES, thematic=thematic, user=request.user)
|
form = InterventionForm(request.POST, request.FILES, thematic=thematic, user=request.user)
|
||||||
|
|
@ -2017,7 +2044,7 @@ def interventions_add(request, thematic_code):
|
||||||
for key in [
|
for key in [
|
||||||
'title', 'lat', 'lon', 'address', 'asset_category', 'status',
|
'title', 'lat', 'lon', 'address', 'asset_category', 'status',
|
||||||
'source_category', 'source_ref', 'source_detail', 'init_description',
|
'source_category', 'source_ref', 'source_detail', 'init_description',
|
||||||
'type', 'origin_type', 'priority'
|
'type', 'sub_type', 'origin_type', 'priority'
|
||||||
]:
|
]:
|
||||||
val = request.GET.get(key)
|
val = request.GET.get(key)
|
||||||
if val not in (None, ""):
|
if val not in (None, ""):
|
||||||
|
|
@ -2093,6 +2120,7 @@ def interventions_add(request, thematic_code):
|
||||||
'thematic': thematic,
|
'thematic': thematic,
|
||||||
'symptoms_data': json.dumps(symptoms_data),
|
'symptoms_data': json.dumps(symptoms_data),
|
||||||
'source_categories_data': json.dumps(source_categories_data),
|
'source_categories_data': json.dumps(source_categories_data),
|
||||||
|
'subtypes_data': json.dumps(subtypes_data),
|
||||||
'maintain_type': maintain_type,
|
'maintain_type': maintain_type,
|
||||||
'observation': observation,
|
'observation': observation,
|
||||||
'observation_photos': observation_photos,
|
'observation_photos': observation_photos,
|
||||||
|
|
@ -2654,14 +2682,18 @@ def update_intervention(request, intervention_id):
|
||||||
# Handle notes separately (special case)
|
# Handle notes separately (special case)
|
||||||
notes_data = data.pop('notes', None)
|
notes_data = data.pop('notes', None)
|
||||||
|
|
||||||
# Sort fields to process assigned_provider before contract
|
# Sort fields to process assigned_provider before contract, and type before sub_type
|
||||||
def _field_priority(item):
|
def _field_priority(item):
|
||||||
f_name = item[0]
|
f_name = item[0]
|
||||||
if f_name == 'assigned_provider':
|
if f_name == 'assigned_provider':
|
||||||
return 0
|
return 0
|
||||||
if f_name == 'contract':
|
if f_name == 'contract':
|
||||||
return 1
|
return 1
|
||||||
|
if f_name == 'type':
|
||||||
return 2
|
return 2
|
||||||
|
if f_name == 'sub_type':
|
||||||
|
return 3
|
||||||
|
return 4
|
||||||
|
|
||||||
for field, new_value in sorted(data.items(), key=_field_priority):
|
for field, new_value in sorted(data.items(), key=_field_priority):
|
||||||
if field not in allowed_update_fields:
|
if field not in allowed_update_fields:
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ 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.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
|
||||||
InterventionLink,
|
InterventionLink, InterventionSubType,
|
||||||
ContractPauseReasonAssignment,
|
ContractPauseReasonAssignment,
|
||||||
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
|
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
|
||||||
InterventionOccupation,
|
InterventionOccupation,
|
||||||
|
|
@ -2604,9 +2604,32 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
||||||
allowed_types = dict(get_type_choices(intervention.thematic)).keys()
|
allowed_types = dict(get_type_choices(intervention.thematic)).keys()
|
||||||
if new_value in allowed_types:
|
if new_value in allowed_types:
|
||||||
intervention.type = new_value
|
intervention.type = new_value
|
||||||
|
if intervention.sub_type and intervention.sub_type.intervention_types:
|
||||||
|
if new_value not in intervention.sub_type.intervention_types:
|
||||||
|
intervention.sub_type = None
|
||||||
return True, None
|
return True, None
|
||||||
return False, _('Type not found')
|
return False, _('Type not found')
|
||||||
|
|
||||||
|
elif field == "sub_type":
|
||||||
|
if new_value in ("", None, "null"):
|
||||||
|
intervention.sub_type = None
|
||||||
|
return True, None
|
||||||
|
try:
|
||||||
|
sub_type_id = int(new_value)
|
||||||
|
sub_type = InterventionSubType.objects.filter(
|
||||||
|
id=sub_type_id,
|
||||||
|
is_active=True,
|
||||||
|
thematic=intervention.thematic,
|
||||||
|
).first()
|
||||||
|
if not sub_type:
|
||||||
|
return False, _('Sous-type introuvable')
|
||||||
|
if sub_type.intervention_types and intervention.type not in sub_type.intervention_types:
|
||||||
|
return False, _("Sous-type non compatible avec le type d'intervention")
|
||||||
|
intervention.sub_type = sub_type
|
||||||
|
return True, None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False, _('Sous-type invalide')
|
||||||
|
|
||||||
elif field == "origin_type":
|
elif field == "origin_type":
|
||||||
if new_value in dict(ORIGIN_TYPE_CHOICES).keys():
|
if new_value in dict(ORIGIN_TYPE_CHOICES).keys():
|
||||||
intervention.origin_type = new_value
|
intervention.origin_type = new_value
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue