From 0f0d75fce85ca9ff33a356f39366207d7e87d94b Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 29 Jul 2014 15:24:21 -0400 Subject: [PATCH] Convert toc test to use mongo not xml ms Included adding a general create_toy_course and create_sample_course method for tests --- .../xmodule/xmodule/modulestore/exceptions.py | 3 + .../xmodule/modulestore/tests/django_utils.py | 318 +++++++++++++++++- .../courseware/tests/test_module_render.py | 38 +-- 3 files changed, 334 insertions(+), 25 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py index a660ff8d54..6f6db66873 100644 --- a/common/lib/xmodule/xmodule/modulestore/exceptions.py +++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py @@ -85,3 +85,6 @@ class InvalidBranchSetting(Exception): super(InvalidBranchSetting, self).__init__() self.expected_setting = expected_setting self.actual_setting = actual_setting + + def __unicode__(self, *args, **kwargs): + return u"Invalid branch: expected {} but got {}".format(self.expected_setting, self.actual_setting) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 071de046c3..ad43e48afc 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """ Modulestore configuration for test cases. """ @@ -8,6 +9,9 @@ from django.contrib.auth.models import User from xmodule.contentstore.django import _CONTENTSTORE from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore import ModuleStoreEnum +from collections import namedtuple +import datetime +import pytz def mixed_store_config(data_dir, mappings): @@ -28,6 +32,7 @@ def mixed_store_config(data_dir, mappings): """ draft_mongo_config = draft_mongo_store_config(data_dir) xml_config = xml_store_config(data_dir) + split_mongo = split_mongo_store_config(data_dir) store = { 'default': { @@ -36,7 +41,8 @@ def mixed_store_config(data_dir, mappings): 'mappings': mappings, 'stores': [ draft_mongo_config['default'], - xml_config['default'] + split_mongo['default'], + xml_config['default'], ] } } @@ -71,6 +77,33 @@ def draft_mongo_store_config(data_dir): return store +def split_mongo_store_config(data_dir): + """ + Defines split module store. + """ + modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'fs_root': data_dir, + 'render_template': 'edxmako.shortcuts.render_to_string', + # ??? does this & draft need xblock_mixins? + } + + store = { + 'default': { + 'NAME': 'draft', + 'ENGINE': 'xmodule.modulestore.split_mongo.split.SplitMongoModuleStore', + 'DOC_STORE_CONFIG': { + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore{0}'.format(uuid4().hex[:5]), + }, + 'OPTIONS': modulestore_options + } + } + + return store + + def xml_store_config(data_dir): """ Defines default module store using XMLModuleStore. @@ -89,6 +122,189 @@ def xml_store_config(data_dir): return store +# used to create course subtrees in ModuleStoreTestCase.create_test_course +# adds to self properties w/ the given block_id which hold the UsageKey for easy retrieval. +# fields is a dictionary of keys and values. sub_tree is a collection of BlockInfo +BlockInfo = namedtuple('BlockInfo', 'block_id, category, fields, sub_tree') +default_block_info_tree = [ + BlockInfo( + 'chapter_x', 'chapter', {}, [ + BlockInfo( + 'sequential_x1', 'sequential', {}, [ + BlockInfo( + 'vertical_x1a', 'vertical', {}, [ + BlockInfo('problem_x1a_1', 'problem', {}, []), + BlockInfo('problem_x1a_2', 'problem', {}, []), + BlockInfo('problem_x1a_3', 'problem', {}, []), + BlockInfo('html_x1a_1', 'html', {}, []), + ] + ) + ] + ) + ] + ), + BlockInfo( + 'chapter_y', 'chapter', {}, [ + BlockInfo( + 'sequential_y1', 'sequential', {}, [ + BlockInfo( + 'vertical_y1a', 'vertical', {}, [ + BlockInfo('problem_y1a_1', 'problem', {}, []), + BlockInfo('problem_y1a_2', 'problem', {}, []), + BlockInfo('problem_y1a_3', 'problem', {}, []), + ] + ) + ] + ) + ] + ) +] +# equivalent to toy course in xml +TOY_BLOCK_INFO_TREE = [ + BlockInfo( + 'Overview', "chapter", {"display_name" : "Overview"}, [ + BlockInfo( + "Toy_Videos", "videosequence", { + "xml_attributes": {"filename": ["", None]}, "display_name": "Toy Videos", "format": "Lecture Sequence" + }, [ + BlockInfo( + "secret:toylab", "html", { + "data": "Lab 2A: Superposition Experiment\n\n<<<<<<< Updated upstream\n

