""" Management command: migrate_project_permissions ================================================ Migre les droits existants vers le nouveau modèle de permissions de projets. Deux opérations : 1. UserThematics.can_view_interventions = True -> can_view_projects = True 2. Lecture du fichier JSON exporte par `export_project_members` : - tout role -> ProjectUserAccess(can_view=True) - role='owner' -> ProjectUserAccess(can_edit=True) Flux recommande en production : 1. python manage.py export_project_members 2. python manage.py migrate projects 0020_projectteamaccess_projectuseraccess 3. python manage.py migrate common 0064_userthematics_project_permissions 4. python manage.py migrate_project_permissions [--input project_members_export.json] 5. python manage.py migrate projects 0021_remove_projectmember Usage : python manage.py migrate_project_permissions [--dry-run] [--input fichier.json] """ import json from pathlib import Path from django.core.management.base import BaseCommand, CommandError from django.db import transaction DEFAULT_INPUT = Path("project_members_export.json") class Command(BaseCommand): help = "Migre les droits existants vers le nouveau modele de permissions de projets." def add_arguments(self, parser): parser.add_argument( '--dry-run', action='store_true', help="Simule la migration sans ecrire en base.", ) parser.add_argument( '--input', '-i', type=Path, default=DEFAULT_INPUT, help=f"Fichier JSON exporte par export_project_members (defaut : {DEFAULT_INPUT})", ) def handle(self, *args, **options): dry_run = options['dry_run'] if dry_run: self.stdout.write(self.style.WARNING("=== MODE DRY-RUN : aucune ecriture en base ===\n")) self._migrate_thematics(dry_run) self._import_members(options['input'], dry_run) self.stdout.write(self.style.SUCCESS("\nMigration des permissions terminee.")) def _migrate_thematics(self, dry_run): from common.models import UserThematics qs = UserThematics.objects.filter(can_view_interventions=True, can_view_projects=False) count = qs.count() self.stdout.write( f"[Thematiques] {count} enregistrement(s) a mettre a jour " "(can_view_interventions=True -> can_view_projects=True)." ) if count and not dry_run: with transaction.atomic(): updated = qs.update(can_view_projects=True) self.stdout.write(self.style.SUCCESS(f" -> {updated} mis a jour.")) def _import_members(self, input_path, dry_run): from projects.models import ProjectUserAccess if not input_path.exists(): raise CommandError( f"Fichier introuvable : '{input_path}'. " "Lancez d'abord : python manage.py export_project_members" ) data = json.loads(input_path.read_text(encoding='utf-8')) total = len(data) self.stdout.write(f"[ProjectMember] {total} entree(s) a importer depuis '{input_path}'.") created = updated = skipped = 0 with transaction.atomic(): for row in data: project_id = row['project_id'] user_id = row['member_id'] can_edit = row['role'] == 'owner' existing = ProjectUserAccess.objects.filter( project_id=project_id, user_id=user_id ).first() if existing: changed = False if not existing.can_view: existing.can_view = True changed = True if can_edit and not existing.can_edit: existing.can_edit = True changed = True if changed: if not dry_run: existing.save(update_fields=['can_view', 'can_edit']) updated += 1 self.stdout.write( f" MODIFIE user_id={user_id} -> project_id={project_id} " f"(can_view=True, can_edit={existing.can_edit})" ) else: skipped += 1 else: if not dry_run: ProjectUserAccess.objects.create( project_id=project_id, user_id=user_id, can_view=True, can_edit=can_edit, ) created += 1 self.stdout.write( f" CREE user_id={user_id} -> project_id={project_id} " f"(can_view=True, can_edit={can_edit})" ) self.stdout.write( self.style.SUCCESS( f" -> {created} cree(s), {updated} mis a jour, {skipped} inchange(s)." ) )