Skip to content
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

refactor: [AXM-583] refactor offline content generation #2573

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
42 changes: 42 additions & 0 deletions lms/templates/courseware/courseware-chromeless.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,45 @@
}());
</script>
% endif

% if is_offline_content:
<script type="text/javascript">
(function() {
function sendMessageToiOS(message) {
window?.webkit?.messageHandlers?.iOSBridge?.postMessage(message);
}

function sendMessageToAndroid(message) {
window?.AndroidBridge?.postMessage(message);
}

function receiveMessageFromiOS(message) {
console.log("Message received from iOS:", message);
}

function receiveMessageFromAndroid(message) {
console.log("Message received from Android:", message);
}

if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.iOSBridge) {
window.addEventListener("messageFromiOS", function (event) {
receiveMessageFromiOS(event.data);
});
}
if (window.AndroidBridge) {
window.addEventListener("messageFromAndroid", function (event) {
receiveMessageFromAndroid(event.data);
});
}
const originalAjax = $.ajax;
$.ajax = function (options) {
sendMessageToiOS(options);
sendMessageToiOS(JSON.stringify(options));
sendMessageToAndroid(options);
sendMessageToAndroid(JSON.stringify(options));
console.log(options, JSON.stringify(options));
return originalAjax.call(this, options);
};
}());
</script>
% endif
44 changes: 27 additions & 17 deletions openedx/features/offline_mode/assets_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import requests

from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage

from xmodule.assetstore.assetmgr import AssetManager
Expand Down Expand Up @@ -37,34 +36,43 @@ def read_static_file(path):
return file.read()


def save_asset_file(xblock, path, filename):
def save_asset_file(temp_dir, xblock, path, filename):
"""
Saves an asset file to the default storage.

If the filename contains a '/', it reads the static file directly from the file system.
Otherwise, it fetches the asset from the AssetManager.
Args:
temp_dir (str): The temporary directory where the assets are stored.
xblock (XBlock): The XBlock instance
path (str): The path where the asset is located.
filename (str): The name of the file to be saved.
"""
if filename.endswith('djangojs.js'):
return

try:
if '/' in filename:
if filename.startswith('assets/'):
asset_filename = filename.split('/')[-1]
asset_key = StaticContent.get_asset_key_from_path(xblock.location.course_key, asset_filename)
content = AssetManager.find(asset_key).data
file_path = os.path.join(temp_dir, filename)
else:
static_path = get_static_file_path(filename)
content = read_static_file(static_path)
else:
asset_key = StaticContent.get_asset_key_from_path(xblock.location.course_key, path)
content = AssetManager.find(asset_key).data
file_path = os.path.join(temp_dir, 'assets', filename)
except (ItemNotFoundError, NotFoundError):
log.info(f"Asset not found: {filename}")

else:
base_path = block_storage_path(xblock)
file_path = os.path.join(base_path, 'assets', filename)
default_storage.save(file_path, ContentFile(content))
create_subdirectories_for_asset(file_path)
with open(file_path, 'wb') as file:
file.write(content)


def create_subdirectories_for_asset(file_path):
Copy link
Collaborator

Choose a reason for hiding this comment

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

openedx/features/offline_mode/assets_management.py:70:0: C0116: Missing function or method docstring (missing-function-docstring)

Need to be fixed in the follow up @KyryloKireiev FYI

out_dir_name = '/'
for dir_name in file_path.split('/')[:-1]:
out_dir_name = os.path.join(out_dir_name, dir_name)
if out_dir_name and not os.path.exists(out_dir_name):
os.mkdir(out_dir_name)


def remove_old_files(xblock):
Expand Down Expand Up @@ -128,7 +136,7 @@ def block_storage_path(xblock=None, usage_key=None):
str: The constructed base storage path.
"""
loc = usage_key or getattr(xblock, 'location', None)
return f'{str(loc.course_key)}/{loc.block_id}/' if loc else ''
return f'{str(loc.course_key)}/' if loc else ''


def is_modified(xblock):
Expand All @@ -148,15 +156,17 @@ def is_modified(xblock):
return xblock.published_on > last_modified


def save_mathjax_to_xblock_assets(xblock):
def save_mathjax_to_xblock_assets(temp_dir):
"""
Saves MathJax to the local static directory.

