Commit b2bda104 by cahrens

Merge branch 'master' into bug/christina/studio

Conflicts:
	cms/djangoapps/contentstore/features/common.py
	cms/djangoapps/contentstore/features/section.py
	cms/djangoapps/contentstore/features/subsection.feature
parents 2d677a83 210101dc
Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs
I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_true, assert_false, assert_equal
......@@ -18,13 +19,14 @@ DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css)
world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
world.css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$')
......@@ -35,24 +37,8 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
# def is_invisible(driver):
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
time.sleep(float(1))
css_click_at(css)
# is_invisible is not returning a boolean, not working
# try:
# css_click_at(css)
# wait_for(is_invisible)
# except WebDriverException, e:
# css_click_at(css)
# wait_for(is_invisible)
world.css_click_at(css)
@step(u'I edit the value of a policy key$')
......@@ -61,7 +47,7 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
......@@ -85,7 +71,7 @@ def i_see_default_advanced_settings(step):
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
key_elements = css_find(KEY_CSS)
key_elements = world.css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
......@@ -118,13 +104,13 @@ def assert_policy_entries(expected_keys, expected_values):
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect")
def get_index_of(expected_key):
for counter in range(len(css_find(KEY_CSS))):
for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = css_find(KEY_CSS)[counter].value
key = world.css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
......@@ -133,14 +119,14 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value
return world.css_find(VALUE_CSS)[index].value
def change_display_name_value(step, new_value):
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")
\ No newline at end of file
press_the_notification_button(step, "Save")
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_true, assert_equal
from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css)
world.css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a'
css_click(link_css)
world.css_click(link_css)
@step('I have opened Checklists$')
......@@ -20,7 +24,7 @@ def i_have_opened_checklists(step):
@step('I see the four default edX checklists$')
def i_see_default_checklists(step):
checklists = css_find('.checklist-title')
checklists = world.css_find('.checklist-title')
assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
......@@ -58,7 +62,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', css_find('.outline .title-1')[0].text)
assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text)
assert_equal(1, len(world.browser.windows))
......@@ -90,30 +94,30 @@ def i_am_brought_to_help_page_in_new_window(step):
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
try:
statusCount = css_find('#course-checklist1 .status-count').first
statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed)
except StaleElementReferenceException:
return False
wait_for(verify_count)
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
world.wait_for(verify_count)
assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text)
# Would like to check the CSS width, but not sure how to do that.
assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task):
css_click('#course-checklist' + str(checklist) +'-task' + str(task))
world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task)
action_link = css_find('#course-checklist' + str(checklist) + ' a')[task]
action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task]
# text will be empty initially, wait for it to populate
def verify_action_link_text(driver):
return action_link.text == actionText
wait_for(verify_action_link_text)
world.wait_for(verify_action_link_text)
action_link.click()
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
from nose.tools import assert_true
from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
......@@ -18,14 +16,15 @@ from logging import getLogger
logger = getLogger(__name__)
########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001
# in your settings.py file.
world.browser.visit(django_url('/'))
world.visit('/')
signin_css = 'a.action-signin'
assert world.browser.is_element_present_by_css(signin_css, 10)
assert world.is_css_present(signin_css)
@step('I am logged into Studio$')
......@@ -46,12 +45,12 @@ def i_press_the_category_delete_icon(step, category):
css = 'a.delete-button.delete-subsection-button span.delete-icon'
else:
assert False, 'Invalid category: %s' % category
css_click(css)
world.css_click(css)
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
clear_courses()
world.clear_courses()
log_into_studio()
create_a_course()
......@@ -77,80 +76,13 @@ def create_studio_user(
user_profile = world.UserProfileFactory(user=studio_user)
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 assert_css_with_text(css, text):
assert_true(world.browser.is_element_present_by_css(css, 5))
assert_equal(world.browser.find_by_css(css).text, text)
def css_click(css):
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
css_find(css).first.click()
except WebDriverException, e:
css_click_at(css)
def css_click_at(css, x=10, y=10):
'''
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
e = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
def css_fill(css, value):
world.browser.find_by_css(css).first.fill(value)
def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 5).until(func)
def id_find(id):
return world.browser.find_by_id(id)
def clear_courses():
flush_xmodule_store()
def fill_in_course_info(
name='Robot Super Course',
org='MITx',
num='101'):
css_fill('.new-course-name', name)
css_fill('.new-course-org', org)
css_fill('.new-course-number', num)
world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num)
def log_into_studio(
......@@ -158,21 +90,22 @@ def log_into_studio(
email='robot+studio@edx.org',
password='test',
is_staff=False):
create_studio_user(uname=uname, email=email, is_staff=is_staff)
world.browser.cookies.delete()
world.browser.visit(django_url('/'))
signin_css = 'a.action-signin'
world.browser.is_element_present_by_css(signin_css, 10)
world.visit('/')
# click the signin button
css_click(signin_css)
signin_css = 'a.action-signin'
world.is_css_present(signin_css)
world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
assert_true(world.browser.is_element_present_by_css('.new-course-button', 5))
assert_true(world.is_css_present('.new-course-button'))
def create_a_course():
......@@ -187,37 +120,37 @@ def create_a_course():
world.browser.reload()
course_link_css = 'span.class-name'
css_click(course_link_css)
world.css_click(course_link_css)
course_title_css = 'span.course-title'
assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
assert_true(world.is_css_present(course_title_css))
def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button'
css_click(link_css)
world.css_click(link_css)
name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save'
css_fill(name_css, name)
css_click(save_css)
world.css_fill(name_css, name)
world.css_click(save_css)
span_css = 'span.section-name-span'
assert_true(world.browser.is_element_present_by_css(span_css, 5))
assert_true(world.is_css_present(span_css))
def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item'
css_click(css)
world.css_click(css)
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
css_fill(name_css, name)
css_click(save_css)
world.css_fill(name_css, name)
world.css_click(save_css)
def set_date_and_time(date_css, desired_date, time_css, desired_time):
css_fill(date_css, desired_date)
world.css_fill(date_css, desired_date)
# hit TAB to get to the time field
e = css_find(date_css).first
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, desired_time)
e = css_find(time_css).first
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
\ No newline at end of file
time.sleep(float(1))
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys
import time
......@@ -25,9 +27,9 @@ DEFAULT_TIME = "12:00am"
def test_i_select_schedule_and_details(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css)
world.css_click(expand_icon_css)
link_css = 'li.nav-course-settings-schedule a'
css_click(link_css)
world.css_click(link_css)
@step('I have set course dates$')
......@@ -97,9 +99,9 @@ def test_i_clear_the_course_start_date(step):
@step('I receive a warning about course start date$')
def test_i_receive_a_warning_about_course_start_date(step):
assert_css_with_text('.message-error', 'The course must have an assigned start date.')
assert_true('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_true('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.'))
assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('The previously set start date is shown on refresh$')
......@@ -124,9 +126,9 @@ def test_i_have_entered_a_new_course_start_date(step):
@step('The warning about course start date goes away$')
def test_the_warning_about_course_start_date_goes_away(step):
assert_equal(0, len(css_find('.message-error')))
assert_false('error' in css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_false('error' in css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
assert_equal(0, len(world.css_find('.message-error')))
assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class'))
assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
@step('My new course start date is shown on refresh$')
......@@ -142,8 +144,8 @@ def set_date_or_time(css, date_or_time):
"""
Sets date or time field.
"""
css_fill(css, date_or_time)
e = css_find(css).first
world.css_fill(css, date_or_time)
e = world.css_find(css).first
# hit Enter to apply the changes
e._element.send_keys(Keys.ENTER)
......@@ -152,7 +154,7 @@ def verify_date_or_time(css, date_or_time):
"""
Verifies date or time field.
"""
assert_equal(date_or_time, css_find(css).first.value)
assert_equal(date_or_time, world.css_find(css).first.value)
def pause():
......
......@@ -10,4 +10,4 @@ Feature: Create Course
And I fill in the new course information
And I press the "Save" button
Then the Courseware page has loaded in Studio
And I see a link for adding a new section
\ No newline at end of file
And I see a link for adding a new section
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
......@@ -6,12 +9,12 @@ from common import *
@step('There are no courses$')
def no_courses(step):
clear_courses()
world.clear_courses()
@step('I click the New Course button$')
def i_click_new_course(step):
css_click('.new-course-button')
world.css_click('.new-course-button')
@step('I fill in the new course information$')
......@@ -27,7 +30,7 @@ def i_create_a_course(step):
@step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name'
css_click(course_css)
world.css_click(course_css)
############ ASSERTIONS ###################
......@@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step):
course_title_css = 'span.course-title'
assert world.browser.is_element_present_by_css(course_title_css)
assert world.is_css_present(course_title_css)
@step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name'
assert_css_with_text(course_css, 'Robot Super Course')
assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$')
def course_is_loaded(step):
class_css = 'a.class-name'
assert_css_with_text(class_css, 'Robot Super Course')
assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1'
assert_css_with_text(header_css, tab_name)
assert world.css_has_text(header_css, tab_name)
@step('I see a link for adding a new section$')
def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button'
assert_css_with_text(link_css, '+ New Section')
assert world.css_has_text(link_css, '+ New Section')
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_equal
......@@ -8,7 +11,7 @@ from nose.tools import assert_equal
@step('I click the new section link$')
def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button'
css_click(link_css)
world.css_click(link_css)
@step('I enter the section name and click save$')
......@@ -29,7 +32,7 @@ def i_have_added_new_section(step):
@step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button'
css_click(button_css)
world.css_click(button_css)
@step('I save a new section release date$')
......@@ -54,13 +57,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step):
@step('I click to edit the section name$')
def i_click_to_edit_section_name(step):
css_click('span.section-name-span')
world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step):
css = '.edit-section-name'
assert world.browser.is_element_present_by_css(css, 5)
assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
......@@ -75,7 +78,7 @@ def i_see_a_release_date_for_my_section(step):
import re
css = 'span.published-status'
assert world.browser.is_element_present_by_css(css)
assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text
# e.g. 11/06/2012 at 16:25
......@@ -89,20 +92,20 @@ def i_see_a_release_date_for_my_section(step):
@step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item'
assert world.browser.is_element_present_by_css(css)
assert world.is_css_present(css)
@step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings'
assert False, world.browser.find_by_css(css).visible
assert not world.css_visible(css)
@step('the section release date is updated$')
def the_section_release_date_is_updated(step):
css = 'span.published-status'
status_text = world.browser.find_by_css(css).text
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
status_text = world.css_text(css)
assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ###################
......@@ -110,10 +113,10 @@ def the_section_release_date_is_updated(step):
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
css_fill(name_css, name)
css_click(save_css)
world.css_fill(name_css, name)
world.css_click(save_css)
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
assert_css_with_text(section_css, name)
assert world.css_has_text(section_css, name)
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
......@@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = css_find(submit_css)
e = world.css_find(submit_css)
e.type(' ')
@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper')
......
Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author
I want to toggle the visibility of each section's subsection details in the overview listing
As a course author
I want to toggle the visibility of each section's subsection details in the overview listing
Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
When I navigate to the course overview page
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Expand/collapse for a course with no sections
Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
When I navigate to the course overview page
And I add a section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page
And I navigate to the course overview page
When I press the "section" delete icon
And I confirm the alert
Then I see the "Collapse All Sections" link
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_true, assert_false, assert_equal
......@@ -8,13 +11,13 @@ logger = getLogger(__name__)
@step(u'I have a course with no sections$')
def have_a_course(step):
clear_courses()
world.clear_courses()
course = world.CourseFactory.create()
@step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step):
clear_courses()
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
......@@ -25,7 +28,7 @@ def have_a_course_with_1_section(step):
@step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step):
clear_courses()
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
......@@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step):
def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True)
course_locator = '.class-name'
css_click(course_locator)
world.css_click(course_locator)
@step(u'I navigate to the courseware page of a course with multiple sections')
......@@ -66,44 +69,44 @@ def i_add_a_section(step):
@step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator, 5))
assert_true(world.browser.is_element_present_by_css(span_locator))
# first make sure that the expand/collapse text is the one you expected
assert_equal(world.browser.find_by_css(span_locator).value, text)
css_click(span_locator)
world.css_click(span_locator)
@step(u'I collapse the first section$')
def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse'
css_click(collapse_locator)
world.css_click(collapse_locator)
@step(u'I expand the first section$')
def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand'
css_click(expand_locator)
world.css_click(expand_locator)
@step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator, 5))
assert_equal(world.browser.find_by_css(span_locator).value, text)
assert_true(world.browser.find_by_css(span_locator).visible)
assert_true(world.is_css_present(span_locator))
assert_equal(world.css_find(span_locator).value, text)
assert_true(world.css_visible(span_locator))
@step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible
span_locator = '.toggle-button-sections span'
assert_true(world.browser.is_element_present_by_css(span_locator))
assert_false(world.browser.find_by_css(span_locator).visible)
assert_true(world.is_css_present(span_locator))
assert_false(world.css_visible(span_locator))
@step(u'all sections are expanded$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator)
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_true(s.visible)
......@@ -111,6 +114,6 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$')
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
subsections = world.browser.find_by_css(subsection_locator)
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_false(s.visible)
......@@ -3,28 +3,27 @@ Feature: Create Subsection
As a course author
I want to create and edit subsections
# Scenario: Add a new subsection to a section
# Given I have opened a new course section in Studio
# When I click the New Subsection link
# And I enter the subsection name and click save
# Then I see my subsection on the Courseware page
#
# Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
# Given I have opened a new course section in Studio
# When I click the New Subsection link
# And I enter a subsection name with a quote and click save
# Then I see my subsection name with a quote on the Courseware page
# And I click to edit the subsection name
# Then I see the complete subsection name with a quote in the editor
Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
# @skip-phantom
# Scenario: Delete a subsection
# Given I have opened a new course section in Studio
# And I have added a new subsection
# And I see my subsection on the Courseware page
# When I press the "subsection" delete icon
# And I confirm the alert
# Then the subsection does not exist
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter a subsection name with a quote and click save
Then I see my subsection name with a quote on the Courseware page
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
Given I have opened a new course section in Studio
And I have added a new subsection
And I mark it as Homework
Then I see it marked as Homework
And I reload the page
Then I see it marked as Homework
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
......@@ -32,4 +31,14 @@ Feature: Create Subsection
Then I see the correct dates
And I reload the page
Then I see the correct dates
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection
And I see my subsection on the Courseware page
When I press the "subsection" delete icon
And I confirm the alert
Then the subsection does not exist
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
from nose.tools import assert_equal, assert_true
......@@ -7,7 +10,7 @@ from nose.tools import assert_equal, assert_true
@step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step):
clear_courses()
world.clear_courses()
log_into_studio()
create_a_course()
add_section()
......@@ -27,8 +30,7 @@ def i_have_opened_a_new_subsection(step):
@step('I click the New Subsection link')
def i_click_the_new_subsection_link(step):
css = 'a.new-subsection-item'
css_click(css)
world.css_click('a.new-subsection-item')
@step('I enter the subsection name and click save$')
......@@ -43,14 +45,14 @@ def i_save_subsection_name_with_quote(step):
@step('I click to edit the subsection name$')
def i_click_to_edit_subsection_name(step):
css_click('span.subsection-name-value')
world.css_click('span.subsection-name-value')
@step('I see the complete subsection name with a quote in the editor$')
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
assert world.browser.is_element_present_by_css(css, 5)
assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
assert world.is_css_present(css)
assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have set a release date and due date in different years$')
......@@ -68,6 +70,17 @@ def i_see_the_correct_dates(step):
assert_equal('4:00am', css_find('input#due_time').first.value)
@step('I mark it as Homework$')
def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle')
world.browser.click_link_by_text('Homework')
@step('I see it marked as Homework$')
def i_see_it_marked__as_homework(step):
assert_equal(world.css_find(".status-label").value, 'Homework')
############ ASSERTIONS ###################
......@@ -92,11 +105,12 @@ def the_subsection_does_not_exist(step):
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'
css_fill(name_css, name)
css_click(save_css)
world.css_fill(name_css, name)
world.css_click(save_css)
def see_subsection_name(name):
css = 'span.subsection-name'
assert world.browser.is_element_present_by_css(css)
assert world.is_css_present(css)
css = 'span.subsection-name-value'
assert_css_with_text(css, name)
assert world.css_has_text(css, name)
......@@ -113,6 +113,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.metadata_inheritance_cache = cache
store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
......
import threading
_request_cache_threadlocal = threading.local()
_request_cache_threadlocal.data = {}
class RequestCache(object):
@classmethod
def get_request_cache(cls):
return _request_cache_threadlocal
def clear_request_cache(self):
_request_cache_threadlocal.data = {}
def process_request(self, request):
self.clear_request_cache()
return None
def process_response(self, request, response):
self.clear_request_cache()
return response
\ No newline at end of file
......@@ -325,7 +325,12 @@ def change_enrollment(request):
"course:{0}".format(course_num),
"run:{0}".format(run)])
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
try:
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
except IntegrityError:
# If we've already created this enrollment in a separate transaction,
# then just continue
pass
return {'success': True}
elif action == "unenroll":
......@@ -369,14 +374,14 @@ def login_user(request, error=""):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
log.warning("Login failed - Unknown user email: {0}".format(email))
log.warning(u"Login failed - Unknown user email: {0}".format(email))
return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) # TODO: User error message
username = user.username
user = authenticate(username=username, password=password)
if user is None:
log.warning("Login failed - password for {0} is invalid".format(email))
log.warning(u"Login failed - password for {0} is invalid".format(email))
return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'}))
......@@ -392,7 +397,7 @@ def login_user(request, error=""):
log.critical("Login failed - Could not create session. Is memcached running?")
log.exception(e)
log.info("Login success - {0} ({1})".format(username, email))
log.info(u"Login success - {0} ({1})".format(username, email))
try_change_enrollment(request)
......@@ -400,7 +405,7 @@ def login_user(request, error=""):
return HttpResponse(json.dumps({'success': True}))
log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from .factories import *
from django.conf import settings
from django.http import HttpRequest
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from bs4 import BeautifulSoup
import os.path
from urllib import quote_plus
from lettuce.django import django_url
@world.absorb
def create_user(uname):
# If the user already exists, don't try to create it again
if len(User.objects.filter(username=uname)) > 0:
return
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test')
portal_user.save()
registration = world.RegistrationFactory(user=portal_user)
registration.register(portal_user)
registration.activate()
user_profile = world.UserProfileFactory(user=portal_user)
@world.absorb
def log_in(username, password):
'''
Log the user in programatically
'''
# Authenticate the user
user = authenticate(username=username, password=password)
assert(user is not None and user.is_active)
# Send a fake HttpRequest to log the user in
# We need to process the request using
# Session middleware and Authentication middleware
# to ensure that session state can be stored
request = HttpRequest()
SessionMiddleware().process_request(request)
AuthenticationMiddleware().process_request(request)
login(request, user)
# Save the session
request.session.save()
# Retrieve the sessionid and add it to the browser's cookies
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
try:
world.browser.cookies.add(cookie_dict)
# WebDriver has an issue where we cannot set cookies
# before we make a GET request, so if we get an error,
# we load the '/' page and try again
except:
world.browser.visit(django_url('/'))
world.browser.cookies.add(cookie_dict)
@world.absorb
def register_by_course_id(course_id, is_staff=False):
create_user('robot')
u = User.objects.get(username='robot')
if is_staff:
u.is_staff = True
u.save()
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
@world.absorb
def save_the_course_content(path='/tmp'):
html = world.browser.html.encode('ascii', 'ignore')
soup = BeautifulSoup(html)
# get rid of the header, we only want to compare the body
soup.head.decompose()
# for now, remove the data-id attributes, because they are
# causing mismatches between cms-master and master
for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
del item['data-id']
# we also need to remove them from unrendered problems,
# where they are contained in the text of divs instead of
# in attributes of tags
# Be careful of whether or not it was the last attribute
# and needs a trailing space
for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
for item in soup.find_all(text=re.compile(' data-id=".*?"')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
# prettify the html so it will compare better, with
# each HTML tag on its own line
output = soup.prettify()
# use string slicing to grab everything after 'courseware/' in the URL
u = world.browser.url
section_url = u[u.find('courseware/') + 11:]
if not os.path.exists(path):
os.makedirs(path)
filename = '%s.html' % (quote_plus(section_url))
f = open('%s/%s' % (path, filename), 'w')
f.write(output)
f.close
@world.absorb
def clear_courses():
# 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()
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from .factories import *
from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
from django.conf import settings
from django.http import HttpRequest
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment
from urllib import quote_plus
from nose.tools import assert_equals
from bs4 import BeautifulSoup
from nose.tools import assert_equals, assert_in
import time
import re
import os.path
from selenium.common.exceptions import WebDriverException
from logging import getLogger
logger = getLogger(__name__)
......@@ -22,7 +14,7 @@ logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds):
time.sleep(float(seconds))
world.wait(seconds)
@step('I reload the page$')
......@@ -37,42 +29,42 @@ def browser_back(step):
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
world.browser.visit(django_url('/'))
assert world.browser.is_element_present_by_css('header.global', 10)
world.visit('/')
assert world.is_css_present('header.global')
@step(u'I (?:visit|access|open) the dashboard$')
def i_visit_the_dashboard(step):
world.browser.visit(django_url('/dashboard'))
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
world.visit('/dashboard')
assert world.is_css_present('section.container.dashboard')
@step('I should be on the dashboard page$')
def i_should_be_on_the_dashboard(step):
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
assert world.is_css_present('section.container.dashboard')
assert world.browser.title == 'Dashboard'
@step(u'I (?:visit|access|open) the courses page$')
def i_am_on_the_courses_page(step):
world.browser.visit(django_url('/courses'))
assert world.browser.is_element_present_by_css('section.courses')
world.visit('/courses')
assert world.is_css_present('section.courses')
@step(u'I press the "([^"]*)" button$')
def and_i_press_the_button(step, value):
button_css = 'input[value="%s"]' % value
world.browser.find_by_css(button_css).first.click()
world.css_click(button_css)
@step(u'I click the link with the text "([^"]*)"$')
def click_the_link_with_the_text_group1(step, linktext):
world.browser.find_link_by_text(linktext).first.click()
world.click_link(linktext)
@step('I should see that the path is "([^"]*)"$')
def i_should_see_that_the_path_is(step, path):
assert world.browser.url == django_url(path)
assert world.url_equals(path)
@step(u'the page title should be "([^"]*)"$')
......@@ -85,10 +77,15 @@ def the_page_title_should_contain(step, title):
assert(title in world.browser.title)
@step('I log in$')
def i_log_in(step):
world.log_in('robot', 'test')
@step('I am a logged in user$')
def i_am_logged_in_user(step):
create_user('robot')
log_in('robot', 'test')
world.create_user('robot')
world.log_in('robot', 'test')
@step('I am not logged in$')
......@@ -98,151 +95,46 @@ def i_am_not_logged_in(step):
@step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id):
register_by_course_id(course_id, True)
world.register_by_course_id(course_id, True)
@step('I log in$')
def i_log_in(step):
log_in('robot', 'test')
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
def click_the_link_called(step, text):
world.click_link(text)
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
create_user('robot')
@step(r'should see that the url is "([^"]*)"$')
def should_have_the_url(step, url):
assert_equals(world.browser.url, url)
#### helper functions
@world.absorb
def scroll_to_bottom():
# Maximize the browser
world.browser.execute_script("window.scrollTo(0, screen.height);")
@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
def should_see_a_link_called(step, text):
assert len(world.browser.find_link_by_text(text)) > 0
@world.absorb
def create_user(uname):
# If the user already exists, don't try to create it again
if len(User.objects.filter(username=uname)) > 0:
return
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
def should_see_in_the_page(step, text):
assert_in(text, world.css_text('body'))
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test')
portal_user.save()
registration = world.RegistrationFactory(user=portal_user)
registration.register(portal_user)
registration.activate()
@step('I am logged in$')
def i_am_logged_in(step):
world.create_user('robot')
world.log_in('robot', 'test')
world.browser.visit(django_url('/'))
user_profile = world.UserProfileFactory(user=portal_user)
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
@world.absorb
def log_in(username, password):
'''
Log the user in programatically
'''
# Authenticate the user
user = authenticate(username=username, password=password)
assert(user is not None and user.is_active)
# Send a fake HttpRequest to log the user in
# We need to process the request using
# Session middleware and Authentication middleware
# to ensure that session state can be stored
request = HttpRequest()
SessionMiddleware().process_request(request)
AuthenticationMiddleware().process_request(request)
login(request, user)
# Save the session
request.session.save()
# Retrieve the sessionid and add it to the browser's cookies
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
try:
world.browser.cookies.add(cookie_dict)
# WebDriver has an issue where we cannot set cookies
# before we make a GET request, so if we get an error,
# we load the '/' page and try again
except:
world.browser.visit(django_url('/'))
world.browser.cookies.add(cookie_dict)
@world.absorb
def register_by_course_id(course_id, is_staff=False):
create_user('robot')
u = User.objects.get(username='robot')
if is_staff:
u.is_staff = True
u.save()
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
@world.absorb
def save_the_html(path='/tmp'):
u = world.browser.url
html = world.browser.html.encode('ascii', 'ignore')
filename = '%s.html' % quote_plus(u)
f = open('%s/%s' % (path, filename), 'w')
f.write(html)
f.close
@world.absorb
def save_the_course_content(path='/tmp'):
html = world.browser.html.encode('ascii', 'ignore')
soup = BeautifulSoup(html)
# get rid of the header, we only want to compare the body
soup.head.decompose()
# for now, remove the data-id attributes, because they are
# causing mismatches between cms-master and master
for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
del item['data-id']
# we also need to remove them from unrendered problems,
# where they are contained in the text of divs instead of
# in attributes of tags
# Be careful of whether or not it was the last attribute
# and needs a trailing space
for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
for item in soup.find_all(text=re.compile(' data-id=".*?"')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
# prettify the html so it will compare better, with
# each HTML tag on its own line
output = soup.prettify()
# use string slicing to grab everything after 'courseware/' in the URL
u = world.browser.url
section_url = u[u.find('courseware/') + 11:]
if not os.path.exists(path):
os.makedirs(path)
filename = '%s.html' % (quote_plus(section_url))
f = open('%s/%s' % (path, filename), 'w')
f.write(output)
f.close
@world.absorb
def css_click(css_selector):
try:
world.browser.find_by_css(css_selector).click()
except WebDriverException:
# Occassionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again
time.sleep(1)
world.browser.find_by_css(css_selector).click()
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
import time
from urllib import quote_plus
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
@world.absorb
def wait(seconds):
time.sleep(float(seconds))
@world.absorb
def wait_for(func):
WebDriverWait(world.browser.driver, 5).until(func)
@world.absorb
def visit(url):
world.browser.visit(django_url(url))
@world.absorb
def url_equals(url):
return world.browser.url == django_url(url)
@world.absorb
def is_css_present(css_selector):
return world.browser.is_element_present_by_css(css_selector, wait_time=4)
@world.absorb
def css_has_text(css_selector, text):
return world.css_text(css_selector) == text
@world.absorb
def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css)
@world.absorb
def css_click(css_selector):
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
world.browser.find_by_css(css_selector).click()
except WebDriverException:
# Occassionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again
time.sleep(1)
world.browser.find_by_css(css_selector).click()
@world.absorb
def css_click_at(css, x=10, y=10):
'''
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
e = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
@world.absorb
def css_fill(css_selector, text):
world.browser.find_by_css(css_selector).first.fill(text)
@world.absorb
def click_link(partial_text):
world.browser.find_link_by_partial_text(partial_text).first.click()
@world.absorb
def css_text(css_selector):
# Wait for the css selector to appear
if world.is_css_present(css_selector):
return world.browser.find_by_css(css_selector).first.text
else:
return ""
@world.absorb
def css_visible(css_selector):
return world.browser.find_by_css(css_selector).visible
@world.absorb
def save_the_html(path='/tmp'):
u = world.browser.url
html = world.browser.html.encode('ascii', 'ignore')
filename = '%s.html' % quote_plus(u)
f = open('%s/%s' % (path, filename), 'w')
f.write(html)
f.close
......@@ -109,7 +109,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
references to metadata_inheritance_tree
"""
def __init__(self, modulestore, module_data, default_class, resources_fs,
error_tracker, render_template, metadata_cache=None):
error_tracker, render_template, cached_metadata=None):
"""
modulestore: the module store that can be used to retrieve additional modules
......@@ -134,7 +134,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# cdodge: other Systems have a course_id attribute defined. To keep things consistent, let's
# define an attribute here as well, even though it's None
self.course_id = None
self.metadata_cache = metadata_cache
self.cached_metadata = cached_metadata
def load_item(self, location):
"""
......@@ -170,8 +171,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
module = class_(self, location, model_data)
if self.metadata_cache is not None:
metadata_to_inherit = self.metadata_cache.get(metadata_cache_key(location), {}).get('parent_metadata', {}).get(location.url(), {})
if self.cached_metadata is not None:
metadata_to_inherit = self.cached_metadata.get(location.url(), {})
inherit_metadata(module, metadata_to_inherit)
return module
except:
......@@ -223,7 +224,8 @@ class MongoModuleStore(ModuleStoreBase):
def __init__(self, host, db, collection, fs_root, render_template,
port=27017, default_class=None,
error_tracker=null_error_tracker,
user=None, password=None, **kwargs):
user=None, password=None, request_cache=None,
metadata_inheritance_cache_subsystem=None, **kwargs):
ModuleStoreBase.__init__(self)
......@@ -254,8 +256,10 @@ class MongoModuleStore(ModuleStoreBase):
self.error_tracker = error_tracker
self.render_template = render_template
self.ignore_write_events_on_courses = []
self.request_cache = request_cache
self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem
def get_metadata_inheritance_tree(self, location):
def compute_metadata_inheritance_tree(self, location):
'''
TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
'''
......@@ -323,32 +327,45 @@ class MongoModuleStore(ModuleStoreBase):
if root is not None:
_compute_inherited_metadata(root)
return {'parent_metadata': metadata_to_inherit,
'timestamp': datetime.now()}
return metadata_to_inherit
def get_cached_metadata_inheritance_trees(self, locations, force_refresh=False):
def get_cached_metadata_inheritance_tree(self, location, force_refresh=False):
'''
TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
'''
trees = {}
if locations and self.metadata_inheritance_cache is not None and not force_refresh:
trees = self.metadata_inheritance_cache.get_many(list(set([metadata_cache_key(loc) for loc in locations])))
else:
# This is to help guard against an accident prod runtime without a cache
logging.warning('Running MongoModuleStore without metadata_inheritance_cache. '
'This should not happen in production!')
to_cache = {}
for loc in locations:
cache_key = metadata_cache_key(loc)
if cache_key not in trees:
to_cache[cache_key] = trees[cache_key] = self.get_metadata_inheritance_tree(loc)
if to_cache and self.metadata_inheritance_cache is not None:
self.metadata_inheritance_cache.set_many(to_cache)
return trees
key = metadata_cache_key(location)
tree = {}
if not force_refresh:
# see if we are first in the request cache (if present)
if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}):
return self.request_cache.data['metadata_inheritance'][key]
# then look in any caching subsystem (e.g. memcached)
if self.metadata_inheritance_cache_subsystem is not None:
tree = self.metadata_inheritance_cache_subsystem.get(key, {})
else:
logging.warning('Running MongoModuleStore without a metadata_inheritance_cache_subsystem. This is OK in localdev and testing environment. Not OK in production.')
if not tree:
# if not in subsystem, or we are on force refresh, then we have to compute
tree = self.compute_metadata_inheritance_tree(location)
# now write out computed tree to caching subsystem (e.g. memcached), if available
if self.metadata_inheritance_cache_subsystem is not None:
self.metadata_inheritance_cache_subsystem.set(key, tree)
# now populate a request_cache, if available. NOTE, we are outside of the
# scope of the above if: statement so that after a memcache hit, it'll get
# put into the request_cache
if self.request_cache is not None:
# we can't assume the 'metadatat_inheritance' part of the request cache dict has been
# defined
if 'metadata_inheritance' not in self.request_cache.data:
self.request_cache.data['metadata_inheritance'] = {}
self.request_cache.data['metadata_inheritance'][key] = tree
return tree
def refresh_cached_metadata_inheritance_tree(self, location):
"""
......@@ -357,15 +374,7 @@ class MongoModuleStore(ModuleStoreBase):
"""
pseudo_course_id = '/'.join([location.org, location.course])
if pseudo_course_id not in self.ignore_write_events_on_courses:
self.get_cached_metadata_inheritance_trees([location], force_refresh=True)
def clear_cached_metadata_inheritance_tree(self, location):
"""
Delete the cached metadata inheritance tree for the org/course combination
for location
"""
if self.metadata_inheritance_cache is not None:
self.metadata_inheritance_cache.delete(metadata_cache_key(location))
self.get_cached_metadata_inheritance_tree(location, force_refresh=True)
def _clean_item_data(self, item):
"""
......@@ -411,18 +420,7 @@ class MongoModuleStore(ModuleStoreBase):
return data
def _cache_metadata_inheritance(self, items, depth, force_refresh=False):
"""
Retrieves all course metadata inheritance trees needed to load items
"""
locations = [
Location(item['location']) for item in items
if not (item['location']['category'] == 'course' and depth == 0)
]
return self.get_cached_metadata_inheritance_trees(locations, force_refresh=force_refresh)
def _load_item(self, item, data_cache, metadata_cache):
def _load_item(self, item, data_cache, apply_cached_metadata=True):
"""
Load an XModuleDescriptor from item, using the children stored in data_cache
"""
......@@ -434,6 +432,10 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root)
cached_metadata = {}
if apply_cached_metadata:
cached_metadata = self.get_cached_metadata_inheritance_tree(Location(item['location']))
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter
system = CachingDescriptorSystem(
......@@ -443,7 +445,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs,
self.error_tracker,
self.render_template,
metadata_cache,
cached_metadata,
)
return system.load_item(item['location'])
......@@ -453,11 +455,11 @@ class MongoModuleStore(ModuleStoreBase):
to specified depth
"""
data_cache = self._cache_children(items, depth)
inheritance_cache = self._cache_metadata_inheritance(items, depth)
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
# bother with the metadata inheritence
return [self._load_item(item, data_cache, inheritance_cache) for item in items]
# bother with the metadata inheritance
return [self._load_item(item, data_cache,
apply_cached_metadata=(item['location']['category']!='course' or depth !=0)) for item in items]
def get_courses(self):
'''
......
......@@ -103,58 +103,3 @@ class TestMongoModuleStore(object):
def test_path_to_location(self):
'''Make sure that path_to_location works'''
check_path_to_location(self.store)
def test_metadata_inheritance_query_count(self):
'''
When retrieving items from mongo, we should only query the cache a number of times
equal to the number of courses being retrieved from.
We should also not query
'''
self.store.metadata_inheritance_cache = Mock()
get_many = self.store.metadata_inheritance_cache.get_many
set_many = self.store.metadata_inheritance_cache.set_many
get_many.return_value = {('edX', 'toy'): {}}
self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=0)
assert_false(get_many.called)
assert_false(set_many.called)
get_many.reset_mock()
self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=3)
get_many.assert_called_with([('edX', 'toy')])
assert_equals(0, set_many.call_count)
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=0)
assert_false(get_many.called)
assert_false(set_many.called)
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=3)
assert_equals(1, get_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0]))
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys()))
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, None, None), depth=0)
assert_equals(1, get_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0]))
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys()))
get_many.reset_mock()
def test_metadata_inheritance_query_count_forced_refresh(self):
self.store.metadata_inheritance_cache = Mock()
get_many = self.store.metadata_inheritance_cache.get_many
set_many = self.store.metadata_inheritance_cache.set_many
get_many.return_value = {('edX', 'toy'): {}}
self.store.get_cached_metadata_inheritance_trees(
[Location("i4x://edX/toy/course/2012_Fall"), Location("i4x://edX/simple/course/2012_Fall")],
True
)
assert_false(get_many.called)
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(set_many.call_args[0][0].keys()))
/* This file defines a processor in between the student's math input
(AsciiMath) and what is read by MathJax. It allows for our own
customizations, such as use of the syntax "a_b__x" in superscripts, or
possibly coloring certain variables, etc&.
It is used in the <textline> definition like the following:
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
*/
window.SymbolicMathjaxPreprocessor = function () {
this.fn = function (eqn) {
// flags and config
var superscriptsOn = true;
if (superscriptsOn) {
// find instances of "__" and make them superscripts ("^") and tag them
// as such. Specifcally replace instances of "__X" or "__{XYZ}" with
// "^{CHAR$1}", marking superscripts as different from powers
// a zero width space--this is an invisible character that no one would
// use, that gets passed through MathJax and to the server
var c = "\u200b";
eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}');
// NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath
// input, which is too bad. This would be preferable to this char tag
}
return eqn;
};
};
#################
Symbolic Response
#################
This document plans to document features that the current symbolic response
supports. In general it allows the input and validation of math expressions,
up to commutativity and some identities.
********
Features
********
This is a partial list of features, to be revised as we go along:
* sub and superscripts: an expression following the ``^`` character
indicates exponentiation. To use superscripts in variables, the syntax
is ``b_x__d`` for the variable ``b`` with subscript ``x`` and super
``d``.
An example of a problem::
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
It's a bit of a pain to enter that.
* The script-style math variant. What would be outputted in latex if you
entered ``\mathcal{N}``. This is used in some variables.
An example::
<symbolicresponse expect="scriptN_B + x" size="30">
<textline math="1"/>
</symbolicresponse>
There is no fancy preprocessing needed, but if you had superscripts or
something, you would need to include that part.
......@@ -3,13 +3,11 @@ from django.test.utils import override_settings
import xmodule.modulestore.django
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class WikiRedirectTestCase(PageLoader):
class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
......@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader):
self.activate_user(self.student)
self.activate_user(self.instructor)
def test_wiki_redirect(self):
"""
Test that requesting wiki URLs redirect properly to or out of classes.
......@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp['Location'], 'http://testserver' + destination)
def create_course_page(self, course):
"""
Test that loading the course wiki page creates the wiki page.
......@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertTrue("course info" in resp.content.lower())
self.assertTrue("courseware" in resp.content.lower())
def test_course_navigator(self):
""""
Test that going from a course page to a wiki page contains the course navigator.
......@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader):
self.enroll(self.toy)
self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={'course_id': self.toy.id})
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_equals, assert_in
from lettuce.django import django_url
......@@ -6,83 +9,13 @@ from student.models import CourseEnrollment
from xmodule.modulestore import Location
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
import time
from xmodule.course_module import CourseDescriptor
from courseware.courses import get_course_by_id
from xmodule import seq_module, vertical_module
from logging import getLogger
logger = getLogger(__name__)
@step(u'I wait (?:for )?"(\d+)" seconds?$')
def wait(step, seconds):
time.sleep(float(seconds))
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
world.browser.visit(django_url('/'))
assert world.browser.is_element_present_by_css('header.global', 10)
@step(u'I (?:visit|access|open) the dashboard$')
def i_visit_the_dashboard(step):
world.browser.visit(django_url('/dashboard'))
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
def click_the_link_called(step, text):
world.browser.find_link_by_text(text).click()
@step('I should be on the dashboard page$')
def i_should_be_on_the_dashboard(step):
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
assert world.browser.title == 'Dashboard'
@step(u'I (?:visit|access|open) the courses page$')
def i_am_on_the_courses_page(step):
world.browser.visit(django_url('/courses'))
assert world.browser.is_element_present_by_css('section.courses')
@step('I should see that the path is "([^"]*)"$')
def i_should_see_that_the_path_is(step, path):
assert world.browser.url == django_url(path)
@step(u'the page title should be "([^"]*)"$')
def the_page_title_should_be(step, title):
assert world.browser.title == title
@step(r'should see that the url is "([^"]*)"$')
def should_have_the_url(step, url):
assert_equals(world.browser.url, url)
@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
def should_see_a_link_called(step, text):
assert len(world.browser.find_link_by_text(text)) > 0
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
def should_see_in_the_page(step, text):
assert_in(text, world.browser.html)
@step('I am logged in$')
def i_am_logged_in(step):
world.create_user('robot')
world.log_in('robot', 'test')
world.browser.visit(django_url('/'))
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
TEST_COURSE_ORG = 'edx'
TEST_COURSE_NAME = 'Test Course'
TEST_SECTION_NAME = "Problem"
......@@ -94,7 +27,7 @@ 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()
world.clear_courses()
# Create the course
# We always use the same org and display name,
......@@ -135,29 +68,6 @@ def add_tab_to_course(step, course, extra_tab_name):
display_name=str(extra_tab_name))
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
@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(" ", "_"))
......@@ -177,3 +87,87 @@ def section_location(course_num):
course=course_num,
category='sequential',
name=TEST_SECTION_NAME.replace(" ", "_"))
def get_courses():
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number.
'''
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
return courses
def get_courseware_with_tabs(course_id):
"""
Given a course_id (string), return a courseware array of dictionaries for the
top three levels of navigation. Same as get_courseware() except include
the tabs on the right hand main navigation page.
This hides the appropriate courseware as defined by the hide_from_toc field:
chapter.lms.hide_from_toc
Example:
[{
'chapter_name': 'Overview',
'sections': [{
'clickable_tab_count': 0,
'section_name': 'Welcome',
'tab_classes': []
}, {
'clickable_tab_count': 1,
'section_name': 'System Usage Sequence',
'tab_classes': ['VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Lab0: Using the tools',
'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Circuit Sandbox',
'tab_classes': []
}]
}, {
'chapter_name': 'Week 1',
'sections': [{
'clickable_tab_count': 4,
'section_name': 'Administrivia and Circuit Elements',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Basic Circuit Analysis',
'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Resistor Divider',
'tab_classes': []
}, {
'clickable_tab_count': 0,
'section_name': 'Week 1 Tutorials',
'tab_classes': []
}]
}, {
'chapter_name': 'Midterm Exam',
'sections': [{
'clickable_tab_count': 2,
'section_name': 'Midterm Exam',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
}]
}]
"""
course = get_course_by_id(course_id)
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
courseware = [{'chapter_name': c.display_name_with_default,
'sections': [{'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
'class': t.__class__.__name__}
for t in s.get_children()]}
for s in c.get_children() if not s.lms.hide_from_toc]}
for c in chapters]
return courseware
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
@step('I click on View Courseware')
def i_click_on_view_courseware(step):
css = 'a.enter-course'
world.browser.find_by_css(css).first.click()
world.css_click('a.enter-course')
@step('I click on the "([^"]*)" tab$')
def i_click_on_the_tab(step, tab):
world.browser.find_link_by_partial_text(tab).first.click()
def i_click_on_the_tab(step, tab_text):
world.click_link(tab_text)
world.save_the_html()
@step('I visit the courseware URL$')
def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
world.visit('/courses/MITx/6.002x/2012_Fall/courseware')
@step(u'I do not see "([^"]*)" anywhere on the page')
......@@ -27,18 +27,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text):
@step(u'I am on the dashboard page$')
def i_am_on_the_dashboard_page(step):
assert world.browser.is_element_present_by_css('section.courses')
assert world.browser.url == django_url('/dashboard')
assert world.is_css_present('section.courses')
assert world.url_equals('/dashboard')
@step('the "([^"]*)" tab is active$')
def the_tab_is_active(step, tab):
css = '.course-tabs a.active'
active_tab = world.browser.find_by_css(css)
assert (active_tab.text == tab)
def the_tab_is_active(step, tab_text):
assert world.css_text('.course-tabs a.active') == tab_text
@step('the login dialog is visible$')
def login_dialog_visible(step):
css = 'form#login_form.login_form'
assert world.browser.find_by_css(css).visible
assert world.css_visible('form#login_form.login_form')
......@@ -3,7 +3,7 @@ Feature: All the high level tabs should work
As a student
I want to navigate through the high level tabs
Scenario: I can navigate to all high -level tabs in a course
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 am logged in
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_in
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import step, world
from django.contrib.auth.models import User
......@@ -28,9 +31,7 @@ def i_should_see_the_login_error_message(step, msg):
@step(u'click the dropdown arrow$')
def click_the_dropdown(step):
css = ".dropdown"
e = world.browser.find_by_css(css)
e.click()
world.css_click('.dropdown')
#### helper functions
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
from nose.tools import assert_equals, assert_in
......@@ -12,7 +15,7 @@ def navigate_to_an_openended_question(step):
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
world.css_click(tab_css)
@step('I navigate to an openended question as staff$')
......@@ -22,81 +25,69 @@ def navigate_to_an_openended_question_as_staff(step):
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
world.css_click(tab_css)
@step(u'I enter the answer "([^"]*)"$')
def enter_the_answer_text(step, text):
textarea_css = 'textarea'
world.browser.find_by_css(textarea_css).first.fill(text)
world.css_fill('textarea', text)
@step(u'I submit the answer "([^"]*)"$')
def i_submit_the_answer_text(step, text):
textarea_css = 'textarea'
world.browser.find_by_css(textarea_css).first.fill(text)
check_css = 'input.check'
world.browser.find_by_css(check_css).click()
world.css_fill('textarea', text)
world.css_click('input.check')
@step('I click the link for full output$')
def click_full_output_link(step):
link_css = 'a.full'
world.browser.find_by_css(link_css).first.click()
world.css_click('a.full')
@step(u'I visit the staff grading page$')
def i_visit_the_staff_grading_page(step):
# course_u = '/courses/MITx/3.091x/2012_Fall'
# sg_url = '%s/staff_grading' % course_u
world.browser.click_link_by_text('Instructor')
world.browser.click_link_by_text('Staff grading')
# world.browser.visit(django_url(sg_url))
world.click_link('Instructor')
world.click_link('Staff grading')
@step(u'I see the grader message "([^"]*)"$')
def see_grader_message(step, msg):
message_css = 'div.external-grader-message'
grader_msg = world.browser.find_by_css(message_css).text
assert_in(msg, grader_msg)
assert_in(msg, world.css_text(message_css))
@step(u'I see the grader status "([^"]*)"$')
def see_the_grader_status(step, status):
status_css = 'div.grader-status'
grader_status = world.browser.find_by_css(status_css).text
assert_equals(status, grader_status)
assert_equals(status, world.css_text(status_css))
@step('I see the red X$')
def see_the_red_x(step):
x_css = 'div.grader-status > span.incorrect'
assert world.browser.find_by_css(x_css)
assert world.is_css_present('div.grader-status > span.incorrect')
@step(u'I see the grader score "([^"]*)"$')
def see_the_grader_score(step, score):
score_css = 'div.result-output > p'
score_text = world.browser.find_by_css(score_css).text
score_text = world.css_text(score_css)
assert_equals(score_text, 'Score: %s' % score)
@step('I see the link for full output$')
def see_full_output_link(step):
link_css = 'a.full'
assert world.browser.find_by_css(link_css)
assert world.is_css_present('a.full')
@step('I see the spelling grading message "([^"]*)"$')
def see_spelling_msg(step, msg):
spelling_css = 'div.spelling'
spelling_msg = world.browser.find_by_css(spelling_css).text
spelling_msg = world.css_text('div.spelling')
assert_equals('Spelling: %s' % msg, spelling_msg)
@step(u'my answer is queued for instructor grading$')
def answer_is_queued_for_instructor_grading(step):
list_css = 'ul.problem-list > li > a'
actual_msg = world.browser.find_by_css(list_css).text
actual_msg = world.css_text(list_css)
expected_msg = "(0 graded, 1 pending)"
assert_in(expected_msg, actual_msg)
......@@ -2,6 +2,8 @@
Steps for problem.feature lettuce tests
'''
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
......@@ -339,7 +341,7 @@ def assert_answer_mark(step, problem_type, correctness):
# At least one of the correct selectors should be present
for sel in selector_dict[problem_type]:
has_expected = world.browser.is_element_present_by_css(sel, wait_time=4)
has_expected = world.is_css_present(sel)
# As soon as we find the selector, break out of the loop
if has_expected:
......@@ -366,7 +368,7 @@ def inputfield(problem_type, choice=None, input_num=1):
# If the input element doesn't exist, fail immediately
assert(world.browser.is_element_present_by_css(sel, wait_time=4))
assert world.is_css_present(sel)
# Retrieve the input element
return world.browser.find_by_css(sel)
......
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
from common import TEST_COURSE_ORG, TEST_COURSE_NAME
......@@ -13,17 +16,17 @@ def i_register_for_the_course(step, course):
register_link = intro_section.find_by_css('a.register')
register_link.click()
assert world.browser.is_element_present_by_css('section.container.dashboard')
assert world.is_css_present('section.container.dashboard')
@step(u'I should see the course numbered "([^"]*)" in my dashboard$')
def i_should_see_that_course_in_my_dashboard(step, course):
course_link_css = 'section.my-courses a[href*="%s"]' % course
assert world.browser.is_element_present_by_css(course_link_css)
assert world.is_css_present(course_link_css)
@step(u'I press the "([^"]*)" button in the Unenroll dialog')
def i_press_the_button_in_the_unenroll_dialog(step, value):
button_css = 'section#unenroll-modal input[value="%s"]' % value
world.browser.find_by_css(button_css).click()
assert world.browser.is_element_present_by_css('section.container.dashboard')
world.css_click(button_css)
assert world.is_css_present('section.container.dashboard')
from lettuce import world, step
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
......@@ -22,4 +24,4 @@ def i_check_checkbox(step, checkbox):
@step('I should see "([^"]*)" in the dashboard banner$')
def i_should_see_text_in_the_dashboard_banner_section(step, text):
css_selector = "section.dashboard-banner h2"
assert (text in world.browser.find_by_css(css_selector).text)
assert (text in world.css_text(css_selector))
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from re import sub
from nose.tools import assert_equals
from xmodule.modulestore.django import modulestore
from courses import *
from common import *
from logging import getLogger
logger = getLogger(__name__)
......@@ -32,20 +35,20 @@ def i_verify_all_the_content_of_each_course(step):
pass
for test_course in registered_courses:
test_course.find_by_css('a').click()
test_course.css_click('a')
check_for_errors()
# Get the course. E.g. 'MITx/6.002x/2012_Fall'
current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url))
validate_course(current_course, ids)
world.browser.find_link_by_text('Courseware').click()
assert world.browser.is_element_present_by_id('accordion', wait_time=2)
world.click_link('Courseware')
assert world.is_css_present('accordion')
check_for_errors()
browse_course(current_course)
# clicking the user link gets you back to the user's home page
world.browser.find_by_css('.user-link').click()
world.css_click('.user-link')
check_for_errors()
......@@ -94,7 +97,7 @@ def browse_course(course_id):
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
## sometimes the course-content takes a long time to load
assert world.browser.is_element_present_by_css('.course-content', wait_time=5)
assert world.is_css_present('.course-content')
## look for server error div
check_for_errors()
......
#pylint: disable=C0111
#pylint: disable=W0621
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
from lettuce import before, after, world
from django.conf import settings
......
from django.test import TestCase
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from student.models import Registration, UserProfile
import json
class LoginTest(TestCase):
'''
Test student.views.login_user() view
'''
def setUp(self):
# Create one user and save it to the database
self.user = User.objects.create_user('test', 'test@edx.org', 'test_password')
self.user.is_active = True
self.user.save()
# Create a registration for the user
Registration().register(self.user)
# Create a profile for the user
UserProfile(user=self.user).save()
# Create the test client
self.client = Client()
# Store the login url
self.url = reverse('login')
def test_login_success(self):
response = self._login_response('test@edx.org', 'test_password')
self._assert_response(response, success=True)
def test_login_success_unicode_email(self):
unicode_email = u'test@edx.org' + unichr(40960)
self.user.email = unicode_email
self.user.save()
response = self._login_response(unicode_email, 'test_password')
self._assert_response(response, success=True)
def test_login_fail_no_user_exists(self):
response = self._login_response('not_a_user@edx.org', 'test_password')
self._assert_response(response, success=False,
value='Email or password is incorrect')
def test_login_fail_wrong_password(self):
response = self._login_response('test@edx.org', 'wrong_password')
self._assert_response(response, success=False,
value='Email or password is incorrect')
def test_login_not_activated(self):
# De-activate the user
self.user.is_active = False
self.user.save()
# Should now be unable to login
response = self._login_response('test@edx.org', 'test_password')
self._assert_response(response, success=False,
value="This account has not been activated")
def test_login_unicode_email(self):
unicode_email = u'test@edx.org' + unichr(40960)
response = self._login_response(unicode_email, 'test_password')
self._assert_response(response, success=False)
def test_login_unicode_password(self):
unicode_password = u'test_password' + unichr(1972)
response = self._login_response('test@edx.org', unicode_password)
self._assert_response(response, success=False)
def _login_response(self, email, password):
post_params = {'email': email, 'password': password}
return self.client.post(self.url, post_params)
def _assert_response(self, response, success=None, value=None):
'''
Assert that the response had status 200 and returned a valid
JSON-parseable dict.
If success is provided, assert that the response had that
value for 'success' in the JSON dict.
If value is provided, assert that the response contained that
value for 'value' in the JSON dict.
'''
self.assertEqual(response.status_code, 200)
try:
response_dict = json.loads(response.content)
except ValueError:
self.fail("Could not parse response content as JSON: %s"
% str(response.content))
if success is not None:
self.assertEqual(response_dict['success'], success)
if value is not None:
msg = ("'%s' did not contain '%s'" %
(str(response_dict['value']), str(value)))
self.assertTrue(value in response_dict['value'], msg)
......@@ -2,16 +2,16 @@ from mock import MagicMock
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.core.urlresolvers import reverse
from django.test.utils import override_settings
from xmodule.modulestore.exceptions import ItemNotFoundError
import courseware.module_render as render
from xmodule.modulestore.django import modulestore
from courseware.tests.tests import PageLoader
import courseware.module_render as render
from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.model_data import ModelDataCache
from .factories import UserFactory
......@@ -38,7 +38,7 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class ModuleRenderTestCase(PageLoader):
class ModuleRenderTestCase(LoginEnrollmentTestCase):
def setUp(self):
self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
self.course_id = 'edX/toy/2012_Fall'
......@@ -54,10 +54,9 @@ class ModuleRenderTestCase(PageLoader):
mock_request = MagicMock()
mock_request.FILES.keys.return_value = ['file_id']
mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location,
'dummy').content,
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
settings.MAX_FILEUPLOADS_PER_INPUT}))
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, 'dummy').content,
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
settings.MAX_FILEUPLOADS_PER_INPUT}))
mock_request_2 = MagicMock()
mock_request_2.FILES.keys.return_value = ['file_id']
inputfile = Stub()
......@@ -68,7 +67,7 @@ class ModuleRenderTestCase(PageLoader):
self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location,
'dummy').content,
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
mock_request_3 = MagicMock()
mock_request_3.POST.copy.return_value = {}
mock_request_3.FILES = False
......@@ -79,10 +78,10 @@ class ModuleRenderTestCase(PageLoader):
self.assertRaises(ItemNotFoundError, render.modx_dispatch,
mock_request_3, 'dummy', self.location, 'toy')
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy',
self.location, self.course_id)
self.location, self.course_id)
mock_request_3.POST.copy.return_value = {'position': 1}
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
self.location, self.course_id), HttpResponse)
self.location, self.course_id), HttpResponse)
def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
......@@ -124,19 +123,19 @@ class TestTOC(TestCase):
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': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
self.assertEqual(expected, actual)
......@@ -151,19 +150,19 @@ class TestTOC(TestCase):
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': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
self.assertEqual(expected, actual)
import json
import logging
import xml.sax.saxutils as saxutils
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.http import HttpResponse, Http404
from django.utils import simplejson
from django.http import Http404
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from course_groups.cohorts import is_course_cohorted, get_cohort_id, is_commentable_cohorted, get_cohorted_commentables, get_cohort, get_course_cohorts, get_cohort_by_id
from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted,
get_cohorted_commentables, get_course_cohorts, get_cohort_by_id)
from courseware.access import has_access
from urllib import urlencode
from operator import methodcaller
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from django_comment_client.utils import (merge_dict, extract, strip_none,
strip_blank, get_courseware_context)
from django_comment_client.permissions import cached_has_permission
from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
import django_comment_client.utils as utils
import comment_client as cc
import xml.sax.saxutils as saxutils
THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20
......@@ -31,6 +25,7 @@ escapedict = {'"': '&quot;'}
log = logging.getLogger("edx.discussions")
@login_required
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
"""
This may raise cc.utils.CommentClientError or
......@@ -60,7 +55,6 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
cc_user.default_sort_key = request.GET.get('sort_key')
cc_user.save()
#there are 2 dimensions to consider when executing a search with respect to group id
#is user a moderator
#did the user request a group
......@@ -91,18 +85,17 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
#now add the group name if the thread has a group id
for thread in threads:
if thread.get('group_id'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
else:
thread['group_name'] = ""
thread['group_string'] = "This post visible to everyone."
#patch for backward compatibility to comments service
if not 'pinned' in thread:
thread['pinned'] = False
query_params['page'] = page
query_params['num_pages'] = num_pages
......@@ -110,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
return threads, query_params
@login_required
def inline_discussion(request, course_id, discussion_id):
"""
Renders JSON for DiscussionModules
......@@ -142,14 +136,14 @@ def inline_discussion(request, course_id, discussion_id):
cohorts_list = list()
if is_cohorted:
cohorts_list.append({'name':'All Groups','id':None})
cohorts_list.append({'name': 'All Groups', 'id': None})
#if you're a mod, send all cohorts and let you pick
if is_moderator:
cohorts = get_course_cohorts(course_id)
for c in cohorts:
cohorts_list.append({'name':c.name, 'id':c.id})
cohorts_list.append({'name': c.name, 'id': c.id})
else:
#students don't get to choose
......@@ -216,9 +210,6 @@ def forum_form_discussion(request, course_id):
user_cohort_id = get_cohort_id(request.user, course_id)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
......@@ -242,6 +233,7 @@ def forum_form_discussion(request, course_id):
return render_to_response('discussion/index.html', context)
@login_required
def single_thread(request, course_id, discussion_id, thread_id):
course = get_course_with_access(request.user, course_id, 'load')
......@@ -250,11 +242,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
#patch for backward compatibility with comments service
if not 'pinned' in thread.attributes:
thread['pinned'] = False
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404
......@@ -352,7 +344,7 @@ def user_profile(request, course_id, user_id):
query_params = {
'page': request.GET.get('page', 1),
'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities
}
}
threads, page, num_pages = profiled_user.active_threads(query_params)
query_params['page'] = page
......@@ -369,8 +361,6 @@ def user_profile(request, course_id, user_id):
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
})
else:
context = {
'course': course,
'user': request.user,
......@@ -426,5 +416,5 @@ def followed_threads(request, course_id, user_id):
}
return render_to_response('discussion/user_profile.html', context)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
raise Http404
from django.contrib.auth.models import User, Group
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from mock import Mock
from django.test.utils import override_settings
import xmodule.modulestore.django
from student.models import CourseEnrollment
from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save
from django.dispatch.dispatcher import _make_id
import string
import random
from .permissions import has_permission
from .models import Role, Permission
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore
import comment_client
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
#class TestCohorting(PageLoader):
# """Check that cohorting works properly"""
#
# def setUp(self):
# xmodule.modulestore.django._MODULESTORES = {}
#
# # Assume courses are there
# self.toy = modulestore().get_course("edX/toy/2012_Fall")
#
# # Create two accounts
# self.student = 'view@test.com'
# self.student2 = 'view2@test.com'
# self.password = 'foo'
# self.create_account('u1', self.student, self.password)
# self.create_account('u2', self.student2, self.password)
# self.activate_user(self.student)
# self.activate_user(self.student2)
#
# def test_create_thread(self):
# my_save = Mock()
# comment_client.perform_request = my_save
#
# resp = self.client.post(
# reverse('django_comment_client.base.views.create_thread',
# kwargs={'course_id': 'edX/toy/2012_Fall',
# 'commentable_id': 'General'}),
# {'some': "some",
# 'data': 'data'})
# self.assertTrue(my_save.called)
#
# #self.assertEqual(resp.status_code, 200)
# #self.assertEqual(my_save.something, "expected", "complaint if not true")
#
# self.toy.cohort_config = {"cohorted": True}
#
# # call the view again ...
#
# # assert that different things happened
from django.contrib.auth.models import User
from django.test import TestCase
from student.models import CourseEnrollment
from django_comment_client.permissions import has_permission
from django_comment_client.models import Role
class PermissionsTestCase(TestCase):
......
......@@ -8,13 +8,6 @@ Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
"""
import courseware.tests.tests as ct
import json
from nose import SkipTest
from mock import patch, Mock
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
......@@ -26,13 +19,13 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
from django_comment_client.utils import has_forum_access
from courseware.access import _course_staff_group_name
import courseware.tests.tests as ct
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
'''
Check for download of csv
'''
......@@ -55,7 +48,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(ct.user(self.instructor))
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
......@@ -63,7 +56,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_download_grades_csv(self):
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
......@@ -101,9 +93,8 @@ def action_name(operation, rolename):
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardForumAdmin(ct.PageLoader):
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
'''
Check for change in forum admin role memberships
'''
......@@ -112,7 +103,6 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
self.course_id = "edX/toy/2012_Fall"
self.toy = modulestore().get_course(self.course_id)
......@@ -127,14 +117,12 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(ct.user(self.instructor))
g.user_set.add(get_user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def initialize_roles(self, course_id):
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
......
"""Tests for License package"""
import logging
import json
from uuid import uuid4
from random import shuffle
from tempfile import NamedTemporaryFile
from factory import Factory, SubFactory
from django.test import TestCase
from django.core.management import call_command
from .models import CourseSoftware, UserLicense
from django.core.urlresolvers import reverse
from licenses.models import CourseSoftware, UserLicense
from courseware.tests.tests import LoginEnrollmentTestCase, get_user
COURSE_1 = 'edX/toy/2012_Fall'
SOFTWARE_1 = 'matlab'
SOFTWARE_2 = 'stata'
SERIAL_1 = '123456abcde'
log = logging.getLogger(__name__)
class CourseSoftwareFactory(Factory):
'''Factory for generating CourseSoftware objects in database'''
FACTORY_FOR = CourseSoftware
name = SOFTWARE_1
full_name = SOFTWARE_1
url = SOFTWARE_1
course_id = COURSE_1
class UserLicenseFactory(Factory):
'''
Factory for generating UserLicense objects in database
By default, the user assigned is null, indicating that the
serial number has not yet been assigned.
'''
FACTORY_FOR = UserLicense
software = SubFactory(CourseSoftwareFactory)
serial = SERIAL_1
class LicenseTestCase(LoginEnrollmentTestCase):
'''Tests for licenses.views'''
def setUp(self):
'''creates a user and logs in'''
self.setup_viewtest_user()
self.software = CourseSoftwareFactory()
def test_get_license(self):
UserLicenseFactory(user=get_user(self.viewtest_email), software=self.software)
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('error' in json_returned)
self.assertTrue('serial' in json_returned)
self.assertEquals(json_returned['serial'], SERIAL_1)
def test_get_nonexistent_license(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('serial' in json_returned)
self.assertTrue('error' in json_returned)
def test_create_nonexistent_license(self):
'''Should not assign a license to an unlicensed user when none are available'''
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'true'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('serial' in json_returned)
self.assertTrue('error' in json_returned)
def test_create_license(self):
'''Should assign a license to an unlicensed user if one is unassigned'''
# create an unassigned license
UserLicenseFactory(software=self.software)
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'true'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('error' in json_returned)
self.assertTrue('serial' in json_returned)
self.assertEquals(json_returned['serial'], SERIAL_1)
def test_get_license_from_wrong_course(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format('some/other/course'))
self.assertEqual(404, response.status_code)
def test_get_license_from_non_ajax(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(404, response.status_code)
def test_get_license_without_software(self):
response = self.client.post(reverse('user_software_license'),
{'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(404, response.status_code)
def test_get_license_without_login(self):
self.logout()
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
# if we're not logged in, we should be referred to the login page
self.assertEqual(302, response.status_code)
class CommandTest(TestCase):
'''Test management command for importing serial numbers'''
def test_import_serial_numbers(self):
size = 20
......@@ -51,31 +167,33 @@ class CommandTest(TestCase):
licenses_count = UserLicense.objects.all().count()
self.assertEqual(3 * size, licenses_count)
cs = CourseSoftware.objects.get(pk=1)
software = CourseSoftware.objects.get(pk=1)
lics = UserLicense.objects.filter(software=cs)[:size]
lics = UserLicense.objects.filter(software=software)[:size]
known_serials = list(l.serial for l in lics)
known_serials.extend(generate_serials(10))
shuffle(known_serials)
log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
with NamedTemporaryFile() as f:
f.write('\n'.join(known_serials))
f.flush()
args = [COURSE_1, SOFTWARE_1, f.name]
with NamedTemporaryFile() as tmpfile:
tmpfile.write('\n'.join(known_serials))
tmpfile.flush()
args = [COURSE_1, SOFTWARE_1, tmpfile.name]
call_command('import_serial_numbers', *args)
log.debug('Check if we added only the new ones')
licenses_count = UserLicense.objects.filter(software=cs).count()
licenses_count = UserLicense.objects.filter(software=software).count()
self.assertEqual((2 * size) + 10, licenses_count)
def generate_serials(size=20):
'''generate a list of serial numbers'''
return [str(uuid4()) for _ in range(size)]
def generate_serials_file(size=20):
'''output list of generated serial numbers to a temp file'''
serials = generate_serials(size)
temp_file = NamedTemporaryFile()
......
......@@ -7,12 +7,13 @@ from collections import namedtuple, defaultdict
from mitxmako.shortcuts import render_to_string
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import HttpResponse, Http404
from django.views.decorators.csrf import requires_csrf_token, csrf_protect
from django.views.decorators.csrf import requires_csrf_token
from .models import CourseSoftware
from .models import get_courses_licenses, get_or_create_license, get_license
from licenses.models import CourseSoftware
from licenses.models import get_courses_licenses, get_or_create_license, get_license
log = logging.getLogger("mitx.licenses")
......@@ -44,6 +45,7 @@ def get_licenses_by_course(user, courses):
return data_by_course
@login_required
@requires_csrf_token
def user_software_license(request):
if request.method != 'POST' or not request.is_ajax():
......@@ -65,19 +67,21 @@ def user_software_license(request):
try:
software = CourseSoftware.objects.get(name=software_name,
course_id=course_id)
print software
except CourseSoftware.DoesNotExist:
raise Http404
user = User.objects.get(id=user_id)
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise Http404
if generate:
license = get_or_create_license(user, software)
software_license = get_or_create_license(user, software)
else:
license = get_license(user, software)
software_license = get_license(user, software)
if license:
response = {'serial': license.serial}
if software_license:
response = {'serial': software_license.serial}
else:
response = {'error': 'No serial number found'}
......
......@@ -4,22 +4,22 @@ Tests for open ended grading interfaces
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading
"""
from django.test import TestCase
from open_ended_grading import staff_grading_service
from xmodule.open_ended_grading_classes import peer_grading_service
from xmodule import peer_grading_module
import json
from mock import MagicMock
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group
from mitxmako.shortcuts import render_to_string
from courseware.access import _course_staff_group_name
import courseware.tests.tests as ct
from xmodule.open_ended_grading_classes import peer_grading_service
from xmodule import peer_grading_module
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from nose import SkipTest
from mock import patch, Mock, MagicMock
import json
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
from open_ended_grading import staff_grading_service
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
import logging
......@@ -30,8 +30,8 @@ from django.http import QueryDict
from xmodule.tests import test_util_open_ended
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(ct.PageLoader):
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(LoginEnrollmentTestCase):
'''
Check that staff grading service proxy works. Basically just checking the
access control and error handling logic -- all the actual work is on the
......@@ -56,7 +56,7 @@ class TestStaffGradingService(ct.PageLoader):
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(ct.user(self.instructor))
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
......@@ -126,8 +126,8 @@ class TestStaffGradingService(ct.PageLoader):
self.assertIsNotNone(d['problem_list'])
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestPeerGradingService(ct.PageLoader):
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestPeerGradingService(LoginEnrollmentTestCase):
'''
Check that staff grading service proxy works. Basically just checking the
access control and error handling logic -- all the actual work is on the
......
......@@ -364,6 +364,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django_comment_client.middleware.AjaxExceptionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......
......@@ -74,6 +74,15 @@ def to_latex(x):
# LatexPrinter._print_dot = _print_dot
xs = latex(x)
xs = xs.replace(r'\XI', 'XI') # workaround for strange greek
# substitute back into latex form for scripts
# literally something of the form
# 'scriptN' becomes '\\mathcal{N}'
# note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms
xs = re.sub(r'script([a-zA-Z0-9]+)',
'\\mathcal{\\1}',
xs)
#return '<math>%s{}{}</math>' % (xs[1:-1])
if xs[0] == '$':
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6
......@@ -106,6 +115,7 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
'N': sympy.Symbol('N'), # or it is some kind of sympy function
#'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
......@@ -247,6 +257,127 @@ class formula(object):
fix_hat(k)
fix_hat(xml)
def flatten_pmathml(xml):
''' Give the text version of certain PMathML elements
Sometimes MathML will be given with each letter separated (it
doesn't know if its implicit multiplication or what). From an xml
node, find the (text only) variable name it represents. So it takes
<mrow>
<mi>m</mi>
<mi>a</mi>
<mi>x</mi>
</mrow>
and returns 'max', for easier use later on.
'''
tag = gettag(xml)
if tag == 'mn': return xml.text
elif tag == 'mi': return xml.text
elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml])
raise Exception, '[flatten_pmathml] unknown tag %s' % tag
def fix_mathvariant(parent):
'''Fix certain kinds of math variants
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
with 'scriptN'. There have been problems using script_N or script(N)
'''
for child in parent:
if (gettag(child) == 'mstyle' and child.get('mathvariant') == 'script'):
newchild = etree.Element('mi')
newchild.text = 'script%s' % flatten_pmathml(child[0])
parent.replace(child, newchild)
fix_mathvariant(child)
fix_mathvariant(xml)
# find "tagged" superscripts
# they have the character \u200b in the superscript
# replace them with a__b so snuggle doesn't get confused
def fix_superscripts(xml):
''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
In the javascript, variables with '__X' in them had an invisible
character inserted into the sup (to distinguish from powers)
E.g. normal:
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mi>c</mi>
</msubsup>
to be interpreted '(a_b)^c' (nothing done by this method)
And modified:
<msubsup>
<mi>b</mi>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>d</mi>
</mrow>
</msubsup>
to be interpreted 'a_b__c'
also:
<msup>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>B</mi>
</mrow>
</msup>
to be 'x__B'
'''
for k in xml:
tag = gettag(k)
# match things like the last example--
# the second item in msub is an mrow with the first
# character equal to \u200b
if (tag == 'msup' and
len(k) == 2 and gettag(k[1]) == 'mrow' and
gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew
# replace the msup with 'X__Y'
k[1].remove(k[1][0])
newk = etree.Element('mi')
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
xml.replace(k, newk)
# match things like the middle example-
# the third item in msubsup is an mrow with the first
# character equal to \u200b
if (tag == 'msubsup' and
len(k) == 3 and gettag(k[2]) == 'mrow' and
gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew
# replace the msubsup with 'X_Y__Z'
k[2].remove(k[2][0])
newk = etree.Element('mi')
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
xml.replace(k, newk)
fix_superscripts(k)
fix_superscripts(xml)
# Snuggle returns an error when it sees an <msubsup>
# replace such elements with an <msup>, except the first element is of
# the form a_b. I.e. map a_b^c => (a_b)^c
def fix_msubsup(parent):
for child in parent:
# fix msubsup
if (gettag(child) == 'msubsup' and len(child) == 3):
newchild = etree.Element('msup')
newbase = etree.Element('mi')
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
newexp = child[2]
newchild.append(newbase)
newchild.append(newexp)
parent.replace(child, newchild)
fix_msubsup(child)
fix_msubsup(xml)
self.xml = xml
return self.xml
......@@ -257,6 +388,7 @@ class formula(object):
try:
xml = self.preprocess_pmathml(self.expr)
except Exception, err:
log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr))
return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml, pretty_print=True)
self.the_pmathml = pmathml
......
"""
Tests of symbolic math
"""
import unittest
import formula
import re
from lxml import etree
def stripXML(xml):
xml = xml.replace('\n', '')
xml = re.sub(r'\> +\<', '><', xml)
return xml
class FormulaTest(unittest.TestCase):
# for readability later
mathml_start = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mstyle displaystyle="true">'
mathml_end = '</mstyle></math>'
def setUp(self):
self.formulaInstance = formula.formula('')
def test_replace_mathvariants(self):
expr = '''
<mstyle mathvariant="script">
<mi>N</mi>
</mstyle>'''
expected = '<mi>scriptN</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
self.assertEqual(test, expected)
def test_fix_simple_superscripts(self):
expr = '''
<msup>
<mi>a</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>b</mi>
</mrow>
</msup>'''
expected = '<mi>a__b</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
self.assertEqual(test, expected)
def test_fix_complex_superscripts(self):
expr = '''
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>c</mi>
</mrow>
</msubsup>'''
expected = '<mi>a_b__c</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
self.assertEqual(test, expected)
def test_fix_msubsup(self):
expr = '''
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mi>c</mi>
</msubsup>'''
expected = '<msup><mi>a_b</mi><mi>c</mi></msup>' # which is (a_b)^c
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
self.assertEqual(test, expected)
......@@ -2,13 +2,15 @@ import logging
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from request_cache.middleware import RequestCache
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.metadata_inheritance_cache = cache
store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
......
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