Commit 84238160 by Will Daly

LMS contentstore lettuce tests now dynamically create courses in mongo

using terrain.factories.py and capa.tests.response_xml_factory
parent a20a3a02
...@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory): ...@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory):
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, *args, **kwargs):
""" """
kwargs must include parent_location, template. Can contain display_name Uses *kwargs*:
target_class is ignored
*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'] DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location')) parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template')) template = Location(kwargs.get('template'))
data = kwargs.get('data')
display_name = kwargs.get('display_name') display_name = kwargs.get('display_name')
metadata = kwargs.get('metadata', {})
store = modulestore('direct') store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py # This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location) 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) new_item = store.clone_item(template, dest_location)
...@@ -143,8 +163,15 @@ class XModuleItemFactory(Factory): ...@@ -143,8 +163,15 @@ class XModuleItemFactory(Factory):
if display_name is not None: if display_name is not None:
new_item.display_name = display_name 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)) 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: if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()]) store.update_children(parent_location, parent.children + [new_item.location.url()])
......
...@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title): ...@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title):
assert_equals(world.browser.title, 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$') @step('I am a logged in user$')
def i_am_logged_in_user(step): def i_am_logged_in_user(step):
create_user('robot') create_user('robot')
...@@ -80,18 +85,6 @@ def i_am_not_logged_in(step): ...@@ -80,18 +85,6 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete() 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 "([^"]*)"$') @step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id): def i_am_staff_for_course_by_id(step, course_id):
register_by_course_id(course_id, True) register_by_course_id(course_id, True)
...@@ -139,13 +132,16 @@ def log_in(email, password): ...@@ -139,13 +132,16 @@ def log_in(email, password):
world.browser.is_element_present_by_css('header.global', 10) world.browser.is_element_present_by_css('header.global', 10)
world.browser.click_link_by_href('#login-modal') world.browser.click_link_by_href('#login-modal')
# wait for the login dialog to load # Wait for the login dialog to load
assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) # 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 # For some reason, the page sometimes includes two #login_form
# elements, the first of which is not visible. # elements, the first of which is not visible.
# To avoid this, we always select the last of the two #login_form # To avoid this, we always select the last of the two #login_form dialogs
# dialogs
login_form = world.browser.find_by_css('form#login_form').last login_form = world.browser.find_by_css('form#login_form').last
login_form.find_by_name('email').fill(email) login_form.find_by_name('email').fill(email)
......
...@@ -151,13 +151,11 @@ class ResponseXMLFactory(object): ...@@ -151,13 +151,11 @@ class ResponseXMLFactory(object):
choice_element = etree.SubElement(group_element, "choice") choice_element = etree.SubElement(group_element, "choice")
choice_element.set("correct", "true" if correct_val else "false") 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 # 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: if name:
choice_element.text = str(name)
choice_element.set("name", str(name)) choice_element.set("name", str(name))
return group_element return group_element
......
...@@ -5,6 +5,10 @@ from lettuce.django import django_url ...@@ -5,6 +5,10 @@ from lettuce.django import django_url
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from student.models import CourseEnrollment 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 import time
from logging import getLogger from logging import getLogger
...@@ -81,17 +85,53 @@ def i_am_not_logged_in(step): ...@@ -81,17 +85,53 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete() 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 "([^"]*)"$') @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') world.create_user('robot')
u = User.objects.get(username='robot') u = User.objects.get(username='robot')
# If the user is not already enrolled, enroll the user. # If the user is not already enrolled, enroll the user.
if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0: CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
CourseEnrollment.objects.create(user=u, course_id=course_id)
world.log_in('robot@edx.org', 'test') 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$') @step(u'I am an edX user$')
def i_am_an_edx_user(step): def i_am_an_edx_user(step):
...@@ -101,3 +141,34 @@ 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$') @step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname): def registered_edx_user(step, uname):
world.create_user(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(" ", "_"))
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 ...@@ -3,21 +3,18 @@ Feature: All the high level tabs should work
As a student As a student
I want to navigate through the high level tabs I want to navigate through the high level tabs
# Note this didn't work as a scenario outline because Scenario: I can navigate to all high-level tabs in a course
# before each scenario was not flushing the database Given: I am registered for the course "6.002x"
# TODO: break this apart so that if one fails the others And The course "6.002x" has extra tab "Custom Tab"
# will still run And I log in
Scenario: A student can see all tabs of the course And I click on View Courseware
Given I am registered for the course "MITx/6.002x/2013_Spring" When I click on the "<TabName>" tab
And I log in Then the page title should contain "<PageTitle>"
And I click on View Courseware
When I click on the "Courseware" tab Examples:
Then the page title should be "6.002x Courseware" | TabName | PageTitle |
When I click on the "Course Info" tab | Courseware | 6.002x Courseware |
Then the page title should be "6.002x Course Info" | Course Info | 6.002x Course Info |
When I click on the "Textbook" tab | Custom Tab | 6.002x Custom Tab |
Then the page title should be "6.002x Textbook" | Wiki | edX Wiki |
When I click on the "Wiki" tab | Progress | 6.002x Progress |
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"
...@@ -2,14 +2,118 @@ from lettuce import world, step ...@@ -2,14 +2,118 @@ from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
from selenium.webdriver.support.ui import Select from selenium.webdriver.support.ui import Select
import random 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') @step(u'I am viewing a "([^"]*)" problem')
def view_problem(step, problem_type): def view_problem(step, problem_type):
i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') i_am_registered_for_the_course(step, 'model_course')
url = django_url(problem_url(problem_type))
# 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) world.browser.visit(url)
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"') @step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
def answer_problem(step, problem_type, correctness): def answer_problem(step, problem_type, correctness):
""" Mark a given problem type correct or incorrect, then submit it. """ Mark a given problem type correct or incorrect, then submit it.
...@@ -21,7 +125,7 @@ def answer_problem(step, problem_type, correctness): ...@@ -21,7 +125,7 @@ def answer_problem(step, problem_type, correctness):
assert(correctness in ['correct', 'incorrect']) assert(correctness in ['correct', 'incorrect'])
if problem_type == "drop down": 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' option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
world.browser.select(select_name, option_text) world.browser.select(select_name, option_text)
...@@ -125,21 +229,6 @@ def assert_answer_mark(step, problem_type, correctness): ...@@ -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)) 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): def inputfield(problem_type, choice=None):
""" Return the <input> element for *problem_type*. """ Return the <input> element for *problem_type*.
For example, if problem_type is 'string', return For example, if problem_type is 'string', return
...@@ -148,19 +237,14 @@ def inputfield(problem_type, choice=None): ...@@ -148,19 +237,14 @@ def inputfield(problem_type, choice=None):
*choice* is the name of the checkbox input in a group *choice* is the name of the checkbox input in a group
of checkboxes. """ of checkboxes. """
field_extensions = { 'drop down': 'Drop_Down_Problem', sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_")
'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
if choice is not None: if choice is not None:
base = "_choice_" if problem_type == "multiple choice" else "_" base = "_choice_" if problem_type == "multiple choice" else "_"
sel = sel + base + str(choice) 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) return world.browser.find_by_css(sel)
...@@ -4,13 +4,14 @@ Feature: Register for a course ...@@ -4,13 +4,14 @@ Feature: Register for a course
I want to register for a class on the edX website I want to register for a class on the edX website
Scenario: I can register for a course 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 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 Then I should see the course numbered "6.002x" in my dashboard
Scenario: I can unregister for a course 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 And I visit the dashboard
When I click the link with the text "Unregister" When I click the link with the text "Unregister"
And I press the "Unregister" button in the Unenroll dialog And I press the "Unregister" button in the Unenroll dialog
......
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
from common import TEST_COURSE_ORG, TEST_COURSE_NAME
@step('I register for the course "([^"]*)"$') @step('I register for the course "([^"]*)"$')
def i_register_for_the_course(step, course_id): def i_register_for_the_course(step, course):
world.browser.visit(django_url('courses/%s/about' % course_id)) 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') intro_section = world.browser.find_by_css('section.intro')
register_link = intro_section.find_by_css('a.register') register_link = intro_section.find_by_css('a.register')
......
...@@ -8,16 +8,24 @@ from .test import * ...@@ -8,16 +8,24 @@ from .test import *
# otherwise the browser will not render the pages correctly # otherwise the browser will not render the pages correctly
DEBUG = True DEBUG = True
# Show the courses that are in the data directory # Use the mongo store for acceptance tests
COURSES_ROOT = ENV_ROOT / "data" modulestore_options = {
DATA_DIR = COURSES_ROOT '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 = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': modulestore_options
'data_dir': DATA_DIR, },
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'direct': {
} 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment