feat(contracts): add global permissions to view contract posts and prices on UserConfig
- Add can_view_all_posts and can_view_all_prices boolean fields to UserConfig - Add migration 0020 with automatic data migration for Legal Office users - Update permissions helpers, UserConfigAdmin, copy_user_config and intervention detail view - Add unit tests for global posts and prices permissions
This commit is contained in:
parent
9a85696d90
commit
b307791b90
10 changed files with 123 additions and 10 deletions
|
|
@ -620,7 +620,7 @@ class UserContractAccessAdmin(admin.ModelAdmin):
|
||||||
@admin.register(UserConfig)
|
@admin.register(UserConfig)
|
||||||
class UserConfigAdmin(admin.ModelAdmin):
|
class UserConfigAdmin(admin.ModelAdmin):
|
||||||
list_display = ("user", "display_contracts", "display_thematics", "display_views", "is_intern", "display_roles", "company", "display_member_teams", "duplicate_button")
|
list_display = ("user", "display_contracts", "display_thematics", "display_views", "is_intern", "display_roles", "company", "display_member_teams", "duplicate_button")
|
||||||
list_filter = ("thematics", "roles", "company", "user__companymember__teams", "contracts", "accessible_views", "auto_logout_hours")
|
list_filter = ("thematics", "roles", "company", "user__companymember__teams", "contracts", "can_view_all_posts", "can_view_all_prices", "accessible_views", "auto_logout_hours")
|
||||||
search_fields = ("user__username", "user__first_name", "user__last_name", "contracts__contract_number")
|
search_fields = ("user__username", "user__first_name", "user__last_name", "contracts__contract_number")
|
||||||
inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline]
|
inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline]
|
||||||
filter_horizontal = ("linked_configs", "accessible_views")
|
filter_horizontal = ("linked_configs", "accessible_views")
|
||||||
|
|
@ -637,6 +637,10 @@ class UserConfigAdmin(admin.ModelAdmin):
|
||||||
'fields': ('accessible_views',),
|
'fields': ('accessible_views',),
|
||||||
'description': "Configurer les vues auxquelles l'utilisateur a accès."
|
'description': "Configurer les vues auxquelles l'utilisateur a accès."
|
||||||
}),
|
}),
|
||||||
|
('Permissions globales / Contrats', {
|
||||||
|
'fields': ('can_view_all_posts', 'can_view_all_prices'),
|
||||||
|
'description': "Permet de consulter les prestations et/ou les prix sur tous les contrats sans configuration contractuelle individuelle."
|
||||||
|
}),
|
||||||
('eSign / Signalisation', {
|
('eSign / Signalisation', {
|
||||||
'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'),
|
'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'),
|
||||||
'description': "Permissions spécifiques pour l'application eSign / Signalisation."
|
'description': "Permissions spécifiques pour l'application eSign / Signalisation."
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ class Command(BaseCommand):
|
||||||
'hide_intervention_map_by_default': cfg.hide_intervention_map_by_default,
|
'hide_intervention_map_by_default': cfg.hide_intervention_map_by_default,
|
||||||
'auto_logout_hours': cfg.auto_logout_hours,
|
'auto_logout_hours': cfg.auto_logout_hours,
|
||||||
'can_add_unique_tag_documents': cfg.can_add_unique_tag_documents,
|
'can_add_unique_tag_documents': cfg.can_add_unique_tag_documents,
|
||||||
|
'can_view_all_posts': cfg.can_view_all_posts,
|
||||||
|
'can_view_all_prices': cfg.can_view_all_prices,
|
||||||
'roles': list(cfg.roles.values_list('name', flat=True)),
|
'roles': list(cfg.roles.values_list('name', flat=True)),
|
||||||
'accessible_views': list(cfg.accessible_views.values_list('code', flat=True)),
|
'accessible_views': list(cfg.accessible_views.values_list('code', flat=True)),
|
||||||
'linked_configs': list(cfg.linked_configs.values_list('user__username', flat=True)),
|
'linked_configs': list(cfg.linked_configs.values_list('user__username', flat=True)),
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,8 @@ class Command(BaseCommand):
|
||||||
'hide_intervention_map_by_default': entry.get('hide_intervention_map_by_default', False),
|
'hide_intervention_map_by_default': entry.get('hide_intervention_map_by_default', False),
|
||||||
'auto_logout_hours': entry.get('auto_logout_hours'),
|
'auto_logout_hours': entry.get('auto_logout_hours'),
|
||||||
'can_add_unique_tag_documents': entry.get('can_add_unique_tag_documents', False),
|
'can_add_unique_tag_documents': entry.get('can_add_unique_tag_documents', False),
|
||||||
|
'can_view_all_posts': entry.get('can_view_all_posts', False),
|
||||||
|
'can_view_all_prices': entry.get('can_view_all_prices', False),
|
||||||
'default_thematic': default_thematic,
|
'default_thematic': default_thematic,
|
||||||
'company': company_obj,
|
'company': company_obj,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-10-07 09:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_legal_office_posts_and_prices(apps, schema_editor):
|
||||||
|
UserConfig = apps.get_model('common', 'UserConfig')
|
||||||
|
Company = apps.get_model('contracts', 'Company')
|
||||||
|
legal_companies = Company.objects.filter(name__icontains="Legal Office")
|
||||||
|
if legal_companies.exists():
|
||||||
|
UserConfig.objects.filter(company__in=legal_companies).update(
|
||||||
|
can_view_all_posts=True,
|
||||||
|
can_view_all_prices=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0019_userthematics_can_edit_management_zones'),
|
||||||
|
('contracts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userconfig',
|
||||||
|
name='can_view_all_posts',
|
||||||
|
field=models.BooleanField(default=False, help_text="Permet à l'utilisateur de consulter les postes/prestations sur l'ensemble des contrats sans nécessiter d'accès contractuel explicite.", verbose_name='Peut voir toutes les prestations'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userconfig',
|
||||||
|
name='can_view_all_prices',
|
||||||
|
field=models.BooleanField(default=False, help_text="Permet à l'utilisateur de consulter les prix des postes sur l'ensemble des contrats sans nécessiter d'accès contractuel explicite.", verbose_name='Peut voir tous les prix'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_legal_office_posts_and_prices,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -305,6 +305,16 @@ class UserConfig(models.Model):
|
||||||
verbose_name="Peut ajouter des documents avec tags uniques",
|
verbose_name="Peut ajouter des documents avec tags uniques",
|
||||||
help_text="Permet aux utilisateurs externes d'ajouter/modifier des documents avec des tags uniques (CME, PIS, As-Built, etc.).",
|
help_text="Permet aux utilisateurs externes d'ajouter/modifier des documents avec des tags uniques (CME, PIS, As-Built, etc.).",
|
||||||
)
|
)
|
||||||
|
can_view_all_posts = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Peut voir toutes les prestations"),
|
||||||
|
help_text=_("Permet à l'utilisateur de consulter les postes/prestations sur l'ensemble des contrats sans nécessiter d'accès contractuel explicite."),
|
||||||
|
)
|
||||||
|
can_view_all_prices = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Peut voir tous les prix"),
|
||||||
|
help_text=_("Permet à l'utilisateur de consulter les prix des postes sur l'ensemble des contrats sans nécessiter d'accès contractuel explicite."),
|
||||||
|
)
|
||||||
esign_can_edit = models.BooleanField(
|
esign_can_edit = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name="eSign : Peut éditer",
|
verbose_name="eSign : Peut éditer",
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,10 @@ def copy_user_config(source_user_or_config: Any, target_user_or_config: Any, cop
|
||||||
dict(source_config.calendar_preferences) if source_config.calendar_preferences is not None else None
|
dict(source_config.calendar_preferences) if source_config.calendar_preferences is not None else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Permissions spécifiques (eSign, documents, panoramax, transport exceptionnel)
|
# Permissions spécifiques (eSign, documents, panoramax, transport exceptionnel, contrats)
|
||||||
target_config.can_add_unique_tag_documents = source_config.can_add_unique_tag_documents
|
target_config.can_add_unique_tag_documents = source_config.can_add_unique_tag_documents
|
||||||
|
target_config.can_view_all_posts = source_config.can_view_all_posts
|
||||||
|
target_config.can_view_all_prices = source_config.can_view_all_prices
|
||||||
target_config.esign_can_edit = source_config.esign_can_edit
|
target_config.esign_can_edit = source_config.esign_can_edit
|
||||||
target_config.esign_can_manage_rcp = source_config.esign_can_manage_rcp
|
target_config.esign_can_manage_rcp = source_config.esign_can_manage_rcp
|
||||||
target_config.esign_can_validate_rcp = source_config.esign_can_validate_rcp
|
target_config.esign_can_validate_rcp = source_config.esign_can_validate_rcp
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,17 @@ def user_has_access_to_contract(user, contract_id, for_assets=False):
|
||||||
|
|
||||||
def user_can_view_contract_prices(user, contract_id):
|
def user_can_view_contract_prices(user, contract_id):
|
||||||
"""Vrai si l'utilisateur est admin ou dispose du droit can_view_prices sur le contrat."""
|
"""Vrai si l'utilisateur est admin ou dispose du droit can_view_prices sur le contrat."""
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = user.config
|
config = user.config
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if getattr(config, 'can_view_all_prices', False):
|
||||||
|
return True
|
||||||
|
|
||||||
user_roles = set(config.roles.values_list("name", flat=True))
|
user_roles = set(config.roles.values_list("name", flat=True))
|
||||||
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
||||||
return False
|
return False
|
||||||
|
|
@ -102,9 +108,10 @@ def user_can_view_contract_prices(user, contract_id):
|
||||||
if "admin" in user_roles or "top_manager" in user_roles:
|
if "admin" in user_roles or "top_manager" in user_roles:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
cid = getattr(contract_id, 'id', contract_id)
|
||||||
return UserContractAccess.objects.filter(
|
return UserContractAccess.objects.filter(
|
||||||
user_config=config,
|
user_config=config,
|
||||||
contract_id=contract_id,
|
contract_id=cid,
|
||||||
can_view_prices=True
|
can_view_prices=True
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
@ -209,11 +216,17 @@ def user_has_access_to_contract_posts(user, contract_id):
|
||||||
"""
|
"""
|
||||||
Vérifie que l'utilisateur a accès aux postes du contrat.
|
Vérifie que l'utilisateur a accès aux postes du contrat.
|
||||||
"""
|
"""
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = user.config
|
config = user.config
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if getattr(config, 'can_view_all_posts', False):
|
||||||
|
return True
|
||||||
|
|
||||||
user_roles = set(config.roles.values_list("name", flat=True))
|
user_roles = set(config.roles.values_list("name", flat=True))
|
||||||
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
||||||
return False
|
return False
|
||||||
|
|
@ -221,9 +234,10 @@ def user_has_access_to_contract_posts(user, contract_id):
|
||||||
if "admin" in user_roles or "top_manager" in user_roles:
|
if "admin" in user_roles or "top_manager" in user_roles:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
cid = getattr(contract_id, 'id', contract_id)
|
||||||
return UserContractAccess.objects.filter(
|
return UserContractAccess.objects.filter(
|
||||||
user_config=config,
|
user_config=config,
|
||||||
contract_id=contract_id,
|
contract_id=cid,
|
||||||
can_view_posts=True
|
can_view_posts=True
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -723,3 +723,37 @@ class ContractGeojsonEndpointTests(TestCase):
|
||||||
self.assertEqual(len(data["features"]), 0)
|
self.assertEqual(len(data["features"]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalPostsAndPricesPermissionsTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(username="viewer_user", password="password123")
|
||||||
|
self.role_viewer, _ = Role.objects.get_or_create(name="viewer")
|
||||||
|
self.config = UserConfig.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
can_view_all_posts=False,
|
||||||
|
can_view_all_prices=False,
|
||||||
|
)
|
||||||
|
self.config.roles.add(self.role_viewer)
|
||||||
|
self.contract = Contract.objects.create(
|
||||||
|
contract_number="CTR-TEST-001",
|
||||||
|
description="Test Contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_viewer_cannot_view_posts_or_prices(self):
|
||||||
|
from contracts.permissions import user_can_view_contract_prices, user_has_access_to_contract_posts
|
||||||
|
self.assertFalse(user_can_view_contract_prices(self.user, self.contract.id))
|
||||||
|
self.assertFalse(user_has_access_to_contract_posts(self.user, self.contract.id))
|
||||||
|
|
||||||
|
def test_viewer_with_can_view_all_posts(self):
|
||||||
|
from contracts.permissions import user_has_access_to_contract_posts
|
||||||
|
self.config.can_view_all_posts = True
|
||||||
|
self.config.save()
|
||||||
|
self.assertTrue(user_has_access_to_contract_posts(self.user, self.contract.id))
|
||||||
|
|
||||||
|
def test_viewer_with_can_view_all_prices(self):
|
||||||
|
from contracts.permissions import user_can_view_contract_prices
|
||||||
|
self.config.can_view_all_prices = True
|
||||||
|
self.config.save()
|
||||||
|
self.assertTrue(user_can_view_contract_prices(self.user, self.contract.id))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,10 +220,13 @@ class ControlQuantityInlineFormSet(BaseInlineFormSet):
|
||||||
|
|
||||||
qs = Contract.objects.none()
|
qs = Contract.objects.none()
|
||||||
if user and hasattr(user, "config"):
|
if user and hasattr(user, "config"):
|
||||||
allowed_ids = UserContractAccess.objects.filter(
|
if getattr(user.config, 'can_view_all_posts', False):
|
||||||
user_config=user.config, can_view_posts=True
|
qs = Contract.objects.all().order_by("contract_number")
|
||||||
).values_list("contract_id", flat=True)
|
else:
|
||||||
qs = Contract.objects.filter(id__in=allowed_ids).order_by("contract_number")
|
allowed_ids = UserContractAccess.objects.filter(
|
||||||
|
user_config=user.config, can_view_posts=True
|
||||||
|
).values_list("contract_id", flat=True)
|
||||||
|
qs = Contract.objects.filter(id__in=allowed_ids).order_by("contract_number")
|
||||||
|
|
||||||
# Injecter le même queryset dans chaque form
|
# Injecter le même queryset dans chaque form
|
||||||
for form in self.forms:
|
for form in self.forms:
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ def intervention_detail(request, intervention_id):
|
||||||
can_check_intervention = True
|
can_check_intervention = True
|
||||||
|
|
||||||
# Filter the positions associated with this intervention
|
# Filter the positions associated with this intervention
|
||||||
if user_contract_access and user_contract_access.can_view_posts:
|
if getattr(user_config, 'can_view_all_posts', False) or (user_contract_access and user_contract_access.can_view_posts):
|
||||||
can_view_posts = True
|
can_view_posts = True
|
||||||
contract_posts = list(
|
contract_posts = list(
|
||||||
InterventionContractPost.objects.filter(intervention=intervention)
|
InterventionContractPost.objects.filter(intervention=intervention)
|
||||||
|
|
@ -702,7 +702,10 @@ def intervention_detail(request, intervention_id):
|
||||||
"contract_posts": contract_posts,
|
"contract_posts": contract_posts,
|
||||||
"total_general": total_general,
|
"total_general": total_general,
|
||||||
"total_validated": total_validated,
|
"total_validated": total_validated,
|
||||||
'can_view_prices': user_contract_access.can_view_prices if user_contract_access else False,
|
'can_view_prices': (
|
||||||
|
getattr(user_config, 'can_view_all_prices', False)
|
||||||
|
or (user_contract_access.can_view_prices if user_contract_access else False)
|
||||||
|
),
|
||||||
'geom_geojson': geom_geojson,
|
'geom_geojson': geom_geojson,
|
||||||
'allowed_transitions': json.dumps(allowed_transitions),
|
'allowed_transitions': json.dumps(allowed_transitions),
|
||||||
'add_note_form': InterventionNoteForm(),
|
'add_note_form': InterventionNoteForm(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue