Skip to content
Draft
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
6 changes: 6 additions & 0 deletions lms/envs/devstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing
LEARNING_MICROFRONTEND_URL = os.environ.get("LEARNING_MICROFRONTEND_URL", "http://localhost:2000")
LEARNING_MICROFRONTEND_NETLOC = os.environ.get("LEARNING_MICROFRONTEND_NETLOC", urlparse(LEARNING_MICROFRONTEND_URL).netloc)

# Additional Learning MFE URLs for development scenarios (multiple apps, different ports, etc.)
ADDITIONAL_LEARNING_MFE_URLS = [
"http://apps.local.openedx.io:8080", # Your second frontend app
# Add more URLs as needed for your development setup
]

###################### Cross-domain requests ######################
ENABLE_CORS_HEADERS = True
CORS_ALLOW_CREDENTIALS = True
Expand Down
29 changes: 25 additions & 4 deletions openedx/features/course_experience/url_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,34 @@ def is_request_from_learning_mfe(request: HttpRequest):
"""
Returns whether the given request was made by the frontend-app-learning MFE.
"""
# Primary MFE URL
url_str = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
if not url_str:

# Additional MFE URLs for development/multi-app scenarios
additional_urls = configuration_helpers.get_value(
'ADDITIONAL_LEARNING_MFE_URLS',
getattr(settings, 'ADDITIONAL_LEARNING_MFE_URLS', [])
)

# Combine all allowed URLs
allowed_urls = []
if url_str:
allowed_urls.append(url_str)
allowed_urls.extend(additional_urls)

if not allowed_urls:
return False

url = urlparse(url_str)
mfe_url_base = f'{url.scheme}://{url.netloc}'
return request.META.get('HTTP_REFERER', '').startswith(mfe_url_base)
referer = request.META.get('HTTP_REFERER', '')

# Check if referer matches any of the allowed MFE URLs
for mfe_url in allowed_urls:
url = urlparse(mfe_url)
mfe_url_base = f'{url.scheme}://{url.netloc}'
if referer.startswith(mfe_url_base):
return True

return False
82 changes: 82 additions & 0 deletions xmodule/html_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from django.conf import settings
from fs.errors import ResourceNotFound
import json
from lxml import etree
from path import Path as path
from web_fragments.fragment import Fragment
Expand Down Expand Up @@ -144,6 +145,37 @@ def get_html(self):
return data
return self.data

@XBlock.handler
def get_content(self, request, suffix=''):
"""
API handler that returns the content of this HTML block in a standardized format.

Returns JSON with:
- content: The HTML content of the block
- content_type: Type of content (always 'html' for HTML blocks)
- display_name: The display name of the block
- block_id: The usage ID of the block
- block_type: The type of block (always 'html' for HTML blocks)
"""
from webob import Response

data = {
'content': self.get_html(),
'content_type': 'html',
'display_name': self.display_name or 'HTML Component',
'block_id': str(self.scope_ids.usage_id),
'block_type': 'html',
'metadata': {
'editor_type': getattr(self, 'editor', 'visual'),
'uses_latex': getattr(self, 'use_latex_compiler', False),
}
}

return Response(
json.dumps(data).encode('utf-8'),
content_type='application/json'
)

def studio_view(self, _context):
"""
Return the studio view.
Expand Down Expand Up @@ -542,3 +574,53 @@ def reset_class():

reset_class()
HtmlBlock.__name__ = "HtmlBlock"

# Ensure the get_content handler is available on the final HtmlBlock class
def get_content_handler(self, request, suffix=''):
"""
API handler that returns the content of this HTML block in a standardized format.

Returns JSON with:
- content: The HTML content of the block
- content_type: Type of content (always 'html' for HTML blocks)
- display_name: The display name of the block
- block_id: The usage ID of the block
- block_type: The type of block (always 'html' for HTML blocks)
"""
from webob import Response

# Get HTML content and replace static URLs with proper course asset URLs
html_content = self.get_html()
if html_content:
from common.djangoapps.static_replace import replace_static_urls
html_content = replace_static_urls(
html_content,
course_id=self.scope_ids.usage_id.context_key
)

# Check if this block can be completed on view
can_complete_on_view = False
completion_service = self.runtime.service(self, 'completion')
if completion_service and completion_service.completion_tracking_enabled():
can_complete_on_view = completion_service.can_mark_block_complete_on_view(self)

data = {
'content': html_content,
'content_type': 'html',
'display_name': self.display_name or 'HTML Component',
'block_id': str(self.scope_ids.usage_id),
'block_type': 'html',
'can_complete_on_view': can_complete_on_view,
'metadata': {
'editor_type': getattr(self, 'editor', 'visual'),
'uses_latex': getattr(self, 'use_latex_compiler', False),
}
}

return Response(
json.dumps(data).encode('utf-8'),
content_type='application/json'
)

# Add the handler to the final HtmlBlock class
HtmlBlock.get_content = XBlock.handler(get_content_handler)
137 changes: 137 additions & 0 deletions xmodule/video_block/video_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,143 @@ def make_transcript_http_response(content, filename, language, content_type, add

return response

@XBlock.handler
def get_content(self, request, dispatch):
"""
API handler that returns the content of this video block in a standardized format.

