219 lines
8.9 KiB
Python
219 lines
8.9 KiB
Python
"""
|
|
Management command: create_project_folders
|
|
==========================================
|
|
Backfill that, for every Project:
|
|
1. Creates a DocumentFolder + DocumentFolderAttachment (if missing).
|
|
2. Migrates each legacy ProjectDocument into the new ManagedDocument /
|
|
DocumentVersion / DocumentAttachment system (if not already migrated).
|
|
|
|
Idempotent: safe to run multiple times.
|
|
- Folder step : skipped when a DocumentFolderAttachment already exists.
|
|
- Document step: skipped when ProjectDocument.managed_document is already set.
|
|
|
|
Usage:
|
|
python manage.py create_project_folders
|
|
python manage.py create_project_folders --dry-run
|
|
python manage.py create_project_folders --folders-only
|
|
python manage.py create_project_folders --docs-only
|
|
"""
|
|
|
|
import os
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
|
|
from documents.models import (
|
|
DocumentAttachment,
|
|
DocumentFolderAttachment,
|
|
DocumentVersion,
|
|
ManagedDocument,
|
|
)
|
|
from projects.models import Project, ProjectDocument
|
|
from projects.signals import ensure_project_document_folder
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Create a default DocumentFolder for every project that lacks one, "
|
|
"and migrate legacy ProjectDocument records into the managed-document system."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be done without making any changes.",
|
|
)
|
|
parser.add_argument(
|
|
"--folders-only",
|
|
action="store_true",
|
|
help="Only create folders; skip document migration.",
|
|
)
|
|
parser.add_argument(
|
|
"--docs-only",
|
|
action="store_true",
|
|
help="Only migrate documents; skip folder creation (folder must already exist).",
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Entry point #
|
|
# ------------------------------------------------------------------ #
|
|
def handle(self, *args, **options):
|
|
dry_run = options["dry_run"]
|
|
folders_only = options["folders_only"]
|
|
docs_only = options["docs_only"]
|
|
|
|
if dry_run:
|
|
self.stdout.write(self.style.WARNING("DRY RUN — no changes will be saved."))
|
|
|
|
project_ct = ContentType.objects.get_for_model(Project)
|
|
|
|
# IDs of projects that already have a folder attachment
|
|
already_foldered = set(
|
|
DocumentFolderAttachment.objects.filter(content_type=project_ct)
|
|
.values_list("object_id", flat=True)
|
|
.distinct()
|
|
)
|
|
|
|
projects = Project.objects.prefetch_related("documents").order_by("id")
|
|
total_projects = projects.count()
|
|
self.stdout.write(f"Found {total_projects} project(s).\n")
|
|
|
|
folders_created = 0
|
|
folders_skipped = 0
|
|
docs_migrated = 0
|
|
docs_skipped = 0
|
|
|
|
for project in projects:
|
|
self.stdout.write(f"[{project.code}] {project.name}")
|
|
|
|
# ── 1. Folder ──────────────────────────────────────────────
|
|
folder = None
|
|
if not docs_only:
|
|
if project.pk in already_foldered:
|
|
self.stdout.write(" folder: already exists — skipped")
|
|
folders_skipped += 1
|
|
# Retrieve the existing folder for the document step
|
|
attachment = (
|
|
DocumentFolderAttachment.objects
|
|
.filter(content_type=project_ct, object_id=project.pk)
|
|
.select_related("folder")
|
|
.first()
|
|
)
|
|
folder = attachment.folder if attachment else None
|
|
else:
|
|
if dry_run:
|
|
self.stdout.write(" folder: WOULD CREATE")
|
|
folders_created += 1
|
|
else:
|
|
folder = ensure_project_document_folder(project)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(f" folder: created [{folder.slug}]")
|
|
)
|
|
folders_created += 1
|
|
else:
|
|
# docs-only: retrieve existing folder
|
|
attachment = (
|
|
DocumentFolderAttachment.objects
|
|
.filter(content_type=project_ct, object_id=project.pk)
|
|
.select_related("folder")
|
|
.first()
|
|
)
|
|
folder = attachment.folder if attachment else None
|
|
|
|
# ── 2. Documents ───────────────────────────────────────────
|
|
if folders_only:
|
|
continue
|
|
|
|
legacy_docs = list(project.documents.select_related("uploaded_by").all())
|
|
if not legacy_docs:
|
|
self.stdout.write(" docs : none")
|
|
continue
|
|
|
|
for proj_doc in legacy_docs:
|
|
if not proj_doc.file or not proj_doc.file.name:
|
|
continue
|
|
|
|
if proj_doc.managed_document_id:
|
|
self.stdout.write(
|
|
f" docs : {proj_doc.filename!r} — already migrated, skipped"
|
|
)
|
|
docs_skipped += 1
|
|
continue
|
|
|
|
if dry_run:
|
|
self.stdout.write(
|
|
f" docs : WOULD MIGRATE {proj_doc.filename!r}"
|
|
)
|
|
docs_migrated += 1
|
|
continue
|
|
|
|
managed = self._migrate_one(proj_doc, project, project_ct, folder)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f" docs : migrated {proj_doc.filename!r} -> ManagedDocument #{managed.pk}"
|
|
)
|
|
)
|
|
docs_migrated += 1
|
|
|
|
# ── Summary ────────────────────────────────────────────────────
|
|
verb = "would be " if dry_run else ""
|
|
self.stdout.write("")
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"Done. "
|
|
f"Folders: {folders_created} {verb}created, {folders_skipped} skipped. "
|
|
f"Documents: {docs_migrated} {verb}migrated, {docs_skipped} already done."
|
|
)
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Migration of a single ProjectDocument #
|
|
# ------------------------------------------------------------------ #
|
|
@transaction.atomic
|
|
def _migrate_one(self, proj_doc, project, project_ct, folder):
|
|
"""
|
|
Create ManagedDocument + DocumentVersion + DocumentAttachment for a
|
|
single ProjectDocument and mark it as migrated.
|
|
|
|
The file is NOT moved or copied — DocumentVersion.file simply references
|
|
the existing storage path. The folder is added automatically by
|
|
DocumentAttachment.apply_default_folder() (triggered by its save()).
|
|
"""
|
|
title = os.path.splitext(proj_doc.filename)[0] or proj_doc.filename
|
|
|
|
managed = ManagedDocument.objects.create(
|
|
title=title,
|
|
created_by=proj_doc.uploaded_by,
|
|
)
|
|
|
|
# Build a DocumentVersion referencing the existing file path directly.
|
|
# Assigning a string to a FileField stores the path without re-uploading.
|
|
version = DocumentVersion(
|
|
document=managed,
|
|
uploaded_by=proj_doc.uploaded_by,
|
|
comment="Migré depuis ProjectDocument (héritage)",
|
|
status=DocumentVersion.STATUS_APPROVED,
|
|
)
|
|
version.file = proj_doc.file.name # existing storage path, no upload
|
|
if proj_doc.thumbnail and proj_doc.thumbnail.name:
|
|
version.thumbnail = proj_doc.thumbnail.name
|
|
# version.save() auto-sets version_number, calls document.touch(),
|
|
# attempts thumbnail generation (fails gracefully for non-images).
|
|
version.save()
|
|
|
|
# DocumentAttachment.save() → apply_default_folder() adds project folder
|
|
# to managed.folders automatically (because DocumentFolderAttachment
|
|
# with is_default=True already exists for this project).
|
|
DocumentAttachment.objects.create(
|
|
document=managed,
|
|
content_type=project_ct,
|
|
object_id=project.pk,
|
|
attached_by=proj_doc.uploaded_by,
|
|
)
|
|
|
|
# Mark legacy record as migrated (idempotence flag)
|
|
ProjectDocument.objects.filter(pk=proj_doc.pk).update(managed_document=managed)
|
|
|
|
return managed
|