Skip to content

Feat: Outline import#1478

Draft
NicolasRitouet wants to merge 25 commits intosuitenumerique:mainfrom
NicolasRitouet:feature/outline-import
Draft

Feat: Outline import#1478
NicolasRitouet wants to merge 25 commits intosuitenumerique:mainfrom
NicolasRitouet:feature/outline-import

Conversation

@NicolasRitouet
Copy link
Copy Markdown
Collaborator

@NicolasRitouet NicolasRitouet commented Oct 12, 2025

Purpose

Add Outline import functionality to allow users to migrate their documentation from Outline by uploading a .zip export file.

Proposal

  • Add backend API endpoint to handle Outline .zip upload
    • Implement Outline export processing service that:
      • Extracts markdown files from the archive
      • Converts markdown content to BlockNote format (Yjs)
      • Preserves document hierarchy and nested documents
      • Handles image attachments by uploading them to object storage
    • Add frontend import page at /import/outline
    • Fix CSRF token handling in upload request
    • Prevent duplicate documents for nested structures (e.g., Doc.md + Doc/ directory)
    • Add French translations for the import UI

outline-import

External contributions

Thank you for your contribution! 🎉

Please ensure the following items are checked before submitting your pull request:

  • I have read and followed the contributing guidelines
  • I have read and agreed to the Code of Conduct
  • I have signed off my commits with git commit --signoff (DCO compliance)
  • I have signed my commits with my SSH or GPG key (git commit -S)
  • My commit messages follow the required format: <gitmoji>(type) title description
  • I have added a changelog entry under ## [Unreleased] section (if noticeable change)
  • I have added corresponding tests for new features or bug fixes (if applicable)

…kend:\n- Add POST /api/v1.0/outline_import/upload (zip)\n- Parse .md files, create doc tree from folders, rewrite image links to attachments, convert to Yjs via Y-provider\nFrontend:\n- Add /import/outline page with zip file picker + POST\n- Add menu entry 'Import from Outline' in left panel header\n- Add minimal i18n keys (en, fr)
…ous forbidden\n- Authenticated happy path with local image and mocked conversion
…import.py and call from view\n- Keep view thin; service handles zip, images, conversion, attachments\n- Fix imports accordingly
…ject unsafe paths)\n- Ignore __MACOSX and hidden entries\n- Service unit tests (happy path + zip slip)\n- Change API path to /imports/outline/upload and update front + tests
…kNote elements

- Convert H4/H5/H6 headings to compatible formats (H4→H3 with marker, H5→bold with arrow, H6→paragraph with bullet)
- Convert horizontal rules (---, ***, ___) to [DIVIDER_BLOCK] markers
- Preserve task lists formatting for proper checkbox rendering
- Add comprehensive unit tests for all conversion cases

This ensures Outline exports with all 6 heading levels and other markdown features
are properly imported into BlockNote.js which only supports 3 heading levels.
Resolved conflict in translations.json by keeping Outline import translations
- Add CSRF token to Outline import upload request
- Fix content save by removing invalid update_fields parameter
- Handle nested documents properly to avoid duplicates when a document
  has child documents (e.g., Doc.md with Doc/ directory)
@NicolasRitouet NicolasRitouet marked this pull request as draft October 12, 2025 15:03
@NicolasRitouet
Copy link
Copy Markdown
Collaborator Author

In progress:

  • UI is ugly
  • attachments are not uploaded
  • not all content blocks have been tested

@NicolasRitouet NicolasRitouet changed the title Feature/outline import Feat: Outline import Oct 12, 2025

def _upload_attachment(user, doc: models.Document, arcname: str, data: bytes) -> str:
"""Upload a binary asset into object storage and return its public media URL."""
content_type, _ = mimetypes.guess_type(arcname)
Copy link
Copy Markdown
Collaborator

@StephanMeijer StephanMeijer Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the library underneath using? Mimetype guessing is not always stable (even when using libmagic it can differ from version/environment).
I would suggest good testing, preferably in different environments if possible.

Comment on lines +101 to +103
parts = [p for p in name.split("/") if p]
if any(part == ".." for part in parts):
raise OutlineImportError("Unsafe path in archive")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always unsafe or only when .. goes beyond the root you are iterating over?

Comment on lines +21 to +27
uploaded = request.FILES.get("file")
if not uploaded:
raise drf.exceptions.ValidationError({"file": "File is required"})

name = getattr(uploaded, "name", "")
if not name.endswith(".zip"):
raise drf.exceptions.ValidationError({"file": "Must be a .zip file"})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should rely on a drf serializer here to validate the input instead of doing it in the view. You can maybe reused the FileUploadSerializer present in the serializer module (src/backend/core/api/serializers.py)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then once the input validated you have to rely on the malware_detection feature to validate the zip content. But we have to imagine a workflow, this process is async. Once the malware detection ended, the process_outile_zip should start.

