Merge pull request #26744 from edx/AA-461
[AA-461] Export highlights to s3 for use by braze
This commit is contained in:
0
cms/djangoapps/export_course_metadata/__init__.py
Normal file
0
cms/djangoapps/export_course_metadata/__init__.py
Normal file
18
cms/djangoapps/export_course_metadata/apps.py
Normal file
18
cms/djangoapps/export_course_metadata/apps.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Define the export_course_metadata Django App.
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExportCourseMetadataConfig(AppConfig):
|
||||
"""
|
||||
App for exporting a subset of course metadata
|
||||
"""
|
||||
name = 'cms.djangoapps.export_course_metadata'
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Connect signal handler that exports course metadata
|
||||
"""
|
||||
from . import signals # pylint: disable=unused-import
|
||||
32
cms/djangoapps/export_course_metadata/signals.py
Normal file
32
cms/djangoapps/export_course_metadata/signals.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
This file exports metadata about the course.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.dispatch import receiver
|
||||
from openedx.core.djangoapps.schedules.content_highlights import get_all_course_highlights
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
|
||||
from .storage import course_metadata_export_storage
|
||||
from .toggles import EXPORT_COURSE_METADATA_FLAG
|
||||
|
||||
|
||||
@receiver(SignalHandler.course_published)
|
||||
def export_course_metadata(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Export course metadata on course publish.
|
||||
|
||||
File format
|
||||
'{"highlights": [["week1highlight1", "week1highlight2"], ["week1highlight1", "week1highlight2"], [], []]}'
|
||||
To retrieve highlights for week1, you would need to do
|
||||
course_metadata['highlights'][0]
|
||||
|
||||
This data is initially being used by Braze Connected Content to include
|
||||
section highlights in emails, but may be used for other things in the future.
|
||||
"""
|
||||
if EXPORT_COURSE_METADATA_FLAG.is_enabled():
|
||||
highlights = get_all_course_highlights(course_key)
|
||||
highlights_content = ContentFile(json.dumps({'highlights': highlights}))
|
||||
course_metadata_export_storage.save('course_metadata_export/{}.json'.format(course_key), highlights_content)
|
||||
20
cms/djangoapps/export_course_metadata/storage.py
Normal file
20
cms/djangoapps/export_course_metadata/storage.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Storage backend for course metadata export.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import get_storage_class
|
||||
from storages.backends.s3boto import S3BotoStorage
|
||||
|
||||
|
||||
class CourseMetadataExportS3Storage(S3BotoStorage): # pylint: disable=abstract-method
|
||||
"""
|
||||
S3 backend for course metadata export
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
bucket = settings.COURSE_METADATA_EXPORT_BUCKET
|
||||
super().__init__(bucket=bucket, custom_domain=None, querystring_auth=True)
|
||||
|
||||
course_metadata_export_storage = get_storage_class(settings.COURSE_METADATA_EXPORT_STORAGE)()
|
||||
56
cms/djangoapps/export_course_metadata/test_signals.py
Normal file
56
cms/djangoapps/export_course_metadata/test_signals.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Tests for signals.py
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from .signals import export_course_metadata
|
||||
from .toggles import EXPORT_COURSE_METADATA_FLAG
|
||||
|
||||
|
||||
@override_waffle_flag(EXPORT_COURSE_METADATA_FLAG, True)
|
||||
class TestExportCourseMetadata(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the export_course_metadata function
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
SignalHandler.course_published.disconnect(export_course_metadata)
|
||||
self.course = CourseFactory.create(highlights_enabled_for_messaging=True)
|
||||
self.course_key = self.course.id
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
SignalHandler.course_published.disconnect(export_course_metadata)
|
||||
|
||||
def _create_chapter(self, **kwargs):
|
||||
ItemFactory.create(
|
||||
parent=self.course,
|
||||
category='chapter',
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@patch('cms.djangoapps.export_course_metadata.signals.course_metadata_export_storage')
|
||||
@patch('cms.djangoapps.export_course_metadata.signals.ContentFile')
|
||||
def test_happy_path(self, patched_content, patched_storage):
|
||||
""" Ensure we call the storage class with the correct parameters and course metadata """
|
||||
all_highlights = [["week1highlight1", "week1highlight2"], ["week1highlight1", "week1highlight2"], [], []]
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
for week_highlights in all_highlights:
|
||||
self._create_chapter(highlights=week_highlights)
|
||||
SignalHandler.course_published.connect(export_course_metadata)
|
||||
SignalHandler.course_published.send(sender=None, course_key=self.course_key)
|
||||
patched_content.assert_called_once_with(
|
||||
'{"highlights": [["week1highlight1", "week1highlight2"], ["week1highlight1", "week1highlight2"], [], []]}'
|
||||
)
|
||||
patched_storage.save.assert_called_once_with(
|
||||
'course_metadata_export/course-v1:org.0+course_0+Run_0.json', patched_content.return_value
|
||||
)
|
||||
15
cms/djangoapps/export_course_metadata/toggles.py
Normal file
15
cms/djangoapps/export_course_metadata/toggles.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Toggles for export_course_metadata app
|
||||
"""
|
||||
|
||||
from edx_toggles.toggles import WaffleFlag
|
||||
|
||||
# .. toggle_name: export_course_metadata
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Export of course metadata (initially to s3 for use by braze)
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2021-03-01
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_tickets: AA-461
|
||||
EXPORT_COURSE_METADATA_FLAG = WaffleFlag('cms.export_course_metadata', __name__)
|
||||
@@ -967,6 +967,8 @@ HTTPS = 'on'
|
||||
ROOT_URLCONF = 'cms.urls'
|
||||
|
||||
COURSE_IMPORT_EXPORT_BUCKET = ''
|
||||
COURSE_METADATA_EXPORT_BUCKET = ''
|
||||
|
||||
ALTERNATE_WORKER_QUEUES = 'lms'
|
||||
|
||||
STATIC_URL_BASE = '/static/'
|
||||
@@ -1051,6 +1053,8 @@ derived('LOCALE_PATHS')
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
COURSE_IMPORT_EXPORT_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
COURSE_METADATA_EXPORT_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
|
||||
|
||||
##### EMBARGO #####
|
||||
EMBARGO_SITE_REDIRECT_URL = None
|
||||
@@ -1388,6 +1392,7 @@ INSTALLED_APPS = [
|
||||
'common.djangoapps.student.apps.StudentConfig', # misleading name due to sharing with lms
|
||||
'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run
|
||||
'cms.djangoapps.xblock_config.apps.XBlockConfig',
|
||||
'cms.djangoapps.export_course_metadata.apps.ExportCourseMetadataConfig',
|
||||
|
||||
# New (Blockstore-based) XBlock runtime
|
||||
'openedx.core.djangoapps.xblock.apps.StudioXBlockAppConfig',
|
||||
|
||||
@@ -350,6 +350,13 @@ else:
|
||||
|
||||
USER_TASKS_ARTIFACT_STORAGE = COURSE_IMPORT_EXPORT_STORAGE
|
||||
|
||||
COURSE_METADATA_EXPORT_BUCKET = ENV_TOKENS.get('COURSE_METADATA_EXPORT_BUCKET', '')
|
||||
|
||||
if COURSE_METADATA_EXPORT_BUCKET:
|
||||
COURSE_METADATA_EXPORT_STORAGE = 'cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage'
|
||||
else:
|
||||
COURSE_METADATA_EXPORT_STORAGE = DEFAULT_FILE_STORAGE
|
||||
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
|
||||
# The normal database user does not have enough permissions to run migrations.
|
||||
|
||||
@@ -14,6 +14,22 @@ from xmodule.modulestore.django import modulestore
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_all_course_highlights(course_key):
|
||||
"""
|
||||
This ignores access checks, since highlights may be lurking in currently
|
||||
inaccessible content.
|
||||
Returns a list of all the section highlights in the course
|
||||
"""
|
||||
try:
|
||||
course = _get_course_with_highlights(course_key)
|
||||
|
||||
except CourseUpdateDoesNotExist:
|
||||
return []
|
||||
else:
|
||||
highlights = [section.highlights for section in course.get_children() if not section.hide_from_toc]
|
||||
return highlights
|
||||
|
||||
|
||||
def course_has_highlights(course):
|
||||
"""
|
||||
Does the course have any highlights for any section/week in it?
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from openedx.core.djangoapps.schedules.content_highlights import (
|
||||
course_has_highlights_from_store,
|
||||
get_all_course_highlights,
|
||||
get_next_section_highlights,
|
||||
get_week_highlights
|
||||
)
|
||||
@@ -58,6 +59,14 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
assert course_has_highlights_from_store(self.course_key)
|
||||
assert get_week_highlights(self.user, self.course_key, week_num=1) == highlights
|
||||
|
||||
def test_get_all_course_highlights(self):
|
||||
all_highlights = [["week1highlight1", "week1highlight2"], ["week1highlight1", "week1highlight2"], [], []]
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
for week_highlights in all_highlights:
|
||||
self._create_chapter(highlights=week_highlights)
|
||||
|
||||
assert get_all_course_highlights(self.course_key) == all_highlights
|
||||
|
||||
def test_highlights_disabled_for_messaging(self):
|
||||
highlights = ['A test highlight.']
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
|
||||
Reference in New Issue
Block a user