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
This commit is contained in:
Will Daly
2014-11-10 15:05:37 -05:00
1907 changed files with 25784 additions and 74080 deletions

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# 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])

View File

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

View File

@@ -56,6 +56,8 @@ from xmodule.contentstore.content import StaticContent
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
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ContentStoreTestCase(CourseTestCase):
@@ -69,7 +71,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
Tests which legitimately need to import a course
"""
def test_no_static_link_rewrites_on_import(self):
course_items = import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
course_items = import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course = course_items[0]
handouts_usage_key = course.id.make_usage_key('course_info', 'handouts')
@@ -81,7 +83,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
self.assertIn('/static/', handouts.data)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['toy'])
err_cnt = perform_xlint(TEST_DATA_DIR, ['toy'])
self.assertGreater(err_cnt, 0)
def test_about_overrides(self):
@@ -90,7 +92,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
course_items = import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
course_items = import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course_key = course_items[0].id
effort = self.store.get_item(course_key.make_usage_key('about', 'effort'))
self.assertEqual(effort.data, '6 hours')
@@ -105,7 +107,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
'''
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store, verbose=True)
course = self.store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
@@ -153,7 +155,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
Test that course info updates are imported and exported with all content fields ('data', 'items')
"""
content_store = contentstore()
data_dir = "common/test/data/"
data_dir = TEST_DATA_DIR
courses = import_from_xml(
self.store, self.user.id, data_dir, ['course_info_updates'],
static_content_store=content_store, verbose=True,
@@ -166,7 +168,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
self.assertIsNotNone(course_updates)
# check that course which is imported has files 'updates.html' and 'updates.items.json'
filesystem = OSFS(data_dir + 'course_info_updates/info')
filesystem = OSFS(data_dir + '/course_info_updates/info')
self.assertTrue(filesystem.exists('updates.html'))
self.assertTrue(filesystem.exists('updates.items.json'))
@@ -204,7 +206,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
def test_rewrite_nonportable_links_on_import(self):
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)
# first check a static asset link
course_key = SlashSeparatedCourseKey('edX', 'toy', 'run')
@@ -250,6 +252,14 @@ class ImportRequiredTestCases(ContentStoreTestCase):
# check for about content
self.verify_content_existence(self.store, root_dir, course_id, 'about', 'about', '.html')
# assert that there is an html and video directory in drafts:
draft_dir = OSFS(root_dir / 'test_export/drafts')
self.assertTrue(draft_dir.exists('html'))
self.assertTrue(draft_dir.exists('video'))
# and assert that they contain the created modules
self.assertIn(self.DRAFT_HTML + ".xml", draft_dir.listdir('html'))
self.assertIn(self.DRAFT_VIDEO + ".xml", draft_dir.listdir('video'))
# check for grading_policy.json
filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall')
self.assertTrue(filesystem.exists('grading_policy.json'))
@@ -302,6 +312,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
"""Verifies all temporary attributes added during export are removed"""
self.assertNotIn('index_in_children_list', attributes)
self.assertNotIn('parent_sequential_url', attributes)
self.assertNotIn('parent_url', attributes)
vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL))
verify_export_attrs_removed(vertical.xml_attributes)
@@ -314,7 +325,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
def test_export_course_with_metadata_only_video(self):
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
# create a new video module and add it as a child to a vertical
@@ -343,7 +354,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
"""
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['word_cloud'])
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['word_cloud'])
course_id = SlashSeparatedCourseKey('HarvardX', 'ER22x', '2013_Spring')
verticals = self.store.get_items(course_id, qualifiers={'category': 'vertical'})
@@ -370,7 +381,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
"""
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
verticals = self.store.get_items(course_id, qualifiers={'category': 'vertical'})
@@ -401,7 +412,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
"""
content_store = contentstore()
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
@@ -423,7 +434,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
def test_export_course_without_content_store(self):
# Create toy course
course_items = import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
course_items = import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course_id = course_items[0].id
root_dir = path(mkdtemp_clean())
@@ -844,15 +855,14 @@ class MiscCourseTests(ContentStoreTestCase):
self.assertContains(resp, unicode(asset_key))
def test_prefetch_children(self):
# make sure we haven't done too many round trips to DB
# note we say 4 round trips here for:
# make sure we haven't done too many round trips to DB:
# 1) the course,
# 2 & 3) for the chapters and sequentials
# Because we're querying from the top of the tree, we cache information needed for inheritance,
# so we don't need to make an extra query to compute it.
# set the branch to 'publish' in order to prevent extra lookups of draft versions
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, self.course.id):
with check_mongo_calls(3, 0):
with check_mongo_calls(3):
course = self.store.get_course(self.course.id, depth=2)
# make sure we pre-fetched a known sequential which should be at depth=2
@@ -864,7 +874,7 @@ class MiscCourseTests(ContentStoreTestCase):
# Now, test with the branch set to draft. No extra round trips b/c it doesn't go deep enough to get
# beyond direct only categories
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
with check_mongo_calls(3, 0):
with check_mongo_calls(3):
self.store.get_course(self.course.id, depth=2)
def _check_verticals(self, locations):
@@ -1212,7 +1222,7 @@ class ContentStoreTest(ContentStoreTestCase):
)
self.assertEqual(resp.status_code, 200)
course_items = import_from_xml(self.store, self.user.id, 'common/test/data/', ['simple'])
course_items = import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['simple'])
course_key = course_items[0].id
resp = self._show_course_overview(course_key)
@@ -1259,7 +1269,7 @@ class ContentStoreTest(ContentStoreTestCase):
target_course_id = _get_course_id(self.course_data)
_create_course(self, target_course_id, self.course_data)
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], target_course_id=target_course_id)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_course_id=target_course_id)
modules = self.store.get_items(target_course_id)
@@ -1294,7 +1304,7 @@ class ContentStoreTest(ContentStoreTestCase):
course_module.save()
# Import a course with wiki_slug == location.course
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], target_course_id=target_course_id)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_course_id=target_course_id)
course_module = self.store.get_course(target_course_id)
self.assertEquals(course_module.wiki_slug, 'toy')
@@ -1309,17 +1319,17 @@ class ContentStoreTest(ContentStoreTestCase):
_create_course(self, target_course_id, course_data)
# Import a course with wiki_slug == location.course
import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], target_course_id=target_course_id)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_course_id=target_course_id)
course_module = self.store.get_course(target_course_id)
self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring')
# Now try importing a course with wiki_slug == '{0}.{1}.{2}'.format(location.org, location.course, location.run)
import_from_xml(self.store, self.user.id, 'common/test/data/', ['two_toys'], target_course_id=target_course_id)
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['two_toys'], target_course_id=target_course_id)
course_module = self.store.get_course(target_course_id)
self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring')
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(self.store, self.user.id, 'common/test/data/', ['simple'])
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['simple'])
did_load_item = False
try:
course_key = SlashSeparatedCourseKey('edX', 'simple', 'problem')
@@ -1341,7 +1351,7 @@ class ContentStoreTest(ContentStoreTestCase):
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
def test_metadata_inheritance(self):
course_items = import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'])
course_items = import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'])
course = course_items[0]
verticals = self.store.get_items(course.id, qualifiers={'category': 'vertical'})
@@ -1410,7 +1420,7 @@ class ContentStoreTest(ContentStoreTestCase):
courses = import_from_xml(
self.store,
self.user.id,
'common/test/data/',
TEST_DATA_DIR,
['conditional_and_poll'],
static_content_store=content_store
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,6 @@
"""
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