# Fail fast if the upload is not a valid zip archive
with zipfile.ZipFile(io.BytesIO(content)):
pass
created_ids = process_outline_zip(request.user, content)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggestion to save the uploaded zip on the bucket storage (you can rely on the django storage API). Doing this you can create a celery task to process the file in an async way

return f"{settings.MEDIA_BASE_URL}{settings.MEDIA_URL}{key}"


def process_outline_zip(user, zip_bytes: bytes) -> list[str]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the previous comment made, asking to save the file on the bucket, you can transform this function in a celery task and execute it asynchronously

Comment on lines +53 to +57
models.DocumentAccess.objects.update_or_create(
document=doc,
user=user,
defaults={"role": models.RoleChoices.OWNER},
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
models.DocumentAccess.objects.update_or_create(
document=doc,
user=user,
defaults={"role": models.RoleChoices.OWNER},
)

You have to define an owner access for the user only on the root document. Then the children will inherit from this access.

creator=user,
title=part,
link_reach=models.LinkReachChoices.RESTRICTED,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
)
)
models.DocumentAccess.objects.create(
document=doc,
user=user,
role=models.RoleChoices.OWNER,
)

Comment on lines +152 to +160
if parent_doc is None:
doc = models.Document.add_root(
depth=1,
creator=user,
title=title,
link_reach=models.LinkReachChoices.RESTRICTED,
)
else:
doc = parent_doc.add_child(creator=user, title=title)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if parent_doc is None:
doc = models.Document.add_root(
depth=1,
creator=user,
title=title,
link_reach=models.LinkReachChoices.RESTRICTED,
)
else:
doc = parent_doc.add_child(creator=user, title=title)

This is managed in _ensure_dir_documents function. You will probably have duplicated docs at the end

Comment on lines +168 to +172
models.DocumentAccess.objects.update_or_create(
document=doc,
user=user,
defaults={"role": models.RoleChoices.OWNER},
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
models.DocumentAccess.objects.update_or_create(
document=doc,
user=user,
defaults={"role": models.RoleChoices.OWNER},
)

Managed in the _ensure_dir_documents function

extra_args = {
"Metadata": {
"owner": str(user.id),
"status": enums.DocumentAttachmentStatus.READY,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"status": enums.DocumentAttachmentStatus.READY,
"status": enums.DocumentAttachmentStatus.PROCESSING,

NicolasRitouet and others added 3 commits November 29, 2025 06:20
- Use python-magic instead of mimetypes for reliable MIME detection
- Set attachment status to PROCESSING instead of READY (pending malware scan)
- Create DocumentAccess only for root documents (children inherit access)
- Reorganize imports (stdlib, third-party, django, local)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
… review

- Add SessionAuthentication to upload endpoint
- Add trailing slash to upload URL for Django compatibility
- Register outline_import task in celery autodiscover
- Add migration merge for comments feature conflict
- Improve import UI with drag-drop, progress states, and translations
- Add logging for markdown conversion failures
@virgile-dev virgile-dev added the ✏️ Needs design A problem or need has been identified that requires UX design investigation label Jan 29, 2026
@virgile-dev
Copy link
Copy Markdown
Collaborator

@rl-83 hey ! we need you help figuring out the right UX for this.
This is an awesome contribution. It just needs good UX and polished UI.
Thanks a bunch.

NicolasRitouet and others added 5 commits January 29, 2026 12:42
Apply ruff formatting to outline import files and add merge migration
to resolve conflict between outline import and template removal migrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix ruff lint: move imports to top level, fix import sorting,
  bind loop variables in closures, add noqa for intentional patterns
- Fix tests: add trailing slashes to URLs, update to async flow
  (202 status, mock storage/malware instead of sync processing)
- Fix frontend: use @gouvfr-lasuite/cunningham-react instead of
  @openfun/cunningham-react, fix color token names (brand-* and gray-*
  instead of primary-* and greyscale-*)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract archive processing into _process_archive to properly close
the ZipFile via a context manager, preventing resource leaks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@robin-lecomte
Copy link
Copy Markdown
Collaborator

Hello gentlemen,

Here is my UX proposal.

I suggest reusing the Docs import button, but enhancing it with a dedicated modal that provides users with additional information.

CleanShot 2026-02-10 at 13 59 09 CleanShot 2026-02-10 at 13 55 03

I considered adding a “How to import via Outscale” button that would open a help modal, but I figured people could probably find the information on their own. In the import modal, we already mention that Outscale imports use a ZIP format. Do you think that’s clear enough? I’d like to avoid cluttering the interface with too many tips, especially if we add more import methods in the future.

@robin-lecomte robin-lecomte added ✅ Design approved The PO has approved the design: developers can start implementation and removed ✏️ Needs design A problem or need has been identified that requires UX design investigation labels Feb 10, 2026
@virgile-dev virgile-dev moved this from To do to Backlog in LaSuite Docs Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✅ Design approved The PO has approved the design: developers can start implementation

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

5 participants