Files
edx-platform/cms/djangoapps/contentstore/tests/test_contentstore.py
Christina Roberts 0b91c1c346 Merge pull request #1487 from MITx/fix/cdodge/cleanup-deleted-templates
clean up templates that were removed from disk - we need to remove from ...
2013-02-13 06:29:35 -08:00

443 lines
17 KiB
Python

import json
import shutil
from django.test.client import Client
from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempfile import mkdtemp
import json
from fs.osfs import OSFS
import copy
from mock import Mock
from json import dumps, loads
from student.models import Registration
from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
from utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
Tests that rely on the toy courses.
TODO: refactor using CourseFactory so they do not.
"""
def setUp(self):
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(uname, email, password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
self.client = Client()
self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
print "Checking ", descriptor.location.url()
print descriptor.__class__, descriptor.location
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
def test_edit_unit_toy(self):
self.check_edit_unit('toy')
def test_edit_unit_full(self):
self.check_edit_unit('full')
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
reverse_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
self.assertEqual(reverse_tabs, course_tabs)
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
# this one should be in a non-override folder
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = ms.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
def test_clone_course(self):
course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
import_from_xml(modulestore(), 'common/test/data/', ['full'])
resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
ms = modulestore('direct')
cs = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(ms, cs, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location)
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
fs = OSFS(root_dir / 'test_export')
self.assertTrue(fs.exists(dirname))
query_loc = Location('i4x', location.org, location.course, category_name, None)
items = modulestore.get_items(query_loc)
for item in items:
fs = OSFS(root_dir / ('test_export/' + dirname))
self.assertTrue(fs.exists(item.location.name + filename_suffix))
def test_export_course(self):
ms = modulestore('direct')
cs = contentstore()
import_from_xml(ms, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp())
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
export_to_xml(ms, cs, location, root_dir, 'test_export')
# check for static tabs
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read())
course = ms.get_item(location)
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
# remove old course
delete_course(ms, cs, location)
# reimport
import_from_xml(ms, root_dir, ['test_export'])
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
ms = modulestore('direct')
cs = contentstore()
# import a test course
import_from_xml(ms, 'common/test/data/', ['full'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
# get module info
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
# make sure we got a successful response
self.assertEqual(resp.status_code, 200)
# check that /static/ has been converted to the full path
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
"""
def setUp(self):
"""
These tests need a user in the DB so that the django Test Client
can log them in.
They inherit from the ModuleStoreTestCase class so that the mongodb collection
will be cleared out before each test case execution and deleted
afterwards.
"""
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(uname, email, password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
self.client = Client()
self.client.login(username=uname, password=password)
self.course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
def test_create_course(self):
"""Test new course creation - happy path"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
def test_create_course_duplicate_course(self):
"""Test new course creation - error path"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp)
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.')
def test_create_course_duplicate_number(self):
"""Test new course creation - error path"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
self.course_data['display_name'] = 'Robot Super Course Two'
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp)
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
'There is already a course defined with the same organization and course number.')
def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name"""
self.course_data['org'] = 'University of California, Berkeley'
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp)
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
# Create a course so there is something to view
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<h1>My Courses</h1>',
status_code=200,
html=True)
def test_course_factory(self):
"""Test that the course factory works correctly."""
course = CourseFactory.create()
self.assertIsInstance(course, CourseDescriptor)
def test_item_factory(self):
"""Test that the item factory works correctly."""
course = CourseFactory.create()
item = ItemFactory.create(parent_location=course.location)
self.assertIsInstance(item, SequenceDescriptor)
def test_course_index_view_with_course(self):
"""Test viewing the index page with an existing course"""
CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<span class="class-name">Robot Super Educational Course</span>',
status_code=200,
html=True)
def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course"""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
data = {
'org': 'MITx',
'course': '999',
'name': Location.clean('Robot Super Course'),
}
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(resp,
'<a href="/MITx/999/course/Robot_Super_Course" class="class-name">Robot Super Course</a>',
status_code=200,
html=True)
def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section"""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
section_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One',
}
resp = self.client.post(reverse('clone_item'), section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertRegexpMatches(data['id'],
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/problem/Empty'
}
resp = self.client.post(reverse('clone_item'), problem_data)
self.assertEqual(resp.status_code, 200)
payload = parse_json(resp)
problem_loc = payload['id']
problem = get_modulestore(problem_loc).get_item(problem_loc)
# should be a CapaDescriptor
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
ms = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty')
ms.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
update_templates()
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
verify_create = ms.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
self.assertTrue(asserted)