Returns JSON with:
- content: Video metadata including URLs, transcripts, etc.
- content_type: Type of content (always 'video' for video blocks)
- display_name: The display name of the block
- block_id: The usage ID of the block
- block_type: The type of block (always 'video' for video blocks)
"""
from webob import Response
import json

# Get video URLs
video_urls = []
if self.html5_sources:
video_urls.extend(self.html5_sources)

# Get public video URL if available
public_url = self.get_public_video_url()
if public_url:
video_urls.append(public_url)

# Get YouTube URLs if available
youtube_urls = {}
if self.youtube_id_1_0:
youtube_urls['1.0'] = f"https://www.youtube.com/watch?v={self.youtube_id_1_0}"
if self.youtube_id_0_75:
youtube_urls['0.75'] = f"https://www.youtube.com/watch?v={self.youtube_id_0_75}"
if self.youtube_id_1_25:
youtube_urls['1.25'] = f"https://www.youtube.com/watch?v={self.youtube_id_1_25}"
if self.youtube_id_1_5:
youtube_urls['1.5'] = f"https://www.youtube.com/watch?v={self.youtube_id_1_5}"

# Get transcript information
transcripts = getattr(self, 'transcripts', {})

# Get track URL and rewrite if it's a static URL
track_url = getattr(self, 'track', None)
if track_url and track_url.startswith('/static/'):
from xmodule.contentstore.content import StaticContent
from common.djangoapps.static_replace.models import AssetBaseUrlConfig, AssetExcludedExtensionsConfig
# Convert /static/path to proper course asset URL
asset_path = track_url.replace('/static/', '')
try:
base_url = AssetBaseUrlConfig.get_base_url()
excluded_exts = AssetExcludedExtensionsConfig.get_excluded_extensions()
track_url = StaticContent.get_canonicalized_asset_path(
self.scope_ids.usage_id.context_key,
asset_path,
base_url,
excluded_exts
)
except Exception:
# If conversion fails, keep original URL
pass

# Get handout URL and rewrite if it's a static URL or asset key
handout_url = getattr(self, 'handout', None)
if handout_url:
from xmodule.contentstore.content import StaticContent
from common.djangoapps.static_replace.models import AssetBaseUrlConfig, AssetExcludedExtensionsConfig

if handout_url.startswith('/static/'):
# Convert /static/path to proper course asset URL
asset_path = handout_url.replace('/static/', '')
try:
base_url = AssetBaseUrlConfig.get_base_url()
excluded_exts = AssetExcludedExtensionsConfig.get_excluded_extensions()
handout_url = StaticContent.get_canonicalized_asset_path(
self.scope_ids.usage_id.context_key,
asset_path,
base_url,
excluded_exts
)
except Exception:
# If conversion fails, keep original URL
pass
elif handout_url.startswith('/asset-v1:') or handout_url.startswith('asset-v1:'):
# Convert existing asset key to full course asset URL
try:
# Remove leading slash if present
asset_key_str = handout_url[1:] if handout_url.startswith('/') else handout_url
# Extract just the filename from the asset key for get_canonicalized_asset_path
# asset-v1:Course+type@asset+block@filename.ext -> filename.ext
if '@' in asset_key_str:
filename = asset_key_str.split('@')[-1]
base_url = AssetBaseUrlConfig.get_base_url()
excluded_exts = AssetExcludedExtensionsConfig.get_excluded_extensions()
handout_url = StaticContent.get_canonicalized_asset_path(
self.scope_ids.usage_id.context_key,
filename,
base_url,
excluded_exts
)
except Exception:
# If conversion fails, keep original URL
pass

content = {
'video_urls': video_urls,
'youtube_urls': youtube_urls,
'transcripts': transcripts,
'start_time': self.start_time.total_seconds() if self.start_time else 0,
'end_time': self.end_time.total_seconds() if self.end_time else 0,
'download_allowed': getattr(self, 'download_video', False),
'track_url': track_url,
'handout_url': handout_url,
}

# Check if this block can be completed on view
can_complete_on_view = False
completion_service = self.runtime.service(self, 'completion')
if completion_service and completion_service.completion_tracking_enabled():
can_complete_on_view = completion_service.can_mark_block_complete_on_view(self)

data = {
'content': content,
'content_type': 'video',
'display_name': self.display_name or 'Video Component',
'block_id': str(self.scope_ids.usage_id),
'block_type': 'video',
'can_complete_on_view': can_complete_on_view,
'metadata': {
'download_allowed': getattr(self, 'download_video', False),
'has_captions': bool(transcripts),
'video_duration': self.saved_video_position.total_seconds() if self.saved_video_position else 0,
}
}

return Response(
json.dumps(data).encode('utf-8'),
content_type='application/json'
)

@XBlock.handler
def transcript(self, request, dispatch):
"""
Expand Down
Loading