Isn't the toy course great?

\n\n

Let's add some markup that uses non-ascii characters.\nFor example, we should be able to write words like encyclopædia, or foreign words like français.\nLooking beyond latin-1, we should handle math symbols: πr² ≤ ∞.\nAnd it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π.\n

\n=======\n

Isn't the toy course great? — ≤

\n>>>>>>> Stashed changes\n", + "xml_attributes": { "filename" : [ "html/secret/toylab.xml", "html/secret/toylab.xml" ] }, + "display_name" : "Toy lab" + }, [] + ), + BlockInfo( + "toyjumpto", "html", { + "data" : "This is a link to another page and some Chinese 四節比分和七年前

Some more Chinese 四節比分和七年前

\n", + "xml_attributes": { "filename" : [ "html/toyjumpto.xml", "html/toyjumpto.xml" ] } + }, []), + BlockInfo( + "toyhtml", "html", { + "data" : "Sample", + "xml_attributes" : { "filename" : [ "html/toyhtml.xml", "html/toyhtml.xml" ] } + }, []), + BlockInfo( + "nonportable", "html", { + "data": "link\n", + "xml_attributes" : { "filename" : [ "html/nonportable.xml", "html/nonportable.xml" ] } + }, []), + BlockInfo( + "nonportable_link", "html", { + "data": "link\n\n", + "xml_attributes": {"filename": ["html/nonportable_link.xml", "html/nonportable_link.xml"]} + }, []), + BlockInfo( + "badlink", "html", { + "data": "\n", + "xml_attributes" : { "filename" : [ "html/badlink.xml", "html/badlink.xml" ] } + }, []), + BlockInfo( + "with_styling", "html", { + "data": "

Red text here

", + "xml_attributes": {"filename": ["html/with_styling.xml", "html/with_styling.xml"]} + }, []), + BlockInfo( + "just_img", "html", { + "data": "", + "xml_attributes": {"filename": [ "html/just_img.xml", "html/just_img.xml" ] } + }, []), + BlockInfo( + "Video_Resources", "video", { + "youtube_id_1_0" : "1bK-WdDi6Qw", "display_name" : "Video Resources" + }, []), + ]), + BlockInfo( + "Welcome", "video", {"data": "", "youtube_id_1_0": "p2Q6BrNhdh8", "display_name": "Welcome"}, [] + ), + BlockInfo( + "video_123456789012", "video", {"data": "", "youtube_id_1_0": "p2Q6BrNhdh8", "display_name": "Test Video"}, [] + ), + BlockInfo( + "video_4f66f493ac8f", "video", {"youtube_id_1_0": "p2Q6BrNhdh8"}, [] + ) + ] + ), + BlockInfo( + "secret:magic", "chapter", { + "xml_attributes": {"filename": [ "chapter/secret/magic.xml", "chapter/secret/magic.xml"]} + }, [ + BlockInfo( + "toyvideo", "video", {"youtube_id_1_0": "OEoXaMPEzfMA", "display_name": "toyvideo"}, [] + ) + ] + ), + BlockInfo( + "poll_test", "chapter", {}, [ + BlockInfo( + "T1_changemind_poll_foo", "poll_question", { + "question": "

Have you changed your mind? ’

", + "answers": [{"text": "Yes", "id": "yes"}, {"text": "No", "id": "no"}], + "xml_attributes": {"reset": "false", "filename": ["", None]}, + "display_name": "Change your answer" + }, []) ] + ), + BlockInfo( + "vertical_container", "chapter", { + "xml_attributes": {"filename": ["chapter/vertical_container.xml", "chapter/vertical_container.xml"]} + }, [ + BlockInfo("vertical_sequential", "sequential", {}, [ + BlockInfo("vertical_test", "vertical", { + "xml_attributes": {"filename": ["vertical/vertical_test.xml", "vertical_test"]} + }, [ + BlockInfo( + "sample_video", "video", { + "youtube_id_1_25": "AKqURZnYqpk", + "youtube_id_0_75": "JMD_ifUUfsU", + "youtube_id_1_0": "OEoXaMPEzfM", + "display_name": "default", + "youtube_id_1_5": "DYpADpL7jAY" + }, []), + BlockInfo( + "separate_file_video", "video", { + "youtube_id_1_25": "AKqURZnYqpk", + "youtube_id_0_75": "JMD_ifUUfsU", + "youtube_id_1_0": "OEoXaMPEzfM", + "display_name": "default", + "youtube_id_1_5": "DYpADpL7jAY" + }, []), + BlockInfo( + "video_with_end_time", "video", { + "youtube_id_1_25": "AKqURZnYqpk", + "display_name": "default", + "youtube_id_1_0": "OEoXaMPEzfM", + "end_time": datetime.timedelta(seconds=10), + "youtube_id_1_5": "DYpADpL7jAY", + "youtube_id_0_75": "JMD_ifUUfsU" + }, []), + BlockInfo( + "T1_changemind_poll_foo_2", "poll_question", { + "question": "

