feat: implement authorization request module including forms, templates, and view logic
This commit is contained in:
parent
787799cf11
commit
64e93d7ae4
4 changed files with 294 additions and 10 deletions
|
|
@ -134,8 +134,38 @@ class AuthorizationFilterForm(forms.Form):
|
||||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}),
|
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
agent = forms.MultipleChoiceField(
|
||||||
|
label=_("Agent traitant"),
|
||||||
|
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
search_text = forms.CharField(
|
search_text = forms.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Recherche…')}),
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Recherche…')}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db.models import Q
|
||||||
|
from common.utils import get_short_name
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
agent_ids = RoadAuthorization.objects.exclude(agent__isnull=True).values_list('agent_id', flat=True)
|
||||||
|
agent_qs = User.objects.filter(
|
||||||
|
Q(id__in=agent_ids) |
|
||||||
|
Q(config__accessible_views__code='authorizations') |
|
||||||
|
Q(is_superuser=True)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
agent_choices = [
|
||||||
|
('unassigned', _("Non assigné")),
|
||||||
|
]
|
||||||
|
user_list = [
|
||||||
|
(str(u.pk), get_short_name(u)) for u in agent_qs
|
||||||
|
]
|
||||||
|
user_list.sort(key=lambda x: x[1].lower())
|
||||||
|
agent_choices.extend(user_list)
|
||||||
|
self.fields['agent'].choices = agent_choices
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n static %}
|
{% load i18n static short_name %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet">
|
||||||
|
|
@ -63,13 +63,13 @@
|
||||||
<form method="get" class="card mb-3 shadow-sm">
|
<form method="get" class="card mb-3 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 align-items-end">
|
<div class="row g-3 align-items-end">
|
||||||
<div class="col-md-4">
|
<div class="col-xl-3 col-md-4">
|
||||||
<label for="{{ filter_form.search_text.id_for_label }}" class="form-label small fw-semibold">
|
<label for="{{ filter_form.search_text.id_for_label }}" class="form-label small fw-semibold">
|
||||||
{% translate "Recherche" %}
|
{% translate "Recherche" %}
|
||||||
</label>
|
</label>
|
||||||
{{ filter_form.search_text }}
|
{{ filter_form.search_text }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-xl-3 col-md-4">
|
||||||
<label class="form-label small fw-semibold">{% translate "Type" %}</label>
|
<label class="form-label small fw-semibold">{% translate "Type" %}</label>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-light border w-100 text-start d-flex justify-content-between align-items-center dropdown-toggle"
|
<button class="btn btn-light border w-100 text-start d-flex justify-content-between align-items-center dropdown-toggle"
|
||||||
|
|
@ -108,7 +108,46 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl-2 col-md-4">
|
||||||
|
<label class="form-label small fw-semibold">{% translate "Agent" %}</label>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-light border w-100 text-start d-flex justify-content-between align-items-center dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
id="agentDropdownMenuButton"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-auto-close="outside"
|
||||||
|
aria-expanded="false"
|
||||||
|
style="background-color: #fff; border-color: #dee2e6; color: #212529; font-size: 0.875rem; padding: 0.4375rem 0.75rem; border-radius: 6px; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;">
|
||||||
|
<span id="selected-agents-label" class="text-truncate me-2">{% translate "Tous les agents" %}</span>
|
||||||
|
<span class="badge rounded-pill bg-primary d-none" id="selected-agents-badge">0</span>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu p-3 shadow-lg" aria-labelledby="agentDropdownMenuButton" style="max-height: 350px; overflow-y: auto; min-width: 280px; border-radius: 10px; border: 1px solid rgba(0,0,0,0.08); z-index: 1050;">
|
||||||
|
<div class="mb-2 pb-2 border-bottom d-flex align-items-center justify-content-between gap-2">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted" style="font-size: 0.8rem;"></i></span>
|
||||||
|
<input type="text" id="agent-search-input" class="form-control bg-light border-start-0 ps-0" placeholder="{% translate 'Rechercher un agent...' %}" style="font-size: 0.8rem;">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-link btn-xs p-0 text-decoration-none text-danger small flex-shrink-0" id="clear-agents-btn" style="font-size: 0.75rem;">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="agent-choices-list">
|
||||||
|
{% for choice in filter_form.agent %}
|
||||||
|
<div class="form-check dropdown-item-checkbox p-2 rounded d-flex align-items-center" style="transition: background-color 0.2s; margin-bottom: 2px;">
|
||||||
|
{{ choice.tag }}
|
||||||
|
<label class="form-check-label ms-2 cursor-pointer w-100 text-truncate" for="{{ choice.id_for_label }}" style="font-size: 0.85rem; color: #495057; cursor: pointer; user-select: none;">
|
||||||
|
{{ choice.choice_label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div id="agent-no-results" class="text-center text-muted py-3 d-none" style="font-size: 0.8rem;">
|
||||||
|
{% translate "Aucun agent correspondant" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
<div class="form-check mt-2">
|
<div class="form-check mt-2">
|
||||||
{{ filter_form.include_archives }}
|
{{ filter_form.include_archives }}
|
||||||
<label class="form-check-label" for="{{ filter_form.include_archives.id_for_label }}">
|
<label class="form-check-label" for="{{ filter_form.include_archives.id_for_label }}">
|
||||||
|
|
@ -116,7 +155,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 d-flex gap-2">
|
<div class="col-xl-2 col-md-6 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
<i class="bi bi-search"></i> {% translate "Filtrer" %}
|
<i class="bi bi-search"></i> {% translate "Filtrer" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -139,6 +178,7 @@
|
||||||
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Type" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Type" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
||||||
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Statut" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Statut" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
||||||
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Demandeur" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Demandeur" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
||||||
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Agent" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
||||||
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Localisation" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Localisation" %} <i class="bi bi-arrow-down-up ms-1 text-muted" style="font-size: 0.8rem;"></i></th>
|
||||||
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Date de création" %} <i class="bi bi-arrow-down ms-1 text-primary" style="font-size: 0.8rem;"></i></th>
|
<th class="sortable" style="cursor: pointer; user-select: none;">{% translate "Date de création" %} <i class="bi bi-arrow-down ms-1 text-primary" style="font-size: 0.8rem;"></i></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
|
@ -163,6 +203,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td data-sort="{{ auth.applicant_name }}">{{ auth.applicant_name }}</td>
|
<td data-sort="{{ auth.applicant_name }}">{{ auth.applicant_name }}</td>
|
||||||
|
<td data-sort="{{ auth.agent|short_name|default:'' }}">{{ auth.agent|short_name|default:"—" }}</td>
|
||||||
<td data-sort="{{ auth.location }}" class="text-truncate" style="max-width: 200px;" title="{{ auth.location }}">{{ auth.location }}</td>
|
<td data-sort="{{ auth.location }}" class="text-truncate" style="max-width: 200px;" title="{{ auth.location }}">{{ auth.location }}</td>
|
||||||
<td data-sort="{{ auth.creation_date|date:'Y-m-d-H-i-s' }}">{{ auth.creation_date|date:"d/m/Y" }}</td>
|
<td data-sort="{{ auth.creation_date|date:'Y-m-d-H-i-s' }}">{{ auth.creation_date|date:"d/m/Y" }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -173,7 +214,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="text-center text-muted py-4">
|
<td colspan="8" class="text-center text-muted py-4">
|
||||||
{% translate "Aucune demande trouvée." %}
|
{% translate "Aucune demande trouvée." %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -192,6 +233,7 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Type dropdown management
|
||||||
const checkboxes = document.querySelectorAll('#type-choices-list input[type="checkbox"]');
|
const checkboxes = document.querySelectorAll('#type-choices-list input[type="checkbox"]');
|
||||||
const label = document.getElementById('selected-types-label');
|
const label = document.getElementById('selected-types-label');
|
||||||
const badge = document.getElementById('selected-types-badge');
|
const badge = document.getElementById('selected-types-badge');
|
||||||
|
|
@ -268,10 +310,87 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent dropdown management
|
||||||
|
const agentCheckboxes = document.querySelectorAll('#agent-choices-list input[type="checkbox"]');
|
||||||
|
const agentLabel = document.getElementById('selected-agents-label');
|
||||||
|
const agentBadge = document.getElementById('selected-agents-badge');
|
||||||
|
const clearAgentsBtn = document.getElementById('clear-agents-btn');
|
||||||
|
const agentSearchInput = document.getElementById('agent-search-input');
|
||||||
|
const agentNoResults = document.getElementById('agent-no-results');
|
||||||
|
|
||||||
|
function updateAgentDropdownLabel() {
|
||||||
|
const checked = Array.from(agentCheckboxes).filter(cb => cb.checked);
|
||||||
|
const count = checked.length;
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
agentLabel.textContent = "{% translate 'Tous les agents' %}";
|
||||||
|
agentBadge.classList.add('d-none');
|
||||||
|
} else if (count === 1) {
|
||||||
|
const labelText = checked[0].nextElementSibling.textContent.trim();
|
||||||
|
agentLabel.textContent = labelText;
|
||||||
|
agentBadge.textContent = '1';
|
||||||
|
agentBadge.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
agentLabel.textContent = `${count} {% translate 'agents sélectionnés' %}`;
|
||||||
|
agentBadge.textContent = count;
|
||||||
|
agentBadge.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAgentDropdownLabel();
|
||||||
|
|
||||||
|
agentCheckboxes.forEach(cb => {
|
||||||
|
cb.addEventListener('change', updateAgentDropdownLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clearAgentsBtn) {
|
||||||
|
clearAgentsBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
agentCheckboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
updateAgentDropdownLabel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentSearchInput) {
|
||||||
|
agentSearchInput.addEventListener('input', () => {
|
||||||
|
const query = agentSearchInput.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
const items = document.querySelectorAll('#agent-choices-list .dropdown-item-checkbox');
|
||||||
|
items.forEach(item => {
|
||||||
|
const labelText = item.querySelector('.form-check-label').textContent.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||||
|
if (labelText.includes(query)) {
|
||||||
|
item.classList.remove('d-none');
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
item.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
agentNoResults.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
agentNoResults.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
agentSearchInput.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
agentSearchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic table sorting
|
// Dynamic table sorting
|
||||||
const headers = document.querySelectorAll('th.sortable');
|
const headers = document.querySelectorAll('th.sortable');
|
||||||
const tbody = document.querySelector('table tbody');
|
const tbody = document.querySelector('table tbody');
|
||||||
let currentSortedColIndex = 5; // Date de création index
|
let currentSortedColIndex = 6; // Date de création index
|
||||||
let currentAscending = false; // descending by default (date de création desc)
|
let currentAscending = false; // descending by default (date de création desc)
|
||||||
|
|
||||||
headers.forEach((header) => {
|
headers.forEach((header) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from authorizations.models import RoadAuthorization, RoadAuthorizationDate
|
from authorizations.models import RoadAuthorization, RoadAuthorizationDate
|
||||||
|
from common.models import UserConfig, AppView
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class RoadAuthorizationDateTest(TestCase):
|
class RoadAuthorizationDateTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -47,3 +54,116 @@ class RoadAuthorizationDateTest(TestCase):
|
||||||
end_date=end
|
end_date=end
|
||||||
)
|
)
|
||||||
self.assertEqual(str(auth_date), "— → 29/06/2026 18:00")
|
self.assertEqual(str(auth_date), "— → 29/06/2026 18:00")
|
||||||
|
|
||||||
|
|
||||||
|
class RoadAuthorizationListAgentTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
poly = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)))
|
||||||
|
self.multipoly = MultiPolygon(poly, srid=4326)
|
||||||
|
|
||||||
|
# Users
|
||||||
|
self.staff_user = User.objects.create_user(
|
||||||
|
username="staff_auth",
|
||||||
|
password="password123",
|
||||||
|
first_name="Staff",
|
||||||
|
last_name="Member"
|
||||||
|
)
|
||||||
|
self.config = UserConfig.objects.create(user=self.staff_user, is_intern=True)
|
||||||
|
self.auth_view, _ = AppView.objects.get_or_create(
|
||||||
|
code="authorizations",
|
||||||
|
defaults={"name_fr": "Autorisations", "name_nl": "Autorisaties", "url_name": "authorizations:authorization_list"}
|
||||||
|
)
|
||||||
|
self.config.accessible_views.add(self.auth_view)
|
||||||
|
|
||||||
|
self.agent1 = User.objects.create_user(
|
||||||
|
username="adupont",
|
||||||
|
password="password123",
|
||||||
|
first_name="Alice",
|
||||||
|
last_name="Dupont"
|
||||||
|
)
|
||||||
|
self.agent2 = User.objects.create_user(
|
||||||
|
username="bsmith",
|
||||||
|
password="password123",
|
||||||
|
first_name="Bob",
|
||||||
|
last_name="Smith"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Authorizations
|
||||||
|
self.auth1 = RoadAuthorization.objects.create(
|
||||||
|
type=2,
|
||||||
|
description="Terrasse Alice",
|
||||||
|
applicant_name="Demandeur Alpha",
|
||||||
|
applicant_email="alpha@example.com",
|
||||||
|
applicant_address="Rue 1",
|
||||||
|
location="Bruxelles Centre",
|
||||||
|
geom=self.multipoly,
|
||||||
|
agent=self.agent1
|
||||||
|
)
|
||||||
|
self.auth2 = RoadAuthorization.objects.create(
|
||||||
|
type=21,
|
||||||
|
description="Échafaudage Bob",
|
||||||
|
applicant_name="Demandeur Beta",
|
||||||
|
applicant_email="beta@example.com",
|
||||||
|
applicant_address="Rue 2",
|
||||||
|
location="Ixelles",
|
||||||
|
geom=self.multipoly,
|
||||||
|
agent=self.agent2
|
||||||
|
)
|
||||||
|
self.auth3 = RoadAuthorization.objects.create(
|
||||||
|
type=22,
|
||||||
|
description="Conteneur Non Assigné",
|
||||||
|
applicant_name="Demandeur Gamma",
|
||||||
|
applicant_email="gamma@example.com",
|
||||||
|
applicant_address="Rue 3",
|
||||||
|
location="Schaerbeek",
|
||||||
|
geom=self.multipoly,
|
||||||
|
agent=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(username="staff_auth", password="password123")
|
||||||
|
self.url = reverse('authorizations:authorization_list')
|
||||||
|
|
||||||
|
def test_list_view_displays_agent_short_names(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
|
||||||
|
# Alice Dupont -> Alice D.
|
||||||
|
self.assertIn("Alice D.", content)
|
||||||
|
# Bob Smith -> Bob S.
|
||||||
|
self.assertIn("Bob S.", content)
|
||||||
|
# Auth3 has no agent -> should display dash "—"
|
||||||
|
self.assertIn("—", content)
|
||||||
|
|
||||||
|
def test_filter_by_specific_agent(self):
|
||||||
|
response = self.client.get(self.url, {'agent': [str(self.agent1.pk)]})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
auths = list(response.context['authorizations'])
|
||||||
|
self.assertIn(self.auth1, auths)
|
||||||
|
self.assertNotIn(self.auth2, auths)
|
||||||
|
self.assertNotIn(self.auth3, auths)
|
||||||
|
|
||||||
|
def test_filter_by_unassigned_agent(self):
|
||||||
|
response = self.client.get(self.url, {'agent': ['unassigned']})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
auths = list(response.context['authorizations'])
|
||||||
|
self.assertIn(self.auth3, auths)
|
||||||
|
self.assertNotIn(self.auth1, auths)
|
||||||
|
self.assertNotIn(self.auth2, auths)
|
||||||
|
|
||||||
|
def test_filter_by_multiple_agents(self):
|
||||||
|
response = self.client.get(self.url, {'agent': [str(self.agent1.pk), 'unassigned']})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
auths = list(response.context['authorizations'])
|
||||||
|
self.assertIn(self.auth1, auths)
|
||||||
|
self.assertIn(self.auth3, auths)
|
||||||
|
self.assertNotIn(self.auth2, auths)
|
||||||
|
|
||||||
|
def test_search_by_agent_name(self):
|
||||||
|
response = self.client.get(self.url, {'search_text': 'Alice'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
auths = list(response.context['authorizations'])
|
||||||
|
self.assertIn(self.auth1, auths)
|
||||||
|
self.assertNotIn(self.auth2, auths)
|
||||||
|
self.assertNotIn(self.auth3, auths)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,12 +330,12 @@ def authorization_list(request):
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
filter_form = AuthorizationFilterForm(request.GET or None)
|
filter_form = AuthorizationFilterForm(request.GET or None)
|
||||||
qs = RoadAuthorization.objects.prefetch_related('dates', 'documents').order_by('-creation_date')
|
qs = RoadAuthorization.objects.select_related('agent').prefetch_related('dates', 'documents').order_by('-creation_date')
|
||||||
|
|
||||||
if filter_form.is_valid():
|
if filter_form.is_valid():
|
||||||
data = filter_form.cleaned_data
|
data = filter_form.cleaned_data
|
||||||
|
|
||||||
# Text search over description, location, applicant fields and code
|
# Text search over description, location, applicant fields, code and agent
|
||||||
search = data.get('search_text', '').strip()
|
search = data.get('search_text', '').strip()
|
||||||
if search:
|
if search:
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
@ -344,7 +344,10 @@ def authorization_list(request):
|
||||||
Q(location__icontains=search) |
|
Q(location__icontains=search) |
|
||||||
Q(applicant_name__icontains=search) |
|
Q(applicant_name__icontains=search) |
|
||||||
Q(applicant_email__icontains=search) |
|
Q(applicant_email__icontains=search) |
|
||||||
Q(code__icontains=search)
|
Q(code__icontains=search) |
|
||||||
|
Q(agent__first_name__icontains=search) |
|
||||||
|
Q(agent__last_name__icontains=search) |
|
||||||
|
Q(agent__username__icontains=search)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Type filter
|
# Type filter
|
||||||
|
|
@ -352,6 +355,18 @@ def authorization_list(request):
|
||||||
if types:
|
if types:
|
||||||
qs = qs.filter(type__in=types)
|
qs = qs.filter(type__in=types)
|
||||||
|
|
||||||
|
# Agent filter
|
||||||
|
agents = data.get('agent')
|
||||||
|
if agents:
|
||||||
|
from django.db.models import Q
|
||||||
|
agent_q = Q()
|
||||||
|
if 'unassigned' in agents:
|
||||||
|
agent_q |= Q(agent__isnull=True)
|
||||||
|
user_agent_ids = [a for a in agents if a != 'unassigned']
|
||||||
|
if user_agent_ids:
|
||||||
|
agent_q |= Q(agent_id__in=user_agent_ids)
|
||||||
|
qs = qs.filter(agent_q)
|
||||||
|
|
||||||
# Archive filter: exclude entries whose latest end_date is > 2 years ago
|
# Archive filter: exclude entries whose latest end_date is > 2 years ago
|
||||||
if not data.get('include_archives'):
|
if not data.get('include_archives'):
|
||||||
cutoff = datetime.now().astimezone() - relativedelta(years=2)
|
cutoff = datetime.now().astimezone() - relativedelta(years=2)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue