Skip to content

Commit

Permalink
feat: new offline mode state
Browse files Browse the repository at this point in the history
  • Loading branch information
vzadorozhnii authored and NiedielnitsevIvan committed May 29, 2024
1 parent dbfe6a1 commit 074c5bd
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 40 deletions.
10 changes: 7 additions & 3 deletions lms/djangoapps/discussion/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
send_thread_created_notification,
send_response_endorsed_notifications
)
from lms.djangoapps.mobile_api.offline_mode.tasks import generate_course_media
from openedx.core.djangoapps.django_comment_common import signals
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.theming.helpers import get_current_site
from lms.djangoapps.offline_mode.utils.xblock_helpers import get_xblock_view_response, generate_request_with_service_user

log = logging.getLogger(__name__)

Expand All @@ -48,8 +48,12 @@ def update_discussions_on_course_publish(sender, course_key, **kwargs): # pylin
args=[context],
countdown=settings.DISCUSSION_SETTINGS['COURSE_PUBLISH_TASK_DELAY'],
)
# import pdb; pdb.set_trace()
generate_course_media(six.text_type(course_key))

import pdb;
pdb.set_trace()
request = generate_request_with_service_user()
result = get_xblock_view_response(request, 'block-v1:new+123+new+type@problem+block@f7693d5dde094f65a28485582125936d', 'student_view')
print(result)


@receiver(signals.comment_created)
Expand Down
7 changes: 0 additions & 7 deletions lms/djangoapps/mobile_api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,3 @@ class MobileApiConfig(AppConfig):
"""
name = 'lms.djangoapps.mobile_api'
verbose_name = "Mobile API"

def ready(self):
"""
Connect signal handlers.
"""
from lms.djangoapps.mobile_api.offline_mode import signals # pylint: disable=unused-import
from lms.djangoapps.mobile_api.offline_mode import tasks # pylint: disable=unused-import
11 changes: 8 additions & 3 deletions lms/djangoapps/offline_mode/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
from django.dispatch import receiver
from openedx_events.content_authoring.signals import XBLOCK_PUBLISHED


from xmodule.modulestore.django import SignalHandler

from .tasks import generate_course_media
from .utils.assets_management import remove_old_files


@receiver([XBLOCK_PUBLISHED])
def listen_course_publish(**kwargs):
if USER_TOURS_DISABLED.is_disabled():
def listen_xblock_publish(**kwargs):
if MEDIA_GENERATION_ENABLED.is_disabled():
return
generate_course_media.delay(six.text_type(course_key))
usage_key = UsageKey.from_string(kwargs.get('usage_key_string'))
xblock = modulestore().get_item(usage_key)
remove_old_files(xblock)

14 changes: 5 additions & 9 deletions lms/djangoapps/offline_mode/tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,21 @@
# Mocking the XBLOCK_PUBLISHED signal
XBLOCK_PUBLISHED = Signal(providing_args=["course_key"])

class ListenCoursePublishSignalTest(TestCase):
class ListenXBlockPublishSignalTest(TestCase):

def setUp(self):
self.course_key = 'course-v1:edX+DemoX+Demo_Course'
self.usage_key = ''

@patch('myapp.signals.generate_course_media.delay')
@patch('myapp.signals.USER_TOURS_DISABLED.is_disabled', return_value=False)
def test_listen_course_publish_signal_handler(self, mock_is_disabled, mock_generate_course_media):
# Simulate sending the signal
XBLOCK_PUBLISHED.send(sender=None, course_key=self.course_key)
XBLOCK_PUBLISHED.send(sender=None, course_key=self.usage_key)

# Check if the generate_course_media task was called with the correct arguments
mock_generate_course_media.assert_called_once_with(self.course_key)
mock_generate_course_media.assert_called_once_with(self.usage_key)

@patch('myapp.signals.generate_course_media.delay')
@patch('myapp.signals.USER_TOURS_DISABLED.is_disabled', return_value=True)
def test_listen_course_publish_signal_handler_disabled(self, mock_is_disabled, mock_generate_course_media):
# Simulate sending the signal
XBLOCK_PUBLISHED.send(sender=None, course_key=self.course_key)
XBLOCK_PUBLISHED.send(sender=None, course_key=self.usage_key)

# Check that the generate_course_media task was not called since the feature is disabled
mock_generate_course_media.assert_not_called()
45 changes: 45 additions & 0 deletions lms/djangoapps/offline_mode/tests/test_zip_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import unittest
from unittest.mock import Mock, patch, call
import zipfile

from lms.djangoapps.offline_mode.utils.zip_management import create_zip_file


class CreateZipFileTest(unittest.TestCase):

@patch('your_module.default_storage')
@patch('your_module.zipfile.ZipFile')
def test_create_zip_file(self, mock_zipfile, mock_default_storage):
# Setup mock paths
base_path = 'test_base_path/'
file_name = 'test_file.zip'
index_html_path = f'{base_path}index.html'
assets_path = f'{base_path}assets/'
asset_file_path = f'{assets_path}test_asset.txt'

# Mock default_storage behavior
mock_default_storage.path.side_effect = lambda x: x
mock_default_storage.listdir.side_effect = [
(['assets'], ['index.html']), # Root directory
([], ['test_asset.txt']) # Assets directory
]

# Mock zipfile behavior
mock_zf_instance = Mock()
mock_zipfile.return_value = mock_zf_instance

# Call the function to test
create_zip_file(base_path, file_name)

# Assertions
mock_zipfile.assert_called_once_with(f'{base_path}{file_name}', 'w')
mock_zf_instance.write.assert_any_call(index_html_path, 'index.html')
mock_zf_instance.write.assert_any_call(asset_file_path, 'assets/test_asset.txt')
mock_zf_instance.close.assert_called_once()

expected_calls = [
call(path=f'{base_path}index.html'),
call(path=f'{assets_path}'),
]
self.assertEqual(mock_default_storage.path.call_count, 2)
mock_default_storage.path.assert_has_calls(expected_calls, any_order=True)
12 changes: 12 additions & 0 deletions lms/djangoapps/offline_mode/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
URLs for mobile API
"""


