feat: Export highlights to s3 for use by braze

Things other than highlights may be exported in the future. The storage class is flexible so backends other than s3 may be used in the future.
AA-461
This commit is contained in:
Matthew Piatetsky
2021-02-25 14:56:21 -05:00
parent 1938ccda71
commit 8d1d7b2222
10 changed files with 178 additions and 0 deletions

View 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

View 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)

View 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)()

View 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
)

View 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__)

View File

@@ -968,6 +968,8 @@ HTTPS = 'on'
ROOT_URLCONF = 'cms.urls'
COURSE_IMPORT_EXPORT_BUCKET = ''
COURSE_METADATA_EXPORT_BUCKET = ''
ALTERNATE_WORKER_QUEUES = 'lms'
STATIC_URL_BASE = '/static/'
@@ -1052,6 +1054,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
@@ -1389,6 +1393,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',

View File

@@ -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.

View File

@@ -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?

View File

@@ -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):