feat(interventions): add combined filter for manager, controller and inspector with shortnames and teams
This commit is contained in:
parent
b86d805cb5
commit
3d9e7c3202
11 changed files with 647 additions and 69 deletions
Binary file not shown.
|
|
@ -8407,5 +8407,29 @@ msgstr "Cannot modify material from a canceled preparation order."
|
|||
msgid "Bon annulé"
|
||||
msgstr "Canceled order"
|
||||
|
||||
msgid "Rôle et personne assignée"
|
||||
msgstr "Role and assigned person"
|
||||
|
||||
msgid "Rôle"
|
||||
msgstr "Role"
|
||||
|
||||
msgid "Personne"
|
||||
msgstr "Person"
|
||||
|
||||
msgid "Gestionnaire"
|
||||
msgstr "Manager"
|
||||
|
||||
msgid "Contrôleur"
|
||||
msgstr "Controller"
|
||||
|
||||
msgid "Inspecteur"
|
||||
msgstr "Inspector"
|
||||
|
||||
msgid "-- Sélectionner un rôle… --"
|
||||
msgstr "-- Select a role… --"
|
||||
|
||||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Choose a person… --"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8514,5 +8514,29 @@ msgstr "Impossible de modifier le matériel d'un bon de préparation annulé."
|
|||
msgid "Bon annulé"
|
||||
msgstr "Bon annulé"
|
||||
|
||||
msgid "Rôle et personne assignée"
|
||||
msgstr "Rôle et personne assignée"
|
||||
|
||||
msgid "Rôle"
|
||||
msgstr "Rôle"
|
||||
|
||||
msgid "Personne"
|
||||
msgstr "Personne"
|
||||
|
||||
msgid "Gestionnaire"
|
||||
msgstr "Gestionnaire"
|
||||
|
||||
msgid "Contrôleur"
|
||||
msgstr "Contrôleur"
|
||||
|
||||
msgid "Inspecteur"
|
||||
msgstr "Inspecteur"
|
||||
|
||||
msgid "-- Sélectionner un rôle… --"
|
||||
msgstr "-- Sélectionner un rôle… --"
|
||||
|
||||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Choisir une personne… --"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8448,4 +8448,28 @@ msgstr "Kan materiaal van een geannuleerde voorbereidingsbon niet wijzigen."
|
|||
msgid "Bon annulé"
|
||||
msgstr "Geannuleerde bon"
|
||||
|
||||
msgid "Rôle et personne assignée"
|
||||
msgstr "Rol en toegewezen persoon"
|
||||
|
||||
msgid "Rôle"
|
||||
msgstr "Rol"
|
||||
|
||||
msgid "Personne"
|
||||
msgstr "Persoon"
|
||||
|
||||
msgid "Gestionnaire"
|
||||
msgstr "Beheerder"
|
||||
|
||||
msgid "Contrôleur"
|
||||
msgstr "Controleur"
|
||||
|
||||
msgid "Inspecteur"
|
||||
msgstr "Inspecteur"
|
||||
|
||||
msgid "-- Sélectionner un rôle… --"
|
||||
msgstr "-- Selecteer een rol… --"
|
||||
|
||||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Kies een persoon… --"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -330,6 +330,7 @@ document.getElementById('reset-filters').addEventListener('click', async functio
|
|||
togglePauseReasonFilter();
|
||||
filterCategoriesByThematics();
|
||||
filterProvidersAndContracts();
|
||||
resetRolePersonFilter();
|
||||
|
||||
// Explicitly clear date fields
|
||||
document.getElementById('begin_date_min').value = '';
|
||||
|
|
@ -2185,8 +2186,8 @@ function filterProvidersAndContracts() {
|
|||
}
|
||||
});
|
||||
}
|
||||
if (typeof updateInterventionManagers === 'function') {
|
||||
updateInterventionManagers();
|
||||
if (typeof updateRolePersonManagers === 'function') {
|
||||
updateRolePersonManagers();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2278,7 +2279,9 @@ function countActiveFilters() {
|
|||
'contract': false,
|
||||
'assigned_team': false,
|
||||
'assigned_member': false,
|
||||
'assigned_controller': false,
|
||||
'intervention_manager': false,
|
||||
'inspector': false,
|
||||
'pause_reason': false,
|
||||
'begin_date_min': false,
|
||||
'begin_date_max': false,
|
||||
|
|
@ -2330,7 +2333,8 @@ function getCurrentFilters() {
|
|||
// Get all filter parameters
|
||||
const filterKeys = [
|
||||
'thematic', 'status', 'priority', 'maintain_type', 'category',
|
||||
'type', 'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager',
|
||||
'type', 'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector',
|
||||
'pause_reason', 'locations', 'overdue', 'to_be_planned_soon'
|
||||
];
|
||||
|
||||
|
|
@ -2407,7 +2411,8 @@ function countActiveFilters() {
|
|||
// Define filter parameters used in the global filter modal
|
||||
const filterParams = [
|
||||
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
];
|
||||
|
|
@ -2451,7 +2456,8 @@ function savePreferencesFromUrl() {
|
|||
// Collect all filter parameters
|
||||
const filterParams = [
|
||||
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
];
|
||||
|
|
@ -2504,7 +2510,8 @@ async function savePreferencesFromForm(urlParams) {
|
|||
// Collect all filter parameters from URLSearchParams
|
||||
const filterParams = [
|
||||
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
];
|
||||
|
|
@ -2558,6 +2565,7 @@ async function savePreferencesFromForm(urlParams) {
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateFilterBadge();
|
||||
initDateFilterPresets();
|
||||
initRolePersonFilter();
|
||||
|
||||
// Save preferences when table sort changes (user action only)
|
||||
const $table = $('#interventions-table');
|
||||
|
|
@ -2637,62 +2645,153 @@ if (filterModal) {
|
|||
});
|
||||
}
|
||||
|
||||
function updateInterventionManagers() {
|
||||
const checkedContracts = document.querySelectorAll('.filter-options[data-field="contract"] input[type="checkbox"]:checked');
|
||||
const container = document.getElementById('intervention-manager-options-container');
|
||||
const row = document.getElementById('intervention-manager-filter-row');
|
||||
if (!container || !row) return;
|
||||
// ==================== Role & Person Filter (Gestionnaire / Contrôleur / Inspecteur) ====================
|
||||
|
||||
// Get currently selected managers from URL or from checkboxes if they already exist
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const selectedManagers = new Set(urlParams.getAll('intervention_manager'));
|
||||
|
||||
// Also check if any are currently checked in the UI before re-drawing
|
||||
const existingChecked = container.querySelectorAll('input[type="checkbox"]:checked');
|
||||
existingChecked.forEach(cb => selectedManagers.add(cb.value));
|
||||
|
||||
// Gather unique managers
|
||||
const managersMap = new Map();
|
||||
let anyUsesManager = false;
|
||||
|
||||
checkedContracts.forEach(cb => {
|
||||
const optionDiv = cb.closest('.filter-option');
|
||||
if (optionDiv) {
|
||||
const usesManager = optionDiv.getAttribute('data-uses-manager') === 'true';
|
||||
if (usesManager) {
|
||||
anyUsesManager = true;
|
||||
const managersStr = optionDiv.getAttribute('data-managers');
|
||||
if (managersStr) {
|
||||
function getRolePersonsData() {
|
||||
const scriptEl = document.getElementById('role-persons-data');
|
||||
if (!scriptEl) return { manager: [], controller: [], inspector: [] };
|
||||
try {
|
||||
const managers = JSON.parse(managersStr);
|
||||
managers.forEach(mgr => {
|
||||
managersMap.set(String(mgr.id), mgr.name);
|
||||
});
|
||||
return JSON.parse(scriptEl.textContent) || { manager: [], controller: [], inspector: [] };
|
||||
} catch (e) {
|
||||
console.error("Error parsing managers JSON", e);
|
||||
console.error("Error parsing role-persons data", e);
|
||||
return { manager: [], controller: [], inspector: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function syncRolePersonHiddenInputs() {
|
||||
const roleSelect = document.getElementById('filter-person-role');
|
||||
const personSelect = document.getElementById('filter-person-select');
|
||||
const hiddenMgr = document.getElementById('hidden-filter-intervention-manager');
|
||||
const hiddenCtrl = document.getElementById('hidden-filter-assigned-controller');
|
||||
const hiddenInsp = document.getElementById('hidden-filter-inspector');
|
||||
|
||||
if (!roleSelect || !personSelect) return;
|
||||
|
||||
const role = roleSelect.value;
|
||||
const personId = personSelect.value;
|
||||
|
||||
if (hiddenMgr) hiddenMgr.value = (role === 'manager' && personId) ? personId : '';
|
||||
if (hiddenCtrl) hiddenCtrl.value = (role === 'controller' && personId) ? personId : '';
|
||||
if (hiddenInsp) hiddenInsp.value = (role === 'inspector' && personId) ? personId : '';
|
||||
}
|
||||
|
||||
function populatePersonSelect(role, preselectedId) {
|
||||
const personSelect = document.getElementById('filter-person-select');
|
||||
if (!personSelect) return;
|
||||
|
||||
const chooseLabel = typeof gettext === 'function' ? gettext("-- Choisir une personne… --") : "-- Choisir une personne… --";
|
||||
personSelect.innerHTML = `<option value="">${chooseLabel}</option>`;
|
||||
|
||||
if (!role) {
|
||||
personSelect.disabled = true;
|
||||
syncRolePersonHiddenInputs();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getRolePersonsData();
|
||||
let persons = data[role] || [];
|
||||
|
||||
// If role is manager, filter by checked contracts if any are checked
|
||||
if (role === 'manager') {
|
||||
const checkedContractBoxes = document.querySelectorAll('.filter-options[data-field="contract"] input[type="checkbox"]:checked');
|
||||
if (checkedContractBoxes.length > 0) {
|
||||
const checkedContractIds = Array.from(checkedContractBoxes).map(cb => parseInt(cb.value, 10)).filter(Boolean);
|
||||
if (checkedContractIds.length > 0) {
|
||||
const filtered = persons.filter(p => {
|
||||
const pContracts = (p.contracts || []).map(cid => parseInt(cid, 10));
|
||||
return pContracts.some(cid => checkedContractIds.includes(cid));
|
||||
});
|
||||
if (filtered.length > 0) {
|
||||
persons = filtered;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persons.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
if (preselectedId && String(p.id) === String(preselectedId)) {
|
||||
opt.selected = true;
|
||||
}
|
||||
personSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
if (anyUsesManager && managersMap.size > 0) {
|
||||
// Build the HTML for options
|
||||
let html = '';
|
||||
managersMap.forEach((name, id) => {
|
||||
const isChecked = selectedManagers.has(id) ? 'checked' : '';
|
||||
html += `
|
||||
<div class="filter-option">
|
||||
<input type="checkbox" name="intervention_manager" id="intervention_manager_${id}" value="${id}" ${isChecked}>
|
||||
<label for="intervention_manager_${id}">${name}</label>
|
||||
</div>
|
||||
`;
|
||||
personSelect.disabled = false;
|
||||
if (preselectedId) {
|
||||
personSelect.value = String(preselectedId);
|
||||
}
|
||||
syncRolePersonHiddenInputs();
|
||||
}
|
||||
|
||||
function updateRolePersonManagers() {
|
||||
const roleSelect = document.getElementById('filter-person-role');
|
||||
const personSelect = document.getElementById('filter-person-select');
|
||||
if (!roleSelect || !personSelect) return;
|
||||
|
||||
if (roleSelect.value === 'manager') {
|
||||
const currentPersonId = personSelect.value;
|
||||
populatePersonSelect('manager', currentPersonId);
|
||||
}
|
||||
}
|
||||
|
||||
function resetRolePersonFilter() {
|
||||
const roleSelect = document.getElementById('filter-person-role');
|
||||
const personSelect = document.getElementById('filter-person-select');
|
||||
if (roleSelect) roleSelect.value = '';
|
||||
if (personSelect) {
|
||||
const chooseLabel = typeof gettext === 'function' ? gettext("-- Choisir une personne… --") : "-- Choisir une personne… --";
|
||||
personSelect.innerHTML = `<option value="">${chooseLabel}</option>`;
|
||||
personSelect.disabled = true;
|
||||
}
|
||||
syncRolePersonHiddenInputs();
|
||||
}
|
||||
|
||||
function initRolePersonFilter() {
|
||||
const roleSelect = document.getElementById('filter-person-role');
|
||||
const personSelect = document.getElementById('filter-person-select');
|
||||
const clearBtn = document.getElementById('clear-role-person-filter');
|
||||
|
||||
if (!roleSelect || !personSelect) return;
|
||||
|
||||
roleSelect.addEventListener('change', function() {
|
||||
populatePersonSelect(this.value);
|
||||
});
|
||||
container.innerHTML = html;
|
||||
row.classList.remove('d-none');
|
||||
|
||||
personSelect.addEventListener('change', function() {
|
||||
syncRolePersonHiddenInputs();
|
||||
});
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function() {
|
||||
resetRolePersonFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// Determine initial role and person from server context or URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let initRole = typeof SELECTED_ROLE !== 'undefined' ? SELECTED_ROLE : '';
|
||||
let initPersonId = typeof SELECTED_PERSON_ID !== 'undefined' ? SELECTED_PERSON_ID : '';
|
||||
|
||||
if (!initRole) {
|
||||
if (urlParams.get('intervention_manager')) {
|
||||
initRole = 'manager';
|
||||
initPersonId = urlParams.get('intervention_manager');
|
||||
} else if (urlParams.get('assigned_controller')) {
|
||||
initRole = 'controller';
|
||||
initPersonId = urlParams.get('assigned_controller');
|
||||
} else if (urlParams.get('inspector')) {
|
||||
initRole = 'inspector';
|
||||
initPersonId = urlParams.get('inspector');
|
||||
}
|
||||
}
|
||||
|
||||
if (initRole) {
|
||||
roleSelect.value = initRole;
|
||||
populatePersonSelect(initRole, initPersonId);
|
||||
} else {
|
||||
container.innerHTML = '';
|
||||
row.classList.add('d-none');
|
||||
personSelect.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -770,21 +770,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gestionnaires (dynamic) -->
|
||||
<div class="row mt-3 d-none" id="intervention-manager-filter-row">
|
||||
<div class="col-md-12 filter-section">
|
||||
<div class="filter-section-header">
|
||||
<label>{% translate "Gestionnaire d'intervention" %}</label>
|
||||
<div class="filter-section-buttons">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="intervention_manager">{% translate "Tous" %}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="intervention_manager">{% translate "Aucun" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-options" data-field="intervention_manager" id="intervention-manager-options-container">
|
||||
<!-- Dynamic options will be inserted here by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Équipes / Membres assignés -->
|
||||
|
|
@ -825,6 +810,42 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rôle et Responsable / Intervenant assigné (Gestionnaire / Contrôleur / Inspecteur) -->
|
||||
<div class="row mt-3 pt-3 border-top">
|
||||
<div class="col-md-12 filter-section">
|
||||
<div class="filter-section-header">
|
||||
<label class="d-flex align-items-center">
|
||||
<i class="bi bi-person-badge me-2 text-primary"></i>{% translate "Rôle et personne assignée" %}
|
||||
</label>
|
||||
<div class="filter-section-buttons">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-role-person-filter">
|
||||
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 align-items-center mt-1">
|
||||
<div class="col-md-4">
|
||||
<label for="filter-person-role" class="form-label small text-muted mb-1">{% translate "Rôle" %}</label>
|
||||
<select class="form-select form-select-sm" id="filter-person-role">
|
||||
<option value="">{% translate "-- Sélectionner un rôle… --" %}</option>
|
||||
<option value="manager" {% if selected_role == "manager" %}selected{% endif %}>{% translate "Gestionnaire" %}</option>
|
||||
<option value="controller" {% if selected_role == "controller" %}selected{% endif %}>{% translate "Contrôleur" %}</option>
|
||||
<option value="inspector" {% if selected_role == "inspector" %}selected{% endif %}>{% translate "Inspecteur" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label for="filter-person-select" class="form-label small text-muted mb-1">{% translate "Personne" %}</label>
|
||||
<select class="form-select form-select-sm" id="filter-person-select" {% if not selected_role %}disabled{% endif %}>
|
||||
<option value="">{% translate "-- Choisir une personne… --" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="intervention_manager" id="hidden-filter-intervention-manager" value="{% if selected_role == 'manager' %}{{ selected_person_id }}{% endif %}">
|
||||
<input type="hidden" name="assigned_controller" id="hidden-filter-assigned-controller" value="{% if selected_role == 'controller' %}{{ selected_person_id }}{% endif %}">
|
||||
<input type="hidden" name="inspector" id="hidden-filter-inspector" value="{% if selected_role == 'inspector' %}{{ selected_person_id }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localisations (full width) -->
|
||||
<div class="filter-section mt-3 pt-3 border-top">
|
||||
<div class="filter-section-header">
|
||||
|
|
@ -1466,8 +1487,12 @@
|
|||
const SAVED_SORT = {{ saved_sort|default:"{}"|safe }};
|
||||
const SAVED_COLUMNS = {{ saved_columns|default:"null"|safe }};
|
||||
const subcatNoneLabel = "{% translate '(aucune sous-catégorie)' %}";
|
||||
const SELECTED_ROLE = '{{ selected_role|default:""|escapejs }}';
|
||||
const SELECTED_PERSON_ID = '{{ selected_person_id|default:""|escapejs }}';
|
||||
</script>
|
||||
|
||||
{{ role_persons_data|json_script:"role-persons-data" }}
|
||||
|
||||
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static 'common/base_script.js' %}"></script>
|
||||
|
|
|
|||
|
|
@ -2004,6 +2004,198 @@ class InterventionEndDateFilteringTests(TestCase):
|
|||
self.assertIn(itv_reopened, apply_intervention_filters_from_querydict(qs, params_august, user=self.user))
|
||||
|
||||
|
||||
class InterventionRolePersonFilteringTests(TestCase):
|
||||
def setUp(self):
|
||||
self.User = get_user_model()
|
||||
self.admin_user = self.User.objects.create_user(
|
||||
username='admin_role_test',
|
||||
password='pwd',
|
||||
first_name='Jean',
|
||||
last_name='Dupont'
|
||||
)
|
||||
self.role_admin, _ = Role.objects.get_or_create(name='admin')
|
||||
self.config = UserConfig.objects.create(user=self.admin_user, is_intern=True)
|
||||
self.config.roles.add(self.role_admin)
|
||||
|
||||
self.thematic = Thematic.objects.create(
|
||||
code='thematic_role_test',
|
||||
name_fr='Thématique Test',
|
||||
name_nl='Thema Test',
|
||||
allow_planning_without_contract=True
|
||||
)
|
||||
UserThematics.objects.create(
|
||||
user_config=self.config,
|
||||
thematic=self.thematic,
|
||||
can_view_interventions=True,
|
||||
can_edit_interventions=True,
|
||||
can_inspect=True,
|
||||
)
|
||||
|
||||
self.company = Company.objects.create(name='Entreprise Test')
|
||||
self.control_team = CompanyTeam.objects.create(
|
||||
name='Équipe Contrôle Qualité',
|
||||
company=self.company,
|
||||
operation_types=[CompanyTeam.OPERATION_CONTROLS]
|
||||
)
|
||||
self.control_team.thematics.add(self.thematic)
|
||||
|
||||
self.controller_user = self.User.objects.create_user(
|
||||
username='ctrl_user_test',
|
||||
password='pwd',
|
||||
first_name='Marc',
|
||||
last_name='Lambert'
|
||||
)
|
||||
self.controller_member = CompanyMember.objects.create(
|
||||
name='Marc Lambert',
|
||||
user=self.controller_user
|
||||
)
|
||||
self.controller_member.teams.add(self.control_team)
|
||||
|
||||
self.inspector_user = self.User.objects.create_user(
|
||||
username='insp_user_test',
|
||||
password='pwd',
|
||||
first_name='Claire',
|
||||
last_name='Bernard'
|
||||
)
|
||||
self.insp_config = UserConfig.objects.create(user=self.inspector_user, is_intern=True)
|
||||
UserThematics.objects.create(
|
||||
user_config=self.insp_config,
|
||||
thematic=self.thematic,
|
||||
can_view_interventions=True,
|
||||
can_inspect=True,
|
||||
)
|
||||
|
||||
self.manager_user = self.User.objects.create_user(
|
||||
username='mgr_user_test',
|
||||
password='pwd',
|
||||
first_name='Paul',
|
||||
last_name='Martin'
|
||||
)
|
||||
|
||||
# Interventions
|
||||
self.itv_manager = Intervention.objects.create(
|
||||
title='Intervention avec gestionnaire',
|
||||
thematic=self.thematic,
|
||||
created_by=self.admin_user,
|
||||
intervention_manager=self.manager_user
|
||||
)
|
||||
self.itv_controller = Intervention.objects.create(
|
||||
title='Intervention avec contrôleur',
|
||||
thematic=self.thematic,
|
||||
created_by=self.admin_user,
|
||||
assigned_controller=self.controller_member
|
||||
)
|
||||
self.itv_inspection_checked = Intervention.objects.create(
|
||||
title='Intervention avec inspection liée',
|
||||
thematic=self.thematic,
|
||||
created_by=self.admin_user,
|
||||
)
|
||||
self.itv_type_inspection = Intervention.objects.create(
|
||||
title='Intervention type inspection',
|
||||
thematic=self.thematic,
|
||||
created_by=self.admin_user,
|
||||
type='inspection',
|
||||
assigned_member=CompanyMember.objects.create(
|
||||
name='Claire Bernard',
|
||||
user=self.inspector_user
|
||||
)
|
||||
)
|
||||
|
||||
from inspections.models import Inspection
|
||||
self.inspection = Inspection.objects.create(
|
||||
checked_intervention=self.itv_inspection_checked,
|
||||
inspector=self.inspector_user,
|
||||
asset_content_type=ContentType.objects.get_for_model(Intervention),
|
||||
asset_object_id=self.itv_inspection_checked.id
|
||||
)
|
||||
|
||||
def test_format_shortname_helpers(self):
|
||||
from interventions.views.helpers import _format_user_shortname_with_team, _format_member_shortname_with_team
|
||||
|
||||
# User with team (via CompanyMember)
|
||||
formatted_ctrl_user = _format_user_shortname_with_team(self.controller_user)
|
||||
self.assertIn("Marc L.", formatted_ctrl_user)
|
||||
self.assertIn("Équipe Contrôle Qualité", formatted_ctrl_user)
|
||||
|
||||
# Member with team
|
||||
formatted_member = _format_member_shortname_with_team(self.controller_member)
|
||||
self.assertEqual(formatted_member, "Marc L. (Équipe Contrôle Qualité)")
|
||||
|
||||
# User without team
|
||||
formatted_mgr = _format_user_shortname_with_team(self.manager_user)
|
||||
self.assertEqual(formatted_mgr, "Paul M.")
|
||||
|
||||
def test_interventions_index_role_person_context(self):
|
||||
self.client.login(username='admin_role_test', password='pwd')
|
||||
url = reverse('interventions:interventions_index')
|
||||
|
||||
# 1. No filter (follows default redirect)
|
||||
resp = self.client.get(url, follow=True)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('role_persons_data', resp.context)
|
||||
self.assertEqual(resp.context['selected_role'], '')
|
||||
self.assertEqual(resp.context['selected_person_id'], '')
|
||||
|
||||
# 2. Filter by manager
|
||||
resp_mgr = self.client.get(f"{url}?intervention_manager={self.manager_user.id}")
|
||||
self.assertEqual(resp_mgr.status_code, 200)
|
||||
self.assertEqual(resp_mgr.context['selected_role'], 'manager')
|
||||
self.assertEqual(resp_mgr.context['selected_person_id'], str(self.manager_user.id))
|
||||
|
||||
# 3. Filter by controller
|
||||
resp_ctrl = self.client.get(f"{url}?assigned_controller={self.controller_member.id}")
|
||||
self.assertEqual(resp_ctrl.status_code, 200)
|
||||
self.assertEqual(resp_ctrl.context['selected_role'], 'controller')
|
||||
self.assertEqual(resp_ctrl.context['selected_person_id'], str(self.controller_member.id))
|
||||
|
||||
# 4. Filter by inspector
|
||||
resp_insp = self.client.get(f"{url}?inspector={self.inspector_user.id}")
|
||||
self.assertEqual(resp_insp.status_code, 200)
|
||||
self.assertEqual(resp_insp.context['selected_role'], 'inspector')
|
||||
self.assertEqual(resp_insp.context['selected_person_id'], str(self.inspector_user.id))
|
||||
|
||||
def test_filter_by_assigned_controller_and_inspector(self):
|
||||
from django.http import QueryDict
|
||||
from interventions.views.helpers import apply_intervention_filters_from_querydict
|
||||
|
||||
qs = Intervention.objects.all()
|
||||
|
||||
# Filter by controller via apply_intervention_filters_from_querydict
|
||||
params_ctrl = QueryDict(f'assigned_controller={self.controller_member.id}')
|
||||
filtered_ctrl = list(apply_intervention_filters_from_querydict(qs, params_ctrl, user=self.admin_user))
|
||||
self.assertIn(self.itv_controller, filtered_ctrl)
|
||||
self.assertNotIn(self.itv_manager, filtered_ctrl)
|
||||
|
||||
# Filter by inspector via apply_intervention_filters_from_querydict
|
||||
params_insp = QueryDict(f'inspector={self.inspector_user.id}')
|
||||
filtered_insp = list(apply_intervention_filters_from_querydict(qs, params_insp, user=self.admin_user))
|
||||
self.assertIn(self.itv_inspection_checked, filtered_insp)
|
||||
self.assertIn(self.itv_type_inspection, filtered_insp)
|
||||
self.assertNotIn(self.itv_controller, filtered_insp)
|
||||
self.assertNotIn(self.itv_manager, filtered_insp)
|
||||
|
||||
# Test via interventions_geojson
|
||||
self.client.login(username='admin_role_test', password='pwd')
|
||||
geojson_url = reverse('interventions:interventions_geojson')
|
||||
|
||||
# GeoJSON: controller
|
||||
resp_ctrl = self.client.get(f"{geojson_url}?assigned_controller={self.controller_member.id}")
|
||||
self.assertEqual(resp_ctrl.status_code, 200)
|
||||
features_ctrl = resp_ctrl.json().get('features', [])
|
||||
itv_ids_ctrl = [f['properties']['id'] for f in features_ctrl]
|
||||
self.assertIn(self.itv_controller.id, itv_ids_ctrl)
|
||||
self.assertNotIn(self.itv_manager.id, itv_ids_ctrl)
|
||||
|
||||
# GeoJSON: inspector
|
||||
resp_insp = self.client.get(f"{geojson_url}?inspector={self.inspector_user.id}")
|
||||
self.assertEqual(resp_insp.status_code, 200)
|
||||
features_insp = resp_insp.json().get('features', [])
|
||||
itv_ids_insp = [f['properties']['id'] for f in features_insp]
|
||||
self.assertIn(self.itv_inspection_checked.id, itv_ids_insp)
|
||||
self.assertIn(self.itv_type_inspection.id, itv_ids_insp)
|
||||
self.assertNotIn(self.itv_controller.id, itv_ids_insp)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ def index(request):
|
|||
for mgr in managers_qs:
|
||||
managers_data.append({
|
||||
'id': mgr.id,
|
||||
'name': _get_user_short_name(mgr)
|
||||
'name': _format_user_shortname_with_team(mgr)
|
||||
})
|
||||
managers_json = json.dumps(managers_data)
|
||||
|
||||
|
|
@ -356,6 +356,103 @@ def index(request):
|
|||
list(m_thematics),
|
||||
))
|
||||
|
||||
# Build role-persons data (Gestionnaire / Contrôleur / Inspecteur)
|
||||
# 1. Managers
|
||||
user_access_map = defaultdict(set)
|
||||
for uca in UserContractAccess.objects.filter(contract__in=contracts_qs, can_approve=True):
|
||||
user_access_map[uca.user_config.user_id].add(uca.contract_id)
|
||||
managed_itv_map = defaultdict(set)
|
||||
for itv_data in Intervention.objects.filter(contract__in=contracts_qs, intervention_manager__isnull=False).values('intervention_manager_id', 'contract_id').distinct():
|
||||
managed_itv_map[itv_data['intervention_manager_id']].add(itv_data['contract_id'])
|
||||
|
||||
manager_users_qs = (
|
||||
User.objects.filter(
|
||||
Q(config__usercontractaccess__contract__in=contracts_qs, config__usercontractaccess__can_approve=True) |
|
||||
Q(managed_interventions__contract__in=contracts_qs) |
|
||||
Q(config__roles__name='manager')
|
||||
)
|
||||
.prefetch_related('companymember_set__teams', 'config__company')
|
||||
.distinct()
|
||||
.order_by('first_name', 'last_name', 'username')
|
||||
)
|
||||
manager_items = []
|
||||
for mgr in manager_users_qs:
|
||||
c_ids = list(user_access_map[mgr.id] | managed_itv_map[mgr.id])
|
||||
manager_items.append({
|
||||
'id': mgr.id,
|
||||
'name': _format_user_shortname_with_team(mgr),
|
||||
'contracts': c_ids,
|
||||
})
|
||||
|
||||
# 2. Controllers
|
||||
controller_members_qs = (
|
||||
CompanyMember.objects.filter(
|
||||
Q(teams__operation_types__contains=[CompanyTeam.OPERATION_CONTROLS]) |
|
||||
Q(controlled_interventions__isnull=False)
|
||||
)
|
||||
)
|
||||
if allowed_company_ids is not None:
|
||||
controller_members_qs = controller_members_qs.filter(teams__company_id__in=allowed_company_ids)
|
||||
if allowed_thematic_ids:
|
||||
controller_members_qs = controller_members_qs.filter(teams__thematics__id__in=allowed_thematic_ids)
|
||||
controller_members_qs = (
|
||||
controller_members_qs
|
||||
.select_related('user')
|
||||
.prefetch_related('teams__company')
|
||||
.distinct()
|
||||
.order_by('name')
|
||||
)
|
||||
controller_items = [
|
||||
{
|
||||
'id': m.id,
|
||||
'name': _format_member_shortname_with_team(m),
|
||||
}
|
||||
for m in controller_members_qs
|
||||
]
|
||||
|
||||
# 3. Inspectors
|
||||
from inspections.models import Inspection
|
||||
inspector_users_qs = (
|
||||
User.objects.filter(
|
||||
Q(config__userthematics__can_inspect=True) |
|
||||
Q(inspections__isnull=False) |
|
||||
Q(companymember__teams__operation_types__contains=[CompanyTeam.OPERATION_INSPECTIONS]) |
|
||||
Q(companymember__assigned_inspections__isnull=False)
|
||||
)
|
||||
.prefetch_related('companymember_set__teams', 'config__company')
|
||||
.distinct()
|
||||
.order_by('first_name', 'last_name', 'username')
|
||||
)
|
||||
inspector_items = [
|
||||
{
|
||||
'id': u.id,
|
||||
'name': _format_user_shortname_with_team(u),
|
||||
}
|
||||
for u in inspector_users_qs
|
||||
]
|
||||
|
||||
role_persons_data = {
|
||||
'manager': manager_items,
|
||||
'controller': controller_items,
|
||||
'inspector': inspector_items,
|
||||
}
|
||||
|
||||
selected_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
|
||||
selected_controllers = _expand_csv_params(request.GET.getlist('assigned_controller'))
|
||||
selected_inspectors = _expand_csv_params(request.GET.getlist('inspector'))
|
||||
|
||||
selected_role = ''
|
||||
selected_person_id = ''
|
||||
if selected_managers:
|
||||
selected_role = 'manager'
|
||||
selected_person_id = str(selected_managers[0])
|
||||
elif selected_controllers:
|
||||
selected_role = 'controller'
|
||||
selected_person_id = str(selected_controllers[0])
|
||||
elif selected_inspectors:
|
||||
selected_role = 'inspector'
|
||||
selected_person_id = str(selected_inspectors[0])
|
||||
|
||||
# Status classes (Bootstrap contextual classes)
|
||||
status_classes = {
|
||||
'in_preparation': 'secondary',
|
||||
|
|
@ -415,7 +512,12 @@ def index(request):
|
|||
'selected_teams': request.GET.getlist('assigned_team'),
|
||||
'selected_members': request.GET.getlist('assigned_member'),
|
||||
'selected_control_teams': request.GET.getlist('assigned_control_team'),
|
||||
'selected_controllers': request.GET.getlist('assigned_controller'),
|
||||
'selected_controllers': selected_controllers,
|
||||
'selected_managers': selected_managers,
|
||||
'selected_inspectors': selected_inspectors,
|
||||
'selected_role': selected_role,
|
||||
'selected_person_id': selected_person_id,
|
||||
'role_persons_data': role_persons_data,
|
||||
'selected_pause_reasons': pause_reasons,
|
||||
'selected_preparation_states': preparation_states,
|
||||
'begin_date_min': begin_date_min or '',
|
||||
|
|
@ -471,6 +573,7 @@ def interventions_geojson(request):
|
|||
assigned_controllers = _expand_csv_params(request.GET.getlist('assigned_controller'))
|
||||
pause_reasons = _expand_csv_params(request.GET.getlist('pause_reason'))
|
||||
intervention_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
|
||||
inspectors = _expand_csv_params(request.GET.getlist('inspector'))
|
||||
preparation_states = _expand_csv_params(request.GET.getlist('preparation_state'))
|
||||
begin_date_min = request.GET.get('begin_date_min') or None
|
||||
begin_date_max = request.GET.get('begin_date_max') or None
|
||||
|
|
@ -555,6 +658,21 @@ def interventions_geojson(request):
|
|||
qs = qs.filter(Q(assigned_control_team_id__in=assigned_control_teams) | Q(assigned_controller__teams__in=assigned_control_teams)).distinct()
|
||||
if intervention_managers:
|
||||
qs = qs.filter(intervention_manager_id__in=intervention_managers)
|
||||
if inspectors:
|
||||
from inspections.models import Inspection
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
ct = ContentType.objects.get_for_model(Intervention)
|
||||
insp_target_qs = Inspection.objects.filter(
|
||||
Q(inspector_id__in=inspectors) |
|
||||
Q(assigned_member_id__in=inspectors) |
|
||||
Q(assigned_member__user_id__in=inspectors)
|
||||
)
|
||||
qs = qs.filter(
|
||||
Q(quality_inspections__in=insp_target_qs) |
|
||||
Q(generated_inspections__in=insp_target_qs) |
|
||||
Q(id__in=insp_target_qs.filter(asset_content_type=ct).values('asset_object_id')) |
|
||||
(Q(type='inspection') & (Q(assigned_member_id__in=inspectors) | Q(assigned_member__user_id__in=inspectors)))
|
||||
).distinct()
|
||||
if followed_only == '1' and request.user.is_authenticated:
|
||||
qs = qs.filter(subscriptions__user=request.user)
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ __all__ = [
|
|||
'_flatten_asset_collection',
|
||||
'_force_delete_operations_for_assets',
|
||||
'_format_location_label',
|
||||
'_format_member_shortname_with_team',
|
||||
'_format_user_shortname_with_team',
|
||||
'_get_allowed_asset_category_ids',
|
||||
'_get_asset_classes',
|
||||
'_get_asset_model_classes',
|
||||
|
|
@ -1765,7 +1767,10 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
|
|||
contracts = _expand_csv_params(params.getlist('contract'))
|
||||
assigned_teams = _expand_csv_params(params.getlist('assigned_team'))
|
||||
assigned_members = _expand_csv_params(params.getlist('assigned_member'))
|
||||
assigned_control_teams = _expand_csv_params(params.getlist('assigned_control_team'))
|
||||
assigned_controllers = _expand_csv_params(params.getlist('assigned_controller'))
|
||||
intervention_managers = _expand_csv_params(params.getlist('intervention_manager'))
|
||||
inspectors = _expand_csv_params(params.getlist('inspector'))
|
||||
pause_reasons = _expand_csv_params(params.getlist('pause_reason'))
|
||||
preparation_states = _expand_csv_params(params.getlist('preparation_state'))
|
||||
begin_date_min = params.get('begin_date_min') or None
|
||||
|
|
@ -1812,8 +1817,27 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
|
|||
qs = qs.filter(assigned_member_id__in=assigned_members)
|
||||
if assigned_teams:
|
||||
qs = qs.filter(Q(assigned_team_id__in=assigned_teams) | Q(assigned_member__teams__in=assigned_teams)).distinct()
|
||||
if assigned_controllers:
|
||||
qs = qs.filter(assigned_controller_id__in=assigned_controllers)
|
||||
if assigned_control_teams:
|
||||
qs = qs.filter(Q(assigned_control_team_id__in=assigned_control_teams) | Q(assigned_controller__teams__in=assigned_control_teams)).distinct()
|
||||
if intervention_managers:
|
||||
qs = qs.filter(intervention_manager_id__in=intervention_managers)
|
||||
if inspectors:
|
||||
from inspections.models import Inspection
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
ct = ContentType.objects.get_for_model(Intervention)
|
||||
insp_target_qs = Inspection.objects.filter(
|
||||
Q(inspector_id__in=inspectors) |
|
||||
Q(assigned_member_id__in=inspectors) |
|
||||
Q(assigned_member__user_id__in=inspectors)
|
||||
)
|
||||
qs = qs.filter(
|
||||
Q(quality_inspections__in=insp_target_qs) |
|
||||
Q(generated_inspections__in=insp_target_qs) |
|
||||
Q(id__in=insp_target_qs.filter(asset_content_type=ct).values('asset_object_id')) |
|
||||
(Q(type='inspection') & (Q(assigned_member_id__in=inspectors) | Q(assigned_member__user_id__in=inspectors)))
|
||||
).distinct()
|
||||
if followed_only == '1' and user and user.is_authenticated:
|
||||
qs = qs.filter(subscriptions__user=user)
|
||||
|
||||
|
|
@ -2387,6 +2411,54 @@ def _get_user_short_name(user):
|
|||
return display
|
||||
|
||||
|
||||
def _format_user_shortname_with_team(user):
|
||||
"""
|
||||
Format a User's short name with team/company in parentheses.
|
||||
E.g., "Prénom N. (Nom Équipe)" or "Prénom N."
|
||||
"""
|
||||
if not user:
|
||||
return ""
|
||||
short_name = _get_user_short_name(user)
|
||||
teams = [
|
||||
t.name
|
||||
for cm in user.companymember_set.all()
|
||||
for t in cm.teams.all()
|
||||
if t.name
|
||||
]
|
||||
if teams:
|
||||
team_str = ", ".join(dict.fromkeys(teams))
|
||||
elif hasattr(user, 'config') and user.config and user.config.company:
|
||||
team_str = user.config.company.name
|
||||
else:
|
||||
team_str = ""
|
||||
return f"{short_name} ({team_str})" if team_str else short_name
|
||||
|
||||
|
||||
def _format_member_shortname_with_team(member):
|
||||
"""
|
||||
Format a CompanyMember's short name with team in parentheses.
|
||||
E.g., "Prénom N. (Nom Équipe)" or "Prénom N."
|
||||
"""
|
||||
if not member:
|
||||
return ""
|
||||
if member.user and (member.user.first_name or member.user.last_name):
|
||||
short_name = _get_user_short_name(member.user)
|
||||
elif member.name:
|
||||
parts = member.name.strip().split()
|
||||
if len(parts) >= 2 and parts[-1].isalpha():
|
||||
short_name = f"{parts[0]} {parts[-1][0].upper()}."
|
||||
else:
|
||||
short_name = member.name
|
||||
elif member.user:
|
||||
short_name = getattr(member.user, 'username', '')
|
||||
else:
|
||||
short_name = ""
|
||||
|
||||
teams = [t.name for t in member.teams.all() if t.name]
|
||||
team_str = ", ".join(dict.fromkeys(teams)) if teams else ""
|
||||
return f"{short_name} ({team_str})" if team_str else short_name
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_available_occupation_members(user, intervention):
|
||||
|
|
|
|||
Loading…
Reference in a new issue