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):
@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(" ", "_"))
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
......
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
}
}
......
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