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,
|
||||
).first()
|
||||
if ut:
|
||||
return UserThematicStatusPermission.objects.filter(
|
||||
if UserThematicStatusPermission.objects.filter(
|
||||
user_thematic=ut,
|
||||
status=status,
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -144,19 +144,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function updateField(interventionId, field, value) {
|
||||
async function updateFieldsBatch(interventionId, payload) {
|
||||
const response = await fetch(`/interventions/${interventionId}/update/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: new URLSearchParams({ field, value })
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
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;
|
||||
}
|
||||
|
|
@ -227,11 +228,16 @@
|
|||
if (spinnerEl) spinnerEl.classList.remove('d-none');
|
||||
|
||||
try {
|
||||
const payload = {};
|
||||
if (providerChanged) {
|
||||
await updateField(interventionId, 'assigned_provider', providerValue);
|
||||
payload.assigned_provider = providerValue;
|
||||
}
|
||||
if (contractChanged) {
|
||||
await updateField(interventionId, 'contract', contractValue);
|
||||
payload.contract = contractValue;
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length > 0) {
|
||||
await updateFieldsBatch(interventionId, payload);
|
||||
}
|
||||
|
||||
if (providerDisplay && providerSelect) {
|
||||
|
|
|
|||
|
|
@ -600,6 +600,94 @@ class GetContractsByProviderTests(TestCase):
|
|||
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:
|
||||
return False, _('You do not have access to this contract')
|
||||
intervention.contract = contract
|
||||
if contract.company and intervention.assigned_provider != contract.company:
|
||||
intervention.assigned_provider = contract.company
|
||||
return True, None
|
||||
return False, _('Contract not found')
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue