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)
|
||||
class UserConfigAdmin(admin.ModelAdmin):
|
||||
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")
|
||||
inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline]
|
||||
filter_horizontal = ("linked_configs", "accessible_views")
|
||||
|
|
@ -637,6 +637,10 @@ class UserConfigAdmin(admin.ModelAdmin):
|
|||
'fields': ('accessible_views',),
|
||||
'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', {
|
||||
'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'),
|
||||
'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,
|
||||
'auto_logout_hours': cfg.auto_logout_hours,
|
||||
'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)),
|
||||
'accessible_views': list(cfg.accessible_views.values_list('code', 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),
|
||||
'auto_logout_hours': entry.get('auto_logout_hours'),
|
||||
'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,
|
||||
'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",
|
||||
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(
|
||||
default=False,
|
||||
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
|
||||
)
|
||||
|
||||
# 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_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_manage_rcp = source_config.esign_can_manage_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):
|
||||
"""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:
|
||||
config = user.config
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
if getattr(config, 'can_view_all_prices', False):
|
||||
return True
|
||||
|
||||
user_roles = set(config.roles.values_list("name", flat=True))
|
||||
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
||||
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:
|
||||
return True
|
||||
|
||||
cid = getattr(contract_id, 'id', contract_id)
|
||||
return UserContractAccess.objects.filter(
|
||||
user_config=config,
|
||||
contract_id=contract_id,
|
||||
contract_id=cid,
|
||||
can_view_prices=True
|
||||
).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.
|
||||
"""
|
||||
if getattr(user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
try:
|
||||
config = user.config
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
if getattr(config, 'can_view_all_posts', False):
|
||||
return True
|
||||
|
||||
user_roles = set(config.roles.values_list("name", flat=True))
|
||||
if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS):
|
||||
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:
|
||||
return True
|
||||
|
||||
cid = getattr(contract_id, 'id', contract_id)
|
||||
return UserContractAccess.objects.filter(
|
||||
user_config=config,
|
||||
contract_id=contract_id,
|
||||
contract_id=cid,
|
||||
can_view_posts=True
|
||||
).exists()
|
||||
|
||||
|
|
|
|||
|
|
@ -723,3 +723,37 @@ class ContractGeojsonEndpointTests(TestCase):
|
|||
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,6 +220,9 @@ class ControlQuantityInlineFormSet(BaseInlineFormSet):
|
|||
|
||||
qs = Contract.objects.none()
|
||||
if user and hasattr(user, "config"):
|
||||
if getattr(user.config, 'can_view_all_posts', False):
|
||||
qs = Contract.objects.all().order_by("contract_number")
|
||||
else:
|
||||
allowed_ids = UserContractAccess.objects.filter(
|
||||
user_config=user.config, can_view_posts=True
|
||||
).values_list("contract_id", flat=True)
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ def intervention_detail(request, intervention_id):
|
|||
can_check_intervention = True
|
||||
|
||||
# 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
|
||||
contract_posts = list(
|
||||
InterventionContractPost.objects.filter(intervention=intervention)
|
||||
|
|
@ -702,7 +702,10 @@ def intervention_detail(request, intervention_id):
|
|||
"contract_posts": contract_posts,
|
||||
"total_general": total_general,
|
||||
"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,
|
||||
'allowed_transitions': json.dumps(allowed_transitions),
|
||||
'add_note_form': InterventionNoteForm(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue