Merge pull request #29779 from open-craft/jill/TNL-8746
[BD-14] Install the blockstore app into edx-platform, behind a waffle switch
This commit is contained in:
@@ -111,6 +111,10 @@ from lms.envs.common import (
|
||||
# Enterprise service settings
|
||||
ENTERPRISE_CATALOG_INTERNAL_ROOT_URL,
|
||||
|
||||
# Blockstore
|
||||
BLOCKSTORE_USE_BLOCKSTORE_APP_API,
|
||||
BUNDLE_ASSET_STORAGE_SETTINGS,
|
||||
|
||||
# Methods to derive settings
|
||||
_make_mako_template_dirs,
|
||||
_make_locale_paths,
|
||||
@@ -1746,6 +1750,9 @@ INSTALLED_APPS = [
|
||||
|
||||
# For edx ace template tags
|
||||
'edx_ace',
|
||||
|
||||
# Blockstore
|
||||
'blockstore.apps.bundles',
|
||||
]
|
||||
|
||||
|
||||
@@ -2102,6 +2109,7 @@ ENABLE_COMPREHENSIVE_THEMING = False
|
||||
|
||||
DATABASE_ROUTERS = [
|
||||
'openedx.core.lib.django_courseware_routers.StudentModuleHistoryExtendedRouter',
|
||||
'openedx.core.lib.blockstore_api.db_routers.BlockstoreRouter',
|
||||
]
|
||||
|
||||
############################ Cache Configuration ###############################
|
||||
|
||||
@@ -27,6 +27,8 @@ from .common import *
|
||||
|
||||
# import settings from LMS for consistent behavior with CMS
|
||||
from lms.envs.test import ( # pylint: disable=wrong-import-order
|
||||
BLOCKSTORE_USE_BLOCKSTORE_APP_API,
|
||||
BLOCKSTORE_API_URL,
|
||||
COMPREHENSIVE_THEME_DIRS, # unimport:skip
|
||||
DEFAULT_FILE_STORAGE,
|
||||
ECOMMERCE_API_URL,
|
||||
@@ -40,7 +42,8 @@ from lms.envs.test import ( # pylint: disable=wrong-import-order
|
||||
REGISTRATION_EXTRA_FIELDS,
|
||||
GRADES_DOWNLOAD,
|
||||
SITE_NAME,
|
||||
WIKI_ENABLED
|
||||
WIKI_ENABLED,
|
||||
XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE,
|
||||
)
|
||||
|
||||
|
||||
@@ -175,6 +178,12 @@ CACHES = {
|
||||
'course_structure_cache': {
|
||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
|
||||
},
|
||||
'blockstore': {
|
||||
'KEY_PREFIX': 'blockstore',
|
||||
'KEY_FUNCTION': 'common.djangoapps.util.memcache.safe_key',
|
||||
'LOCATION': 'edx_loc_mem_cache',
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
}
|
||||
|
||||
############################### BLOCKSTORE #####################################
|
||||
@@ -182,6 +191,13 @@ CACHES = {
|
||||
RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1')
|
||||
BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/")
|
||||
BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key')
|
||||
BUNDLE_ASSET_STORAGE_SETTINGS = dict(
|
||||
STORAGE_CLASS='django.core.files.storage.FileSystemStorage',
|
||||
STORAGE_KWARGS=dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
)
|
||||
|
||||
################################# CELERY ######################################
|
||||
|
||||
@@ -267,12 +283,6 @@ TEST_ELASTICSEARCH_USE_SSL = os.environ.get(
|
||||
TEST_ELASTICSEARCH_HOST = os.environ.get('EDXAPP_TEST_ELASTICSEARCH_HOST', 'edx.devstack.elasticsearch710')
|
||||
TEST_ELASTICSEARCH_PORT = int(os.environ.get('EDXAPP_TEST_ELASTICSEARCH_PORT', '9200'))
|
||||
|
||||
############################# TEMPLATE CONFIGURATION #############################
|
||||
# Adds mako template dirs for content_libraries tests
|
||||
MAKO_TEMPLATE_DIRS_BASE.append(
|
||||
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates'
|
||||
)
|
||||
|
||||
########################## AUTHOR PERMISSION #######################
|
||||
FEATURES['ENABLE_CREATOR_GROUP'] = False
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ if settings.DEBUG:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns += static(
|
||||
settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['base_url'],
|
||||
document_root=settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location']
|
||||
|
||||
@@ -9,7 +9,7 @@ from urllib.parse import parse_qsl, quote_plus, urlencode, urlparse, urlunparse
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import AssetKey, CourseKey
|
||||
from opaque_keys.edx.locator import AssetLocator
|
||||
from opaque_keys.edx.locator import AssetLocator, LibraryLocatorV2
|
||||
from PIL import Image
|
||||
|
||||
from xmodule.assetstore.assetmgr import AssetManager
|
||||
@@ -123,7 +123,7 @@ class StaticContent: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
@staticmethod
|
||||
def get_base_url_path_for_course_assets(course_key): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
if course_key is None:
|
||||
if (course_key is None) or isinstance(course_key, LibraryLocatorV2):
|
||||
return None
|
||||
|
||||
assert isinstance(course_key, CourseKey)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<!--
|
||||
common/test/problem.html
|
||||
Placeholder template for openedx content_library tests -->
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.utils.translation import ngettext, gettext as _
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
%>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<h3 class="hd hd-3 problem-header" id="${ short_id }-problem-title" aria-describedby="${ id }-problem-progress" tabindex="-1">
|
||||
${ problem['name'] }
|
||||
</h3>
|
||||
|
||||
<div class="problem-progress" id="${ id }-problem-progress"></div>
|
||||
|
||||
<div class="problem">
|
||||
${ HTML(problem['html']) }
|
||||
<div class="action">
|
||||
<input type="hidden" name="problem_id" value="${ problem['name'] }" />
|
||||
|
||||
<div class="problem-action-buttons-wrapper">
|
||||
% if demand_hint_possible:
|
||||
<span class="problem-action-button-wrapper">
|
||||
<button type="button" class="hint-button problem-action-btn btn-link btn-small" data-value="${_('Hint')}" ${'' if should_enable_next_hint else 'disabled'}>${_('Hint')}</button>
|
||||
</span>
|
||||
% endif
|
||||
% if save_button:
|
||||
<span class="problem-action-button-wrapper">
|
||||
<button type="button" class="save problem-action-btn btn-link btn-small" data-value="${_('Save')}">
|
||||
<span aria-hidden="true">${_('Save')}</span>
|
||||
<span class="sr">${_("Save your answer")}</span>
|
||||
</button>
|
||||
</span>
|
||||
% endif
|
||||
% if reset_button:
|
||||
<span class="problem-action-button-wrapper">
|
||||
<button type="button" class="reset problem-action-btn btn-link btn-small" data-value="${_('Reset')}"><span aria-hidden="true">${_('Reset')}</span><span class="sr">${_("Reset your answer")}</span></button>
|
||||
</span>
|
||||
% endif
|
||||
% if answer_available:
|
||||
<span class="problem-action-button-wrapper">
|
||||
<button type="button" class="show problem-action-btn btn-link btn-small" aria-describedby="${ short_id }-problem-title"><span class="show-label">${_('Show answer')}</span></button>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="submit-attempt-container">
|
||||
<button type="button" class="submit btn-brand" data-submitting="${ submit_button_submitting }" data-value="${ submit_button }" data-should-enable-submit-button="${ should_enable_submit_button }" aria-describedby="submission_feedback_${short_id}" ${'' if should_enable_submit_button else 'disabled'}>
|
||||
<span class="submit-label">${ submit_button }</span>
|
||||
</button>
|
||||
|
||||
% if submit_disabled_cta:
|
||||
% if submit_disabled_cta.get('event_data'):
|
||||
<button class="submit-cta-link-button btn-link btn-small" onclick="emit_event(${submit_disabled_cta['event_data']})">
|
||||
${submit_disabled_cta['link_name']}
|
||||
</button>
|
||||
<span class="submit-cta-description" tabindex="0" role="note" aria-label="description">
|
||||
<span data-tooltip="${submit_disabled_cta['description']}" data-tooltip-show-on-click="true"
|
||||
class="fa fa-info-circle fa-lg" aria-hidden="true">
|
||||
</span>
|
||||
</span>
|
||||
<span class="sr">(${submit_disabled_cta['description']})</span>
|
||||
% else:
|
||||
<form class="submit-cta" method="post" action="${submit_disabled_cta['link']}">
|
||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
% for form_name, form_value in submit_disabled_cta['form_values'].items():
|
||||
<input type="hidden" name="${form_name}" value="${form_value}">
|
||||
% endfor
|
||||
<button class="submit-cta-link-button btn-link btn-small">
|
||||
${submit_disabled_cta['link_name']}
|
||||
</button>
|
||||
<span class="submit-cta-description" tabindex="0" role="note" aria-label="description">
|
||||
<span data-tooltip="${submit_disabled_cta['description']}" data-tooltip-show-on-click="true"
|
||||
class="fa fa-info-circle fa-lg" aria-hidden="true">
|
||||
</span>
|
||||
</span>
|
||||
<span class="sr">(${submit_disabled_cta['description']})</span>
|
||||
</form>
|
||||
% endif
|
||||
% endif
|
||||
<div class="submission-feedback ${'cta-enabled' if submit_disabled_cta else ''}" id="submission_feedback_${short_id}">
|
||||
## When attempts are not 0, the CTA above will contain a message about the number of used attempts
|
||||
% if attempts_allowed and (not submit_disabled_cta or attempts_used == 0):
|
||||
${ngettext("You have used {num_used} of {num_total} attempt", "You have used {num_used} of {num_total} attempts", attempts_allowed).format(num_used=attempts_used, num_total=attempts_allowed)}
|
||||
% endif
|
||||
<span class="sr">${_("Some problems have options such as save, reset, hints, or show answer. These options follow the Submit button.")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function emit_event(message) {
|
||||
parent.postMessage(message, '*');
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!--
|
||||
common/tests/problem_ajax.html
|
||||
Placeholder template for openedx content_library tests -->
|
||||
<div id="problem_${element_id}" class="problems-wrapper" role="group"
|
||||
aria-labelledby="${element_id}-problem-title"
|
||||
data-problem-id="${id}" data-url="${ajax_url}"
|
||||
data-problem-score="${current_score}"
|
||||
data-problem-total-possible="${total_possible}"
|
||||
data-attempts-used="${attempts_used}"
|
||||
data-content="${content | h}"
|
||||
data-graded="${graded}">
|
||||
<p class="loading-spinner">
|
||||
<i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
|
||||
<span class="sr">Loading…</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,122 +0,0 @@
|
||||
<!--
|
||||
common/test/video.html
|
||||
Placeholder template for openedx content_library tests -->
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.js_utils import (
|
||||
dump_js_escaped_json, js_escaped_string
|
||||
)
|
||||
%>
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<h3 class="hd hd-2">${display_name}</h3>
|
||||
% endif
|
||||
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video closed"
|
||||
data-metadata='${metadata}'
|
||||
data-bumper-metadata='${bumper_metadata}'
|
||||
data-autoadvance-enabled="${autoadvance_enabled}"
|
||||
data-poster='${poster}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<div class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<div class="video-player">
|
||||
<div id="${id}"></div>
|
||||
<h4 class="hd hd-4 video-error is-hidden">${_('No playable video sources found.')}</h4>
|
||||
<h4 class="hd hd-4 video-hls-error is-hidden">
|
||||
${_('Your browser does not support this video format. Try using a different browser.')}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="video-player-post"></div>
|
||||
<div class="closed-captions"></div>
|
||||
<div class="video-controls is-hidden">
|
||||
<div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
% if download_video_link or track or handout or branding_info:
|
||||
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
|
||||
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
|
||||
% if download_video_link:
|
||||
<div class="wrapper-download-video">
|
||||
<h4 class="hd hd-5">${_('Video')}</h4>
|
||||
<a class="btn-link video-sources video-download-button" href="${download_video_link}">
|
||||
${_('Download video file')}
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
% if track:
|
||||
<div class="wrapper-download-transcripts">
|
||||
<h4 class="hd hd-5">${_('Transcripts')}</h4>
|
||||
% if transcript_download_format:
|
||||
<ul class="list-download-transcripts">
|
||||
% for item in transcript_download_formats_list:
|
||||
<li class="transcript-option">
|
||||
<% dname = _("Download {file}").format(file=item['display_name']) %>
|
||||
<a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% if handout:
|
||||
<div class="wrapper-handouts">
|
||||
<h4 class="hd hd-5">${_('Handouts')}</h4>
|
||||
<a class="btn-link" href="${handout}">${_('Download Handout')}</a>
|
||||
</div>
|
||||
% endif
|
||||
% if branding_info:
|
||||
<div class="branding">
|
||||
<span class="host-tag">${branding_info['logo_tag']}</span>
|
||||
<a href="${branding_info['url']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% if cdn_eval:
|
||||
<script>
|
||||
//TODO: refactor this js into a separate file.
|
||||
function sendPerformanceBeacon(id, expgroup, value, event_name) {
|
||||
var data = {event: event_name, id: id, expgroup: expgroup, value: value, page: "html5vid"};
|
||||
$.ajax({method: "POST", url: "/performance", data: data});
|
||||
}
|
||||
var cdnStartTime;
|
||||
var salt = Math.floor((1 + Math.random()) * 0x100000).toString(36);
|
||||
var id = "${id | n, js_escaped_string}";
|
||||
function initializeCDNExperiment() {
|
||||
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, "", "load");
|
||||
cdnStartTime = Date.now();
|
||||
$.each(['loadstart', 'abort', 'error', 'stalled', 'loadedmetadata',
|
||||
'loadeddata', 'canplay', 'canplaythrough', 'seeked'],
|
||||
function(index, eventName) {
|
||||
$("#video_" + id).bind("html5:" + eventName, null, function() {
|
||||
timeElapsed = Date.now() - cdnStartTime;
|
||||
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, timeElapsed, eventName);
|
||||
});
|
||||
});
|
||||
}
|
||||
$("#video_" + id).bind("initialize", null, initializeCDNExperiment);
|
||||
if ($("#video_" + id).hasClass("is-initialized")) {
|
||||
initializeCDNExperiment();
|
||||
}
|
||||
</script>
|
||||
% endif;
|
||||
@@ -1085,6 +1085,7 @@ STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.json"
|
||||
|
||||
DATABASE_ROUTERS = [
|
||||
'openedx.core.lib.django_courseware_routers.StudentModuleHistoryExtendedRouter',
|
||||
'openedx.core.lib.blockstore_api.db_routers.BlockstoreRouter',
|
||||
'edx_django_utils.db.read_replica.ReadReplicaRouter',
|
||||
]
|
||||
|
||||
@@ -3243,6 +3244,9 @@ INSTALLED_APPS = [
|
||||
|
||||
# For save for later
|
||||
'lms.djangoapps.save_for_later',
|
||||
|
||||
# Blockstore
|
||||
'blockstore.apps.bundles',
|
||||
]
|
||||
|
||||
######################### CSRF #########################################
|
||||
@@ -4970,6 +4974,10 @@ MAILCHIMP_NEW_USER_LIST_ID = ""
|
||||
BLOCKSTORE_PUBLIC_URL_ROOT = 'http://localhost:18250'
|
||||
BLOCKSTORE_API_URL = 'http://localhost:18250/api/v1/'
|
||||
|
||||
# Disable the Blockstore app API by default.
|
||||
# See openedx.core.lib.blockstore_api.config for details.
|
||||
BLOCKSTORE_USE_BLOCKSTORE_APP_API = False
|
||||
|
||||
# .. setting_name: XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE
|
||||
# .. setting_default: default
|
||||
# .. setting_description: The django cache key of the cache to use for storing anonymous user state for XBlocks.
|
||||
@@ -4985,6 +4993,40 @@ XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'default'
|
||||
# configured to expire after one hour.
|
||||
BLOCKSTORE_BUNDLE_CACHE_TIMEOUT = 3000
|
||||
|
||||
# .. setting_name: BUNDLE_ASSET_URL_STORAGE_KEY
|
||||
# .. setting_default: None
|
||||
# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_SECRET` is
|
||||
# set, and `boto3` is installed, this is used as an AWS IAM access key for
|
||||
# generating signed, read-only URLs for blockstore assets stored in S3.
|
||||
# Otherwise, URLs are generated based on the default storage configuration.
|
||||
# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
|
||||
BUNDLE_ASSET_URL_STORAGE_KEY = None
|
||||
|
||||
# .. setting_name: BUNDLE_ASSET_URL_STORAGE_SECRET
|
||||
# .. setting_default: None
|
||||
# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_KEY` is
|
||||
# set, and `boto3` is installed, this is used as an AWS IAM secret key for
|
||||
# generating signed, read-only URLs for blockstore assets stored in S3.
|
||||
# Otherwise, URLs are generated based on the default storage configuration.
|
||||
# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
|
||||
BUNDLE_ASSET_URL_STORAGE_SECRET = None
|
||||
|
||||
# .. setting_name: BUNDLE_ASSET_STORAGE_SETTINGS
|
||||
# .. setting_default: dict, appropriate for file system storage.
|
||||
# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_KEY` is
|
||||
# set, and `boto3` is installed, this provides the bucket name and location for blockstore assets stored in S3.
|
||||
# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
|
||||
BUNDLE_ASSET_STORAGE_SETTINGS = dict(
|
||||
# Backend storage
|
||||
# STORAGE_CLASS='storages.backends.s3boto.S3BotoStorage',
|
||||
# STORAGE_KWARGS=dict(bucket='bundle-asset-bucket', location='/path-to-bundles/'),
|
||||
STORAGE_CLASS='django.core.files.storage.FileSystemStorage',
|
||||
STORAGE_KWARGS=dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
)
|
||||
|
||||
######################### MICROSITE ###############################
|
||||
MICROSITE_ROOT_DIR = '/edx/app/edxapp/edx-microsite'
|
||||
MICROSITE_CONFIGURATION = {}
|
||||
|
||||
@@ -19,7 +19,6 @@ from .production import * # pylint: disable=wildcard-import, unused-wildcard-im
|
||||
|
||||
# Don't use S3 in devstack, fall back to filesystem
|
||||
del DEFAULT_FILE_STORAGE
|
||||
MEDIA_ROOT = "/edx/var/edxapp/uploads"
|
||||
ORA2_FILEUPLOAD_BACKEND = 'django'
|
||||
|
||||
|
||||
|
||||
@@ -223,16 +223,6 @@ CACHES = {
|
||||
},
|
||||
}
|
||||
|
||||
############################### BLOCKSTORE #####################################
|
||||
# Blockstore tests
|
||||
RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1')
|
||||
BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/")
|
||||
BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key')
|
||||
XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'blockstore' # This must be set to a working cache for the tests to pass
|
||||
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
############################# SECURITY SETTINGS ################################
|
||||
# Default to advanced security in common.py, so tests can reset here to use
|
||||
# a simpler security model
|
||||
@@ -314,7 +304,7 @@ ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = OrderedDict([
|
||||
############################ STATIC FILES #############################
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = TEST_ROOT / "uploads"
|
||||
MEDIA_URL = "/static/uploads/"
|
||||
MEDIA_URL = "/uploads/"
|
||||
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
|
||||
|
||||
_NEW_STATICFILES_DIRS = []
|
||||
@@ -553,6 +543,24 @@ add_plugins(__name__, ProjectType.LMS, SettingsType.TEST)
|
||||
|
||||
derive_settings(__name__)
|
||||
|
||||
############################### BLOCKSTORE #####################################
|
||||
# Blockstore tests
|
||||
RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1')
|
||||
BLOCKSTORE_USE_BLOCKSTORE_APP_API = not RUN_BLOCKSTORE_TESTS
|
||||
BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/")
|
||||
BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key')
|
||||
XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'blockstore' # This must be set to a working cache for the tests to pass
|
||||
BUNDLE_ASSET_STORAGE_SETTINGS = dict(
|
||||
STORAGE_CLASS='django.core.files.storage.FileSystemStorage',
|
||||
STORAGE_KWARGS=dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
)
|
||||
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
############### Settings for edx-rbac ###############
|
||||
SYSTEM_WIDE_ROLE_CLASSES = os.environ.get("SYSTEM_WIDE_ROLE_CLASSES", [])
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.validators import validate_unicode_slug
|
||||
from django.db import IntegrityError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.utils.translation import gettext as _
|
||||
from elasticsearch.exceptions import ConnectionError as ElasticConnectionError
|
||||
from lxml import etree
|
||||
@@ -436,15 +436,18 @@ def create_library(
|
||||
)
|
||||
# Now create the library reference in our database:
|
||||
try:
|
||||
ref = ContentLibrary.objects.create(
|
||||
org=org,
|
||||
slug=slug,
|
||||
type=library_type,
|
||||
bundle_uuid=bundle.uuid,
|
||||
allow_public_learning=allow_public_learning,
|
||||
allow_public_read=allow_public_read,
|
||||
license=library_license,
|
||||
)
|
||||
# Atomic transaction required because if this fails,
|
||||
# we need to delete the bundle in the exception handler.
|
||||
with transaction.atomic():
|
||||
ref = ContentLibrary.objects.create(
|
||||
org=org,
|
||||
slug=slug,
|
||||
type=library_type,
|
||||
bundle_uuid=bundle.uuid,
|
||||
allow_public_learning=allow_public_learning,
|
||||
allow_public_read=allow_public_read,
|
||||
license=library_license,
|
||||
)
|
||||
except IntegrityError:
|
||||
delete_bundle(bundle.uuid)
|
||||
raise LibraryAlreadyExists(slug) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Helper code for working with Blockstore bundles that contain OLX
|
||||
"""
|
||||
|
||||
import dateutil.parser
|
||||
import logging # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from functools import lru_cache # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -347,12 +346,17 @@ class LibraryBundle:
|
||||
problem/quiz1/definition.xml
|
||||
problem/quiz1/static/image1.png
|
||||
Then this will return
|
||||
[BundleFile(path="image1.png", size, url, hash_digest)]
|
||||
[BundleFileData(path="image1.png", size, url, hash_digest)]
|
||||
"""
|
||||
path_prefix = self.get_static_prefix_for_definition(definition_key)
|
||||
path_prefix_len = len(path_prefix)
|
||||
return [
|
||||
blockstore_api.BundleFile(path=f.path[path_prefix_len:], size=f.size, url=f.url, hash_digest=f.hash_digest)
|
||||
blockstore_api.BundleFileData(
|
||||
path=f.path[path_prefix_len:],
|
||||
size=f.size,
|
||||
url=f.url,
|
||||
hash_digest=f.hash_digest,
|
||||
)
|
||||
for f in get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name)
|
||||
if f.path.startswith(path_prefix)
|
||||
]
|
||||
@@ -369,8 +373,7 @@ class LibraryBundle:
|
||||
version = get_bundle_version_number(self.bundle_uuid)
|
||||
if version == 0:
|
||||
return None
|
||||
created_at_str = blockstore_api.get_bundle_version(self.bundle_uuid, version)['snapshot']['created_at']
|
||||
last_published_time = dateutil.parser.parse(created_at_str)
|
||||
last_published_time = blockstore_api.get_bundle_version(self.bundle_uuid, version).created_at
|
||||
self.cache.set(cache_key, last_published_time)
|
||||
return last_published_time
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ Tests for Blockstore-based Content Libraries
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from urllib.parse import urlencode
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest import mock, skipUnless
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import LiveServerTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from organizations.models import Organization
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
@@ -47,8 +49,39 @@ URL_BLOCK_METADATA_URL = '/api/xblock/v2/xblocks/{block_key}/'
|
||||
URL_BLOCK_XBLOCK_HANDLER = '/api/xblock/v2/xblocks/{block_key}/handler/{user_id}-{secure_token}/{handler_name}/'
|
||||
|
||||
|
||||
# Decorator for tests that require blockstore
|
||||
requires_blockstore = unittest.skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
|
||||
# Decorators for tests that require the blockstore service/app
|
||||
requires_blockstore = skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
|
||||
|
||||
requires_blockstore_app = skipUnless(settings.BLOCKSTORE_USE_BLOCKSTORE_APP_API, "Requires blockstore app")
|
||||
|
||||
|
||||
class BlockstoreAppTestMixin:
|
||||
"""
|
||||
Sets up the environment for tests to be run using the installed Blockstore app.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Ensure there's an active request, so that bundle file URLs can be made absolute.
|
||||
"""
|
||||
super().setUp()
|
||||
|
||||
# Patch the blockstore get_current_request to use our live_server_url
|
||||
mock.patch('blockstore.apps.api.methods.get_current_request',
|
||||
mock.Mock(return_value=self._get_current_request())).start()
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
|
||||
def _get_current_request(self):
|
||||
"""
|
||||
Returns a request object using the live_server_url, if available.
|
||||
"""
|
||||
request_args = {}
|
||||
if hasattr(self, 'live_server_url'):
|
||||
live_server_url = urlparse(self.live_server_url)
|
||||
name, port = live_server_url.netloc.split(':')
|
||||
request_args['SERVER_NAME'] = name
|
||||
request_args['SERVER_PORT'] = port or '80'
|
||||
request_args['wsgi.url_scheme'] = live_server_url.scheme
|
||||
return RequestFactory().request(**request_args)
|
||||
|
||||
|
||||
def elasticsearch_test(func):
|
||||
@@ -63,9 +96,11 @@ def elasticsearch_test(func):
|
||||
'host': settings.TEST_ELASTICSEARCH_HOST,
|
||||
'port': settings.TEST_ELASTICSEARCH_PORT,
|
||||
}])(func)
|
||||
func = patch("openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS", new={
|
||||
'refresh': 'wait_for'
|
||||
})(func)
|
||||
func = mock.patch(
|
||||
"openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS",
|
||||
new={
|
||||
'refresh': 'wait_for'
|
||||
})(func)
|
||||
return func
|
||||
else:
|
||||
@classmethod
|
||||
@@ -77,20 +112,19 @@ def elasticsearch_test(func):
|
||||
size=MAX_SIZE
|
||||
)
|
||||
|
||||
func = patch(
|
||||
func = mock.patch(
|
||||
"openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS",
|
||||
new={}
|
||||
)(func)
|
||||
func = patch(
|
||||
func = mock.patch(
|
||||
"openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase._perform_elastic_search",
|
||||
new=mock_perform
|
||||
)(func)
|
||||
return func
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
@skip_unless_cms # Content Libraries REST API is only available in Studio
|
||||
class ContentLibrariesRestApiTest(APITestCase):
|
||||
class _ContentLibrariesRestApiTestMixin:
|
||||
"""
|
||||
Base class for Blockstore-based Content Libraries test that use the REST API
|
||||
|
||||
@@ -350,3 +384,26 @@ class ContentLibrariesRestApiTest(APITestCase):
|
||||
"""
|
||||
url = URL_BLOCK_GET_HANDLER_URL.format(block_key=block_key, handler_name=handler_name)
|
||||
return self._api('get', url, None, expect_response=200)["handler_url"]
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class ContentLibrariesRestApiBlockstoreServiceTest(_ContentLibrariesRestApiTestMixin, APITestCase):
|
||||
"""
|
||||
Base class for Blockstore-based Content Libraries test that use the REST API
|
||||
and the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class ContentLibrariesRestApiTest(
|
||||
_ContentLibrariesRestApiTestMixin,
|
||||
BlockstoreAppTestMixin,
|
||||
APITestCase,
|
||||
LiveServerTestCase,
|
||||
):
|
||||
"""
|
||||
Base class for Blockstore-based Content Libraries test that use the REST API
|
||||
and the installed Blockstore app.
|
||||
|
||||
We run this test with a live server, so that the blockstore asset files can be served.
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,7 @@ from xblock.core import XBlock
|
||||
|
||||
from openedx.core.djangoapps.content_libraries.libraries_index import LibraryBlockIndexer, ContentLibraryIndexer
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
ContentLibrariesRestApiTest,
|
||||
elasticsearch_test,
|
||||
URL_BLOCK_METADATA_URL,
|
||||
@@ -33,8 +34,7 @@ from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesTest(ContentLibrariesRestApiTest):
|
||||
class ContentLibrariesTestMixin:
|
||||
"""
|
||||
General tests for Blockstore-based Content Libraries
|
||||
|
||||
@@ -369,6 +369,60 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest):
|
||||
|
||||
# fin
|
||||
|
||||
def test_library_blocks_studio_view(self):
|
||||
"""
|
||||
Test the happy path of working with an HTML XBlock in a the studio_view of a content library.
|
||||
"""
|
||||
lib = self._create_library(slug="testlib2", title="A Test Library", description="Testing XBlocks")
|
||||
lib_id = lib["id"]
|
||||
assert lib['has_unpublished_changes'] is False
|
||||
|
||||
# A library starts out empty:
|
||||
assert self._get_library_blocks(lib_id) == []
|
||||
|
||||
# Add a 'html' XBlock to the library:
|
||||
block_data = self._add_block_to_library(lib_id, "html", "html1")
|
||||
self.assertDictContainsEntries(block_data, {
|
||||
"id": "lb:CL-TEST:testlib2:html:html1",
|
||||
"display_name": "Text",
|
||||
"block_type": "html",
|
||||
"has_unpublished_changes": True,
|
||||
})
|
||||
block_id = block_data["id"]
|
||||
# Confirm that the result contains a definition key, but don't check its value,
|
||||
# which for the purposes of these tests is an implementation detail.
|
||||
assert 'def_key' in block_data
|
||||
|
||||
# now the library should contain one block and have unpublished changes:
|
||||
assert self._get_library_blocks(lib_id) == [block_data]
|
||||
assert self._get_library(lib_id)['has_unpublished_changes'] is True
|
||||
|
||||
# Publish the changes:
|
||||
self._commit_library_changes(lib_id)
|
||||
assert self._get_library(lib_id)['has_unpublished_changes'] is False
|
||||
# And now the block information should also show that block has no unpublished changes:
|
||||
block_data["has_unpublished_changes"] = False
|
||||
self.assertDictContainsEntries(self._get_library_block(block_id), block_data)
|
||||
assert self._get_library_blocks(lib_id) == [block_data]
|
||||
|
||||
# Now update the block's OLX:
|
||||
orig_olx = self._get_library_block_olx(block_id)
|
||||
assert '<html' in orig_olx
|
||||
new_olx = "<html><b>Hello world!</b></html>"
|
||||
self._set_library_block_olx(block_id, new_olx)
|
||||
# now reading it back, we should get that exact OLX (no change to whitespace etc.):
|
||||
assert self._get_library_block_olx(block_id) == new_olx
|
||||
# And the display name and "unpublished changes" status of the block should be updated:
|
||||
self.assertDictContainsEntries(self._get_library_block(block_id), {
|
||||
"display_name": "Text",
|
||||
"has_unpublished_changes": True,
|
||||
})
|
||||
|
||||
# Now view the XBlock's studio view (including draft changes):
|
||||
fragment = self._render_block_view(block_id, "studio_view")
|
||||
assert 'resources' in fragment
|
||||
assert 'Hello world!' in fragment['content']
|
||||
|
||||
@ddt.data(True, False)
|
||||
@patch("openedx.core.djangoapps.content_libraries.views.LibraryApiPagination.page_size", new=2)
|
||||
def test_list_library_blocks(self, is_indexing_enabled):
|
||||
@@ -872,6 +926,26 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest):
|
||||
assert len(types) > 1
|
||||
|
||||
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesBlockstoreServiceTest(
|
||||
ContentLibrariesTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
General tests for Blockstore-based Content Libraries, using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesTest(
|
||||
ContentLibrariesTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
General tests for Blockstore-based Content Libraries, using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ContentLibraryXBlockValidationTest(APITestCase):
|
||||
"""Tests only focused on service validation, no Blockstore needed."""
|
||||
@@ -941,8 +1015,7 @@ class AltBlock(XBlock):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesXBlockTypeOverrideTest(ContentLibrariesRestApiTest):
|
||||
class ContentLibrariesXBlockTypeOverrideTestMixin:
|
||||
"""
|
||||
Tests for Blockstore-based Content Libraries XBlock API,
|
||||
where the expected XBlock type returned is overridden in the request.
|
||||
@@ -1073,3 +1146,23 @@ class ContentLibrariesXBlockTypeOverrideTest(ContentLibrariesRestApiTest):
|
||||
assert f"lb:CL-TEST:handler-{slug}:video:handler-{slug}" in response['transcripts']['en']
|
||||
del response['transcripts']['en']
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesXBlockTypeOverrideBlockstoreServiceTest(
|
||||
ContentLibrariesXBlockTypeOverrideTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
Tests for the Content Libraries XBlock API type override using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@elasticsearch_test
|
||||
class ContentLibrariesXBlockTypeOverrideTest(
|
||||
ContentLibrariesXBlockTypeOverrideTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
Tests for the Content Libraries XBlock API type override using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -10,12 +10,14 @@ from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
|
||||
from search.search_engine_base import SearchEngine
|
||||
|
||||
from openedx.core.djangoapps.content_libraries.libraries_index import ContentLibraryIndexer, LibraryBlockIndexer
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest, elasticsearch_test
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
ContentLibrariesRestApiTest,
|
||||
elasticsearch_test,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
|
||||
@elasticsearch_test
|
||||
class ContentLibraryIndexerTest(ContentLibrariesRestApiTest):
|
||||
class ContentLibraryIndexerTestMixin:
|
||||
"""
|
||||
Tests the operation of ContentLibraryIndexer
|
||||
"""
|
||||
@@ -181,7 +183,27 @@ class ContentLibraryIndexerTest(ContentLibrariesRestApiTest):
|
||||
|
||||
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
|
||||
@elasticsearch_test
|
||||
class LibraryBlockIndexerTest(ContentLibrariesRestApiTest):
|
||||
class ContentLibraryIndexerBlockstoreServiceTest(
|
||||
ContentLibraryIndexerTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
Tests the operation of ContentLibraryIndexer using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
|
||||
@elasticsearch_test
|
||||
class ContentLibraryIndexerTest(
|
||||
ContentLibraryIndexerTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
Tests the operation of ContentLibraryIndexer using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
|
||||
class LibraryBlockIndexerTestMixin:
|
||||
"""
|
||||
Tests the operation of LibraryBlockIndexer
|
||||
"""
|
||||
@@ -279,3 +301,25 @@ class LibraryBlockIndexerTest(ContentLibrariesRestApiTest):
|
||||
LibraryBlockIndexer.get_items([block['id']])
|
||||
self._delete_library(lib['id'])
|
||||
assert LibraryBlockIndexer.get_items([block['id']]) == []
|
||||
|
||||
|
||||
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
|
||||
@elasticsearch_test
|
||||
class LibraryBlockIndexerBlockstoreServiceTest(
|
||||
LibraryBlockIndexerTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
Tests the operation of LibraryBlockIndexer using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
|
||||
@elasticsearch_test
|
||||
class LibraryBlockIndexerTest(
|
||||
LibraryBlockIndexerTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
Tests the operation of LibraryBlockIndexer using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,8 @@ from gettext import GNUTranslations
|
||||
|
||||
from completion.test_utils import CompletionWaffleTestMixin
|
||||
from django.db import connections
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test import LiveServerTestCase, TestCase
|
||||
from django.utils.text import slugify
|
||||
from organizations.models import Organization
|
||||
from rest_framework.test import APIClient
|
||||
from xblock.core import XBlock
|
||||
@@ -14,7 +15,9 @@ from xblock.core import XBlock
|
||||
from lms.djangoapps.courseware.model_data import get_score
|
||||
from openedx.core.djangoapps.content_libraries import api as library_api
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
BlockstoreAppTestMixin,
|
||||
requires_blockstore,
|
||||
requires_blockstore_app,
|
||||
URL_BLOCK_RENDER_VIEW,
|
||||
URL_BLOCK_GET_HANDLER_URL,
|
||||
URL_BLOCK_METADATA_URL,
|
||||
@@ -33,26 +36,27 @@ class ContentLibraryContentTestMixin:
|
||||
"""
|
||||
Mixin for content library tests that creates two students and a library.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Create a couple students that the tests can use
|
||||
cls.student_a = UserFactory.create(username="Alice", email="alice@example.com", password="edx")
|
||||
cls.student_b = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
self.student_a = UserFactory.create(username="Alice", email="alice@example.com", password="edx")
|
||||
self.student_b = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
|
||||
# Create a collection using Blockstore API directly only because there
|
||||
# is not yet any Studio REST API for doing so:
|
||||
cls.collection = blockstore_api.create_collection("Content Library Test Collection")
|
||||
self.collection = blockstore_api.create_collection("Content Library Test Collection")
|
||||
# Create an organization
|
||||
cls.organization = Organization.objects.create(
|
||||
self.organization = Organization.objects.create(
|
||||
name="Content Libraries Tachyon Exploration & Survey Team",
|
||||
short_name="CL-TEST",
|
||||
)
|
||||
cls.library = library_api.create_library(
|
||||
collection_uuid=cls.collection.uuid,
|
||||
_, slug = self.id().rsplit('.', 1)
|
||||
self.library = library_api.create_library(
|
||||
collection_uuid=self.collection.uuid,
|
||||
library_type=COMPLEX,
|
||||
org=cls.organization,
|
||||
slug=cls.__name__,
|
||||
title=(cls.__name__ + " Test Lib"),
|
||||
org=self.organization,
|
||||
slug=slugify(slug),
|
||||
title=(f"{slug} Test Lib"),
|
||||
description="",
|
||||
allow_public_learning=True,
|
||||
allow_public_read=False,
|
||||
@@ -60,10 +64,7 @@ class ContentLibraryContentTestMixin:
|
||||
)
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
# EphemeralKeyValueStore requires a working cache, and the default test cache doesn't work:
|
||||
@override_settings(XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE='blockstore')
|
||||
class ContentLibraryRuntimeTest(ContentLibraryContentTestMixin, TestCase):
|
||||
class ContentLibraryRuntimeTestMixin(ContentLibraryContentTestMixin):
|
||||
"""
|
||||
Basic tests of the Blockstore-based XBlock runtime using XBlocks in a
|
||||
content library.
|
||||
@@ -181,10 +182,25 @@ class ContentLibraryRuntimeTest(ContentLibraryContentTestMixin, TestCase):
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class ContentLibraryRuntimeBServiceTest(ContentLibraryRuntimeTestMixin, TestCase):
|
||||
"""
|
||||
Tests XBlock runtime using XBlocks in a content library using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class ContentLibraryRuntimeTest(ContentLibraryRuntimeTestMixin, BlockstoreAppTestMixin, LiveServerTestCase):
|
||||
"""
|
||||
Tests XBlock runtime using XBlocks in a content library using the installed Blockstore app.
|
||||
|
||||
We run this test with a live server, so that the blockstore asset files can be served.
|
||||
"""
|
||||
|
||||
|
||||
# We can remove the line below to enable this in Studio once we implement a session-backed
|
||||
# field data store which we can use for both studio users and anonymous users
|
||||
@skip_unless_lms
|
||||
class ContentLibraryXBlockUserStateTest(ContentLibraryContentTestMixin, TestCase):
|
||||
class ContentLibraryXBlockUserStateTestMixin(ContentLibraryContentTestMixin):
|
||||
"""
|
||||
Test that the Blockstore-based XBlock runtime can store and retrieve student
|
||||
state for XBlocks when learners access blocks directly in a library context,
|
||||
@@ -487,8 +503,27 @@ class ContentLibraryXBlockUserStateTest(ContentLibraryContentTestMixin, TestCase
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class ContentLibraryXBlockUserStateBServiceTest(ContentLibraryXBlockUserStateTestMixin, TestCase):
|
||||
"""
|
||||
Tests XBlock user state for XBlocks in a content library using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class ContentLibraryXBlockUserStateTest(
|
||||
ContentLibraryXBlockUserStateTestMixin,
|
||||
BlockstoreAppTestMixin,
|
||||
LiveServerTestCase,
|
||||
):
|
||||
"""
|
||||
Tests XBlock user state for XBlocks in a content library using the installed Blockstore app.
|
||||
|
||||
We run this test with a live server, so that the blockstore asset files can be served.
|
||||
"""
|
||||
|
||||
|
||||
@skip_unless_lms # No completion tracking in Studio
|
||||
class ContentLibraryXBlockCompletionTest(ContentLibraryContentTestMixin, CompletionWaffleTestMixin, TestCase):
|
||||
class ContentLibraryXBlockCompletionTestMixin(ContentLibraryContentTestMixin, CompletionWaffleTestMixin):
|
||||
"""
|
||||
Test that the Blockstore-based XBlocks can track their completion status
|
||||
using the completion library.
|
||||
@@ -539,3 +574,30 @@ class ContentLibraryXBlockCompletionTest(ContentLibraryContentTestMixin, Complet
|
||||
|
||||
# Now the block is completed
|
||||
assert get_block_completion_status() == 1
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class ContentLibraryXBlockCompletionBServiceTest(
|
||||
ContentLibraryXBlockCompletionTestMixin,
|
||||
CompletionWaffleTestMixin,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Test that the Blockstore-based XBlocks can track their completion status
|
||||
using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class ContentLibraryXBlockCompletionTest(
|
||||
ContentLibraryXBlockCompletionTestMixin,
|
||||
CompletionWaffleTestMixin,
|
||||
BlockstoreAppTestMixin,
|
||||
LiveServerTestCase,
|
||||
):
|
||||
"""
|
||||
Test that the Blockstore-based XBlocks can track their completion status
|
||||
using the installed Blockstore app.
|
||||
|
||||
We run this test with a live server, so that the blockstore asset files can be served.
|
||||
"""
|
||||
|
||||
@@ -5,7 +5,10 @@ Tests for static asset files in Blockstore-based Content Libraries
|
||||
|
||||
import requests
|
||||
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
ContentLibrariesRestApiTest,
|
||||
)
|
||||
|
||||
# Binary data representing an SVG image file
|
||||
SVG_DATA = """<svg xmlns="http://www.w3.org/2000/svg" height="30" width="100">
|
||||
@@ -23,7 +26,7 @@ I'm Anant Agarwal, I'm the president of edX,
|
||||
"""
|
||||
|
||||
|
||||
class ContentLibrariesStaticAssetsTest(ContentLibrariesRestApiTest):
|
||||
class ContentLibrariesStaticAssetsTestMixin:
|
||||
"""
|
||||
Tests for static asset files in Blockstore-based Content Libraries
|
||||
|
||||
@@ -166,3 +169,21 @@ class ContentLibrariesStaticAssetsTest(ContentLibrariesRestApiTest):
|
||||
self._commit_library_changes(library["id"])
|
||||
check_sjson()
|
||||
check_download()
|
||||
|
||||
|
||||
class ContentLibrariesStaticAssetsBlockstoreServiceTest(
|
||||
ContentLibrariesStaticAssetsTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
Tests for static asset files in Blockstore-based Content Libraries, using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
class ContentLibrariesStaticAssetsTest(
|
||||
ContentLibrariesStaticAssetsTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
Tests for static asset files in Blockstore-based Content Libraries, using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.test import TestCase, override_settings
|
||||
from openedx.core.djangoapps.content_libraries.constants import PROBLEM
|
||||
|
||||
from .base import (
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
ContentLibrariesRestApiTest,
|
||||
URL_LIB_LTI_JWKS,
|
||||
skip_unless_cms,
|
||||
@@ -49,9 +50,7 @@ class LtiToolJwksViewTest(TestCase):
|
||||
self.assertJSONEqual(response.content, '{"keys": []}')
|
||||
|
||||
|
||||
@override_features(ENABLE_CONTENT_LIBRARIES=True,
|
||||
ENABLE_CONTENT_LIBRARIES_LTI_TOOL=True)
|
||||
class LibraryBlockLtiUrlViewTest(ContentLibrariesRestApiTest):
|
||||
class LibraryBlockLtiUrlViewTestMixin:
|
||||
"""
|
||||
Test generating LTI URL for a block in a library.
|
||||
"""
|
||||
@@ -66,12 +65,12 @@ class LibraryBlockLtiUrlViewTest(ContentLibrariesRestApiTest):
|
||||
)
|
||||
|
||||
block = self._add_block_to_library(library['id'], PROBLEM, PROBLEM)
|
||||
usage_key = str(block.usage_key)
|
||||
usage_key = str(block['id'])
|
||||
|
||||
url = f'/api/libraries/v2/blocks/{usage_key}/lti/'
|
||||
expected_lti_url = f"/api/libraries/v2/lti/1.3/launch/?id={usage_key}"
|
||||
|
||||
response = self._api("GET", url, None, expect_response=200)
|
||||
response = self._api("get", url, None, expect_response=200)
|
||||
|
||||
self.assertDictEqual(response, {"lti_url": expected_lti_url})
|
||||
|
||||
@@ -79,9 +78,26 @@ class LibraryBlockLtiUrlViewTest(ContentLibrariesRestApiTest):
|
||||
"""
|
||||
Test the LTI URL cannot be generated as the block not found.
|
||||
"""
|
||||
self._api("get", '/api/libraries/v2/blocks/lb:CL-TEST:libgg:problem:bad-block/lti/', None, expect_response=404)
|
||||
|
||||
self._create_library(
|
||||
slug="libgg", title="A Test Library", description="Testing library", library_type=PROBLEM,
|
||||
)
|
||||
|
||||
self._api("GET", '/api/libraries/v2/blocks/not-existing-key/lti/', None, expect_response=404)
|
||||
@override_features(ENABLE_CONTENT_LIBRARIES=True,
|
||||
ENABLE_CONTENT_LIBRARIES_LTI_TOOL=True)
|
||||
class LibraryBlockLtiUrlViewBlockstoreServiceTest(
|
||||
LibraryBlockLtiUrlViewTestMixin,
|
||||
ContentLibrariesRestApiBlockstoreServiceTest,
|
||||
):
|
||||
"""
|
||||
Test generating LTI URL for a block in a library, using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@override_features(ENABLE_CONTENT_LIBRARIES=True,
|
||||
ENABLE_CONTENT_LIBRARIES_LTI_TOOL=True)
|
||||
class LibraryBlockLtiUrlViewTest(
|
||||
LibraryBlockLtiUrlViewTestMixin,
|
||||
ContentLibrariesRestApiTest,
|
||||
):
|
||||
"""
|
||||
Test generating LTI URL for a block in a library, using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -241,6 +241,8 @@ class XBlockRuntime(RuntimeShim, Runtime):
|
||||
anonymous_user_id=self.anonymous_student_id,
|
||||
)
|
||||
elif service_name == "mako":
|
||||
if self.system.student_data_mode == XBlockRuntimeSystem.STUDENT_DATA_EPHEMERAL:
|
||||
return MakoService(namespace_prefix='lms.')
|
||||
return MakoService()
|
||||
elif service_name == "i18n":
|
||||
return ModuleI18nService(block=block)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
Tests for BundleCache
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from openedx.core.djangolib.blockstore_cache import BundleCache
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
BlockstoreAppTestMixin,
|
||||
requires_blockstore,
|
||||
requires_blockstore_app,
|
||||
)
|
||||
from openedx.core.lib import blockstore_api as api
|
||||
|
||||
|
||||
@@ -23,9 +26,8 @@ class TestWithBundleMixin:
|
||||
cls.draft = api.get_or_create_bundle_draft(cls.bundle.uuid, draft_name="test-draft")
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
|
||||
@patch('openedx.core.djangolib.blockstore_cache.MAX_BLOCKSTORE_CACHE_DELAY', 0)
|
||||
class BundleCacheTest(TestWithBundleMixin, unittest.TestCase):
|
||||
class BundleCacheTestMixin(TestWithBundleMixin):
|
||||
"""
|
||||
Tests for BundleCache
|
||||
"""
|
||||
@@ -80,8 +82,7 @@ class BundleCacheTest(TestWithBundleMixin, unittest.TestCase):
|
||||
assert cache.get(key2) is None
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
|
||||
class BundleCacheClearTest(TestWithBundleMixin, unittest.TestCase):
|
||||
class BundleCacheClearTest(TestWithBundleMixin, TestCase):
|
||||
"""
|
||||
Tests for BundleCache's clear() method.
|
||||
Requires MAX_BLOCKSTORE_CACHE_DELAY to be non-zero. This clear() method does
|
||||
@@ -111,3 +112,17 @@ class BundleCacheClearTest(TestWithBundleMixin, unittest.TestCase):
|
||||
# Now "clear" the cache, forcing the check of the new version:
|
||||
cache.clear()
|
||||
assert cache.get(key1) is None
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class BundleCacheBlockstoreServiceTest(BundleCacheTestMixin, TestCase):
|
||||
"""
|
||||
Tests BundleCache using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class BundleCacheTest(BundleCacheTestMixin, BlockstoreAppTestMixin, TestCase):
|
||||
"""
|
||||
Tests BundleCache using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -5,15 +5,16 @@ This API does not do any caching; consider using BundleCache or (in
|
||||
openedx.core.djangolib.blockstore_cache) together with these API methods for
|
||||
improved performance.
|
||||
"""
|
||||
from .models import (
|
||||
Collection,
|
||||
Bundle,
|
||||
Draft,
|
||||
BundleFile,
|
||||
DraftFile,
|
||||
LinkReference,
|
||||
LinkDetails,
|
||||
DraftLinkDetails,
|
||||
from blockstore.apps.api.data import (
|
||||
BundleFileData,
|
||||
)
|
||||
from blockstore.apps.api.exceptions import (
|
||||
CollectionNotFound,
|
||||
BundleNotFound,
|
||||
DraftNotFound,
|
||||
BundleVersionNotFound,
|
||||
BundleFileNotFound,
|
||||
BundleStorageError,
|
||||
)
|
||||
from .methods import (
|
||||
# Collections:
|
||||
@@ -47,11 +48,3 @@ from .methods import (
|
||||
# Misc:
|
||||
force_browser_url,
|
||||
)
|
||||
from .exceptions import (
|
||||
BlockstoreException,
|
||||
CollectionNotFound,
|
||||
BundleNotFound,
|
||||
DraftNotFound,
|
||||
BundleFileNotFound,
|
||||
BundleStorageError,
|
||||
)
|
||||
|
||||
13
openedx/core/lib/blockstore_api/config/__init__.py
Normal file
13
openedx/core/lib/blockstore_api/config/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Helper method to indicate when the blockstore app API is enabled.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from .waffle import BLOCKSTORE_USE_BLOCKSTORE_APP_API # pylint: disable=invalid-django-waffle-import
|
||||
|
||||
|
||||
def use_blockstore_app():
|
||||
"""
|
||||
Use the Blockstore app API if the settings say to (e.g. in test)
|
||||
or if the waffle switch is enabled.
|
||||
"""
|
||||
return settings.BLOCKSTORE_USE_BLOCKSTORE_APP_API or BLOCKSTORE_USE_BLOCKSTORE_APP_API.is_enabled()
|
||||
20
openedx/core/lib/blockstore_api/config/waffle.py
Normal file
20
openedx/core/lib/blockstore_api/config/waffle.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Toggles for blockstore.
|
||||
"""
|
||||
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
|
||||
# .. toggle_name: blockstore.use_blockstore_app_api
|
||||
# .. toggle_implementation: WaffleSwitch
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Enable to use the installed blockstore app's Python API directly instead of the
|
||||
# external blockstore service REST API.
|
||||
# The blockstore REST API is used by default.
|
||||
# .. toggle_use_cases: temporary, open_edx
|
||||
# .. toggle_creation_date: 2022-01-13
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_tickets: TNL-8705, BD-14
|
||||
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
|
||||
BLOCKSTORE_USE_BLOCKSTORE_APP_API = WaffleSwitch(
|
||||
'blockstore.use_blockstore_app_api', __name__
|
||||
)
|
||||
60
openedx/core/lib/blockstore_api/db_routers.py
Normal file
60
openedx/core/lib/blockstore_api/db_routers.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Blockstore database router.
|
||||
|
||||
Blockstore started life as an IDA, but is now a Django app plugin within edx-platform.
|
||||
This router exists to smooth blockstore's transition into edxapp.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class BlockstoreRouter:
|
||||
"""
|
||||
A Database Router that uses the ``blockstore`` database, if it's configured in settings.
|
||||
"""
|
||||
ROUTE_APP_LABELS = {'bundles'}
|
||||
DATABASE_NAME = 'blockstore'
|
||||
|
||||
def _use_blockstore(self, model):
|
||||
"""
|
||||
Return True if the given model should use the blockstore database.
|
||||
|
||||
Ensures that a ``blockstore`` database is configured, and checks the ``model``'s app label.
|
||||
"""
|
||||
return (self.DATABASE_NAME in settings.DATABASES) and (model._meta.app_label in self.ROUTE_APP_LABELS)
|
||||
|
||||
def db_for_read(self, model, **hints): # pylint: disable=unused-argument
|
||||
"""
|
||||
Use the BlockstoreRouter.DATABASE_NAME when reading blockstore app tables.
|
||||
"""
|
||||
if self._use_blockstore(model):
|
||||
return self.DATABASE_NAME
|
||||
return None
|
||||
|
||||
def db_for_write(self, model, **hints): # pylint: disable=unused-argument
|
||||
"""
|
||||
Use the BlockstoreRouter.DATABASE_NAME when writing to blockstore app tables.
|
||||
"""
|
||||
if self._use_blockstore(model):
|
||||
return self.DATABASE_NAME
|
||||
return None
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints): # pylint: disable=unused-argument
|
||||
"""
|
||||
Allow relations if both objects are blockstore app models.
|
||||
"""
|
||||
if self._use_blockstore(obj1) and self._use_blockstore(obj2):
|
||||
return True
|
||||
return None
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints): # pylint: disable=unused-argument
|
||||
"""
|
||||
Ensure the blockstore tables only appear in the blockstore database.
|
||||
"""
|
||||
if model_name is not None:
|
||||
model = hints.get('model')
|
||||
if model is not None and self._use_blockstore(model):
|
||||
return db == self.DATABASE_NAME
|
||||
if db == self.DATABASE_NAME:
|
||||
return False
|
||||
|
||||
return None
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Exceptions that may be raised by the Blockstore API
|
||||
"""
|
||||
|
||||
|
||||
class BlockstoreException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(BlockstoreException):
|
||||
pass
|
||||
|
||||
|
||||
class CollectionNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class BundleNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class DraftNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class BundleFileNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class BundleStorageError(BlockstoreException):
|
||||
pass
|
||||
@@ -3,6 +3,7 @@ API Client methods for working with Blockstore bundles and drafts
|
||||
"""
|
||||
|
||||
import base64
|
||||
from functools import wraps
|
||||
from urllib.parse import urlencode
|
||||
from uuid import UUID
|
||||
|
||||
@@ -11,23 +12,40 @@ from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import requests
|
||||
|
||||
from .models import (
|
||||
Bundle,
|
||||
Collection,
|
||||
Draft,
|
||||
BundleFile,
|
||||
DraftFile,
|
||||
LinkDetails,
|
||||
LinkReference,
|
||||
DraftLinkDetails,
|
||||
from blockstore.apps.api.data import (
|
||||
BundleData,
|
||||
CollectionData,
|
||||
DraftData,
|
||||
BundleVersionData,
|
||||
BundleFileData,
|
||||
DraftFileData,
|
||||
BundleLinkData,
|
||||
DraftLinkData,
|
||||
Dependency,
|
||||
)
|
||||
from .exceptions import (
|
||||
from blockstore.apps.api.exceptions import (
|
||||
NotFound,
|
||||
CollectionNotFound,
|
||||
BundleNotFound,
|
||||
DraftNotFound,
|
||||
BundleFileNotFound,
|
||||
)
|
||||
import blockstore.apps.api.methods as blockstore_api_methods
|
||||
|
||||
from .config import use_blockstore_app
|
||||
|
||||
|
||||
def toggle_blockstore_api(func):
|
||||
"""
|
||||
Decorator function to toggle usage of the Blockstore service
|
||||
and the in-built Blockstore app dependency.
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if use_blockstore_app():
|
||||
return getattr(blockstore_api_methods, func.__name__)(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def api_url(*path_parts):
|
||||
@@ -55,17 +73,17 @@ def api_request(method, url, **kwargs):
|
||||
def _collection_from_response(data):
|
||||
"""
|
||||
Given data about a Collection returned by any blockstore REST API, convert it to
|
||||
a Collection instance.
|
||||
a CollectionData instance.
|
||||
"""
|
||||
return Collection(uuid=UUID(data['uuid']), title=data['title'])
|
||||
return CollectionData(uuid=UUID(data['uuid']), title=data['title'])
|
||||
|
||||
|
||||
def _bundle_from_response(data):
|
||||
"""
|
||||
Given data about a Bundle returned by any blockstore REST API, convert it to
|
||||
a Bundle instance.
|
||||
a BundleData instance.
|
||||
"""
|
||||
return Bundle(
|
||||
return BundleData(
|
||||
uuid=UUID(data['uuid']),
|
||||
title=data['title'],
|
||||
description=data['description'],
|
||||
@@ -78,25 +96,51 @@ def _bundle_from_response(data):
|
||||
)
|
||||
|
||||
|
||||
def _bundle_version_from_response(data):
|
||||
"""
|
||||
Given data about a BundleVersion returned by any blockstore REST API, convert it to
|
||||
a BundleVersionData instance.
|
||||
"""
|
||||
return BundleVersionData(
|
||||
bundle_uuid=UUID(data['bundle_uuid']),
|
||||
version=data.get('version', 0),
|
||||
change_description=data['change_description'],
|
||||
created_at=dateutil.parser.parse(data['snapshot']['created_at']),
|
||||
files={
|
||||
path: BundleFileData(path=path, **filedata)
|
||||
for path, filedata in data['snapshot']['files'].items()
|
||||
},
|
||||
links={
|
||||
name: BundleLinkData(
|
||||
name=name,
|
||||
direct=Dependency(**link["direct"]),
|
||||
indirect=[Dependency(**ind) for ind in link["indirect"]],
|
||||
)
|
||||
for name, link in data['snapshot']['links'].items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _draft_from_response(data):
|
||||
"""
|
||||
Given data about a Draft returned by any blockstore REST API, convert it to
|
||||
a Draft instance.
|
||||
a DraftData instance.
|
||||
"""
|
||||
return Draft(
|
||||
return DraftData(
|
||||
uuid=UUID(data['uuid']),
|
||||
bundle_uuid=UUID(data['bundle_uuid']),
|
||||
name=data['name'],
|
||||
created_at=dateutil.parser.parse(data['staged_draft']['created_at']),
|
||||
updated_at=dateutil.parser.parse(data['staged_draft']['updated_at']),
|
||||
files={
|
||||
path: DraftFile(path=path, **file)
|
||||
path: DraftFileData(path=path, **file)
|
||||
for path, file in data['staged_draft']['files'].items()
|
||||
},
|
||||
links={
|
||||
name: DraftLinkDetails(
|
||||
name: DraftLinkData(
|
||||
name=name,
|
||||
direct=LinkReference(**link["direct"]),
|
||||
indirect=[LinkReference(**ind) for ind in link["indirect"]],
|
||||
direct=Dependency(**link["direct"]),
|
||||
indirect=[Dependency(**ind) for ind in link["indirect"]],
|
||||
modified=link["modified"],
|
||||
)
|
||||
for name, link in data['staged_draft']['links'].items()
|
||||
@@ -104,6 +148,7 @@ def _draft_from_response(data):
|
||||
)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_collection(collection_uuid):
|
||||
"""
|
||||
Retrieve metadata about the specified collection
|
||||
@@ -118,6 +163,7 @@ def get_collection(collection_uuid):
|
||||
return _collection_from_response(data)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def create_collection(title):
|
||||
"""
|
||||
Create a new collection.
|
||||
@@ -126,6 +172,7 @@ def create_collection(title):
|
||||
return _collection_from_response(result)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def update_collection(collection_uuid, title):
|
||||
"""
|
||||
Update a collection's title
|
||||
@@ -136,6 +183,7 @@ def update_collection(collection_uuid, title):
|
||||
return _collection_from_response(result)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def delete_collection(collection_uuid):
|
||||
"""
|
||||
Delete a collection
|
||||
@@ -144,6 +192,7 @@ def delete_collection(collection_uuid):
|
||||
api_request('delete', api_url('collections', str(collection_uuid)))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundles(uuids=None, text_search=None):
|
||||
"""
|
||||
Get the details of all bundles
|
||||
@@ -159,6 +208,7 @@ def get_bundles(uuids=None, text_search=None):
|
||||
return [_bundle_from_response(item) for item in response]
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle(bundle_uuid):
|
||||
"""
|
||||
Retrieve metadata about the specified bundle
|
||||
@@ -173,6 +223,7 @@ def get_bundle(bundle_uuid):
|
||||
return _bundle_from_response(data)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def create_bundle(collection_uuid, slug, title="New Bundle", description=""):
|
||||
"""
|
||||
Create a new bundle.
|
||||
@@ -188,6 +239,7 @@ def create_bundle(collection_uuid, slug, title="New Bundle", description=""):
|
||||
return _bundle_from_response(result)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def update_bundle(bundle_uuid, **fields):
|
||||
"""
|
||||
Update a bundle's title, description, slug, or collection.
|
||||
@@ -207,6 +259,7 @@ def update_bundle(bundle_uuid, **fields):
|
||||
return _bundle_from_response(result)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def delete_bundle(bundle_uuid):
|
||||
"""
|
||||
Delete a bundle
|
||||
@@ -215,6 +268,7 @@ def delete_bundle(bundle_uuid):
|
||||
api_request('delete', api_url('bundles', str(bundle_uuid)))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_draft(draft_uuid):
|
||||
"""
|
||||
Retrieve metadata about the specified draft.
|
||||
@@ -228,6 +282,7 @@ def get_draft(draft_uuid):
|
||||
return _draft_from_response(data)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_or_create_bundle_draft(bundle_uuid, draft_name):
|
||||
"""
|
||||
Retrieve metadata about the specified draft.
|
||||
@@ -245,6 +300,7 @@ def get_or_create_bundle_draft(bundle_uuid, draft_name):
|
||||
return get_draft(UUID(response["uuid"]))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def commit_draft(draft_uuid):
|
||||
"""
|
||||
Commit all of the pending changes in the draft, creating a new version of
|
||||
@@ -255,6 +311,7 @@ def commit_draft(draft_uuid):
|
||||
api_request('post', api_url('drafts', str(draft_uuid), 'commit'))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def delete_draft(draft_uuid):
|
||||
"""
|
||||
Delete the specified draft, removing any staged changes/files/deletes.
|
||||
@@ -264,6 +321,7 @@ def delete_draft(draft_uuid):
|
||||
api_request('delete', api_url('drafts', str(draft_uuid)))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_version(bundle_uuid, version_number):
|
||||
"""
|
||||
Get the details of the specified bundle version
|
||||
@@ -271,9 +329,10 @@ def get_bundle_version(bundle_uuid, version_number):
|
||||
if version_number == 0:
|
||||
return None
|
||||
version_url = api_url('bundle_versions', str(bundle_uuid) + ',' + str(version_number))
|
||||
return api_request('get', version_url)
|
||||
return _bundle_version_from_response(api_request('get', version_url))
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_version_files(bundle_uuid, version_number):
|
||||
"""
|
||||
Get a list of the files in the specified bundle version
|
||||
@@ -281,9 +340,10 @@ def get_bundle_version_files(bundle_uuid, version_number):
|
||||
if version_number == 0:
|
||||
return []
|
||||
version_info = get_bundle_version(bundle_uuid, version_number)
|
||||
return [BundleFile(path=path, **file_metadata) for path, file_metadata in version_info["snapshot"]["files"].items()]
|
||||
return list(version_info.files.values())
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_version_links(bundle_uuid, version_number):
|
||||
"""
|
||||
Get a dictionary of the links in the specified bundle version
|
||||
@@ -291,22 +351,16 @@ def get_bundle_version_links(bundle_uuid, version_number):
|
||||
if version_number == 0:
|
||||
return {}
|
||||
version_info = get_bundle_version(bundle_uuid, version_number)
|
||||
return {
|
||||
name: LinkDetails(
|
||||
name=name,
|
||||
direct=LinkReference(**link["direct"]),
|
||||
indirect=[LinkReference(**ind) for ind in link["indirect"]],
|
||||
)
|
||||
for name, link in version_info['snapshot']['links'].items()
|
||||
}
|
||||
return version_info.links
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_files_dict(bundle_uuid, use_draft=None):
|
||||
"""
|
||||
Get a dict of all the files in the specified bundle.
|
||||
|
||||
Returns a dict where the keys are the paths (strings) and the values are
|
||||
BundleFile or DraftFile tuples.
|
||||
BundleFileData or DraftFileData tuples.
|
||||
"""
|
||||
bundle = get_bundle(bundle_uuid)
|
||||
if use_draft and use_draft in bundle.drafts: # pylint: disable=unsupported-membership-test
|
||||
@@ -319,6 +373,7 @@ def get_bundle_files_dict(bundle_uuid, use_draft=None):
|
||||
return {file_meta.path: file_meta for file_meta in get_bundle_version_files(bundle_uuid, bundle.latest_version)}
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_files(bundle_uuid, use_draft=None):
|
||||
"""
|
||||
Get an iterator over all the files in the specified bundle or draft.
|
||||
@@ -326,12 +381,13 @@ def get_bundle_files(bundle_uuid, use_draft=None):
|
||||
return get_bundle_files_dict(bundle_uuid, use_draft).values()
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_links(bundle_uuid, use_draft=None):
|
||||
"""
|
||||
Get a dict of all the links in the specified bundle.
|
||||
|
||||
Returns a dict where the keys are the link names (strings) and the values
|
||||
are LinkDetails or DraftLinkDetails tuples.
|
||||
are BundleLinkData or DraftLinkData tuples.
|
||||
"""
|
||||
bundle = get_bundle(bundle_uuid)
|
||||
if use_draft and use_draft in bundle.drafts: # pylint: disable=unsupported-membership-test
|
||||
@@ -344,6 +400,7 @@ def get_bundle_links(bundle_uuid, use_draft=None):
|
||||
return get_bundle_version_links(bundle_uuid, bundle.latest_version)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_file_metadata(bundle_uuid, path, use_draft=None):
|
||||
"""
|
||||
Get the metadata of the specified file.
|
||||
@@ -358,6 +415,7 @@ def get_bundle_file_metadata(bundle_uuid, path, use_draft=None):
|
||||
)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def get_bundle_file_data(bundle_uuid, path, use_draft=None):
|
||||
"""
|
||||
Read all the data in the given bundle file and return it as a
|
||||
@@ -370,6 +428,7 @@ def get_bundle_file_data(bundle_uuid, path, use_draft=None):
|
||||
return r.content
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def write_draft_file(draft_uuid, path, contents):
|
||||
"""
|
||||
Create or overwrite the file at 'path' in the specified draft with the given
|
||||
@@ -382,11 +441,12 @@ def write_draft_file(draft_uuid, path, contents):
|
||||
"""
|
||||
api_request('patch', api_url('drafts', str(draft_uuid)), json={
|
||||
'files': {
|
||||
path: encode_str_for_draft(contents) if contents is not None else None,
|
||||
path: _encode_str_for_draft(contents) if contents is not None else None,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def set_draft_link(draft_uuid, link_name, bundle_uuid, version):
|
||||
"""
|
||||
Create or replace the link with the given name in the specified draft so
|
||||
@@ -405,7 +465,7 @@ def set_draft_link(draft_uuid, link_name, bundle_uuid, version):
|
||||
})
|
||||
|
||||
|
||||
def encode_str_for_draft(input_str):
|
||||
def _encode_str_for_draft(input_str):
|
||||
"""
|
||||
Given a string, return UTF-8 representation that is then base64 encoded.
|
||||
"""
|
||||
@@ -416,10 +476,10 @@ def encode_str_for_draft(input_str):
|
||||
return base64.b64encode(binary)
|
||||
|
||||
|
||||
@toggle_blockstore_api
|
||||
def force_browser_url(blockstore_file_url):
|
||||
"""
|
||||
Ensure that the given URL Blockstore is a URL accessible from the end user's
|
||||
browser.
|
||||
Ensure that the given devstack URL is a URL accessible from the end user's browser.
|
||||
"""
|
||||
# Hack: on some devstacks, we must necessarily use different URLs for
|
||||
# accessing Blockstore file data from within and outside of docker
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""
|
||||
Data models used for Blockstore API Client
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
def _convert_to_uuid(value):
|
||||
if not isinstance(value, UUID):
|
||||
return UUID(value)
|
||||
return value
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class Collection:
|
||||
"""
|
||||
Metadata about a blockstore collection
|
||||
"""
|
||||
uuid = attr.ib(type=UUID, converter=_convert_to_uuid)
|
||||
title = attr.ib(type=str)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class Bundle:
|
||||
"""
|
||||
Metadata about a blockstore bundle
|
||||
"""
|
||||
uuid = attr.ib(type=UUID, converter=_convert_to_uuid)
|
||||
title = attr.ib(type=str)
|
||||
description = attr.ib(type=str)
|
||||
slug = attr.ib(type=str)
|
||||
drafts = attr.ib(type=dict) # Dict of drafts, where keys are the draft names and values are draft UUIDs
|
||||
# Note that if latest_version is 0, it means that no versions yet exist
|
||||
latest_version = attr.ib(type=int, validator=attr.validators.instance_of(int))
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class Draft:
|
||||
"""
|
||||
Metadata about a blockstore draft
|
||||
"""
|
||||
uuid = attr.ib(type=UUID, converter=_convert_to_uuid)
|
||||
bundle_uuid = attr.ib(type=UUID, converter=_convert_to_uuid)
|
||||
name = attr.ib(type=str)
|
||||
updated_at = attr.ib(type=datetime, validator=attr.validators.instance_of(datetime))
|
||||
files = attr.ib(type=dict)
|
||||
links = attr.ib(type=dict)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class BundleFile:
|
||||
"""
|
||||
Metadata about a file in a blockstore bundle or draft.
|
||||
"""
|
||||
path = attr.ib(type=str)
|
||||
size = attr.ib(type=int)
|
||||
url = attr.ib(type=str)
|
||||
hash_digest = attr.ib(type=str)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class DraftFile(BundleFile):
|
||||
"""
|
||||
Metadata about a file in a blockstore draft.
|
||||
"""
|
||||
modified = attr.ib(type=bool) # Was this file modified in the draft?
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class LinkReference:
|
||||
"""
|
||||
A pointer to a specific BundleVersion
|
||||
"""
|
||||
bundle_uuid = attr.ib(type=UUID, converter=_convert_to_uuid)
|
||||
version = attr.ib(type=int)
|
||||
snapshot_digest = attr.ib(type=str)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class LinkDetails:
|
||||
"""
|
||||
Details about a specific link in a BundleVersion or Draft
|
||||
"""
|
||||
name = attr.ib(type=str)
|
||||
direct = attr.ib(type=LinkReference)
|
||||
indirect = attr.ib(type=list) # List of LinkReference objects
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class DraftLinkDetails(LinkDetails):
|
||||
"""
|
||||
Details about a specific link in a Draft
|
||||
"""
|
||||
modified = attr.ib(type=bool)
|
||||
@@ -2,22 +2,25 @@
|
||||
Tests for xblock_utils.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.lib import blockstore_api as api
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
BlockstoreAppTestMixin,
|
||||
requires_blockstore,
|
||||
requires_blockstore_app,
|
||||
)
|
||||
|
||||
# A fake UUID that won't represent any real bundle/draft/collection:
|
||||
BAD_UUID = UUID('12345678-0000-0000-0000-000000000000')
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
|
||||
class BlockstoreApiClientTest(unittest.TestCase):
|
||||
class BlockstoreApiClientTestMixin:
|
||||
"""
|
||||
Test for the Blockstore API Client.
|
||||
Tests for the Blockstore API Client.
|
||||
|
||||
The goal of these tests is not to test that Blockstore works correctly, but
|
||||
that the API client can interact with it and all the API client methods
|
||||
@@ -192,3 +195,17 @@ class BlockstoreApiClientTest(unittest.TestCase):
|
||||
# Finally, test deleting a link from course's draft:
|
||||
api.set_draft_link(course_draft.uuid, link2_name, None, None)
|
||||
assert not api.get_bundle_links(course_bundle.uuid, use_draft=course_draft.name)
|
||||
|
||||
|
||||
@requires_blockstore
|
||||
class BlockstoreServiceApiClientTest(BlockstoreApiClientTestMixin, TestCase):
|
||||
"""
|
||||
Test the Blockstore API Client, using the standalone Blockstore service.
|
||||
"""
|
||||
|
||||
|
||||
@requires_blockstore_app
|
||||
class BlockstoreAppApiClientTest(BlockstoreApiClientTestMixin, BlockstoreAppTestMixin, TestCase):
|
||||
"""
|
||||
Test the Blockstore API Client, using the installed Blockstore app.
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/openedx/blockstore.git@1.2.1#egg=blockstore==1.2.1
|
||||
# via -r requirements/edx/github.in
|
||||
-e common/lib/capa
|
||||
# via
|
||||
# -r requirements/edx/local.in
|
||||
@@ -54,6 +56,7 @@ attrs==21.4.0
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# aiohttp
|
||||
# blockstore
|
||||
# edx-ace
|
||||
# openedx-events
|
||||
babel==2.10.1
|
||||
@@ -182,6 +185,7 @@ django==3.2.13
|
||||
# via
|
||||
# -c requirements/edx/../common_constraints.txt
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# django-appconf
|
||||
# django-classy-tags
|
||||
# django-config-models
|
||||
@@ -272,6 +276,8 @@ django-crum==0.7.9
|
||||
# edx-rbac
|
||||
# edx-toggles
|
||||
# super-csv
|
||||
django-environ==0.8.1
|
||||
# via blockstore
|
||||
django-fernet-fields==0.6
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
@@ -280,6 +286,7 @@ django-fernet-fields==0.6
|
||||
django-filter==21.1
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# edx-enterprise
|
||||
# lti-consumer-xblock
|
||||
django-ipware==4.0.2
|
||||
@@ -359,6 +366,7 @@ django-user-tasks==3.0.0
|
||||
django-waffle==2.4.1
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -372,8 +380,10 @@ django-webpack-loader==0.7.0
|
||||
djangorestframework==3.12.4
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# django-user-tasks
|
||||
# djangorestframework-expander
|
||||
# drf-jwt
|
||||
# drf-yasg
|
||||
# edx-api-doc-tools
|
||||
@@ -386,6 +396,8 @@ djangorestframework==3.12.4
|
||||
# edx-submissions
|
||||
# ora2
|
||||
# super-csv
|
||||
djangorestframework-expander==0.2.3
|
||||
# via blockstore
|
||||
djangorestframework-xml==2.0.0
|
||||
# via edx-enterprise
|
||||
docopt==0.6.2
|
||||
@@ -403,7 +415,9 @@ drf-yasg==1.20.0
|
||||
edx-ace==1.5.0
|
||||
# via -r requirements/edx/base.in
|
||||
edx-api-doc-tools==1.6.0
|
||||
# via -r requirements/edx/base.in
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
edx-auth-backends==4.1.0
|
||||
# via -r requirements/edx/base.in
|
||||
edx-braze-client==0.1.3
|
||||
@@ -422,12 +436,15 @@ edx-celeryutils==1.2.1
|
||||
edx-completion==4.2.0
|
||||
# via -r requirements/edx/base.in
|
||||
edx-django-release-util==1.2.0
|
||||
# via -r requirements/edx/base.in
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
edx-django-sites-extensions==4.0.0
|
||||
# via -r requirements/edx/base.in
|
||||
edx-django-utils==4.6.0
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -685,6 +702,7 @@ mysqlclient==2.1.0
|
||||
newrelic==7.10.0.175
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
nltk==3.7
|
||||
# via
|
||||
@@ -756,6 +774,8 @@ py2neo==2021.2.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.in
|
||||
pyblake2==1.1.2
|
||||
# via blockstore
|
||||
pycountry==22.3.5
|
||||
# via -r requirements/edx/base.in
|
||||
pycparser==2.21
|
||||
@@ -847,6 +867,7 @@ pytz==2022.1
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# babel
|
||||
# blockstore
|
||||
# celery
|
||||
# django
|
||||
# django-ses
|
||||
@@ -987,6 +1008,7 @@ soupsieve==2.3.2.post1
|
||||
sqlparse==0.4.2
|
||||
# via
|
||||
# -r requirements/edx/base.in
|
||||
# blockstore
|
||||
# django
|
||||
staff-graded-xblock==2.0.1
|
||||
# via -r requirements/edx/base.in
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/openedx/blockstore.git@1.2.1#egg=blockstore==1.2.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e common/lib/capa
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -79,6 +81,7 @@ attrs==21.4.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# aiohttp
|
||||
# blockstore
|
||||
# edx-ace
|
||||
# jsonschema
|
||||
# openedx-events
|
||||
@@ -267,6 +270,7 @@ django==3.2.13
|
||||
# via
|
||||
# -c requirements/edx/../common_constraints.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# django-appconf
|
||||
# django-classy-tags
|
||||
# django-config-models
|
||||
@@ -364,6 +368,8 @@ django-crum==0.7.9
|
||||
# super-csv
|
||||
django-debug-toolbar==3.2.4
|
||||
# via -r requirements/edx/development.in
|
||||
django-environ==0.8.1
|
||||
# via blockstore
|
||||
django-fernet-fields==0.6
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -372,6 +378,7 @@ django-fernet-fields==0.6
|
||||
django-filter==21.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# edx-enterprise
|
||||
# lti-consumer-xblock
|
||||
django-ipware==4.0.2
|
||||
@@ -457,6 +464,7 @@ django-user-tasks==3.0.0
|
||||
django-waffle==2.4.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -470,8 +478,10 @@ django-webpack-loader==0.7.0
|
||||
djangorestframework==3.12.4
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# django-user-tasks
|
||||
# djangorestframework-expander
|
||||
# drf-jwt
|
||||
# drf-yasg
|
||||
# edx-api-doc-tools
|
||||
@@ -484,6 +494,8 @@ djangorestframework==3.12.4
|
||||
# edx-submissions
|
||||
# ora2
|
||||
# super-csv
|
||||
djangorestframework-expander==0.2.3
|
||||
# via blockstore
|
||||
djangorestframework-xml==2.0.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -512,11 +524,15 @@ drf-yasg==1.20.0
|
||||
edx-ace==1.5.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
edx-api-doc-tools==1.6.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
edx-auth-backends==4.1.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
edx-braze-client==0.1.3
|
||||
# via -r requirements/edx/testing.txt
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
edx-bulk-grades==1.0.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -531,12 +547,15 @@ edx-celeryutils==1.2.1
|
||||
edx-completion==4.2.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
edx-django-release-util==1.2.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
edx-django-sites-extensions==4.0.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
edx-django-utils==4.6.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -917,6 +936,7 @@ mysqlclient==2.1.0
|
||||
newrelic==7.10.0.175
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
nltk==3.7
|
||||
# via
|
||||
@@ -1027,6 +1047,8 @@ py2neo==2021.2.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
pyblake2==1.1.2
|
||||
# via blockstore
|
||||
pycodestyle==2.8.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
pycountry==22.3.5
|
||||
@@ -1195,6 +1217,7 @@ pytz==2022.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# babel
|
||||
# blockstore
|
||||
# celery
|
||||
# django
|
||||
# django-ses
|
||||
@@ -1402,6 +1425,7 @@ sphinxcontrib-serializinghtml==1.1.5
|
||||
sqlparse==0.4.2
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# blockstore
|
||||
# django
|
||||
# django-debug-toolbar
|
||||
staff-graded-xblock==2.0.1
|
||||
|
||||
@@ -63,6 +63,7 @@ git+https://github.com/edx/MongoDBProxy.git@d92bafe9888d2940f647a7b2b2383b29c752
|
||||
git+https://github.com/edx/django-require.git@0c54adb167142383b26ea6b3edecc3211822a776#egg=django-require==1.0.12
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/openedx/blockstore.git@1.2.1#egg=blockstore==1.2.1
|
||||
-e git+https://github.com/edx/codejail.git@3.1.3#egg=codejail==3.1.3
|
||||
-e git+https://github.com/edx/RateXBlock.git@2.0.1#egg=rate-xblock
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/openedx/blockstore.git@1.2.1#egg=blockstore==1.2.1
|
||||
# via -r requirements/edx/base.txt
|
||||
-e common/lib/capa
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -74,6 +76,7 @@ attrs==21.4.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# aiohttp
|
||||
# blockstore
|
||||
# edx-ace
|
||||
# openedx-events
|
||||
# outcome
|
||||
@@ -255,6 +258,7 @@ distlib==0.3.4
|
||||
# via
|
||||
# -c requirements/edx/../common_constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# django-appconf
|
||||
# django-classy-tags
|
||||
# django-config-models
|
||||
@@ -349,6 +353,10 @@ django-crum==0.7.9
|
||||
# edx-rbac
|
||||
# edx-toggles
|
||||
# super-csv
|
||||
django-environ==0.8.1
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
django-fernet-fields==0.6
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -357,6 +365,7 @@ django-fernet-fields==0.6
|
||||
django-filter==21.1
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# edx-enterprise
|
||||
# lti-consumer-xblock
|
||||
django-ipware==4.0.2
|
||||
@@ -442,6 +451,7 @@ django-user-tasks==3.0.0
|
||||
django-waffle==2.4.1
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -455,8 +465,10 @@ django-webpack-loader==0.7.0
|
||||
djangorestframework==3.12.4
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# django-user-tasks
|
||||
# djangorestframework-expander
|
||||
# drf-jwt
|
||||
# drf-yasg
|
||||
# edx-api-doc-tools
|
||||
@@ -469,6 +481,10 @@ djangorestframework==3.12.4
|
||||
# edx-submissions
|
||||
# ora2
|
||||
# super-csv
|
||||
djangorestframework-expander==0.2.3
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
djangorestframework-xml==2.0.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -495,11 +511,15 @@ drf-yasg==1.20.0
|
||||
edx-ace==1.5.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-api-doc-tools==1.6.0
|
||||
# via -r requirements/edx/base.txt
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
edx-auth-backends==4.1.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-braze-client==0.1.3
|
||||
# via -r requirements/edx/base.txt
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
edx-bulk-grades==1.0.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -514,12 +534,15 @@ edx-celeryutils==1.2.1
|
||||
edx-completion==4.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-django-release-util==1.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
edx-django-sites-extensions==4.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-django-utils==4.6.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# django-config-models
|
||||
# edx-drf-extensions
|
||||
# edx-enterprise
|
||||
@@ -864,6 +887,7 @@ mysqlclient==2.1.0
|
||||
newrelic==7.10.0.175
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# edx-django-utils
|
||||
nltk==3.7
|
||||
# via
|
||||
@@ -966,6 +990,10 @@ py2neo==2021.2.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
pyblake2==1.1.2
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
pycodestyle==2.8.0
|
||||
# via -r requirements/edx/testing.in
|
||||
pycountry==22.3.5
|
||||
@@ -1122,6 +1150,7 @@ pytz==2022.1
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# babel
|
||||
# blockstore
|
||||
# celery
|
||||
# django
|
||||
# django-ses
|
||||
@@ -1297,6 +1326,7 @@ soupsieve==2.3.2.post1
|
||||
sqlparse==0.4.2
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# blockstore
|
||||
# django
|
||||
staff-graded-xblock==2.0.1
|
||||
# via -r requirements/edx/base.txt
|
||||
|
||||
Reference in New Issue
Block a user