feat: implement granular intervention permissions and support batch field updates via JSON API
This commit is contained in:
parent
cde2c7e52f
commit
cfd11654ab
4 changed files with 129 additions and 8 deletions
|
|
@ -1023,11 +1023,36 @@ def can_edit_intervention(user, intervention, user_config=None):
|
||||||
thematic=thematic,
|
thematic=thematic,
|
||||||
).first()
|
).first()
|
||||||
if ut:
|
if ut:
|
||||||
return UserThematicStatusPermission.objects.filter(
|
if UserThematicStatusPermission.objects.filter(
|
||||||
user_thematic=ut,
|
user_thematic=ut,
|
||||||
status=status,
|
status=status,
|
||||||
can_edit=True,
|
can_edit=True,
|
||||||
).exists()
|
).exists():
|
||||||
|
return True
|
||||||
|
# Si pas de contrat et pas de permission de statut thématique explicite :
|
||||||
|
if contract is None:
|
||||||
|
# 1. Le créateur de l'intervention peut toujours la modifier s'il n'y a pas de contrat
|
||||||
|
if intervention.created_by_id == user.id:
|
||||||
|
return True
|
||||||
|
# 2. Utilisateur ayant des droits d'édition sur au moins un contrat de la thématique
|
||||||
|
if UserContractStatusPermission.objects.filter(
|
||||||
|
user_contract__user_config=user_config,
|
||||||
|
user_contract__contract__thematics=thematic,
|
||||||
|
status=status,
|
||||||
|
can_edit=True,
|
||||||
|
).exists():
|
||||||
|
return True
|
||||||
|
# 3. Utilisateur ayant des droits d'édition sur au moins un contrat du prestataire assigné
|
||||||
|
if intervention.assigned_provider_id and UserContractStatusPermission.objects.filter(
|
||||||
|
user_contract__user_config=user_config,
|
||||||
|
user_contract__contract__company_id=intervention.assigned_provider_id,
|
||||||
|
status=status,
|
||||||
|
can_edit=True,
|
||||||
|
).exists():
|
||||||
|
return True
|
||||||
|
# 4. Si l'utilisateur a can_edit_interventions sur la thématique pour les statuts initiaux
|
||||||
|
if ut.can_edit_interventions and status in ('in_preparation', 'to_be_approved', 'to_be_planned'):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,19 +144,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateField(interventionId, field, value) {
|
async function updateFieldsBatch(interventionId, payload) {
|
||||||
const response = await fetch(`/interventions/${interventionId}/update/`, {
|
const response = await fetch(`/interventions/${interventionId}/update/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({ field, value })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok || !data.success) {
|
if (!response.ok || !data.success) {
|
||||||
throw new Error(data.error || gettext('Une erreur est survenue.'));
|
const errMsg = data.error || (data.errors && data.errors.join(', ')) || gettext('Une erreur est survenue.');
|
||||||
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
@ -227,11 +228,16 @@
|
||||||
if (spinnerEl) spinnerEl.classList.remove('d-none');
|
if (spinnerEl) spinnerEl.classList.remove('d-none');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const payload = {};
|
||||||
if (providerChanged) {
|
if (providerChanged) {
|
||||||
await updateField(interventionId, 'assigned_provider', providerValue);
|
payload.assigned_provider = providerValue;
|
||||||
}
|
}
|
||||||
if (contractChanged) {
|
if (contractChanged) {
|
||||||
await updateField(interventionId, 'contract', contractValue);
|
payload.contract = contractValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(payload).length > 0) {
|
||||||
|
await updateFieldsBatch(interventionId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerDisplay && providerSelect) {
|
if (providerDisplay && providerSelect) {
|
||||||
|
|
|
||||||
|
|
@ -600,6 +600,94 @@ class GetContractsByProviderTests(TestCase):
|
||||||
self.assertIn("C-002", contract_numbers)
|
self.assertIn("C-002", contract_numbers)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickProviderContractUpdateTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from common.models import UserContractAccess, UserContractStatusPermission
|
||||||
|
self.User = get_user_model()
|
||||||
|
self.manager_role, _ = Role.objects.get_or_create(name='manager')
|
||||||
|
self.thematic = Thematic.objects.create(name_fr='Voirie Test', allow_planning_without_contract=True)
|
||||||
|
|
||||||
|
self.company1 = Company.objects.create(name='Company 1')
|
||||||
|
self.company2 = Company.objects.create(name='Company 2')
|
||||||
|
|
||||||
|
self.contract1 = Contract.objects.create(
|
||||||
|
contract_number="C-001",
|
||||||
|
company=self.company1,
|
||||||
|
start_date=timezone.now().date(),
|
||||||
|
end_date=timezone.now().date() + timedelta(days=365)
|
||||||
|
)
|
||||||
|
self.contract1.thematics.add(self.thematic)
|
||||||
|
|
||||||
|
self.contract2 = Contract.objects.create(
|
||||||
|
contract_number="C-002",
|
||||||
|
company=self.company2,
|
||||||
|
start_date=timezone.now().date(),
|
||||||
|
end_date=timezone.now().date() + timedelta(days=365)
|
||||||
|
)
|
||||||
|
self.contract2.thematics.add(self.thematic)
|
||||||
|
|
||||||
|
# User with limited contract access
|
||||||
|
self.user = self.User.objects.create_user(username='contract-manager', password='pwd')
|
||||||
|
self.user_config = UserConfig.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
is_intern=True,
|
||||||
|
limit_interventions_to_contracts=True,
|
||||||
|
)
|
||||||
|
self.user_config.roles.add(self.manager_role)
|
||||||
|
|
||||||
|
# User thematic without explicit UserThematicStatusPermission
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=self.user_config,
|
||||||
|
thematic=self.thematic,
|
||||||
|
can_view_interventions=True,
|
||||||
|
can_edit_interventions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access and edit permissions on both contracts for to_be_planned status
|
||||||
|
uca1 = UserContractAccess.objects.create(user_config=self.user_config, contract=self.contract1, can_view_interventions=True)
|
||||||
|
UserContractStatusPermission.objects.create(user_contract=uca1, status='to_be_planned', can_edit=True, can_view=True)
|
||||||
|
|
||||||
|
uca2 = UserContractAccess.objects.create(user_config=self.user_config, contract=self.contract2, can_view_interventions=True)
|
||||||
|
UserContractStatusPermission.objects.create(user_contract=uca2, status='to_be_planned', can_edit=True, can_view=True)
|
||||||
|
|
||||||
|
self.intervention = Intervention.objects.create(
|
||||||
|
title='Test Quick Edit Intervention',
|
||||||
|
thematic=self.thematic,
|
||||||
|
status='to_be_planned',
|
||||||
|
assigned_provider=self.company1,
|
||||||
|
contract=self.contract1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_batch_update_provider_and_contract(self):
|
||||||
|
from interventions.permissions import can_edit_intervention
|
||||||
|
self.assertTrue(can_edit_intervention(self.user, self.intervention))
|
||||||
|
|
||||||
|
self.client.login(username='contract-manager', password='pwd')
|
||||||
|
url = reverse('interventions:update_intervention', args=[self.intervention.id])
|
||||||
|
payload = {
|
||||||
|
'assigned_provider': str(self.company2.id),
|
||||||
|
'contract': str(self.contract2.id),
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
|
||||||
|
self.intervention.refresh_from_db()
|
||||||
|
self.assertEqual(self.intervention.assigned_provider, self.company2)
|
||||||
|
self.assertEqual(self.intervention.contract, self.contract2)
|
||||||
|
|
||||||
|
def test_can_edit_intervention_when_contract_is_none(self):
|
||||||
|
from interventions.permissions import can_edit_intervention
|
||||||
|
self.intervention.contract = None
|
||||||
|
self.intervention.assigned_provider = self.company2
|
||||||
|
self.intervention.save()
|
||||||
|
|
||||||
|
# User should still be allowed to edit because they have edit rights on contracts of this thematic/provider
|
||||||
|
self.assertTrue(can_edit_intervention(self.user, self.intervention))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2495,6 +2495,8 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
||||||
if not has_access:
|
if not has_access:
|
||||||
return False, _('You do not have access to this contract')
|
return False, _('You do not have access to this contract')
|
||||||
intervention.contract = contract
|
intervention.contract = contract
|
||||||
|
if contract.company and intervention.assigned_provider != contract.company:
|
||||||
|
intervention.assigned_provider = contract.company
|
||||||
return True, None
|
return True, None
|
||||||
return False, _('Contract not found')
|
return False, _('Contract not found')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue