LMS contentstore lettuce tests now dynamically create courses in mongo
using terrain.factories.py and capa.tests.response_xml_factory
This commit is contained in:
@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory):
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
Uses *kwargs*:
|
||||
|
||||
*parent_location* (required): the location of the parent module
|
||||
(e.g. the parent course or section)
|
||||
|
||||
*template* (required): the template to create the item from
|
||||
(e.g. i4x://templates/section/Empty)
|
||||
|
||||
*data* (optional): the data for the item
|
||||
(e.g. XML problem definition for a problem item)
|
||||
|
||||
*display_name* (optional): the display name of the item
|
||||
|
||||
*metadata* (optional): dictionary of metadata attributes
|
||||
|
||||
*target_class* is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
data = kwargs.get('data')
|
||||
display_name = kwargs.get('display_name')
|
||||
metadata = kwargs.get('metadata', {})
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
# If a display name is set, use that
|
||||
dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
|
||||
dest_location = parent_location._replace(category=template.category,
|
||||
name=dest_name)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
@@ -143,8 +163,15 @@ class XModuleItemFactory(Factory):
|
||||
if display_name is not None:
|
||||
new_item.display_name = display_name
|
||||
|
||||
# Add additional metadata or override current metadata
|
||||
new_item.metadata.update(metadata)
|
||||
|
||||
store.update_metadata(new_item.location.url(), own_metadata(new_item))
|
||||
|
||||
# replace the data with the optional *data* parameter
|
||||
if data is not None:
|
||||
store.update_item(new_item.location, data)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title):
|
||||
assert_equals(world.browser.title, title)
|
||||
|
||||
|
||||
@step(u'the page title should contain "([^"]*)"$')
|
||||
def the_page_title_should_contain(step, title):
|
||||
assert(title in world.browser.title)
|
||||
|
||||
|
||||
@step('I am a logged in user$')
|
||||
def i_am_logged_in_user(step):
|
||||
create_user('robot')
|
||||
@@ -80,18 +85,6 @@ def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
|
||||
@step('I am registered for a course$')
|
||||
def i_am_registered_for_a_course(step):
|
||||
create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall')
|
||||
|
||||
|
||||
@step('I am registered for course "([^"]*)"$')
|
||||
def i_am_registered_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id)
|
||||
|
||||
|
||||
@step('I am staff for course "([^"]*)"$')
|
||||
def i_am_staff_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id, True)
|
||||
@@ -139,13 +132,16 @@ def log_in(email, password):
|
||||
world.browser.is_element_present_by_css('header.global', 10)
|
||||
world.browser.click_link_by_href('#login-modal')
|
||||
|
||||
# wait for the login dialog to load
|
||||
assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10))
|
||||
# Wait for the login dialog to load
|
||||
# This is complicated by the fact that sometimes a second #login_form
|
||||
# dialog loads, while the first one remains hidden.
|
||||
# We give them both time to load, starting with the second one.
|
||||
world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=2)
|
||||
world.browser.is_element_present_by_css('form#login_form', wait_time=2)
|
||||
|
||||
# For some reason, the page sometimes includes two #login_form
|
||||
# elements, the first of which is not visible.
|
||||
# To avoid this, we always select the last of the two #login_form
|
||||
# dialogs
|
||||
# To avoid this, we always select the last of the two #login_form dialogs
|
||||
login_form = world.browser.find_by_css('form#login_form').last
|
||||
|
||||
login_form.find_by_name('email').fill(email)
|
||||
|
||||
@@ -151,13 +151,11 @@ class ResponseXMLFactory(object):
|
||||
choice_element = etree.SubElement(group_element, "choice")
|
||||
choice_element.set("correct", "true" if correct_val else "false")
|
||||
|
||||
# Add some text describing the choice
|
||||
etree.SubElement(choice_element, "startouttext")
|
||||
etree.text = "Choice description"
|
||||
etree.SubElement(choice_element, "endouttext")
|
||||
|
||||
# Add a name identifying the choice, if one exists
|
||||
# For simplicity, we use the same string as both the
|
||||
# name attribute and the text of the element
|
||||
if name:
|
||||
choice_element.text = str(name)
|
||||
choice_element.set("name", str(name))
|
||||
|
||||
return group_element
|
||||
|
||||
@@ -5,6 +5,10 @@ from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from terrain.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
import time
|
||||
|
||||
from logging import getLogger
|
||||
@@ -81,17 +85,53 @@ def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
|
||||
TEST_COURSE_ORG = 'edx'
|
||||
TEST_COURSE_NAME = 'Test Course'
|
||||
TEST_SECTION_NAME = "Problem"
|
||||
|
||||
@step(u'The course "([^"]*)" exists$')
|
||||
def create_course(step, course):
|
||||
|
||||
# First clear the modulestore so we don't try to recreate
|
||||
# the same course twice
|
||||
# This also ensures that the necessary templates are loaded
|
||||
flush_xmodule_store()
|
||||
|
||||
# Create the course
|
||||
# We always use the same org and display name,
|
||||
# but vary the course identifier (e.g. 600x or 191x)
|
||||
course = CourseFactory.create(org=TEST_COURSE_ORG,
|
||||
number=course,
|
||||
display_name=TEST_COURSE_NAME)
|
||||
|
||||
# Add a section to the course to contain problems
|
||||
section = ItemFactory.create(parent_location=course.location,
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
problem_section = ItemFactory.create(parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
@step(u'I am registered for the course "([^"]*)"$')
|
||||
def i_am_registered_for_the_course(step, course_id):
|
||||
def i_am_registered_for_the_course(step, course):
|
||||
# Create the course
|
||||
create_course(step, course)
|
||||
|
||||
# Create the user
|
||||
world.create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
|
||||
# If the user is not already enrolled, enroll the user.
|
||||
if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0:
|
||||
CourseEnrollment.objects.create(user=u, course_id=course_id)
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
|
||||
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
|
||||
@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
|
||||
def add_tab_to_course(step, course, extra_tab_name):
|
||||
section_item = ItemFactory.create(parent_location=course_location(course),
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name=str(extra_tab_name))
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
def i_am_an_edx_user(step):
|
||||
@@ -101,3 +141,34 @@ def i_am_an_edx_user(step):
|
||||
@step(u'User "([^"]*)" is an edX user$')
|
||||
def registered_edx_user(step, uname):
|
||||
world.create_user(uname)
|
||||
|
||||
|
||||
def flush_xmodule_store():
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
_MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
update_templates()
|
||||
|
||||
def course_id(course_num):
|
||||
return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
|
||||
TEST_COURSE_NAME.replace(" ", "_"))
|
||||
|
||||
def course_location(course_num):
|
||||
return Location(loc_or_tag="i4x",
|
||||
org=TEST_COURSE_ORG,
|
||||
course=course_num,
|
||||
category='course',
|
||||
name=TEST_COURSE_NAME.replace(" ", "_"))
|
||||
|
||||
def section_location(course_num):
|
||||
return Location(loc_or_tag="i4x",
|
||||
org=TEST_COURSE_ORG,
|
||||
course=course_num,
|
||||
category='sequential',
|
||||
name=TEST_SECTION_NAME.replace(" ", "_"))
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Feature: View the Courseware Tab
|
||||
As a student in an edX course
|
||||
In order to work on the course
|
||||
I want to view the info on the courseware tab
|
||||
|
||||
Scenario: I can get to the courseware tab when logged in
|
||||
Given I am registered for the course "MITx/6.002x/2013_Spring"
|
||||
And I log in
|
||||
And I click on View Courseware
|
||||
When I click on the "Courseware" tab
|
||||
Then the "Courseware" tab is active
|
||||
@@ -3,21 +3,18 @@ Feature: All the high level tabs should work
|
||||
As a student
|
||||
I want to navigate through the high level tabs
|
||||
|
||||
# Note this didn't work as a scenario outline because
|
||||
# before each scenario was not flushing the database
|
||||
# TODO: break this apart so that if one fails the others
|
||||
# will still run
|
||||
Scenario: A student can see all tabs of the course
|
||||
Given I am registered for the course "MITx/6.002x/2013_Spring"
|
||||
And I log in
|
||||
And I click on View Courseware
|
||||
When I click on the "Courseware" tab
|
||||
Then the page title should be "6.002x Courseware"
|
||||
When I click on the "Course Info" tab
|
||||
Then the page title should be "6.002x Course Info"
|
||||
When I click on the "Textbook" tab
|
||||
Then the page title should be "6.002x Textbook"
|
||||
When I click on the "Wiki" tab
|
||||
Then the page title should be "6.002x | edX Wiki"
|
||||
When I click on the "Progress" tab
|
||||
Then the page title should be "6.002x Progress"
|
||||
Scenario: I can navigate to all high-level tabs in a course
|
||||
Given: I am registered for the course "6.002x"
|
||||
And The course "6.002x" has extra tab "Custom Tab"
|
||||
And I log in
|
||||
And I click on View Courseware
|
||||
When I click on the "<TabName>" tab
|
||||
Then the page title should contain "<PageTitle>"
|
||||
|
||||
Examples:
|
||||
| TabName | PageTitle |
|
||||
| Courseware | 6.002x Courseware |
|
||||
| Course Info | 6.002x Course Info |
|
||||
| Custom Tab | 6.002x Custom Tab |
|
||||
| Wiki | edX Wiki |
|
||||
| Progress | 6.002x Progress |
|
||||
|
||||
@@ -2,14 +2,118 @@ from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from selenium.webdriver.support.ui import Select
|
||||
import random
|
||||
from common import i_am_registered_for_the_course
|
||||
import textwrap
|
||||
from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
|
||||
from terrain.factories import ItemFactory
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
|
||||
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
|
||||
StringResponseXMLFactory, NumericalResponseXMLFactory, \
|
||||
FormulaResponseXMLFactory, CustomResponseXMLFactory
|
||||
|
||||
# Factories from capa.tests.response_xml_factory that we will use
|
||||
# to generate the problem XML, with the keyword args used to configure
|
||||
# the output.
|
||||
problem_factory_dict = {
|
||||
'drop down': {
|
||||
'factory': OptionResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Option 2',
|
||||
'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
|
||||
'correct_option': 'Option 2'}},
|
||||
|
||||
'multiple choice': {
|
||||
'factory': MultipleChoiceResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choice 3',
|
||||
'choices': [False, False, True, False],
|
||||
'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}},
|
||||
|
||||
'checkbox': {
|
||||
'factory': ChoiceResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choices 1 and 3',
|
||||
'choice_type':'checkbox',
|
||||
'choices':[True, False, True, False, False],
|
||||
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
|
||||
|
||||
'string': {
|
||||
'factory': StringResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The answer is "correct string"',
|
||||
'case_sensitive': False,
|
||||
'answer': 'correct string' }},
|
||||
|
||||
'numerical': {
|
||||
'factory': NumericalResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The answer is pi + 1',
|
||||
'answer': '4.14159',
|
||||
'tolerance': '0.00001',
|
||||
'math_display': True }},
|
||||
|
||||
'formula': {
|
||||
'factory': FormulaResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]',
|
||||
'sample_dict': {'x': (-100, 100), 'y': (-100, 100) },
|
||||
'num_samples': 10,
|
||||
'tolerance': 0.00001,
|
||||
'math_display': True,
|
||||
'answer': 'x^2+2*x+y'}},
|
||||
|
||||
'script': {
|
||||
'factory': CustomResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'Enter two integers that sum to 10.',
|
||||
'cfn': 'test_add_to_ten',
|
||||
'expect': '10',
|
||||
'num_inputs': 2,
|
||||
'script': textwrap.dedent("""
|
||||
def test_add_to_ten(expect,ans):
|
||||
try:
|
||||
a1=int(ans[0])
|
||||
a2=int(ans[1])
|
||||
except ValueError:
|
||||
a1=0
|
||||
a2=0
|
||||
return (a1+a2)==int(expect)
|
||||
""") }},
|
||||
}
|
||||
|
||||
def add_problem_to_course(course, problem_type):
|
||||
|
||||
assert(problem_type in problem_factory_dict)
|
||||
|
||||
# Generate the problem XML using capa.tests.response_xml_factory
|
||||
factory_dict = problem_factory_dict[problem_type]
|
||||
problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs'])
|
||||
|
||||
# Create a problem item using our generated XML
|
||||
# We set rerandomize=always in the metadata so that the "Reset" button
|
||||
# will appear.
|
||||
problem_item = ItemFactory.create(parent_location=section_location(course),
|
||||
template="i4x://edx/templates/problem/Blank_Common_Problem",
|
||||
display_name=str(problem_type),
|
||||
data=problem_xml,
|
||||
metadata={'rerandomize':'always'})
|
||||
|
||||
@step(u'I am viewing a "([^"]*)" problem')
|
||||
def view_problem(step, problem_type):
|
||||
i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring')
|
||||
url = django_url(problem_url(problem_type))
|
||||
i_am_registered_for_the_course(step, 'model_course')
|
||||
|
||||
# Ensure that the course has this problem type
|
||||
add_problem_to_course('model_course', problem_type)
|
||||
|
||||
# Go to the one section in the factory-created course
|
||||
# which should be loaded with the correct problem
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
|
||||
def answer_problem(step, problem_type, correctness):
|
||||
""" Mark a given problem type correct or incorrect, then submit it.
|
||||
@@ -21,7 +125,7 @@ def answer_problem(step, problem_type, correctness):
|
||||
assert(correctness in ['correct', 'incorrect'])
|
||||
|
||||
if problem_type == "drop down":
|
||||
select_name = "input_i4x-edX-model_course-problem-Drop_Down_Problem_2_1"
|
||||
select_name = "input_i4x-edx-model_course-problem-drop_down_2_1"
|
||||
option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
|
||||
world.browser.select(select_name, option_text)
|
||||
|
||||
@@ -125,21 +229,6 @@ def assert_answer_mark(step, problem_type, correctness):
|
||||
assert(world.browser.is_element_not_present_by_css(sel, wait_time=4))
|
||||
|
||||
|
||||
def problem_url(problem_type):
|
||||
""" Construct a url to a page with the given problem type """
|
||||
base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/'
|
||||
url_extensions = { 'drop down': 'Drop_Down_Problems',
|
||||
'multiple choice': 'Multiple_Choice_Problems',
|
||||
'checkbox': 'Checkbox_Problems',
|
||||
'string': 'String_Problems',
|
||||
'numerical': 'Numerical_Problems',
|
||||
'formula': 'Formula_Problems', }
|
||||
|
||||
assert(problem_type in url_extensions)
|
||||
return base + url_extensions[problem_type]
|
||||
|
||||
|
||||
|
||||
def inputfield(problem_type, choice=None):
|
||||
""" Return the <input> element for *problem_type*.
|
||||
For example, if problem_type is 'string', return
|
||||
@@ -148,19 +237,14 @@ def inputfield(problem_type, choice=None):
|
||||
*choice* is the name of the checkbox input in a group
|
||||
of checkboxes. """
|
||||
|
||||
field_extensions = { 'drop down': 'Drop_Down_Problem',
|
||||
'multiple choice': 'Multiple_Choice_Problem',
|
||||
'checkbox': 'Checkbox_Problem',
|
||||
'string': 'String_Problem',
|
||||
'numerical': 'Numerical_Problem',
|
||||
'formula': 'Formula_Problem', }
|
||||
|
||||
assert(problem_type in field_extensions)
|
||||
extension = field_extensions[problem_type]
|
||||
sel = "input#input_i4x-edX-model_course-problem-%s_2_1" % extension
|
||||
sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_")
|
||||
|
||||
if choice is not None:
|
||||
base = "_choice_" if problem_type == "multiple choice" else "_"
|
||||
sel = sel + base + str(choice)
|
||||
|
||||
# If the input element doesn't exist, fail immediately
|
||||
assert(world.browser.is_element_present_by_css(sel, wait_time=4))
|
||||
|
||||
# Retrieve the input element
|
||||
return world.browser.find_by_css(sel)
|
||||
|
||||
@@ -4,13 +4,14 @@ Feature: Register for a course
|
||||
I want to register for a class on the edX website
|
||||
|
||||
Scenario: I can register for a course
|
||||
Given I am logged in
|
||||
Given The course "6.002x" exists
|
||||
And I am logged in
|
||||
And I visit the courses page
|
||||
When I register for the course "MITx/6.002x/2013_Spring"
|
||||
When I register for the course "6.002x"
|
||||
Then I should see the course numbered "6.002x" in my dashboard
|
||||
|
||||
Scenario: I can unregister for a course
|
||||
Given I am registered for the course "MITx/6.002x/2013_Spring"
|
||||
Given I am registered for the course "6.002x"
|
||||
And I visit the dashboard
|
||||
When I click the link with the text "Unregister"
|
||||
And I press the "Unregister" button in the Unenroll dialog
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import TEST_COURSE_ORG, TEST_COURSE_NAME
|
||||
|
||||
@step('I register for the course "([^"]*)"$')
|
||||
def i_register_for_the_course(step, course_id):
|
||||
world.browser.visit(django_url('courses/%s/about' % course_id))
|
||||
def i_register_for_the_course(step, course):
|
||||
cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
|
||||
url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
|
||||
world.browser.visit(url)
|
||||
|
||||
intro_section = world.browser.find_by_css('section.intro')
|
||||
register_link = intro_section.find_by_css('a.register')
|
||||
|
||||
@@ -8,16 +8,24 @@ from .test import *
|
||||
# otherwise the browser will not render the pages correctly
|
||||
DEBUG = True
|
||||
|
||||
# Show the courses that are in the data directory
|
||||
COURSES_ROOT = ENV_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
# Use the mongo store for acceptance tests
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'data_dir': DATA_DIR,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
}
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user