from django.shortcuts import get_object_or_404 from django.core.exceptions import PermissionDenied from django.db.models import Q from django.utils.translation import gettext_lazy as _ from functools import wraps from projects.models import Project, ProjectTeamAccess, ProjectUserAccess from common.models import UserConfig, UserThematics def _has_explicit_view_access(user, project, user_config): """Vérifie uniquement les accès explicites (équipe ou utilisateur direct).""" from contracts.models import CompanyMember team_ids = CompanyMember.objects.filter( user=user ).values_list('teams__id', flat=True) if team_ids and ProjectTeamAccess.objects.filter( project=project, team__in=team_ids, can_view=True ).exists(): return True return ProjectUserAccess.objects.filter(project=project, user=user, can_view=True).exists() def can_view_project(user, project): try: user_config = UserConfig.objects.get(user=user) except UserConfig.DoesNotExist: return False # Les admins et top_managers voient tout, y compris les projets en accès strict if user_config.roles.filter(name__in=['admin', 'top_manager']).exists(): return True # En accès strict : seuls les accès explicites (équipe/utilisateur) sont pris en compte if project.strict_access: return _has_explicit_view_access(user, project, user_config) # Vérifie si l'utilisateur est interne et a accès à la thématique (permission projet dédiée) thematics = project.thematics.all() if project.thematics.exists() else None if thematics and user_config.is_intern: if UserThematics.objects.filter( user_config=user_config, thematic__in=thematics, can_view_projects=True ).exists(): return True # Accès explicites (équipe ou utilisateur) return _has_explicit_view_access(user, project, user_config) def project_permission_required(view_func): @wraps(view_func) def _wrapped_view(request, project_id, *args, **kwargs): project = get_object_or_404(Project, pk=project_id) if not can_view_project(request.user, project): raise PermissionDenied(_("Vous n'avez pas la permission de voir ce projet.")) return view_func(request, project_id, *args, **kwargs) return _wrapped_view def can_add_or_edit_project(user, project=None, ignore_status_lock=False): try: user_config = UserConfig.objects.get(user=user) except UserConfig.DoesNotExist: return False # Verrouille l'édition d'attributs quand le projet est comptabilisé/clos/annulé, # sauf si l'appel demande explicitement d'ignorer ce verrou (ex: changement de statut). if project and not ignore_status_lock and project.status in ['invoiced', 'closed', 'canceled']: return False # Les admins peuvent tout modifier if user_config.roles.filter(name__in=['admin']).exists(): return True # Les externes ne peuvent pas modifier les propriétés d'un projet if not user_config.is_intern: return False # Vérifie les accès explicites en édition (équipe ou utilisateur direct) if project: from contracts.models import CompanyMember team_ids = CompanyMember.objects.filter( user=user ).values_list('teams__id', flat=True) if team_ids and ProjectTeamAccess.objects.filter( project=project, team__in=team_ids, can_edit=True ).exists(): return True if ProjectUserAccess.objects.filter(project=project, user=user, can_edit=True).exists(): return True # En accès strict : les permissions thématiques ne s'appliquent pas if project and project.strict_access: return False # Vérifie si l'utilisateur est interne, a le rôle manager et a accès à la thématique du projet if project and project.thematics and user_config.is_intern and user_config.roles.filter(name='manager').exists(): if UserThematics.objects.filter( user_config=user_config, thematic__in=project.thematics.all(), can_edit_projects=True ).exists(): return True if not project and user_config.is_intern and user_config.roles.filter(name='manager').exists(): return True return False def can_add_project_documents(user, project): """Autorise l'ajout de documents : édition requise pour les internes, vue seule suffit pour les externes.""" if not can_view_project(user, project): return False try: user_config = UserConfig.objects.get(user=user) except UserConfig.DoesNotExist: return False if not user_config.is_intern: return True return can_add_or_edit_project(user, project, ignore_status_lock=True) def can_manage_project_orders(user, project=None): """Admins ou éditeurs du projet uniquement.""" try: user_config = UserConfig.objects.get(user=user) except UserConfig.DoesNotExist: return False if not user_config.roles.filter(name__in=['admin', 'manager']).exists(): return False if project: return can_add_or_edit_project(user, project) return True def can_manage_project_members(user, project): """Seul un utilisateur ayant des droits d'édition sur le projet peut gérer les accès.""" if not can_view_project(user, project): return False return can_add_or_edit_project(user, project) def project_manage_members_permission_required(view_func): @wraps(view_func) def _wrapped_view(request, project_id, *args, **kwargs): project = get_object_or_404(Project, pk=project_id) if not can_manage_project_members(request.user, project): raise PermissionDenied(_("Seul le responsable du projet ou un manager peut gérer les membres.")) return view_func(request, project_id, *args, **kwargs) return _wrapped_view def project_add_or_edit_permission_required(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): project = None # Vérifie si 'project_id' est fourni dans les paramètres de la vue project_id = kwargs.get('project_id') if project_id is not None: project = get_object_or_404(Project, pk=project_id) if not can_add_or_edit_project(request.user, project): raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter ou de modifier ce projet.")) return view_func(request, *args, **kwargs) return _wrapped_view def filter_viewable_projects_for_user(user): """ Renvoie un queryset des projets visibles par l'utilisateur selon les règles de can_view_project(). """ try: user_config = UserConfig.objects.select_related('user').prefetch_related('roles', 'thematics').get(user=user) except UserConfig.DoesNotExist: return Project.objects.none() projects = Project.objects.all() # Rôles admin ou top_manager → accès global if user_config.roles.filter(name__in=['admin', 'top_manager']).exists(): return projects from contracts.models import CompanyMember team_ids = list(CompanyMember.objects.filter( user=user ).values_list('teams__id', flat=True)) # Accès explicites (équipe ou utilisateur) — valables même en accès strict q_explicit = Q(user_accesses__user=user, user_accesses__can_view=True) if team_ids: q_explicit |= Q(team_accesses__team__in=team_ids, team_accesses__can_view=True) # Accès via thématique (uniquement pour les projets sans accès strict) q_thematic = Q() if user_config.is_intern: thematic_ids = list(UserThematics.objects.filter( user_config=user_config, can_view_projects=True ).values_list('thematic__id', flat=True)) if thematic_ids: q_thematic = Q(strict_access=False, thematics__id__in=thematic_ids) return projects.filter(q_explicit | q_thematic).distinct() # ============================================================================= # Guard functions for private file access # ============================================================================= def guard_project_document_file(obj, user): """ Guard function for ProjectDocument.file and ProjectDocument.thumbnail fields. Checks if user has access to the project. """ from .models import ProjectDocument if not isinstance(obj, ProjectDocument): return False if not user.is_authenticated: return False return can_view_project(user, obj.project)