diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py index 0ab7cfacf042..2e10fdb3b23c 100644 --- a/openedx/features/offline_mode/assets_management.py +++ b/openedx/features/offline_mode/assets_management.py @@ -1,6 +1,7 @@ import shutil -import os import logging +import os +import requests from django.conf import settings from django.core.files.base import ContentFile @@ -11,7 +12,7 @@ from xmodule.exceptions import NotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError -# from .file_management import get_static_file_path, read_static_file +from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH log = logging.getLogger(__name__) @@ -43,6 +44,8 @@ def save_asset_file(xblock, path, filename): 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: static_path = get_static_file_path(filename) @@ -140,11 +143,17 @@ def is_modified(xblock): :return: """ file_path = os.path.join(base_storage_path(xblock), 'content_html.zip') - # file_path = f'{base_storage_path(xblock)}content_html.zip' # FIXME: change filename, and change to os.path.join - # + try: last_modified = default_storage.get_created_time(file_path) except OSError: return True return xblock.published_on > last_modified + + +def save_mathjax_to_local_static(): + if not default_storage.exists(MATHJAX_STATIC_PATH): + response = requests.get(MATHJAX_CDN_URL) + default_storage.save(MATHJAX_STATIC_PATH, ContentFile(response.content)) + log.info(f"Successfully saved MathJax to {MATHJAX_STATIC_PATH}") diff --git a/openedx/features/offline_mode/constants.py b/openedx/features/offline_mode/constants.py new file mode 100644 index 000000000000..161b7d0a3998 --- /dev/null +++ b/openedx/features/offline_mode/constants.py @@ -0,0 +1,5 @@ +import os + +MATHJAX_VERSION = '2.7.5' +MATHJAX_CDN_URL = f'https://cdn.jsdelivr.net/npm/mathjax@{MATHJAX_VERSION}/MathJax.js' +MATHJAX_STATIC_PATH = os.path.join('offline_mode_shared_static', 'js', f'MathJax-{MATHJAX_VERSION}.js') diff --git a/openedx/features/offline_mode/html_manipulator.py b/openedx/features/offline_mode/html_manipulator.py index adbd4ec4fa45..d095362a13e2 100644 --- a/openedx/features/offline_mode/html_manipulator.py +++ b/openedx/features/offline_mode/html_manipulator.py @@ -1,7 +1,14 @@ +import os import re + from bs4 import BeautifulSoup -from .assets_management import save_asset_file +from django.conf import settings + +from .assets_management import save_asset_file, save_mathjax_to_local_static +from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH + +RELATIVE_PATH_DIFF = '../../../../' class HtmlManipulator: @@ -16,21 +23,37 @@ def __init__(self, xblock, html_data): self.xblock = xblock def _replace_mathjax_link(self): - # FIXME: version shouldn't be hardcoded - mathjax_pattern = re.compile(r'src="https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js[^"]*"') - return mathjax_pattern.sub('src="/static/mathjax/MathJax.js"', self.html_data) + """ + Replace MathJax CDN link with local path to MathJax.js file. + """ + mathjax_pattern = re.compile(fr'src="{MATHJAX_CDN_URL}[^"]*"') + self.html_data = mathjax_pattern.sub( + f'src="{RELATIVE_PATH_DIFF}{MATHJAX_STATIC_PATH}"', + self.html_data + ) def _replace_static_links(self): - pattern = re.compile(r'/static/[\w./-]+') - return pattern.sub(self._replace_link, self.html_data) + """ + Replace static links with local links. + """ + static_links_pattern = os.path.join(settings.STATIC_URL, '[\w./-]+') + pattern = re.compile(fr'{static_links_pattern}') + self.html_data = pattern.sub(self._replace_link, self.html_data) def _replace_link(self, match): + """ + Returns the local path of the asset file. + """ link = match.group() - filename = link.split('/static/')[-1] + filename = link.split(settings.STATIC_URL)[-1] save_asset_file(self.xblock, link, filename) return f'assets/{filename}' - def _replace_iframe(self, soup): + @staticmethod + def _replace_iframe(soup): + """ + Replace iframe tags with anchor tags. + """ for node in soup.find_all('iframe'): replacement = soup.new_tag('p') tag_a = soup.new_tag('a') @@ -39,7 +62,13 @@ def _replace_iframe(self, soup): replacement.append(tag_a) node.replace_with(replacement) - def _add_js_bridge(self, soup): + @staticmethod + def _add_js_bridge(soup): + """ + Add JS bridge script to the HTML content. + :param soup: + :return: + """ script_tag = soup.new_tag('script') # FIXME: this script should be loaded from a file script_tag.string = """ @@ -83,8 +112,15 @@ def _add_js_bridge(self, soup): return soup def process_html(self): - self._replace_mathjax_link() + """ + Prepares HTML content for local use. + + Changes links to static files to paths to pre-generated static files for offline use. + """ + save_mathjax_to_local_static() self._replace_static_links() + self._replace_mathjax_link() + soup = BeautifulSoup(self.html_data, 'html.parser') self._replace_iframe(soup) self._add_js_bridge(soup)