If MathJax is not already saved, it fetches MathJax from
the CDN and saves it to the local static directory.
"""
file_path = os.path.join(block_storage_path(xblock), MATHJAX_STATIC_PATH)
if not default_storage.exists(file_path):
file_path = os.path.join(temp_dir, MATHJAX_STATIC_PATH)
if not os.path.exists(file_path):
response = requests.get(MATHJAX_CDN_URL)
default_storage.save(file_path, ContentFile(response.content))
with open(file_path, 'wb') as file:
file.write(response.content)

log.info(f"Successfully saved MathJax to {file_path}")
2 changes: 1 addition & 1 deletion openedx/features/offline_mode/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
MATHJAX_CDN_URL = f'https://cdn.jsdelivr.net/npm/mathjax@{MATHJAX_VERSION}/MathJax.js'
MATHJAX_STATIC_PATH = os.path.join('assets', 'js', f'MathJax-{MATHJAX_VERSION}.js')

DEFAULT_OFFLINE_SUPPORTED_XBLOCKS = ['problem']
DEFAULT_OFFLINE_SUPPORTED_XBLOCKS = ['html']
OFFLINE_SUPPORTED_XBLOCKS = getattr(settings, 'OFFLINE_SUPPORTED_XBLOCKS', DEFAULT_OFFLINE_SUPPORTED_XBLOCKS)
65 changes: 34 additions & 31 deletions openedx/features/offline_mode/html_manipulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,30 @@ class HtmlManipulator:
Changes links to static files to paths to pre-generated static files for offline use.
"""

def __init__(self, xblock, html_data):
def __init__(self, xblock, html_data, temp_dir):
self.html_data = html_data
self.xblock = xblock
self.temp_dir = temp_dir

def process_html(self):
"""
Prepares HTML content for local usage.

Changes links to static files to paths to pre-generated static files for offline use.
"""
self._replace_asset_links()
self._replace_static_links()
self._replace_mathjax_link()

soup = BeautifulSoup(self.html_data, 'html.parser')
self._replace_iframe(soup)
return str(soup)

def _replace_mathjax_link(self):
"""
Replace MathJax CDN link with local path to MathJax.js file.
"""
save_mathjax_to_xblock_assets(self.xblock)
save_mathjax_to_xblock_assets(self.temp_dir)
mathjax_pattern = re.compile(fr'src="{MATHJAX_CDN_URL}[^"]*"')
self.html_data = mathjax_pattern.sub(f'src="{MATHJAX_STATIC_PATH}"', self.html_data)

Expand All @@ -39,13 +54,29 @@ def _replace_static_links(self):
pattern = re.compile(fr'{static_links_pattern}')
self.html_data = pattern.sub(self._replace_link, self.html_data)

def _replace_asset_links(self):
"""
Replace static links with local links.
"""
pattern = re.compile(r'/assets/[\w./@:+-]+')
self.html_data = pattern.sub(self._replace_asset_link, self.html_data)

def _replace_asset_link(self, match):
"""
Returns the local path of the asset file.
"""
link = match.group()
filename = link[1:] if link.startswith('/') else link # Remove the leading '/'
save_asset_file(self.temp_dir, self.xblock, link, filename)
return filename

def _replace_link(self, match):
"""
Returns the local path of the asset file.
"""
link = match.group()
filename = link.split(settings.STATIC_URL)[-1]
save_asset_file(self.xblock, link, filename)
save_asset_file(self.temp_dir, self.xblock, link, filename)
return f'assets/{filename}'

@staticmethod
Expand All @@ -60,31 +91,3 @@ def _replace_iframe(soup):
tag_a.string = node.get('title', node.get('src'))
replacement.append(tag_a)
node.replace_with(replacement)

@staticmethod
def _add_js_bridge(soup):
"""
Add JS bridge script to the HTML content.
"""
script_tag = soup.new_tag('script')
with open('openedx/features/offline_mode/static/offline_mode/js/bridge.js', 'r') as file:
script_tag.string = file.read()
if soup.body:
soup.body.append(script_tag)
else:
soup.append(script_tag)
return soup

def process_html(self):
"""
Prepares HTML content for local use.

Changes links to static files to paths to pre-generated static files for offline use.
"""
self._replace_static_links()
self._replace_mathjax_link()

soup = BeautifulSoup(self.html_data, 'html.parser')
self._replace_iframe(soup)
self._add_js_bridge(soup)
return str(soup)
1 change: 1 addition & 0 deletions openedx/features/offline_mode/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def render_xblock_from_lms(self):
'on_courseware_page': True,
'is_learning_mfe': False,
'is_mobile_app': True,
'is_offline_content': True,
'render_course_wide_assets': True,
'LANGUAGE_CODE': 'en',

Expand Down
35 changes: 0 additions & 35 deletions openedx/features/offline_mode/static/offline_mode/js/bridge.js

This file was deleted.

Loading
Loading