Skip to content

✨(api) add API route to fetch document content #1213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to
### Added

- ✨(frontend) add duplicate action to doc tree #1175
- ✨(api) add API route to fetch document content #1206

### Changed

Expand Down
2 changes: 1 addition & 1 deletion docker/auth/realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"sslRequired": "none",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": true,
Expand Down
6 changes: 3 additions & 3 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from core import choices, enums, models, utils
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
from core.services.yprovider_services import (
ConversionError,
YdocConverter,
YProviderAPI,
)


Expand Down Expand Up @@ -431,7 +431,7 @@ def create(self, validated_data):
language = user.language or language

try:
document_content = YdocConverter().convert(validated_data["content"])
document_content = YProviderAPI().convert(validated_data["content"])
except ConversionError as err:
raise serializers.ValidationError(
{"content": ["Could not convert content"]}
Expand Down
64 changes: 64 additions & 0 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
from core import authentication, choices, enums, models
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.yprovider_services import (
ServiceUnavailableError as YProviderServiceUnavailableError,
)
from core.services.yprovider_services import (
ValidationError as YProviderValidationError,
)
from core.services.yprovider_services import (
YProviderAPI,
)
from core.tasks.mail import send_ask_for_access_mail
from core.utils import extract_attachments, filter_descendants

Expand Down Expand Up @@ -1443,6 +1452,61 @@ def cors_proxy(self, request, *args, **kwargs):
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

@drf.decorators.action(
detail=True,
methods=["get"],
url_path="content",
name="Get document content in different formats",
)
def content(self, request, pk=None):
"""
Retrieve document content in different formats (JSON, Markdown, HTML).

Query parameters:
- content_format: The desired output format (json, markdown, html)

Returns:
JSON response with content in the specified format.
"""

document = self.get_object()

content_format = request.query_params.get("content_format", "json").lower()
if content_format not in {"json", "markdown", "html"}:
raise drf.exceptions.ValidationError(
"Invalid format. Must be one of: json, markdown, html"
)

# Get the base64 content from the document
content = None
base64_content = document.content
if base64_content is not None:
# Convert using the y-provider service
try:
yprovider = YProviderAPI()
result = yprovider.content(base64_content, content_format)
content = result["content"]
except YProviderValidationError as e:
return drf_response.Response(
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
)
except YProviderServiceUnavailableError as e:
logger.error("Error getting content for document %s: %s", pk, e)
return drf_response.Response(
{"error": "Failed to get document content"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

return drf_response.Response(
{
"id": str(document.id),
"title": document.title,
"content": content,
"created_at": document.created_at,
"updated_at": document.updated_at,
}
)


class DocumentAccessViewSet(
ResourceAccessViewsetMixin,
Expand Down
1 change: 1 addition & 0 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ def get_abilities(self, user):
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,
Expand Down
53 changes: 0 additions & 53 deletions src/backend/core/services/converter_services.py

This file was deleted.

80 changes: 80 additions & 0 deletions src/backend/core/services/yprovider_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Y-Provider API services."""

import json
from base64 import b64encode

from django.conf import settings

import requests


class ConversionError(Exception):
"""Base exception for conversion-related errors."""


class ValidationError(ConversionError):
"""Raised when the input validation fails."""


class ServiceUnavailableError(ConversionError):
"""Raised when the conversion service is unavailable."""


class YProviderAPI:
"""Service class for Y-Provider API operations."""

@property
def auth_header(self):
"""Build microservice authentication header."""
# Note: Yprovider microservice accepts only raw token, which is not recommended
return f"Bearer {settings.Y_PROVIDER_API_KEY}"

def _request(self, url, data, content_type):
"""Make a request to the Y-Provider API."""
response = requests.post(
url,
data=data,
headers={
"Authorization": self.auth_header,
"Content-Type": content_type,
},
timeout=settings.CONVERSION_API_TIMEOUT,
verify=settings.CONVERSION_API_SECURE,
)
response.raise_for_status()
return response

def convert(self, text):
"""Convert a Markdown text into our internal format using an external microservice."""

if not text:
raise ValidationError("Input text cannot be empty")

try:
response = self._request(
f"{settings.Y_PROVIDER_API_BASE_URL}{settings.CONVERSION_API_ENDPOINT}/",
text,
"text/markdown",
)
return b64encode(response.content).decode("utf-8")
except requests.RequestException as err:
raise ServiceUnavailableError(
"Failed to connect to backend service",
) from err

def content(self, base64_content, format_type):
"""Convert base64 Yjs content to different formats using the y-provider service."""

if not base64_content:
raise ValidationError("Input content cannot be empty")

data = json.dumps({"content": base64_content, "format": format_type})
try:
response = self._request(
f"{settings.Y_PROVIDER_API_BASE_URL}content/", data, "application/json"
)
return response.json()
except requests.RequestException as err:
raise ServiceUnavailableError(
"Failed to connect to backend service",
) from err
Loading
Loading