From 3811290cfa69a533ff52b243620227ece6c35652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Mon, 3 Jun 2024 11:52:17 +0300 Subject: [PATCH 1/7] feat: [AXM-349] Implement media generation for problem xblock --- openedx/features/offline_mode/__init__.py | 0 .../offline_mode/assets_management.py | 150 ++++++++++++++++++ .../features/offline_mode/html_manipulator.py | 91 +++++++++++ openedx/features/offline_mode/tasks.py | 17 ++ openedx/features/offline_mode/utils.py | 53 +++++++ 5 files changed, 311 insertions(+) create mode 100644 openedx/features/offline_mode/__init__.py create mode 100644 openedx/features/offline_mode/assets_management.py create mode 100644 openedx/features/offline_mode/html_manipulator.py create mode 100644 openedx/features/offline_mode/tasks.py create mode 100644 openedx/features/offline_mode/utils.py diff --git a/openedx/features/offline_mode/__init__.py b/openedx/features/offline_mode/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py new file mode 100644 index 000000000000..0ab7cfacf042 --- /dev/null +++ b/openedx/features/offline_mode/assets_management.py @@ -0,0 +1,150 @@ +import shutil +import os +import logging + +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 +from xmodule.contentstore.content import StaticContent +from xmodule.exceptions import NotFoundError +from xmodule.modulestore.exceptions import ItemNotFoundError + +# from .file_management import get_static_file_path, read_static_file + + +log = logging.getLogger(__name__) + + +def get_static_file_path(relative_path): + """ + Constructs the absolute path for a static file based on its relative path. + """ + base_path = settings.STATIC_ROOT + return os.path.join(base_path, relative_path) + + +def read_static_file(path): + """ + Reads the contents of a static file in binary mode. + """ + with open(path, 'rb') as file: + return file.read() + + +def save_asset_file(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: + xblock (XBlock): The XBlock instance + path (str): The path where the asset is located. + filename (str): The name of the file to be saved. + """ + try: + if '/' in filename: + 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 + except (ItemNotFoundError, NotFoundError): + pass + else: + base_path = base_storage_path(xblock) + file_path = os.path.join(base_path, 'assets', filename) + default_storage.save(file_path, ContentFile(content)) + + +def remove_old_files(xblock): + """ + Removes the 'asset' directory, 'index.html', and 'offline_content.zip' files + in the specified base path directory. + Args: + (XBlock): The XBlock instance + """ + try: + base_path = base_storage_path(xblock) + + # Define the paths to the specific items to delete + asset_path = os.path.join(base_path, 'asset') + index_file_path = os.path.join(base_path, 'index.html') + # FIXME: change filename to block_id or move to constants + offline_zip_path = os.path.join(base_path, 'offline_content.zip') + + # Delete the 'asset' directory if it exists + if os.path.isdir(asset_path): + shutil.rmtree(asset_path) + log.info(f"Successfully deleted the directory: {asset_path}") + + # Delete the 'index.html' file if it exists + if os.path.isfile(index_file_path): + os.remove(index_file_path) + log.info(f"Successfully deleted the file: {index_file_path}") + + # Delete the 'offline_content.zip' file if it exists + if os.path.isfile(offline_zip_path): + os.remove(offline_zip_path) + log.info(f"Successfully deleted the file: {offline_zip_path}") + + except Exception as e: + log.error(f"Error occurred while deleting the files or directory: {e}") + + +def is_offline_content_present(xblock): + """ + Checks whether 'offline_content.zip' file is present in the specified base path directory. + Args: + xblock (XBlock): The XBlock instance + Returns: + bool: True if the file is present, False otherwise + """ + try: + base_path = base_storage_path(xblock) + # FIXME: change filename to block_id or move to constants + # Define the path to the 'offline_content.zip' file + offline_zip_path = os.path.join(base_path, 'offline_content.zip') + + # Check if the file exists + if os.path.isfile(offline_zip_path): + return True + else: + return False + + except Exception as e: + log.error(f"Error occurred while checking the file: {e}") + return False + + +def base_storage_path(xblock): + """ + Generates the base storage path for the given XBlock. + The path is constructed based on the XBlock's location, which includes the organization, + course, block type, and block ID. + Args: + xblock (XBlock): The XBlock instance for which to generate the storage path. + Returns: + str: The constructed base storage path. + """ + # FIXME: change to os.path.join? + loc = xblock.location + return f'{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/' + + +def is_modified(xblock): + """ + Check if the xblock has been modified since the last time the offline content was generated. + :param 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 diff --git a/openedx/features/offline_mode/html_manipulator.py b/openedx/features/offline_mode/html_manipulator.py new file mode 100644 index 000000000000..adbd4ec4fa45 --- /dev/null +++ b/openedx/features/offline_mode/html_manipulator.py @@ -0,0 +1,91 @@ +import re +from bs4 import BeautifulSoup + +from .assets_management import save_asset_file + + +class HtmlManipulator: + """ + Class to manipulate the HTML content of an XBlock. + + + """ + + def __init__(self, xblock, html_data): + self.html_data = 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) + + def _replace_static_links(self): + pattern = re.compile(r'/static/[\w./-]+') + return pattern.sub(self._replace_link, self.html_data) + + def _replace_link(self, match): + link = match.group() + filename = link.split('/static/')[-1] + save_asset_file(self.xblock, link, filename) + return f'assets/{filename}' + + def _replace_iframe(self, soup): + for node in soup.find_all('iframe'): + replacement = soup.new_tag('p') + tag_a = soup.new_tag('a') + tag_a['href'] = node.get('src') + tag_a.string = node.get('title', node.get('src')) + replacement.append(tag_a) + node.replace_with(replacement) + + def _add_js_bridge(self, soup): + script_tag = soup.new_tag('script') + # FIXME: this script should be loaded from a file + script_tag.string = """ + // JS bridge script + 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); + }; + """ + if soup.body: + soup.body.append(script_tag) + else: + soup.append(script_tag) + return soup + + def process_html(self): + self._replace_mathjax_link() + self._replace_static_links() + soup = BeautifulSoup(self.html_data, 'html.parser') + self._replace_iframe(soup) + self._add_js_bridge(soup) + return str(soup) diff --git a/openedx/features/offline_mode/tasks.py b/openedx/features/offline_mode/tasks.py new file mode 100644 index 000000000000..5e689f6725ef --- /dev/null +++ b/openedx/features/offline_mode/tasks.py @@ -0,0 +1,17 @@ +from celery import shared_task +from opaque_keys.edx.keys import CourseKey + +from xmodule.modulestore.django import modulestore + +from .renderer import XBlockRenderer +from .utils import generate_offline_content + + +@shared_task +def get_rendered_xblock_from_lms(course_id): + course_key = CourseKey.from_string(course_id) + for xblock in modulestore().get_items(course_key, qualifiers={'category': 'problem'}): + # if is_offline_supported(xblock): + # continue + html_data = XBlockRenderer(str(xblock.id)).render_xblock_from_lms() + generate_offline_content(xblock, html_data) diff --git a/openedx/features/offline_mode/utils.py b/openedx/features/offline_mode/utils.py new file mode 100644 index 000000000000..5f3b193af973 --- /dev/null +++ b/openedx/features/offline_mode/utils.py @@ -0,0 +1,53 @@ +import os +import logging + +from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage + +from zipfile import ZipFile + + +from .assets_management import base_storage_path, remove_old_files, is_modified +from .html_manipulator import HtmlManipulator + +User = get_user_model() +log = logging.getLogger(__name__) + + +def create_zip_file(base_path, file_name): + zf = ZipFile(default_storage.path(base_path + file_name), 'w') + zf.write(default_storage.path(base_path + 'index.html'), 'index.html') + + def add_files_to_zip(zip_file, current_base_path, current_path_in_zip): + try: + directories, files = default_storage.listdir(current_base_path) + except OSError: + return + + for file_name in files: + full_path = os.path.join(current_base_path, file_name) + zip_file.write(full_path, os.path.join(current_path_in_zip, file_name)) + + for directory in directories: + add_files_to_zip(zip_file, os.path.join(current_base_path, directory), + os.path.join(current_path_in_zip, directory)) + + add_files_to_zip(zf, default_storage.path(base_path + 'assets/'), 'assets') + zf.close() + + +def generate_offline_content(xblock, html_data): + if not is_modified(xblock): + return + + base_path = base_storage_path(xblock) + remove_old_files(xblock) + html_manipulator = HtmlManipulator(xblock, html_data) + updated_html = html_manipulator.process_html() + + default_storage.save( + os.path.join(base_path, 'index.html'), + ContentFile(updated_html), + ) + create_zip_file(base_path, 'offline_content.zip') From dfb7e6b4aaba3eb68a537841870bc67e0555c2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Wed, 5 Jun 2024 12:02:08 +0300 Subject: [PATCH 2/7] feat: [AXM-349] refactor offline content generation --- .../offline_mode/assets_management.py | 17 ++++-- openedx/features/offline_mode/constants.py | 5 ++ .../features/offline_mode/html_manipulator.py | 56 +++++++++++++++---- 3 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 openedx/features/offline_mode/constants.py 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) From 98249eebe610da2f1038ba4e891c7af5b780619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Wed, 5 Jun 2024 13:43:33 +0300 Subject: [PATCH 3/7] style: [AXM-349] fix style issues --- .../offline_mode/assets_management.py | 51 +++++++++---------- openedx/features/offline_mode/constants.py | 10 ++++ .../features/offline_mode/html_manipulator.py | 48 ++++------------- openedx/features/offline_mode/renderer.py | 2 +- .../static/offline_mode/js/bridge.js | 35 +++++++++++++ openedx/features/offline_mode/tasks.py | 22 +++++--- openedx/features/offline_mode/utils.py | 26 +++++++++- 7 files changed, 119 insertions(+), 75 deletions(-) create mode 100644 openedx/features/offline_mode/static/offline_mode/js/bridge.js diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py index 2e10fdb3b23c..8a4643188936 100644 --- a/openedx/features/offline_mode/assets_management.py +++ b/openedx/features/offline_mode/assets_management.py @@ -1,3 +1,6 @@ +""" +This module contains utility functions for managing assets and files. +""" import shutil import logging import os @@ -12,7 +15,7 @@ from xmodule.exceptions import NotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError -from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH +from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH, OFFLINE_CONTENT_ARCHIVE_NAME log = logging.getLogger(__name__) @@ -37,6 +40,7 @@ def read_static_file(path): def save_asset_file(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: @@ -44,8 +48,6 @@ 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) @@ -70,12 +72,9 @@ def remove_old_files(xblock): """ try: base_path = base_storage_path(xblock) - - # Define the paths to the specific items to delete asset_path = os.path.join(base_path, 'asset') index_file_path = os.path.join(base_path, 'index.html') - # FIXME: change filename to block_id or move to constants - offline_zip_path = os.path.join(base_path, 'offline_content.zip') + offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) # Delete the 'asset' directory if it exists if os.path.isdir(asset_path): @@ -83,47 +82,37 @@ def remove_old_files(xblock): log.info(f"Successfully deleted the directory: {asset_path}") # Delete the 'index.html' file if it exists - if os.path.isfile(index_file_path): + if default_storage.exists(index_file_path): os.remove(index_file_path) log.info(f"Successfully deleted the file: {index_file_path}") # Delete the 'offline_content.zip' file if it exists - if os.path.isfile(offline_zip_path): + if default_storage.exists(offline_zip_path): os.remove(offline_zip_path) log.info(f"Successfully deleted the file: {offline_zip_path}") - except Exception as e: + except OSError as e: log.error(f"Error occurred while deleting the files or directory: {e}") def is_offline_content_present(xblock): """ Checks whether 'offline_content.zip' file is present in the specified base path directory. + Args: xblock (XBlock): The XBlock instance Returns: bool: True if the file is present, False otherwise """ - try: - base_path = base_storage_path(xblock) - # FIXME: change filename to block_id or move to constants - # Define the path to the 'offline_content.zip' file - offline_zip_path = os.path.join(base_path, 'offline_content.zip') - - # Check if the file exists - if os.path.isfile(offline_zip_path): - return True - else: - return False - - except Exception as e: - log.error(f"Error occurred while checking the file: {e}") - return False + base_path = base_storage_path(xblock) + offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) + return default_storage.exists(offline_zip_path) def base_storage_path(xblock): """ Generates the base storage path for the given XBlock. + The path is constructed based on the XBlock's location, which includes the organization, course, block type, and block ID. Args: @@ -131,7 +120,6 @@ def base_storage_path(xblock): Returns: str: The constructed base storage path. """ - # FIXME: change to os.path.join? loc = xblock.location return f'{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/' @@ -139,8 +127,9 @@ def base_storage_path(xblock): def is_modified(xblock): """ Check if the xblock has been modified since the last time the offline content was generated. - :param xblock: - :return: + + Args: + xblock (XBlock): The XBlock instance to check. """ file_path = os.path.join(base_storage_path(xblock), 'content_html.zip') @@ -153,6 +142,12 @@ def is_modified(xblock): def save_mathjax_to_local_static(): + """ + 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. + """ if not default_storage.exists(MATHJAX_STATIC_PATH): response = requests.get(MATHJAX_CDN_URL) default_storage.save(MATHJAX_STATIC_PATH, ContentFile(response.content)) diff --git a/openedx/features/offline_mode/constants.py b/openedx/features/offline_mode/constants.py index 161b7d0a3998..ce6f8782d8bb 100644 --- a/openedx/features/offline_mode/constants.py +++ b/openedx/features/offline_mode/constants.py @@ -1,5 +1,15 @@ +""" +Constants for offline mode app. +""" import os +from django.conf import settings + 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') + +OFFLINE_CONTENT_ARCHIVE_NAME = 'offline_content.zip' + +DEFAULT_OFFLINE_SUPPORTED_XBLOCKS = ['html', 'problem'] +OFFLINE_SUPPORTED_XBLOCKS = getattr(settings, 'OFFLINE_SUPPORTED_XBLOCKS', DEFAULT_OFFLINE_SUPPORTED_XBLOCKS) diff --git a/openedx/features/offline_mode/html_manipulator.py b/openedx/features/offline_mode/html_manipulator.py index d095362a13e2..2e785d01a6ca 100644 --- a/openedx/features/offline_mode/html_manipulator.py +++ b/openedx/features/offline_mode/html_manipulator.py @@ -1,3 +1,6 @@ +""" +Module to prepare HTML content for offline use. +""" import os import re @@ -8,14 +11,16 @@ from .assets_management import save_asset_file, save_mathjax_to_local_static from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH + +# Relative path difference between the HTML content and the shared static files. RELATIVE_PATH_DIFF = '../../../../' class HtmlManipulator: """ - Class to manipulate the HTML content of an XBlock. - + Class to prepare HTML content for offline use. + Changes links to static files to paths to pre-generated static files for offline use. """ def __init__(self, xblock, html_data): @@ -36,7 +41,7 @@ def _replace_static_links(self): """ Replace static links with local links. """ - static_links_pattern = os.path.join(settings.STATIC_URL, '[\w./-]+') + static_links_pattern = os.path.join(settings.STATIC_URL, r'[\w./-]+') pattern = re.compile(fr'{static_links_pattern}') self.html_data = pattern.sub(self._replace_link, self.html_data) @@ -70,41 +75,8 @@ def _add_js_bridge(soup): :return: """ script_tag = soup.new_tag('script') - # FIXME: this script should be loaded from a file - script_tag.string = """ - // JS bridge script - 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); - }; - """ + 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: diff --git a/openedx/features/offline_mode/renderer.py b/openedx/features/offline_mode/renderer.py index 9de75e76f9a9..2262492f6295 100644 --- a/openedx/features/offline_mode/renderer.py +++ b/openedx/features/offline_mode/renderer.py @@ -9,7 +9,7 @@ from django.contrib.sessions.backends.db import SessionStore from django.http import HttpRequest -from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.keys import UsageKey from xmodule.modulestore.django import modulestore from common.djangoapps.edxmako.shortcuts import render_to_string diff --git a/openedx/features/offline_mode/static/offline_mode/js/bridge.js b/openedx/features/offline_mode/static/offline_mode/js/bridge.js new file mode 100644 index 000000000000..03e6755f28cd --- /dev/null +++ b/openedx/features/offline_mode/static/offline_mode/js/bridge.js @@ -0,0 +1,35 @@ +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); +}; diff --git a/openedx/features/offline_mode/tasks.py b/openedx/features/offline_mode/tasks.py index 5e689f6725ef..dd487cbbb28f 100644 --- a/openedx/features/offline_mode/tasks.py +++ b/openedx/features/offline_mode/tasks.py @@ -1,17 +1,25 @@ +""" +Tasks for offline mode feature. +""" from celery import shared_task +from edx_django_utils.monitoring import set_code_owner_attribute from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore +from .constants import OFFLINE_SUPPORTED_XBLOCKS from .renderer import XBlockRenderer -from .utils import generate_offline_content +from .utils import generate_offline_content, is_offline_supported_block @shared_task -def get_rendered_xblock_from_lms(course_id): +@set_code_owner_attribute +def generate_offline_content_for_course(course_id): + """ + Generates offline content for all supported XBlocks in the course. + """ course_key = CourseKey.from_string(course_id) - for xblock in modulestore().get_items(course_key, qualifiers={'category': 'problem'}): - # if is_offline_supported(xblock): - # continue - html_data = XBlockRenderer(str(xblock.id)).render_xblock_from_lms() - generate_offline_content(xblock, html_data) + for xblock in modulestore().get_items(course_key, qualifiers={'category': OFFLINE_SUPPORTED_XBLOCKS}): + if is_offline_supported_block(xblock): + html_data = XBlockRenderer(str(xblock.id)).render_xblock_from_lms() + generate_offline_content(xblock, html_data) diff --git a/openedx/features/offline_mode/utils.py b/openedx/features/offline_mode/utils.py index 5f3b193af973..418c49465caa 100644 --- a/openedx/features/offline_mode/utils.py +++ b/openedx/features/offline_mode/utils.py @@ -1,3 +1,6 @@ +""" +Utility functions and classes for offline mode. +""" import os import logging @@ -9,6 +12,7 @@ from .assets_management import base_storage_path, remove_old_files, is_modified +from .constants import OFFLINE_CONTENT_ARCHIVE_NAME, OFFLINE_SUPPORTED_XBLOCKS from .html_manipulator import HtmlManipulator User = get_user_model() @@ -16,10 +20,16 @@ def create_zip_file(base_path, file_name): + """ + Creates a zip file with the content of the base_path directory. + """ zf = ZipFile(default_storage.path(base_path + file_name), 'w') zf.write(default_storage.path(base_path + 'index.html'), 'index.html') def add_files_to_zip(zip_file, current_base_path, current_path_in_zip): + """ + Recursively adds files to the zip file. + """ try: directories, files = default_storage.listdir(current_base_path) except OSError: @@ -38,6 +48,13 @@ def add_files_to_zip(zip_file, current_base_path, current_path_in_zip): def generate_offline_content(xblock, html_data): + """ + Generates archive with XBlock content for offline mode. + + Args: + xblock (XBlock): The XBlock instance + html_data (str): The HTML data of the XBlock + """ if not is_modified(xblock): return @@ -50,4 +67,11 @@ def generate_offline_content(xblock, html_data): os.path.join(base_path, 'index.html'), ContentFile(updated_html), ) - create_zip_file(base_path, 'offline_content.zip') + create_zip_file(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) + + +def is_offline_supported_block(xblock): + """ + Returns True if the block is supported for offline mode, False otherwise. + """ + return xblock.location.block_type in OFFLINE_SUPPORTED_XBLOCKS From 4caafacb23b26f7a9c09f2c046ad6111f41f285d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Thu, 6 Jun 2024 10:35:18 +0300 Subject: [PATCH 4/7] refactor: [AXM-349] move MathJax to assets --- .../offline_mode/assets_management.py | 39 ++++++++++--------- openedx/features/offline_mode/constants.py | 2 +- .../features/offline_mode/html_manipulator.py | 15 ++----- openedx/features/offline_mode/utils.py | 13 +++---- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py index 8a4643188936..e942bcf6e3db 100644 --- a/openedx/features/offline_mode/assets_management.py +++ b/openedx/features/offline_mode/assets_management.py @@ -56,30 +56,31 @@ def save_asset_file(xblock, path, filename): asset_key = StaticContent.get_asset_key_from_path(xblock.location.course_key, path) content = AssetManager.find(asset_key).data except (ItemNotFoundError, NotFoundError): - pass + log.info(f"Asset not found: {filename}") + else: - base_path = base_storage_path(xblock) + base_path = block_storage_path(xblock) file_path = os.path.join(base_path, 'assets', filename) default_storage.save(file_path, ContentFile(content)) def remove_old_files(xblock): """ - Removes the 'asset' directory, 'index.html', and 'offline_content.zip' files + Removes the 'assets' directory, 'index.html', and 'offline_content.zip' files in the specified base path directory. Args: (XBlock): The XBlock instance """ try: - base_path = base_storage_path(xblock) - asset_path = os.path.join(base_path, 'asset') + base_path = block_storage_path(xblock) + assets_path = os.path.join(base_path, 'assets') index_file_path = os.path.join(base_path, 'index.html') offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) - # Delete the 'asset' directory if it exists - if os.path.isdir(asset_path): - shutil.rmtree(asset_path) - log.info(f"Successfully deleted the directory: {asset_path}") + # Delete the 'assets' directory if it exists + if os.path.isdir(assets_path): + shutil.rmtree(assets_path) + log.info(f"Successfully deleted the directory: {assets_path}") # Delete the 'index.html' file if it exists if default_storage.exists(index_file_path): @@ -104,12 +105,12 @@ def is_offline_content_present(xblock): Returns: bool: True if the file is present, False otherwise """ - base_path = base_storage_path(xblock) + base_path = block_storage_path(xblock) offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) return default_storage.exists(offline_zip_path) -def base_storage_path(xblock): +def block_storage_path(xblock=None, usage_key=None): """ Generates the base storage path for the given XBlock. @@ -117,11 +118,12 @@ def base_storage_path(xblock): course, block type, and block ID. Args: xblock (XBlock): The XBlock instance for which to generate the storage path. + usage_key (UsageKey): The UsageKey of the XBlock. Returns: str: The constructed base storage path. """ - loc = xblock.location - return f'{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/' + loc = usage_key or getattr(xblock, 'location', None) + return f'{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/' if loc else '' def is_modified(xblock): @@ -131,7 +133,7 @@ def is_modified(xblock): Args: xblock (XBlock): The XBlock instance to check. """ - file_path = os.path.join(base_storage_path(xblock), 'content_html.zip') + file_path = os.path.join(block_storage_path(xblock), 'content_html.zip') try: last_modified = default_storage.get_created_time(file_path) @@ -141,14 +143,15 @@ def is_modified(xblock): return xblock.published_on > last_modified -def save_mathjax_to_local_static(): +def save_mathjax_to_xblock_assets(xblock): """ 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. """ - if not default_storage.exists(MATHJAX_STATIC_PATH): + file_path = os.path.join(block_storage_path(xblock), MATHJAX_STATIC_PATH) + if not default_storage.exists(file_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}") + default_storage.save(file_path, ContentFile(response.content)) + log.info(f"Successfully saved MathJax to {file_path}") diff --git a/openedx/features/offline_mode/constants.py b/openedx/features/offline_mode/constants.py index ce6f8782d8bb..c5e9bd2dfcbc 100644 --- a/openedx/features/offline_mode/constants.py +++ b/openedx/features/offline_mode/constants.py @@ -7,7 +7,7 @@ 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') +MATHJAX_STATIC_PATH = os.path.join('assets', 'js', f'MathJax-{MATHJAX_VERSION}.js') OFFLINE_CONTENT_ARCHIVE_NAME = 'offline_content.zip' diff --git a/openedx/features/offline_mode/html_manipulator.py b/openedx/features/offline_mode/html_manipulator.py index 2e785d01a6ca..f8fce464ae2e 100644 --- a/openedx/features/offline_mode/html_manipulator.py +++ b/openedx/features/offline_mode/html_manipulator.py @@ -8,14 +8,10 @@ from django.conf import settings -from .assets_management import save_asset_file, save_mathjax_to_local_static +from .assets_management import save_asset_file, save_mathjax_to_xblock_assets from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH -# Relative path difference between the HTML content and the shared static files. -RELATIVE_PATH_DIFF = '../../../../' - - class HtmlManipulator: """ Class to prepare HTML content for offline use. @@ -31,11 +27,9 @@ def _replace_mathjax_link(self): """ Replace MathJax CDN link with local path to MathJax.js file. """ + save_mathjax_to_xblock_assets(self.xblock) 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 - ) + self.html_data = mathjax_pattern.sub(f'src="{MATHJAX_STATIC_PATH}"', self.html_data) def _replace_static_links(self): """ @@ -71,8 +65,6 @@ def _replace_iframe(soup): def _add_js_bridge(soup): """ Add JS bridge script to the HTML content. - :param soup: - :return: """ script_tag = soup.new_tag('script') with open('openedx/features/offline_mode/static/offline_mode/js/bridge.js', 'r') as file: @@ -89,7 +81,6 @@ def process_html(self): 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() diff --git a/openedx/features/offline_mode/utils.py b/openedx/features/offline_mode/utils.py index 418c49465caa..e54c70811466 100644 --- a/openedx/features/offline_mode/utils.py +++ b/openedx/features/offline_mode/utils.py @@ -10,8 +10,7 @@ from zipfile import ZipFile - -from .assets_management import base_storage_path, remove_old_files, is_modified +from .assets_management import block_storage_path, remove_old_files, is_modified from .constants import OFFLINE_CONTENT_ARCHIVE_NAME, OFFLINE_SUPPORTED_XBLOCKS from .html_manipulator import HtmlManipulator @@ -31,13 +30,13 @@ def add_files_to_zip(zip_file, current_base_path, current_path_in_zip): Recursively adds files to the zip file. """ try: - directories, files = default_storage.listdir(current_base_path) + directories, filenames = default_storage.listdir(current_base_path) except OSError: return - for file_name in files: - full_path = os.path.join(current_base_path, file_name) - zip_file.write(full_path, os.path.join(current_path_in_zip, file_name)) + for filename in filenames: + full_path = os.path.join(current_base_path, filename) + zip_file.write(full_path, os.path.join(current_path_in_zip, filename)) for directory in directories: add_files_to_zip(zip_file, os.path.join(current_base_path, directory), @@ -58,7 +57,7 @@ def generate_offline_content(xblock, html_data): if not is_modified(xblock): return - base_path = base_storage_path(xblock) + base_path = block_storage_path(xblock) remove_old_files(xblock) html_manipulator = HtmlManipulator(xblock, html_data) updated_html = html_manipulator.process_html() From 035064939e06f923ab6f724eba80682ca3b16491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Thu, 6 Jun 2024 10:44:43 +0300 Subject: [PATCH 5/7] fix: [AXM-349] fix wrong filename --- openedx/features/offline_mode/assets_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py index e942bcf6e3db..1b24505f0a0e 100644 --- a/openedx/features/offline_mode/assets_management.py +++ b/openedx/features/offline_mode/assets_management.py @@ -133,7 +133,7 @@ def is_modified(xblock): Args: xblock (XBlock): The XBlock instance to check. """ - file_path = os.path.join(block_storage_path(xblock), 'content_html.zip') + file_path = os.path.join(block_storage_path(xblock), OFFLINE_CONTENT_ARCHIVE_NAME) try: last_modified = default_storage.get_created_time(file_path) From d9a6a05cd7f84a13beaa9599459e0bd77a44e2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Thu, 6 Jun 2024 19:05:12 +0300 Subject: [PATCH 6/7] refactor: [AXM-349] refactor generated file pathes and generating task launching --- .../offline_mode/assets_management.py | 21 ++++++++++------- openedx/features/offline_mode/constants.py | 4 +--- openedx/features/offline_mode/tasks.py | 23 ++++++++++++++----- openedx/features/offline_mode/utils.py | 19 +++++++-------- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/openedx/features/offline_mode/assets_management.py b/openedx/features/offline_mode/assets_management.py index 1b24505f0a0e..46c6af06c518 100644 --- a/openedx/features/offline_mode/assets_management.py +++ b/openedx/features/offline_mode/assets_management.py @@ -15,7 +15,7 @@ from xmodule.exceptions import NotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError -from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH, OFFLINE_CONTENT_ARCHIVE_NAME +from .constants import MATHJAX_CDN_URL, MATHJAX_STATIC_PATH log = logging.getLogger(__name__) @@ -48,6 +48,9 @@ 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) @@ -75,7 +78,7 @@ def remove_old_files(xblock): base_path = block_storage_path(xblock) assets_path = os.path.join(base_path, 'assets') index_file_path = os.path.join(base_path, 'index.html') - offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) + offline_zip_path = os.path.join(base_path, f'{xblock.location.block_id}.zip') # Delete the 'assets' directory if it exists if os.path.isdir(assets_path): @@ -96,18 +99,20 @@ def remove_old_files(xblock): log.error(f"Error occurred while deleting the files or directory: {e}") -def is_offline_content_present(xblock): +def get_offline_block_content_path(xblock=None, usage_key=None): """ Checks whether 'offline_content.zip' file is present in the specified base path directory. Args: xblock (XBlock): The XBlock instance + usage_key (UsageKey): The UsageKey of the XBlock Returns: bool: True if the file is present, False otherwise """ - base_path = block_storage_path(xblock) - offline_zip_path = os.path.join(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) - return default_storage.exists(offline_zip_path) + usage_key = usage_key or getattr(xblock, 'location', None) + base_path = block_storage_path(usage_key=usage_key) + offline_zip_path = os.path.join(base_path, f'{usage_key.block_id}.zip') + return offline_zip_path if default_storage.exists(offline_zip_path) else None def block_storage_path(xblock=None, usage_key=None): @@ -123,7 +128,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'{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/' if loc else '' + return f'{str(loc.course_key)}/{loc.block_id}/' if loc else '' def is_modified(xblock): @@ -133,7 +138,7 @@ def is_modified(xblock): Args: xblock (XBlock): The XBlock instance to check. """ - file_path = os.path.join(block_storage_path(xblock), OFFLINE_CONTENT_ARCHIVE_NAME) + file_path = os.path.join(block_storage_path(xblock), f'{xblock.location.block_id}.zip') try: last_modified = default_storage.get_created_time(file_path) diff --git a/openedx/features/offline_mode/constants.py b/openedx/features/offline_mode/constants.py index c5e9bd2dfcbc..b6609a4ce520 100644 --- a/openedx/features/offline_mode/constants.py +++ b/openedx/features/offline_mode/constants.py @@ -9,7 +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') -OFFLINE_CONTENT_ARCHIVE_NAME = 'offline_content.zip' - -DEFAULT_OFFLINE_SUPPORTED_XBLOCKS = ['html', 'problem'] +DEFAULT_OFFLINE_SUPPORTED_XBLOCKS = ['problem'] OFFLINE_SUPPORTED_XBLOCKS = getattr(settings, 'OFFLINE_SUPPORTED_XBLOCKS', DEFAULT_OFFLINE_SUPPORTED_XBLOCKS) diff --git a/openedx/features/offline_mode/tasks.py b/openedx/features/offline_mode/tasks.py index dd487cbbb28f..75e786f43ca4 100644 --- a/openedx/features/offline_mode/tasks.py +++ b/openedx/features/offline_mode/tasks.py @@ -3,13 +3,13 @@ """ from celery import shared_task from edx_django_utils.monitoring import set_code_owner_attribute -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey from xmodule.modulestore.django import modulestore from .constants import OFFLINE_SUPPORTED_XBLOCKS from .renderer import XBlockRenderer -from .utils import generate_offline_content, is_offline_supported_block +from .utils import generate_offline_content @shared_task @@ -19,7 +19,18 @@ def generate_offline_content_for_course(course_id): Generates offline content for all supported XBlocks in the course. """ course_key = CourseKey.from_string(course_id) - for xblock in modulestore().get_items(course_key, qualifiers={'category': OFFLINE_SUPPORTED_XBLOCKS}): - if is_offline_supported_block(xblock): - html_data = XBlockRenderer(str(xblock.id)).render_xblock_from_lms() - generate_offline_content(xblock, html_data) + for offline_supported_block_type in OFFLINE_SUPPORTED_XBLOCKS: + for xblock in modulestore().get_items(course_key, qualifiers={'category': offline_supported_block_type}): + html_data = XBlockRenderer(str(xblock.location)).render_xblock_from_lms() + generate_offline_content_for_block.apply_async([str(xblock.location), html_data]) + + +@shared_task +@set_code_owner_attribute +def generate_offline_content_for_block(block_id, html_data): + """ + Generates offline content for the specified block. + """ + block_usage_key = UsageKey.from_string(block_id) + xblock = modulestore().get_item(block_usage_key) + generate_offline_content(xblock, html_data) diff --git a/openedx/features/offline_mode/utils.py b/openedx/features/offline_mode/utils.py index e54c70811466..466ab3c849df 100644 --- a/openedx/features/offline_mode/utils.py +++ b/openedx/features/offline_mode/utils.py @@ -11,7 +11,7 @@ from zipfile import ZipFile from .assets_management import block_storage_path, remove_old_files, is_modified -from .constants import OFFLINE_CONTENT_ARCHIVE_NAME, OFFLINE_SUPPORTED_XBLOCKS +from .constants import OFFLINE_SUPPORTED_XBLOCKS from .html_manipulator import HtmlManipulator User = get_user_model() @@ -39,11 +39,15 @@ def add_files_to_zip(zip_file, current_base_path, current_path_in_zip): zip_file.write(full_path, os.path.join(current_path_in_zip, filename)) for directory in directories: - add_files_to_zip(zip_file, os.path.join(current_base_path, directory), - os.path.join(current_path_in_zip, directory)) + add_files_to_zip( + zip_file, + os.path.join(current_base_path, directory), + os.path.join(current_path_in_zip, directory) + ) add_files_to_zip(zf, default_storage.path(base_path + 'assets/'), 'assets') zf.close() + log.info(f'Offline content for {file_name} has been generated.') def generate_offline_content(xblock, html_data): @@ -66,11 +70,4 @@ def generate_offline_content(xblock, html_data): os.path.join(base_path, 'index.html'), ContentFile(updated_html), ) - create_zip_file(base_path, OFFLINE_CONTENT_ARCHIVE_NAME) - - -def is_offline_supported_block(xblock): - """ - Returns True if the block is supported for offline mode, False otherwise. - """ - return xblock.location.block_type in OFFLINE_SUPPORTED_XBLOCKS + create_zip_file(base_path, f'{xblock.location.block_id}.zip') From 450e04a89b317e58afb9657cde6272715a5691d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Fri, 7 Jun 2024 09:37:28 +0300 Subject: [PATCH 7/7] style: [AXM-349] remove unused import --- openedx/features/offline_mode/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openedx/features/offline_mode/utils.py b/openedx/features/offline_mode/utils.py index 466ab3c849df..772a2fdb9b6b 100644 --- a/openedx/features/offline_mode/utils.py +++ b/openedx/features/offline_mode/utils.py @@ -11,7 +11,6 @@ from zipfile import ZipFile from .assets_management import block_storage_path, remove_old_files, is_modified -from .constants import OFFLINE_SUPPORTED_XBLOCKS from .html_manipulator import HtmlManipulator User = get_user_model()