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
1
AUTHORS
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 970 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1018 B |
|
Before Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 144 B |
|
Before Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 970 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 973 B |
|
Before Width: | Height: | Size: 1022 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 581 B After Width: | Height: | Size: 581 B |
|
Before Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 960 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 95 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1012 B |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 951 B |
|
Before Width: | Height: | Size: 952 B |
|
Before Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 983 B |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 952 B |
@@ -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,
|
||||
|
||||
46
cms/static/js/models/xblock_validation.js
Normal 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;
|
||||
});
|
||||
152
cms/static/js/spec/models/xblock_validation_spec.js
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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"));
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
132
cms/static/js/spec/views/xblock_validation_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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')}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
34
cms/static/js/views/unit_outline_child.js
Normal 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()
|
||||
@@ -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) {
|
||||
|
||||
76
cms/static/js/views/xblock_validation.js
Normal 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;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||