Merge branch 'master' into feature/markchang/studio-nps
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#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")
|
||||
press_the_notification_button(step, "Save")
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#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()
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#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
|
||||
@@ -15,14 +13,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$')
|
||||
@@ -43,12 +42,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()
|
||||
|
||||
@@ -74,80 +73,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(
|
||||
@@ -155,21 +87,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)
|
||||
|
||||
# click the signin button
|
||||
css_click(signin_css)
|
||||
create_studio_user(uname=uname, email=email, is_staff=is_staff)
|
||||
|
||||
world.browser.cookies.delete()
|
||||
world.visit('/')
|
||||
|
||||
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():
|
||||
@@ -184,26 +117,26 @@ 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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#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
|
||||
And I see a link for adding a new section
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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')
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
@@ -10,7 +13,7 @@ import time
|
||||
@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$')
|
||||
@@ -31,19 +34,19 @@ 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$')
|
||||
def i_save_a_new_section_release_date(step):
|
||||
date_css = 'input.start-date.date.hasDatepicker'
|
||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||
css_fill(date_css, '12/25/2013')
|
||||
world.css_fill(date_css, '12/25/2013')
|
||||
# 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, '12:00am')
|
||||
e = css_find(time_css).first
|
||||
world.css_fill(time_css, '12:00am')
|
||||
e = world.css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
world.browser.click_link_by_text('Save')
|
||||
@@ -64,13 +67,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"')
|
||||
|
||||
|
||||
@@ -85,7 +88,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
|
||||
@@ -99,20 +102,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 ###################
|
||||
@@ -120,10 +123,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)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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')
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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)
|
||||
|
||||
@@ -17,6 +17,14 @@ Feature: Create Subsection
|
||||
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
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
Given I have opened a new course section in Studio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
@@ -7,7 +10,7 @@ from nose.tools import assert_equal
|
||||
|
||||
@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()
|
||||
@@ -15,8 +18,7 @@ def i_have_opened_a_new_course_section(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$')
|
||||
@@ -31,14 +33,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 added a new subsection$')
|
||||
@@ -46,6 +48,17 @@ def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
|
||||
|
||||
@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 ###################
|
||||
|
||||
|
||||
@@ -70,11 +83,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',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
<div class="item-actions">
|
||||
|
||||
0
common/djangoapps/request_cache/__init__.py
Normal file
0
common/djangoapps/request_cache/__init__.py
Normal file
20
common/djangoapps/request_cache/middleware.py
Normal file
20
common/djangoapps/request_cache/middleware.py
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
140
common/djangoapps/terrain/course_helpers.py
Normal file
140
common/djangoapps/terrain/course_helpers.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#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()
|
||||
@@ -1,20 +1,12 @@
|
||||
#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(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.css_text('body'))
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
def i_am_an_edx_user(step):
|
||||
create_user('robot')
|
||||
|
||||
#### helper functions
|
||||
world.create_user('robot')
|
||||
|
||||
|
||||
@world.absorb
|
||||
def scroll_to_bottom():
|
||||
# Maximize the browser
|
||||
world.browser.execute_script("window.scrollTo(0, screen.height);")
|
||||
|
||||
|
||||
@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_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)
|
||||
|
||||
117
common/djangoapps/terrain/ui_helpers.py
Normal file
117
common/djangoapps/terrain/ui_helpers.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#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
|
||||
'''
|
||||
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]
|
||||
|
||||
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!')
|
||||
# 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.')
|
||||
|
||||
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 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)
|
||||
|
||||
if to_cache and self.metadata_inheritance_cache is not None:
|
||||
self.metadata_inheritance_cache.set_many(to_cache)
|
||||
# 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 trees
|
||||
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()))
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
from lettuce import world
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule import seq_module, vertical_module
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
## support functions
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def process_section(element, num_tabs=0):
|
||||
'''
|
||||
Process section reads through whatever is in 'course-content' and classifies it according to sequence module type.
|
||||
|
||||
This function is recursive
|
||||
|
||||
There are 6 types, with 6 actions.
|
||||
|
||||
Sequence Module
|
||||
-contains one child module
|
||||
|
||||
Vertical Module
|
||||
-contains other modules
|
||||
-process it and get its children, then process them
|
||||
|
||||
Capa Module
|
||||
-problem type, contains only one problem
|
||||
-for this, the most complex type, we created a separate method, process_problem
|
||||
|
||||
Video Module
|
||||
-video type, contains only one video
|
||||
-we only check to ensure that a section with class of video exists
|
||||
|
||||
HTML Module
|
||||
-html text
|
||||
-we do not check anything about it
|
||||
|
||||
Custom Tag Module
|
||||
-a custom 'hack' module type
|
||||
-there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type
|
||||
|
||||
can be used like this:
|
||||
e = world.browser.find_by_css('section.course-content section')
|
||||
process_section(e)
|
||||
|
||||
'''
|
||||
if element.has_class('xmodule_display xmodule_SequenceModule'):
|
||||
logger.debug('####### Processing xmodule_SequenceModule')
|
||||
child_modules = element.find_by_css("div>div>section[class^='xmodule']")
|
||||
for mod in child_modules:
|
||||
process_section(mod)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_VerticalModule'):
|
||||
logger.debug('####### Processing xmodule_VerticalModule')
|
||||
vert_list = element.find_by_css("li section[class^='xmodule']")
|
||||
for item in vert_list:
|
||||
process_section(item)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_CapaModule'):
|
||||
logger.debug('####### Processing xmodule_CapaModule')
|
||||
assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module"
|
||||
p = element.find_by_css("section[id^='problem']").first
|
||||
p_id = p['id']
|
||||
logger.debug('####################')
|
||||
logger.debug('id is "%s"' % p_id)
|
||||
logger.debug('####################')
|
||||
process_problem(p, p_id)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_VideoModule'):
|
||||
logger.debug('####### Processing xmodule_VideoModule')
|
||||
assert element.find_by_css("section[class^='video']"), "No video found in Video Module"
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_HtmlModule'):
|
||||
logger.debug('####### Processing xmodule_HtmlModule')
|
||||
pass
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_CustomTagModule'):
|
||||
logger.debug('####### Processing xmodule_CustomTagModule')
|
||||
pass
|
||||
|
||||
else:
|
||||
assert False, "Class for element not recognized!!"
|
||||
|
||||
|
||||
def process_problem(element, problem_id):
|
||||
'''
|
||||
Process problem attempts to
|
||||
1) scan all the input fields and reset them
|
||||
2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect')
|
||||
3) click the 'show answer' button IF it exists and IF the answer is not already displayed
|
||||
4) enter the correct answer in each input box
|
||||
5) click the 'check' button and verify that answers are correct
|
||||
|
||||
Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM.
|
||||
The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective.
|
||||
'''
|
||||
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
|
||||
## clear out all input to ensure an incorrect result
|
||||
for field in input_fields:
|
||||
field.find_by_css("input").first.fill('')
|
||||
|
||||
## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect'
|
||||
# This would need to be reworked because multiple choice problems don't have this status
|
||||
# if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect':
|
||||
prob_xmod.find_by_css("section.action input.check").first.click()
|
||||
|
||||
## all elements become disconnected after the click
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
# Wait for the ajax reload
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
for field in input_fields:
|
||||
assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id)
|
||||
|
||||
show_button = element.find_by_css("section.action input.show").first
|
||||
## this logic is to ensure we do not accidentally hide the answers
|
||||
if show_button.value.lower() == 'show answer':
|
||||
show_button.click()
|
||||
else:
|
||||
pass
|
||||
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
|
||||
## in each field, find the answer, and send it to the field.
|
||||
## Note that this does not work if the answer type is a strange format, e.g. "either a or b"
|
||||
for field in input_fields:
|
||||
field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text)
|
||||
|
||||
prob_xmod.find_by_css("section.action input.check").first.click()
|
||||
|
||||
## assert that we entered the correct answers
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
for field in input_fields:
|
||||
## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space)
|
||||
assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
#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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#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()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#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
|
||||
|
||||
@@ -522,6 +522,12 @@ def static_university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id that does not have any courses.
|
||||
"""
|
||||
# Redirect to the properly capitalized org_id
|
||||
last_path = request.path.split('/')[-1]
|
||||
if last_path != org_id:
|
||||
return redirect('static_university_profile', org_id=org_id)
|
||||
|
||||
# Render template
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
context = dict(courses=[], org_id=org_id)
|
||||
return render_to_response(template_file, context)
|
||||
@@ -533,17 +539,28 @@ def university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id. 404 if it's not valid.
|
||||
"""
|
||||
virtual_orgs_ids = settings.VIRTUAL_UNIVERSITIES
|
||||
meta_orgs = getattr(settings, 'META_UNIVERSITIES', {})
|
||||
|
||||
# Get all the ids associated with this organization
|
||||
all_courses = modulestore().get_courses()
|
||||
valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES)
|
||||
if org_id not in valid_org_ids:
|
||||
valid_orgs_ids = set(c.org for c in all_courses)
|
||||
valid_orgs_ids.update(virtual_orgs_ids + meta_orgs.keys())
|
||||
|
||||
if org_id not in valid_orgs_ids:
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
courses = sort_by_announcement(courses)
|
||||
# Grab all courses for this organization(s)
|
||||
org_ids = set([org_id] + meta_orgs.get(org_id, []))
|
||||
org_courses = []
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
for key in org_ids:
|
||||
cs = get_courses_by_university(request.user, domain=domain)[key]
|
||||
org_courses.extend(cs)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
org_courses = sort_by_announcement(org_courses)
|
||||
|
||||
context = dict(courses=org_courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
return render_to_response(template_file, context)
|
||||
|
||||
@@ -76,6 +76,7 @@ LOGGING = get_logger_config(LOG_DIR,
|
||||
COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
|
||||
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
|
||||
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
|
||||
META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
|
||||
@@ -9,6 +9,7 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
|
||||
SUBDOMAIN_BRANDING['edge'] = 'edge'
|
||||
SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
|
||||
VIRTUAL_UNIVERSITIES = ['edge']
|
||||
META_UNIVERSITIES = {}
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -113,6 +113,9 @@ SUBDOMAIN_BRANDING = {
|
||||
# have an actual course with that org set
|
||||
VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
# Organization that contain other organizations
|
||||
META_UNIVERSITIES = {'UTx': ['UTAustinX']}
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
############################## Course static files ##########################
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
23
lms/templates/university_profile/utaustinx.html
Normal file
23
lms/templates/university_profile/utaustinx.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>UTAustinX</title></%block>
|
||||
|
||||
<%block name="university_header">
|
||||
<header class="search" style="background: url('/static/images/university/utaustin/utaustin-cover_2025x550.jpg')">
|
||||
<div class="inner-wrapper university-search">
|
||||
<hgroup>
|
||||
<div class="logo">
|
||||
<img src="${static.url('images/university/utaustin/utaustin-standalone_187x80.png')}" />
|
||||
</div>
|
||||
<h1>UTAustinx</h1>
|
||||
</hgroup>
|
||||
</div>
|
||||
</header>
|
||||
</%block>
|
||||
|
||||
<%block name="university_description">
|
||||
<p>The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools, and is the first major American university to build a medical school in the past 50 years.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
@@ -1,5 +1,8 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%block name="title"><title>UTx</title></%block>
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
<%block name="university_description">
|
||||
<p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p>
|
||||
<p>Find out about the <a href="${reverse('university_profile', args=['UTAustinX'])}">University of Texas Austin</a>.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
38
lms/urls.py
38
lms/urls.py
@@ -69,44 +69,22 @@ urlpatterns = ('',
|
||||
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'UTx'}),
|
||||
url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'WellesleyX'}),
|
||||
url(r'^university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'GeorgetownX'}),
|
||||
|
||||
# Dan accidentally sent out a press release with lower case urls for McGill, Toronto,
|
||||
# Rice, ANU, Delft, and EPFL. Hence the redirects.
|
||||
url(r'^university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'McGillX'}),
|
||||
url(r'^university_profile/mcgillx$',
|
||||
RedirectView.as_view(url='/university_profile/McGillX')),
|
||||
|
||||
url(r'^university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'TorontoX'}),
|
||||
url(r'^university_profile/torontox$',
|
||||
RedirectView.as_view(url='/university_profile/TorontoX')),
|
||||
|
||||
url(r'^university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'RiceX'}),
|
||||
url(r'^university_profile/ricex$',
|
||||
RedirectView.as_view(url='/university_profile/RiceX')),
|
||||
|
||||
url(r'^university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'ANUx'}),
|
||||
url(r'^university_profile/anux$',
|
||||
RedirectView.as_view(url='/university_profile/ANUx')),
|
||||
|
||||
url(r'^university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'DelftX'}),
|
||||
url(r'^university_profile/delftx$',
|
||||
RedirectView.as_view(url='/university_profile/DelftX')),
|
||||
|
||||
url(r'^university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'EPFLx'}),
|
||||
url(r'^university_profile/epflx$',
|
||||
RedirectView.as_view(url='/university_profile/EPFLx')),
|
||||
|
||||
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile',
|
||||
name="university_profile"),
|
||||
|
||||
Reference in New Issue
Block a user