Have you changed your mind?

", + "answers": [{"text": "Yes", "id": "yes"}, {"text": "No", "id": "no"}], + "xml_attributes": {"reset": "false", "filename": [ "", None]}, + "display_name": "Change your answer" + }, []), + ]), + BlockInfo("unicode", "html", { + "data": "…", "xml_attributes": {"filename": ["", None]} + }, []) + ]), + ] + ), + BlockInfo( + "handout_container", "chapter", { + "xml_attributes" : {"filename" : ["chapter/handout_container.xml", "chapter/handout_container.xml"]} + }, [ + BlockInfo( + "html_7e5578f25f79", "html", { + "data": " handouts", + "xml_attributes": {"filename": ["", None]} + }, [] + ), + ] + ) +] + class ModuleStoreTestCase(TestCase): """ Subclass for any test case that uses a ModuleStore. @@ -242,3 +458,103 @@ class ModuleStoreTestCase(TestCase): # Call superclass implementation super(ModuleStoreTestCase, self)._post_teardown() + + def create_sample_course(self, org, course, run, block_info_tree=default_block_info_tree, course_fields=None): + """ + create a course in the default modulestore from the collection of BlockInfo + records defining the course tree + Returns: + course_loc: the CourseKey for the created course + """ + with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, None): + # TODO use a single transaction (version, inheritance cache, etc) for this whole thing + course = self.store.create_course(org, course, run, self.user.id, fields=course_fields) + self.course_loc = course.location + + def create_sub_tree(parent_loc, block_info): + block = self.store.create_child( + self.user.id, + # TODO remove version_agnostic() when we impl the single transaction + parent_loc.version_agnostic(), + block_info.category, block_id=block_info.block_id, + fields=block_info.fields, + ) + for tree in block_info.sub_tree: + create_sub_tree(block.location, tree) + setattr(self, block_info.block_id, block.location.version_agnostic()) + + for tree in block_info_tree: + create_sub_tree(self.course_loc, tree) + + self.store.publish(self.course_loc, self.user.id) + return self.course_loc.course_key.version_agnostic() + + def create_toy_course(self, org='edX', course='toy', run='2012_Fall'): + """ + Create an equiavlent to the toy xml course + """ + self.toy_loc = self.create_sample_course( + org, course, run, TOY_BLOCK_INFO_TREE, + { + "textbooks" : [["Textbook", "https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"]], + "wiki_slug" : "toy", + "display_name" : "Toy Course", + "graded" : True, + "tabs" : [ + {"type" : "courseware", "name" : "Courseware"}, + {"type" : "course_info", "name" : "Course Info"}, + {"type" : "static_tab", "name" : "Syllabus", "url_slug" : "syllabus"}, + {"type" : "static_tab", "name" : "Resources", "url_slug" : "resources"}, + {"type" : "discussion", "name" : "Discussion"}, + {"type" : "wiki", "name" : "Wiki"}, + {"type" : "progress", "name" : "Progress"} + ], + "discussion_topics" : {"General" : {"id" : "i4x-edX-toy-course-2012_Fall"}}, + "graceperiod" : datetime.timedelta(days=2, seconds=21599), + "start" : datetime.datetime(2015, 07, 17, 12, tzinfo=pytz.utc), + "xml_attributes" : {"filename" : ["course/2012_Fall.xml", "course/2012_Fall.xml"]}, + "pdf_textbooks" : [ + { + "tab_title" : "Sample Multi Chapter Textbook", + "id" : "MyTextbook", + "chapters" : [ + {"url" : "/static/Chapter1.pdf", "title" : "Chapter 1"}, + {"url" : "/static/Chapter2.pdf", "title" : "Chapter 2"} + ] + } + ], + "course_image" : "just_a_test.jpg", + } + ) + with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.toy_loc): + self.store.create_item( + self.user.id, self.toy_loc, "about", block_id="short_description", + fields={"data" : "A course about toys."} + ) + self.store.create_item( + self.user.id, self.toy_loc, "about", block_id="effort", + fields={"data": "6 hours"} + ) + self.store.create_item( + self.user.id, self.toy_loc, "about", block_id="end_date", + fields={"data": "TBD"} + ) + self.store.create_item( + self.user.id, self.toy_loc, "about", block_id="overview", + fields={ + "data": "
\n

