Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions draw/api/drive_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# OPTIONAL Frappe Drive integration: register a Draw diagram as a Drive file so it
# shows up in the user's Drive, like a Sheets/Slides/Writer doc. Deliberately
# soft-coupled — Draw does NOT list Drive in required_apps, so it stays installable
# standalone (e.g. on Frappe Cloud without Drive). Every entry point no-ops cleanly
# when Drive is absent or not yet set up (no team).
#
# A diagram is registered as a Drive "native document" File: a File row with
# content_doctype="Draw Diagram" + content_docname=<name> (Drive's own pattern for
# app-owned docs). Rendering the diagram INSIDE Drive needs a Drive-side viewer for
# this content type (a Drive change), which is out of scope here.

import frappe

DIAGRAM_MIME = "frappe_doc"


def drive_installed() -> bool:
return "drive" in frappe.get_installed_apps()


def _default_team() -> str | None:
"""A Drive team to place the file in: the first the user belongs to, else the
first team on the site. None when Drive has no team yet (not set up)."""
if frappe.db.has_column("Drive Team Member", "user"):
mine = frappe.get_all(
"Drive Team Member", filters={"user": frappe.session.user}, pluck="parent", limit=1
)
if mine:
return mine[0]
teams = frappe.get_all("Drive Team", pluck="name", limit=1)
return teams[0] if teams else None


def register_in_drive(diagram_name: str, team: str | None = None) -> str | None:
"""Ensure a Drive File links to this diagram; returns the File name (or None
when Drive is unavailable / has no team). Idempotent — reuses an existing link."""
if not drive_installed() or not frappe.db.exists("Draw Diagram", diagram_name):
return None
team = team or _default_team()
if not team:
return None # Drive present but not set up (no team) — nothing to do yet.

existing = frappe.db.exists(
"File", {"content_doctype": "Draw Diagram", "content_docname": diagram_name}
)
if existing:
return existing

diagram = frappe.get_doc("Draw Diagram", diagram_name)
drive_file = frappe.get_doc(
{
"doctype": "File",
"file_name": diagram.title or diagram_name,
"is_private": 1,
"team": team,
"content_doctype": "Draw Diagram",
"content_docname": diagram_name,
"mime_type": DIAGRAM_MIME,
}
).insert(ignore_permissions=True)
return drive_file.name


@frappe.whitelist()
def add_to_drive(name: str) -> dict:
"""Whitelisted entry point for an "Add to Drive" action. Returns whether Drive
is available and the linked File name if created."""
file_name = register_in_drive(name)
return {"drive_installed": drive_installed(), "file": file_name}
Comment on lines +63 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing permission check before Drive registrationfrappe.db.exists("Draw Diagram", diagram_name) bypasses Frappe's permission layer, so any authenticated user can call add_to_drive with any diagram name (including private diagrams they can't read) and link it into their Drive. Add frappe.has_permission("Draw Diagram", doc=name, throw=True) at the top of add_to_drive before calling register_in_drive.

37 changes: 37 additions & 0 deletions draw/draw/doctype/draw_diagram/test_draw_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,43 @@ def test_get_shares_reports_level_for_dialog(self):
self.assertEqual(row["level"], "edit")
self.assertTrue(row["can_edit"])

def test_register_in_drive_when_available(self):
# Optional Drive integration — only exercised when Drive is installed
# (CI's fresh site has no Drive, so this skips there).
from draw.api.drive_integration import drive_installed, register_in_drive

if not drive_installed():
self.skipTest("Frappe Drive not installed")

teams = frappe.get_all("Drive Team", pluck="name")
team = (
teams[0]
if teams
else frappe.get_doc({"doctype": "Drive Team", "title": "Draw CI Team"}).insert(
ignore_permissions=True
).name
)
doc = self._make("unified", {"schemaVersion": 1, "diagramType": "unified"})

file_name = register_in_drive(doc.name, team=team)
self.addCleanup(lambda: frappe.delete_doc("File", file_name, force=True, ignore_permissions=True))
self.assertTrue(file_name)
self.assertTrue(
frappe.db.exists(
"File", {"content_doctype": "Draw Diagram", "content_docname": doc.name}
)
)
# Idempotent — a second call reuses the same File.
self.assertEqual(register_in_drive(doc.name, team=team), file_name)

def test_drive_registration_noops_without_team(self):
# Registration is a safe no-op when Drive isn't set up / not installed.
from draw.api import drive_integration

Comment on lines +136 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Test asserts nothing when Drive is installedtest_drive_registration_noops_without_team only calls assertIsNone inside the if not drive_installed() branch, so on a bench with Drive installed but no team the test body is empty and passes vacuously. Add an else branch (or an unconditional assertion) to cover the "installed but no team" path the test name promises.

doc = self._make("block", {"schemaVersion": 1, "diagramType": "block"})
if not drive_integration.drive_installed():
self.assertIsNone(drive_integration.register_in_drive(doc.name))

def test_unshare_revokes_access(self):
from draw.api.share import get_diagram_shares, share_diagram, unshare_diagram

Expand Down
Loading