View File

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

View File

@@ -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
)
]
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'
}
)
validation = StudioValidation(split_test.location)
validation.add(mocked_message)
mocked_validation_messages.return_value = validation
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_warning_message_present(self, mocked_validation_messages):
group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
self.assertEqual(expected_result.to_json(), group_configuration['usage'][0]['validation'])
def test_error_message_present(self):
"""
Tests if validation message is present.
Tests if validation message is present (error case).
"""
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
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.validate_split_test')
def verify_validation_update_usage_info(self, expected_result, mocked_message, mocked_validation_messages):
"""
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.add_usage_info(self.course, self.store)[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(self, mocked_validation_messages):
def test_update_usage_info(self):
"""
Tests if validation message is present when updating 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])
self.assertEqual(
group_configuration['usage'][0]['validation'],
{
'message': u'This content experiment has issues that affect content visibility.',
'type': 'warning'
}
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)
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
def test_update_usage_info_no_message(self, mocked_validation_messages):
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

View File

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

View File

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

View File

@@ -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": [

View File

@@ -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 = [

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

View File

Before

Width:  |  Height:  |  Size: 581 B

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 B

View File

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

View File

@@ -0,0 +1,46 @@
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;
});

View File

@@ -0,0 +1,152 @@
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"
);
});
});
}
);

View File

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

View File

@@ -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'));

View File

@@ -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"));
})
});
});
});

View File

@@ -0,0 +1,132 @@
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);
});
});
}
);

View File

@@ -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();

View File

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

View File

@@ -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" });
},

View File

@@ -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')}
);
}
});

View File

@@ -0,0 +1,34 @@
/**
* 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()

View File

@@ -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"));
}
}
});

View File

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

View File

@@ -0,0 +1,76 @@
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;
});

View File

@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More