About This Course

\n

Include your long course description here. The long course description should contain 150-400 words.

\n\n

This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.

\n
\n\n
\n

Prerequisites

\n

Add information about course prerequisites here.

\n
\n\n
\n

Course Staff

\n
\n
\n \"Course\n
\n\n

Staff Member #1

\n

Biography of instructor/staff member #1

\n
\n\n
\n
\n \"Course\n
\n\n

Staff Member #2

\n

Biography of instructor/staff member #2

\n
\n
\n\n
\n
\n

Frequently Asked Questions

\n
\n

Do I need to buy a textbook?

\n

No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.

\n
\n\n
\n

Question #2

\n

Your answer would be displayed here.

\n
\n
\n
\n" + } + ) + self.store.create_item( + self.user.id, self.toy_loc, "course_info", "handouts", + fields={"data": "Sample"} + ) + self.store.create_item( + self.user.id, self.toy_loc, "static_tab", "resources", + fields={"display_name": "Resources"}, + ) + self.store.create_item( + self.user.id, self.toy_loc, "static_tab", "syllabus", + fields={"display_name": "Syllabus"}, + ) + return self.toy_loc diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 40d3d64797..6e2185c474 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -9,7 +9,6 @@ import json from django.http import Http404, HttpResponse from django.core.urlresolvers import reverse from django.conf import settings -from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from django.contrib.auth.models import AnonymousUser @@ -314,24 +313,22 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase): ) -@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) -class TestTOC(TestCase): +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestTOC(ModuleStoreTestCase): """Check the Table of Contents for a course""" def setUp(self): - - # Toy courses should be loaded - self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') - self.toy_course = modulestore().get_course(self.course_key) - self.portal_user = UserFactory() + super(TestTOC, self).setUp() + self.course_key = self.create_toy_course() + self.chapter = 'Overview' + chapter_url = '%s/%s/%s' % ('/courses', self.course_key, self.chapter) + factory = RequestFactory() + self.request = factory.get(chapter_url) + self.request.user = UserFactory() + self.toy_course = self.store.get_course(self.toy_loc) + self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents( + self.toy_loc, self.request.user, self.toy_course, depth=2) def test_toc_toy_from_chapter(self): - chapter = 'Overview' - chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter) - factory = RequestFactory() - request = factory.get(chapter_url) - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - self.toy_course.id, self.portal_user, self.toy_course, depth=2) - expected = ([{'active': True, 'sections': [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, 'format': u'Lecture Sequence', 'due': None, 'active': False}, @@ -347,19 +344,12 @@ class TestTOC(TestCase): 'format': '', 'due': None, 'active': False}], 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) - actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, field_data_cache) + actual = render.toc_for_course(self.request.user, self.request, self.toy_course, self.chapter, None, self.field_data_cache) for toc_section in expected: self.assertIn(toc_section, actual) def test_toc_toy_from_section(self): - chapter = 'Overview' - chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter) section = 'Welcome' - factory = RequestFactory() - request = factory.get(chapter_url) - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - self.toy_course.id, self.portal_user, self.toy_course, depth=2) - expected = ([{'active': True, 'sections': [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, 'format': u'Lecture Sequence', 'due': None, 'active': False}, @@ -375,7 +365,7 @@ class TestTOC(TestCase): 'format': '', 'due': None, 'active': False}], 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) - actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, field_data_cache) + actual = render.toc_for_course(self.request.user, self.request, self.toy_course, self.chapter, section, self.field_data_cache) for toc_section in expected: self.assertIn(toc_section, actual)