Commit 5085b716 by Will Daly

Merge remote-tracking branch 'origin/master' into will/combine-reg-login-form

Conflicts:
	common/djangoapps/third_party_auth/pipeline.py
	lms/djangoapps/verify_student/tests/test_views.py
parents 8f1cbada b0b1744e

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

......@@ -179,3 +179,4 @@ Henry Tareque <henry.tareque@gmail.com>
Eugeny Kolpakov <eugeny.kolpakov@gmail.com>
Omar Al-Ithawi <oithawi@qrf.org>
Louis Pilfold <louis@lpil.uk>
Akiva Leffert <akiva@edx.org>
......@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected.
Common: Add configurable reset button to units
Studio: Add support xblock validation messages on Studio unit/container page. TNL-683
LMS: Support adding cohorts from the instructor dashboard. TNL-162
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
......
# pylint: disable=C0111
# pylint: disable=missing-docstring
from lettuce import world, step
from selenium.webdriver.common.keys import Keys
......@@ -20,9 +20,10 @@ SELECTORS = {
# We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5
@step('youtube stub server (.*) YouTube API')
def configure_youtube_api(_step, action):
action=action.strip()
action = action.strip()
if action == 'proxies':
world.youtube.config['youtube_api_blocked'] = False
elif action == 'blocks':
......@@ -30,6 +31,7 @@ def configure_youtube_api(_step, action):
else:
raise ValueError('Parameter `action` should be one of "proxies" or "blocks".')
@step('I have created a Video component$')
def i_created_a_video_component(step):
step.given('I am in Studio editing a new unit')
......@@ -47,6 +49,7 @@ def i_created_a_video_component(step):
if not world.youtube.config.get('youtube_api_blocked'):
world.wait_for_visible(SELECTORS['controls'])
@step('I have created a Video component with subtitles$')
def i_created_a_video_with_subs(_step):
_step.given('I have created a Video component with subtitles "OEoXaMPEzfM"')
......@@ -221,7 +224,7 @@ def see_a_range_slider_with_proper_range(_step):
def do_not_see_or_not_button_video(_step, action, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete()
action=action.strip()
action = action.strip()
button = button_type.strip()
if action == 'do not':
assert not world.is_css_present(VIDEO_BUTTONS[button])
......
......@@ -11,6 +11,9 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo.base import location_to_query
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.xml_importer import import_from_xml
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
class ExportAllCourses(ModuleStoreTestCase):
......@@ -30,7 +33,7 @@ class ExportAllCourses(ModuleStoreTestCase):
import_from_xml(
self.module_store,
'**replace_user**',
'common/test/data/',
TEST_DATA_DIR,
['dot-underscore'],
static_content_store=self.content_store,
do_import_static=True,
......
......@@ -607,11 +607,13 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_correct_http_status(self):
json_data = json.dumps({
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
"days_early_for_beta": {"value": "supposed to be an integer",
"display_name": "Days Early for Beta Users", },
"advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
})
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
"days_early_for_beta": {
"value": "supposed to be an integer",
"display_name": "Days Early for Beta Users",
},
"advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
})
response = self.client.ajax_post(self.course_setting_url, json_data)
self.assertEqual(400, response.status_code)
......@@ -623,7 +625,7 @@ class CourseMetadataEditingTest(CourseTestCase):
"days_early_for_beta": {"value": 2},
},
user=self.user
)
)
self.update_check(test_model)
# try fresh fetch to ensure persistence
fresh = modulestore().get_course(self.course.id)
......
......@@ -66,8 +66,10 @@ class TemplateTests(unittest.TestCase):
self.assertEqual(index_info['course'], 'course')
self.assertEqual(index_info['run'], '2014')
test_chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location)
test_chapter = persistent_factories.ItemFactory.create(
display_name='chapter 1',
parent_location=test_course.location
)
self.assertIsInstance(test_chapter, SequenceDescriptor)
# refetch parent which should now point to child
test_course = self.split_store.get_course(test_course.id.version_agnostic())
......@@ -156,8 +158,10 @@ class TemplateTests(unittest.TestCase):
course='history', run='doomed', org='edu.harvard',
display_name='doomed test course',
user_id='testbot')
persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location)
persistent_factories.ItemFactory.create(
display_name='chapter 1',
parent_location=test_course.location
)
id_locator = test_course.id.for_branch(ModuleStoreEnum.BranchName.draft)
guid_locator = test_course.location.course_agnostic()
......@@ -180,10 +184,17 @@ class TemplateTests(unittest.TestCase):
display_name='history test course',
user_id='testbot'
)
chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location, user_id='testbot')
sub = persistent_factories.ItemFactory.create(display_name='subsection 1',
parent_location=chapter.location, user_id='testbot', category='vertical')
chapter = persistent_factories.ItemFactory.create(
display_name='chapter 1',
parent_location=test_course.location,
user_id='testbot'
)
sub = persistent_factories.ItemFactory.create(
display_name='subsection 1',
parent_location=chapter.location,
user_id='testbot',
category='vertical'
)
first_problem = persistent_factories.ItemFactory.create(
display_name='problem 1', parent_location=sub.location, user_id='testbot', category='problem',
data="<problem></problem>"
......
......@@ -24,6 +24,8 @@ from uuid import uuid4
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
@ddt.ddt
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
......@@ -48,7 +50,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
......@@ -70,7 +72,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
course_items = import_from_xml(
module_store,
self.user.id,
'common/test/data',
TEST_DATA_DIR,
['test_import_course_2'],
target_course_id=course.id,
verbose=True,
......@@ -86,7 +88,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['2014_Uni'],
target_course_id=course_id
)
......@@ -131,7 +133,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
content_store = contentstore()
module_store = modulestore()
import_from_xml(module_store, self.user.id, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True)
import_from_xml(module_store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store, do_import_static=False, verbose=True)
course = module_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
......@@ -142,7 +144,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
def test_no_static_link_rewrites_on_import(self):
module_store = modulestore()
courses = import_from_xml(module_store, self.user.id, 'common/test/data/', ['toy'], do_import_static=False, verbose=True)
courses = import_from_xml(module_store, self.user.id, TEST_DATA_DIR, ['toy'], do_import_static=False, verbose=True)
course_key = courses[0].id
handouts = module_store.get_item(course_key.make_usage_key('course_info', 'handouts'))
......@@ -183,7 +185,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['conditional'],
target_course_id=target_course_id
)
......@@ -213,7 +215,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['open_ended'],
target_course_id=target_course_id
)
......@@ -254,7 +256,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
[source_course_name],
target_course_id=target_course_id
)
......
......@@ -2,6 +2,9 @@ from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
# This test is in the CMS module because the test configuration to use a draft
......@@ -10,7 +13,7 @@ class DraftReorderTestCase(ModuleStoreTestCase):
def test_order(self):
store = modulestore()
course_items = import_from_xml(store, self.user.id, 'common/test/data/', ['import_draft_order'])
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['import_draft_order'])
course_key = course_items[0].id
sequential = store.get_item(course_key.make_usage_key('sequential', '0f4f7649b10141b0bdc9922dcf94515a'))
verticals = sequential.children
......
......@@ -7,8 +7,10 @@ from xblock.fields import String
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo.draft import as_draft
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
class StubXBlock(XBlock):
......@@ -59,7 +61,7 @@ class XBlockImportTest(ModuleStoreTestCase):
"""
courses = import_from_xml(
self.store, self.user.id, 'common/test/data', [course_dir]
self.store, self.user.id, TEST_DATA_DIR, [course_dir]
)
xblock_location = courses[0].id.make_usage_key('stubxblock', 'xblock_test')
......
......@@ -18,6 +18,9 @@ from student.models import Registration
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from contentstore.utils import reverse_url
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
def parse_json(response):
......@@ -128,6 +131,8 @@ class CourseTestCase(ModuleStoreTestCase):
PRIVATE_VERTICAL = 'a_private_vertical'
PUBLISHED_VERTICAL = 'a_published_vertical'
SEQUENTIAL = 'vertical_sequential'
DRAFT_HTML = 'draft_html'
DRAFT_VIDEO = 'draft_video'
LOCKED_ASSET_KEY = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt')
def import_and_populate_course(self):
......@@ -135,7 +140,7 @@ class CourseTestCase(ModuleStoreTestCase):
Imports the test toy course and populates it with additional test data
"""
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], static_content_store=content_store)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store)
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
# create an Orphan
......@@ -167,6 +172,20 @@ class CourseTestCase(ModuleStoreTestCase):
sequential.children.append(public_vertical.location)
self.store.update_item(sequential, self.user.id)
# create an html and video component to make drafts:
draft_html = self.store.create_item(self.user.id, course_id, 'html', self.DRAFT_HTML)
draft_video = self.store.create_item(self.user.id, course_id, 'video', self.DRAFT_VIDEO)
# add them as children to the public_vertical
public_vertical.children.append(draft_html.location)
public_vertical.children.append(draft_video.location)
self.store.update_item(public_vertical, self.user.id)
# publish changes to vertical
self.store.publish(public_vertical.location, self.user.id)
# convert html/video to draft
self.store.convert_to_draft(draft_html.location, self.user.id)
self.store.convert_to_draft(draft_video.location, self.user.id)
# lock an asset
content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True)
......@@ -199,18 +218,25 @@ class CourseTestCase(ModuleStoreTestCase):
self.assertEqual(self.store.has_published_version(item), publish_state)
def get_and_verify_publish_state(item_type, item_name, publish_state):
"""Gets the given item from the store and verifies the publish state of the item is as expected."""
"""
Gets the given item from the store and verifies the publish state
of the item is as expected.
"""
item = self.store.get_item(course_id.make_usage_key(item_type, item_name))
verify_item_publish_state(item, publish_state)
return item
# verify that the draft vertical is draft
# verify draft vertical has a published version with published children
vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, True)
for child in vertical.get_children():
verify_item_publish_state(child, True)
# make sure that we don't have a sequential that is not in draft mode
# verify that it has a draft too
self.assertTrue(getattr(vertical, "is_draft", False))
# make sure that we don't have a sequential that is in draft mode
sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, True)
self.assertFalse(getattr(sequential, "is_draft", False))
# verify that we have the private vertical
private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, False)
......@@ -218,10 +244,24 @@ class CourseTestCase(ModuleStoreTestCase):
# verify that we have the public vertical
public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, True)
# verify that we have the draft html
draft_html = self.store.get_item(course_id.make_usage_key('html', self.DRAFT_HTML))
self.assertTrue(getattr(draft_html, 'is_draft', False))
# verify that we have the draft video
draft_video = self.store.get_item(course_id.make_usage_key('video', self.DRAFT_VIDEO))
self.assertTrue(getattr(draft_video, 'is_draft', False))
# verify verticals are children of sequential
for vert in [vertical, private_vertical, public_vertical]:
self.assertIn(vert.location, sequential.children)
# verify draft html is the child of the public vertical
self.assertIn(draft_html.location, public_vertical.children)
# verify draft video is the child of the public vertical
self.assertIn(draft_video.location, public_vertical.children)
# verify textbook exists
course = self.store.get_course(course_id)
self.assertGreater(len(course.textbooks), 0)
......
......@@ -1290,10 +1290,12 @@ class GroupConfiguration(object):
'container_handler',
course.location.course_key.make_usage_key(unit.location.block_type, unit.location.name)
)
validation_summary = split_test.general_validation_message()
usage_info[split_test.user_partition_id].append({
'label': '{} / {}'.format(unit.display_name, split_test.display_name),
'url': unit_url,
'validation': split_test.general_validation_message,
'validation': validation_summary.to_json() if validation_summary else None,
})
return usage_info
......
"""
Unit tests for the asset upload endpoint.
"""
# pylint: disable=C0111
# pylint: disable=W0621
# pylint: disable=W0212
from datetime import datetime
from io import BytesIO
from pytz import UTC
......@@ -13,12 +8,17 @@ import json
from contentstore.tests.utils import CourseTestCase
from contentstore.views import assets
from contentstore.utils import reverse_course_url
from xmodule.assetstore.assetmgr import AssetMetadataFoundTemporary
from xmodule.assetstore import AssetMetadata
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
class AssetsTestCase(CourseTestCase):
......@@ -30,12 +30,18 @@ class AssetsTestCase(CourseTestCase):
self.url = reverse_course_url('assets_handler', self.course.id)
def upload_asset(self, name="asset-1"):
"""
Post to the asset upload url
"""
f = BytesIO(name)
f.name = name + ".txt"
return self.client.post(self.url, {"name": name, "file": f})
class BasicAssetsTestCase(AssetsTestCase):
"""
Test getting assets via html w/o additional args
"""
def test_basic(self):
resp = self.client.get(self.url, HTTP_ACCEPT='text/html')
self.assertEquals(resp.status_code, 200)
......@@ -52,7 +58,7 @@ class BasicAssetsTestCase(AssetsTestCase):
course_items = import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['toy'],
static_content_store=contentstore(),
verbose=True
......@@ -76,6 +82,9 @@ class PaginationTestCase(AssetsTestCase):
Tests the pagination of assets returned from the REST API.
"""
def test_json_responses(self):
"""
Test the ajax asset interfaces
"""
self.upload_asset("asset-1")
self.upload_asset("asset-2")
self.upload_asset("asset-3")
......@@ -95,20 +104,26 @@ class PaginationTestCase(AssetsTestCase):
self.assert_correct_asset_response(self.url + "?page_size=3&page=1", 0, 3, 3)
def assert_correct_asset_response(self, url, expected_start, expected_length, expected_total):
"""
Get from the url and ensure it contains the expected number of responses
"""
resp = self.client.get(url, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content)
assets = json_response['assets']
assets_response = json_response['assets']
self.assertEquals(json_response['start'], expected_start)
self.assertEquals(len(assets), expected_length)
self.assertEquals(len(assets_response), expected_length)
self.assertEquals(json_response['totalCount'], expected_total)
def assert_correct_sort_response(self, url, sort, direction):
"""
Get from the url w/ a sort option and ensure items honor that sort
"""
resp = self.client.get(url + '?sort=' + sort + '&direction=' + direction, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content)
assets = json_response['assets']
name1 = assets[0][sort]
name2 = assets[1][sort]
name3 = assets[2][sort]
assets_response = json_response['assets']
name1 = assets_response[0][sort]
name2 = assets_response[1][sort]
name3 = assets_response[2][sort]
if direction == 'asc':
self.assertLessEqual(name1, name2)
self.assertLessEqual(name2, name3)
......@@ -134,6 +149,49 @@ class UploadTestCase(AssetsTestCase):
self.assertEquals(resp.status_code, 400)
class DownloadTestCase(AssetsTestCase):
"""
Unit tests for downloading a file.
"""
def setUp(self):
super(DownloadTestCase, self).setUp()
self.url = reverse_course_url('assets_handler', self.course.id)
# First, upload something.
self.asset_name = 'download_test'
resp = self.upload_asset(self.asset_name)
self.assertEquals(resp.status_code, 200)
self.uploaded_url = json.loads(resp.content)['asset']['url']
def test_download(self):
# Now, download it.
resp = self.client.get(self.uploaded_url, HTTP_ACCEPT='text/html')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp.content, self.asset_name)
def test_download_not_found_throw(self):
url = self.uploaded_url.replace(self.asset_name, 'not_the_asset_name')
resp = self.client.get(url, HTTP_ACCEPT='text/html')
self.assertEquals(resp.status_code, 404)
def test_metadata_found_in_modulestore(self):
# Insert asset metadata into the modulestore (with no accompanying asset).
asset_key = self.course.id.make_asset_key(AssetMetadata.ASSET_TYPE, 'pic1.jpg')
asset_md = AssetMetadata(asset_key, {
'internal_name': 'EKMND332DDBK',
'basename': 'pix/archive',
'locked': False,
'curr_version': '14',
'prev_version': '13'
})
modulestore().save_asset_metadata(asset_md, 15)
# Get the asset metadata and have it be found in the modulestore.
# Currently, no asset metadata should be found in the modulestore. The code is not yet storing it there.
# If asset metadata *is* found there, an exception is raised. This test ensures the exception is indeed raised.
# THIS IS TEMPORARY. Soon, asset metadata *will* be stored in the modulestore.
with self.assertRaises((AssetMetadataFoundTemporary, NameError)):
self.client.get(unicode(asset_key), HTTP_ACCEPT='text/html')
class AssetToJsonTestCase(AssetsTestCase):
"""
Unit test for transforming asset information into something
......@@ -147,6 +205,7 @@ class AssetToJsonTestCase(AssetsTestCase):
location = course_key.make_asset_key('asset', 'my_file_name.jpg')
thumbnail_location = course_key.make_asset_key('thumbnail', 'my_file_name_thumb.jpg')
# pylint: disable=protected-access
output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location, True)
self.assertEquals(output["display_name"], "my_file")
......@@ -185,6 +244,7 @@ class LockAssetTestCase(AssetsTestCase):
resp = self.client.post(
url,
# pylint: disable=protected-access
json.dumps(assets._get_asset_json("sample_static.txt", upload_date, asset_location, None, lock)),
"application/json"
)
......@@ -196,7 +256,7 @@ class LockAssetTestCase(AssetsTestCase):
course_items = import_from_xml(
module_store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['toy'],
static_content_store=contentstore(),
verbose=True
......
......@@ -8,20 +8,17 @@ import datetime
from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url, add_instructor
from contentstore.views.access import has_course_access
from contentstore.views.course import course_outline_initial_state, _course_outline_json
from contentstore.views.course import course_outline_initial_state
from contentstore.views.item import create_xblock_info, VisibilityState
from course_action_state.models import CourseRerunState
from util.date_utils import get_default_time_display
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls, \
mongo_uses_error_check
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from opaque_keys.edx.locator import CourseLocator
from student.tests.factories import UserFactory
from course_action_state.managers import CourseRerunUIStateManager
from django.conf import settings
import ddt
import threading
import pytz
......@@ -312,28 +309,3 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(_get_release_date(response), get_default_time_display(self.course.start))
_assert_settings_link_present(response)
@ddt.ddt
class OutlinePerfTest(TestCourseOutline):
def setUp(self):
with modulestore().default_store(ModuleStoreEnum.Type.split):
super(OutlinePerfTest, self).setUp()
@ddt.data(1, 2, 4, 8)
def test_query_counts(self, num_threads):
"""
Test that increasing threads does not increase query counts
"""
def test_client():
with modulestore().default_store(ModuleStoreEnum.Type.split):
with modulestore().bulk_operations(self.course.id):
course = modulestore().get_course(self.course.id, depth=0)
return _course_outline_json(None, course)
per_thread = 4
with check_mongo_calls(per_thread * num_threads, 0):
outline_threads = [threading.Thread(target=test_client) for __ in xrange(num_threads)]
[thread.start() for thread in outline_threads]
# now wait until they all finish
[thread.join() for thread in outline_threads]
......@@ -9,7 +9,7 @@ from contentstore.views.course import GroupConfiguration
from contentstore.tests.utils import CourseTestCase
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.split_test_module import ValidationMessage, ValidationMessageType
from xmodule.validation import StudioValidation, StudioValidationMessage
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
......@@ -541,87 +541,75 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods):
def setUp(self):
super(GroupConfigurationsValidationTestCase, self).setUp()
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_error_message_present(self, mocked_validation_messages):
@patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test')
def verify_validation_add_usage_info(self, expected_result, mocked_message, mocked_validation_messages):
"""
Tests if validation message is present.
Helper method for testing validation information present after add_usage_info.
"""
self._add_user_partitions()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
mocked_validation_messages.return_value = [
ValidationMessage(
split_test,
u"Validation message",
ValidationMessageType.error
)
]
validation = StudioValidation(split_test.location)
validation.add(mocked_message)
mocked_validation_messages.return_value = validation
group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
self.assertEqual(
group_configuration['usage'][0]['validation'],
{
'message': u'This content experiment has issues that affect content visibility.',
'type': 'error'
}
)
self.assertEqual(expected_result.to_json(), group_configuration['usage'][0]['validation'])
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_warning_message_present(self, mocked_validation_messages):
def test_error_message_present(self):
"""
Tests if validation message is present.
Tests if validation message is present (error case).
"""
self._add_user_partitions()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
mocked_message = StudioValidationMessage(StudioValidationMessage.ERROR, u"Validation message")
expected_result = StudioValidationMessage(
StudioValidationMessage.ERROR, u"This content experiment has issues that affect content visibility."
)
self.verify_validation_add_usage_info(expected_result, mocked_message) # pylint: disable=no-value-for-parameter
mocked_validation_messages.return_value = [
ValidationMessage(
split_test,
u"Validation message",
ValidationMessageType.warning
)
]
group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
self.assertEqual(
group_configuration['usage'][0]['validation'],
{
'message': u'This content experiment has issues that affect content visibility.',
'type': 'warning'
}
def test_warning_message_present(self):
"""
Tests if validation message is present (warning case).
"""
mocked_message = StudioValidationMessage(StudioValidationMessage.WARNING, u"Validation message")
expected_result = StudioValidationMessage(
StudioValidationMessage.WARNING, u"This content experiment has issues that affect content visibility."
)
self.verify_validation_add_usage_info(expected_result, mocked_message) # pylint: disable=no-value-for-parameter
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_update_usage_info(self, mocked_validation_messages):
@patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test')
def verify_validation_update_usage_info(self, expected_result, mocked_message, mocked_validation_messages):
"""
Tests if validation message is present when updating usage info.
Helper method for testing validation information present after update_usage_info.
"""
self._add_user_partitions()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
mocked_validation_messages.return_value = [
ValidationMessage(
split_test,
u"Validation message",
ValidationMessageType.warning
)
]
group_configuration = GroupConfiguration.update_usage_info(self.store, self.course, self.course.user_partitions[0])
validation = StudioValidation(split_test.location)
if mocked_message is not None:
validation.add(mocked_message)
mocked_validation_messages.return_value = validation
group_configuration = GroupConfiguration.update_usage_info(
self.store, self.course, self.course.user_partitions[0]
)
self.assertEqual(
group_configuration['usage'][0]['validation'],
{
'message': u'This content experiment has issues that affect content visibility.',
'type': 'warning'
}
expected_result.to_json() if expected_result is not None else None,
group_configuration['usage'][0]['validation']
)
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_update_usage_info_no_message(self, mocked_validation_messages):
def test_update_usage_info(self):
"""
Tests if validation message is present when updating usage info.
"""
mocked_message = StudioValidationMessage(StudioValidationMessage.WARNING, u"Validation message")
expected_result = StudioValidationMessage(
StudioValidationMessage.WARNING, u"This content experiment has issues that affect content visibility."
)
# pylint: disable=no-value-for-parameter
self.verify_validation_update_usage_info(expected_result, mocked_message)
def test_update_usage_info_no_message(self):
"""
Tests if validation message is not present when updating usage info.
"""
self._add_user_partitions()
self._create_content_experiment(cid=0, name_suffix='0')
mocked_validation_messages.return_value = []
group_configuration = GroupConfiguration.update_usage_info(self.store, self.course, self.course.user_partitions[0])
self.assertEqual(group_configuration['usage'][0]['validation'], None)
self.verify_validation_update_usage_info(None, None) # pylint: disable=no-value-for-parameter
......@@ -130,8 +130,10 @@ class GetItemTest(ItemTest):
root_usage_key = self._create_vertical()
html, __ = self._get_container_preview(root_usage_key)
# Verify that the Studio wrapper is not added
self.assertNotIn('wrapper-xblock', html)
# XBlock messages are added by the Studio wrapper.
self.assertIn('wrapper-xblock-message', html)
# Make sure that "wrapper-xblock" does not appear by itself (without -message at end).
self.assertNotRegexpMatches(html, r'wrapper-xblock[^-]+')
# Verify that the header and article tags are still added
self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
......
......@@ -215,14 +215,8 @@ with open(CONFIG_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
AUTH_TOKENS = json.load(auth_file)
############### XBlock filesystem field config ##########
if 'XBLOCK_FS_STORAGE_BUCKET' in ENV_TOKENS:
DJFS = {
'type' : 's3fs',
'bucket' : ENV_TOKENS.get('XBLOCK_FS_STORAGE_BUCKET', None),
'prefix' : ENV_TOKENS.get('XBLOCK_FS_STORAGE_PREFIX', '/xblock-storage/'),
'aws_access_key_id' : AUTH_TOKENS.get('AWS_ACCESS_KEY_ID', None),
'aws_secret_access_key' : AUTH_TOKENS.get('AWS_SECRET_ACCESS_KEY', None)
}
if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None:
DJFS = AUTH_TOKENS['DJFS']
EMAIL_HOST_USER = AUTH_TOKENS.get('EMAIL_HOST_USER', EMAIL_HOST_USER)
EMAIL_HOST_PASSWORD = AUTH_TOKENS.get('EMAIL_HOST_PASSWORD', EMAIL_HOST_PASSWORD)
......
......@@ -89,6 +89,13 @@
"url": "http://localhost:18060/",
"username": "lms"
},
"DJFS": {
"type": "s3fs",
"bucket": "test",
"prefix": "test",
"aws_access_key_id": "test",
"aws_secret_access_key": "test"
},
"SECRET_KEY": "",
"XQUEUE_INTERFACE": {
"basic_auth": [
......
......@@ -292,7 +292,6 @@ MANAGERS = ADMINS
# Static content
STATIC_URL = '/static/' + git.revision + "/"
ADMIN_MEDIA_PREFIX = '/static/admin/'
STATIC_ROOT = ENV_ROOT / "staticfiles" / git.revision
STATICFILES_DIRS = [
......
......@@ -213,6 +213,7 @@ define([
"js/spec/models/component_template_spec",
"js/spec/models/explicit_url_spec",
"js/spec/models/xblock_info_spec",
"js/spec/models/xblock_validation_spec",
"js/spec/utils/drag_and_drop_spec",
"js/spec/utils/handle_iframe_binding_spec",
......@@ -228,6 +229,7 @@ define([
"js/spec/views/xblock_spec",
"js/spec/views/xblock_editor_spec",
"js/spec/views/xblock_string_field_editor_spec",
"js/spec/views/xblock_validation_spec",
"js/spec/views/utils/view_utils_spec",
......
cms/static/img/menu.png

95 Bytes

......@@ -28,6 +28,7 @@ define([
autoUpload: false,
add: function(e, data) {
CourseImport.clearImportDisplay();
CourseImport.okayToNavigateAway = false;
submitBtn.unbind('click');
file = data.files[0];
if (file.name.match(/tar\.gz$/)) {
......@@ -97,7 +98,9 @@ define([
},
start: function(event) {
window.onbeforeunload = function() {
return gettext('Your import is in progress; navigating away will abort it.');
if (!CourseImport.okayToNavigateAway) {
return "${_('Your import is in progress; navigating away will abort it.')}";
}
};
},
sequentialUploads: true,
......
define(["backbone", "gettext", "underscore"], function (Backbone, gettext, _) {
/**
* Model for xblock validation messages as displayed in Studio.
*/
var XBlockValidationModel = Backbone.Model.extend({
defaults: {
summary: {},
messages: [],
empty: true,
xblock_id: null
},
WARNING : "warning",
ERROR: "error",
NOT_CONFIGURED: "not-configured",
parse: function(response) {
if (!response.empty) {
var summary = "summary" in response ? response.summary : {};
var messages = "messages" in response ? response.messages : [];
if (!(_.has(summary, "text")) || !summary.text) {
summary.text = gettext("This component has validation issues.");
}
if (!(_.has(summary, "type")) || !summary.type) {
summary.type = this.WARNING;
// Possible types are ERROR, WARNING, and NOT_CONFIGURED. NOT_CONFIGURED is treated as a warning.
_.find(messages, function (message) {
if (message.type === this.ERROR) {
summary.type = this.ERROR;
return true;
}
return false;
}, this);
}
response.summary = summary;
if (response.showSummaryOnly) {
messages = [];
}
response.messages = messages;
}
return response;
}
});
return XBlockValidationModel;
});
define(['js/models/xblock_validation'],
function(XBlockValidationModel) {
var verifyModel;
verifyModel = function(model, expected_empty, expected_summary, expected_messages, expected_xblock_id) {
expect(model.get("empty")).toBe(expected_empty);
expect(model.get("summary")).toEqual(expected_summary);
expect(model.get("messages")).toEqual(expected_messages);
expect(model.get("xblock_id")).toBe(expected_xblock_id);
};
describe('XBlockValidationModel', function() {
it('handles empty variable', function() {
verifyModel(new XBlockValidationModel({parse: true}), true, {}, [], null);
verifyModel(new XBlockValidationModel({"empty": true}, {parse: true}), true, {}, [], null);
// It is assumed that the "empty" state on the JSON object passed in is correct
// (no attempt is made to correct other variables based on empty==true).
verifyModel(
new XBlockValidationModel(
{"empty": true, "messages": [{"text": "Bad JSON case"}], "xblock_id": "id"},
{parse: true}
),
true,
{},
[{"text": "Bad JSON case"}], "id"
);
});
it('creates a summary if not defined', function() {
// Single warning message.
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "warning"},
[],
"id"
);
// Two messages that compute to a "warning" state in the summary.
verifyModel(
new XBlockValidationModel({
"empty": false,
"messages": [{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "warning"},
[{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"id"
);
// Two messages, with one of them "error", resulting in an "error" state in the summary.
verifyModel(
new XBlockValidationModel({
"empty": false,
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
it('respects summary properties that are defined', function() {
// Summary already present (both text and type), no messages.
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary", "type": "custom type"}
}, {parse: true}),
false,
{"text": "my summary", "type": "custom type"},
[],
"id"
);
// Summary text present, but not type (will get default value of warning).
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"}
}, {parse: true}),
false,
{"text": "my summary", "type": "warning"},
[],
"id"
);
// Summary type present, but not text.
verifyModel(
new XBlockValidationModel({
"empty": false,
"summary": {"type": "custom type"},
"messages": [{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "custom type"},
[{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"id"
);
// Summary text present, type will be computed as error.
verifyModel(
new XBlockValidationModel({
"empty": false,
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
it('clears messages if showSummaryOnly is true', function() {
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"showSummaryOnly": true
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[],
"id"
);
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"showSummaryOnly": false
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
});
}
);
......@@ -215,7 +215,7 @@ define([
'label': 'label1',
'url': 'url1',
'validation': {
'message': "Warning message",
'text': "Warning message",
'type': 'warning'
}
}
......@@ -233,7 +233,7 @@ define([
'label': 'label1',
'url': 'url1',
'validation': {
'message': "Error message",
'text': "Error message",
'type': 'error'
}
}
......
......@@ -88,6 +88,17 @@ define(["jquery", "js/common_helpers/ajax_helpers", "js/common_helpers/template_
expect(unitOutlineView.$('.list-units')).toExist();
});
it('highlights the current unit', function() {
createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
$('.outline-unit').each(function(i) {
if ($(this).data('locator') === model.get('id')) {
expect($(this)).toHaveClass('is-current');
} else {
expect($(this)).not.toHaveClass('is-current');
}
});
});
it('can add a unit', function() {
var redirectSpy;
createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
......
......@@ -102,6 +102,14 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/xblock", "
]);
expect(promise.isRejected()).toBe(true);
});
it('Triggers an event to the runtime when a notification-action-button is clicked', function () {
var notifySpy = spyOn(xblockView, "notifyRuntime").andCallThrough();
postXBlockRequest(AjaxHelpers.requests(this), []);
xblockView.$el.find(".notification-action-button").click();
expect(notifySpy).toHaveBeenCalledWith("add-missing-groups", model.get("id"));
})
});
});
});
define(['jquery', 'js/models/xblock_validation', 'js/views/xblock_validation', 'js/common_helpers/template_helpers'],
function($, XBlockValidationModel, XBlockValidationView, TemplateHelpers) {
beforeEach(function () {
TemplateHelpers.installTemplate('xblock-validation-messages');
});
describe('XBlockValidationView helper methods', function() {
var model, view;
beforeEach(function () {
model = new XBlockValidationModel({parse: true});
view = new XBlockValidationView({model: model});
view.render();
});
it('has a getIcon method', function() {
var getIcon = view.getIcon.bind(view);
expect(getIcon(model.WARNING)).toBe('icon-warning-sign');
expect(getIcon(model.NOT_CONFIGURED)).toBe('icon-warning-sign');
expect(getIcon(model.ERROR)).toBe('icon-exclamation-sign');
expect(getIcon("unknown")).toBeNull();
});
it('has a getDisplayName method', function() {
var getDisplayName = view.getDisplayName.bind(view);
expect(getDisplayName(model.WARNING)).toBe("Warning");
expect(getDisplayName(model.NOT_CONFIGURED)).toBe("Warning");
expect(getDisplayName(model.ERROR)).toBe("Error");
expect(getDisplayName("unknown")).toBeNull();
});
it('can add additional classes', function() {
var noContainerContent = "no-container-content", notConfiguredModel, nonRootView, rootView;
expect(view.getAdditionalClasses()).toBe("");
expect(view.$('.validation')).not.toHaveClass(noContainerContent);
notConfiguredModel = new XBlockValidationModel({
"empty": false, "summary": {"text": "Not configured", "type": model.NOT_CONFIGURED},
"xblock_id": "id"
},
{parse: true}
);
nonRootView = new XBlockValidationView({model: notConfiguredModel});
nonRootView.render();
expect(nonRootView.getAdditionalClasses()).toBe("");
expect(view.$('.validation')).not.toHaveClass(noContainerContent);
rootView = new XBlockValidationView({model: notConfiguredModel, root: true});
rootView.render();
expect(rootView.getAdditionalClasses()).toBe(noContainerContent);
expect(rootView.$('.validation')).toHaveClass(noContainerContent);
});
});
describe('XBlockValidationView rendering', function() {
var model, view;
beforeEach(function () {
model = new XBlockValidationModel({
"empty": false,
"summary": {
"text": "Summary message", "type": "error",
"action_label": "Summary Action", "action_class": "edit-button"
},
"messages": [
{
"text": "First message", "type": "warning",
"action_label": "First Message Action", "action_runtime_event": "fix-up"
},
{"text": "Second message", "type": "error"}
],
"xblock_id": "id"
});
view = new XBlockValidationView({model: model});
view.render();
});
it('renders summary and detailed messages types', function() {
var details;
expect(view.$('.xblock-message')).toHaveClass("has-errors");
details = view.$('.xblock-message-item');
expect(details.length).toBe(2);
expect(details[0]).toHaveClass("warning");
expect(details[1]).toHaveClass("error");
});
it('renders summary and detailed messages text', function() {
var details;
expect(view.$('.xblock-message').text()).toContain("Summary message");
details = view.$('.xblock-message-item');
expect(details.length).toBe(2);
expect($(details[0]).text()).toContain("Warning");
expect($(details[0]).text()).toContain("First message");
expect($(details[1]).text()).toContain("Error");
expect($(details[1]).text()).toContain("Second message");
});
it('renders action info', function() {
expect(view.$('a.edit-button .action-button-text').text()).toContain("Summary Action");
expect(view.$('a.notification-action-button .action-button-text').text()).
toContain("First Message Action");
expect(view.$('a.notification-action-button').data("notification-action")).toBe("fix-up");
});
it('renders a summary only', function() {
var summaryOnlyModel = new XBlockValidationModel({
"empty": false,
"summary": {"text": "Summary message", "type": "warning"},
"xblock_id": "id"
}), summaryOnlyView, details;
summaryOnlyView = new XBlockValidationView({model: summaryOnlyModel});
summaryOnlyView.render();
expect(summaryOnlyView.$('.xblock-message')).toHaveClass("has-warnings");
expect(view.$('.xblock-message').text()).toContain("Summary message");
details = summaryOnlyView.$('.xblock-message-item');
expect(details.length).toBe(0);
});
});
}
);
......@@ -35,15 +35,8 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
return !this.model.isVertical();
},
createChildView: function(xblockInfo, parentInfo, parentView) {
return new CourseOutlineView({
model: xblockInfo,
parentInfo: parentInfo,
initialState: this.initialState,
expandedLocators: this.expandedLocators,
template: this.template,
parentView: parentView || this
});
getChildViewClass: function() {
return CourseOutlineView;
},
/**
......@@ -112,7 +105,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
});
// Fetch the full xblock info for the section and then create a view for it
sectionInfo.fetch().done(function() {
sectionView = self.createChildView(sectionInfo, self.model, self);
sectionView = self.createChildView(sectionInfo, self.model, {parentView: self});
sectionView.initialState = initialState;
sectionView.expandedLocators = self.expandedLocators;
sectionView.render();
......
......@@ -49,6 +49,7 @@ define(
*/
var getStatus = function (url, timeout, stage) {
var currentStage = stage || 0;
if (currentStage > 1) { CourseImport.okayToNavigateAway = true; }
if (CourseImport.stopGetStatus) { return ;}
if (currentStage === 4) {
......@@ -87,6 +88,10 @@ define(
* progress.
*/
stopGetStatus: false,
/**
* Whether its fine to navigate away while import is in progress
*/
okayToNavigateAway: false,
/**
* Update DOM to set all stages as not-started (for retrying an upload that
......
......@@ -13,6 +13,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
events: {
"click .edit-button": "editXBlock",
"click .duplicate-button": "duplicateXBlock",
"click .delete-button": "deleteXBlock"
},
options: {
collapsedClass: 'is-collapsed'
},
......@@ -81,12 +87,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Hide both blocks until we know which one to show
xblockView.$el.addClass(hiddenCss);
if (!options || !options.refresh) {
// Add actions to any top level buttons, e.g. "Edit" of the container itself.
// Do not add the actions on "refresh" though, as the handlers are already registered.
self.addButtonActions(this.$el);
}
// Render the xblock
xblockView.render({
done: function() {
......@@ -119,7 +119,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
},
onXBlockRefresh: function(xblockView) {
this.addButtonActions(xblockView.$el);
this.xblockView.refresh();
// Update publish and last modified information from the server.
this.model.fetch();
......@@ -137,25 +136,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
},
addButtonActions: function(element) {
var self = this;
element.find('.edit-button').click(function(event) {
event.preventDefault();
self.editComponent(self.findXBlockElement(event.target));
});
element.find('.duplicate-button').click(function(event) {
event.preventDefault();
self.duplicateComponent(self.findXBlockElement(event.target));
});
element.find('.delete-button').click(function(event) {
event.preventDefault();
self.deleteComponent(self.findXBlockElement(event.target));
});
},
editComponent: function(xblockElement) {
var self = this,
editXBlock: function(event) {
var xblockElement = this.findXBlockElement(event.target),
self = this,
modal = new EditXBlockModal({ });
event.preventDefault();
modal.edit(xblockElement, this.model, {
refresh: function() {
self.refreshXBlock(xblockElement);
......@@ -163,6 +149,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
},
duplicateXBlock: function(event) {
event.preventDefault();
this.duplicateComponent(this.findXBlockElement(event.target));
},
deleteXBlock: function(event) {
event.preventDefault();
this.deleteComponent(this.findXBlockElement(event.target));
},
createPlaceholderElement: function() {
return $("<div/>", { class: "studio-xblock-wrapper" });
},
......
......@@ -3,9 +3,8 @@
* the ancestors of the unit along with its direct siblings. It also has a single "New Unit"
* button to allow a new sibling unit to be added.
*/
define(['js/views/xblock_outline'],
function(XBlockOutlineView) {
define(['underscore', 'js/views/xblock_outline', 'js/views/unit_outline_child'],
function(_, XBlockOutlineView, UnitOutlineChildView) {
var UnitOutlineView = XBlockOutlineView.extend({
// takes XBlockInfo as a model
......@@ -29,7 +28,11 @@ define(['js/views/xblock_outline'],
// i.e. subsection and then section.
for (i=ancestors.length - 1; i >= 0; i--) {
ancestor = ancestors[i];
ancestorView = this.createChildView(ancestor, previousAncestor, ancestorView);
ancestorView = this.createChildView(
ancestor,
previousAncestor,
{parentView: ancestorView, currentUnitId: this.model.get('id')}
);
ancestorView.render();
listElement.append(ancestorView.$el);
previousAncestor = ancestor;
......@@ -37,6 +40,17 @@ define(['js/views/xblock_outline'],
}
}
return ancestorView;
},
getChildViewClass: function() {
return UnitOutlineChildView;
},
getTemplateContext: function() {
return _.extend(
XBlockOutlineView.prototype.getTemplateContext.call(this),
{currentUnitId: this.model.get('id')}
);
}
});
......
/**
* The UnitOutlineChildView is used to render each Section,
* Subsection, and Unit within the Unit Outline component on the unit
* page.
*/
define(['underscore', 'js/views/xblock_outline'],
function(_, XBlockOutlineView) {
var UnitOutlineChildView = XBlockOutlineView.extend({
initialize: function() {
XBlockOutlineView.prototype.initialize.call(this);
this.currentUnitId = this.options.currentUnitId;
},
getTemplateContext: function() {
return _.extend(
XBlockOutlineView.prototype.getTemplateContext.call(this),
{currentUnitId: this.currentUnitId}
);
},
getChildViewClass: function() {
return UnitOutlineChildView;
},
createChildView: function(childInfo, parentInfo, options) {
options = _.isUndefined(options) ? {} : options;
return XBlockOutlineView.prototype.createChildView.call(
this, childInfo, parentInfo, _.extend(options, {currentUnitId: this.currentUnitId})
);
}
});
return UnitOutlineChildView;
}); // end define()
......@@ -4,6 +4,10 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
var XBlockView = BaseView.extend({
// takes XBlockInfo as a model
events: {
"click .notification-action-button": "fireNotificationActionEvent"
},
initialize: function() {
BaseView.prototype.initialize.call(this);
this.view = this.options.view;
......@@ -195,6 +199,14 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
}
// Return an already resolved promise for synchronous updates
return $.Deferred().resolve().promise();
},
fireNotificationActionEvent: function(event) {
var eventName = $(event.currentTarget).data("notification-action");
if (eventName) {
event.preventDefault();
this.notifyRuntime(eventName, this.model.get("id"));
}
}
});
......
......@@ -54,6 +54,15 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
renderTemplate: function() {
var html = this.template(this.getTemplateContext());
if (this.parentInfo) {
this.setElement($(html));
} else {
this.$el.html(html);
}
},
getTemplateContext: function() {
var xblockInfo = this.model,
childInfo = xblockInfo.get('child_info'),
parentInfo = this.parentInfo,
......@@ -62,7 +71,6 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
parentType = parentInfo ? XBlockViewUtils.getXBlockType(parentInfo.get('category')) : null,
addChildName = null,
defaultNewChildName = null,
html,
isCollapsed = this.shouldRenderChildren() && !this.shouldExpandChildren();
if (childInfo) {
addChildName = interpolate(gettext('New %(component_type)s'), {
......@@ -70,7 +78,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
}, true);
defaultNewChildName = childInfo.display_name;
}
html = this.template({
return {
xblockInfo: xblockInfo,
visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(xblockInfo.get('visibility_state')),
typeListClass: XBlockViewUtils.getXBlockListTypeClass(xblockType),
......@@ -86,20 +94,15 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
includesChildren: this.shouldRenderChildren(),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffOnlyMessage: this.model.get('staff_only_message')
});
if (this.parentInfo) {
this.setElement($(html));
} else {
this.$el.html(html);
}
};
},
renderChildren: function() {
var self = this,
xblockInfo = this.model;
if (xblockInfo.get('child_info')) {
_.each(this.model.get('child_info').children, function(child) {
var childOutlineView = self.createChildView(child, xblockInfo);
parentInfo = this.model;
if (parentInfo.get('child_info')) {
_.each(this.model.get('child_info').children, function(childInfo) {
var childOutlineView = self.createChildView(childInfo, parentInfo);
childOutlineView.render();
self.addChildView(childOutlineView);
});
......@@ -182,15 +185,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
return true;
},
createChildView: function(xblockInfo, parentInfo, parentView) {
return new XBlockOutlineView({
model: xblockInfo,
getChildViewClass: function() {
return XBlockOutlineView;
},
createChildView: function(childInfo, parentInfo, options) {
var viewClass = this.getChildViewClass();
return new viewClass(_.extend({
model: childInfo,
parentInfo: parentInfo,
parentView: this,
initialState: this.initialState,
expandedLocators: this.expandedLocators,
template: this.template,
parentView: parentView || this
});
template: this.template
}, options));
},
onSync: function(event) {
......@@ -273,7 +281,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
handleAddEvent: function(event) {
var self = this,
target = $(event.target),
target = $(event.currentTarget),
category = target.data('category');
event.preventDefault();
XBlockViewUtils.addXBlock(target).done(function(locator) {
......
define(["jquery", "underscore", "js/views/baseview", "gettext"],
function ($, _, BaseView, gettext) {
/**
* View for xblock validation messages as displayed in Studio.
*/
var XBlockValidationView = BaseView.extend({
// Takes XBlockValidationModel as a model
initialize: function(options) {
BaseView.prototype.initialize.call(this);
this.template = this.loadTemplate('xblock-validation-messages');
this.root = options.root;
},
render: function () {
this.$el.html(this.template({
validation: this.model,
additionalClasses: this.getAdditionalClasses(),
getIcon: this.getIcon.bind(this),
getDisplayName: this.getDisplayName.bind(this)
}));
return this;
},
/**
* Returns the icon css class based on the message type.
* @param messageType
* @returns string representation of css class that will render the correct icon, or null if unknown type
*/
getIcon: function (messageType) {
if (messageType === this.model.ERROR) {
return 'icon-exclamation-sign';
}
else if (messageType === this.model.WARNING || messageType === this.model.NOT_CONFIGURED) {
return 'icon-warning-sign';
}
return null;
},
/**
* Returns a display name for a message (useful for screen readers), based on the message type.
* @param messageType
* @returns string display name (translated)
*/
getDisplayName: function (messageType) {
if (messageType === this.model.WARNING || messageType === this.model.NOT_CONFIGURED) {
// Translators: This message will be added to the front of messages of type warning,
// e.g. "Warning: this component has not been configured yet".
return gettext("Warning");
}
else if (messageType === this.model.ERROR) {
// Translators: This message will be added to the front of messages of type error,
// e.g. "Error: required field is missing".
return gettext("Error");
}
return null;
},
/**
* Returns additional css classes that can be added to HTML containing the validation messages.
* Useful for rendering NOT_CONFIGURED in a special way.
*
* @returns string of additional css classes (or empty string)
*/
getAdditionalClasses: function () {
if (this.root && this.model.get("summary").type === this.model.NOT_CONFIGURED &&
this.model.get("messages").length === 0) {
return "no-container-content";
}
return "";
}
});
return XBlockValidationView;
});
......@@ -569,11 +569,6 @@ hr.divide {
}
.window {
// border-radius: 3px;
// box-shadow: 0 1px 1px $shadow-l1;
// margin-bottom: $baseline;
// border: 1px solid $gray-l2;
// background: $white;
.window-contents {
padding: 20px;
......@@ -615,162 +610,6 @@ hr.divide {
}
}
// ====================
// system notifications
.toast-notification {
@include transition(all $tmg-f2 linear 0s);
@include linear-gradient(top, rgba(255, 255, 255, .1), rgba(255, 255, 255, 0));
@extend %t-copy-sub1;
display: none;
position: fixed;
top: 20px;
right: 20px;
z-index: 99999;
max-width: 350px;
padding: 15px 20px 17px;
border-radius: 3px;
border: 1px solid #333;
background-color: rgba(30, 30, 30, .92);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 1px 0 rgba(255, 255, 255, .1) inset;
text-align: center;
color: #fff;
p, span {
color: #fff;
}
strong {
@extend %t-copy-base;
@extend %t-strong;
display: block;
margin-bottom: 10px;
text-align: center;
}
.close-button {
@extend %t-action1;
@extend %t-strong;
position: absolute;
top: 0;
right: 0;
width: 27px;
height: 27px;
line-height: 25px;
color: #aaa;
text-align: center;
.close-icon {
@extend %t-action2;
@extend %t-strong;
}
}
.action-button {
@include blue-button;
@include box-sizing(border-box);
@extend %t-action4;
width: 100%;
margin-top: 10px;
text-align: center;
}
}
.waiting {
position: relative;
&:before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 999998;
width: 100%;
height: 100%;
border-radius: inherit;
background: rgba(255, 255, 255, .9);
}
&:after {
@extend .spinner-icon;
content: '';
display: block;
position: absolute;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
z-index: 999999;
}
}
.waiting-inline {
&:after {
content: '';
@extend .spinner-icon;
}
}
.new-button {
@include green-button;
@extend %t-action4;
padding: 8px 20px 10px;
text-align: center;
&.big {
display: block;
}
.icon-plus {
margin-top: -2px;
line-height: 0;
}
}
.view-button {
@include blue-button;
@extend %t-action4;
text-align: center;
&.big {
display: block;
}
.icon-eye-open {
@extend %t-action2;
display: inline-block;
vertical-align: middle;
margin-right: 8px;
margin-top: -3px;
line-height: 0;
}
}
.edit-button.standard,
.delete-button.standard,
.duplicate-button.standard {
@include white-button;
@extend %t-regular;
@extend %t-action4;
float: left;
padding: 3px 10px 4px;
margin-left: 7px;
.edit-icon,
.delete-icon,
.duplicate-icon{
margin-right: 4px;
}
}
.delete-button.standard {
&:hover {
background-color: tint($orange, 75%);
}
}
.tooltip {
@include transition(opacity $tmg-f3 ease-out 0s);
@include font-size(12);
......@@ -818,19 +657,6 @@ hr.divide {
@extend %ui-disabled;
}
.non-list {
list-style: none;
margin: 0;
padding: 0;
}
.wrap {
text-wrap: wrap;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
word-wrap: break-word;
}
// ui - semantic + visual divider
hr.divider {
@extend %cont-text-sr;
......@@ -922,13 +748,3 @@ body.js {
}
}
}
// ====================
// works in progress & testing
body.hide-wip {
.wip-box {
display: none;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment