Merge pull request #26744 from edx/AA-461

[AA-461] Export highlights to s3 for use by braze
This commit is contained in:
Matthew Piatetsky
2021-03-05 06:46:57 -05:00
committed by GitHub
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

@@ -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',

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