from django.urls import include, path

from .views import OfflineXBlockStatusInfoView

urlpatterns = [
path('xblocks_status_info/', OfflineXBlockStatusInfoView.as_view(), name='offline_xblocks_info'),
]
75 changes: 59 additions & 16 deletions lms/djangoapps/offline_mode/utils/assets_management.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import shutil
import os
import logging

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
Expand All @@ -10,6 +12,9 @@
from .file_management import get_static_file_path, read_static_file


log = logging.getLogger(__name__)


def save_asset_file(xblock, path, filename):
"""
Saves an asset file to the default storage.
Expand All @@ -18,7 +23,7 @@ def save_asset_file(xblock, path, filename):
Otherwise, it fetches the asset from the AssetManager.
Args:
xblock (XBlock): The XBlock instance that provides context for the file.
xblock (XBlock): The XBlock instance
path (str): The path where the asset is located.
filename (str): The name of the file to be saved.
"""
Expand All @@ -36,28 +41,66 @@ def save_asset_file(xblock, path, filename):
default_storage.save(f'{base_path}assets/{filename}', ContentFile(content))


def remove_old_files(base_path):
def remove_old_files(xblock):
"""
Removes old files from the specified base path and its 'assets/' subdirectory.
Removes the 'asset' directory, 'index.html', and 'offline_content.zip' files
in the specified base path directory.
Args:
base_path (str): The base path from which to delete files.
(XBlock): The XBlock instance
"""
try:
directories, files = default_storage.listdir(base_path)
except OSError:
pass
else:
for file_name in files:
default_storage.delete(base_path + file_name)
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')
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:
directories, files = default_storage.listdir(base_path + 'assets/')
except OSError:
pass
else:
for file_name in files:
default_storage.delete(base_path + 'assets/' + file_name)
base_path = base_storage_path(xblock)

# 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):
Expand Down
Loading

0 comments on commit 074c5bd

Please sign in to comment.